diff --git a/SodaLive/Sources/Content/ContentApi.swift b/SodaLive/Sources/Content/ContentApi.swift index 4bb2686..e9d36f1 100644 --- a/SodaLive/Sources/Content/ContentApi.swift +++ b/SodaLive/Sources/Content/ContentApi.swift @@ -25,6 +25,7 @@ enum ContentApi { case getMain case getNewContentOfTheme(theme: String) case donation(request: AudioContentDonationRequest) + case modifyComment(request: ModifyCommentRequest) } extension ContentApi: TargetType { @@ -81,6 +82,9 @@ extension ContentApi: TargetType { case .donation: return "/audio-content/donation" + + case .modifyComment: + return "/audio-content/comment" } } @@ -89,7 +93,7 @@ extension ContentApi: TargetType { case .getAudioContentList, .getAudioContentDetail, .getOrderList, .getAudioContentThemeList, .getAudioContentCommentList, .getAudioContentCommentReplyList, .getMain, .getNewContentOfTheme: return .get - case .likeContent, .modifyAudioContent: + case .likeContent, .modifyAudioContent, .modifyComment: return .put case .registerComment, .orderAudioContent, .addAllPlaybackTracking, .uploadAudioContent, .donation: @@ -173,6 +177,9 @@ extension ContentApi: TargetType { case .donation(let request): return .requestJSONEncodable(request) + + case .modifyComment(let request): + return .requestJSONEncodable(request) } } diff --git a/SodaLive/Sources/Content/ContentRepository.swift b/SodaLive/Sources/Content/ContentRepository.swift index 0f71db3..20b9863 100644 --- a/SodaLive/Sources/Content/ContentRepository.swift +++ b/SodaLive/Sources/Content/ContentRepository.swift @@ -76,4 +76,8 @@ final class ContentRepository { func donation(contentId: Int, can: Int, comment: String) -> AnyPublisher { return api.requestPublisher(.donation(request: AudioContentDonationRequest(contentId: contentId, donationCan: can, comment: comment))) } + + func modifyComment(request: ModifyCommentRequest) -> AnyPublisher { + return api.requestPublisher(.modifyComment(request: request)) + } } diff --git a/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentItemView.swift b/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentItemView.swift index 86b4cce..75d9dde 100644 --- a/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentItemView.swift +++ b/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentItemView.swift @@ -10,80 +10,162 @@ import Kingfisher struct AudioContentCommentItemView: View { - let comment: GetAudioContentCommentListItem + @Binding var isDisabledNavigationLink: Bool + + let contentCreatorId: Int + let commentItem: GetAudioContentCommentListItem let isReplyComment: Bool + let isShowPopupMenuButton: Bool + + let modifyComment: (Int, String) -> Void + let onClickDelete: (Int) -> Void + + @State var isShowPopupMenu: Bool = false + @State var isModeModify: Bool = false + @State var comment: String = "" var body: some View { - VStack(alignment: .leading, spacing: 0) { - HStack(spacing: 6.7) { - KFImage(URL(string: comment.profileUrl)) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading, spacing: 0) { - Text(comment.nickname) - .font(.custom(Font.medium.rawValue, size: 13.3)) - .foregroundColor(Color(hex: "eeeeee")) - - Text(comment.date) - .font(.custom(Font.medium.rawValue, size: 10.3)) - .foregroundColor(Color(hex: "525252")) - .padding(.top, 4) - } - - Spacer() - } - - if comment.donationCan > 0 { - HStack(spacing: 3) { - Image("ic_can") + ZStack(alignment: .topTrailing) { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 6.7) { + KFImage(URL(string: commentItem.profileUrl)) .resizable() - .frame(width: 13.3, height: 13.3) + .frame(width: 40, height: 40) + .clipShape(Circle()) - Text("\(comment.donationCan)") - .font(.custom(Font.bold.rawValue, size: 12)) - .foregroundColor(.white) - } - .padding(.horizontal, 6.7) - .padding(.vertical, 2.7) - .background( - comment.donationCan >= 100000 ? Color(hex: "973a3a") : - comment.donationCan >= 50000 ? Color(hex: "d85e37") : - comment.donationCan >= 10000 ? Color(hex: "d38c38") : - comment.donationCan >= 5000 ? Color(hex: "59548f") : - comment.donationCan >= 1000 ? Color(hex: "4d6aa4") : - comment.donationCan >= 500 ? Color(hex: "2d7390") : - Color(hex: "548f7d") - ) - .cornerRadius(10.7) - .padding(.leading, 46.7) - .padding(.bottom, 5) - } - - HStack(spacing: 0) { - VStack(alignment: .leading, spacing: 13.3) { - Text(comment.comment) - .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color(hex: "777777")) - .fixedSize(horizontal: false, vertical: true) - .padding(.top, comment.donationCan > 0 ? 0 : 13.3) - - if !isReplyComment { - Text(comment.replyCount > 0 ? "답글 \(comment.replyCount)개" : "답글 쓰기") + VStack(alignment: .leading, spacing: 0) { + Text(commentItem.nickname) .font(.custom(Font.medium.rawValue, size: 13.3)) - .foregroundColor(Color(hex: "9970ff")) + .foregroundColor(Color(hex: "eeeeee")) + + Text(commentItem.date) + .font(.custom(Font.medium.rawValue, size: 10.3)) + .foregroundColor(Color(hex: "525252")) + .padding(.top, 4) + } + + Spacer() + + if isShowPopupMenuButton && (contentCreatorId == UserDefaults.int(forKey: .userId) || commentItem.writerId == UserDefaults.int(forKey: .userId)) { + Image("ic_seemore_vertical") + .onTapGesture { isShowPopupMenu.toggle() } } } - Spacer() + if commentItem.donationCan > 0 { + HStack(spacing: 3) { + Image("ic_can") + .resizable() + .frame(width: 13.3, height: 13.3) + + Text("\(commentItem.donationCan)") + .font(.custom(Font.bold.rawValue, size: 12)) + .foregroundColor(.white) + } + .padding(.horizontal, 6.7) + .padding(.vertical, 2.7) + .background( + commentItem.donationCan >= 100000 ? Color(hex: "973a3a") : + commentItem.donationCan >= 50000 ? Color(hex: "d85e37") : + commentItem.donationCan >= 10000 ? Color(hex: "d38c38") : + commentItem.donationCan >= 5000 ? Color(hex: "59548f") : + commentItem.donationCan >= 1000 ? Color(hex: "4d6aa4") : + commentItem.donationCan >= 500 ? Color(hex: "2d7390") : + Color(hex: "548f7d") + ) + .cornerRadius(10.7) + .padding(.leading, 46.7) + .padding(.bottom, 5) + } + + HStack(spacing: 0) { + if isModeModify { + HStack(spacing: 0) { + TextField("댓글을 입력해 보세요.", text: $comment) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .accentColor(Color(hex: "9970ff")) + .keyboardType(.default) + .padding(.horizontal, 13.3) + + Spacer() + + Image("btn_message_send") + .resizable() + .frame(width: 35, height: 35) + .padding(6.7) + .onTapGesture { + hideKeyboard() + if commentItem.comment != comment { + modifyComment(commentItem.id, comment) + } + isModeModify = false + } + } + .background(Color(hex: "232323")) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(lineWidth: 1) + .foregroundColor(Color(hex: "9970ff")) + ) + } else { + VStack(alignment: .leading, spacing: 13.3) { + Text(commentItem.comment) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "777777")) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, commentItem.donationCan > 0 ? 0 : 13.3) + + if !isReplyComment { + Text(commentItem.replyCount > 0 ? "답글 \(commentItem.replyCount)개" : "답글 쓰기") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "9970ff")) + } + } + } + + Spacer() + } + .padding(.leading, 46.7) + + Rectangle() + .foregroundColor(Color(hex: "595959")) + .frame(height: 0.5) + .padding(.top, 16.7) } - .padding(.leading, 46.7) - Rectangle() - .foregroundColor(Color(hex: "595959")) - .frame(height: 0.5) - .padding(.top, 16.7) + if isShowPopupMenu { + VStack(spacing: 10) { + if commentItem.writerId == UserDefaults.int(forKey: .userId) { + Text("수정") + .font(.custom(Font.medium.rawValue, size: 14)) + .foregroundColor(Color(hex: "777777")) + .onTapGesture { + isModeModify = true + isDisabledNavigationLink = true + isShowPopupMenu = false + } + } + + if contentCreatorId == UserDefaults.int(forKey: .userId) || + commentItem.writerId == UserDefaults.int(forKey: .userId) + { + Text("삭제") + .font(.custom(Font.medium.rawValue, size: 14)) + .foregroundColor(Color(hex: "777777")) + .onTapGesture { + onClickDelete(commentItem.id) + isShowPopupMenu = false + } + } + } + .padding(10) + .background(Color(hex: "222222")) + } } + .onAppear { comment = commentItem.comment } } } diff --git a/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListView.swift b/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListView.swift index b728022..a7c9463 100644 --- a/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListView.swift +++ b/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListView.swift @@ -11,10 +11,17 @@ import Kingfisher struct AudioContentCommentListView: View { @Binding var isPresented: Bool + + let creatorId: Int let audioContentId: Int @StateObject var viewModel = AudioContentCommentListViewModel() + @State private var commentId: Int = 0 + @State private var isShowDeletePopup: Bool = false + @State private var isDisabledNavigationLink: Bool = false + @State private var isShowReplyView: Bool = false + var body: some View { NavigationView { ZStack { @@ -97,25 +104,66 @@ struct AudioContentCommentListView: View { LazyVStack(spacing: 13.3) { ForEach(0.. 0 { + SodaDialog( + title: "댓글 삭제", + desc: "삭제하시겠습니까?", + confirmButtonTitle: "삭제", + confirmButtonAction: { + viewModel.modifyComment(commentId: commentId, audioContentId: audioContentId, isActive: false) + commentId = 0 + isShowDeletePopup = false + }, + cancelButtonTitle: "취소", + cancelButtonAction: { + commentId = 0 + isShowDeletePopup = false + } + ) + } + if viewModel.isLoading { LoadingView() } diff --git a/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListViewModel.swift b/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListViewModel.swift index 3a24e95..848e49c 100644 --- a/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListViewModel.swift +++ b/SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListViewModel.swift @@ -121,4 +121,77 @@ class AudioContentCommentListViewModel: ObservableObject { } .store(in: &subscription) } + + func modifyComment( + commentId: Int, + audioContentId: Int, + comment: String? = nil, + isActive: Bool? = nil + ) { + if comment == nil && isActive == nil { + errorMessage = "변경사항이 없습니다." + isShowPopup = true + return + } + + if let comment = comment, comment.trimmingCharacters(in: .whitespaces).isEmpty { + errorMessage = "내용을 입력하세요." + isShowPopup = true + return + } + + isLoading = true + + var request = ModifyCommentRequest(commentId: commentId) + + if let comment = comment { + request.comment = comment + } + + if let isActive = isActive { + request.isActive = isActive + } + + repository.modifyComment(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) + + if decoded.success { + self.comment = "" + self.page = 1 + self.isLast = false + + self.commentList.removeAll() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.getCommentList() + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.isLoading = false + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } } diff --git a/SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyView.swift b/SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyView.swift index 5f546a2..6334300 100644 --- a/SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyView.swift +++ b/SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyView.swift @@ -10,12 +10,16 @@ import Kingfisher struct AudioContentListReplyView: View { + let creatorId: Int let audioContentId: Int let parentComment: GetAudioContentCommentListItem @Environment(\.presentationMode) var presentationMode: Binding @StateObject var viewModel = AudioContentListReplyViewModel() + @State private var commentId: Int = 0 + @State private var isShowDeletePopup: Bool = false + var body: some View { ZStack { VStack(spacing: 0) { @@ -87,7 +91,15 @@ struct AudioContentListReplyView: View { .padding(.bottom, 13.3) .padding(.horizontal, 13.3) - AudioContentCommentItemView(comment: parentComment, isReplyComment: true) + AudioContentCommentItemView( + isDisabledNavigationLink: .constant(false), + contentCreatorId: creatorId, + commentItem: parentComment, + isReplyComment: true, + isShowPopupMenuButton: false, + modifyComment: { _, _ in }, + onClickDelete: { _ in } + ) .padding(.horizontal, 26.7) .padding(.bottom, 13.3) @@ -95,7 +107,21 @@ struct AudioContentListReplyView: View { LazyVStack(spacing: 13.3) { ForEach(0.. 0 { + SodaDialog( + title: "댓글 삭제", + desc: "삭제하시겠습니까?", + confirmButtonTitle: "삭제", + confirmButtonAction: { + viewModel.modifyComment(commentId: commentId, audioContentId: audioContentId, isActive: false) + commentId = 0 + isShowDeletePopup = false + }, + cancelButtonTitle: "취소", + cancelButtonAction: { + commentId = 0 + isShowDeletePopup = false + } + ) + } + + if viewModel.isLoading { + LoadingView() + } } .onAppear { viewModel.audioContentId = audioContentId diff --git a/SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyViewModel.swift b/SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyViewModel.swift index 6213e17..dc2ec9b 100644 --- a/SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyViewModel.swift +++ b/SodaLive/Sources/Content/Detail/Comment/AudioContentListReplyViewModel.swift @@ -121,4 +121,77 @@ final class AudioContentListReplyViewModel: ObservableObject { } .store(in: &subscription) } + + func modifyComment( + commentId: Int, + audioContentId: Int, + comment: String? = nil, + isActive: Bool? = nil + ) { + if comment == nil && isActive == nil { + errorMessage = "변경사항이 없습니다." + isShowPopup = true + return + } + + if let comment = comment, comment.trimmingCharacters(in: .whitespaces).isEmpty { + errorMessage = "내용을 입력하세요." + isShowPopup = true + return + } + + isLoading = true + + var request = ModifyCommentRequest(commentId: commentId) + + if let comment = comment { + request.comment = comment + } + + if let isActive = isActive { + request.isActive = isActive + } + + repository.modifyComment(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) + + if decoded.success { + self.comment = "" + self.page = 1 + self.isLast = false + + self.commentList.removeAll() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.getCommentList() + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.isLoading = false + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } } diff --git a/SodaLive/Sources/Content/Detail/Comment/ModifyCommentRequest.swift b/SodaLive/Sources/Content/Detail/Comment/ModifyCommentRequest.swift new file mode 100644 index 0000000..64ef334 --- /dev/null +++ b/SodaLive/Sources/Content/Detail/Comment/ModifyCommentRequest.swift @@ -0,0 +1,12 @@ +// +// ModifyCommentRequest.swift +// SodaLive +// +// Created by klaus on 2023/09/08. +// + +struct ModifyCommentRequest: Encodable { + let commentId: Int + var comment: String? = nil + var isActive: Bool? = nil +} diff --git a/SodaLive/Sources/Content/Detail/ContentDetailView.swift b/SodaLive/Sources/Content/Detail/ContentDetailView.swift index 3fa2ab0..4f594b8 100644 --- a/SodaLive/Sources/Content/Detail/ContentDetailView.swift +++ b/SodaLive/Sources/Content/Detail/ContentDetailView.swift @@ -282,6 +282,7 @@ struct ContentDetailView: View { content: { AudioContentCommentListView( isPresented: $isShowCommentListView, + creatorId: viewModel.audioContent!.creator.creatorId, audioContentId: viewModel.audioContent!.contentId ) }