diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift index 52c0907f..12ceaa35 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift @@ -3,17 +3,20 @@ import SwiftUI struct MainHomeActiveCreatorSection: View { let items: [HomeActiveCreatorItem] let onTapLive: (Int) -> Void + let onTapCreator: (Int) -> Void let onTapContent: (Int) -> Void let onTapCommunityPost: (Int) -> Void init( items: [HomeActiveCreatorItem], onTapLive: @escaping (Int) -> Void = { _ in }, + onTapCreator: @escaping (Int) -> Void = { _ in }, onTapContent: @escaping (Int) -> Void = { _ in }, onTapCommunityPost: @escaping (Int) -> Void = { _ in } ) { self.items = items self.onTapLive = onTapLive + self.onTapCreator = onTapCreator self.onTapContent = onTapContent self.onTapCommunityPost = onTapCommunityPost } @@ -38,16 +41,16 @@ struct MainHomeActiveCreatorSection: View { } private func handleTap(_ item: HomeActiveCreatorItem) { - guard let targetId = item.targetId else { return } - - switch item.activityType { - case .live: - onTapLive(targetId) - case .audio: - onTapContent(targetId) - case .community: - onTapCommunityPost(targetId) - case .unknown: + switch item.activeCreatorTapDestination { + case .live(let roomId): + onTapLive(roomId) + case .creator(let creatorId): + onTapCreator(creatorId) + case .content(let contentId): + onTapContent(contentId) + case .communityPost(let postId): + onTapCommunityPost(postId) + case nil: break } } @@ -141,6 +144,7 @@ struct MainHomeActiveCreatorSection_Previews: PreviewProvider { MainHomeActiveCreatorSection( items: [ HomeActiveCreatorItem( + creatorId: 1, creatorNickname: "크리에이터이름", creatorProfileImage: previewImageUrl, activityType: .live, @@ -148,6 +152,7 @@ struct MainHomeActiveCreatorSection_Previews: PreviewProvider { targetId: 1 ), HomeActiveCreatorItem( + creatorId: 2, creatorNickname: "크리에이터이름", creatorProfileImage: previewImageUrl, activityType: .audio, diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift index 624aba4c..724919ef 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift @@ -38,6 +38,7 @@ struct MainHomeRecommendationView: View { MainHomeActiveCreatorSection( items: viewModel.recommendations?.recentlyActiveCreators ?? [], onTapLive: onTapLive, + onTapCreator: onTapCreator, onTapContent: onTapContent, onTapCommunityPost: onTapCommunityPost ) diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift index fe0afe4a..9d8b6316 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift @@ -27,6 +27,7 @@ struct RecommendationBannerResponse: Decodable, Hashable { } struct HomeActiveCreatorItem: Decodable, Hashable { + let creatorId: Int let creatorNickname: String let creatorProfileImage: String let activityType: RecommendedActivityType @@ -34,6 +35,32 @@ struct HomeActiveCreatorItem: Decodable, Hashable { let targetId: Int? } +enum HomeActiveCreatorTapDestination: Equatable { + case live(Int) + case creator(Int) + case content(Int) + case communityPost(Int) +} + +extension HomeActiveCreatorItem { + var activeCreatorTapDestination: HomeActiveCreatorTapDestination? { + switch activityType { + case .live: + if let targetId { + return .live(targetId) + } + + return creatorId > 0 ? .creator(creatorId) : nil + case .liveReplay, .audio: + return targetId.map(HomeActiveCreatorTapDestination.content) + case .community: + return targetId.map(HomeActiveCreatorTapDestination.communityPost) + case .unknown: + return nil + } + } +} + struct HomeCreatorItem: Decodable, Hashable { let creatorId: Int let creatorNickname: String diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift index 3d99ccb0..77c46414 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift @@ -2,6 +2,7 @@ import Foundation enum RecommendedActivityType: Decodable, Hashable { case live + case liveReplay case audio case community case unknown(String) @@ -11,8 +12,10 @@ enum RecommendedActivityType: Decodable, Hashable { let code = try container.decode(String.self) switch code { - case "LIVE", "LIVE_REPLAY": + case "LIVE": self = .live + case "LIVE_REPLAY": + self = .liveReplay case "AUDIO": self = .audio case "COMMUNITY": @@ -24,7 +27,7 @@ enum RecommendedActivityType: Decodable, Hashable { var title: String { switch self { - case .live: + case .live, .liveReplay: return I18n.HomeRecommendation.activityLive case .audio: return I18n.HomeRecommendation.activityAudio diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md b/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md index e4c89eaf..40d17320 100644 --- a/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md +++ b/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md @@ -537,8 +537,151 @@ - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` - 기대 결과: `BUILD SUCCEEDED` +### Phase 21: 최근 활동 LIVE 이동 fallback 보정 + +**Phase 결과:** 최근 활동 크리에이터의 서버 원본 `activityType == LIVE` 아이템은 `targetId` 유무에 따라 라이브룸 또는 크리에이터 채널로 이동한다. `LIVE_REPLAY`는 표시 문구를 기존 `라이브`로 유지하되, `AUDIO`와 동일하게 콘텐츠 상세 페이지로 이동한다. + +**선행조건:** 서버가 `HomeActiveCreatorItem.creatorId`를 항상 유효한 양수의 non-null `Long` 값으로 제공한다. + +- [x] **Task 21.1: PRD/계획 문서에 LIVE 이동 fallback 요구사항 추가** + - Goal: 인터뷰에서 확정한 응답 필드, 이동 분기, 양수 검사, 회귀 제외 범위를 구현 기준으로 고정한다. + - 대상 파일: + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md` + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - 작업 내용: + - `HomeActiveCreatorItem` 응답에 non-null `creatorId`가 추가됨을 명시한다. + - 서버 원본 `activityType == LIVE`에서 `targetId != null`이면 라이브룸, `targetId == null`이면 `creatorId` 기반 크리에이터 채널로 이동하도록 명시한다. + - 서버가 `creatorId`를 유효한 양수로 보장하더라도 앱에서 `creatorId > 0`을 검사하도록 명시한다. + - `LIVE_REPLAY`에는 `LIVE`의 새 fallback을 적용하지 않고, `AUDIO`와 동일하게 `targetId` 기반 콘텐츠 상세 페이지로 이동하도록 명시한다. + - `AUDIO`, `COMMUNITY`, 알 수 없는 타입의 기존 동작은 변경하지 않도록 명시한다. + - 완료 증거: + - PRD 응답 스키마, 상세 진입 규칙, 성공 기준, 검증 계획과 이 Phase의 구현 Task가 같은 요구사항을 사용한다. + - TDD 예외 사유: + - production 동작을 변경하지 않는 문서 갱신 Task다. + - 검증 기준: + - 실행 명령: `rg "creatorId: Long|activityType == LIVE|targetId == null|creatorId > 0|LIVE_REPLAY" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - 기대 결과: 응답 필드, 분기 조건, 양수 검사, `LIVE_REPLAY` 제외 규칙이 두 문서에서 검색된다. + +- [x] **Task 21.2: 최근 활동 응답 모델과 활동 타입 구분 보정** + - Goal: `creatorId`를 필수 값으로 디코딩하고 `LIVE`와 `LIVE_REPLAY`를 이동 분기에서 구분할 수 있게 한다. + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift` + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift` + - 작업 내용: + - `HomeActiveCreatorItem`에 non-optional `creatorId: Int`를 추가한다. + - `RecommendedActivityType`에서 `LIVE`와 `LIVE_REPLAY`를 별도 case로 디코딩하되 두 case의 표시 문구는 기존 라이브 문구를 유지한다. + - Preview의 `HomeActiveCreatorItem` 생성부에 `creatorId`를 추가한다. + - 완료 증거: + - 모델과 Preview가 신규 필드를 사용하고, `LIVE`와 `LIVE_REPLAY`의 표시 문구는 같지만 enum case는 구분된다. + - 검증 기준: + - 실행 명령: `rg "creatorId|case liveReplay|LIVE_REPLAY|HomeActiveCreatorItem\\(" SodaLive/Sources/V2/Main/Home/Recommendation` + - 기대 결과: 신규 필드와 구분된 활동 타입, 갱신된 Preview 생성부가 검색된다. + +- [x] **Task 21.3: LIVE 분기와 LIVE_REPLAY 콘텐츠 이동 연결 및 회귀 검증** + - Goal: 최근 활동 `LIVE` 아이템의 두 이동 경로와 `LIVE_REPLAY`의 콘텐츠 상세 이동을 기존 추천 상세 진입 callback에 연결한다. + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` + - 확인: `SodaLive/Sources/V2/Main/MainView.swift` + - 확인: `SodaLive.xcodeproj/project.pbxproj` + - 작업 내용: + - `MainHomeActiveCreatorSection`에 기존 `onTapCreator` callback을 전달한다. + - 서버 원본 `LIVE`에서 `targetId`가 있으면 `onTapLive(targetId)`, 없으면 `creatorId > 0` 검사 후 `onTapCreator(creatorId)`를 실행한다. + - `LIVE_REPLAY`는 `AUDIO`와 동일하게 `targetId`가 있을 때 `onTapContent(targetId)`를 실행하고, 없으면 아무 동작도 하지 않는다. + - `AUDIO`, `COMMUNITY`, 알 수 없는 타입의 기존 동작을 유지한다. + - 라이브룸과 크리에이터 채널 이동은 `MainView`의 기존 추천 상세 진입 guard를 재사용한다. + - 완료 증거: + - `LIVE`의 두 분기, `creatorId` 양수 검사, `LIVE_REPLAY`의 콘텐츠 상세 이동이 코드에 존재하고, 정적 검증과 `SodaLive-dev` 빌드가 성공한다. + - 검증 기준: + - 실행 명령: `rg "onTapCreator|creatorId > 0|targetId|liveReplay|onTapLive|onTapContent|onTapCommunityPost" SodaLive/Sources/V2/Main/Home/Recommendation SodaLive/Sources/V2/Main/MainView.swift` + - 기대 결과: `LIVE`의 두 이동 경로, `LIVE_REPLAY`의 `onTapContent` 연결, 다른 활동 타입의 기존 callback이 검색된다. + - 실행 명령: `git diff --check` + - 기대 결과: 출력 없이 성공한다. + - 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj` + - 기대 결과: `OK` + - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - 기대 결과: `BUILD SUCCEEDED` + - 수동 확인: + - `LIVE + targetId != nil` 아이템을 터치하면 라이브룸으로 이동한다. + - `LIVE + targetId == nil + creatorId > 0` 아이템을 터치하면 크리에이터 채널로 이동한다. + - `LIVE_REPLAY + targetId != nil` 아이템을 터치하면 콘텐츠 상세 페이지로 이동한다. + - `LIVE_REPLAY + targetId == nil` 아이템은 이동하지 않는다. + - `AUDIO`, `COMMUNITY`, 알 수 없는 타입은 기존 동작을 유지한다. + +- [x] **Task 21.4: LIVE_REPLAY 이동 요구사항 문서 정정** + - Goal: 잘못 기록된 `LIVE_REPLAY` 라이브룸 이동 기준을 `AUDIO`와 동일한 콘텐츠 상세 이동 기준으로 정정한다. + - 대상 파일: + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md` + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - 작업 내용: + - PRD의 상세 진입 규칙, 성공 기준, Open Questions, 검증 계획을 정정한다. + - Phase 21 결과와 미완료 구현 Task에서 `LIVE_REPLAY`를 `onTapContent(targetId)`에 연결하도록 정정한다. + - `LIVE_REPLAY`의 표시 문구는 기존과 같이 `I18n.HomeRecommendation.activityLive`를 유지하도록 명시한다. + - 기존 검증 기록은 삭제하지 않고 후속 정정 기록을 추가한다. + - 완료 증거: + - 활성 요구사항과 미완료 구현 Task에서 `LIVE_REPLAY`의 라이브룸 이동 표현이 없고 콘텐츠 상세 이동 규칙이 일치한다. + - TDD 예외 사유: + - production 동작을 변경하지 않는 문서 정정 Task다. + - 검증 기준: + - 실행 명령: `rg "LIVE_REPLAY.*(라이브룸|콘텐츠 상세)|onTapContent\\(targetId\\)" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - 기대 결과: 활성 요구사항과 미완료 구현 Task에는 `LIVE_REPLAY`의 콘텐츠 상세 이동만 명시되고, 과거 오류는 정정 기록에서만 설명된다. + ## 검증 기록 +### 2026-07-30 Phase 21 최근 활동 LIVE 이동 fallback 구현 완료 + +- 목적: 최근 활동 크리에이터의 `LIVE` fallback과 `LIVE_REPLAY` 콘텐츠 상세 이동을 코드에 반영 +- 수행 내용: + - `HomeActiveCreatorItem.creatorId`를 필수 `Int`로 추가 + - `RecommendedActivityType.liveReplay`를 추가하고 `LIVE_REPLAY` 디코딩을 `.live`와 분리 + - `LIVE`와 `LIVE_REPLAY`의 표시 문구는 모두 `I18n.HomeRecommendation.activityLive`를 유지 + - `HomeActiveCreatorTapDestination`과 `activeCreatorTapDestination`으로 최근 활동 탭 목적지를 한 곳에서 결정 + - `LIVE + targetId`는 라이브룸, `LIVE + targetId == nil + creatorId > 0`은 크리에이터 채널로 연결 + - `LIVE_REPLAY + targetId`는 콘텐츠 상세로 연결하고, `targetId == nil`이면 이동하지 않도록 처리 + - `AUDIO`, `COMMUNITY`, 알 수 없는 타입의 기존 분기는 유지 +- 검증: + - 임시 `swiftc` RED 체크 실행, 기존 코드에서 `HomeActiveCreatorItem.creatorId`, `RecommendedActivityType.liveReplay`, `activeCreatorTapDestination` 부재로 실패 확인 + - 임시 `swiftc` GREEN 체크 실행, `creatorId` 디코딩, `LIVE_REPLAY` case 분리, `LIVE`/`LIVE_REPLAY`/`AUDIO`/`COMMUNITY`/unknown 라우팅 precondition 통과 확인 + - `rg -n "creatorId|case liveReplay|LIVE_REPLAY|HomeActiveCreatorItem\(|activeCreatorTapDestination|onTapCreator|creatorId > 0|onTapLive|onTapContent|onTapCommunityPost" "SodaLive/Sources/V2/Main/Home/Recommendation" "SodaLive/Sources/V2/Main/MainView.swift"` 실행, Phase 21 구현 근거 확인 + - `git diff --check` 실행, 출력 없이 성공 확인 + - `plutil -lint "SodaLive.xcodeproj/project.pbxproj"` 실행, `OK` 확인 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행, `BUILD SUCCEEDED` 확인 + - `lsp_diagnostics`는 개별 Swift 파일 단독 분석에서 기존 프로젝트 타입(`I18n`, `EventItem`, `DateParser`)을 찾지 못해 false positive를 반환했으므로, 실제 컴파일 검증은 `xcodebuild` 결과를 기준으로 판단 + +### 2026-07-30 Phase 21 LIVE_REPLAY 이동 요구사항 정정 + +- 정정 대상: + - Task 21.1과 Task 21.3에 기록했던 `LIVE_REPLAY`의 기존 라이브룸 이동 기준 + - 이전 `2026-07-30 Phase 21 최근 활동 LIVE 이동 fallback 요구사항 문서화` 기록의 `LIVE_REPLAY` 기존 동작 유지 표현 +- 정정 내용: + - `LIVE_REPLAY`는 라이브룸으로 이동하지 않는다. + - `AUDIO`와 동일하게 `targetId`를 콘텐츠 ID로 사용해 콘텐츠 상세 페이지로 이동한다. + - `targetId == nil`이면 이동하지 않으며 `creatorId` fallback은 적용하지 않는다. + - 표시 문구는 기존과 같이 `I18n.HomeRecommendation.activityLive`를 사용해 `라이브`로 표시한다. +- 반영 범위: + - PRD 상세 진입 규칙, 성공 기준, Open Questions, 검증 계획 + - Phase 21 결과와 Task 21.1, Task 21.3 + - 문서 정정 추적용 Task 21.4 +- 검증: + - `rg "LIVE_REPLAY.*(라이브룸|콘텐츠 상세|activityLive|라이브)|onTapContent\\(targetId\\)" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 활성 요구사항과 미완료 구현 Task에서 `LIVE_REPLAY`의 콘텐츠 상세 이동 및 기존 `라이브` 표시 유지 기준 확인 + - `git diff --check -- docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 출력 없이 성공 확인 + +### 2026-07-30 Phase 21 최근 활동 LIVE 이동 fallback 요구사항 문서화 + +- 목적: `HomeActiveCreatorItem.creatorId` 추가와 최근 활동 `LIVE`의 `targetId` 유무별 이동 규칙을 구현 전에 확정 +- 수행 내용: + - PRD 응답 스키마에 non-null `creatorId: Long` 추가 + - 서버 원본 `LIVE`에서 `targetId`가 있으면 라이브룸, 없으면 `creatorId` 기반 크리에이터 채널로 이동하도록 명시 + - 서버가 유효한 양수를 보장하더라도 앱에서 `creatorId > 0`을 검사하도록 명시 + - `LIVE`와 `LIVE_REPLAY`를 앱 enum case에서 구분하고 `LIVE_REPLAY`에는 새 fallback을 적용하지 않도록 명시 + - `AUDIO`, `COMMUNITY`, 알 수 없는 활동 타입의 기존 동작 유지 기준 추가 + - Phase 21에 문서, 모델/enum, 이동 연결/검증 Task를 추가하고 구현 Task는 미완료로 유지 +- 검증: + - `rg "creatorId: Long|activityType == LIVE|targetId == null|creatorId > 0|LIVE_REPLAY" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 응답 필드, `LIVE` 분기, 양수 검사, `LIVE_REPLAY` 제외 규칙이 두 문서에 반영됨 확인 + - `git diff --check -- docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 출력 없이 성공 확인 + ### 2026-07-10 Phase 20 추천 커뮤니티 게시물 상세 진입 보정 완료 - 목적: 메인 홈 추천 탭의 최근 활동 `COMMUNITY`와 인기 커뮤니티 게시물 탭 액션을 커뮤니티 게시물 상세 페이지로 연결 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/prd.md b/docs/20260602_메인_홈_추천_UI_API_연동/prd.md index a34cf866..d8093c2f 100644 --- a/docs/20260602_메인_홈_추천_UI_API_연동/prd.md +++ b/docs/20260602_메인_홈_추천_UI_API_연동/prd.md @@ -76,6 +76,7 @@ data class HomeLiveItem( ) data class HomeActiveCreatorItem( + val creatorId: Long, val creatorNickname: String, val creatorProfileImage: String, val activityType: String, @@ -159,6 +160,7 @@ enum class RecommendedActivityType(val code: String) { - `AUDIO` -> `I18n.HomeRecommendation.activityAudio` - `COMMUNITY` -> `I18n.HomeRecommendation.activityCommunity` - 알 수 없는 값은 서버 코드를 그대로 노출하지 않고 보조 문구를 숨긴다. +- `LIVE`와 `LIVE_REPLAY`는 표시 문구만 동일하게 사용하며, 탭 이동 분기를 구분할 수 있도록 서로 다른 앱 enum case로 변환한다. 다국어 기본 문구: - ko: `라이브`, `오디오`, `커뮤니티` @@ -179,6 +181,11 @@ enum class RecommendedActivityType(val code: String) { - 민감 콘텐츠 보기 설정이 꺼져 있으면 `AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)`로 안내 문구를 예약하고 `AppState.shared.setAppStep(step: .contentViewSettings)`로 이동한다. - 위 조건을 통과한 경우에만 `liveDetail`, `creatorDetail`, `contentDetail`, `characterDetail` 등 실제 상세 이동을 실행한다. - 추천 탭 현재 라이브 아이템은 `roomId`가 항상 존재하는 값으로 취급하고, 아이템 탭 시 상세 화면을 거치지 않고 기존 `LiveViewModel.enterLiveRoom(roomId:)` 흐름을 호출해 방 상세 조회, 입장 전 결제 확인, 비밀방 비밀번호 dialog, `LiveRoomViewV2` 입장을 재사용한다. +- 최근 활동 크리에이터에서 서버 원본 `activityType == LIVE`이고 `targetId != null`이면 `targetId`를 라이브룸 `roomId`로 사용해 기존 라이브룸 입장 흐름을 실행한다. +- 최근 활동 크리에이터에서 서버 원본 `activityType == LIVE`이고 `targetId == null`이면 `creatorId`를 사용해 크리에이터 채널로 이동한다. +- 최근 활동 크리에이터의 `creatorId`는 서버에서 항상 유효한 양수로 제공하지만, 앱에서도 `creatorId > 0`을 확인한 뒤에만 크리에이터 채널 이동을 실행한다. 검사를 통과하지 못하면 이동하지 않는다. +- `LIVE_REPLAY`는 `AUDIO`와 동일하게 `targetId`를 콘텐츠 ID로 사용해 콘텐츠 상세 페이지로 이동한다. `targetId`가 없으면 이동하지 않으며, `LIVE`의 `creatorId` fallback은 적용하지 않는다. +- 최근 활동 크리에이터의 `AUDIO`, `COMMUNITY`, 알 수 없는 활동 타입은 기존 탭 동작을 유지한다. - 최근 활동 크리에이터에서 `activityType == COMMUNITY`인 아이템은 `targetId`를 커뮤니티 게시물 `postId`로 취급하고, 탭 시 커뮤니티 게시물 상세 페이지로 이동한다. - 인기 커뮤니티 섹션의 커뮤니티 게시물은 `postId` 기준으로 커뮤니티 게시물 상세 페이지로 이동한다. 좋아요, 구매, 오디오 재생 액션은 기존 개별 액션을 유지한다. @@ -302,6 +309,9 @@ MainHomeView - `FeedCommunityView`에 `구매완료` 버튼이 표시되지 않는다. - 사업자 정보는 마지막 섹션에서 화면 width를 채우고, 기본 3줄 말줄임표, 더보기, 전체 표시, 접기 전환이 동작한다. - 상세 진입 탭 액션은 기존 홈과 같은 로그인, 한국 사용자 본인인증, 민감 콘텐츠 보기 설정 guard를 통과한 뒤에만 실행된다. +- 최근 활동 크리에이터의 `LIVE` 활동은 `targetId`가 있으면 라이브룸으로 이동하고, `targetId`가 없고 `creatorId > 0`이면 크리에이터 채널로 이동한다. +- 최근 활동 크리에이터의 `LIVE_REPLAY` 표시 문구는 기존과 같이 `라이브` 계열 I18n 문구를 유지하되, 탭 시에는 `AUDIO`와 동일하게 `targetId` 기반 콘텐츠 상세 페이지로 이동하며 `creatorId` fallback은 적용되지 않는다. +- 최근 활동 크리에이터의 `AUDIO`, `COMMUNITY`, 알 수 없는 활동 타입의 기존 동작에 회귀가 없다. - 최근 활동 크리에이터의 `COMMUNITY` 활동과 인기 커뮤니티 섹션의 게시물은 커뮤니티 게시물 상세 페이지로 이동한다. - `MainHomeView`는 추천/랭킹/팔로잉 탭 shell 역할만 하고, 추천 탭 섹션 조립은 `MainHomeRecommendationView`에 분리되어 있다. - 반복 UI는 기존 widget 또는 공용 컴포넌트를 재사용하고 불필요하게 중복 구현하지 않는다. @@ -312,10 +322,10 @@ MainHomeView - 각 섹션별 최대 표시 개수와 가로/세로 스크롤 정책이 Figma 기준 그대로인지, 서버 응답 전체를 모두 표시해야 하는지 확인이 필요하다. - `HomeLiveItem`에 민감/성인 라이브 여부를 판단할 `isAdult` 또는 동등한 필드가 추가되는지, 아니면 서버에서 추천 라이브를 사전 필터링하는지 확인이 필요하다. - 커뮤니티 `audioUrl`을 추천 탭 카드에서 표시/재생해야 하는지 확인이 필요하다. -- 각 카드 터치 시 상세 이동 대상이 모두 정의되어 있는지 확인이 필요하다. 단, 최근 활동 `COMMUNITY`와 인기 커뮤니티 게시물은 커뮤니티 게시물 상세 페이지로 확정한다. +- 각 카드 터치 시 상세 이동 대상이 모두 정의되어 있는지 확인이 필요하다. 단, 최근 활동 `LIVE`, `LIVE_REPLAY`, `COMMUNITY`, 인기 커뮤니티 게시물의 이동 규칙은 확정한다. ## 12. Verification Plan - 문서 검증: API URL, 응답 모델, 제외 섹션, FeedCommunityView, 사업자 정보 요구가 PRD와 계획 문서에 반영되었는지 확인한다. - 정적 검증: 신규 추천 API가 기존 `HomeApi`에 추가되지 않았고, Figma URL이 앱 코드에 직접 포함되지 않았는지 확인한다. - 빌드 검증: `docs/agent-guides/build-test-verification.md` 기준으로 가능한 iOS 빌드 또는 최소 정적 검증을 실행한다. -- 기능 검증: 추천 API 성공/실패, 빈 섹션, 활동 타입 I18n, 상세 진입 guard, 커뮤니티 카드 상태, 사업자 정보 더보기/접기를 확인한다. +- 기능 검증: 추천 API 성공/실패, 빈 섹션, 활동 타입 I18n, 상세 진입 guard, 최근 활동 `LIVE`의 `targetId` 유무별 이동, `creatorId` 양수 검사, `LIVE_REPLAY`의 콘텐츠 상세 이동, 다른 활동 타입의 회귀 여부, 커뮤니티 카드 상태, 사업자 정보 더보기/접기를 확인한다. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md new file mode 100644 index 00000000..b2d6108b --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md @@ -0,0 +1,147 @@ +# Phase 21 최근 활동 LIVE 이동 fallback 보정 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 21 / Task 21.1~21.4 | +| 기준 commit 또는 working tree | `0232cd43356dd1988dea6cf03563716542cd491c` 기준 working tree의 Phase 21 변경 6개 파일 | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `docs/20260602_메인_홈_추천_UI_API_연동/prd.md`, `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +### 목적 + +- Phase 21의 완료 체크와 실제 모델, 활동 타입 변환, 탭 목적지, callback 연결 및 검증 기록이 일치하는지 확인한다. +- `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`, 알 수 없는 활동 타입의 이동 규칙과 기존 상세 진입 guard 재사용 여부를 확인한다. + +### 포함 범위 + +- 코드: + - `SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` + - `SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift` + - `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift` + - `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` + - `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` + - `SodaLive/Sources/V2/Main/MainView.swift` +- 테스트: + - 변경된 실제 모델 소스를 `swiftc`로 컴파일한 임시 디코딩·라우팅 회귀 체크 + - 저장소에 앱 테스트 번들 타깃이 없어 별도 unit test는 실행하지 않음 +- 문서: + - PRD의 응답 스키마, 진입 규칙, 성공 기준, 검증 계획 + - `plan-task.md`의 Phase 21 Task와 검증 기록 +- 정적/빌드 검증: + - 공백 오류, 프로젝트 파일 유효성, `SodaLive-dev` Debug 빌드 + +### 제외 범위 + +- Phase 1~20 구현의 재검토 +- 실제 서버 응답 계약 자체의 재검증 +- 로그인·본인인증·민감 콘텐츠 설정 상태를 포함한 실기기/시뮬레이터 수동 탭 검증 +- Phase 21과 무관한 기존 코드와 빌드 경고 + +## 3. 판정 기준 + +### 심각도 + +| 심각도 | 기준 | +|---|---| +| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 | +| High | 확정 요구사항·API Contract 위반 또는 주요 회귀 | +| Medium | 제한된 조건에서 발생하는 기능·접근성·복구 문제 | +| Low | 유지보수성, 문서 정합성 또는 비핵심 UX 문제 | + +### 상태 + +| 상태 | 의미 | 후속 처리 | +|---|---|---| +| 후보 | 근거를 발견했지만 아직 재현·판정하지 않음 | 검증 후 상태 변경 | +| 확정 | 코드·test·문서 근거로 문제가 확인됨 | `plan-task.md` 회귀 수정 Task 후보 | +| 오탐 | 요구사항이나 실행 결과상 문제가 아님 | 근거를 남기고 종료 | +| 보류 | 외부 계약·환경·제품 결정이 필요함 | 담당 주체와 재개 조건 기록 | +| 수정 완료 | 수정과 관련 검증이 완료됨 | 실행 명령과 결과 연결 | + +## 4. 검토한 근거 + +### 문서와 코드 + +- 요구사항: + - `prd.md:78-85`의 `HomeActiveCreatorItem.creatorId` + - `prd.md:184-189`의 최근 활동 `LIVE`, `LIVE_REPLAY`, 기존 활동 타입 이동 규칙 + - `prd.md:312-314`의 Phase 21 성공 기준 +- 계획: + - `plan-task.md:540-629`의 Task 21.1~21.4 + - `plan-task.md:633-670`의 Phase 21 구현 및 정정 검증 기록 +- 코드: + - `MainHomeRecommendationResponse.swift:29-61`의 필수 `creatorId`와 탭 목적지 분기 + - `RecommendedActivityType.swift:3-38`의 `LIVE`/`LIVE_REPLAY` 분리 및 동일 I18n 문구 + - `MainHomeActiveCreatorSection.swift:3-55`의 callback 전달 + - `MainHomeRecommendationView.swift:38-44`의 `onTapCreator` 연결 + - `MainView.swift:474-509`의 양수 검사, 상세 진입 guard 및 최종 이동 +- 테스트: + - 실제 변경 모델 소스와 임시 stub/check 소스를 함께 컴파일해 9개 디코딩·라우팅 시나리오와 `LIVE`/`LIVE_REPLAY` 표시 문구 일치를 확인했다. + - 임시 소스와 실행 바이너리는 검증 직후 `/private/tmp`에서 삭제했다. + +### 실행 환경 + +```text +OS: macOS 26.0 (25A354) +Xcode: 26.0 (17A324) +Swift: 6.2 +빌드 대상: Any iOS Device / SodaLive-dev / Debug +환경 변수: 별도 설정 없음, 인증 토큰 및 민감정보 미사용 +``` + +### 실행한 검증 + +| 명령 또는 수동 검증 | 결과 | 핵심 증거 | +|---|---|---| +| `git diff --check` | 성공 | exit code 0, 출력 없음 | +| `plutil -lint SodaLive.xcodeproj/project.pbxproj` | 성공 | `OK` | +| `xcrun swiftc /private/tmp/sodalive_phase21_review_stubs.swift SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift /private/tmp/sodalive_phase21_review_check.swift -o /private/tmp/sodalive_phase21_review_check` | 성공 | 실제 변경 모델 소스 컴파일 성공 | +| `/private/tmp/sodalive_phase21_review_check` | 성공 | `Phase 21 routing checks passed: 9` | +| `xcodebuild -workspace SodaLive.xcworkspace -scheme SodaLive-dev -configuration Debug build` (sandbox) | 실패 | exit code 66, CoreSimulator·캐시 접근 제한으로 workspace를 열지 못한 환경 실패 | +| `xcodebuild -workspace SodaLive.xcworkspace -scheme SodaLive-dev -configuration Debug build` (sandbox 외 재실행) | 성공 | exit code 0, `BUILD SUCCEEDED` | +| 실기기/시뮬레이터 탭 검증 | 불가 | 실제 서버 응답과 인증 상태를 준비하지 않아 제외 범위로 유지 | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +- 문서의 `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`, unknown 분기와 실제 코드가 일치한다. +- `LIVE + targetId == nil`의 `creatorId > 0` 검사와 최종 callback의 양수 guard가 확인된다. +- 기존 `performRecommendationDetailAction`과 라이브룸 입장 흐름을 재사용한다. +- working tree 기준 정적 검증, 임시 회귀 체크 및 `SodaLive-dev` Debug 빌드가 성공했다. + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. + +실기기/시뮬레이터 수동 탭 검증은 제외 범위이며, 코드·문서 불일치나 자동 검증 실패로 판정하지 않았다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +확정 발견 사항이 없으므로 `plan-task.md` Phase 21에 신규 회귀 수정 Task를 추가하지 않는다. + +## 8. 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | Phase 21 문서, 변경 코드, callback 소비 지점, 정적 검증 및 빌드 확인 | +| 후보 항목 판정 완료 | 충족 | 남은 후보 없음 | +| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 항목 없음 | +| 검증 명령과 결과 기록 | 충족 | 4절에 실제 명령, 성공·실패 및 환경 사유 기록 | + +**최종 결론:** 확정 발견 사항 없음 + +**남은 항목:** 자동 리뷰 범위 내 없음. 실기기/시뮬레이터 수동 탭 검증은 제외 범위다. + +## 9. 수정 후 검증 기록 + +해당 없음. 확정 발견 사항과 후속 수정이 없다.