diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index cdb21d7b..60316f0c 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -279,6 +279,9 @@ 401C105A2FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10592FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift */; }; 401C105B2FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10592FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift */; }; 401C105C2FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10592FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift */; }; + 401C105E2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C105D2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift */; }; + 401C105F2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C105D2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift */; }; + 401C10602FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C105D2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.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 */; }; @@ -1824,6 +1827,7 @@ 401C10542FEFC00000A5BBA3 /* MainHomeFollowingRankingNewsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingRankingNewsCard.swift; sourceTree = ""; }; 401C10582FEFD00000A5BBA3 /* MainHomeFollowingNewsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingNewsSection.swift; sourceTree = ""; }; 401C10592FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingContentNewsCard.swift; sourceTree = ""; }; + 401C105D2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingEmptyStateView.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 = ""; }; @@ -2842,6 +2846,7 @@ 401C10542FEFC00000A5BBA3 /* MainHomeFollowingRankingNewsCard.swift */, 401C10592FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift */, 401C10582FEFD00000A5BBA3 /* MainHomeFollowingNewsSection.swift */, + 401C105D2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift */, ); path = Components; sourceTree = ""; @@ -5517,6 +5522,7 @@ 401C10512FEFC00000A5BBA3 /* MainHomeFollowingRankingNewsCard.swift in Sources */, 401C105A2FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift in Sources */, 401C10552FEFD00000A5BBA3 /* MainHomeFollowingNewsSection.swift in Sources */, + 401C105E2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift in Sources */, 409143302A85652F00EEBFB1 /* CanChargeView.swift in Sources */, 40975CA32B572298004E2F1B /* LiveRoomOverlayStrokeTextButton.swift in Sources */, 40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */, @@ -6288,6 +6294,7 @@ 401C10522FEFC00000A5BBA3 /* MainHomeFollowingRankingNewsCard.swift in Sources */, 401C105B2FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift in Sources */, 401C10562FEFD00000A5BBA3 /* MainHomeFollowingNewsSection.swift in Sources */, + 401C105F2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift in Sources */, B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */, CA65AD0D677B4A8E9CAC0E15 /* MainTab.swift in Sources */, A0643C829BE7377DC09112CB /* MainTabBarButton.swift in Sources */, @@ -7089,6 +7096,7 @@ 401C10532FEFC00000A5BBA3 /* MainHomeFollowingRankingNewsCard.swift in Sources */, 401C105C2FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift in Sources */, 401C10572FEFD00000A5BBA3 /* MainHomeFollowingNewsSection.swift in Sources */, + 401C10602FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.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/Components/MainHomeFollowingEmptyStateView.swift b/SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingEmptyStateView.swift new file mode 100644 index 00000000..2042669e --- /dev/null +++ b/SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingEmptyStateView.swift @@ -0,0 +1,47 @@ +import SwiftUI + +struct MainHomeFollowingEmptyStateView: View { + let message: String + let loginAction: (() -> Void)? + + init(message: String, loginAction: (() -> Void)? = nil) { + self.message = message + self.loginAction = loginAction + } + + var body: some View { + VStack(spacing: SodaSpacing.s16) { + Spacer() + + Text(message) + .appFont(.body5) + .foregroundColor(Color.gray500) + .multilineTextAlignment(.center) + + if let loginAction { + Button(action: loginAction) { + Text(I18n.HomeFollowing.loginButtonTitle) + .appFont(size: 14, weight: .bold) + .foregroundColor(.black) + .padding(.horizontal, SodaSpacing.s16) + .frame(height: 40) + .background(Color.soda400) + .clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14, style: .continuous)) + } + .buttonStyle(.plain) + } + + Spacer() + } + .frame(maxWidth: .infinity) + .background(Color.black) + } +} + +struct MainHomeFollowingEmptyStateView_Previews: PreviewProvider { + static var previews: some View { + MainHomeFollowingEmptyStateView(message: I18n.HomeFollowing.emptyStateMessage) + .frame(width: 390, height: 320) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingNewsSection.swift b/SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingNewsSection.swift index 17be87ab..463f1ee7 100644 --- a/SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingNewsSection.swift +++ b/SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingNewsSection.swift @@ -16,12 +16,12 @@ struct MainHomeFollowingNewsSection: View { } var body: some View { - if !recentNews.isEmpty { + if !renderableNews.isEmpty { VStack(alignment: .leading, spacing: SodaSpacing.s14) { SectionTitle(title: I18n.HomeFollowing.recentNewsTitle) VStack(alignment: .leading, spacing: SodaSpacing.s8) { - ForEach(recentNews) { news in + ForEach(renderableNews) { news in newsItemView(news) } } @@ -30,6 +30,25 @@ struct MainHomeFollowingNewsSection: View { } } + private var renderableNews: [FollowingNewsResponse] { + recentNews.filter { news in + switch news.type { + case .creatorRanking: + return news.creatorRanking != nil + case .contentRanking: + return news.contentRanking != nil + case .communityPost: + return news.communityPost != nil + case .audioContent: + return news.audioContent != nil + case .photoContent: + return news.photoContent != nil + case .unknown: + return false + } + } + } + @ViewBuilder private func newsItemView(_ news: FollowingNewsResponse) -> some View { switch news.type { diff --git a/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift b/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift index 7bde8270..797f2fcd 100644 --- a/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift +++ b/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift @@ -3,6 +3,8 @@ import SwiftUI struct MainHomeFollowingView: View { let onTapLive: (Int) -> Void let onTapCreator: (Int) -> Void + let onTapContent: (Int) -> Void + let onTapLogin: () -> Void let onTapFollowingAll: () -> Void let onTapChatTab: () -> Void let onTapChatRoom: (Int) -> Void @@ -13,6 +15,8 @@ struct MainHomeFollowingView: View { init( onTapLive: @escaping (Int) -> Void = { _ in }, onTapCreator: @escaping (Int) -> Void = { _ in }, + onTapContent: @escaping (Int) -> Void = { _ in }, + onTapLogin: @escaping () -> Void = {}, onTapFollowingAll: @escaping () -> Void = {}, onTapChatTab: @escaping () -> Void = {}, onTapChatRoom: @escaping (Int) -> Void = { _ in }, @@ -20,6 +24,8 @@ struct MainHomeFollowingView: View { ) { self.onTapLive = onTapLive self.onTapCreator = onTapCreator + self.onTapContent = onTapContent + self.onTapLogin = onTapLogin self.onTapFollowingAll = onTapFollowingAll self.onTapChatTab = onTapChatTab self.onTapChatRoom = onTapChatRoom @@ -27,41 +33,78 @@ struct MainHomeFollowingView: View { } var body: some View { - VStack(spacing: 0) { - MainHomeFollowingCreatorSection( - followingCreators: viewModel.response?.followingCreators ?? [], - onTapCreator: onTapCreator, - onTapAll: onTapFollowingAll - ) - - MainHomeFollowingLiveSection( - onAirLives: viewModel.response?.onAirLives ?? [], - onTapLive: onTapLive - ) - - if let recentChats = viewModel.response?.recentChats, !recentChats.isEmpty { - MainHomeFollowingChatSection( - recentChats: recentChats, - onTapTitle: onTapChatTab, - onTapChatRoom: onTapChatRoom - ) - } - - if let monthlySchedules = viewModel.response?.monthlySchedules, !monthlySchedules.isEmpty { - MainHomeFollowingScheduleSection( - monthlySchedules: monthlySchedules, - onTapSchedule: onTapSchedule - ) - } - - MainPlaceholderTabView(title: I18n.HomeFollowing.tabTitle) - } + content .onAppear { if viewModel.hasLoaded == false { viewModel.fetchFollowing() } } } + + @ViewBuilder + private var content: some View { + if viewModel.isLoading { + ProgressView() + .tint(.white) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.black) + } else if viewModel.isLoginRequired { + MainHomeFollowingEmptyStateView( + message: I18n.HomeFollowing.loginRequiredMessage, + loginAction: onTapLogin + ) + } else if !viewModel.message.isEmpty { + MainHomeFollowingEmptyStateView(message: viewModel.message) + } else if let response = viewModel.response { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + MainHomeFollowingCreatorSection( + followingCreators: response.followingCreators, + onTapCreator: onTapCreator, + onTapAll: onTapFollowingAll + ) + + if !response.onAirLives.isEmpty { + MainHomeFollowingLiveSection( + onAirLives: response.onAirLives, + onTapLive: onTapLive + ) + .padding(.top, 28) + } + + if !response.recentChats.isEmpty { + MainHomeFollowingChatSection( + recentChats: response.recentChats, + onTapTitle: onTapChatTab, + onTapChatRoom: onTapChatRoom + ) + .padding(.top, 48) + } + + if !response.monthlySchedules.isEmpty { + MainHomeFollowingScheduleSection( + monthlySchedules: response.monthlySchedules, + onTapSchedule: onTapSchedule + ) + .padding(.top, 48) + } + + if !response.recentNews.isEmpty { + MainHomeFollowingNewsSection( + recentNews: response.recentNews, + onTapCreator: onTapCreator, + onTapContent: onTapContent + ) + .padding(.top, 48) + } + } + .padding(.bottom, SodaSpacing.s20) + } + .background(Color.black) + } else { + MainHomeFollowingEmptyStateView(message: I18n.HomeFollowing.emptyStateMessage) + } + } } struct MainHomeFollowingView_Previews: PreviewProvider { diff --git a/SodaLive/Sources/V2/Main/Home/MainHomeView.swift b/SodaLive/Sources/V2/Main/Home/MainHomeView.swift index b2e8bbeb..fe5c9813 100644 --- a/SodaLive/Sources/V2/Main/Home/MainHomeView.swift +++ b/SodaLive/Sources/V2/Main/Home/MainHomeView.swift @@ -12,6 +12,7 @@ struct MainHomeView: View { let onTapBanner: (RecommendationBannerResponse) -> Void let onTapFollowAll: (@escaping () -> Void) -> Void let onSelectFollowingTab: () -> Bool + let onTapFollowingLogin: () -> Void let onTapFollowingAll: () -> Void let onTapChatTab: () -> Void let onTapChatRoom: (Int) -> Void @@ -78,6 +79,8 @@ struct MainHomeView: View { MainHomeFollowingView( onTapLive: onTapLive, onTapCreator: onTapCreator, + onTapContent: onTapContent, + onTapLogin: onTapFollowingLogin, onTapFollowingAll: onTapFollowingAll, onTapChatTab: onTapChatTab, onTapChatRoom: onTapChatRoom, @@ -118,6 +121,7 @@ struct MainHomeView_Previews: PreviewProvider { onTapBanner: { _ in }, onTapFollowAll: { action in action() }, onSelectFollowingTab: { true }, + onTapFollowingLogin: {}, onTapFollowingAll: {}, onTapChatTab: {}, onTapChatRoom: { _ in }, diff --git a/SodaLive/Sources/V2/Main/MainView.swift b/SodaLive/Sources/V2/Main/MainView.swift index 7cacf704..f03cde90 100644 --- a/SodaLive/Sources/V2/Main/MainView.swift +++ b/SodaLive/Sources/V2/Main/MainView.swift @@ -165,6 +165,7 @@ struct MainView: View { onTapBanner: handleRecommendationBannerTap, onTapFollowAll: handleRecommendationFollowAllTap, onSelectFollowingTab: handleFollowingTabSelection, + onTapFollowingLogin: handleFollowingLoginTap, onTapFollowingAll: handleFollowingAllTap, onTapChatTab: handleFollowingChatTabTap, onTapChatRoom: handleFollowingChatRoomTap, @@ -527,6 +528,10 @@ struct MainView: View { return true } + private func handleFollowingLoginTap() { + AppState.shared.setAppStep(step: .login) + } + private func handleRecommendationBannerTap(_ item: RecommendationBannerResponse) { if let eventItem = item.eventItem { performRecommendationDetailAction { diff --git a/docs/20260630_메인_홈_팔로잉_탭/plan-task.md b/docs/20260630_메인_홈_팔로잉_탭/plan-task.md index a4eee359..f4f4b876 100644 --- a/docs/20260630_메인_홈_팔로잉_탭/plan-task.md +++ b/docs/20260630_메인_홈_팔로잉_탭/plan-task.md @@ -377,7 +377,7 @@ ### Phase 11: 화면 조립, empty/error 상태, 상세 진입 callback 연결 -- [ ] **Task 11.1: 팔로잉 탭 화면 조립** +- [x] **Task 11.1: 팔로잉 탭 화면 조립** - 대상 파일: - 수정: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift` - 작업 내용: @@ -392,7 +392,7 @@ - 실행 명령: `rg "MainHomeFollowingViewModel|fetchFollowing|MainHomeFollowingCreatorSection|MainHomeFollowingLiveSection|MainHomeFollowingChatSection|MainHomeFollowingScheduleSection|MainHomeFollowingNewsSection|MainHomeFollowingEmptyStateView|emptyStateMessage|loadFailedMessage|MainPlaceholderTabView" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift` - 기대 결과: `MainPlaceholderTabView`는 검색되지 않고, 팔로잉 화면 상태 분기와 섹션 조립이 확인된다. -- [ ] **Task 11.2: empty/login required view 생성** +- [x] **Task 11.2: empty/login required view 생성** - 대상 파일: - 생성: `SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingEmptyStateView.swift` - 작업 내용: @@ -403,7 +403,7 @@ - 실행 명령: `rg "struct MainHomeFollowingEmptyStateView|message|loginButtonTitle|Button|Color.black|frame\\(maxWidth: \\.infinity" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingEmptyStateView.swift` - 기대 결과: empty/API 실패/로그인 필요 상태 공용 안내 view가 확인된다. -- [ ] **Task 11.3: 최근 소식 섹션 조립** +- [x] **Task 11.3: 최근 소식 섹션 조립** - 대상 파일: - 생성 또는 수정: `SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingNewsSection.swift` - 작업 내용: @@ -419,7 +419,7 @@ - 실행 명령: `rg "struct MainHomeFollowingNewsSection|recentNewsTitle|visibleFromAtUtc|creatorRanking|contentRanking|communityPost|audioContent|photoContent|unknown|MainHomeFollowingRankingNewsCard|MainHomeFollowingContentNewsCard|CommunityPostCard" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingNewsSection.swift` - 기대 결과: 최근 소식 타입별 payload 분기와 unknown 숨김 정책이 확인된다. -- [ ] **Task 11.4: 상세 진입 callback 연결** +- [x] **Task 11.4: 상세 진입 callback 연결** - 대상 파일: - 수정: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` - 수정: `SodaLive/Sources/V2/Main/MainView.swift` @@ -554,3 +554,17 @@ - 2026-07-01: `rg "localhost:3845|imgRectangle|http://localhost" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingContentNewsCard.swift` 결과가 없어 Figma localhost asset 미사용을 확인했다. - 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataPhase10Test"`는 `Scheme SodaLive is not currently configured for the test action.`으로 실패해 테스트 액션 미구성 상태를 확인했다. - 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataPhase10" build`를 실행해 Phase 10 신규 Swift 파일 컴파일과 빌드 성공을 확인했다. 기존 duplicate build file/run script/AppIntents/dependency scan warning은 남아 있으나 컴파일 오류는 없었다. +- 2026-07-01: Phase 11 구현 전 `rg "struct MainHomeFollowingEmptyStateView" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingEmptyStateView.swift`는 파일 없음으로 실패했고, `rg "MainPlaceholderTabView" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift`는 placeholder 잔존을 확인했으며, `rg "MainHomeFollowingNewsSection\(" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift`는 결과가 없어 최근 소식 미조립 상태를 확인했다. +- 2026-07-01: Phase 11 구현을 진행해 `MainHomeFollowingEmptyStateView`를 추가하고, `MainHomeFollowingView`에서 `isLoading`, `isLoginRequired`, `message`, `response` 상태를 분기하도록 조립했다. content 상태에서는 팔로잉 크리에이터, On Air, 최근 대화, 이달의 스케줄, 최근 소식 순서로 렌더링하고, `MainPlaceholderTabView`를 제거했다. +- 2026-07-01: `MainHomeFollowingView`에 `onTapContent` callback을 추가하고 `MainHomeView`에서 기존 `onTapContent`를 전달해 최근 소식의 콘텐츠 랭킹/오디오/화보 tap이 기존 `MainView.handleRecommendationContentTap(contentId:)` 경로를 재사용하도록 연결했다. `COMMUNITY_POST`는 기존 Task 9.2 제약에 따라 상세 진입을 연결하지 않았다. +- 2026-07-01: `SodaLive.xcodeproj/project.pbxproj`에 `MainHomeFollowingEmptyStateView.swift` 파일 참조와 Sources build file을 추가해 Xcode navigator와 target compile sources에서 확인 가능하도록 등록했다. +- 2026-07-01: `rg "struct MainHomeFollowingEmptyStateView|message|loginButtonTitle|Button|Color.black|frame\(maxWidth: \.infinity|SodaRadius\.r14" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingEmptyStateView.swift`로 empty/login required view 요구사항을 확인했다. +- 2026-07-01: `rg "MainHomeFollowingViewModel|fetchFollowing|ProgressView|MainHomeFollowingCreatorSection|MainHomeFollowingLiveSection|MainHomeFollowingChatSection|MainHomeFollowingScheduleSection|MainHomeFollowingNewsSection|MainHomeFollowingEmptyStateView|emptyStateMessage|loadFailedMessage|MainPlaceholderTabView" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift` 및 `if rg "MainPlaceholderTabView" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift; then exit 1; else true; fi`로 상태 분기/섹션 조립과 placeholder 제거를 확인했다. +- 2026-07-01: `rg "onTapContent|MainHomeFollowingView\(|handleRecommendationContentTap|onTapFollowingAll|onTapChatRoom|onTapFollowingSchedule" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift`로 팔로잉 탭 상세 진입 callback이 기존 route에 연결됨을 확인했다. +- 2026-07-01: 최초 Phase 11 빌드는 `MainHomeFollowingEmptyStateView.swift`의 존재하지 않는 `SodaRadius.r20` 사용으로 실패했다. `SodaLive/Sources/UI/Theme/Radius.swift`에서 `r4`, `r8`, `r14`만 존재함을 확인하고 `SodaRadius.r14`로 수정했다. +- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataPhase11" build`를 재실행해 Phase 11 빌드 성공을 확인했다. 기존 duplicate build file/run script/AppIntents/dependency scan warning은 남아 있으나 컴파일 오류는 없었다. +- 2026-07-01: Phase 11 리뷰에서 `isLoginRequired` 상태의 로그인 버튼 액션 미연결과 최근 소식 payload mismatch 시 제목만 남을 수 있는 문제를 지적받았다. `MainHomeFollowingView -> MainHomeView -> MainView.handleFollowingLoginTap()` callback을 추가해 로그인 필요 상태의 `MainHomeFollowingEmptyStateView`에 `loginAction`을 전달하고 `.login` route로 연결했다. +- 2026-07-01: `MainHomeFollowingNewsSection`에 `renderableNews`를 추가해 `.unknown` 및 payload가 없는 news item을 섹션 렌더링 전에 제외했다. `if !renderableNews.isEmpty`와 `ForEach(renderableNews)`를 사용해 실제 카드가 없는 경우 최근 소식 제목만 표시되지 않도록 수정했다. +- 2026-07-01: `rg "onTapLogin|onTapFollowingLogin|handleFollowingLoginTap|loginAction: onTapLogin|setAppStep\(step: \.login\)" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift`로 로그인 필요 상태의 버튼 action과 `.login` route 연결을 확인했다. +- 2026-07-01: `rg "renderableNews|if !renderableNews\.isEmpty|ForEach\(renderableNews\)|case \.unknown|return false" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingNewsSection.swift`로 최근 소식 섹션의 렌더 가능 item 필터링과 unknown 숨김을 확인했다. +- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataPhase11" build`를 재실행해 Phase 11 리뷰 수정 후 빌드 성공을 확인했다. 기존 duplicate build file/run script/Crashlytics dSYM/AppIntents/dependency scan warning은 남아 있으나 컴파일 오류는 없었다.