재생목록 만들기 페이지 추가
This commit is contained in:
		
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_check_blue.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/ic_check_blue.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/ic_check_blue.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 631 B | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_playlist_add.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_playlist_add.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_playlist_add.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_playlist_add.imageset/ic_playlist_add.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_playlist_add.imageset/ic_playlist_add.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 962 B | 
| @@ -11,44 +11,67 @@ struct ContentBoxView: View { | ||||
|      | ||||
|     @StateObject var viewModel = ContentBoxViewModel() | ||||
|      | ||||
|     @State private var isShowCreatePlaylist = false | ||||
|     @State private var isReloadData = false | ||||
|      | ||||
|     var body: some View { | ||||
|         NavigationView { | ||||
|             VStack(spacing: 13.3) { | ||||
|                 DetailNavigationBar(title: "내 보관함") | ||||
|                  | ||||
|                 ScrollView(.horizontal, showsIndicators: false) { | ||||
|                     HStack(spacing: 12) { | ||||
|                         ContentBoxTabView( | ||||
|                             title: "구매목록", | ||||
|                             isSelected: viewModel.currentTab == .orderlist | ||||
|                         ) | ||||
|                         .onTapGesture { | ||||
|                             if viewModel.currentTab != .orderlist { | ||||
|                                 viewModel.currentTab = .orderlist | ||||
|         ZStack { | ||||
|             NavigationView { | ||||
|                 VStack(spacing: 13.3) { | ||||
|                     DetailNavigationBar(title: "내 보관함") | ||||
|                      | ||||
|                     ScrollView(.horizontal, showsIndicators: false) { | ||||
|                         HStack(spacing: 12) { | ||||
|                             ContentBoxTabView( | ||||
|                                 title: "구매목록", | ||||
|                                 isSelected: viewModel.currentTab == .orderlist | ||||
|                             ) | ||||
|                             .onTapGesture { | ||||
|                                 if viewModel.currentTab != .orderlist { | ||||
|                                     viewModel.currentTab = .orderlist | ||||
|                                 } | ||||
|                             } | ||||
|                              | ||||
|                             ContentBoxTabView( | ||||
|                                 title: "재생목록", | ||||
|                                 isSelected: viewModel.currentTab == .playlist | ||||
|                             ) | ||||
|                             .onTapGesture { | ||||
|                                 if viewModel.currentTab != .playlist { | ||||
|                                     viewModel.currentTab = .playlist | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                          | ||||
|                         ContentBoxTabView( | ||||
|                             title: "재생목록", | ||||
|                             isSelected: viewModel.currentTab == .playlist | ||||
|                         ) | ||||
|                         .onTapGesture { | ||||
|                             if viewModel.currentTab != .playlist { | ||||
|                                 viewModel.currentTab = .playlist | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                 } | ||||
|                  | ||||
|                 if viewModel.currentTab == .playlist { | ||||
|                     ContentPlaylistListView() | ||||
|                         .padding(.bottom, 13.3) | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                 } else { | ||||
|                     OrderListAllInnerView() | ||||
|                     } | ||||
|                      | ||||
|                     if viewModel.currentTab == .playlist { | ||||
|                         if isReloadData { | ||||
|                             Color.clear | ||||
|                                 .onAppear { | ||||
|                                     DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { | ||||
|                                         isReloadData = false | ||||
|                                     } | ||||
|                                 } | ||||
|                         } else { | ||||
|                             ContentPlaylistListView( | ||||
|                                 onClickCreate: { isShowCreatePlaylist = true } | ||||
|                             ) | ||||
|                             .padding(.bottom, 13.3) | ||||
|                             .padding(.horizontal, 13.3) | ||||
|                         } | ||||
|                     } else { | ||||
|                         OrderListAllInnerView() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             if isShowCreatePlaylist { | ||||
|                 ContentPlaylistCreateView( | ||||
|                     isShowing: $isShowCreatePlaylist, | ||||
|                     reloadData: $isReloadData | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ enum ContentApi { | ||||
|     case likeContent(request: PutAudioContentLikeRequest) | ||||
|     case registerComment(request: RegisterAudioContentCommentRequest) | ||||
|     case orderAudioContent(request: OrderRequest) | ||||
|     case getOrderList(page: Int, size: Int) | ||||
|     case getOrderList(orderType: OrderType?, page: Int, size: Int) | ||||
|     case addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest) | ||||
|     case getAudioContentThemeList | ||||
|     case uploadAudioContent(parameters: [MultipartFormData]) | ||||
| @@ -180,12 +180,16 @@ extension ContentApi: TargetType { | ||||
|         case .orderAudioContent(let request): | ||||
|             return .requestJSONEncodable(request) | ||||
|              | ||||
|         case .getOrderList(let page, let size): | ||||
|             let parameters = [ | ||||
|         case .getOrderList(let orderType, let page, let size): | ||||
|             var parameters = [ | ||||
|                 "page": page - 1, | ||||
|                 "size": size | ||||
|             ] as [String : Any] | ||||
|              | ||||
|             if let orderType = orderType { | ||||
|                 parameters["orderType"] = orderType | ||||
|             } | ||||
|              | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .addAllPlaybackTracking(let request): | ||||
|   | ||||
| @@ -34,8 +34,8 @@ final class ContentRepository { | ||||
|         return api.requestPublisher(.orderAudioContent(request: OrderRequest(contentId: contentId, orderType: orderType))) | ||||
|     } | ||||
|      | ||||
|     func getOrderList(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getOrderList(page: page, size: size)) | ||||
|     func getOrderList(orderType: OrderType?, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getOrderList(orderType: orderType, page: page, size: size)) | ||||
|     } | ||||
|      | ||||
|     func addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest) -> AnyPublisher<Response, MoyaError> { | ||||
|   | ||||
| @@ -15,4 +15,8 @@ class ContentPlaylistListRepository { | ||||
|     func getPlaylistList() -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getPlaylistList) | ||||
|     } | ||||
|      | ||||
|     func createPlaylist(request: CreatePlaylistRequest) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.createPlaylist(request: request)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,8 @@ struct ContentPlaylistListView: View { | ||||
|      | ||||
|     @ObservedObject var viewModel = ContentPlaylistListViewModel() | ||||
|      | ||||
|     let onClickCreate: () -> Void | ||||
|      | ||||
|     var body: some View { | ||||
|         BaseView(isLoading: $viewModel.isLoading) { | ||||
|             VStack(spacing: 13.3) { | ||||
| @@ -21,7 +23,9 @@ struct ContentPlaylistListView: View { | ||||
|                     .frame(maxWidth: .infinity) | ||||
|                     .background(Color.button) | ||||
|                     .cornerRadius(5.3) | ||||
|                     .contentShape(Rectangle()) | ||||
|                     .onTapGesture { | ||||
|                         onClickCreate() | ||||
|                     } | ||||
|                  | ||||
|                 if viewModel.playlists.isEmpty { | ||||
| @@ -52,13 +56,31 @@ struct ContentPlaylistListView: View { | ||||
|                     } | ||||
|                     .frame(maxWidth: .infinity) | ||||
|                      | ||||
|                     ForEach(0..<viewModel.playlists.count, id: \.self) { index in | ||||
|                         let playlist = viewModel.playlists[index] | ||||
|                          | ||||
|                         ContentPlaylistItemView(item: playlist) | ||||
|                     ScrollView(.vertical) { | ||||
|                         LazyVStack(spacing: 11) { | ||||
|                             ForEach(0..<viewModel.playlists.count, id: \.self) { index in | ||||
|                                 let playlist = viewModel.playlists[index] | ||||
|                                 ContentPlaylistItemView(item: playlist) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { | ||||
|                 HStack { | ||||
|                     Spacer() | ||||
|                     Text(viewModel.errorMessage) | ||||
|                         .padding(.vertical, 13.3) | ||||
|                         .frame(width: screenSize().width - 66.7, alignment: .center) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                         .background(Color.button) | ||||
|                         .foregroundColor(Color.white) | ||||
|                         .multilineTextAlignment(.leading) | ||||
|                         .cornerRadius(20) | ||||
|                         .padding(.bottom, 66.7) | ||||
|                     Spacer() | ||||
|                 } | ||||
|             } | ||||
|             .onAppear { | ||||
|                 viewModel.getPlaylistList() | ||||
|             } | ||||
| @@ -67,5 +89,5 @@ struct ContentPlaylistListView: View { | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentPlaylistListView() | ||||
|     ContentPlaylistListView(onClickCreate: {}) | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,172 @@ | ||||
| // | ||||
| //  ContentPlaylistCreateView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 12/7/24. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentPlaylistCreateView: View { | ||||
|     @StateObject var viewModel = ContentPlaylistCreateViewModel() | ||||
|      | ||||
|     @Binding var isShowing: Bool | ||||
|     @Binding var reloadData: Bool | ||||
|      | ||||
|     @State private var isShowAddContentView = false | ||||
|      | ||||
|     var body: some View { | ||||
|         BaseView(isLoading: $viewModel.isLoading) { | ||||
|             VStack(alignment: .leading, spacing: 0) { | ||||
|                 HStack(spacing: 0) { | ||||
|                     Button { | ||||
|                         isShowing = false | ||||
|                     } label: { | ||||
|                         Image("ic_back") | ||||
|                             .resizable() | ||||
|                             .frame(width: 20, height: 20) | ||||
|                          | ||||
|                         Text("새 재생목록 만들기") | ||||
|                             .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                             .foregroundColor(Color.grayee) | ||||
|                     } | ||||
|                      | ||||
|                     Spacer() | ||||
|                      | ||||
|                     Text("저장") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||
|                         .foregroundColor(Color.grayee) | ||||
|                         .frame(minHeight: 48) | ||||
|                         .onTapGesture { | ||||
|                             viewModel.savePlaylist { | ||||
|                                 reloadData = true | ||||
|                                 isShowing = false | ||||
|                             } | ||||
|                         } | ||||
|                 } | ||||
|                 .frame(height: 50) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|                 .frame(maxWidth: .infinity) | ||||
|                 .background(Color.black) | ||||
|                  | ||||
|                 HStack(spacing: 0) { | ||||
|                     Text("재생목록 제목") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 16.7)) | ||||
|                         .foregroundColor(Color.grayee) | ||||
|                      | ||||
|                     Spacer() | ||||
|                      | ||||
|                     Text("\(viewModel.title.count)/30") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                         .foregroundColor(Color.gray77) | ||||
|                         .onChange(of: viewModel.title) { newValue in | ||||
|                             if newValue.count > 30 { | ||||
|                                 viewModel.title = String(newValue.prefix(30)) | ||||
|                             } | ||||
|                         } | ||||
|                 } | ||||
|                 .padding(.top, 26.7) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|                  | ||||
|                 TextField("", text: $viewModel.title) | ||||
|                     .autocapitalization(.none) | ||||
|                     .disableAutocorrection(true) | ||||
|                     .font(.custom(Font.bold.rawValue, size: 13.3)) | ||||
|                     .foregroundColor(Color.grayee) | ||||
|                     .keyboardType(.webSearch) | ||||
|                     .frame(maxWidth: .infinity) | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                     .padding(.vertical, 17) | ||||
|                     .background(Color.gray22) | ||||
|                     .cornerRadius(6.7) | ||||
|                     .padding(.top, 13.3) | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                  | ||||
|                 HStack(spacing: 0) { | ||||
|                     Text("재생목록 설명을 입력해 주세요") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 16.7)) | ||||
|                         .foregroundColor(Color.grayee) | ||||
|                      | ||||
|                     Spacer() | ||||
|                      | ||||
|                     Text("\(viewModel.desc.count)/40") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                         .foregroundColor(Color.gray77) | ||||
|                         .onChange(of: viewModel.desc) { newValue in | ||||
|                             if newValue.count > 40 { | ||||
|                                 viewModel.desc = String(newValue.prefix(40)) | ||||
|                             } | ||||
|                         } | ||||
|                 } | ||||
|                 .padding(.top, 26.7) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|                  | ||||
|                 TextField("", text: $viewModel.desc) | ||||
|                     .autocapitalization(.none) | ||||
|                     .disableAutocorrection(true) | ||||
|                     .font(.custom(Font.bold.rawValue, size: 13.3)) | ||||
|                     .foregroundColor(Color.grayee) | ||||
|                     .keyboardType(.webSearch) | ||||
|                     .frame(maxWidth: .infinity) | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                     .padding(.vertical, 17) | ||||
|                     .background(Color.gray22) | ||||
|                     .cornerRadius(6.7) | ||||
|                     .padding(.top, 13.3) | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                  | ||||
|                 HStack(spacing: 8) { | ||||
|                     Image("btn_plus_round") | ||||
|                      | ||||
|                     Text("새로운 콘텐츠 추가/제거") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 14.7)) | ||||
|                         .foregroundColor(Color.button) | ||||
|                 } | ||||
|                 .padding(.top, 26.7) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|                 .onTapGesture { | ||||
|                     isShowAddContentView = true | ||||
|                 } | ||||
|                  | ||||
|                 ScrollView(.vertical, showsIndicators: false) { | ||||
|                     LazyVStack(alignment: .leading, spacing: 13.3) { | ||||
|                         ForEach(0..<viewModel.contentList.count, id: \.self) { index in | ||||
|                             PlaylistCreateContentView(content: viewModel.contentList[index]) | ||||
|                         } | ||||
|                     } | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                 } | ||||
|                 .padding(.vertical, 13.3) | ||||
|             } | ||||
|             .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { | ||||
|                 HStack { | ||||
|                     Spacer() | ||||
|                     Text(viewModel.errorMessage) | ||||
|                         .padding(.vertical, 13.3) | ||||
|                         .frame(width: screenSize().width - 66.7, alignment: .center) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                         .background(Color.button) | ||||
|                         .foregroundColor(Color.white) | ||||
|                         .multilineTextAlignment(.leading) | ||||
|                         .cornerRadius(20) | ||||
|                         .padding(.bottom, 66.7) | ||||
|                     Spacer() | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             if isShowAddContentView { | ||||
|                 PlaylistAddContentView( | ||||
|                     isShowing: $isShowAddContentView, | ||||
|                     contentList: $viewModel.contentList | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentPlaylistCreateView( | ||||
|         isShowing: .constant(true), | ||||
|         reloadData: .constant(false) | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,90 @@ | ||||
| // | ||||
| //  ContentPlaylistCreateViewModel.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 12/7/24. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Combine | ||||
|  | ||||
| final class ContentPlaylistCreateViewModel: ObservableObject { | ||||
|     private let repository = ContentPlaylistListRepository() | ||||
|     private var subscription = Set<AnyCancellable>() | ||||
|      | ||||
|     @Published var isLoading = false | ||||
|     @Published var errorMessage = "" | ||||
|     @Published var isShowPopup = false | ||||
|      | ||||
|     @Published var title: String = "" | ||||
|     @Published var desc: String = "" | ||||
|     var contentList = [AudioContentPlaylistContent]() | ||||
|      | ||||
|     func savePlaylist(onSuccess: @escaping () -> Void) { | ||||
|         if (validate()) { | ||||
|             isLoading = true | ||||
|              | ||||
|             let contentIdAndOrderList = contentList.mapIndexed { index, item in | ||||
|                 PlaylistContentIdAndOrder(contentId: item.id, order: index + 1) | ||||
|             } | ||||
|              | ||||
|             repository.createPlaylist( | ||||
|                 request: CreatePlaylistRequest( | ||||
|                     title: title, | ||||
|                     desc: desc, | ||||
|                     contentIdAndOrderList: contentIdAndOrderList | ||||
|                 ) | ||||
|             ) | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] response in | ||||
|                 self.isLoading = false | ||||
|                 let responseData = response.data | ||||
|                  | ||||
|                 do { | ||||
|                     let jsonDecoder = JSONDecoder() | ||||
|                     let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) | ||||
|                     self.isLoading = false | ||||
|                      | ||||
|                     if decoded.success { | ||||
|                         onSuccess() | ||||
|                     } else { | ||||
|                         if let message = decoded.message { | ||||
|                             self.errorMessage = message | ||||
|                         } else { | ||||
|                             self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         } | ||||
|                          | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                     self.isShowPopup = true | ||||
|                     self.isLoading = false | ||||
|                 } | ||||
|             } | ||||
|             .store(in: &subscription) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func validate() -> Bool { | ||||
|         if (title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || title.count < 3) { | ||||
|             errorMessage = "제목을 3자 이상 입력하세요" | ||||
|             isShowPopup = true | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         if (contentList.isEmpty) { | ||||
|             errorMessage = "콘텐츠를 1개 이상 추가하세요" | ||||
|             isShowPopup = true | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| // | ||||
| //  CreatePlaylistRequest.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 12/8/24. | ||||
| // | ||||
|  | ||||
| struct CreatePlaylistRequest: Encodable { | ||||
|     let title: String | ||||
|     let desc: String? | ||||
|     let contentIdAndOrderList: [PlaylistContentIdAndOrder] | ||||
| } | ||||
| @@ -0,0 +1,89 @@ | ||||
| // | ||||
| //  PlaylistAddContentItemView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 12/9/24. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct PlaylistAddContentItemView: View { | ||||
|      | ||||
|     let item: GetAudioContentOrderListItem | ||||
|     let onClick: () -> Void | ||||
|     @State private var isChecked: Bool | ||||
|      | ||||
|     init(item: GetAudioContentOrderListItem, onClick: @escaping () -> Void, isChecked: Bool) { | ||||
|         self.item = item | ||||
|         self.onClick = onClick | ||||
|         self._isChecked = State(initialValue: isChecked) | ||||
|     } | ||||
|      | ||||
|     var body: some View { | ||||
|         HStack(spacing: 13.3) { | ||||
|             KFImage(URL(string: item.coverImageUrl)) | ||||
|                 .cancelOnDisappear(true) | ||||
|                 .downsampling(size: CGSize(width: 40, height: 40)) | ||||
|                 .resizable() | ||||
|                 .scaledToFill() | ||||
|                 .frame(width: 40, height: 40, alignment: .center) | ||||
|                 .cornerRadius(5.3) | ||||
|                 .clipped() | ||||
|              | ||||
|             VStack(alignment: .leading, spacing: 2.6) { | ||||
|                 HStack(spacing: 8) { | ||||
|                     Text(item.themeStr) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 10)) | ||||
|                         .foregroundColor(Color(hex: "3bac6a")) | ||||
|                         .padding(2.6) | ||||
|                         .background(Color(hex: "28312b")) | ||||
|                         .cornerRadius(2.6) | ||||
|                      | ||||
|                     if let duration = item.duration { | ||||
|                         Text(duration) | ||||
|                             .font(.custom(Font.medium.rawValue, size: 10)) | ||||
|                             .foregroundColor(Color.gray77) | ||||
|                             .padding(2.6) | ||||
|                             .background(Color.gray22) | ||||
|                             .cornerRadius(2.6) | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 Text(item.title) | ||||
|                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                     .foregroundColor(Color.grayd2) | ||||
|                     .lineLimit(2) | ||||
|                     .truncationMode(.tail) | ||||
|             } | ||||
|              | ||||
|             Spacer() | ||||
|              | ||||
|             Image(isChecked ? "ic_check_blue" : "ic_playlist_add") | ||||
|                 .padding(8) | ||||
|                 .onTapGesture { | ||||
|                     onClick() | ||||
|                     isChecked.toggle() | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     PlaylistAddContentItemView( | ||||
|         item: GetAudioContentOrderListItem( | ||||
|             contentId: 1, | ||||
|             coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|             creatorNickname: "유저1", | ||||
|             title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....", | ||||
|             themeStr: "커버곡", | ||||
|             duration: "00:30:20", | ||||
|             isAdult: false, | ||||
|             orderType: .KEEP, | ||||
|             likeCount: 0, | ||||
|             commentCount: 0 | ||||
|         ), | ||||
|         onClick: {}, | ||||
|         isChecked: true | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,128 @@ | ||||
| // | ||||
| //  PlaylistAddContentView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 12/8/24. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct PlaylistAddContentView: View { | ||||
|      | ||||
|     @StateObject var viewModel = OrderListAllViewModel() | ||||
|      | ||||
|     @Binding var isShowing: Bool | ||||
|     @Binding var contentList: [AudioContentPlaylistContent] | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 13.3) { | ||||
|             ZStack { | ||||
|                 Text("새로운 콘텐츠 추가/제거") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color.grayee) | ||||
|                  | ||||
|                 HStack(spacing: 0) { | ||||
|                     Spacer() | ||||
|                      | ||||
|                     Text("닫기") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||
|                         .foregroundColor(Color.grayee) | ||||
|                         .frame(minHeight: 48) | ||||
|                         .onTapGesture { isShowing = false } | ||||
|                 } | ||||
|             } | ||||
|             .frame(height: 50) | ||||
|             .frame(maxWidth: .infinity) | ||||
|             .background(Color.black) | ||||
|              | ||||
|             HStack(alignment: .center, spacing: 5.3) { | ||||
|                 Text("전체") | ||||
|                     .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||
|                     .foregroundColor(Color.white) | ||||
|                  | ||||
|                 Text("\(viewModel.totalCount)개") | ||||
|                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                     .foregroundColor(Color.gray90) | ||||
|             } | ||||
|              | ||||
|             ScrollView(.vertical, showsIndicators: false) { | ||||
|                 LazyVStack(spacing: 10.7) { | ||||
|                     ForEach(0..<viewModel.orderList.count, id: \.self) { index in | ||||
|                         let item = viewModel.orderList[index] | ||||
|                         PlaylistAddContentItemView( | ||||
|                             item: item, | ||||
|                             onClick: { | ||||
|                                 let isChecked = contentList.contains(where: { | ||||
|                                     $0.id == item.contentId | ||||
|                                 }) | ||||
|                                  | ||||
|                                 if isChecked { | ||||
|                                     contentList.removeAll(where: { | ||||
|                                         $0.id == item.contentId | ||||
|                                     }) | ||||
|                                 } else { | ||||
|                                     contentList.append( | ||||
|                                         AudioContentPlaylistContent( | ||||
|                                             id: item.contentId, | ||||
|                                             title: item.title, | ||||
|                                             category: item.themeStr, | ||||
|                                             coverUrl: item.coverImageUrl, | ||||
|                                             duration: item.duration ?? "00:00:00", | ||||
|                                             creatorNickname: item.creatorNickname, | ||||
|                                             creatorProfileUrl: "" | ||||
|                                         ) | ||||
|                                     ) | ||||
|                                 } | ||||
|                             }, | ||||
|                             isChecked: contentList.contains(where: { | ||||
|                                 $0.id == item.contentId | ||||
|                             }) | ||||
|                         ) | ||||
|                         .contentShape(Rectangle()) | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                         .padding(.top, 6.7) | ||||
|                         .onAppear { | ||||
|                             if index == viewModel.orderList.count - 1 { | ||||
|                                 viewModel.getOrderList(orderType: .KEEP) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             .onAppear { | ||||
|                 viewModel.getOrderList(orderType: .KEEP) | ||||
|             } | ||||
|         } | ||||
|         .padding(.horizontal, 13.3) | ||||
|         .frame(maxWidth: .infinity, maxHeight: .infinity) | ||||
|         .background(Color.black) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     PlaylistAddContentView( | ||||
|         isShowing: .constant(true), | ||||
|         contentList: .constant( | ||||
|             [ | ||||
|                 AudioContentPlaylistContent( | ||||
|                     id: 1, | ||||
|                     title: "안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요", | ||||
|                     category: "커버곡", | ||||
|                     coverUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                     duration: "00:30:20", | ||||
|                     creatorNickname: "유저1", | ||||
|                     creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 AudioContentPlaylistContent( | ||||
|                     id: 2, | ||||
|                     title: "안녕하세요 오늘은 커버곡을 들려드리려고 해요", | ||||
|                     category: "커버곡", | ||||
|                     coverUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                     duration: "00:30:20", | ||||
|                     creatorNickname: "유저2", | ||||
|                     creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ) | ||||
|             ] | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| // | ||||
| //  PlaylistContentIdAndOrder.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 12/8/24. | ||||
| // | ||||
|  | ||||
| struct PlaylistContentIdAndOrder: Encodable { | ||||
|     let contentId: Int | ||||
|     let order: Int | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| // | ||||
| //  PlaylistCreateContentView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 12/8/24. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct PlaylistCreateContentView: View { | ||||
|      | ||||
|     let content: AudioContentPlaylistContent | ||||
|      | ||||
|     var body: some View { | ||||
|         HStack(spacing: 13.3) { | ||||
|             KFImage(URL(string: content.coverUrl)) | ||||
|                 .cancelOnDisappear(true) | ||||
|                 .downsampling(size: CGSize(width: 40, height: 40)) | ||||
|                 .resizable() | ||||
|                 .scaledToFill() | ||||
|                 .frame(width: 40, height: 40, alignment: .center) | ||||
|                 .cornerRadius(5.3) | ||||
|              | ||||
|             VStack(alignment: .leading, spacing: 6.7) { | ||||
|                 HStack(spacing: 8) { | ||||
|                     Text(content.category) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 8)) | ||||
|                         .foregroundColor(Color(hex: "3bac6a")) | ||||
|                         .padding(2.6) | ||||
|                         .background(Color(hex: "28312b")) | ||||
|                         .cornerRadius(2.6) | ||||
|                      | ||||
|                     Text(content.duration) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 8)) | ||||
|                         .foregroundColor(Color.gray77) | ||||
|                         .padding(2.6) | ||||
|                         .background(Color.gray22) | ||||
|                         .cornerRadius(2.6) | ||||
|                 } | ||||
|                  | ||||
|                 Text(content.title) | ||||
|                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                     .foregroundColor(Color.grayd2) | ||||
|                     .multilineTextAlignment(.leading) | ||||
|                     .lineLimit(3) | ||||
|                     .truncationMode(.tail) | ||||
|                     .fixedSize(horizontal: false, vertical: true) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     PlaylistCreateContentView( | ||||
|         content: AudioContentPlaylistContent( | ||||
|             id: 1, | ||||
|             title: "안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요", | ||||
|             category: "커버곡", | ||||
|             coverUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|             duration: "00:30:20", | ||||
|             creatorNickname: "유저1", | ||||
|             creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,28 @@ | ||||
| // | ||||
| //  GetPlaylistDetailResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 12/8/24. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| struct GetPlaylistDetailResponse: Decodable { | ||||
|     let playlistId: Int | ||||
|     let title: String | ||||
|     let desc: String | ||||
|     let createdDate: String | ||||
|     let contentCount: Int | ||||
|     let playlistCoverImageList: [String] | ||||
|     let contentList: [AudioContentPlaylistContent] | ||||
| } | ||||
|  | ||||
| struct AudioContentPlaylistContent: Decodable { | ||||
|     let id: Int | ||||
|     let title: String | ||||
|     let category: String | ||||
|     let coverUrl: String | ||||
|     let duration: String | ||||
|     let creatorNickname: String | ||||
|     let creatorProfileUrl: String | ||||
| } | ||||
| @@ -10,6 +10,7 @@ import Moya | ||||
|  | ||||
| enum PlaylistApi { | ||||
|     case getPlaylistList | ||||
|     case createPlaylist(request: CreatePlaylistRequest) | ||||
| } | ||||
|  | ||||
| extension PlaylistApi: TargetType { | ||||
| @@ -19,7 +20,7 @@ extension PlaylistApi: TargetType { | ||||
|      | ||||
|     var path: String { | ||||
|         switch self { | ||||
|         case .getPlaylistList: | ||||
|         case .getPlaylistList, .createPlaylist: | ||||
|             return "/audio-content/playlist" | ||||
|         } | ||||
|     } | ||||
| @@ -28,6 +29,9 @@ extension PlaylistApi: TargetType { | ||||
|         switch self { | ||||
|         case .getPlaylistList: | ||||
|             return .get | ||||
|              | ||||
|         case .createPlaylist: | ||||
|             return .post | ||||
|         } | ||||
|     } | ||||
|      | ||||
| @@ -35,6 +39,9 @@ extension PlaylistApi: TargetType { | ||||
|         switch self { | ||||
|         case .getPlaylistList: | ||||
|             return .requestPlain | ||||
|              | ||||
|         case .createPlaylist(let request): | ||||
|             return .requestJSONEncodable(request) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|   | ||||
							
								
								
									
										14
									
								
								SodaLive/Sources/Extensions/CollectionExtension.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								SodaLive/Sources/Extensions/CollectionExtension.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| // | ||||
| //  CollectionExtension.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 12/8/24. | ||||
| // | ||||
|  | ||||
| extension Collection { | ||||
|     func mapIndexed<T>(_ transform: (Index, Element) -> T) -> [T] { | ||||
|         return self.enumerated().map { index, element in | ||||
|             transform(self.index(startIndex, offsetBy: index), element) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -25,11 +25,11 @@ final class OrderListAllViewModel: ObservableObject { | ||||
|     var isLast = false | ||||
|     private let pageSize = 10 | ||||
|      | ||||
|     func getOrderList() { | ||||
|     func getOrderList(orderType: OrderType? = nil) { | ||||
|         if (!isLast && !isLoading) { | ||||
|             isLoading = true | ||||
|              | ||||
|             repository.getOrderList(page: page, size: pageSize) | ||||
|             repository.getOrderList(orderType: orderType, page: page, size: pageSize) | ||||
|                 .sink { result in | ||||
|                     switch result { | ||||
|                     case .finished: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung