feat(home): 팔로잉 탭 화면을 조립한다

This commit is contained in:
Yu Sung
2026-07-01 17:36:07 +09:00
parent 5b679389a6
commit f93e65372b
7 changed files with 175 additions and 35 deletions

View File

@@ -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 = "<group>"; };
401C10582FEFD00000A5BBA3 /* MainHomeFollowingNewsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingNewsSection.swift; sourceTree = "<group>"; };
401C10592FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingContentNewsCard.swift; sourceTree = "<group>"; };
401C105D2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingEmptyStateView.swift; sourceTree = "<group>"; };
40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecentContent+CoreDataClass.swift"; sourceTree = "<group>"; };
40235DEF2E37983D00B09A87 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
40235DF22E37987300B09A87 /* RecentContentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentContentService.swift; sourceTree = "<group>"; };
@@ -2842,6 +2846,7 @@
401C10542FEFC00000A5BBA3 /* MainHomeFollowingRankingNewsCard.swift */,
401C10592FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift */,
401C10582FEFD00000A5BBA3 /* MainHomeFollowingNewsSection.swift */,
401C105D2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -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 */,

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 },

View File

@@ -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 {

View File

@@ -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은 남아 있으나 컴파일 오류는 없었다.