From 0fdb9edd2396041c40587d21fa0485ce6a6f3245 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Mon, 9 Dec 2024 18:11:28 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=AC=EC=83=9D=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ic_check_blue.imageset/Contents.json | 21 +++ .../ic_check_blue.imageset/ic_check_blue.png | Bin 0 -> 631 bytes .../ic_playlist_add.imageset/Contents.json | 21 +++ .../ic_playlist_add.png | Bin 0 -> 962 bytes .../Sources/Content/Box/ContentBoxView.swift | 87 +++++---- SodaLive/Sources/Content/ContentApi.swift | 10 +- .../Sources/Content/ContentRepository.swift | 4 +- .../ContentPlaylistListRepository.swift | 4 + .../Playlist/ContentPlaylistListView.swift | 32 +++- .../Create/ContentPlaylistCreateView.swift | 172 ++++++++++++++++++ .../ContentPlaylistCreateViewModel.swift | 90 +++++++++ .../Create/CreatePlaylistRequest.swift | 12 ++ .../Create/PlaylistAddContentItemView.swift | 89 +++++++++ .../Create/PlaylistAddContentView.swift | 128 +++++++++++++ .../Create/PlaylistContentIdAndOrder.swift | 11 ++ .../Create/PlaylistCreateContentView.swift | 66 +++++++ .../Detail/GetPlaylistDetailResponse.swift | 28 +++ .../Content/Playlist/PlaylistApi.swift | 9 +- .../Extensions/CollectionExtension.swift | 14 ++ .../OrderList/OrderListAllViewModel.swift | 4 +- 20 files changed, 757 insertions(+), 45 deletions(-) create mode 100644 SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/ic_check_blue.png create mode 100644 SodaLive/Resources/Assets.xcassets/ic_playlist_add.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_playlist_add.imageset/ic_playlist_add.png create mode 100644 SodaLive/Sources/Content/Playlist/Create/ContentPlaylistCreateView.swift create mode 100644 SodaLive/Sources/Content/Playlist/Create/ContentPlaylistCreateViewModel.swift create mode 100644 SodaLive/Sources/Content/Playlist/Create/CreatePlaylistRequest.swift create mode 100644 SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentItemView.swift create mode 100644 SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentView.swift create mode 100644 SodaLive/Sources/Content/Playlist/Create/PlaylistContentIdAndOrder.swift create mode 100644 SodaLive/Sources/Content/Playlist/Create/PlaylistCreateContentView.swift create mode 100644 SodaLive/Sources/Content/Playlist/Detail/GetPlaylistDetailResponse.swift create mode 100644 SodaLive/Sources/Extensions/CollectionExtension.swift diff --git a/SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/Contents.json new file mode 100644 index 0000000..055e7a5 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/Contents.json @@ -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 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/ic_check_blue.png b/SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/ic_check_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..ac310783b3fe3abce08d0c94334b68c8994e91a2 GIT binary patch literal 631 zcmV--0*L*IP)JsB7=~dOhADx7Jz?;sx=Ml6KN@f&PsG49ZZCx8yXr$Ko1&mC z&&1RE*ak$7QFh6dX?)beIkF%GOib6K70#2z>x2HOcr@LL@*OUc#dlX`&5cgjMwW<5 z*jAQ^M%aZc5rwcTSt590m$F3A!meeBV1<3i5}CsCS+(F0x_=g!K}GnIC5*!IRW%j89o2E4A$W$}Sg5@EFSvjJijB%MwOmtSje+1-(jFAxjvA#TVS; zX=+$Ffd!0KkGwOgk|nY?t%*wDNS9bm!|mCsz@C96Rmu{X<)OS3bCLp*+oXwq)z~)D zRm&2E-(f95)aqXb;%N?Gc8xrxv~qmmFJ=d9@#3jw3RN1>=5U6z%yI* zw6skAMmEyByJ>C!vcyis!$#`)pDBD|q-mE=#|I)y?9{c?k^aU=n=&$h7Jk3SX{1Av zC9KLrT~Qt2JDnA20S_U`64v!_fwY;LWC`m=yu8pQ<8|o?HOdmsIv%)@QAv51JE&$^ z!fA^qS2_S$!g){2C>@9_;iZY!e^EWrz<$`WX)yGiE;0$|MoH7f zL#2IjBN;0pJy0eOEE)vg_ z?wNS|1tVcJwG@fZs}iT*=~xmUB$pP6f5N52e|M00igWBwp-F8V?kn~w*Yj~1vv+ds z6$W6?)e=jbwvc19(tS)@$=e%Qlo(}{zOMQ{s_Xy2$3HB%t6HACy5H~r!T{D7Ou02rRKEP=1SNw3tT32YPc(i{sU@|wtDjA1)usDF zs-avnQ{!%aYtCNBXa;lGA!EOxT$;^g4Q-53B+cWsT6E_WlT%I81;0T(EE3+3Vb4nU zpv%L-43^h4T+sKG?k(58hAiU_BIz0l&*On>)6!dvYFi-bA0swH1MiD3e!EIh2XaGgyxH*R)E( ke5d&li^Jh?I2`Yte}%ces!oKN6#xJL07*qoM6N<$f?DIp0RR91 literal 0 HcmV?d00001 diff --git a/SodaLive/Sources/Content/Box/ContentBoxView.swift b/SodaLive/Sources/Content/Box/ContentBoxView.swift index 3af13bd..5dd9ee7 100644 --- a/SodaLive/Sources/Content/Box/ContentBoxView.swift +++ b/SodaLive/Sources/Content/Box/ContentBoxView.swift @@ -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 + ) + } } } } diff --git a/SodaLive/Sources/Content/ContentApi.swift b/SodaLive/Sources/Content/ContentApi.swift index c46b17a..49e48b0 100644 --- a/SodaLive/Sources/Content/ContentApi.swift +++ b/SodaLive/Sources/Content/ContentApi.swift @@ -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): diff --git a/SodaLive/Sources/Content/ContentRepository.swift b/SodaLive/Sources/Content/ContentRepository.swift index dd70e6c..ed9c622 100644 --- a/SodaLive/Sources/Content/ContentRepository.swift +++ b/SodaLive/Sources/Content/ContentRepository.swift @@ -34,8 +34,8 @@ final class ContentRepository { return api.requestPublisher(.orderAudioContent(request: OrderRequest(contentId: contentId, orderType: orderType))) } - func getOrderList(page: Int, size: Int) -> AnyPublisher { - return api.requestPublisher(.getOrderList(page: page, size: size)) + func getOrderList(orderType: OrderType?, page: Int, size: Int) -> AnyPublisher { + return api.requestPublisher(.getOrderList(orderType: orderType, page: page, size: size)) } func addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest) -> AnyPublisher { diff --git a/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift b/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift index 648128b..ae739de 100644 --- a/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift +++ b/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift @@ -15,4 +15,8 @@ class ContentPlaylistListRepository { func getPlaylistList() -> AnyPublisher { return api.requestPublisher(.getPlaylistList) } + + func createPlaylist(request: CreatePlaylistRequest) -> AnyPublisher { + return api.requestPublisher(.createPlaylist(request: request)) + } } diff --git a/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift b/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift index 016f5d9..982c4d5 100644 --- a/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift +++ b/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift @@ -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.. 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..() + + @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 + } +} diff --git a/SodaLive/Sources/Content/Playlist/Create/CreatePlaylistRequest.swift b/SodaLive/Sources/Content/Playlist/Create/CreatePlaylistRequest.swift new file mode 100644 index 0000000..2fba953 --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/Create/CreatePlaylistRequest.swift @@ -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] +} diff --git a/SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentItemView.swift b/SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentItemView.swift new file mode 100644 index 0000000..34c8708 --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentItemView.swift @@ -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 + ) +} diff --git a/SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentView.swift b/SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentView.swift new file mode 100644 index 0000000..13a6931 --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/Create/PlaylistAddContentView.swift @@ -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..(_ transform: (Index, Element) -> T) -> [T] { + return self.enumerated().map { index, element in + transform(self.index(startIndex, offsetBy: index), element) + } + } +} diff --git a/SodaLive/Sources/MyPage/OrderList/OrderListAllViewModel.swift b/SodaLive/Sources/MyPage/OrderList/OrderListAllViewModel.swift index 8ace5eb..04fc487 100644 --- a/SodaLive/Sources/MyPage/OrderList/OrderListAllViewModel.swift +++ b/SodaLive/Sources/MyPage/OrderList/OrderListAllViewModel.swift @@ -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: