From 949c8cec8e9a368b3a0bf54f681986b1acb6a0ad Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 2 Jul 2026 15:50:38 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=EC=B1=84=EB=84=90=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EC=85=B8=EC=9D=84=20=EB=B6=84=EB=A6=AC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive.xcodeproj/project.pbxproj | 48 ++++--- SodaLive/Sources/ContentView.swift | 2 +- .../CreatorChannel/CreatorChannelView.swift | 118 ++++++++++++++++++ ...el.swift => CreatorChannelViewModel.swift} | 49 +------- .../Home/CreatorChannelHomeView.swift | 111 ++-------------- .../CreatorChannelTab.swift} | 2 +- docs/20260701_크리에이터_채널_홈/plan-task.md | 2 +- 7 files changed, 167 insertions(+), 165 deletions(-) create mode 100644 SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift rename SodaLive/Sources/V2/CreatorChannel/{Home/CreatorChannelHomeViewModel.swift => CreatorChannelViewModel.swift} (69%) rename SodaLive/Sources/V2/CreatorChannel/{Home/Models/CreatorChannelHomeTab.swift => Models/CreatorChannelTab.swift} (91%) diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index f11b1454..e183abd4 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -10,24 +10,27 @@ CC0101012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */; }; CC0102012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */; }; CC0103012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */; }; - CC0101022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift */; }; - CC0102022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift */; }; - CC0103022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift */; }; CC0101032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift */; }; CC0102032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift */; }; CC0103032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift */; }; CC0101042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift */; }; CC0102042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift */; }; CC0103042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift */; }; - CC0101052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift */; }; - CC0102052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift */; }; - CC0103052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift */; }; CC0101062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100062FF0100000A5BBA3 /* CreatorChannelHomeView.swift */; }; CC0102062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100062FF0100000A5BBA3 /* CreatorChannelHomeView.swift */; }; CC0103062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100062FF0100000A5BBA3 /* CreatorChannelHomeView.swift */; }; CC0101072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */; }; CC0102072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */; }; CC0103072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */; }; + CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; }; + CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; }; + CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; }; + CC0101092FF0100000A5BBA3 /* CreatorChannelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100092FF0100000A5BBA3 /* CreatorChannelViewModel.swift */; }; + CC0102092FF0100000A5BBA3 /* CreatorChannelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100092FF0100000A5BBA3 /* CreatorChannelViewModel.swift */; }; + CC0103092FF0100000A5BBA3 /* CreatorChannelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100092FF0100000A5BBA3 /* CreatorChannelViewModel.swift */; }; + CC0101102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100102FF0100000A5BBA3 /* CreatorChannelTab.swift */; }; + CC0102102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100102FF0100000A5BBA3 /* CreatorChannelTab.swift */; }; + CC0103102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100102FF0100000A5BBA3 /* CreatorChannelTab.swift */; }; 00C796627A6758D271215EC5 /* CreatorProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */; }; 042BEE0C973700FB5F4540CD /* I18n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826D626B7714E5C28ED31074 /* I18n.swift */; }; 0470E68C6F302A131264E3AF /* ChannelDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BFD2E09F509C44C92214 /* ChannelDonationViewModel.swift */; }; @@ -1724,12 +1727,13 @@ /* Begin PBXFileReference section */ CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeResponse.swift; sourceTree = ""; }; - CC0100022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeTab.swift; sourceTree = ""; }; CC0100032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeApi.swift; sourceTree = ""; }; CC0100042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeRepository.swift; sourceTree = ""; }; - CC0100052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeViewModel.swift; sourceTree = ""; }; CC0100062FF0100000A5BBA3 /* CreatorChannelHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeView.swift; sourceTree = ""; }; CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelPlaceholderTabView.swift; sourceTree = ""; }; + CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelView.swift; sourceTree = ""; }; + CC0100092FF0100000A5BBA3 /* CreatorChannelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelViewModel.swift; sourceTree = ""; }; + CC0100102FF0100000A5BBA3 /* CreatorChannelTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelTab.swift; sourceTree = ""; }; 0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.swift; sourceTree = ""; }; 0EC05859619BDC03056911E7 /* MainTab.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTab.swift; sourceTree = ""; }; 189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarView.swift; sourceTree = ""; }; @@ -2632,7 +2636,6 @@ isa = PBXGroup; children = ( CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */, - CC0100022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift */, ); path = Models; sourceTree = ""; @@ -2661,7 +2664,6 @@ CC0100212FF0100000A5BBA3 /* Repository */, CC0100222FF0100000A5BBA3 /* Components */, CC0100062FF0100000A5BBA3 /* CreatorChannelHomeView.swift */, - CC0100052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift */, ); path = Home; sourceTree = ""; @@ -2669,11 +2671,22 @@ CC0100242FF0100000A5BBA3 /* CreatorChannel */ = { isa = PBXGroup; children = ( + CC0100252FF0100000A5BBA3 /* Models */, CC0100232FF0100000A5BBA3 /* Home */, + CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */, + CC0100092FF0100000A5BBA3 /* CreatorChannelViewModel.swift */, ); path = CreatorChannel; sourceTree = ""; }; + CC0100252FF0100000A5BBA3 /* Models */ = { + isa = PBXGroup; + children = ( + CC0100102FF0100000A5BBA3 /* CreatorChannelTab.swift */, + ); + path = Models; + sourceTree = ""; + }; 038B15E425DE0639156ADBA9 /* Repository */ = { isa = PBXGroup; children = ( @@ -5559,12 +5572,13 @@ files = ( 400EDE592B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */, CC0101012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */, - CC0101022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift in Sources */, CC0101032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */, CC0101042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */, - CC0101052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift in Sources */, CC0101062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */, CC0101072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, + CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, + CC0101092FF0100000A5BBA3 /* CreatorChannelViewModel.swift in Sources */, + CC0101102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */, 409E35082B0759DE00CCB985 /* MoyaProviderExtension.swift in Sources */, 40DDBD772B2B4FD700A2E8DA /* CreatorCommunityMoreItemView.swift in Sources */, 400EA7742A89C42C004992CF /* StartLiveRequest.swift in Sources */, @@ -6352,12 +6366,13 @@ 372837FE9857C16B6B5087FF /* TitleBar.swift in Sources */, BA2B76FDCAF81D15E2D4E258 /* MainHomeRecommendationViewModel.swift in Sources */, CC0102012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */, - CC0102022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift in Sources */, CC0102032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */, CC0102042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */, - CC0102052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift in Sources */, CC0102062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */, CC0102072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, + CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, + CC0102092FF0100000A5BBA3 /* CreatorChannelViewModel.swift in Sources */, + CC0102102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */, 401B3F092FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */, 401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, 401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, @@ -7161,12 +7176,13 @@ 5EEE98FE0EC4039761C604EE /* TitleBar.swift in Sources */, A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */, CC0103012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */, - CC0103022FF0100000A5BBA3 /* CreatorChannelHomeTab.swift in Sources */, CC0103032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */, CC0103042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */, - CC0103052FF0100000A5BBA3 /* CreatorChannelHomeViewModel.swift in Sources */, CC0103062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */, CC0103072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, + CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, + CC0103092FF0100000A5BBA3 /* CreatorChannelViewModel.swift in Sources */, + CC0103102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */, 401B3F082FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */, 401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, 401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index bd7799ea..e81709cc 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -169,7 +169,7 @@ struct AppStepLayerView: View { LiveReservationCompleteView(reservationCompleteData: response) case .creatorDetail(let userId): - CreatorChannelHomeView(creatorId: userId) + CreatorChannelView(creatorId: userId) case .followerList(let userId): FollowerListView(userId: userId) diff --git a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift new file mode 100644 index 00000000..7ffc5582 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift @@ -0,0 +1,118 @@ +import SwiftUI + +struct CreatorChannelView: View { + let creatorId: Int + + @StateObject private var viewModel = CreatorChannelViewModel() + @Environment(\.dismiss) private var dismiss + + var body: some View { + ZStack { + Color.black.ignoresSafeArea() + + VStack(spacing: 0) { + titleBar + tabBar + selectedTabContent + } + + if viewModel.isLoading { + LoadingView() + } + } + .navigationBarBackButtonHidden(true) + .onAppear { + if viewModel.hasLoaded == false { + viewModel.fetchHome(creatorId: creatorId) + } + } + .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1) + } + + private var titleBar: some View { + HStack(spacing: 12) { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .foregroundColor(.white) + .frame(width: 44, height: 44) + } + + Spacer() + + if let creator = viewModel.response?.creator { + Button { + if creator.isFollow { + viewModel.creatorFollow(follow: false, notify: false) + } else { + viewModel.creatorFollow(follow: true, notify: true) + } + } label: { + Text(creator.isFollow ? I18n.CreatorChannelHome.following : I18n.CreatorChannelHome.follow) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(creator.isFollow ? .white : .black) + .padding(.horizontal, 12) + .frame(height: 30) + .background(creator.isFollow ? Color.gray33 : Color.soda400) + .clipShape(Capsule()) + } + + if creator.isFollow { + Button { + if creator.isNotify { + viewModel.creatorFollow(follow: true, notify: false) + } else { + viewModel.creatorFollow(follow: true, notify: true) + } + } label: { + Image(systemName: creator.isNotify ? "bell.fill" : "bell") + .foregroundColor(.white) + .frame(width: 44, height: 44) + } + } + } + } + .frame(height: 56) + .padding(.horizontal, 8) + .background(Color.black) + } + + private var tabBar: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 20) { + ForEach(CreatorChannelTab.allCases, id: \.self) { tab in + Button { + viewModel.selectedTab = tab + } label: { + VStack(spacing: 8) { + Text(tab.title) + .font(.system(size: 15, weight: viewModel.selectedTab == tab ? .semibold : .regular)) + .foregroundColor(viewModel.selectedTab == tab ? .white : Color.gray500) + + Rectangle() + .fill(viewModel.selectedTab == tab ? Color.soda400 : Color.clear) + .frame(height: 2) + } + } + } + } + .padding(.horizontal, 20) + } + .frame(height: 48) + .background(Color.black) + } + + @ViewBuilder + private var selectedTabContent: some View { + if viewModel.selectedTab != .home || viewModel.isApiFailedPlaceholderVisible { + CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title) + } else if let response = viewModel.response { + CreatorChannelHomeView(response: response) { tab in + viewModel.selectedTab = tab + } + } else { + CreatorChannelPlaceholderTabView(title: CreatorChannelTab.home.title) + } + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeViewModel.swift b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelViewModel.swift similarity index 69% rename from SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeViewModel.swift rename to SodaLive/Sources/V2/CreatorChannel/CreatorChannelViewModel.swift index 72fb6114..e631a2dc 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeViewModel.swift +++ b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelViewModel.swift @@ -1,7 +1,7 @@ import Foundation import Combine -final class CreatorChannelHomeViewModel: ObservableObject { +final class CreatorChannelViewModel: ObservableObject { private let repository = CreatorChannelHomeRepository() private let userRepository = UserRepository() private var subscription = Set() @@ -9,7 +9,7 @@ final class CreatorChannelHomeViewModel: ObservableObject { @Published var isLoading = false @Published var response: CreatorChannelHomeResponse? @Published var hasLoaded = false - @Published var selectedTab: CreatorChannelHomeTab = .home + @Published var selectedTab: CreatorChannelTab = .home @Published var isApiFailedPlaceholderVisible = false @Published var errorMessage = "" @Published var isShowPopup = false @@ -57,10 +57,11 @@ final class CreatorChannelHomeViewModel: ObservableObject { .store(in: &subscription) } - func follow(creatorId: Int, notify: Bool = true) { + func creatorFollow(follow: Bool = true, notify: Bool = true) { + guard let creatorId = response?.creator.creatorId else { return } isLoading = true - userRepository.creatorFollow(creatorId: creatorId, follow: true, notify: notify) + userRepository.creatorFollow(creatorId: creatorId, follow: follow, notify: notify) .sink { [weak self] result in guard let self else { return } @@ -79,45 +80,7 @@ final class CreatorChannelHomeViewModel: ObservableObject { let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: response.data) if decoded.success { - self.updateCreatorFollowState(isFollow: true, isNotify: notify) - } else { - self.errorMessage = decoded.message ?? I18n.Common.commonError - self.isShowPopup = true - } - } catch { - ERROR_LOG(error.localizedDescription) - self.errorMessage = I18n.Common.commonError - self.isShowPopup = true - } - - self.isLoading = false - } - .store(in: &subscription) - } - - func unfollow(creatorId: Int) { - isLoading = true - - userRepository.creatorUnFollow(creatorId: creatorId) - .sink { [weak self] result in - guard let self else { return } - - switch result { - case .finished: - DEBUG_LOG("finish") - case .failure(let error): - ERROR_LOG(error.localizedDescription) - self.isLoading = false - } - } receiveValue: { [weak self] response in - guard let self else { return } - - do { - let jsonDecoder = JSONDecoder() - let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: response.data) - - if decoded.success { - self.updateCreatorFollowState(isFollow: false, isNotify: false) + self.updateCreatorFollowState(isFollow: follow, isNotify: notify) } else { self.errorMessage = decoded.message ?? I18n.Common.commonError self.isShowPopup = true diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift index b50ecf7a..d7eedfda 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift @@ -1,114 +1,19 @@ import SwiftUI struct CreatorChannelHomeView: View { - let creatorId: Int - - @StateObject private var viewModel = CreatorChannelHomeViewModel() - @Environment(\.dismiss) private var dismiss + let response: CreatorChannelHomeResponse + let onSelectTab: (CreatorChannelTab) -> Void var body: some View { - ZStack { - Color.black.ignoresSafeArea() - - VStack(spacing: 0) { - titleBar - tabBar - - if viewModel.selectedTab != .home || viewModel.isApiFailedPlaceholderVisible { - CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title) - } else { - homeContent - } - } - - if viewModel.isLoading { - LoadingView() - } - } - .navigationBarBackButtonHidden(true) - .onAppear { - if viewModel.hasLoaded == false { - viewModel.fetchHome(creatorId: creatorId) - } - } - .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1) - } - - private var titleBar: some View { - HStack(spacing: 12) { - Button { - dismiss() - } label: { - Image(systemName: "chevron.left") - .foregroundColor(.white) - .frame(width: 44, height: 44) - } - - Spacer() - - if let creator = viewModel.response?.creator { - Button { - if creator.isFollow { - viewModel.unfollow(creatorId: creator.creatorId) - } else { - viewModel.follow(creatorId: creator.creatorId, notify: true) - } - } label: { - Text(creator.isFollow ? I18n.CreatorChannelHome.following : I18n.CreatorChannelHome.follow) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(creator.isFollow ? .white : .black) - .padding(.horizontal, 12) - .frame(height: 30) - .background(creator.isFollow ? Color.gray33 : Color.soda400) - .clipShape(Capsule()) - } - } - } - .frame(height: 56) - .padding(.horizontal, 8) - .background(Color.black) - } - - private var tabBar: some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 20) { - ForEach(CreatorChannelHomeTab.allCases, id: \.self) { tab in - Button { - viewModel.selectedTab = tab - } label: { - VStack(spacing: 8) { - Text(tab.title) - .font(.system(size: 15, weight: viewModel.selectedTab == tab ? .semibold : .regular)) - .foregroundColor(viewModel.selectedTab == tab ? .white : Color.gray500) - - Rectangle() - .fill(viewModel.selectedTab == tab ? Color.soda400 : Color.clear) - .frame(height: 2) - } - } - } - } - .padding(.horizontal, 20) - } - .frame(height: 48) - .background(Color.black) - } - - private var homeContent: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { - if let creator = viewModel.response?.creator { - Text(creator.nickname) - .font(.system(size: 24, weight: .bold)) - .foregroundColor(.white) + Text(response.creator.nickname) + .font(.system(size: 24, weight: .bold)) + .foregroundColor(.white) - Text(I18n.CreatorChannelHome.followerCount(creator.followerCount.comma())) - .font(.system(size: 14)) - .foregroundColor(Color.gray500) - } else { - CreatorChannelPlaceholderTabView(title: CreatorChannelHomeTab.home.title) - .frame(minHeight: 240) - } + Text(I18n.CreatorChannelHome.followerCount(response.creator.followerCount.comma())) + .font(.system(size: 14)) + .foregroundColor(Color.gray500) } .frame(maxWidth: .infinity, alignment: .leading) .padding(20) diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeTab.swift b/SodaLive/Sources/V2/CreatorChannel/Models/CreatorChannelTab.swift similarity index 91% rename from SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeTab.swift rename to SodaLive/Sources/V2/CreatorChannel/Models/CreatorChannelTab.swift index 17f01fbc..a6ab9349 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeTab.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Models/CreatorChannelTab.swift @@ -1,6 +1,6 @@ import Foundation -enum CreatorChannelHomeTab: CaseIterable, Hashable { +enum CreatorChannelTab: CaseIterable, Hashable { case home case live case audio diff --git a/docs/20260701_크리에이터_채널_홈/plan-task.md b/docs/20260701_크리에이터_채널_홈/plan-task.md index 1e558545..c465792e 100644 --- a/docs/20260701_크리에이터_채널_홈/plan-task.md +++ b/docs/20260701_크리에이터_채널_홈/plan-task.md @@ -206,7 +206,7 @@ - 실행 명령: `rg "enum CreatorChannelHome|currentLive|latestAudio|fanTalk|introduce|activity|viewAll|liveDurationHours|seriesCount" SodaLive/Sources/I18n/I18n.swift` - 기대 결과: 신규 사용자 노출 문구가 `I18n`에 존재한다. -- [ ] **Task 2.4: 공통 CreatorChannel shell로 구조 정리** +- [x] **Task 2.4: 공통 CreatorChannel shell로 구조 정리** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` - 생성: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelViewModel.swift`