From a8338e6feaf0da9a578f8cb42fe585a3dcb2e3da Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 11 Aug 2023 18:33:48 +0900 Subject: [PATCH] =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=B1=84=EB=84=90=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../btn_message_send.imageset/Contents.json | 21 + .../btn_message_send.png | Bin 0 -> 978 bytes .../btn_notification.imageset/Contents.json | 21 + .../btn_notification.png | Bin 0 -> 4736 bytes .../Contents.json | 21 + .../btn_notification_selected.png | Bin 0 -> 4873 bytes .../ic_crown.imageset/Contents.json | 21 + .../ic_crown.imageset/ic_crown.png | Bin 0 -> 634 bytes .../ic_crown_1.imageset/Contents.json | 21 + .../ic_crown_1.imageset/ic_crown_1.png | Bin 0 -> 2132 bytes .../ic_crown_2.imageset/Contents.json | 21 + .../ic_crown_2.imageset/ic_crown_2.png | Bin 0 -> 2340 bytes .../ic_crown_3.imageset/Contents.json | 21 + .../ic_crown_3.imageset/ic_crown_3.png | Bin 0 -> 2254 bytes .../Contents.json | 21 + .../ic_seemore_vertical.png | Bin 0 -> 198 bytes SodaLive/Sources/App/AppStep.swift | 12 + .../Common/ActivityViewController.swift | 28 + .../Sources/Content/ContentListItemView.swift | 130 +++++ .../Content/Main/ContentMainBannerView.swift | 4 +- .../Content/Main/ContentMainItemView.swift | 2 +- ...ContentMainNewContentCreatorItemView.swift | 4 +- SodaLive/Sources/ContentView.swift | 12 + SodaLive/Sources/Explorer/ExplorerApi.swift | 64 +- .../Sources/Explorer/ExplorerRepository.swift | 24 + SodaLive/Sources/Explorer/ExplorerView.swift | 7 +- .../Profile/CreatorNoticeWriteView.swift | 72 +++ .../Profile/CreatorNoticeWriteViewModel.swift | 63 ++ .../FanTalk/PostWriteCheersRequest.swift | 19 + .../UserProfileFanTalkCheersItemView.swift | 137 +++++ .../FanTalk/UserProfileFanTalkView.swift | 143 +++++ .../FanTalk/UserProfileFanTalkViewModel.swift | 321 ++++++++++ .../FollowerList/FollowerListItemView.swift | 65 +++ .../FollowerList/FollowerListView.swift | 85 +++ .../FollowerList/FollowerListViewModel.swift | 162 ++++++ .../GetFollowerListResponse.swift | 20 + .../Explorer/Profile/GetCheersResponse.swift | 23 + .../Profile/GetCreatorProfileResponse.swift | 61 ++ .../Explorer/Profile/MemberBlockRequest.swift | 12 + .../Profile/PostCreatorNoticeRequest.swift | 12 + .../UserProfileActivitySummaryView.swift | 88 +++ .../Profile/UserProfileContentView.swift | 74 +++ .../Profile/UserProfileCreatorView.swift | 154 +++++ .../Profile/UserProfileDonationView.swift | 82 +++ .../Profile/UserProfileIntroduceView.swift | 33 ++ .../Profile/UserProfileLiveView.swift | 195 +++++++ .../UserProfileSimilarCreatorView.swift | 55 ++ .../Explorer/Profile/UserProfileView.swift | 304 ++++++++++ .../Profile/UserProfileViewModel.swift | 546 ++++++++++++++++++ SodaLive/Sources/Live/LiveApi.swift | 27 +- SodaLive/Sources/Live/LiveRepository.swift | 12 + .../LiveReservationCompleteView.swift | 203 +++++++ .../MakeLiveReservationRequest.swift | 15 + .../MakeLiveReservationResponse.swift | 19 + .../Room/Detail/GetRoomDetailResponse.swift | 31 + .../Room/EnterOrQuitLiveRoomRequest.swift | 14 + SodaLive/Sources/MyPage/MyPageView.swift | 4 +- .../Report/CheersReportDialogView.swift | 96 +++ .../Sources/Report/CheersReportMenuView.swift | 47 ++ .../Report/ProfileReportDialogView.swift | 57 ++ .../Report/ProfileReportMenuView.swift | 86 +++ SodaLive/Sources/Report/ReportApi.swift | 44 ++ .../Sources/Report/ReportRepository.swift | 19 + SodaLive/Sources/Report/ReportRequest.swift | 21 + .../Report/UserBlockConfirmDialogView.swift | 75 +++ .../Sources/Report/UserReportDialogView.swift | 88 +++ .../Sources/User/CreatorFollowRequest.swift | 12 + SodaLive/Sources/User/UserApi.swift | 30 +- SodaLive/Sources/User/UserRepository.swift | 16 + 69 files changed, 4087 insertions(+), 10 deletions(-) create mode 100644 SodaLive/Resources/Assets.xcassets/btn_message_send.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/btn_message_send.imageset/btn_message_send.png create mode 100644 SodaLive/Resources/Assets.xcassets/btn_notification.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/btn_notification.imageset/btn_notification.png create mode 100644 SodaLive/Resources/Assets.xcassets/btn_notification_selected.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/btn_notification_selected.imageset/btn_notification_selected.png create mode 100644 SodaLive/Resources/Assets.xcassets/ic_crown.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_crown.imageset/ic_crown.png create mode 100644 SodaLive/Resources/Assets.xcassets/ic_crown_1.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_crown_1.imageset/ic_crown_1.png create mode 100644 SodaLive/Resources/Assets.xcassets/ic_crown_2.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_crown_2.imageset/ic_crown_2.png create mode 100644 SodaLive/Resources/Assets.xcassets/ic_crown_3.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_crown_3.imageset/ic_crown_3.png create mode 100644 SodaLive/Resources/Assets.xcassets/ic_seemore_vertical.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_seemore_vertical.imageset/ic_seemore_vertical.png create mode 100644 SodaLive/Sources/Common/ActivityViewController.swift create mode 100644 SodaLive/Sources/Content/ContentListItemView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/CreatorNoticeWriteView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/CreatorNoticeWriteViewModel.swift create mode 100644 SodaLive/Sources/Explorer/Profile/FanTalk/PostWriteCheersRequest.swift create mode 100644 SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkCheersItemView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkViewModel.swift create mode 100644 SodaLive/Sources/Explorer/Profile/FollowerList/FollowerListItemView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/FollowerList/FollowerListView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/FollowerList/FollowerListViewModel.swift create mode 100644 SodaLive/Sources/Explorer/Profile/FollowerList/GetFollowerListResponse.swift create mode 100644 SodaLive/Sources/Explorer/Profile/GetCheersResponse.swift create mode 100644 SodaLive/Sources/Explorer/Profile/MemberBlockRequest.swift create mode 100644 SodaLive/Sources/Explorer/Profile/PostCreatorNoticeRequest.swift create mode 100644 SodaLive/Sources/Explorer/Profile/UserProfileActivitySummaryView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/UserProfileContentView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/UserProfileCreatorView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/UserProfileDonationView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/UserProfileIntroduceView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/UserProfileLiveView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/UserProfileSimilarCreatorView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/UserProfileView.swift create mode 100644 SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift create mode 100644 SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift create mode 100644 SodaLive/Sources/Live/Reservation/MakeLiveReservationRequest.swift create mode 100644 SodaLive/Sources/Live/Reservation/MakeLiveReservationResponse.swift create mode 100644 SodaLive/Sources/Live/Room/EnterOrQuitLiveRoomRequest.swift create mode 100644 SodaLive/Sources/Report/CheersReportDialogView.swift create mode 100644 SodaLive/Sources/Report/CheersReportMenuView.swift create mode 100644 SodaLive/Sources/Report/ProfileReportDialogView.swift create mode 100644 SodaLive/Sources/Report/ProfileReportMenuView.swift create mode 100644 SodaLive/Sources/Report/ReportApi.swift create mode 100644 SodaLive/Sources/Report/ReportRepository.swift create mode 100644 SodaLive/Sources/Report/ReportRequest.swift create mode 100644 SodaLive/Sources/Report/UserBlockConfirmDialogView.swift create mode 100644 SodaLive/Sources/Report/UserReportDialogView.swift create mode 100644 SodaLive/Sources/User/CreatorFollowRequest.swift diff --git a/SodaLive/Resources/Assets.xcassets/btn_message_send.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/btn_message_send.imageset/Contents.json new file mode 100644 index 0000000..192ff33 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/btn_message_send.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "btn_message_send.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/btn_message_send.imageset/btn_message_send.png b/SodaLive/Resources/Assets.xcassets/btn_message_send.imageset/btn_message_send.png new file mode 100644 index 0000000000000000000000000000000000000000..5fc9f6c62a5fba6baa7754c1d9d2b342ae4d9ba5 GIT binary patch literal 978 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 O0000NwhS%3V(LDY_;jeq*v8Si-}aV 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 literal 0 HcmV?d00001 diff --git a/SodaLive/Resources/Assets.xcassets/ic_crown.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_crown.imageset/Contents.json new file mode 100644 index 0000000..3bdb10e --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_crown.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_crown.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_crown.imageset/ic_crown.png b/SodaLive/Resources/Assets.xcassets/ic_crown.imageset/ic_crown.png new file mode 100644 index 0000000000000000000000000000000000000000..003ab3cf837f642c2faccb5536e788f16db6ba67 GIT binary patch literal 634 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 literal 0 HcmV?d00001 diff --git a/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical.imageset/Contents.json new file mode 100644 index 0000000..a3bee2b --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_seemore_vertical.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical.imageset/ic_seemore_vertical.png b/SodaLive/Resources/Assets.xcassets/ic_seemore_vertical.imageset/ic_seemore_vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..bc34f5939f80f9e491898cc6d548bd46864eafe7 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKx3?xrnI^qbVSkfJRf%L|H?mvmFKt5w}kh>GZ zx^prwfgGU#pAc8~0-xQxckh7#AX}qJ^gK|OwIs+dnBkf2rmE`qR((JLM^6{W5R22v z2@*;TCj%lF`?q9gWN^rxUFgsdZEfsvi($EmvPV=yOeRyCaDfzKzaZDNK!aJ=+%DhB fh?4frHeq2nV4~vuscPv*pkWN2u6{1-oD!M<@UTGB literal 0 HcmV?d00001 diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index 753c7cc..edd0611 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -55,4 +55,16 @@ enum AppStep { case serviceCenter case createContent + + case liveReservationComplete(response: MakeLiveReservationResponse) + + case creatorDetail(userId: Int) + + case followerList(userId: Int) + + case userProfileDonationAll(userId: Int) + + case userProfileFanTalkAll(userId: Int) + + case creatorNoticeWrite(notice: String) } diff --git a/SodaLive/Sources/Common/ActivityViewController.swift b/SodaLive/Sources/Common/ActivityViewController.swift new file mode 100644 index 0000000..4e049cf --- /dev/null +++ b/SodaLive/Sources/Common/ActivityViewController.swift @@ -0,0 +1,28 @@ +// +// ActivityViewController.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct ActivityViewController: UIViewControllerRepresentable { + var activityItems: [Any] + var applicationActivities: [UIActivity]? = nil + @Environment(\.presentationMode) var presentationMode + + func makeUIViewController(context: Context) -> some UIActivityViewController { + let controller = UIActivityViewController( + activityItems: activityItems, + applicationActivities: applicationActivities + ) + controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in + self.presentationMode.wrappedValue.dismiss() + } + return controller + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + } +} diff --git a/SodaLive/Sources/Content/ContentListItemView.swift b/SodaLive/Sources/Content/ContentListItemView.swift new file mode 100644 index 0000000..c7a996e --- /dev/null +++ b/SodaLive/Sources/Content/ContentListItemView.swift @@ -0,0 +1,130 @@ +// +// ContentListItemView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Kingfisher + +struct ContentListItemView: View { + + let item: GetAudioContentListItem + + var body: some View { + VStack(spacing: 10) { + HStack(spacing: 0) { + ZStack(alignment: .topLeading) { + KFImage(URL(string: item.coverImageUrl)) + .resizable() + .scaledToFill() + .frame(width: 66.7, height: 66.7, alignment: .top) + .clipped() + .cornerRadius(5.3) + + if item.isAdult { + Text("19") + .font(.custom(Font.bold.rawValue, size: 11.3)) + .foregroundColor(Color.white) + .padding(4) + .background(Color(hex: "e53621")) + .clipShape(Circle()) + .padding(.top, 4.3) + .padding(.leading, 4.3) + } + } + + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 8) { + Text(item.themeStr) + .font(.custom(Font.medium.rawValue, size: 8)) + .foregroundColor(Color(hex: "3bac6a")) + .padding(2.6) + .background(Color(hex: "28312b")) + .cornerRadius(2.6) + + Text(item.duration!) + .font(.custom(Font.medium.rawValue, size: 8)) + .foregroundColor(Color(hex: "777777")) + .padding(2.6) + .background(Color(hex: "222222")) + .cornerRadius(2.6) + } + + Text(item.title) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "d2d2d2")) + .padding(.top, 8) + .padding(.bottom, 10) + + HStack(spacing: 13.3) { + HStack(spacing: 6) { + Image("ic_heart") + .resizable() + .frame(width: 13.3, height: 13.3) + + Text("\(item.likeCount)") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + } + + HStack(spacing: 6) { + Image("ic_message_square_777") + .resizable() + .frame(width: 13.3, height: 13.3) + + Text("\(item.commentCount)") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + } + } + } + .padding(.leading, 10.7) + .padding(.top, 8) + .padding(.bottom, 12) + + Spacer() + + if item.price > 0 { + HStack(spacing: 8) { + Image("ic_coin_w") + .resizable() + .frame(width: 17, height: 17) + + Text("\(item.price)") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "909090")) + } + } else { + Text("무료") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "909090")) + } + } + + Rectangle() + .frame(height: 0.5) + .foregroundColor(Color(hex: "595959")) + } + .frame(maxWidth: .infinity) + } +} + +struct ContentListItemView_Previews: PreviewProvider { + static var previews: some View { + ContentListItemView( + item: GetAudioContentListItem( + contentId: 25, + coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + title: "폭우", + price: 110, + themeStr: "test", + duration: "00:04:43", + likeCount: 2, + commentCount: 0, + isAdult: false + ) + ) + } +} diff --git a/SodaLive/Sources/Content/Main/ContentMainBannerView.swift b/SodaLive/Sources/Content/Main/ContentMainBannerView.swift index 4034716..f8d0624 100644 --- a/SodaLive/Sources/Content/Main/ContentMainBannerView.swift +++ b/SodaLive/Sources/Content/Main/ContentMainBannerView.swift @@ -32,7 +32,7 @@ struct ContentMainBannerView: View { case .EVENT: AppState.shared.setAppStep(step: .eventDetail(event: item.eventItem!)) case .CREATOR: - break + AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId!)) 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) @@ -53,7 +53,7 @@ struct ContentMainBannerView: View { case .EVENT: AppState.shared.setAppStep(step: .eventDetail(event: item.eventItem!)) case .CREATOR: - break + AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId!)) 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) diff --git a/SodaLive/Sources/Content/Main/ContentMainItemView.swift b/SodaLive/Sources/Content/Main/ContentMainItemView.swift index 223e9a1..9bb0f46 100644 --- a/SodaLive/Sources/Content/Main/ContentMainItemView.swift +++ b/SodaLive/Sources/Content/Main/ContentMainItemView.swift @@ -47,7 +47,7 @@ struct ContentMainItemView: View { .scaledToFill() .frame(width: 21.3, height: 21.3) .clipShape(Circle()) - .onTapGesture { } + .onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) } Text(item.creatorNickname) .font(.custom(Font.medium.rawValue, size: 12)) diff --git a/SodaLive/Sources/Content/Main/ContentMainNewContentCreatorItemView.swift b/SodaLive/Sources/Content/Main/ContentMainNewContentCreatorItemView.swift index d9862dc..ebca9cd 100644 --- a/SodaLive/Sources/Content/Main/ContentMainNewContentCreatorItemView.swift +++ b/SodaLive/Sources/Content/Main/ContentMainNewContentCreatorItemView.swift @@ -26,7 +26,9 @@ struct ContentMainNewContentCreatorItemView: View { .frame(width: screenSize().width * 0.18) .lineLimit(1) } - .onTapGesture {} + .onTapGesture { + AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) + } } } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 51b0ba9..c5225d2 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -86,6 +86,18 @@ struct ContentView: View { case .createContent: ContentCreateView() + case .liveReservationComplete(let response): + LiveReservationCompleteView(reservationCompleteData: response) + + case .creatorDetail(let userId): + UserProfileView(userId: userId) + + case .followerList(let userId): + FollowerListView(userId: userId) + + case .creatorNoticeWrite(let notice): + CreatorNoticeWriteView(notice: notice) + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/Explorer/ExplorerApi.swift b/SodaLive/Sources/Explorer/ExplorerApi.swift index 4a19fe8..40c4904 100644 --- a/SodaLive/Sources/Explorer/ExplorerApi.swift +++ b/SodaLive/Sources/Explorer/ExplorerApi.swift @@ -11,6 +11,12 @@ import Moya enum ExplorerApi { case getExplorer case searchChannel(channel: String) + case getCreatorProfile(userId: Int) + case getFollowerList(userId: Int, page: Int, size: Int) + case getCreatorProfileCheers(userId: Int, page: Int, size: Int) + case writeCheers(parentCheersId: Int?, creatorId: Int, content: String) + case modifyCheers(cheersId: Int, content: String) + case writeCreatorNotice(request: PostCreatorNoticeRequest) } extension ExplorerApi: TargetType { @@ -25,13 +31,37 @@ extension ExplorerApi: TargetType { case .searchChannel: return "/explorer/search/channel" + + case .getCreatorProfile(let userId): + return "/explorer/profile/\(userId)" + + case .getFollowerList(let userId, _, _): + return "/explorer/profile/\(userId)/follower-list" + + case .getCreatorProfileCheers(let userId, _, _): + return "/explorer/profile/\(userId)/cheers" + + case .writeCheers: + return "/explorer/profile/cheers" + + case .modifyCheers: + return "/explorer/profile/cheers" + + case .writeCreatorNotice: + return "/explorer/profile/notice" } } var method: Moya.Method { switch self { - case .getExplorer, .searchChannel: + case .getExplorer, .searchChannel, .getCreatorProfile, .getFollowerList, .getCreatorProfileCheers: return .get + + case .writeCheers, .writeCreatorNotice: + return .post + + case .modifyCheers: + return .put } } @@ -42,6 +72,38 @@ extension ExplorerApi: TargetType { case .searchChannel(let channel): return .requestParameters(parameters: ["channel" : channel], encoding: URLEncoding.queryString) + + case .getCreatorProfile: + let parameters = ["timezone": TimeZone.current.identifier] as [String: Any] + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + + case .getFollowerList(_, let page, let size): + let parameters = [ + "page": page - 1, + "size": size + ] as [String : Any] + + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + + case .getCreatorProfileCheers(_, let page, let size): + let parameters = [ + "page": page - 1, + "size": size, + "timezone": TimeZone.current.identifier, + ] as [String : Any] + + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + + case .writeCheers(let parentCheersId, let creatorId, let content): + let request = PostWriteCheersRequest(parentId: parentCheersId, creatorId: creatorId, content: content) + return .requestJSONEncodable(request) + + case .modifyCheers(let cheersId, let content): + let request = PutModifyCheersRequest(cheersId: cheersId, content: content) + return .requestJSONEncodable(request) + + case .writeCreatorNotice(let request): + return .requestJSONEncodable(request) } } diff --git a/SodaLive/Sources/Explorer/ExplorerRepository.swift b/SodaLive/Sources/Explorer/ExplorerRepository.swift index 137edd4..a9ceded 100644 --- a/SodaLive/Sources/Explorer/ExplorerRepository.swift +++ b/SodaLive/Sources/Explorer/ExplorerRepository.swift @@ -20,4 +20,28 @@ final class ExplorerRepository { func searchChannel(channel: String) -> AnyPublisher { return api.requestPublisher(.searchChannel(channel: channel)) } + + func getCreatorProfile(id: Int) -> AnyPublisher { + return api.requestPublisher(.getCreatorProfile(userId: id)) + } + + func getFollowerList(userId: Int, page: Int, size: Int) -> AnyPublisher { + return api.requestPublisher(.getFollowerList(userId: userId, page: page, size: size)) + } + + func getCreatorProfileCheers(userId: Int, page: Int, size: Int) -> AnyPublisher { + return api.requestPublisher(.getCreatorProfileCheers(userId: userId, page: page, size: size)) + } + + func writeCheers(parentCheersId: Int?, creatorId: Int, content: String) -> AnyPublisher { + return api.requestPublisher(.writeCheers(parentCheersId: parentCheersId, creatorId: creatorId, content: content)) + } + + func modifyCheers(cheersId: Int, content: String) -> AnyPublisher { + return api.requestPublisher(.modifyCheers(cheersId: cheersId, content: content)) + } + + func writeCreatorNotice(notice: String) -> AnyPublisher { + return api.requestPublisher(.writeCreatorNotice(request: PostCreatorNoticeRequest(notice: notice))) + } } diff --git a/SodaLive/Sources/Explorer/ExplorerView.swift b/SodaLive/Sources/Explorer/ExplorerView.swift index 48d5d15..d4c1861 100644 --- a/SodaLive/Sources/Explorer/ExplorerView.swift +++ b/SodaLive/Sources/Explorer/ExplorerView.swift @@ -68,7 +68,7 @@ struct ExplorerView: View { } .frame(width: screenSize().width - 26.7) .contentShape(Rectangle()) - .onTapGesture {} + .onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: channel.id)) } } } } else { @@ -136,7 +136,10 @@ struct ExplorerView: View { .padding(.top, 3.3) } .contentShape(Rectangle()) - .onTapGesture {} + .onTapGesture { + AppState.shared + .setAppStep(step: .creatorDetail(userId: creator.id)) + } } } } diff --git a/SodaLive/Sources/Explorer/Profile/CreatorNoticeWriteView.swift b/SodaLive/Sources/Explorer/Profile/CreatorNoticeWriteView.swift new file mode 100644 index 0000000..fd2d478 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorNoticeWriteView.swift @@ -0,0 +1,72 @@ +// +// CreatorNoticeWriteView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Combine + +struct CreatorNoticeWriteView: View { + + let notice: String + + @ObservedObject var viewModel = CreatorNoticeWriteViewModel() + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + VStack(spacing: 0) { + DetailNavigationBar(title: "공지사항 쓰기") + + TextViewWrapper( + text: $viewModel.writeNotice, + placeholder: viewModel.placeholder, + textColorHex: "eeeeee", + backgroundColorHex: "222222", + notice: notice + ) + .frame(width: screenSize().width - 26.7, height: 300) + .padding(.top, 13.3) + + Text("저장") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "ffffff")) + .padding(.vertical, 11.7) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "9970ff")) + .cornerRadius(5.3) + .padding(.top, 20) + .onTapGesture { + hideKeyboard() + viewModel.writeCreatorNotice() + } + + Spacer() + } + } + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { + 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(.leading) + .cornerRadius(20) + .padding(.bottom, 66.7) + Spacer() + } + } + .onTapGesture { + hideKeyboard() + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + viewModel.writeNotice = notice + } + } + } +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorNoticeWriteViewModel.swift b/SodaLive/Sources/Explorer/Profile/CreatorNoticeWriteViewModel.swift new file mode 100644 index 0000000..0ebbbd9 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/CreatorNoticeWriteViewModel.swift @@ -0,0 +1,63 @@ +// +// CreatorNoticeWriteViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation +import Combine + +final class CreatorNoticeWriteViewModel: ObservableObject { + let repository = ExplorerRepository() + private var subscription = Set() + + @Published var isLoading = false + @Published var errorMessage = "" + @Published var isShowPopup = false + + @Published var writeNotice = "" + + var placeholder = "공지사항을 입력해 주세요" + + func writeCreatorNotice() { + isLoading = true + + repository.writeCreatorNotice(notice: writeNotice.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? writeNotice : "") + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { response in + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) + + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + + if decoded.success { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + AppState.shared.back() + } + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + + self.isLoading = false + } + .store(in: &subscription) + } +} diff --git a/SodaLive/Sources/Explorer/Profile/FanTalk/PostWriteCheersRequest.swift b/SodaLive/Sources/Explorer/Profile/FanTalk/PostWriteCheersRequest.swift new file mode 100644 index 0000000..ad95fb6 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/FanTalk/PostWriteCheersRequest.swift @@ -0,0 +1,19 @@ +// +// PostWriteCheersRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct PostWriteCheersRequest: Encodable { + let parentId: Int? + let creatorId: Int + let content: String +} + +struct PutModifyCheersRequest: Encodable { + let cheersId: Int + let content: String +} diff --git a/SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkCheersItemView.swift b/SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkCheersItemView.swift new file mode 100644 index 0000000..3c5454c --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkCheersItemView.swift @@ -0,0 +1,137 @@ +// +// UserProfileFanTalkCheersItemView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Kingfisher + +struct UserProfileFanTalkCheersItemView: View { + + let userId: Int + let cheer: GetCheersResponseItem + let writeCheerReply: (String) -> Void + let modifyCheer: (Int, String) -> Void + let reportPopup: (Int) -> Void + + @State var replyContent: String = "" + @State var isShowInputReply = false + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .top, spacing: 6.7) { + KFImage(URL(string: cheer.profileUrl)) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 33.3, height: 33.3)) + .resizable() + .frame(width: 33.3, height: 33.3) + .clipShape(Circle()) + + VStack(alignment: .leading, spacing: 0) { + Text("\(cheer.nickname)") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Text("\(cheer.date)") + .font(.custom(Font.medium.rawValue, size: 10.7)) + .foregroundColor(Color(hex: "525252")) + .padding(.top, 8.3) + + Text("\(cheer.content)") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "777777")) + .padding(.top, 13.3) + + if isShowInputReply { + HStack(spacing: 10) { + TextField("응원댓글에 답글을 남겨보세요!", text: $replyContent) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(13.3) + .background(Color(hex: "232323")) + .accentColor(Color(hex: "9970ff")) + .keyboardType(.default) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(lineWidth: 1) + .foregroundColor(Color(hex: "9970ff")) + ) + + Text("등록") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "ffffff")) + .padding(13.3) + .background(Color(hex: "9970ff")) + .cornerRadius(6.7) + .onTapGesture { + if cheer.replyList.count > 0 { + modifyCheer(cheer.replyList[0].cheersId, replyContent) + } else { + writeCheerReply(replyContent) + } + } + } + .padding(.top, 10) + } else { + if cheer.replyList.count <= 0 { + if userId == UserDefaults.int(forKey: .userId) { + Text("답글쓰기") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.top, 18.3) + .onTapGesture { + isShowInputReply = true + } + } + } else { + let reply = cheer.replyList[0] + VStack(alignment: .leading, spacing: 8.3) { + Text(reply.content) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "ffffff")) + .frame(minWidth: 100) + .padding(.horizontal, 6.7) + .padding(.vertical, 6.7) + .background(Color(hex: "9970ff").opacity(0.3)) + .cornerRadius(16.7) + .padding(.top, 18.3) + + HStack(spacing: 6.7) { + Text(reply.date) + .font(.custom(Font.medium.rawValue, size: 10.7)) + .foregroundColor(Color(hex: "525252")) + + if userId == UserDefaults.int(forKey: .userId) { + Text("답글 수정") + .font(.custom(Font.medium.rawValue, size: 10.7)) + .foregroundColor(Color(hex: "9970ff")) + .onTapGesture { + self.replyContent = reply.content + isShowInputReply = true + } + } + } + } + } + } + } + + Spacer() + + Image("ic_seemore_vertical") + .onTapGesture { reportPopup(cheer.cheersId) } + } + + Rectangle() + .frame(height: 1) + .foregroundColor(Color(hex: "909090").opacity(0.5)) + .padding(.top, 13.3) + } + .frame(width: screenSize().width - 26.7) + } +} diff --git a/SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkView.swift b/SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkView.swift new file mode 100644 index 0000000..6a4d170 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/FanTalk/UserProfileFanTalkView.swift @@ -0,0 +1,143 @@ +// +// UserProfileFanTalkView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Kingfisher + +struct UserProfileFanTalkView: View { + + @StateObject var viewModel = UserProfileFanTalkViewModel() + + let userId: Int + let cheers: GetCheersResponse + let errorPopup: (String) -> Void + let reportPopup: (Int) -> Void + + @Binding var isLoading: Bool + @State private var cheersContent: String = "" + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 0) { + Text("팬 Talk") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Spacer() + + Text("전체보기") + .font(.custom(Font.light.rawValue, size: 11.3)) + .foregroundColor(Color(hex: "bbbbbb")) + .onTapGesture { + AppState.shared.setAppStep(step: .userProfileFanTalkAll(userId: userId)) + } + } + .padding(.horizontal, 13.3) + + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 6.7) { + Text("응원") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Text("\(cheers.totalCount)") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "777777")) + } + .padding(.top, 20) + + Rectangle() + .frame(height: 1) + .foregroundColor(Color(hex: "909090").opacity(0.5)) + .padding(.top, 13.3) + + HStack(spacing: 0) { + TextField("응원댓글을 입력하세요", text: $cheersContent) + .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() + viewModel.writeCheers(creatorId: userId, cheersContent: cheersContent) + cheersContent = "" + } + } + .background(Color(hex: "232323")) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(lineWidth: 1) + .foregroundColor(Color(hex: "9970ff")) + ) + .padding(.top, 13.3) + + Rectangle() + .frame(height: 1) + .foregroundColor(Color(hex: "909090").opacity(0.5)) + .padding(.top, 13.3) + + VStack(spacing: 20) { + if viewModel.cheersTotalCount > 0 { + ForEach(0.. Void)? + var setLoading: ((Bool) -> Void)? + + private var repository = ExplorerRepository() + private let reportRepository = ReportRepository() + private var subscription = Set() + + var cheersPage = 1 + var pageSize = 10 + + private var isCheersLast = false + + func getCheersList(creatorId: Int) { + if !isCheersLast && !isLoading { + if let setLoading = self.setLoading { + setLoading(true) + } + isLoading = true + + repository.getCreatorProfileCheers(userId: creatorId, page: cheersPage, size: pageSize) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + if let setLoading = self.setLoading { + setLoading(false) + } + 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 { + if !data.cheers.isEmpty { + cheersPage += 1 + self.cheersTotalCount = data.totalCount + self.cheersList.append(contentsOf: data.cheers) + } else { + isCheersLast = true + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + if let errorPopup = errorPopup { + errorPopup(self.errorMessage) + } else { + self.isShowPopup = true + } + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + if let errorPopup = errorPopup { + errorPopup(self.errorMessage) + } else { + self.isShowPopup = true + } + } + } + .store(in: &subscription) + } + } + + func writeCheersReply(parentCheersId: Int, creatorId: Int, cheersReplyContent: String) { + if cheersReplyContent.trimmingCharacters(in: .whitespaces).isEmpty { + if let errorPopup = errorPopup { + errorPopup("내용을 입력하세요") + } else { + errorMessage = "내용을 입력하세요" + isShowPopup = true + } + + return + } + + if let setLoading = self.setLoading { + setLoading(true) + } + isLoading = true + + repository.writeCheers(parentCheersId: parentCheersId, creatorId: creatorId, content: cheersReplyContent) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + if let setLoading = self.setLoading { + setLoading(false) + } + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) + + if decoded.success { + cheersPage = 1 + isCheersLast = false + cheersList.removeAll() + self.getCheersList(creatorId: creatorId) + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + if let errorPopup = errorPopup { + errorPopup(self.errorMessage) + } else { + self.isShowPopup = true + } + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + if let errorPopup = errorPopup { + errorPopup(self.errorMessage) + } else { + self.isShowPopup = true + } + } + } + .store(in: &subscription) + } + + func writeCheers(creatorId: Int, cheersContent: String) { + if cheersContent.trimmingCharacters(in: .whitespaces).isEmpty { + if let errorPopup = errorPopup { + errorPopup("내용을 입력하세요") + } else { + errorMessage = "내용을 입력하세요" + isShowPopup = true + } + + return + } + + if let setLoading = self.setLoading { + setLoading(true) + } + isLoading = true + + repository.writeCheers(parentCheersId: nil, creatorId: creatorId, content: cheersContent) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + if let setLoading = self.setLoading { + setLoading(false) + } + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) + + if decoded.success { + cheersPage = 1 + isCheersLast = false + cheersList.removeAll() + self.getCheersList(creatorId: creatorId) + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + if let errorPopup = errorPopup { + errorPopup(self.errorMessage) + } else { + self.isShowPopup = true + } + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + if let errorPopup = errorPopup { + errorPopup(self.errorMessage) + } else { + self.isShowPopup = true + } + } + } + .store(in: &subscription) + } + + func modifyCheersReply(cheersId: Int, creatorId: Int, cheersReplyContent: String) { + if cheersReplyContent.trimmingCharacters(in: .whitespaces).isEmpty { + if let errorPopup = errorPopup { + errorPopup("내용을 입력하세요") + } else { + errorMessage = "내용을 입력하세요" + isShowPopup = true + } + + return + } + + if let setLoading = self.setLoading { + setLoading(true) + } + isLoading = true + + repository.modifyCheers(cheersId: cheersId, content: cheersReplyContent) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + if let setLoading = self.setLoading { + setLoading(false) + } + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) + + if decoded.success { + cheersPage = 1 + isCheersLast = false + cheersList.removeAll() + self.getCheersList(creatorId: creatorId) + } 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) + } + + func report(type: ReportType, reason: String = "프로필 신고") { + isLoading = true + + let request = ReportRequest(type: type, reason: reason, reportedMemberId: nil, cheersId: reportCheersId > 0 && type == .CHEERS ? reportCheersId : nil, audioContentId: nil) + reportRepository.report(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 + + self.reportCheersId = 0 + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) + + 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/Explorer/Profile/FollowerList/FollowerListItemView.swift b/SodaLive/Sources/Explorer/Profile/FollowerList/FollowerListItemView.swift new file mode 100644 index 0000000..118e011 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/FollowerList/FollowerListItemView.swift @@ -0,0 +1,65 @@ +// +// FollowerListItemView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Kingfisher + +struct FollowerListItemView: View { + + let item: GetFollowerListResponseItem + let creatorFollow: (Int) -> Void + let creatorUnFollow: (Int) -> Void + + var body: some View { + VStack(spacing: 13.3) { + HStack(spacing: 0) { + KFImage(URL(string: item.profileImage)) + .resizable() + .frame(width: 60, height: 60) + .clipShape(Circle()) + + Text(item.nickname) + .font(.custom(Font.medium.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.leading, 13.3) + + Spacer() + + if let isFollow = item.isFollow { + Image(isFollow ? "btn_notification_selected" : "btn_notification") + .onTapGesture { + isFollow ? + creatorUnFollow(item.userId) : + creatorFollow(item.userId) + } + } + } + .padding(.top, 13.3) + + Rectangle() + .frame(height: 1) + .frame(maxWidth: .infinity) + .foregroundColor(Color(hex: "909090")) + } + .padding(.horizontal, 20) + } +} + +struct FollowerListItemView_Previews: PreviewProvider { + static var previews: some View { + FollowerListItemView( + item: GetFollowerListResponseItem( + userId: 1, + profileImage: "https://test-cf.sodalive.net/profile/default-profile.png", + nickname: "상남자", + isFollow: false + ), + creatorFollow: { _ in }, + creatorUnFollow: { _ in } + ) + } +} diff --git a/SodaLive/Sources/Explorer/Profile/FollowerList/FollowerListView.swift b/SodaLive/Sources/Explorer/Profile/FollowerList/FollowerListView.swift new file mode 100644 index 0000000..e2da3fb --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/FollowerList/FollowerListView.swift @@ -0,0 +1,85 @@ +// +// FollowerListView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct FollowerListView: View { + + let userId: Int + @StateObject var viewModel = FollowerListViewModel() + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + VStack(spacing: 0) { + DetailNavigationBar(title: "팔로워 리스트") + + HStack(spacing: 4) { + Text("전체") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Text("\(viewModel.totalCount)") + .font(.custom(Font.medium.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "9970ff")) + + Spacer() + } + .padding(.top, 26.7) + .padding(.horizontal, 13.3) + + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(spacing: 0) { + ForEach(0..() + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isLoading = false + + @Published var totalCount = 0 + @Published var followerListItems = [GetFollowerListResponseItem]() + + var userId: Int = 0 + var page = 1 + var isLast = false + private let pageSize = 10 + + func getFollowerList() { + if page == 1 { + followerListItems.removeAll() + } + + if (!isLast && !isLoading) { + isLoading = true + + repository.getFollowerList(userId: userId, page: page, size: pageSize) + .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 { + if !data.items.isEmpty { + page += 1 + self.totalCount = data.totalCount + self.followerListItems.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 + } + } + .store(in: &subscription) + } + } + + func creatorFollow(userId: Int) { + isLoading = true + + userRepository.creatorFollow(creatorId: userId) + .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.isLast = false + self.page = 1 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.getFollowerList() + } + } 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) + } + + func creatorUnFollow(userId: Int) { + isLoading = true + + userRepository.creatorUnFollow(creatorId: userId) + .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.isLast = false + self.page = 1 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.getFollowerList() + } + } 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/Explorer/Profile/FollowerList/GetFollowerListResponse.swift b/SodaLive/Sources/Explorer/Profile/FollowerList/GetFollowerListResponse.swift new file mode 100644 index 0000000..2162522 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/FollowerList/GetFollowerListResponse.swift @@ -0,0 +1,20 @@ +// +// GetFollowerListResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct GetFollowerListResponse: Decodable { + let totalCount: Int + let items: [GetFollowerListResponseItem] +} + +struct GetFollowerListResponseItem: Decodable { + let userId: Int + let profileImage: String + let nickname: String + let isFollow: Bool? +} diff --git a/SodaLive/Sources/Explorer/Profile/GetCheersResponse.swift b/SodaLive/Sources/Explorer/Profile/GetCheersResponse.swift new file mode 100644 index 0000000..29edc9b --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/GetCheersResponse.swift @@ -0,0 +1,23 @@ +// +// GetCheersResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct GetCheersResponse: Decodable { + let totalCount: Int + let cheers: [GetCheersResponseItem] +} + +struct GetCheersResponseItem: Decodable { + let cheersId: Int + let nickname: String + let profileUrl: String + let content: String + let date: String + let replyList: [GetCheersResponseItem] +} + diff --git a/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift b/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift index 48c1df5..f836c2b 100644 --- a/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift +++ b/SodaLive/Sources/Explorer/Profile/GetCreatorProfileResponse.swift @@ -7,6 +7,60 @@ import Foundation +struct GetCreatorProfileResponse: Decodable { + let creator: CreatorResponse + let userDonationRanking: [UserDonationRankingResponse] + let similarCreatorList: [SimilarCreatorResponse] + let liveRoomList: [LiveRoomResponse] + let notice: String + let cheers: GetCheersResponse + let activitySummary: GetCreatorActivitySummary + let isBlock: Bool +} + +struct CreatorResponse: Decodable { + let creatorId: Int + let profileUrl: String + let nickname: String + let tags: [String] + let introduce: String + let instagramUrl: String? + let youtubeUrl: String? + let websiteUrl: String? + let blogUrl: String? + let isNotification: Bool + let notificationRecipientCount: Int +} + +struct UserDonationRankingResponse: Decodable { + let userId: Int + let nickname: String + let profileImage: String + let donationCoin: Int? +} + +struct SimilarCreatorResponse: Decodable { + let userId: Int + let nickname: String + let profileImage: String + let tags: [String] +} + +struct LiveRoomResponse: Decodable { + let roomId: Int + let title: String + let content: String + let isPaid: Bool + let beginDateTime: String + let coverImageUrl: String + let isAdult: Bool + let price: Int + let channelName: String? + let managerNickname: String + let isReservation: Bool + let isActive: Bool +} + struct GetAudioContentListResponse: Decodable { let totalCount: Int let items: [GetAudioContentListItem] @@ -23,3 +77,10 @@ struct GetAudioContentListItem: Decodable { let commentCount: Int let isAdult: Bool } + +struct GetCreatorActivitySummary: Decodable { + let liveCount: Int + let liveTime: Int + let liveContributorCount: Int + let contentCount: Int +} diff --git a/SodaLive/Sources/Explorer/Profile/MemberBlockRequest.swift b/SodaLive/Sources/Explorer/Profile/MemberBlockRequest.swift new file mode 100644 index 0000000..73477f6 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/MemberBlockRequest.swift @@ -0,0 +1,12 @@ +// +// MemberBlockRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct MemberBlockRequest: Encodable { + let blockMemberId: Int +} diff --git a/SodaLive/Sources/Explorer/Profile/PostCreatorNoticeRequest.swift b/SodaLive/Sources/Explorer/Profile/PostCreatorNoticeRequest.swift new file mode 100644 index 0000000..5d094f1 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/PostCreatorNoticeRequest.swift @@ -0,0 +1,12 @@ +// +// PostCreatorNoticeRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct PostCreatorNoticeRequest: Encodable { + let notice: String +} diff --git a/SodaLive/Sources/Explorer/Profile/UserProfileActivitySummaryView.swift b/SodaLive/Sources/Explorer/Profile/UserProfileActivitySummaryView.swift new file mode 100644 index 0000000..f8c00a9 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/UserProfileActivitySummaryView.swift @@ -0,0 +1,88 @@ +// +// UserProfileActivitySummaryView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct UserProfileActivitySummaryView: View { + + let item: GetCreatorActivitySummary + + var body: some View { + HStack(spacing: 0) { + ActivitySummaryItemView( + title: "라이브\n횟수", + count: String(format: "%d", item.liveCount) + ) + + ActivitySummaryDividerView() + + ActivitySummaryItemView( + title: "라이브\n시간", + count: String(format: "%d", item.liveTime) + ) + + ActivitySummaryDividerView() + + ActivitySummaryItemView( + title: "라이브\n참여자", + count: String(format: "%d", item.liveContributorCount) + ) + + ActivitySummaryDividerView() + + ActivitySummaryItemView( + title: "등록\n콘텐츠", + count: String(format: "%d", item.contentCount) + ) + } + .padding(.vertical, 13.3) + .background(Color(hex: "222222")) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(hex: "9970ff"), lineWidth: 1) + ) + } + + @ViewBuilder + func ActivitySummaryItemView(title: String, count: String) -> some View { + HStack(spacing: 0) { + Spacer() + VStack(spacing: 8) { + Text(title) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "909090")) + .multilineTextAlignment(.center) + + Text(count) + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + } + Spacer() + } + } + + @ViewBuilder + func ActivitySummaryDividerView() -> some View { + Rectangle() + .frame(width: 1, height: 33.3) + .foregroundColor(Color(hex: "9970ff")) + } +} + +struct UserProfileActivitySummaryView_Previews: PreviewProvider { + static var previews: some View { + UserProfileActivitySummaryView( + item: GetCreatorActivitySummary( + liveCount: 1000, + liveTime: 1000, + liveContributorCount: 5000, + contentCount: 30 + ) + ) + } +} diff --git a/SodaLive/Sources/Explorer/Profile/UserProfileContentView.swift b/SodaLive/Sources/Explorer/Profile/UserProfileContentView.swift new file mode 100644 index 0000000..d9de2ee --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/UserProfileContentView.swift @@ -0,0 +1,74 @@ +// +// UserProfileContentView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct UserProfileContentView: View { + + let userId: Int + let items: [GetAudioContentListItem] + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 0) { + Text(userId == UserDefaults.int(forKey: .userId) ? "내 콘텐츠" : "콘텐츠") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Spacer() + + Text("전체보기") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "bbbbbb")) + .onTapGesture {} + } + + if userId == UserDefaults.int(forKey: .userId) { + Text("새로운 콘텐츠 등록하기") + .font(.custom(Font.bold.rawValue, size: 15)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.vertical, 17) + .frame(maxWidth: .infinity) + .background(Color(hex: "9970ff")) + .cornerRadius(5.3) + .padding(.top, 21) + .onTapGesture { AppState.shared.setAppStep(step: .createContent) } + } + + VStack(spacing: 10.7) { + ForEach(0.. Void + let creatorUnFollow: () -> Void + let shareChannel: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 20) { + KFImage(URL(string: creator.profileUrl)) + .resizable() + .scaledToFill() + .frame(width: 90, height: 90) + .background(Color(hex: "3e3358")) + .clipShape(Circle()) + + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 0) { + Text(creator.nickname) + .font(.custom(Font.bold.rawValue, size: 20)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.top, 6.7) + + Spacer() + + Image("btn_big_share") + .resizable() + .frame(width: 33.3, height: 33.3) + .onTapGesture { shareChannel() } + } + + if creator.creatorId == UserDefaults.int(forKey: .userId) { + Text("팔로워 리스트") + .font(.custom(Font.bold.rawValue, size: 12)) + .foregroundColor(Color.white) + .padding(.vertical, 8) + .padding(.horizontal, 16) + .overlay( + RoundedRectangle(cornerRadius: 16.7) + .stroke(Color(hex: "9970ff"), lineWidth: 1) + ) + .padding(.top, 13.3) + .onTapGesture { + AppState.shared.setAppStep(step: .followerList(userId: creator.creatorId)) + } + } else { + VStack(alignment: .leading, spacing: 9.3) { + Image(creator.isNotification ? "btn_notification_selected" : "btn_notification") + .resizable() + .frame(width: 83.3, height: 26.7) + .onTapGesture { + if creator.isNotification { + creatorUnFollow() + } else { + creatorFollow() + } + } + + Text("팔로워 \(creator.notificationRecipientCount)명") + .font(.custom(Font.light.rawValue, size: 12)) + .foregroundColor(Color(hex: "bbbbbb")) + } + .padding(.top, 13.3) + } + } + } + + Text(creator.tags.map { "#\($0)" }.joined(separator: " ")) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.top, creator.tags.count > 0 ? 13.3 : 0) + + HStack(spacing: 10) { + Spacer() + + if let websiteUrl = creator.websiteUrl, websiteUrl.count > 0, let url = URL(string: websiteUrl), UIApplication.shared.canOpenURL(url) { + Button(action: {UIApplication.shared.open(url)}) { + Image("ic_website_circle") + .resizable() + .frame(width: 40, height: 40) + } + .padding(.top, 13.3) + } + + if let blogUrl = creator.blogUrl, blogUrl.count > 0, let url = URL(string: blogUrl), UIApplication.shared.canOpenURL(url) { + Button(action: {UIApplication.shared.open(url)}) { + Image("ic_blog_circle") + .resizable() + .frame(width: 40, height: 40) + } + .padding(.top, 13.3) + } + + if let instagramUrl = creator.instagramUrl, instagramUrl.count > 0, let url = URL(string: instagramUrl), UIApplication.shared.canOpenURL(url) { + Button(action: {UIApplication.shared.open(url)}) { + Image("ic_instagram_circle") + .resizable() + .frame(width: 40, height: 40) + } + .padding(.top, 13.3) + } + + if let youtubeUrl = creator.youtubeUrl, youtubeUrl.count > 0, let url = URL(string: youtubeUrl), UIApplication.shared.canOpenURL(url) { + Button(action: {UIApplication.shared.open(url)}) { + Image("ic_youtube_circle") + .resizable() + .frame(width: 40, height: 40) + } + .padding(.top, 13.3) + } + } + } + .padding(20) + .background(Color(hex: "222222")) + .cornerRadius(16.7) + .padding(.top, 20) + .padding(.horizontal, 13.3) + } +} + +struct UserProfileCreatorView_Previews: PreviewProvider { + static var previews: some View { + UserProfileCreatorView( + creator: CreatorResponse( + creatorId: 2, + profileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + nickname: "수다친구1", + tags: ["썸", "연애", "부부"], + introduce: "상담사1 입니다.yyyyyyy\n\n\n\n\n\n\njgdgjdgjdgicyifyicyi\n\n\n\n\n\n\n\n\n\n\n\n", + instagramUrl: Optional("3x2tfZnfLRo"), + youtubeUrl: Optional("https://www.youtube.com/watch?v=3x2tfZnfLRo"), + websiteUrl: Optional("https://instagram.com/dear.zia"), + blogUrl: Optional("dear.zia"), + isNotification: false, + notificationRecipientCount: 2 + ) + ) { + } creatorUnFollow: { + } shareChannel: { + } + } +} diff --git a/SodaLive/Sources/Explorer/Profile/UserProfileDonationView.swift b/SodaLive/Sources/Explorer/Profile/UserProfileDonationView.swift new file mode 100644 index 0000000..6bae502 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/UserProfileDonationView.swift @@ -0,0 +1,82 @@ +// +// UserProfileDonationView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Kingfisher + +struct UserProfileDonationView: View { + + let userId: Int + let donationRankingResponse: [UserDonationRankingResponse] + let rankingCrawns = ["ic_crown_1", "ic_crown_2", "ic_crown_3"] + let rankingColors = [ + [Color(hex: "ffdc00"), Color(hex: "ffb600")], + [Color(hex: "ffffff"), Color(hex: "9f9f9f")], + [Color(hex: "e6a77a"), Color(hex: "c67e4a")], + [Color(hex: "ffffff").opacity(0), Color(hex: "ffffff").opacity(0)] + ] + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 0) { + Text("후원랭킹") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Spacer() + + Text("전체보기") + .font(.custom(Font.light.rawValue, size: 11.3)) + .foregroundColor(Color(hex: "bbbbbb")) + .onTapGesture {} + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 13.3) { + ForEach(0.. Void + let onClickReservation: (LiveRoomResponse) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 26.7) { + + HStack(spacing: 0) { + Text("라이브") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + } + + VStack(spacing: 13.3) { + ForEach(0.. 0 ? "\(liveRoom.price)코인으로 " : "")예약하기") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "000000")) + .frame( + width: screenSize().width - 26.7 - 100, + height: 36.7 + ) + .background(Color(hex: "fdca2f")) + .cornerRadius(5.3) + .onTapGesture { + onClickReservation(liveRoom) + } + } + } + } else { + Text("다시듣기를 지원하지 않습니다") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + .frame( + width: screenSize().width - 26.7 - 100, + height: 36.7 + ) + .background(Color(hex: "525252")) + .cornerRadius(5.3) + } + } + } + .frame(height: 116.7) + + Rectangle() + .frame(height: 1) + .foregroundColor(Color(hex: "909090").opacity(0.5)) + } + } + } + } + } +} diff --git a/SodaLive/Sources/Explorer/Profile/UserProfileSimilarCreatorView.swift b/SodaLive/Sources/Explorer/Profile/UserProfileSimilarCreatorView.swift new file mode 100644 index 0000000..ac973ac --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/UserProfileSimilarCreatorView.swift @@ -0,0 +1,55 @@ +// +// UserProfileSimilarCreatorView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import Kingfisher + +struct UserProfileSimilarCreatorView: View { + + let creators: [SimilarCreatorResponse] + let onClickCreator: (Int) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 26.7) { + Text("함께 들으면 좋은 채널") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + VStack(spacing: 10) { + ForEach(0.. 0 { + UserProfileLiveView( + userId: userId, + liveRoomList: creatorProfile.liveRoomList, + onClickParticipant: { liveRoom in + if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) { + viewModel.errorMessage = "현재 라이브 중입니다." + viewModel.isShowPopup = true + } else { + AppState.shared.isShowPlayer = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + viewModel.enterLiveRoom(roomId: liveRoom.roomId) + } + } + }, + onClickReservation: { liveRoom in + if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) { + viewModel.errorMessage = "내가 만든 라이브는 예약할 수 없습니다." + viewModel.isShowPopup = true + } else { + viewModel.reservationLiveRoom(roomId: liveRoom.roomId) + } + } + ) + .padding(.top, 46.7) + .padding(.horizontal, 13.3) + } + + VStack(spacing: 26.7) { + let introduce = creatorProfile.creator.introduce + UserProfileIntroduceView( + introduce: introduce.trimmingCharacters(in: .whitespaces).count <= 0 ? + "채널 소개내용이 없습니다." : + introduce) + + Rectangle() + .frame(height: 1) + .foregroundColor(Color(hex: "909090").opacity(0.5)) + .padding(.horizontal, 13.3) + } + .padding(.top, 26.7) + + if creatorProfile.userDonationRanking.count > 0 { + VStack(spacing: 26.7) { + UserProfileDonationView(userId: userId, donationRankingResponse: creatorProfile.userDonationRanking) + .padding(.horizontal, 13.3) + + Rectangle() + .frame(height: 6.7) + .foregroundColor(Color(hex: "909090").opacity(0.5)) + } + .padding(.top, 26.7) + } + + VStack(spacing: 26.7) { + UserProfileSimilarCreatorView( + creators: creatorProfile.similarCreatorList, + onClickCreator: { viewModel.getCreatorProfile(userId: $0) } + ) + .padding(.horizontal, 13.3) + + Rectangle() + .frame(height: 6.7) + .foregroundColor(Color(hex: "909090").opacity(0.5)) + } + .padding(.top, 26.7) + + UserProfileFanTalkView( + userId: userId, + cheers: creatorProfile.cheers, + errorPopup: { message in + viewModel.errorMessage = message + viewModel.isShowPopup = true + }, + reportPopup: { cheerId in + viewModel.reportCheersId = cheerId + viewModel.isShowCheersReportMenu = true + }, + isLoading: $viewModel.isLoading + ) + .padding(.top, 26.7) + } + } + } + } + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { + 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(.leading) + .cornerRadius(20) + .padding(.bottom, 66.7) + Spacer() + } + } + + ZStack { + if viewModel.isShowPaymentDialog { + SodaDialog( + title: viewModel.paymentDialogTitle, + desc: viewModel.paymentDialogDesc, + confirmButtonTitle: viewModel.paymentDialogConfirmTitle, + confirmButtonAction: viewModel.paymentDialogConfirmAction, + cancelButtonTitle: viewModel.paymentDialogCancelTitle, + cancelButtonAction: viewModel.hidePaymentPopup + ) + } + + if viewModel.isShowPasswordDialog { + LiveRoomPasswordDialog( + isShowing: $viewModel.isShowPasswordDialog, + can: viewModel.secretOrPasswordDialogCan, + confirmAction: viewModel.passwordDialogConfirmAction + ) + } + + if viewModel.isShowCheersReportMenu { + VStack(spacing: 0) { + CheersReportMenuView( + isShowing: $viewModel.isShowCheersReportMenu, + onClickReport: { viewModel.isShowCheersReportView = true } + ) + + if proxy.safeAreaInsets.bottom > 0 { + Rectangle() + .foregroundColor(Color(hex: "222222")) + .frame(width: proxy.size.width, height: 15.3) + } + } + .ignoresSafeArea() + } + + if viewModel.isShowCheersReportView { + CheersReportDialogView( + isShowing: $viewModel.isShowCheersReportView, + confirmAction: { reason in + viewModel.report(type: .CHEERS, reason: reason) + } + ) + } + + if let creatorProfile = viewModel.creatorProfile, viewModel.isShowReportMenu { + VStack(spacing: 0) { + ProfileReportMenuView( + isShowing: $viewModel.isShowReportMenu, + isBlockedUser: creatorProfile.isBlock, + userBlockAction: { viewModel.isShowUesrBlockConfirm = true }, + userUnBlockAction: { viewModel.userUnBlock(userId: userId) }, + userReportAction: { viewModel.isShowUesrReportView = true }, + profileReportAction: { viewModel.isShowProfileReportConfirm = true } + ) + + if proxy.safeAreaInsets.bottom > 0 { + Rectangle() + .foregroundColor(Color(hex: "222222")) + .frame(width: proxy.size.width, height: 15.3) + } + } + .ignoresSafeArea() + } + + if let creatorProfile = viewModel.creatorProfile, + viewModel.isShowUesrBlockConfirm { + UserBlockConfirmDialogView( + isShowing: $viewModel.isShowUesrBlockConfirm, + nickname: creatorProfile.creator.nickname, + confirmAction: { viewModel.userBlock(userId: userId) } + ) + } + + if viewModel.isShowUesrReportView { + UserReportDialogView( + isShowing: $viewModel.isShowUesrReportView, + confirmAction: { reason in + viewModel.report(type: .USER, userId: userId, reason: reason) + } + ) + } + + if viewModel.isShowProfileReportConfirm { + ProfileReportDialogView( + isShowing: $viewModel.isShowProfileReportConfirm, + confirmAction: { + viewModel.report(type: .PROFILE, userId: userId) + } + ) + } + } + } + .sheet( + isPresented: $viewModel.isShowShareView, + onDismiss: { viewModel.shareMessage = "" }, + content: { + ActivityViewController(activityItems: [viewModel.shareMessage]) + } + ) + .onAppear { + viewModel.getCreatorProfile(userId: userId) + AppState.shared.pushChannelId = 0 + } + } + } +} + +struct UserProfileView_Previews: PreviewProvider { + static var previews: some View { + UserProfileView(userId: 0) + } +} diff --git a/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift b/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift new file mode 100644 index 0000000..8bf5ba7 --- /dev/null +++ b/SodaLive/Sources/Explorer/Profile/UserProfileViewModel.swift @@ -0,0 +1,546 @@ +// +// UserProfileViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation +import Combine + +import FirebaseDynamicLinks + +final class UserProfileViewModel: ObservableObject { + + private var repository = ExplorerRepository() + private let liveRepository = LiveRepository() + private let reportRepository = ReportRepository() + private let userRepository = UserRepository() + private var subscription = Set() + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isLoading = false + + @Published var isExpandNotice = false + + @Published var paymentDialogTitle = "" + @Published var paymentDialogDesc = "" + @Published var isShowPaymentDialog = false + @Published var paymentDialogConfirmAction = {} + @Published var paymentDialogConfirmTitle = "" + + @Published var secretDialogManagerNickname = "" + @Published var secretDialogConfirmAction = {} + @Published var isShowSecretDialog = false + + @Published var secretOrPasswordDialogCan = 0 + + @Published var passwordDialogConfirmAction: (Int) -> Void = { _ in } + @Published var isShowPasswordDialog = false + + @Published var navigationTitle = "채널" + + @Published private(set) var creatorProfile: GetCreatorProfileResponse? + + @Published var isShowShareView = false + @Published var shareMessage = "" + + @Published var isShowReportMenu = false + @Published var isShowUesrBlockConfirm = false + @Published var isShowUesrReportView = false + @Published var isShowProfileReportConfirm = false + + @Published var reportCheersId = 0 + @Published var isShowCheersReportMenu = false + @Published var isShowCheersReportView = false + + + let paymentDialogCancelTitle = "취소" + + func getCreatorProfile(userId: Int) { + creatorProfile = nil + isLoading = true + + repository.getCreatorProfile(id: userId) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.creatorProfile = data + self.navigationTitle = "\(data.creator.nickname)님의 채널" + } 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 hidePaymentPopup() { + isShowPaymentDialog = false + isShowSecretDialog = false + isShowPasswordDialog = false + + paymentDialogTitle = "" + paymentDialogDesc = "" + paymentDialogConfirmAction = {} + + secretOrPasswordDialogCan = 0 + secretDialogManagerNickname = "" + secretDialogConfirmAction = {} + + passwordDialogConfirmAction = { _ in } + } + + func reservationLiveRoom(roomId: Int) { + getRoomDetail(roomId: roomId) { [unowned self] in + if ($0.manager.id == UserDefaults.int(forKey: .userId)) { + self.errorMessage = "내가 만든 라이브는 예약할 수 없습니다." + self.isShowPopup = true + } else { + if $0.isPrivateRoom { + self.passwordDialogConfirmAction = { password in + self.reservation(roomId: roomId, password: password) + } + self.isShowPasswordDialog = true + } else { + if ($0.price == 0 || $0.isPaid) { + self.reservation(roomId: roomId) + } else { + self.paymentDialogTitle = "\($0.price)코인으로 예약" + self.paymentDialogDesc = "'\($0.title)' 라이브에 참여하기 위해 결제합니다." + self.paymentDialogConfirmTitle = "결제 후 예약하기" + self.paymentDialogConfirmAction = { [unowned self] in + hidePaymentPopup() + reservation(roomId: roomId) + } + self.isShowPaymentDialog = true + } + } + } + } + } + + private func reservation(roomId: Int, password: Int? = nil) { + isLoading = true + let request = MakeLiveReservationRequest(roomId: roomId, password: password) + liveRepository.makeReservation(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(ApiResponse.self, from: responseData) + + if let response = decoded.data, decoded.success { + AppState.shared.setAppStep(step: .liveReservationComplete(response: response)) + } 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) + } + + func enterLiveRoom(roomId: Int) { + getRoomDetail(roomId: roomId) { + if let _ = $0.channelName { + if $0.manager.id == UserDefaults.int(forKey: .userId) { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) { + self.enterRoom(roomId: roomId) + } + } else if ($0.price == 0 || $0.isPaid) { + if $0.isSecretRoom { + self.secretDialogManagerNickname = $0.manager.nickname + self.secretDialogConfirmAction = { + self.enterRoom(roomId: roomId) + } + self.secretOrPasswordDialogCan = 0 + self.isShowSecretDialog = true + } else if $0.isPrivateRoom { + self.passwordDialogConfirmAction = { password in + self.enterRoom(roomId: roomId, password: password) + } + self.isShowPasswordDialog = true + } else { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) { + self.enterRoom(roomId: roomId) + } + } + } else { + if $0.isSecretRoom { + self.secretDialogManagerNickname = $0.manager.nickname + self.secretDialogConfirmAction = { + self.enterRoom(roomId: roomId) + } + self.secretOrPasswordDialogCan = $0.price + self.isShowSecretDialog = true + } else if $0.isPrivateRoom { + self.secretOrPasswordDialogCan = $0.price + self.passwordDialogConfirmAction = { password in + self.enterRoom(roomId: roomId, password: password) + } + self.isShowPasswordDialog = true + } else { + self.paymentDialogTitle = "\($0.price)코인으로 입장" + self.paymentDialogDesc = "'\($0.title)' 라이브에 참여하기 위해 결제합니다." + self.paymentDialogConfirmTitle = "결제 후 참여하기" + self.paymentDialogConfirmAction = { [unowned self] in + hidePaymentPopup() + self.enterRoom(roomId: roomId) + } + self.isShowPaymentDialog = true + } + } + } + } + } + + func enterRoom(roomId: Int, onSuccess: (() -> Void)? = nil, password: Int? = nil) { + isLoading = true + let request = EnterOrQuitLiveRoomRequest(roomId: roomId, password: password) + liveRepository.enterRoom(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 { + AppState.shared.roomId = roomId + + if let onSuccess = onSuccess { + onSuccess() + } else { + if roomId > 0 { + AppState.shared.isShowPlayer = true + AppState.shared.setAppStep(step: .main) + } + } + } 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) + } + + func creatorFollow() { + if let creator = creatorProfile { + isLoading = true + + userRepository.creatorFollow(creatorId: creator.creator.creatorId) + .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.getCreatorProfile(userId: creator.creator.creatorId) + } 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) + } + } + + func creatorUnFollow() { + if let creator = creatorProfile { + isLoading = true + + userRepository.creatorUnFollow(creatorId: creator.creator.creatorId) + .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.getCreatorProfile(userId: creator.creator.creatorId) + } 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) + } + } + + private func getRoomDetail(roomId: Int, onSuccess: @escaping (GetRoomDetailResponse) -> Void) { + isLoading = true + liveRepository.getRoomDetail(roomId: roomId) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { 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 { + onSuccess(data) + } 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 shareChannel(userId: Int) { + guard let link = URL(string: "https://yozm.day/?channel_id=\(userId)") else { return } + let dynamicLinksDomainURIPrefix = "https://yozm.page.link" + guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else { + self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요." + self.isShowPopup = true + return + } + + linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "kr.co.vividnext.yozm") + linkBuilder.iOSParameters?.appStoreID = "1630284226" + + linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "kr.co.vividnext.yozm") + + guard let longDynamicLink = linkBuilder.url else { + self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요." + self.isShowPopup = true + return + } + DEBUG_LOG("The long URL is: \(longDynamicLink)") + + DynamicLinkComponents.shortenURL(longDynamicLink, options: nil) { [unowned self] url, warnings, error in + let shortUrl = url?.absoluteString + let urlString = shortUrl != nil ? shortUrl! : longDynamicLink.absoluteString + + self.shareMessage = "요즘라이브 \(self.creatorProfile!.creator.nickname)님의 채널입니다.\n\(urlString)" + self.isShowShareView = true + } + } + + func userBlock(userId: Int) { + isLoading = true + userRepository.memberBlock(userId: userId) + .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 { + getCreatorProfile(userId: userId) + self.errorMessage = "차단하였습니다." + } 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) + } + + func userUnBlock(userId: Int) { + isLoading = true + userRepository.memberUnBlock(userId: userId) + .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 { + getCreatorProfile(userId: userId) + self.errorMessage = "차단이 해제 되었습니다." + } 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) + } + + func report(type: ReportType, userId: Int? = nil, reason: String = "프로필 신고") { + isLoading = true + + let request = ReportRequest(type: type, reason: reason, reportedMemberId: userId, cheersId: reportCheersId > 0 && type == .CHEERS ? reportCheersId : nil, audioContentId: nil) + reportRepository.report(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 + + self.reportCheersId = 0 + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) + + 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/Live/LiveApi.swift b/SodaLive/Sources/Live/LiveApi.swift index 3aaefa8..f0fc69e 100644 --- a/SodaLive/Sources/Live/LiveApi.swift +++ b/SodaLive/Sources/Live/LiveApi.swift @@ -14,6 +14,9 @@ enum LiveApi { case getReservations(isActive: Bool) case getReservation(reservationId: Int) case cancelReservation(request: CancelLiveReservationRequest) + case getRoomDetail(roomId: Int) + case makeReservation(request: MakeLiveReservationRequest) + case enterRoom(request: EnterOrQuitLiveRoomRequest) } extension LiveApi: TargetType { @@ -37,14 +40,26 @@ extension LiveApi: TargetType { case .cancelReservation: return "/live/reservation/cancel" + + case .getRoomDetail(let roomId): + return "/live/room/detail/\(roomId)" + + case .makeReservation: + return "/live/reservation" + + case .enterRoom: + return "/live/room/enter" } } var method: Moya.Method { switch self { - case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation: + case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation, .getRoomDetail: return .get + case .makeReservation, .enterRoom: + return .post + case .cancelReservation: return .put } @@ -92,6 +107,16 @@ extension LiveApi: TargetType { case .cancelReservation(let request): return .requestJSONEncodable(request) + + case .getRoomDetail: + let parameters = ["timezone": TimeZone.current.identifier] as [String: Any] + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + + case .makeReservation(let request): + return .requestJSONEncodable(request) + + case .enterRoom(let request): + return .requestJSONEncodable(request) } } diff --git a/SodaLive/Sources/Live/LiveRepository.swift b/SodaLive/Sources/Live/LiveRepository.swift index d5d6566..2f83900 100644 --- a/SodaLive/Sources/Live/LiveRepository.swift +++ b/SodaLive/Sources/Live/LiveRepository.swift @@ -32,4 +32,16 @@ final class LiveRepository { func cancelReservation(reservationId: Int, reason: String) -> AnyPublisher { return api.requestPublisher(.cancelReservation(request: CancelLiveReservationRequest(reservationId: reservationId, reason: reason))) } + + func getRoomDetail(roomId: Int) -> AnyPublisher { + return api.requestPublisher(.getRoomDetail(roomId: roomId)) + } + + func makeReservation(request: MakeLiveReservationRequest) -> AnyPublisher { + return api.requestPublisher(.makeReservation(request: request)) + } + + func enterRoom(request: EnterOrQuitLiveRoomRequest) -> AnyPublisher { + return api.requestPublisher(.enterRoom(request: request)) + } } diff --git a/SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift b/SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift new file mode 100644 index 0000000..5d4d011 --- /dev/null +++ b/SodaLive/Sources/Live/Reservation/Complete/LiveReservationCompleteView.swift @@ -0,0 +1,203 @@ +// +// LiveReservationCompleteView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct LiveReservationCompleteView: View { + + let reservationCompleteData: MakeLiveReservationResponse + + var body: some View { + BaseView { + VStack(spacing: 0) { + DetailNavigationBar(title: "라이브 예약 완료") { + AppState.shared.setAppStep(step: .main) + } + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + Text("예약이 완료되었습니다.") + .font(.custom(Font.bold.rawValue, size: 20)) + .foregroundColor(Color(hex: "a285eb")) + .frame(width: screenSize().width - 26.7, alignment: .leading) + .padding(.top, 20) + + Image("img_compleate_book") + .resizable() + .scaledToFit() + .frame(width: 233.25) + .padding(.top, 16.7) + .padding(.bottom, 26.7) + + Text("라이브 예약정보") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 53.4, alignment: .leading) + + VStack(spacing: 6.7) { + HStack(spacing: 26.7) { + Text("채널") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "777777")) + + Text(reservationCompleteData.nickname) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + } + .frame(width: screenSize().width - 53.4, alignment: .leading) + + HStack(spacing: 26.7) { + Text("구매내역") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "777777")) + + Text(reservationCompleteData.title) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + } + .frame(width: screenSize().width - 53.4, alignment: .leading) + + HStack(spacing: 26.7) { + Text("예약일자") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "777777")) + + Text(reservationCompleteData.beginDateString) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + } + .frame(width: screenSize().width - 53.4, alignment: .leading) + + HStack(spacing: 26.7) { + Text("라이브 비용") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "777777")) + + Text(reservationCompleteData.price) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + } + .frame(width: screenSize().width - 53.4, alignment: .leading) + } + .padding(.top, 16.7) + + Rectangle() + .frame(width: screenSize().width, height: 6.7) + .foregroundColor(Color(hex: "232323")) + .padding(.vertical, 20) + + Text("결제정보") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 53.4, alignment: .leading) + + VStack(spacing: 13.3) { + HStack(spacing: 0) { + Text("보유코인") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "777777")) + + Spacer() + + Text("\(reservationCompleteData.haveCan)") + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Text(" 코인") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + } + .frame(width: screenSize().width - 53.4, alignment: .leading) + + HStack(spacing: 0) { + Text("결제코인") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "777777")) + + Spacer() + + Text("\(reservationCompleteData.useCan)") + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Text(" 코인") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + } + .frame(width: screenSize().width - 53.4, alignment: .leading) + + HStack(spacing: 0) { + Text("잔여코인") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "777777")) + + Spacer() + + Text("\(reservationCompleteData.remainingCan)") + .font(.custom(Font.bold.rawValue, size: 15.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Text(" 코인") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + } + .frame(width: screenSize().width - 53.4, alignment: .leading) + } + .padding(.top, 20) + + HStack(spacing: 13.3) { + Text("홈으로 이동") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.vertical, 16) + .frame(width: (screenSize().width - 40) / 2) + .background(Color(hex: "9970ff").opacity(0.2)) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(lineWidth: 1) + .foregroundColor(Color(hex: "9970ff")) + ) + .onTapGesture { + AppState.shared.setAppStep(step: .main) + } + + Text("예약 내역 이동") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(.white) + .padding(.vertical, 16) + .frame(width: (screenSize().width - 40) / 2) + .background(Color(hex: "9970ff")) + .cornerRadius(10) + .onTapGesture { + AppState.shared.setAppStep(step: .liveReservation) + } + } + .padding(.vertical, 26.7) + } + } + } + } + } +} + +struct LiveReservationCompleteView_Previews: PreviewProvider { + static var previews: some View { + LiveReservationCompleteView( + reservationCompleteData: MakeLiveReservationResponse( + reservationId: 10, + nickname: "김상담", + title: "여자들이 좋아하는 남자 스타일은?", + beginDateString: "2021년 7월 9일 (금), 오후 02:00", + price: "무료", + haveCan: 100, + useCan: 0, + remainingCan: 100 + ) + ) + } +} diff --git a/SodaLive/Sources/Live/Reservation/MakeLiveReservationRequest.swift b/SodaLive/Sources/Live/Reservation/MakeLiveReservationRequest.swift new file mode 100644 index 0000000..21dc70e --- /dev/null +++ b/SodaLive/Sources/Live/Reservation/MakeLiveReservationRequest.swift @@ -0,0 +1,15 @@ +// +// MakeLiveReservationRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct MakeLiveReservationRequest: Encodable { + let roomId: Int + let password: Int? + let container: String = "ios" + let timezone: String = TimeZone.current.identifier +} diff --git a/SodaLive/Sources/Live/Reservation/MakeLiveReservationResponse.swift b/SodaLive/Sources/Live/Reservation/MakeLiveReservationResponse.swift new file mode 100644 index 0000000..06da5ff --- /dev/null +++ b/SodaLive/Sources/Live/Reservation/MakeLiveReservationResponse.swift @@ -0,0 +1,19 @@ +// +// MakeLiveReservationResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct MakeLiveReservationResponse: Decodable { + let reservationId: Int + let nickname: String + let title: String + let beginDateString: String + let price: String + let haveCan: Int + let useCan: Int + let remainingCan: Int +} diff --git a/SodaLive/Sources/Live/Room/Detail/GetRoomDetailResponse.swift b/SodaLive/Sources/Live/Room/Detail/GetRoomDetailResponse.swift index 62cb9e1..ff631bc 100644 --- a/SodaLive/Sources/Live/Room/Detail/GetRoomDetailResponse.swift +++ b/SodaLive/Sources/Live/Room/Detail/GetRoomDetailResponse.swift @@ -7,6 +7,37 @@ import Foundation +struct GetRoomDetailResponse: Decodable { + let roomId: Int + let price: Int + let title: String + let content: String + let isPaid: Bool + let isPrivateRoom: Bool + let isSecretRoom: Bool + let password: Int? + let tags: [String] + let channelName: String? + let beginDateTime: String + let isNotification: Bool + let numberOfParticipants: Int + let numberOfParticipantsTotal: Int + let manager: GetRoomDetailManager + let participatingUsers: [GetRoomDetailUser] +} + +struct GetRoomDetailManager: Decodable { + let id: Int + let nickname: String + let introduce: String + let youtubeUrl: String? + let instagramUrl: String? + let websiteUrl: String? + let blogUrl: String? + let profileImageUrl: String + let isCounselor: Bool +} + struct GetRoomDetailUser: Decodable, Hashable { let id: Int let nickname: String diff --git a/SodaLive/Sources/Live/Room/EnterOrQuitLiveRoomRequest.swift b/SodaLive/Sources/Live/Room/EnterOrQuitLiveRoomRequest.swift new file mode 100644 index 0000000..f6624fe --- /dev/null +++ b/SodaLive/Sources/Live/Room/EnterOrQuitLiveRoomRequest.swift @@ -0,0 +1,14 @@ +// +// EnterOrQuitLiveRoomRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct EnterOrQuitLiveRoomRequest: Encodable { + let roomId: Int + let container: String = "ios" + var password: Int? = nil +} diff --git a/SodaLive/Sources/MyPage/MyPageView.swift b/SodaLive/Sources/MyPage/MyPageView.swift index ff5ce0d..7fb00dc 100644 --- a/SodaLive/Sources/MyPage/MyPageView.swift +++ b/SodaLive/Sources/MyPage/MyPageView.swift @@ -80,7 +80,9 @@ struct MyPageView: View { .stroke(Color(hex: "9970ff"), lineWidth: 1.3) ) .padding(.top, 26.7) - .onTapGesture {} + .onTapGesture { + AppState.shared.setAppStep(step: .creatorDetail(userId: UserDefaults.int(forKey: .userId))) + } } CanCardView(data: data) { diff --git a/SodaLive/Sources/Report/CheersReportDialogView.swift b/SodaLive/Sources/Report/CheersReportDialogView.swift new file mode 100644 index 0000000..b31b9ea --- /dev/null +++ b/SodaLive/Sources/Report/CheersReportDialogView.swift @@ -0,0 +1,96 @@ +// +// CheersReportDialogView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct CheersReportDialogView: View { + + @Binding var isShowing: Bool + let confirmAction: (String) -> Void + + @State private var selectedIndex: Int? = nil + let reasons = [ + "원치 않는 상업성 콘텐츠 또는 스팸", + "아동 학대", + "증오심 표현 또는 노골적인 폭력", + "테러 조장", + "희롱 또는 괴롭힘", + "자살 또는 자해", + "잘못된 정보" + ] + + var body: some View { + ZStack { + Color.black + .opacity(0.7) + .ignoresSafeArea() + .onTapGesture { isShowing = false } + + VStack(spacing: 13.3) { + Text("응원글 신고") + .font(.custom(Font.medium.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + VStack(spacing: 13.3) { + ForEach(0.. Void + + var body: some View { + ZStack { + Color.black + .opacity(0.7) + .ignoresSafeArea() + .onTapGesture { isShowing = false } + + VStack(spacing: 0) { + Spacer() + + VStack(spacing: 13.3) { + HStack(spacing: 0) { + Text("신고하기") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.white) + + Spacer() + } + .padding(.vertical, 8) + .padding(.horizontal, 26.7) + .contentShape(Rectangle()) + .onTapGesture { + isShowing = false + onClickReport() + } + } + .padding(24) + .background(Color(hex: "222222")) + .cornerRadius(13.3, corners: [.topLeft, .topRight]) + } + } + } +} diff --git a/SodaLive/Sources/Report/ProfileReportDialogView.swift b/SodaLive/Sources/Report/ProfileReportDialogView.swift new file mode 100644 index 0000000..31694e4 --- /dev/null +++ b/SodaLive/Sources/Report/ProfileReportDialogView.swift @@ -0,0 +1,57 @@ +// +// ProfileReportDialogView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct ProfileReportDialogView: View { + + @Binding var isShowing: Bool + let confirmAction: () -> Void + + var body: some View { + ZStack { + Color.black + .opacity(0.7) + .ignoresSafeArea() + .onTapGesture { isShowing = false } + + VStack(spacing: 13.3) { + Text("프로필 사진 신고") + .font(.custom(Font.medium.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Text("신고제도를 남용할 경우, 계정에 제약이 있을 수 있습니다.\n프로필 사진을 신고하시겠습니까?") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "909090")) + + HStack(spacing: 26.7) { + Spacer() + + Text("취소") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "9970ff")) + .onTapGesture { + isShowing = false + } + + Text("신고") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "9970ff")) + .onTapGesture { + isShowing = false + confirmAction() + } + } + .padding(.top, 13.3) + } + .padding(24) + .frame(width: screenSize().width - 33.3) + .background(Color(hex: "222222")) + .cornerRadius(13.3) + } + } +} diff --git a/SodaLive/Sources/Report/ProfileReportMenuView.swift b/SodaLive/Sources/Report/ProfileReportMenuView.swift new file mode 100644 index 0000000..8a5aa99 --- /dev/null +++ b/SodaLive/Sources/Report/ProfileReportMenuView.swift @@ -0,0 +1,86 @@ +// +// ProfileReportMenuView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct ProfileReportMenuView: View { + + @Binding var isShowing: Bool + + let isBlockedUser: Bool + let userBlockAction: () -> Void + let userUnBlockAction: () -> Void + let userReportAction: () -> Void + let profileReportAction: () -> Void + + var body: some View { + ZStack { + Color.black + .opacity(0.7) + .ignoresSafeArea() + .onTapGesture { isShowing = false } + + VStack(spacing: 0) { + Spacer() + + VStack(spacing: 13.3) { + HStack(spacing: 0) { + Text(isBlockedUser ? "사용자 차단해제" : "사용자 차단하기") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.white) + + Spacer() + } + .padding(.vertical, 8) + .padding(.horizontal, 26.7) + .contentShape(Rectangle()) + .onTapGesture { + isShowing = false + if isBlockedUser { + userUnBlockAction() + } else { + userBlockAction() + } + } + + HStack(spacing: 0) { + Text("사용자 신고하기") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.white) + + Spacer() + } + .padding(.vertical, 8) + .padding(.horizontal, 26.7) + .contentShape(Rectangle()) + .onTapGesture { + isShowing = false + userReportAction() + } + + HStack(spacing: 0) { + Text("프로필 신고하기") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.white) + + Spacer() + } + .padding(.vertical, 8) + .padding(.horizontal, 26.7) + .contentShape(Rectangle()) + .onTapGesture { + isShowing = false + profileReportAction() + } + } + .padding(24) + .background(Color(hex: "222222")) + .cornerRadius(13.3, corners: [.topLeft, .topRight]) + } + } + } +} diff --git a/SodaLive/Sources/Report/ReportApi.swift b/SodaLive/Sources/Report/ReportApi.swift new file mode 100644 index 0000000..218deb8 --- /dev/null +++ b/SodaLive/Sources/Report/ReportApi.swift @@ -0,0 +1,44 @@ +// +// ReportApi.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation +import Moya + +enum ReportApi { + case report(request: ReportRequest) +} + +extension ReportApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .report: + return "/report" + } + } + + var method: Moya.Method { + switch self { + case .report: + return .post + } + } + + var task: Task { + switch self { + case .report(let request): + return .requestJSONEncodable(request) + } + } + + var headers: [String : String]? { + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } +} diff --git a/SodaLive/Sources/Report/ReportRepository.swift b/SodaLive/Sources/Report/ReportRepository.swift new file mode 100644 index 0000000..d2017b0 --- /dev/null +++ b/SodaLive/Sources/Report/ReportRepository.swift @@ -0,0 +1,19 @@ +// +// ReportRepository.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +class ReportRepository { + private let api = MoyaProvider() + + func report(request: ReportRequest) -> AnyPublisher { + return api.requestPublisher(.report(request: request)) + } +} diff --git a/SodaLive/Sources/Report/ReportRequest.swift b/SodaLive/Sources/Report/ReportRequest.swift new file mode 100644 index 0000000..9333eb0 --- /dev/null +++ b/SodaLive/Sources/Report/ReportRequest.swift @@ -0,0 +1,21 @@ +// +// ReportRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct ReportRequest: Encodable { + let type: ReportType + let reason: String + let reportedMemberId: Int? + let cheersId: Int? + let audioContentId: Int? +} + +enum ReportType: String, Codable { + case PROFILE, USER, CHEERS, AUDIO_CONTENT +} + diff --git a/SodaLive/Sources/Report/UserBlockConfirmDialogView.swift b/SodaLive/Sources/Report/UserBlockConfirmDialogView.swift new file mode 100644 index 0000000..7c88bae --- /dev/null +++ b/SodaLive/Sources/Report/UserBlockConfirmDialogView.swift @@ -0,0 +1,75 @@ +// +// UserBlockConfirmDialogView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct UserBlockConfirmDialogView: View { + + @Binding var isShowing: Bool + + let nickname: String + let confirmAction: () -> Void + + let notice = """ +사용자를 차단하면 사용자는 아래 기능이 제한됩니다. + +- 내가 개설한 라이브 입장 불가 +- 나에게 메시지 보내기 불가 +- 내 채널의 팬Talk 작성불가 +""" + + var body: some View { + ZStack { + Color.black + .opacity(0.7) + .ignoresSafeArea() + .onTapGesture { isShowing = false } + + VStack(spacing: 13.3) { + Text("사용자 차단") + .font(.custom(Font.medium.rawValue, size: 16.7)) + .foregroundColor(.white) + + Text("\(nickname)님을 차단하시겠습니까?") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.white) + + HStack(spacing: 0) { + Text(notice) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.white) + + Spacer() + } + + HStack(spacing: 26.7) { + Spacer() + + Text("취소") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "9970ff")) + .onTapGesture { + isShowing = false + } + + Text("차단") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "9970ff")) + .onTapGesture { + isShowing = false + confirmAction() + } + } + .padding(.top, 13.3) + } + .padding(24) + .frame(width: screenSize().width - 33.3) + .background(Color(hex: "222222")) + .cornerRadius(13.3) + } + } +} diff --git a/SodaLive/Sources/Report/UserReportDialogView.swift b/SodaLive/Sources/Report/UserReportDialogView.swift new file mode 100644 index 0000000..af67e2a --- /dev/null +++ b/SodaLive/Sources/Report/UserReportDialogView.swift @@ -0,0 +1,88 @@ +// +// UserReportDialogView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct UserReportDialogView: View { + + @Binding var isShowing: Bool + let confirmAction: (String) -> Void + + @State private var selectedIndex: Int? = nil + let reasons = [ + "괴롭힘 및 사이버 폭력", + "개인정보 침해", + "명의 도용", + "폭력적 위협", + "아동 학대", + "보호 대상 집단에 대한 증오심 표현", + "스팸 및 사기", + "나에게 해당하는 문제 없음" + ] + + var body: some View { + ZStack { + Color.black + .opacity(0.7) + .ignoresSafeArea() + .onTapGesture { isShowing = false } + + VStack(spacing: 13.3) { + Text("사용자 신고") + .font(.custom(Font.medium.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + VStack(spacing: 13.3) { + ForEach(0.. AnyPublisher { return api.requestPublisher(.updatePushToken(request: PushTokenUpdateRequest(pushToken: pushToken))) } + + func creatorFollow(creatorId: Int) -> AnyPublisher { + return api.requestPublisher(.creatorFollow(request: CreatorFollowRequest(creatorId: creatorId))) + } + + func creatorUnFollow(creatorId: Int) -> AnyPublisher { + return api.requestPublisher(.creatorUnFollow(request: CreatorFollowRequest(creatorId: creatorId))) + } + + func memberBlock(userId: Int) -> AnyPublisher { + return api.requestPublisher(.memberBlock(request: MemberBlockRequest(blockMemberId: userId))) + } + + func memberUnBlock(userId: Int) -> AnyPublisher { + return api.requestPublisher(.memberUnBlock(request: MemberBlockRequest(blockMemberId: userId))) + } }