From 7bd32f8486726fed448a1b4c1bbcfc039fab8061 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 21 Dec 2023 21:04:08 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20UI,=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive/Sources/App/AppStep.swift | 2 + SodaLive/Sources/ContentView.swift | 3 + .../All/CreatorCommunityAllItemView.swift | 2 + .../All/CreatorCommunityAllView.swift | 13 + .../CreatorCommunityApi.swift | 11 +- .../CreatorCommunityItemView.swift | 2 + .../CreatorCommunityRepository.swift | 4 + .../GetCommunityPostListResponse.swift | 2 + .../Modify/CreatorCommunityModifyView.swift | 271 ++++++++++++++++++ .../CreatorCommunityModifyViewModel.swift | 166 +++++++++++ .../ModifyCommunityPostRequest.swift | 0 .../CreatorCommunityWriteViewModel.swift | 1 - 12 files changed, 474 insertions(+), 3 deletions(-) create mode 100644 SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyViewModel.swift rename SodaLive/Sources/Explorer/Profile/CreatorCommunity/{ => Modify}/ModifyCommunityPostRequest.swift (100%) diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index 732608f..78e0f70 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -115,4 +115,6 @@ enum AppStep { case creatorCommunityAll(creatorId: Int) case creatorCommunityWrite(onSuccess: () -> Void) + + case creatorCommunityModify(postId: Int, onSuccess: () -> Void) } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 608d745..04910ed 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -169,6 +169,9 @@ struct ContentView: View { case .creatorCommunityWrite(let onSuccess): CreatorCommunityWriteView(onSuccess: onSuccess) + case .creatorCommunityModify(let postId, let onSuccess): + CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess) + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift index c9cb5fa..fbefe34 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift @@ -125,6 +125,8 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider { imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", date: "3일전", + isCommentAvailable: false, + isAdult: false, isLike: true, likeCount: 10, commentCount: 0, diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift index 64d6168..057ddf4 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift @@ -71,6 +71,15 @@ struct CreatorCommunityAllView: View { isShowing: $viewModel.isShowReportMenu, isShowCreatorMenu: creatorId == UserDefaults.int(forKey: .userId), modifyAction: { + let postId = viewModel.postId + AppState.shared + .setAppStep( + step: .creatorCommunityModify( + postId: postId, + onSuccess: creatorCommunityModifySuccess + ) + ) + viewModel.postId = 0 }, deleteAction: { if creatorId == UserDefaults.int(forKey: .userId) { @@ -137,6 +146,10 @@ struct CreatorCommunityAllView: View { } } } + + private func creatorCommunityModifySuccess() { + viewModel.getCommunityPostList() + } } struct CreatorCommunityAllView_Previews: PreviewProvider { diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityApi.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityApi.swift index 54e9c95..2c4f9f7 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityApi.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityApi.swift @@ -12,6 +12,7 @@ enum CreatorCommunityApi { case createCommunityPost(parameters: [MultipartFormData]) case modifyCommunityPost(parameters: [MultipartFormData]) case getCommunityPostList(creatorId: Int, page: Int, size: Int) + case getCommunityPostDetail(postId: Int) case communityPostLike(postId: Int) case createCommunityPostComment(comment: String, postId: Int, parentId: Int?) case getCommunityPostCommentList(postId: Int, page: Int, size: Int) @@ -40,6 +41,9 @@ extension CreatorCommunityApi: TargetType { case .getCommentReplyList(let commentId, _, _): return "/creator-community/comment/\(commentId)" + + case .getCommunityPostDetail(let postId): + return "/creator-community/\(postId)" } } @@ -48,7 +52,7 @@ extension CreatorCommunityApi: TargetType { case .createCommunityPost, .communityPostLike, .createCommunityPostComment: return .post - case .getCommunityPostList, .getCommunityPostCommentList, .getCommentReplyList: + case .getCommunityPostList, .getCommunityPostCommentList, .getCommentReplyList, .getCommunityPostDetail: return .get case .modifyComment, .modifyCommunityPost: @@ -99,7 +103,10 @@ extension CreatorCommunityApi: TargetType { case .modifyComment(let request): return .requestJSONEncodable(request) - + + case .getCommunityPostDetail: + let parameters = ["timezone": TimeZone.current.identifier] as [String: Any] + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) } } diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift index f92b866..38513f2 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift @@ -91,6 +91,8 @@ struct CreatorCommunityItemView_Previews: PreviewProvider { imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", content: "안녕하세요", date: "3일전", + isCommentAvailable: false, + isAdult: false, isLike: false, likeCount: 10, commentCount: 0, diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityRepository.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityRepository.swift index 29e2fce..40bc787 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityRepository.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityRepository.swift @@ -25,6 +25,10 @@ class CreatorCommunityRepository { return api.requestPublisher(.getCommunityPostList(creatorId: creatorId, page: page, size: size)) } + func getCommunityPostDetail(postId: Int) -> AnyPublisher { + return api.requestPublisher(.getCommunityPostDetail(postId: postId)) + } + func communityPostLike(postId: Int) -> AnyPublisher { return api.requestPublisher(.communityPostLike(postId: postId)) } diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift index 552f3b8..884a9ca 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift @@ -12,6 +12,8 @@ struct GetCommunityPostListResponse: Decodable { let imageUrl: String? let content: String let date: String + let isCommentAvailable: Bool + let isAdult: Bool let isLike: Bool let likeCount: Int let commentCount: Int diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyView.swift new file mode 100644 index 0000000..3db9527 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyView.swift @@ -0,0 +1,271 @@ +// +// CreatorCommunityModifyView.swift +// SodaLive +// +// Created by klaus on 2023/12/21. +// + +import SwiftUI +import Kingfisher + +struct CreatorCommunityModifyView: View { + + let postId: Int + @StateObject var keyboardHandler = KeyboardHandler() + @StateObject private var viewModel = CreatorCommunityModifyViewModel() + + @State private var isShowPhotoPicker = false + let onSuccess: () -> Void + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + GeometryReader { proxy in + ZStack { + VStack(spacing: 0) { + DetailNavigationBar(title: "게시글 수정") + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + VStack(spacing: 0) { + Text("이미지") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(maxWidth: .infinity, alignment: .leading) + + ZStack { + if let selectedImage = viewModel.postImage { + Image(uiImage: selectedImage) + .resizable() + .scaledToFill() + .frame(width: 107, height: 107) + .background(Color(hex: "3e3358")) + .cornerRadius(8) + .clipped() + } else if let postImageUrl = viewModel.postImageUrl { + KFImage(URL(string: postImageUrl)) + .resizable() + .scaledToFill() + .frame(width: 107, height: 107) + .background(Color(hex: "3e3358")) + .cornerRadius(8) + .clipped() + } else { + Image("ic_logo2") + .resizable() + .scaledToFit() + .padding(13.3) + .frame(width: 107, height: 107) + .background(Color(hex: "13181B")) + .cornerRadius(8) + .clipped() + } + + Image("ic_camera") + .padding(10) + .background(Color(hex: "3BB9F1")) + .cornerRadius(30) + .offset(x: 50, y: 36) + } + .frame(alignment: .bottomTrailing) + .contentShape(Rectangle()) + .onTapGesture { isShowPhotoPicker = true } + + HStack(alignment: .top, spacing: 0) { + Text("※ ") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + + Text("등록할 이미지가 없으면 이미지 없이 게시글만 등록 하셔도 됩니다.") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 24) + + HStack(spacing: 0) { + Text("내용") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Spacer() + + Text("\(viewModel.content.count)자") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "ff5c49")) + + Text(" / 최대 500자") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + } + .padding(.top, 26.7) + + TextViewWrapper( + text: $viewModel.content, + placeholder: viewModel.placeholder, + textColorHex: "eeeeee", + backgroundColorHex: "222222" + ) + .frame(height: 184) + .cornerRadius(6.7) + .padding(.top, 13.3) + + VStack(spacing: 13.3) { + Text("댓글 가능 여부") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack(spacing: 13.3) { + SelectButtonView( + title: "댓글 가능", + isChecked: viewModel.isAvailableComment + ) { + if !viewModel.isAvailableComment { + viewModel.isAvailableComment = true + } + } + + SelectButtonView( + title: "댓글 불가", + isChecked: !viewModel.isAvailableComment + ) { + if viewModel.isAvailableComment { + viewModel.isAvailableComment = false + } + } + } + } + .padding(.top, 26.7) + + VStack(spacing: 13.3) { + Text("연령 제한") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack(spacing: 13.3) { + SelectButtonView( + title: "전체 연령", + isChecked: !viewModel.isAdult + ) { + if viewModel.isAdult { + viewModel.isAdult = false + } + } + + SelectButtonView( + title: "19세 이상", + isChecked: viewModel.isAdult + ) { + if !viewModel.isAdult { + viewModel.isAdult = true + } + } + } + } + .padding(.top, 26.7) + } + .padding(13.3) + + VStack(spacing: 0) { + HStack(spacing: 13.3) { + Text("닫기") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "3BB9F1")) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(Color(hex: "13181B")) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(hex: "3BB9F1"), lineWidth: 1) + ) + .onTapGesture { + hideKeyboard() + AppState.shared.back() + } + + Text("수정") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.white) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(Color(hex: "3BB9F1")) + .cornerRadius(10) + .onTapGesture { + hideKeyboard() + viewModel.modifyCommunityPost { + AppState.shared.back() + + DispatchQueue.main.async { + onSuccess() + } + } + } + } + .padding(13.3) + .frame(maxWidth: .infinity) + .background(Color(hex: "222222")) + .cornerRadius(16.7, corners: [.topLeft, .topRight]) + + Rectangle() + .foregroundColor(Color(hex: "222222")) + .frame(height: keyboardHandler.keyboardHeight) + .frame(maxWidth: .infinity) + + if proxy.safeAreaInsets.bottom > 0 { + Rectangle() + .foregroundColor(Color(hex: "222222")) + .frame(height: 15.3) + .frame(maxWidth: .infinity) + } + } + .padding(.top, 100) + } + } + } + + if isShowPhotoPicker { + ImagePicker( + isShowing: $isShowPhotoPicker, + selectedImage: $viewModel.postImage, + sourceType: .photoLibrary + ) + } + } + .onTapGesture { hideKeyboard() } + .edgesIgnoringSafeArea(.bottom) + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { + GeometryReader { geo in + 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(hex: "9970ff")) + .foregroundColor(Color.white) + .multilineTextAlignment(.center) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + .onAppear { + viewModel.postId = postId + viewModel.getCommunityPostDetail { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + AppState.shared.back() + } + } + } + } + } + } +} + +struct CreatorCommunityModifyView_Previews: PreviewProvider { + static var previews: some View { + CreatorCommunityModifyView(postId: 0) {} + } +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyViewModel.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyViewModel.swift new file mode 100644 index 0000000..47a1d4b --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/CreatorCommunityModifyViewModel.swift @@ -0,0 +1,166 @@ +// +// CreatorCommunityModifyViewModel.swift +// SodaLive +// +// Created by klaus on 2023/12/21. +// + +import UIKit +import Moya +import Combine + +final class CreatorCommunityModifyViewModel: ObservableObject { + private let repository = CreatorCommunityRepository() + private var subscription = Set() + + @Published var isLoading = false + @Published var errorMessage = "" + @Published var isShowPopup = false + + @Published var content = "" + @Published var isAdult = false + @Published var isAvailableComment = true + @Published var postImage: UIImage? = nil + @Published var postImageUrl: String? = nil + @Published private(set) var communityPost: GetCommunityPostListResponse? + + var placeholder = "내용을 입력하세요" + var postId = 0 + + func getCommunityPostDetail(onFailure: (() -> Void)? = nil) { + communityPost = nil + isLoading = true + + repository.getCommunityPostDetail(postId: postId) + .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(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.communityPost = data + + self.content = data.content + self.isAdult = data.isAdult + self.isAvailableComment = data.isCommentAvailable + self.postImageUrl = data.imageUrl + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + if let onFailure = onFailure { + onFailure() + } + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + if let onFailure = onFailure { + onFailure() + } + } + } + .store(in: &subscription) + + } + + func modifyCommunityPost(onSuccess: @escaping () -> Void) { + if !isLoading && validateData() { + isLoading = true + + let request = ModifyCommunityPostRequest( + creatorCommunityId: postId, + content: communityPost!.content != content ? content : nil, + isCommentAvailable: communityPost!.isCommentAvailable != isAvailableComment ? isAvailableComment : nil, + isAdult: communityPost!.isAdult != isAdult ? isAdult : nil + ) + var multipartData = [MultipartFormData]() + + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + let jsonData = try? encoder.encode(request) + + if let jsonData = jsonData { + if let postImage = postImage, let imageData = postImage.jpegData(compressionQuality: 0.8) { + multipartData.append( + MultipartFormData( + provider: .data(imageData), + name: "postImage", + fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg", + mimeType: "image/*") + ) + } + + multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request")) + + repository + .modifyCommunityPost(parameters: multipartData) + .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.errorMessage = "게시물이 수정되었습니다." + self.isShowPopup = true + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + onSuccess() + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + self.isLoading = false + } + } + } + + private func validateData() -> Bool { + if content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || content.count < 5 { + errorMessage = "내용을 5자 이상 입력해 주세요." + isShowPopup = true + return false + } + + return true + } +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/ModifyCommunityPostRequest.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/ModifyCommunityPostRequest.swift similarity index 100% rename from SodaLive/Sources/Explorer/Profile/CreatorCommunity/ModifyCommunityPostRequest.swift rename to SodaLive/Sources/Explorer/Profile/CreatorCommunity/Modify/ModifyCommunityPostRequest.swift diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteViewModel.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteViewModel.swift index 9dc8347..f049113 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteViewModel.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteViewModel.swift @@ -93,7 +93,6 @@ final class CreatorCommunityWriteViewModel: ObservableObject { self.isShowPopup = true self.isLoading = false } - } }