diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index 72b2a98..732608f 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -114,5 +114,5 @@ enum AppStep { case creatorCommunityAll(creatorId: Int) - case creatorCommunityWrite + case creatorCommunityWrite(onSuccess: () -> Void) } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 1bfb065..608d745 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -166,8 +166,8 @@ struct ContentView: View { case .creatorCommunityAll(let creatorId): CreatorCommunityAllView(creatorId: creatorId) - case .creatorCommunityWrite: - CreatorCommunityWriteView() + case .creatorCommunityWrite(let onSuccess): + CreatorCommunityWriteView(onSuccess: onSuccess) default: EmptyView() diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/GetCommunityPostCommentListResponse.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/GetCommunityPostCommentListResponse.swift new file mode 100644 index 0000000..a18690e --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/GetCommunityPostCommentListResponse.swift @@ -0,0 +1,21 @@ +// +// GetCommunityPostCommentListResponse.swift +// SodaLive +// +// Created by klaus on 2023/12/19. +// + +struct GetCommunityPostCommentListResponse: Decodable { + let totalCount: Int + let items: [GetCommunityPostCommentListItem] +} + +struct GetCommunityPostCommentListItem: Decodable { + let id: Int + let writerId: Int + let nickname: String + let profileUrl: String + let comment: String + let date: String + let replyCount: Int +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllViewModel.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllViewModel.swift new file mode 100644 index 0000000..57d9183 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllViewModel.swift @@ -0,0 +1,12 @@ +// +// CreatorCommunityAllViewModel.swift +// SodaLive +// +// Created by klaus on 2023/12/19. +// + +import Foundation + +class CreatorCommunityAllViewModel: ObservableObject { + +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityApi.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityApi.swift new file mode 100644 index 0000000..936e78c --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityApi.swift @@ -0,0 +1,44 @@ +// +// CreatorCommunityApi.swift +// SodaLive +// +// Created by klaus on 2023/12/19. +// + +import Foundation +import Moya + +enum CreatorCommunityApi { + case createCommunityPost(parameters: [MultipartFormData]) +} + +extension CreatorCommunityApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .createCommunityPost: + return "/creator-community" + } + } + + var method: Moya.Method { + switch self { + case .createCommunityPost: + return .post + } + } + + var task: Moya.Task { + switch self { + case .createCommunityPost(let parameters): + return .uploadMultipart(parameters) + } + } + + var headers: [String : String]? { + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift index f21969b..f0de553 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift @@ -6,29 +6,33 @@ // import SwiftUI +import Kingfisher struct CreatorCommunityItemView: View { + + let item: GetCommunityPostListResponse + var body: some View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 11) { - Image("ic_place_holder") + KFImage(URL(string: item.creatorProfileUrl)) .resizable() .frame(width: 40, height: 40) .clipShape(Circle()) - Text("민하나") + Text(item.creatorNickname) .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "eeeeee")) Spacer() - Text("1일") + Text(item.date) .font(.custom(Font.light.rawValue, size: 13.3)) .foregroundColor(Color(hex: "777777")) } HStack(spacing: 0) { - Text("너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!") + Text(item.content) .font(.custom(Font.medium.rawValue, size: 12)) .foregroundColor(Color(hex: "bbbbbb")) .fixedSize(horizontal: false, vertical: true) @@ -36,10 +40,12 @@ struct CreatorCommunityItemView: View { Spacer() - Image("btn_plus_round_rect") - .resizable() - .frame(width: 53.3, height: 53.3) - .cornerRadius(4.7) + if let imageUrl = item.imageUrl { + KFImage(URL(string: imageUrl)) + .resizable() + .frame(width: 53.3, height: 53.3) + .cornerRadius(4.7) + } } HStack(spacing: 13.3) { @@ -48,7 +54,7 @@ struct CreatorCommunityItemView: View { .resizable() .frame(width: 13.3, height: 13.3) - Text("7,680") + Text("\(item.likeCount)") .font(.custom(Font.medium.rawValue, size: 11)) .foregroundColor(Color(hex: "777777")) } @@ -58,7 +64,7 @@ struct CreatorCommunityItemView: View { .resizable() .frame(width: 13.3, height: 13.3) - Text("150") + Text("\(item.commentCount)") .font(.custom(Font.medium.rawValue, size: 11)) .foregroundColor(Color(hex: "777777")) } @@ -73,6 +79,18 @@ struct CreatorCommunityItemView: View { struct CreatorCommunityItemView_Previews: PreviewProvider { static var previews: some View { - CreatorCommunityItemView() + CreatorCommunityItemView( + item: GetCommunityPostListResponse( + creatorNickname: "민하나", + creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + content: "안녕하세요", + date: "3일전", + isLike: false, + likeCount: 10, + commentCount: 0, + firstComment: nil + ) + ) } } diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityNoPostsItemView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityNoPostsItemView.swift index 099fd76..a3d068d 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityNoPostsItemView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityNoPostsItemView.swift @@ -8,6 +8,9 @@ import SwiftUI struct CreatorCommunityNoPostsItemView: View { + + let onSuccess: () -> Void + var body: some View { VStack(spacing: 10.3) { CreatorCommunityWriteItemView() @@ -26,11 +29,15 @@ struct CreatorCommunityNoPostsItemView: View { .frame(maxWidth: .infinity) .background(Color(hex: "222222")) .cornerRadius(10.3) + .contentShape(Rectangle()) + .onTapGesture { + AppState.shared.setAppStep(step: .creatorCommunityWrite(onSuccess: onSuccess)) + } } } struct CreatorCommunityNoPostsItemView_Previews: PreviewProvider { static var previews: some View { - CreatorCommunityNoPostsItemView() + CreatorCommunityNoPostsItemView {} } } diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityRepository.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityRepository.swift new file mode 100644 index 0000000..ee992c0 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityRepository.swift @@ -0,0 +1,19 @@ +// +// CreatorCommunityRepository.swift +// SodaLive +// +// Created by klaus on 2023/12/19. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +class CreatorCommunityRepository { + private let api = MoyaProvider() + + func createCommunityPost(parameters: [MultipartFormData]) -> AnyPublisher { + return api.requestPublisher(.createCommunityPost(parameters: parameters)) + } +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift new file mode 100644 index 0000000..1658ba3 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift @@ -0,0 +1,18 @@ +// +// GetCommunityPostListResponse.swift +// SodaLive +// +// Created by klaus on 2023/12/19. +// + +struct GetCommunityPostListResponse: Decodable { + let creatorNickname: String + let creatorProfileUrl: String + let imageUrl: String? + let content: String + let date: String + let isLike: Bool + let likeCount: Int + let commentCount: Int + let firstComment: GetCommunityPostCommentListItem? +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreateCommunityPostRequest.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreateCommunityPostRequest.swift new file mode 100644 index 0000000..be216ed --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreateCommunityPostRequest.swift @@ -0,0 +1,14 @@ +// +// CreateCommunityPostRequest.swift +// SodaLive +// +// Created by klaus on 2023/12/19. +// + +import Foundation + +struct CreateCommunityPostRequest: Encodable { + let content: String + let isAdult: Bool + let isCommentAvailable: Bool +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteView.swift index e7a1621..d5174b5 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteView.swift @@ -10,9 +10,13 @@ import SwiftUI struct CreatorCommunityWriteView: View { @StateObject var keyboardHandler = KeyboardHandler() + @StateObject private var viewModel = CreatorCommunityWriteViewModel() + + @State private var isShowPhotoPicker = false + let onSuccess: () -> Void var body: some View { - BaseView { + BaseView(isLoading: $viewModel.isLoading) { GeometryReader { proxy in ZStack { VStack(spacing: 0) { @@ -27,13 +31,24 @@ struct CreatorCommunityWriteView: View { .frame(maxWidth: .infinity, alignment: .leading) ZStack { - Image("ic_logo2") - .resizable() - .scaledToFit() - .padding(13.3) - .frame(width: 107, height: 107) - .background(Color(hex: "13181B")) - .cornerRadius(8) + if let selectedImage = viewModel.postImage { + Image(uiImage: selectedImage) + .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) @@ -42,6 +57,8 @@ struct CreatorCommunityWriteView: View { .offset(x: 50, y: 36) } .frame(alignment: .bottomTrailing) + .contentShape(Rectangle()) + .onTapGesture { isShowPhotoPicker = true } HStack(alignment: .top, spacing: 0) { Text("※ ") @@ -62,7 +79,7 @@ struct CreatorCommunityWriteView: View { Spacer() - Text("0자") + Text("\(viewModel.content.count)자") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color(hex: "ff5c49")) + Text(" / 최대 500자") @@ -72,8 +89,8 @@ struct CreatorCommunityWriteView: View { .padding(.top, 26.7) TextViewWrapper( - text: .constant(""), - placeholder: "내용을 입력하세요", + text: $viewModel.content, + placeholder: viewModel.placeholder, textColorHex: "eeeeee", backgroundColorHex: "222222" ) @@ -88,10 +105,22 @@ struct CreatorCommunityWriteView: View { .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 13.3) { - SelectButtonView(title: "댓글 가능", isChecked: true) { + SelectButtonView( + title: "댓글 가능", + isChecked: viewModel.isAvailableComment + ) { + if !viewModel.isAvailableComment { + viewModel.isAvailableComment = true + } } - SelectButtonView(title: "댓글 불가", isChecked: false) { + SelectButtonView( + title: "댓글 불가", + isChecked: !viewModel.isAvailableComment + ) { + if viewModel.isAvailableComment { + viewModel.isAvailableComment = false + } } } } @@ -104,10 +133,22 @@ struct CreatorCommunityWriteView: View { .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 13.3) { - SelectButtonView(title: "전체 연령", isChecked: true) { + SelectButtonView( + title: "전체 연령", + isChecked: !viewModel.isAdult + ) { + if viewModel.isAdult { + viewModel.isAdult = false + } } - SelectButtonView(title: "19세 이상", isChecked: false) { + SelectButtonView( + title: "19세 이상", + isChecked: viewModel.isAdult + ) { + if !viewModel.isAdult { + viewModel.isAdult = true + } } } } @@ -130,6 +171,7 @@ struct CreatorCommunityWriteView: View { ) .onTapGesture { hideKeyboard() + AppState.shared.back() } Text("등록") @@ -141,6 +183,13 @@ struct CreatorCommunityWriteView: View { .cornerRadius(10) .onTapGesture { hideKeyboard() + viewModel.createCommunityPost { + AppState.shared.back() + + DispatchQueue.main.async { + onSuccess() + } + } } } .padding(13.3) @@ -164,8 +213,34 @@ struct CreatorCommunityWriteView: View { } } } + + 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() + } + } + } } } } @@ -173,6 +248,6 @@ struct CreatorCommunityWriteView: View { struct CreatorCommunityWriteView_Previews: PreviewProvider { static var previews: some View { - CreatorCommunityWriteView() + CreatorCommunityWriteView {} } } diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteViewModel.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteViewModel.swift new file mode 100644 index 0000000..9f74c42 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/Write/CreatorCommunityWriteViewModel.swift @@ -0,0 +1,108 @@ +// +// CreatorCommunityWriteViewModel.swift +// SodaLive +// +// Created by klaus on 2023/12/19. +// + +import UIKit + +import Moya +import Combine + +final class CreatorCommunityWriteViewModel: 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 + + var placeholder = "내용을 입력하세요" + + func createCommunityPost(onSuccess: @escaping () -> Void) { + if !isLoading && validateData() { + isLoading = true + + let request = CreateCommunityPostRequest(content: content, isAdult: isAdult, isCommentAvailable: isAvailableComment) + 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/*") + ) + } else { + errorMessage = "이미지를 업로드 하지 못했습니다.\n다시 선택해 주세요" + isShowPopup = true + isLoading = false + return + } + + multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request")) + + repository + .createCommunityPost(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 { + return true + } +} diff --git a/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift b/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift index 70832c0..920c212 100644 --- a/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift +++ b/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift @@ -14,6 +14,7 @@ struct GetCreatorProfileResponse: Decodable { let liveRoomList: [LiveRoomResponse] let contentList: [GetAudioContentListItem] let notice: String + let communityPostList: [GetCommunityPostListResponse] let cheers: GetCheersResponse let activitySummary: GetCreatorActivitySummary let isBlock: Bool diff --git a/SodaLive/Sources/Explorer/Profile/UserProfileView.swift b/SodaLive/Sources/Explorer/Profile/UserProfileView.swift index 4a26e06..aa528ca 100644 --- a/SodaLive/Sources/Explorer/Profile/UserProfileView.swift +++ b/SodaLive/Sources/Explorer/Profile/UserProfileView.swift @@ -59,34 +59,45 @@ struct UserProfileView: View { .padding(.top, 13.3) .padding(.horizontal, 13.3) - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: 13.3) { - if UserDefaults.int(forKey: .userId) == creatorProfile.creator.creatorId { - CreatorCommunityWriteItemView() - .onTapGesture { - AppState.shared.setAppStep(step: .creatorCommunityWrite) - } - } - - CreatorCommunityItemView() - .frame(width: 320) - - CreatorCommunityItemView() - .frame(width: 320) - - CreatorCommunityItemView() - .frame(width: 320) - - CreatorCommunityMoreItemView { - AppState.shared.setAppStep( - step: .creatorCommunityAll(creatorId: userId) - ) + if viewModel.communityPostList.count > 0 { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 13.3) { + if UserDefaults.int(forKey: .userId) == creatorProfile.creator.creatorId { + CreatorCommunityWriteItemView() + .onTapGesture { + AppState.shared.setAppStep( + step: .creatorCommunityWrite( + onSuccess: creatorCommunityWriteSuccess + ) + ) + } + } + + ForEach(0.. 0 || @@ -296,6 +307,10 @@ struct UserProfileView: View { } } } + + private func creatorCommunityWriteSuccess() { + viewModel.getCreatorProfile(userId: userId) + } } struct UserProfileView_Previews: PreviewProvider { diff --git a/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift b/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift index c3e352e..78d8d8d 100644 --- a/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift +++ b/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift @@ -36,6 +36,7 @@ final class UserProfileViewModel: ObservableObject { @Published var navigationTitle = "채널" @Published private(set) var creatorProfile: GetCreatorProfileResponse? + @Published private(set) var communityPostList = [GetCommunityPostListResponse]() @Published var isShowShareView = false @Published var shareMessage = "" @@ -73,6 +74,9 @@ final class UserProfileViewModel: ObservableObject { if let data = decoded.data, decoded.success { self.creatorProfile = data self.navigationTitle = "\(data.creator.nickname)님의 채널" + + self.communityPostList.removeAll() + self.communityPostList.append(contentsOf: data.communityPostList) } else { if let message = decoded.message { self.errorMessage = message