From 394e7ebc3449049ffe0ba250dce1b69dbf03fda6 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Sat, 27 Jun 2026 00:09:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(home):=20=ED=99=88=20=ED=83=AD=20shell?= =?UTF-8?q?=EC=9D=84=20=EC=97=B0=EA=B2=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive.xcodeproj/project.pbxproj | 62 +++++++++- .../Following/MainHomeFollowingView.swift | 13 ++ .../Sources/V2/Main/Home/MainHomeView.swift | 91 ++++++++++++++ .../Home/Ranking/MainHomeRankingView.swift | 13 ++ .../MainHomeRecommendationView.swift | 45 +++++++ .../MainHomeRecommendationViewModel.swift} | 2 +- SodaLive/Sources/V2/Main/MainView.swift | 111 +++++++++++++++++- .../plan-task.md | 30 ++++- 8 files changed, 354 insertions(+), 13 deletions(-) create mode 100644 SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift create mode 100644 SodaLive/Sources/V2/Main/Home/MainHomeView.swift create mode 100644 SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift create mode 100644 SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift rename SodaLive/Sources/V2/Main/Home/{MainHomeViewModel.swift => Recommendation/MainHomeRecommendationViewModel.swift} (98%) diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 1a3f9851..aa002f3d 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -195,11 +195,24 @@ 401A3FA02FEEAFA000A5BBA3 /* FollowRecommendedCreatorsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3F962FEEAFA000A5BBA3 /* FollowRecommendedCreatorsRequest.swift */; }; 401A3FA12FEEAFA000A5BBA3 /* MainHomeRecommendationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3F9B2FEEAFA000A5BBA3 /* MainHomeRecommendationRepository.swift */; }; 401A3FA22FEEAFA000A5BBA3 /* RecommendedActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3F982FEEAFA000A5BBA3 /* RecommendedActivityType.swift */; }; + 401A3FBA2FEEAFA000A5BBA3 /* MainHomeRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */; }; + 401A3FA82FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB42FEEAFA000A5BBA3 /* MainHomeView.swift */; }; + 401A3FA92FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */; }; + 401A3FAA2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */; }; + 401A3FAB2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */; }; 401A3FA32FEEAFA000A5BBA3 /* MainHomeRecommendationApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3F9A2FEEAFA000A5BBA3 /* MainHomeRecommendationApi.swift */; }; 401A3FA42FEEAFA000A5BBA3 /* MainHomeRecommendationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3F972FEEAFA000A5BBA3 /* MainHomeRecommendationResponse.swift */; }; 401A3FA52FEEAFA000A5BBA3 /* FollowRecommendedCreatorsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3F962FEEAFA000A5BBA3 /* FollowRecommendedCreatorsRequest.swift */; }; 401A3FA62FEEAFA000A5BBA3 /* MainHomeRecommendationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3F9B2FEEAFA000A5BBA3 /* MainHomeRecommendationRepository.swift */; }; 401A3FA72FEEAFA000A5BBA3 /* RecommendedActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3F982FEEAFA000A5BBA3 /* RecommendedActivityType.swift */; }; + 401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB42FEEAFA000A5BBA3 /* MainHomeView.swift */; }; + 401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */; }; + 401A3FAE2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */; }; + 401A3FAF2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */; }; + 401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB42FEEAFA000A5BBA3 /* MainHomeView.swift */; }; + 401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */; }; + 401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */; }; + 401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */; }; 40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; }; 40235DEE2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; }; 40235DF02E37983D00B09A87 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEF2E37983D00B09A87 /* PersistenceController.swift */; }; @@ -1565,7 +1578,7 @@ A376733CDBF21EBAF81C8BE2 /* SocialLoginRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0050039EBD24D2FF6D0BD0 /* SocialLoginRequest.swift */; }; A3BDD0D3E14CD72B022AF6A7 /* FollowButtonImageAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC033E531A9420363D017CE /* FollowButtonImageAsset.swift */; }; A3EB81FD727C63FE034048B9 /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B18C00DE256DCC53030630 /* Spacing.swift */; }; - A4EB669DFFFB64681E045EF3 /* MainHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeViewModel.swift */; }; + A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */; }; A840136779D6CC95F94CB681 /* HomeTitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */; }; ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; }; AD6424211AA898794BBE8470 /* SectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C12DFB83FE164B38CC066 /* SectionTitle.swift */; }; @@ -1573,7 +1586,7 @@ B11681AC02BF51C6E2FA6FF3 /* MainTabBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F49E8BB9BACFCA8878B70D /* MainTabBarButton.swift */; }; B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; }; B5E9FA82CF4238F640702D4A /* CreatorDetailDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7406AE69F7667FAA2E6DD23 /* CreatorDetailDialogView.swift */; }; - BA2B76FDCAF81D15E2D4E258 /* MainHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeViewModel.swift */; }; + BA2B76FDCAF81D15E2D4E258 /* MainHomeRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */; }; BA511503C1E037162697D771 /* DonationRankingPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A42B63A1021DD9F8AA3F30 /* DonationRankingPeriod.swift */; }; BCA6CD9E57528C5F4203F1A5 /* CapsuleTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74990A0798E64A97237DD113 /* CapsuleTabBar.swift */; }; BEAAA523CAD3198065A557BC /* EntityInfo-SodaLive-dev.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0ED95AC08C6481751AE9D97 /* EntityInfo-SodaLive-dev.generated.swift */; }; @@ -1709,6 +1722,10 @@ 401A3F982FEEAFA000A5BBA3 /* RecommendedActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedActivityType.swift; sourceTree = ""; }; 401A3F9A2FEEAFA000A5BBA3 /* MainHomeRecommendationApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRecommendationApi.swift; sourceTree = ""; }; 401A3F9B2FEEAFA000A5BBA3 /* MainHomeRecommendationRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRecommendationRepository.swift; sourceTree = ""; }; + 401A3FB42FEEAFA000A5BBA3 /* MainHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeView.swift; sourceTree = ""; }; + 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRecommendationView.swift; sourceTree = ""; }; + 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingView.swift; sourceTree = ""; }; + 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingView.swift; sourceTree = ""; }; 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecentContent+CoreDataClass.swift"; sourceTree = ""; }; 40235DEF2E37983D00B09A87 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; 40235DF22E37987300B09A87 /* RecentContentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentContentService.swift; sourceTree = ""; }; @@ -2369,7 +2386,7 @@ 716260E8214A329EE07CAAFA /* LanguageSettingsViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageSettingsViewModel.swift; sourceTree = ""; }; 72CD656CAEE376701F2B563E /* MainView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 74990A0798E64A97237DD113 /* CapsuleTabBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CapsuleTabBar.swift; sourceTree = ""; }; - 76506E56D017236824FE9B8F /* MainHomeViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainHomeViewModel.swift; sourceTree = ""; }; + 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainHomeRecommendationViewModel.swift; sourceTree = ""; }; 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainPlaceholderTabView.swift; sourceTree = ""; }; 7C21D69F717F8D80644F6AEE /* PushNotificationListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushNotificationListView.swift; sourceTree = ""; }; 7C22ACAC456B3BCB2D9C568D /* AcceptLanguageDebugLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AcceptLanguageDebugLogger.swift; sourceTree = ""; }; @@ -2516,7 +2533,9 @@ isa = PBXGroup; children = ( 401A3F9D2FEEAFA000A5BBA3 /* Recommendation */, - 76506E56D017236824FE9B8F /* MainHomeViewModel.swift */, + 401A3FB82FEEAFA000A5BBA3 /* Ranking */, + 401A3FB92FEEAFA000A5BBA3 /* Following */, + 401A3FB42FEEAFA000A5BBA3 /* MainHomeView.swift */, ); path = Home; sourceTree = ""; @@ -2680,10 +2699,28 @@ children = ( 401A3F992FEEAFA000A5BBA3 /* Models */, 401A3F9C2FEEAFA000A5BBA3 /* Repository */, + 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */, + 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */, ); path = Recommendation; sourceTree = ""; }; + 401A3FB82FEEAFA000A5BBA3 /* Ranking */ = { + isa = PBXGroup; + children = ( + 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */, + ); + path = Ranking; + sourceTree = ""; + }; + 401A3FB92FEEAFA000A5BBA3 /* Following */ = { + isa = PBXGroup; + children = ( + 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */, + ); + path = Following; + sourceTree = ""; + }; 40235DEA2E3797B000B09A87 /* Recent */ = { isa = PBXGroup; children = ( @@ -5274,6 +5311,11 @@ 401A3FA02FEEAFA000A5BBA3 /* FollowRecommendedCreatorsRequest.swift in Sources */, 401A3FA12FEEAFA000A5BBA3 /* MainHomeRecommendationRepository.swift in Sources */, 401A3FA22FEEAFA000A5BBA3 /* RecommendedActivityType.swift in Sources */, + 401A3FBA2FEEAFA000A5BBA3 /* MainHomeRecommendationViewModel.swift in Sources */, + 401A3FA82FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, + 401A3FA92FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, + 401A3FAA2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */, + 401A3FAB2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */, 409143302A85652F00EEBFB1 /* CanChargeView.swift in Sources */, 40975CA32B572298004E2F1B /* LiveRoomOverlayStrokeTextButton.swift in Sources */, 40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */, @@ -6015,7 +6057,11 @@ AD6424211AA898794BBE8470 /* SectionTitle.swift in Sources */, 38D73A7B6BFB9A984834984B /* TextTabBar.swift in Sources */, 372837FE9857C16B6B5087FF /* TitleBar.swift in Sources */, - BA2B76FDCAF81D15E2D4E258 /* MainHomeViewModel.swift in Sources */, + BA2B76FDCAF81D15E2D4E258 /* MainHomeRecommendationViewModel.swift in Sources */, + 401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, + 401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, + 401A3FAE2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */, + 401A3FAF2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */, B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */, CA65AD0D677B4A8E9CAC0E15 /* MainTab.swift in Sources */, A0643C829BE7377DC09112CB /* MainTabBarButton.swift in Sources */, @@ -6784,7 +6830,11 @@ 32595EDE75871A7B2FDCC5AB /* SectionTitle.swift in Sources */, 7A1DDAAE227BF176BDEF1CB9 /* TextTabBar.swift in Sources */, 5EEE98FE0EC4039761C604EE /* TitleBar.swift in Sources */, - A4EB669DFFFB64681E045EF3 /* MainHomeViewModel.swift in Sources */, + A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */, + 401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, + 401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, + 401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */, + 401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */, ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */, 33BA3AB731849284E62AE04B /* MainTab.swift in Sources */, B11681AC02BF51C6E2FA6FF3 /* MainTabBarButton.swift in Sources */, diff --git a/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift b/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift new file mode 100644 index 00000000..cf956eef --- /dev/null +++ b/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct MainHomeFollowingView: View { + var body: some View { + MainPlaceholderTabView(title: "팔로잉") + } +} + +struct MainHomeFollowingView_Previews: PreviewProvider { + static var previews: some View { + MainHomeFollowingView() + } +} diff --git a/SodaLive/Sources/V2/Main/Home/MainHomeView.swift b/SodaLive/Sources/V2/Main/Home/MainHomeView.swift new file mode 100644 index 00000000..902d652f --- /dev/null +++ b/SodaLive/Sources/V2/Main/Home/MainHomeView.swift @@ -0,0 +1,91 @@ +import SwiftUI + +struct MainHomeView: View { + let onTapCanCharge: () -> Void + let onTapSearch: () -> Void + let onTapNotificationList: () -> Void + let onTapLive: (Int) -> Void + let onTapCreator: (Int) -> Void + let onTapContent: (Int) -> Void + let onTapCharacter: (Int) -> Void + let onTapCommunity: (Int) -> Void + + @State private var selectedTab: MainHomeTab = .recommendation + + var body: some View { + VStack(spacing: 0) { + HomeTitleBar { + HStack(spacing: 14) { + Image("ic_bar_cash") + .onTapGesture { onTapCanCharge() } + + Image("ic_bar_search") + .onTapGesture { onTapSearch() } + + Image("ic_bar_bell") + .onTapGesture { onTapNotificationList() } + } + } + + TextTabBar( + items: MainHomeTab.allCases, + selectedItem: $selectedTab, + title: { $0.title } + ) + + tabContent + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .background(Color.black.ignoresSafeArea()) + } + + @ViewBuilder + private var tabContent: some View { + switch selectedTab { + case .recommendation: + MainHomeRecommendationView( + onTapLive: onTapLive, + onTapCreator: onTapCreator, + onTapContent: onTapContent, + onTapCharacter: onTapCharacter, + onTapCommunity: onTapCommunity + ) + case .ranking: + MainHomeRankingView() + case .following: + MainHomeFollowingView() + } + } +} + +private enum MainHomeTab: CaseIterable, Hashable { + case recommendation + case ranking + case following + + var title: String { + switch self { + case .recommendation: + return "추천" + case .ranking: + return "랭킹" + case .following: + return "팔로잉" + } + } +} + +struct MainHomeView_Previews: PreviewProvider { + static var previews: some View { + MainHomeView( + onTapCanCharge: {}, + onTapSearch: {}, + onTapNotificationList: {}, + onTapLive: { _ in }, + onTapCreator: { _ in }, + onTapContent: { _ in }, + onTapCharacter: { _ in }, + onTapCommunity: { _ in } + ) + } +} diff --git a/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift b/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift new file mode 100644 index 00000000..0af17152 --- /dev/null +++ b/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct MainHomeRankingView: View { + var body: some View { + MainPlaceholderTabView(title: "랭킹") + } +} + +struct MainHomeRankingView_Previews: PreviewProvider { + static var previews: some View { + MainHomeRankingView() + } +} diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift new file mode 100644 index 00000000..dc83b3a2 --- /dev/null +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift @@ -0,0 +1,45 @@ +import SwiftUI + +struct MainHomeRecommendationView: View { + let onTapLive: (Int) -> Void + let onTapCreator: (Int) -> Void + let onTapContent: (Int) -> Void + let onTapCharacter: (Int) -> Void + let onTapCommunity: (Int) -> Void + + @StateObject private var viewModel = MainHomeRecommendationViewModel() + + var body: some View { + ZStack { + Color.black + .ignoresSafeArea() + + if viewModel.isLoading && viewModel.recommendations == nil { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + } + } + .onAppear { + if viewModel.recommendations == nil { + viewModel.fetchRecommendations() + } + } + .sodaToast( + isPresented: $viewModel.isShowPopup, + message: viewModel.errorMessage, + autohideIn: 2 + ) + } +} + +struct MainHomeRecommendationView_Previews: PreviewProvider { + static var previews: some View { + MainHomeRecommendationView( + onTapLive: { _ in }, + onTapCreator: { _ in }, + onTapContent: { _ in }, + onTapCharacter: { _ in }, + onTapCommunity: { _ in } + ) + } +} diff --git a/SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift similarity index 98% rename from SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift rename to SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift index 2dd29b1f..d6340b31 100644 --- a/SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift @@ -1,7 +1,7 @@ import Foundation import Combine -final class MainHomeViewModel: ObservableObject { +final class MainHomeRecommendationViewModel: ObservableObject { private let repository = MainHomeRecommendationRepository() private var subscription = Set() diff --git a/SodaLive/Sources/V2/Main/MainView.swift b/SodaLive/Sources/V2/Main/MainView.swift index be038916..41614645 100644 --- a/SodaLive/Sources/V2/Main/MainView.swift +++ b/SodaLive/Sources/V2/Main/MainView.swift @@ -153,7 +153,16 @@ struct MainView: View { private var contentView: some View { switch viewModel.currentTab { case .home: - MainPlaceholderTabView(title: MainTab.home.title) + MainHomeView( + onTapCanCharge: handleHomeCanChargeTap, + onTapSearch: handleHomeSearchTap, + onTapNotificationList: handleHomeNotificationListTap, + onTapLive: handleRecommendationLiveTap, + onTapCreator: handleRecommendationCreatorTap, + onTapContent: handleRecommendationContentTap, + onTapCharacter: handleRecommendationCharacterTap, + onTapCommunity: handleRecommendationCommunityTap + ) case .content: MainPlaceholderTabView(title: MainTab.content.title) case .chat: @@ -416,6 +425,106 @@ struct MainView: View { navigationAction() } + private func handleHomeCanChargeTap() { + appState.setAppStep(step: .canCharge(refresh: {})) + } + + private func handleHomeSearchTap() { + appState.setAppStep(step: .search) + } + + private func handleHomeNotificationListTap() { + appState.setAppStep(step: .notificationList) + } + + private func handleRecommendationLiveTap(roomId: Int) { + guard roomId > 0 else { return } + performRecommendationDetailAction { + openLiveDetail(roomId: roomId) + } + } + + private func handleRecommendationCreatorTap(creatorId: Int) { + guard creatorId > 0 else { return } + performRecommendationDetailAction { + appState.setAppStep(step: .creatorDetail(userId: creatorId)) + } + } + + private func handleRecommendationContentTap(contentId: Int) { + guard contentId > 0 else { return } + performRecommendationDetailAction { + appState.setAppStep(step: .contentDetail(contentId: contentId)) + } + } + + private func handleRecommendationCharacterTap(characterId: Int) { + guard characterId > 0 else { return } + performRecommendationDetailAction { + appState.setAppStep(step: .characterDetail(characterId: characterId)) + } + } + + private func handleRecommendationCommunityTap(creatorId: Int) { + guard creatorId > 0 else { return } + performRecommendationDetailAction { + appState.setAppStep(step: .creatorCommunityAll(creatorId: creatorId)) + } + } + + private func performRecommendationDetailAction( + requiresAdultContent: Bool = false, + action: @escaping () -> Void + ) { + let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + AppState.shared.setAppStep(step: .login) + return + } + + if requiresAdultContent { + let normalizedCountryCode = UserDefaults + .string(forKey: .countryCode) + .trimmingCharacters(in: .whitespacesAndNewlines) + .uppercased() + let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR" + + if isKoreanCountry && auth == false { + pendingAction = action + isShowAuthConfirmView = true + return + } + + if !UserDefaults.isAdultContentVisible() { + pendingAction = nil + moveToContentSettingsWithGuideToast() + return + } + } + + action() + } + + private func openLiveDetail(roomId: Int) { + appState.setAppStep( + step: .liveDetail( + roomId: roomId, + onClickParticipant: { + appState.isShowPlayer = false + liveViewModel.enterLiveRoom(roomId: roomId) + }, + onClickReservation: {}, + onClickStart: {}, + onClickCancel: {} + ) + ) + } + + private func moveToContentSettingsWithGuideToast() { + AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide) + AppState.shared.setAppStep(step: .contentViewSettings) + } + private func confirmExternalNavigation() { guard pendingExternalNavigationAction != nil else { isShowLeaveLiveNavigationDialog = false diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md b/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md index d4960dbb..e0e623bd 100644 --- a/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md +++ b/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md @@ -135,7 +135,7 @@ ### Phase 4: 추천 데이터 상태 계층 정리 -- [ ] **Task 4.1: 추천 ViewModel 데이터 로딩 계층 정리** +- [x] **Task 4.1: 추천 ViewModel 데이터 로딩 계층 정리** - 대상 파일: - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift` - 이동/정리: `SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift` @@ -151,7 +151,7 @@ ### Phase 5: 추천 상세 진입 데이터/라우팅 guard -- [ ] **Task 5.1: 추천 탭 상세 진입 guard 적용** +- [x] **Task 5.1: 추천 탭 상세 진입 guard 적용** - 대상 파일: - 수정: `SodaLive/Sources/V2/Main/MainView.swift` - 생성/보완: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` @@ -174,7 +174,7 @@ ### Phase 6: 홈 탭 shell과 탭별 루트 연결 -- [ ] **Task 6.1: 추천/랭킹/팔로잉 하위 폴더와 루트 View 준비** +- [x] **Task 6.1: 추천/랭킹/팔로잉 하위 폴더와 루트 View 준비** - 대상 파일: - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` - 생성/보완: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift` @@ -187,7 +187,7 @@ - 실행 명령: `find SodaLive/Sources/V2/Main/Home -maxdepth 3 -type f | sort` - 기대 결과: `Recommendation`, `Ranking`, `Following` 하위 폴더의 루트 View 파일이 확인된다. -- [ ] **Task 6.2: MainHomeView 탭 shell 작성/보완** +- [x] **Task 6.2: MainHomeView 탭 shell 작성/보완** - 대상 파일: - 생성/보완: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` - 확인: `SodaLive/Sources/V2/Component/HomeTitleBar.swift` @@ -199,7 +199,7 @@ - 실행 명령: `rg "struct MainHomeView|HomeTitleBar|MainHomeRecommendationView|MainHomeRankingView|MainHomeFollowingView|MainTabBarView|fetchRecommendations" SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift` - 기대 결과: `MainHomeView`는 탭별 루트 View를 조합하고, `fetchRecommendations`를 직접 호출하지 않으며, 탭바 중복 생성은 없어야 한다. -- [ ] **Task 6.3: MainView 홈 탭 연결** +- [x] **Task 6.3: MainView 홈 탭 연결** - 대상 파일: - 수정: `SodaLive/Sources/V2/Main/MainView.swift` - 작업 내용: @@ -442,6 +442,26 @@ ## 검증 기록 +### 2026-06-26 Phase 4~6 구현 완료 + +- 목적: 추천 ViewModel 데이터 로딩 계층을 `Recommendation` 하위로 정리하고, 추천 탭 상세 진입 callback guard와 홈 탭 shell/루트 View를 연결 +- 수행 내용: + - `MainHomeViewModel.swift`를 `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift`로 이동하고 `MainHomeRecommendationViewModel`로 이름 변경 + - `MainHomeRecommendationView`를 추가해 추천 API 로딩/에러 toast 상태를 추천 탭 루트에서 관리하도록 구성 + - `MainHomeView`를 추가해 `HomeTitleBar`, 추천/랭킹/팔로잉 `TextTabBar`, 탭별 루트 View 조합만 담당하도록 구성 + - `MainHomeRankingView`, `MainHomeFollowingView`는 이번 추천 API 범위 밖이므로 최소 placeholder 루트 View만 추가 + - `MainView`의 `.home` 분기를 `MainHomeView`로 교체하고, 추천 상세 진입 callback을 기존 `token`, `auth`, `pendingAction`, `isShowAuthConfirmView`, `authView` 인프라를 재사용하는 guard helper에 연결 + - `HomeLiveItem`에 성인 여부 필드가 없어 라이브 항목 성인 여부는 임의 판단하지 않고, 향후 성인 여부가 명확한 호출에서 `requiresAdultContent`를 사용할 수 있게 중앙 helper만 준비 + - 신규/이동 Swift 파일을 `SodaLive.xcodeproj/project.pbxproj` 빌드 대상에 포함 +- 검증: + - `rg "final class MainHomeRecommendationViewModel|fetchRecommendations|HomeRecommendationResponse|MainHomeRecommendationRepository|isLoading|errorMessage" SodaLive/Sources/V2/Main/Home` 실행, 추천 API 로딩/에러/응답 상태가 `MainHomeRecommendationViewModel` 중심으로 검색됨 확인 + - `rg "pendingAction|isShowAuthConfirmView|setPendingContentSettingsGuideMessage|adultContentEnableGuide|contentViewSettings|MainHomeView\(|MainHomeRecommendationView\(" SodaLive/Sources/V2/Main/MainView.swift SodaLive/Sources/V2/Main/Home` 실행, 신규 추천 탭 callback이 `MainView` guard 인프라를 재사용함 확인 + - `find SodaLive/Sources/V2/Main/Home -maxdepth 3 -type f | sort` 실행, `Recommendation`, `Ranking`, `Following` 하위 루트 파일 확인 + - `rg "struct MainHomeView|HomeTitleBar|MainHomeRecommendationView|MainHomeRankingView|MainHomeFollowingView|MainTabBarView|fetchRecommendations" SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift` 실행, `MainHomeView`가 탭별 루트 View를 조합하고 `fetchRecommendations`를 직접 호출하지 않음 확인 + - `rg "case \\.home|MainHomeView|MainPlaceholderTabView" SodaLive/Sources/V2/Main/MainView.swift` 실행, `.home`이 `MainHomeView`를 표시하고 다른 placeholder 탭은 유지됨 확인 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행, `OK` 확인 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행, `BUILD SUCCEEDED` 확인 + ### 2026-06-26 Phase 1~3 코드 리뷰 보완 - 목적: Phase 2.2 리뷰에서 지적된 과거 추정 응답 필드 혼재를 제거하고, 추천 전용 request 모델 위치를 `Recommendation/Models` 구조로 일치