From a167840162fa1bbd49fe6aade3046b466a69dcc9 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 11 Aug 2023 08:47:10 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8,=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ic_thumb_play.imageset/Contents.json | 21 + .../ic_thumb_play.imageset/ic_thumb_play.png | Bin 0 -> 702 bytes SodaLive/Sources/App/AppStep.swift | 2 + .../AddAllPlaybackTrackingRequest.swift | 19 + SodaLive/Sources/Content/ContentApi.swift | 182 +++++++ .../Content/ContentListViewModel.swift | 93 ++++ .../Sources/Content/ContentRepository.swift | 79 +++ .../Create/ContentCreateSelectThemeView.swift | 88 ++++ .../ContentCreateSelectThemeViewModel.swift | 59 +++ .../Content/Create/ContentCreateView.swift | 463 ++++++++++++++++++ .../Create/ContentCreateViewModel.swift | 216 ++++++++ .../Create/CreateAudioContentRequest.swift | 18 + .../Create/GetAudioContentThemeResponse.swift | 14 + .../GetAudioContentCommentListResponse.swift | 24 + .../RegisterAudioContentCommentRequest.swift | 14 + .../GetAudioContentDetailResponse.swift | 50 ++ .../Detail/PutAudioContentLikeRequest.swift | 14 + .../AudioContentDonationRequest.swift | 15 + .../Content/Main/ContentMainBannerView.swift | 114 +++++ .../Main/ContentMainCurationItemView.swift | 36 ++ .../Main/ContentMainCurationView.swift | 22 + .../Content/Main/ContentMainItemView.swift | 78 +++ .../Content/Main/ContentMainMyStashView.swift | 57 +++ ...ContentMainNewContentCreatorItemView.swift | 43 ++ .../ContentMainNewContentCreatorView.swift | 48 ++ .../Main/ContentMainNewContentThemeView.swift | 51 ++ .../Main/ContentMainNewContentView.swift | 57 +++ .../Content/Main/ContentMainView.swift | 97 +++- .../Content/Main/ContentMainViewModel.swift | 124 +++++ .../Main/GetAudioContentMainResponse.swift | 51 ++ .../Sources/Content/Order/OrderRequest.swift | 14 + SodaLive/Sources/ContentView.swift | 3 + .../Profile/GetCreatorProfileResponse.swift | 25 + 33 files changed, 2190 insertions(+), 1 deletion(-) create mode 100644 SodaLive/Resources/Assets.xcassets/ic_thumb_play.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_thumb_play.imageset/ic_thumb_play.png create mode 100644 SodaLive/Sources/Content/AddAllPlaybackTrackingRequest.swift create mode 100644 SodaLive/Sources/Content/ContentApi.swift create mode 100644 SodaLive/Sources/Content/ContentListViewModel.swift create mode 100644 SodaLive/Sources/Content/ContentRepository.swift create mode 100644 SodaLive/Sources/Content/Create/ContentCreateSelectThemeView.swift create mode 100644 SodaLive/Sources/Content/Create/ContentCreateSelectThemeViewModel.swift create mode 100644 SodaLive/Sources/Content/Create/ContentCreateView.swift create mode 100644 SodaLive/Sources/Content/Create/ContentCreateViewModel.swift create mode 100644 SodaLive/Sources/Content/Create/CreateAudioContentRequest.swift create mode 100644 SodaLive/Sources/Content/Create/GetAudioContentThemeResponse.swift create mode 100644 SodaLive/Sources/Content/Detail/Comment/GetAudioContentCommentListResponse.swift create mode 100644 SodaLive/Sources/Content/Detail/Comment/RegisterAudioContentCommentRequest.swift create mode 100644 SodaLive/Sources/Content/Detail/GetAudioContentDetailResponse.swift create mode 100644 SodaLive/Sources/Content/Detail/PutAudioContentLikeRequest.swift create mode 100644 SodaLive/Sources/Content/Donation/AudioContentDonationRequest.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainBannerView.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainCurationItemView.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainCurationView.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainItemView.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainMyStashView.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainNewContentCreatorItemView.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainNewContentCreatorView.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainNewContentThemeView.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainNewContentView.swift create mode 100644 SodaLive/Sources/Content/Main/ContentMainViewModel.swift create mode 100644 SodaLive/Sources/Content/Main/GetAudioContentMainResponse.swift create mode 100644 SodaLive/Sources/Content/Order/OrderRequest.swift create mode 100644 SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift diff --git a/SodaLive/Resources/Assets.xcassets/ic_thumb_play.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_thumb_play.imageset/Contents.json new file mode 100644 index 0000000..ab30ef1 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_thumb_play.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_thumb_play.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_thumb_play.imageset/ic_thumb_play.png b/SodaLive/Resources/Assets.xcassets/ic_thumb_play.imageset/ic_thumb_play.png new file mode 100644 index 0000000000000000000000000000000000000000..99fc670c8cf9f7b9fb4c8d7427f98203ef9a2d24 GIT binary patch literal 702 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxKmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIBA1AIbU-3xsFBY_{^K%8%2^6y^|``XHBF_kh^fkN>}a38a4Gr2i}0{=a+i|MPp0 z${%0qzkmLJU!uPc=y>&#AirP+gJn{` z`88PI*m<^Zt^Rg7u~>2Sp-JKMa;?tIdh&6}`;2?bZ{B#j;mU3iV3;sgc)B=-Se!mP z<#t(mW9J@^v_3h~dVyuO7k{n0 zTG41~8`G9PyKPSJ?HR`mBhR=Co%>+G@cpHklzwZ1c)|VSf1jJ(k_~H$?$us75|PpY0b*p}$O zA%MH{GxNF$vTc?NZ_d?STKk6aCpx9=_#ATctKrr?TK~QS1=dsO<&eJ zC(DdI@z4&jM>TIQ$Ti6OZ*4d?rEei)PS$yWFS~w(t+?HEXUf&bo7b$n$yf6=Yyp=@ zSl)pmzrrPVmTAf!^0HWA#l>s5Z1^@+?TF@sm?t2;i^{XgsiB2AIr7>v2WR2aN?TBmRw-8F?hQAxvX() + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isLoading = false + + @Published var audioContentList: [GetAudioContentListItem] = [] + @Published var totalCount = 0 + @Published var sort = Sort.NEWEST { + didSet { + page = 1 + isLast = false + getAudioContentList() + } + } + + @Published var scrollToTop = false + + var userId = 0 + var page = 1 + var isLast = false + private let pageSize = 10 + + func getAudioContentList() { + if (!isLast && !isLoading) { + isLoading = true + + repository + .getAudioContentList(userId: userId, page: page, size: pageSize, sort: sort) + .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 { + if page == 1 { + self.audioContentList.removeAll() + self.scrollToTop.toggle() + } + + if !data.items.isEmpty { + page += 1 + self.totalCount = data.totalCount + self.audioContentList.append(contentsOf: data.items) + } else { + isLast = true + } + } 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) + } + } +} diff --git a/SodaLive/Sources/Content/ContentRepository.swift b/SodaLive/Sources/Content/ContentRepository.swift new file mode 100644 index 0000000..6251705 --- /dev/null +++ b/SodaLive/Sources/Content/ContentRepository.swift @@ -0,0 +1,79 @@ +// +// ContentRepository.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +final class ContentRepository { + private let api = MoyaProvider() + + func getAudioContentList(userId: Int, page: Int, size: Int, sort: ContentListViewModel.Sort) -> AnyPublisher { + return api.requestPublisher(.getAudioContentList(userId: userId, page: page, size: size, sort: sort)) + } + + func getAudioContentDetail(audioContentId: Int) -> AnyPublisher { + return api.requestPublisher(.getAudioContentDetail(audioContentId: audioContentId)) + } + + func likeContent(audioContentId: Int) -> AnyPublisher { + return api.requestPublisher(.likeContent(request: PutAudioContentLikeRequest(audioContentId: audioContentId))) + } + + func registerComment(audioContentId: Int, comment: String, parentId: Int? = nil) -> AnyPublisher { + return api.requestPublisher(.registerComment(request: RegisterAudioContentCommentRequest(comment: comment, audioContentId: audioContentId, parentId: parentId))) + } + + func orderAudioContent(audioContentId: Int, orderType: OrderType) -> AnyPublisher { + return api.requestPublisher(.orderAudioContent(request: OrderRequest(audioContentId: audioContentId, orderType: orderType))) + } + + func getOrderList(page: Int, size: Int) -> AnyPublisher { + return api.requestPublisher(.getOrderList(page: page, size: size)) + } + + func addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest) -> AnyPublisher { + return api.requestPublisher(.addAllPlaybackTracking(request: request)) + } + + func getAudioContentThemeList() -> AnyPublisher { + return api.requestPublisher(.getAudioContentThemeList) + } + + func uploadAudioContent(parameters: [MultipartFormData]) -> AnyPublisher { + return api.requestPublisher(.uploadAudioContent(parameters: parameters)) + } + + func getAudioContentCommentList(audioContentId: Int, page: Int, size: Int) -> AnyPublisher { + return api.requestPublisher(.getAudioContentCommentList(audioContentId: audioContentId, page: page, size: size)) + } + + func getAudioContentCommentReplyList(commentId: Int, page: Int, size: Int) -> AnyPublisher { + return api.requestPublisher(.getAudioContentCommentReplyList(commentId: commentId, page: page, size: size)) + } + + func deleteAudioContent(audioContentId: Int) -> AnyPublisher { + return api.requestPublisher(.deleteAudioContent(audioContentId: audioContentId)) + } + + func modifyAudioContent(parameters: [MultipartFormData]) -> AnyPublisher { + return api.requestPublisher(.modifyAudioContent(parameters: parameters)) + } + + func getMain() -> AnyPublisher { + return api.requestPublisher(.getMain) + } + + func getNewContentOfTheme(theme: String) -> AnyPublisher { + return api.requestPublisher(.getNewContentOfTheme(theme: theme)) + } + + func donation(contentId: Int, coin: Int, comment: String) -> AnyPublisher { + return api.requestPublisher(.donation(request: AudioContentDonationRequest(audioContentId: contentId, donationCoin: coin, comment: comment))) + } +} diff --git a/SodaLive/Sources/Content/Create/ContentCreateSelectThemeView.swift b/SodaLive/Sources/Content/Create/ContentCreateSelectThemeView.swift new file mode 100644 index 0000000..471a535 --- /dev/null +++ b/SodaLive/Sources/Content/Create/ContentCreateSelectThemeView.swift @@ -0,0 +1,88 @@ +// +// ContentCreateSelectThemeView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Kingfisher + +struct ContentCreateSelectThemeView: View { + + let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ] + + @StateObject var viewModel = ContentCreateSelectThemeViewModel() + + @Binding var isShowing: Bool + @Binding var selectedTheme: GetAudioContentThemeResponse? + + var body: some View { + ZStack { + Color(hex: "222222").ignoresSafeArea() + + VStack(spacing: 0) { + HStack(alignment: .top, spacing: 0) { + Text("테마 선택") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(.white) + + Spacer() + + Image("ic_close_white") + .resizable() + .frame(width: 20, height: 20) + .onTapGesture { isShowing = false } + } + .padding(.horizontal, 26.7) + .padding(.top, 26.7) + + ScrollView(.vertical, showsIndicators: false) { + LazyVGrid(columns: columns, spacing: 26.7) { + ForEach(viewModel.themes, id: \.self) { theme in + VStack(spacing: 16.7) { + KFImage(URL(string: theme.image)) + .resizable() + .scaledToFill() + .frame(width: 60, height: 60, alignment: .top) + .clipShape(Circle()) + + Text(theme.theme) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "bbbbbb")) + } + .onTapGesture { + selectedTheme = theme + isShowing = false + } + } + } + } + .padding(.horizontal, 20) + .padding(.top, 26.7) + } + + if viewModel.isLoading { + LoadingView() + } + } + .cornerRadius(16.7, corners: [.topLeft, .topRight]) + .onAppear { + viewModel.getThemes() + } + } +} + +struct ContentCreateSelectThemeView_Previews: PreviewProvider { + static var previews: some View { + ContentCreateSelectThemeView( + isShowing: .constant(true), + selectedTheme: .constant(GetAudioContentThemeResponse(id: 1, theme: "", image: "")) + ) + } +} diff --git a/SodaLive/Sources/Content/Create/ContentCreateSelectThemeViewModel.swift b/SodaLive/Sources/Content/Create/ContentCreateSelectThemeViewModel.swift new file mode 100644 index 0000000..b94ed38 --- /dev/null +++ b/SodaLive/Sources/Content/Create/ContentCreateSelectThemeViewModel.swift @@ -0,0 +1,59 @@ +// +// ContentCreateSelectThemeViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation +import Combine + +final class ContentCreateSelectThemeViewModel: ObservableObject { + + private let repository = ContentRepository() + private var subscription = Set() + + @Published var isLoading = false + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var themes: [GetAudioContentThemeResponse] = [] + + func getThemes() { + isLoading = true + + repository.getAudioContentThemeList() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse<[GetAudioContentThemeResponse]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.themes.removeAll() + self.themes.append(contentsOf: data) + } 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) + } +} diff --git a/SodaLive/Sources/Content/Create/ContentCreateView.swift b/SodaLive/Sources/Content/Create/ContentCreateView.swift new file mode 100644 index 0000000..ba34e77 --- /dev/null +++ b/SodaLive/Sources/Content/Create/ContentCreateView.swift @@ -0,0 +1,463 @@ +// +// ContentCreateView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Kingfisher + +struct ContentCreateView: View { + + @StateObject var keyboardHandler = KeyboardHandler() + @StateObject private var viewModel = ContentCreateViewModel() + + @State private var isShowPhotoPicker = false + @State private var isShowSelectAudioView = false + @State private var isShowSelectThemeView = false + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + GeometryReader { proxy in + ZStack { + VStack(spacing: 0) { + DetailNavigationBar(title: "콘텐츠 등록") + + ScrollView(.vertical, showsIndicators: false) { + 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.coverImage { + Image(uiImage: selectedImage) + .resizable() + .scaledToFill() + .frame(width: 107, height: 107) + .background(Color(hex: "3e3358")) + .cornerRadius(8) + .clipped() + } else { + Image("ic_logo") + .resizable() + .scaledToFit() + .padding(13.3) + .frame(width: 107, height: 107) + .background(Color(hex: "3e3358")) + .cornerRadius(8) + } + + Image("ic_camera") + .padding(10) + .background(Color(hex: "9970ff")) + .cornerRadius(30) + .offset(x: 50, y: 36) + } + .frame(alignment: .bottomTrailing) + .onTapGesture { isShowPhotoPicker = true } + + Text("등록") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 13.3) + + Text(viewModel.fileName.trimmingCharacters(in: .whitespacesAndNewlines) == "" ? "파일선택" : viewModel.fileName) + .font(.custom(Font.medium.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .background(Color(hex: "9970ff").opacity(0.2)) + .cornerRadius(5.3) + .overlay( + RoundedCorner(radius: 8) + .stroke(lineWidth: 2) + .foregroundColor(Color(hex: "9970ff")) + ) + .padding(.top, 13.3) + .onTapGesture { isShowSelectAudioView = true } + } + .padding(.top, 13.3) + .padding(.horizontal, 13.3) + + Rectangle() + .foregroundColor(Color(hex: "232323")) + .frame(height: 6.7) + .padding(.top, 26.7) + + VStack(spacing: 0) { + Text("제목") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(maxWidth: .infinity, alignment: .leading) + + TextField("제목을 입력하세요", text: $viewModel.title) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.vertical, 16.7) + .padding(.horizontal, 13.3) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + .keyboardType(.default) + .padding(.top, 13.3) + + HStack(spacing: 0) { + Text("내용") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Spacer() + + Text("\(viewModel.detail.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.detail, + placeholder: viewModel.placeholder, + textColorHex: "eeeeee", + backgroundColorHex: "222222" + ) + .frame(height: 184) + .cornerRadius(6.7) + .padding(.top, 13.3) + + Text("테마") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 26.7) + + HStack(spacing: 13.3) { + if let theme = viewModel.theme { + KFImage(URL(string: theme.image)) + .resizable() + .frame(width: 33.3, height: 33.3) + .clipShape(Circle()) + } + + Text(viewModel.theme != nil ? + viewModel.theme!.theme : + "테마 선택") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "9970ff")) + } + .padding(.vertical, viewModel.theme != nil ? 8 : 13.3) + .frame(maxWidth: .infinity) + .background(Color(hex: "9970ff").opacity(0.2)) + .cornerRadius(24) + .overlay( + RoundedRectangle(cornerRadius: 24) + .stroke(lineWidth: 2) + .foregroundColor(Color(hex: "9970ff")) + ) + .padding(.top, 13.3) + .onTapGesture { + isShowSelectThemeView = true + hideKeyboard() + } + + Text("태그") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 26.7) + + TextField("예: #연애 #커버곡", text: $viewModel.hashtags) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.vertical, 16.7) + .padding(.horizontal, 13.3) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + .keyboardType(.default) + .padding(.top, 13.3) + } + .padding(.top, 26.7) + .padding(.horizontal, 13.3) + + Rectangle() + .foregroundColor(Color(hex: "232323")) + .frame(height: 6.7) + .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.isFree) { + if !viewModel.isFree { + viewModel.isFree = true + } + } + + SelectButtonView(title: "유료", isChecked: !viewModel.isFree) { + if viewModel.isFree { + viewModel.isFree = false + } + } + } + + if !viewModel.isFree { + VStack(spacing: 0) { + Text("소장가격") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "d2d2d2")) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack(spacing: 0) { + TextField("가격을 입력하세요(10코인 이상)", text: $viewModel.priceString) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + .cornerRadius(6.7) + .keyboardType(.numberPad) + .padding(.trailing, 10) + + Spacer() + + Text("코인") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + } + .padding(.vertical, 17) + .padding(.horizontal, 13.3) + .background(Color(hex: "222222")) + .cornerRadius(5.3) + .padding(.top, 5.3) + + Rectangle() + .foregroundColor(Color(hex: "232323")) + .frame(height: 1) + .padding(.top, 11) + + Text("※ 이용기간 대여 (7일) | 소장 (서비스종료시까지)") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 13.3) + + Text("※ 대여가격은 소장가격의 70%로 자동 반영") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("※ 콘텐츠의 최소금액은 10코인 입니다") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.top, 26.7) + } + } + .padding(.top, 26.7) + .padding(.horizontal, 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.isAdult) { + if viewModel.isAdult { + viewModel.isAdult = false + } + } + + SelectButtonView(title: "19세 이상", isChecked: viewModel.isAdult) { + if !viewModel.isAdult { + viewModel.isAdult = true + } + } + } + + Text("성인콘텐츠를 전체관람가로 등록할 시 발생하는 법적 책임은 회사와 상관없이 콘텐츠를 등록한 본인에게 있습니다.\n콘텐츠 내용은 물론 제목도 19금 여부를 체크해 주시기 바랍니다.") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "DD4500")) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 13.3) + } + .padding(.top, 26.7) + .padding(.horizontal, 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) + .padding(.horizontal, 13.3) + + VStack(spacing: 0) { + HStack(alignment: .top, spacing: 0) { + Text("등록") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.white) + .frame(height: 50) + .frame(maxWidth: .infinity) + .background(Color(hex: "9970ff")) + .cornerRadius(10) + .padding(13.3) + } + .frame(maxWidth: .infinity) + .background(Color(hex: "222222")) + .cornerRadius(16.7, corners: [.topLeft, .topRight]) + .onTapGesture { + hideKeyboard() + viewModel.uploadAudioContent() + } + + 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, 30) + } + } + .fileImporter( + isPresented: $isShowSelectAudioView, + allowedContentTypes: [.audio], + allowsMultipleSelection: false + ) { result in + switch result { + case .success(let url): + // Handle selected file URL + viewModel.selectedFileUrl = url[0] + case .failure(let error): + // Handle error if needed + print("File import error: \(error.localizedDescription)") + } + } + + if isShowPhotoPicker { + ImagePicker( + isShowing: $isShowPhotoPicker, + selectedImage: $viewModel.coverImage, + sourceType: .photoLibrary + ) + } + + if viewModel.isShowCompletePopup { + SodaDialog( + title: "콘텐츠 업로드", + desc: "등록한 콘텐츠가 업로드 중입니다.\n" + + "콘텐츠 등록이 완료되면 알림을 보내드립니다.\n" + + "이 페이지를 나가도 콘텐츠는 자동으로 등록됩니다.", + confirmButtonTitle: "확인", + confirmButtonAction: { AppState.shared.back() }, + cancelButtonTitle: "", + cancelButtonAction: {} + ) + } + + GeometryReader { proxy in + VStack { + Spacer() + ContentCreateSelectThemeView( + isShowing: $isShowSelectThemeView, + selectedTheme: $viewModel.theme + ) + .frame(width: proxy.size.width, height: proxy.size.height * 0.9) + .offset(y: isShowSelectThemeView ? 0 : proxy.size.height * 0.9) + .animation(.easeInOut(duration: 0.49), value: self.isShowSelectThemeView) + } + } + .edgesIgnoringSafeArea(.bottom) + } + .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() + } + } + } + } + } + } +} + +struct SelectButtonView: View { + + let title: String + let isChecked: Bool + let action: () -> Void + + var body: some View { + HStack(spacing: 6.7) { + if isChecked { + Image("ic_select_check") + } + + Text(title) + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor(isChecked ? .white : Color(hex: "9970ff")) + } + .frame(height: 48.7) + .frame(maxWidth: .infinity) + .background(isChecked ? Color(hex: "9970ff") : Color(hex: "1f1734")) + .cornerRadius(6.7) + .onTapGesture { + hideKeyboard() + action() + } + } +} + +struct ContentCreateView_Previews: PreviewProvider { + static var previews: some View { + ContentCreateView() + } +} diff --git a/SodaLive/Sources/Content/Create/ContentCreateViewModel.swift b/SodaLive/Sources/Content/Create/ContentCreateViewModel.swift new file mode 100644 index 0000000..7974a48 --- /dev/null +++ b/SodaLive/Sources/Content/Create/ContentCreateViewModel.swift @@ -0,0 +1,216 @@ +// +// ContentCreateViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import UIKit +import Moya +import Combine + +final class ContentCreateViewModel: ObservableObject { + + private let repository = ContentRepository() + private var subscription = Set() + + @Published var isLoading = false + @Published var isShowCompletePopup = false + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var fileName = "" + + @Published var title = "" + @Published var detail = "" + @Published var hashtags: String = "" + @Published var theme: GetAudioContentThemeResponse? = nil + @Published var coverImage: UIImage? = nil + @Published var selectedFileUrl: URL? = nil { + didSet { + if let fileUrl = selectedFileUrl { + fileName = fileUrl.lastPathComponent + } + } + } + + @Published var isAvailableComment = true + @Published var isAdult = false + @Published var priceString = "0" { + didSet { + if priceString.count > 5 { + priceString = String(priceString.prefix(5)) + } else { + if let price = Int(priceString) { + self.price = price + } else { + self.price = 0 + } + } + } + } + @Published var price = 0 + + @Published var isFree = true { + didSet { + if isFree { + priceString = "0" + } + } + } + + var placeholder = "내용을 입력하세요" + + func uploadAudioContent() { + if !isLoading && validateData() { + isLoading = true + + let request = CreateAudioContentRequest( + title: title, + detail: detail, + tags: hashtags, + price: price, + themeId: theme!.id, + 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 coverImage = coverImage, let imageData = coverImage.jpegData(compressionQuality: 0.8) { + multipartData.append( + MultipartFormData( + provider: .data(imageData), + name: "coverImage", + fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg", + mimeType: "image/*") + ) + } else { + errorMessage = "커버이미지를 업로드 하지 못했습니다.\n다시 선택해 주세요" + isShowPopup = true + isLoading = false + return + } + + if let selectedFileUrl = selectedFileUrl { + if selectedFileUrl.startAccessingSecurityScopedResource() { + defer { + selectedFileUrl.stopAccessingSecurityScopedResource() + } + + if let data = try? Data(contentsOf: selectedFileUrl) { + multipartData.append( + MultipartFormData( + provider: .data(data), + name: "contentFile", + fileName: selectedFileUrl.lastPathComponent, + mimeType: "audio/*" + ) + ) + } else { + errorMessage = "콘텐츠 파일을 업로드 하지 못했습니다.\n다시 선택해 주세요" + isShowPopup = true + isLoading = false + return + } + } else { + errorMessage = "콘텐츠 파일을 업로드 하지 못했습니다.\n다시 선택해 주세요" + isShowPopup = true + isLoading = false + return + } + } else { + errorMessage = "콘텐츠 파일을 업로드 하지 못했습니다.\n다시 선택해 주세요" + isShowPopup = true + isLoading = false + return + } + + multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request")) + + repository + .uploadAudioContent(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.isShowCompletePopup = true + } 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 title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + errorMessage = "제목을 입력해 주세요." + isShowPopup = true + return false + } + + if detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || detail.count < 5 { + errorMessage = "내용을 5자 이상 입력해 주세요." + isShowPopup = true + return false + } + + if theme == nil { + errorMessage = "테마를 선택해 주세요." + isShowPopup = true + return false + } + + if coverImage == nil { + errorMessage = "커버이미지를 선택해 주세요." + isShowPopup = true + return false + } + + if selectedFileUrl == nil { + errorMessage = "오디오 콘텐츠를 선택해 주세요." + isShowPopup = true + return false + } + + if !isFree && price < 10 { + errorMessage = "콘텐츠의 최소금액은 10코인 입니다." + isShowPopup = true + return false + } + + return true + } +} diff --git a/SodaLive/Sources/Content/Create/CreateAudioContentRequest.swift b/SodaLive/Sources/Content/Create/CreateAudioContentRequest.swift new file mode 100644 index 0000000..98bc462 --- /dev/null +++ b/SodaLive/Sources/Content/Create/CreateAudioContentRequest.swift @@ -0,0 +1,18 @@ +// +// CreateAudioContentRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct CreateAudioContentRequest: Encodable { + let title: String + let detail: String + let tags: String + let price: Int + let themeId: Int + let isAdult: Bool + let isCommentAvailable: Bool +} diff --git a/SodaLive/Sources/Content/Create/GetAudioContentThemeResponse.swift b/SodaLive/Sources/Content/Create/GetAudioContentThemeResponse.swift new file mode 100644 index 0000000..fde0682 --- /dev/null +++ b/SodaLive/Sources/Content/Create/GetAudioContentThemeResponse.swift @@ -0,0 +1,14 @@ +// +// GetAudioContentThemeResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct GetAudioContentThemeResponse: Decodable, Hashable { + let id: Int + let theme: String + let image: String +} diff --git a/SodaLive/Sources/Content/Detail/Comment/GetAudioContentCommentListResponse.swift b/SodaLive/Sources/Content/Detail/Comment/GetAudioContentCommentListResponse.swift new file mode 100644 index 0000000..0f2095c --- /dev/null +++ b/SodaLive/Sources/Content/Detail/Comment/GetAudioContentCommentListResponse.swift @@ -0,0 +1,24 @@ +// +// GetAudioContentCommentListResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct GetAudioContentCommentListResponse: Decodable { + let totalCount: Int + let items: [GetAudioContentCommentListItem] +} + +struct GetAudioContentCommentListItem: Decodable { + let id: Int + let writerId: Int + let nickname: String + let profileUrl: String + let comment: String + let donationCoin: Int + let date: String + let replyCount: Int +} diff --git a/SodaLive/Sources/Content/Detail/Comment/RegisterAudioContentCommentRequest.swift b/SodaLive/Sources/Content/Detail/Comment/RegisterAudioContentCommentRequest.swift new file mode 100644 index 0000000..fa9e682 --- /dev/null +++ b/SodaLive/Sources/Content/Detail/Comment/RegisterAudioContentCommentRequest.swift @@ -0,0 +1,14 @@ +// +// RegisterAudioContentCommentRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct RegisterAudioContentCommentRequest: Encodable { + let comment: String + let audioContentId: Int + let parentId: Int? +} diff --git a/SodaLive/Sources/Content/Detail/GetAudioContentDetailResponse.swift b/SodaLive/Sources/Content/Detail/GetAudioContentDetailResponse.swift new file mode 100644 index 0000000..3ba61fe --- /dev/null +++ b/SodaLive/Sources/Content/Detail/GetAudioContentDetailResponse.swift @@ -0,0 +1,50 @@ +// +// GetAudioContentDetailResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct GetAudioContentDetailResponse: Decodable { + let contentId: Int + let title: String + let detail: String + let coverImageUrl: String + let contentUrl: String + let themeStr: String + let tag: String + let price: Int + let duration: String + let isAdult: Bool + let isMosaic: Bool + let existOrdered: Bool + let orderType: OrderType? + let remainingTime: String? + let creatorOtherContentList: [OtherContentResponse] + let sameThemeOtherContentList: [OtherContentResponse] + let isCommentAvailable: Bool + let isLike: Bool + let likeCount: Int + let commentList: [GetAudioContentCommentListItem] + let commentCount: Int + let creator: AudioContentCreator +} + +enum OrderType: String, Codable { + case RENTAL, KEEP +} + +struct OtherContentResponse: Decodable { + let contentId: Int + let title: String + let coverUrl: String +} + +struct AudioContentCreator: Decodable { + let creatorId: Int + let nickname: String + let profileImageUrl: String + let isFollowing: Bool +} diff --git a/SodaLive/Sources/Content/Detail/PutAudioContentLikeRequest.swift b/SodaLive/Sources/Content/Detail/PutAudioContentLikeRequest.swift new file mode 100644 index 0000000..652777b --- /dev/null +++ b/SodaLive/Sources/Content/Detail/PutAudioContentLikeRequest.swift @@ -0,0 +1,14 @@ +// +// PutAudioContentLikeRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +struct PutAudioContentLikeRequest: Encodable { + let audioContentId: Int +} + +struct PutAudioContentLikeResponse: Decodable { + let like: Bool +} diff --git a/SodaLive/Sources/Content/Donation/AudioContentDonationRequest.swift b/SodaLive/Sources/Content/Donation/AudioContentDonationRequest.swift new file mode 100644 index 0000000..98bf078 --- /dev/null +++ b/SodaLive/Sources/Content/Donation/AudioContentDonationRequest.swift @@ -0,0 +1,15 @@ +// +// AudioContentDonationRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct AudioContentDonationRequest: Encodable { + let audioContentId: Int + let donationCoin: Int + let comment: String + let container: String = "ios" +} diff --git a/SodaLive/Sources/Content/Main/ContentMainBannerView.swift b/SodaLive/Sources/Content/Main/ContentMainBannerView.swift new file mode 100644 index 0000000..4034716 --- /dev/null +++ b/SodaLive/Sources/Content/Main/ContentMainBannerView.swift @@ -0,0 +1,114 @@ +// +// ContentMainBannerView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Kingfisher + +struct ContentMainBannerView: View { + + let items: [GetAudioContentBannerResponse] + @State private var currentIndex = 0 + @State private var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() + + var body: some View { + VStack(spacing: 0) { + TabView(selection: $currentIndex) { + ForEach(0.. 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + } + .cornerRadius(4.7) + } else { + KFImage(URL(string: item.thumbnailImageUrl)) + .resizable() + .scaledToFill() + .frame( + width: screenSize().width - 26.7, + height: (screenSize().width - 26.7) * 0.53 + ) + .onTapGesture { + switch item.type { + case .EVENT: + AppState.shared.setAppStep(step: .eventDetail(event: item.eventItem!)) + case .CREATOR: + break + case .LINK: + if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + } + .cornerRadius(4.7) + } + } + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + .frame( + width: screenSize().width - 26.7, + height: (screenSize().width - 26.7) * 0.53 + ) + + HStack(spacing: 4) { + ForEach(0.. Void + + @Binding var selectedTheme: String + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: 8) { + ForEach(0.. Void + @Binding var selectedTheme: String + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text("새로운 콘텐츠") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "eeeeee")) + + ContentMainNewContentThemeView(themes: themes, selectTheme: selectTheme, selectedTheme: $selectedTheme) + .padding(.vertical, 16.7) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: 13.3) { + ForEach(0.. 0 { + ContentMainNewContentCreatorView(items: viewModel.newContentUploadCreatorList) + .padding(.bottom, 26.7) + } + + if viewModel.bannerList.count > 0 { + ContentMainBannerView(items: viewModel.bannerList) + .padding(.bottom, 40) + } + + if viewModel.orderList.count > 0 { + ContentMainMyStashView(items: viewModel.orderList) + .padding(.bottom, 40) + } + + ContentMainNewContentView( + themes: viewModel.themeList, + items: viewModel.newContentList, + selectTheme: { viewModel.selectedTheme = $0 }, + selectedTheme: $viewModel.selectedTheme + ) + + if viewModel.curationList.count > 0 { + ContentMainCurationView(items: viewModel.curationList) + .padding(.top, 40) + .padding(.bottom, 20) + } + } + .padding(13.3) + } + + if UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue { + HStack(spacing: 5) { + Image("ic_thumb_play") + .resizable() + .frame(width: 20, height: 20) + + Text("콘텐츠 업로드") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(.white) + } + .padding(13.3) + .background(Color(hex: "9970ff")) + .cornerRadius(44) + .padding(.trailing, 16.7) + .padding(.bottom, 16.7) + .onTapGesture { + AppState.shared.setAppStep(step: .createContent) + } + } + + if viewModel.isLoading { + LoadingView() + } + } + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { + GeometryReader { geo in + HStack { + Spacer() + Text(viewModel.errorMessage) + .padding(.vertical, 13.3) + .padding(.horizontal, 6.7) + .frame(width: geo.size.width - 66.7, alignment: .center) + .font(.custom(Font.medium.rawValue, size: 12)) + .background(Color(hex: "9970ff")) + .foregroundColor(Color.white) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + .onAppear { + viewModel.getMain() + } } } diff --git a/SodaLive/Sources/Content/Main/ContentMainViewModel.swift b/SodaLive/Sources/Content/Main/ContentMainViewModel.swift new file mode 100644 index 0000000..3d1e10a --- /dev/null +++ b/SodaLive/Sources/Content/Main/ContentMainViewModel.swift @@ -0,0 +1,124 @@ +// +// ContentMainViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +import Combine + +final class ContentMainViewModel: ObservableObject { + + private let repository = ContentRepository() + private var subscription = Set() + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isLoading = false + + @Published var newContentUploadCreatorList = [GetNewContentUploadCreator]() + @Published var newContentList = [GetAudioContentMainItem]() + @Published var bannerList = [GetAudioContentBannerResponse]() + @Published var orderList = [GetAudioContentMainItem]() + @Published var themeList = [String]() + @Published var curationList = [GetAudioContentCurationResponse]() + + @Published var selectedTheme = "전체" { + didSet { + getNewContentOfTheme() + } + } + + func getMain() { + isLoading = true + + repository.getMain() + .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) + self.isLoading = false + + if let data = decoded.data, decoded.success { + self.newContentUploadCreatorList.removeAll() + self.newContentList.removeAll() + self.bannerList.removeAll() + self.orderList.removeAll() + self.curationList.removeAll() + self.themeList.removeAll() + + self.newContentUploadCreatorList.append(contentsOf: data.newContentUploadCreatorList) + self.newContentList.append(contentsOf: data.newContentList) + self.bannerList.append(contentsOf: data.bannerList) + self.orderList.append(contentsOf: data.orderList) + self.curationList.append(contentsOf: data.curationList) + + self.themeList.append("전체") + self.themeList.append(contentsOf: data.themeList) + } 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 getNewContentOfTheme() { + repository.getNewContentOfTheme(theme: selectedTheme == "전체" ? "" : selectedTheme) + .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<[GetAudioContentMainItem]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.newContentList.removeAll() + self.newContentList.append(contentsOf: data) + } 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) + } +} diff --git a/SodaLive/Sources/Content/Main/GetAudioContentMainResponse.swift b/SodaLive/Sources/Content/Main/GetAudioContentMainResponse.swift new file mode 100644 index 0000000..011571a --- /dev/null +++ b/SodaLive/Sources/Content/Main/GetAudioContentMainResponse.swift @@ -0,0 +1,51 @@ +// +// GetAudioContentMainResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct GetAudioContentMainResponse: Decodable { + let newContentUploadCreatorList: [GetNewContentUploadCreator] + let bannerList: [GetAudioContentBannerResponse] + let orderList: [GetAudioContentMainItem] + let themeList: [String] + let newContentList: [GetAudioContentMainItem] + let curationList: [GetAudioContentCurationResponse] +} + +struct GetNewContentUploadCreator: Decodable { + let creatorId: Int + let creatorNickname: String + let creatorProfileImageUrl: String +} + +struct GetAudioContentMainItem: Decodable { + let contentId: Int + let coverImageUrl: String + let title: String + let isAdult: Bool + let creatorId: Int + let creatorProfileImageUrl: String + let creatorNickname: String +} + +struct GetAudioContentCurationResponse: Decodable { + let title: String + let description: String + let audioContents: [GetAudioContentMainItem] +} + +struct GetAudioContentBannerResponse: Decodable { + let type: AudioContentBannerType + let thumbnailImageUrl: String + let eventItem: EventItem? + let creatorId: Int? + let link: String? +} + +enum AudioContentBannerType: String, Decodable { + case EVENT, CREATOR, LINK +} diff --git a/SodaLive/Sources/Content/Order/OrderRequest.swift b/SodaLive/Sources/Content/Order/OrderRequest.swift new file mode 100644 index 0000000..90a26b9 --- /dev/null +++ b/SodaLive/Sources/Content/Order/OrderRequest.swift @@ -0,0 +1,14 @@ +// +// OrderRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct OrderRequest: Encodable { + let audioContentId: Int + let orderType: OrderType + let container: String = "ios" +} diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index ba31642..51b0ba9 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -83,6 +83,9 @@ struct ContentView: View { case .serviceCenter: ServiceCenterView() + case .createContent: + ContentCreateView() + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift b/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift new file mode 100644 index 0000000..48c1df5 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift @@ -0,0 +1,25 @@ +// +// GetCreatorProfileResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct GetAudioContentListResponse: Decodable { + let totalCount: Int + let items: [GetAudioContentListItem] +} + +struct GetAudioContentListItem: Decodable { + let contentId: Int + let coverImageUrl: String + let title: String + let price: Int + let themeStr: String + let duration: String? + let likeCount: Int + let commentCount: Int + let isAdult: Bool +}