From 9ca1493255a7e885062b9a23d4e543ed38a641fb Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Tue, 10 Dec 2024 14:06:34 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=AC=EC=83=9D=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ContentPlaylistListRepository.swift | 4 + .../Detail/ContentPlaylistDetailView.swift | 305 +++++++++--------- .../Modify/ContentPlaylistModifyView.swift | 177 ++++++++++ .../ContentPlaylistModifyViewModel.swift | 145 +++++++++ .../Modify/UpdatePlaylistRequest.swift | 12 + .../Content/Playlist/PlaylistApi.swift | 10 + 6 files changed, 509 insertions(+), 144 deletions(-) create mode 100644 SodaLive/Sources/Content/Playlist/Modify/ContentPlaylistModifyView.swift create mode 100644 SodaLive/Sources/Content/Playlist/Modify/ContentPlaylistModifyViewModel.swift create mode 100644 SodaLive/Sources/Content/Playlist/Modify/UpdatePlaylistRequest.swift diff --git a/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift b/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift index b31cae2..06a5abb 100644 --- a/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift +++ b/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift @@ -27,4 +27,8 @@ class ContentPlaylistListRepository { func deletePlaylist(playlistId: Int) -> AnyPublisher { return api.requestPublisher(.deletePlaylist(playlistId: playlistId)) } + + func updatePlaylist(playlistId: Int, request: UpdatePlaylistRequest) -> AnyPublisher { + return api.requestPublisher(.updatePlaylist(playlistId: playlistId, request: request)) + } } diff --git a/SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailView.swift b/SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailView.swift index c2ec741..43b00ec 100644 --- a/SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailView.swift +++ b/SodaLive/Sources/Content/Playlist/Detail/ContentPlaylistDetailView.swift @@ -18,175 +18,184 @@ struct ContentPlaylistDetailView: View { @State private var isShowPopupMenu = false @State private var isShowDeleteConfirm = false + @State private var isShowModify = false + var body: some View { BaseView(isLoading: $viewModel.isLoading) { - VStack(spacing: 21.3) { - HStack(spacing: 5.3) { - Image("ic_back") - .resizable() - .frame(width: 20, height: 20) - .padding(8) - .onTapGesture { - isShowing = false - } - - Spacer() - - Image("ic_edit_white") - .padding(8) - .onTapGesture { - } - - Image("ic_seemore_vertical_white") - .padding(8) - .onTapGesture { - isShowPopupMenu = true - } - } - .padding(.horizontal, 13.3) - .frame(height: 50) - .frame(maxWidth: .infinity) - .background(Color.black) + if reloadData { + Color.clear - ScrollView(.vertical, showsIndicators: false) { - if let response = viewModel.response { - VStack(alignment: .leading, spacing: 0) { - HStack(alignment: .top, spacing: 13.3) { - VStack(alignment: .center, spacing: 0) { - HStack(spacing: 0) { - KFImage(URL(string: response.playlistCoverImageList[0])) - .cancelOnDisappear(true) - .downsampling(size: CGSize(width: 80, height: 80)) - .resizable() - .scaledToFill() - .clipped() - .frame(maxWidth: .infinity, maxHeight: .infinity) - - if response.playlistCoverImageList.count > 2 { - KFImage(URL(string: response.playlistCoverImageList[1])) + LoadingView() + } else { + VStack(spacing: 21.3) { + HStack(spacing: 5.3) { + Image("ic_back") + .resizable() + .frame(width: 20, height: 20) + .padding(8) + .onTapGesture { + isShowing = false + } + + Spacer() + + Image("ic_edit_white") + .padding(8) + .onTapGesture { + isShowModify = true + } + + Image("ic_seemore_vertical_white") + .padding(8) + .onTapGesture { + isShowPopupMenu = true + } + } + .padding(.horizontal, 13.3) + .frame(height: 50) + .frame(maxWidth: .infinity) + .background(Color.black) + + ScrollView(.vertical, showsIndicators: false) { + if let response = viewModel.response { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .top, spacing: 13.3) { + VStack(alignment: .center, spacing: 0) { + HStack(spacing: 0) { + KFImage(URL(string: response.playlistCoverImageList[0])) .cancelOnDisappear(true) .downsampling(size: CGSize(width: 80, height: 80)) .resizable() .scaledToFill() .clipped() - .frame(maxWidth: 40, maxHeight: 40) + .frame(maxWidth: .infinity, maxHeight: .infinity) + + if response.playlistCoverImageList.count > 2 { + KFImage(URL(string: response.playlistCoverImageList[1])) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 80, height: 80)) + .resizable() + .scaledToFill() + .clipped() + .frame(maxWidth: 40, maxHeight: 40) + } + } + + HStack(spacing: 0) { + if response.playlistCoverImageList.count > 2 { + KFImage(URL(string: response.playlistCoverImageList[2])) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 80, height: 80)) + .resizable() + .scaledToFill() + .clipped() + .frame(maxWidth: 40, maxHeight: 40) + } + + if response.playlistCoverImageList.count > 3 { + KFImage(URL(string: response.playlistCoverImageList[3])) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 80, height: 80)) + .resizable() + .scaledToFill() + .clipped() + .frame(maxWidth: 40, maxHeight: 40) + } } } + .frame(width: 80, height: 80) + .background(Color.graybb) + .cornerRadius(4) - HStack(spacing: 0) { - if response.playlistCoverImageList.count > 2 { - KFImage(URL(string: response.playlistCoverImageList[2])) - .cancelOnDisappear(true) - .downsampling(size: CGSize(width: 80, height: 80)) - .resizable() - .scaledToFill() - .clipped() - .frame(maxWidth: 40, maxHeight: 40) - } + VStack(alignment: .leading, spacing: 6.7) { + Text(response.title) + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.grayd2) + .lineLimit(2) + .truncationMode(.tail) - if response.playlistCoverImageList.count > 3 { - KFImage(URL(string: response.playlistCoverImageList[3])) - .cancelOnDisappear(true) - .downsampling(size: CGSize(width: 80, height: 80)) - .resizable() - .scaledToFill() - .clipped() - .frame(maxWidth: 40, maxHeight: 40) - } + Text(response.desc.prefix(100)) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color.gray90) + .truncationMode(.tail) } } - .frame(width: 80, height: 80) - .background(Color.graybb) - .cornerRadius(4) - VStack(spacing: 6.7) { - Text(response.title) - .font(.custom(Font.bold.rawValue, size: 18.3)) - .foregroundColor(Color.grayd2) - .lineLimit(2) - .truncationMode(.tail) - - Text(response.desc.prefix(100)) - .font(.custom(Font.medium.rawValue, size: 12)) + HStack(spacing: 0) { + Text("만든 날짜 \(response.createdDate)") + .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color.gray90) - .truncationMode(.tail) - } - } - - HStack(spacing: 0) { - Text("만든 날짜 \(response.createdDate)") - .font(.custom(Font.medium.rawValue, size: 13.3)) - .foregroundColor(Color.gray90) - - Spacer() - - Text("\(response.contentCount)개") - .font(.custom(Font.medium.rawValue, size: 13.3)) - .foregroundColor(Color.grayee) - } - .padding(.top, 13.3) - - HStack(spacing: 13.3) { - HStack(spacing: 5.3) { - Image("ic_playlist_play") - Text("Play") - .font(.custom(Font.bold.rawValue, size: 14.7)) - .foregroundColor(Color.white) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 11) - .background(Color.button) - .cornerRadius(5.3) - .contentShape(Rectangle()) - .onTapGesture { - } - - HStack(spacing: 5.3) { - Image("ic_playlist_shuffle") + Spacer() - Text("Shuffle") - .font(.custom(Font.bold.rawValue, size: 14.7)) - .foregroundColor(Color.white) + Text("\(response.contentCount)개") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) } - .frame(maxWidth: .infinity) - .padding(.vertical, 11) - .background(Color.button) - .cornerRadius(5.3) - .contentShape(Rectangle()) - .onTapGesture { + .padding(.top, 13.3) + + HStack(spacing: 13.3) { + HStack(spacing: 5.3) { + Image("ic_playlist_play") + + Text("Play") + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor(Color.white) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 11) + .background(Color.button) + .cornerRadius(5.3) + .contentShape(Rectangle()) + .onTapGesture { + } + + HStack(spacing: 5.3) { + Image("ic_playlist_shuffle") + + Text("Shuffle") + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor(Color.white) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 11) + .background(Color.button) + .cornerRadius(5.3) + .contentShape(Rectangle()) + .onTapGesture { + } } + .padding(.top, 18) + + LazyVStack(alignment: .leading, spacing: 0) { + 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]() + + @Published var response: GetPlaylistDetailResponse? = nil + + var playlistId: Int = 0 { + didSet { + if playlistId > 0 { + getPlaylistDetail() + } + } + } + + private func getPlaylistDetail() { + isLoading = true + + repository.getPlaylistDetail(playlistId: playlistId) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.response = data + self.title = data.title + self.desc = data.desc + self.contentList.append(contentsOf: data.contentList) + } 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) + } + + func modifyPlaylist(onSuccess: @escaping () -> Void) { + if let response = response, validate() { + isLoading = true + + let contentIdAndOrderList = contentList.mapIndexed { index, item in + PlaylistContentIdAndOrder(contentId: item.id, order: index + 1) + } + + let request = UpdatePlaylistRequest( + title: self.title != response.title ? self.title : nil, + desc: self.desc != response.desc ? self.desc : nil, + contentIdAndOrderList: contentIdAndOrderList + ) + + repository.updatePlaylist( + playlistId: playlistId, + request: request + ) + .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/Modify/UpdatePlaylistRequest.swift b/SodaLive/Sources/Content/Playlist/Modify/UpdatePlaylistRequest.swift new file mode 100644 index 0000000..dbd18a5 --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/Modify/UpdatePlaylistRequest.swift @@ -0,0 +1,12 @@ +// +// UpdatePlaylistRequest.swift +// SodaLive +// +// Created by klaus on 12/10/24. +// + +struct UpdatePlaylistRequest: Encodable { + let title: String? + let desc: String? + let contentIdAndOrderList: [PlaylistContentIdAndOrder] +} diff --git a/SodaLive/Sources/Content/Playlist/PlaylistApi.swift b/SodaLive/Sources/Content/Playlist/PlaylistApi.swift index 4e940c2..8ab59d9 100644 --- a/SodaLive/Sources/Content/Playlist/PlaylistApi.swift +++ b/SodaLive/Sources/Content/Playlist/PlaylistApi.swift @@ -13,6 +13,7 @@ enum PlaylistApi { case createPlaylist(request: CreatePlaylistRequest) case getPlaylistDetail(playlistId: Int) case deletePlaylist(playlistId: Int) + case updatePlaylist(playlistId: Int, request: UpdatePlaylistRequest) } extension PlaylistApi: TargetType { @@ -30,6 +31,9 @@ extension PlaylistApi: TargetType { case .deletePlaylist(let playlistId): return "/audio-content/playlist/\(playlistId)" + + case .updatePlaylist(let playlistId, _): + return "/audio-content/playlist/\(playlistId)" } } @@ -43,6 +47,9 @@ extension PlaylistApi: TargetType { case .deletePlaylist: return .delete + + case .updatePlaylist: + return .put } } @@ -53,6 +60,9 @@ extension PlaylistApi: TargetType { case .createPlaylist(let request): return .requestJSONEncodable(request) + + case .updatePlaylist(_, let request): + return .requestJSONEncodable(request) } }