From 033a9d3a0bf30db884c115daead1b9d10bcaf43f Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Wed, 1 Jul 2026 19:40:14 +0900 Subject: [PATCH] =?UTF-8?q?docs(creator):=20=EC=B1=84=EB=84=90=20=ED=99=88?= =?UTF-8?q?=20=EA=B3=84=ED=9A=8D=EC=9D=84=20=EC=A0=95=EB=A6=AC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive.xcodeproj/project.pbxproj | 30 +- docs/20260701_크리에이터_채널_홈/plan-task.md | 552 ++++++++++++++++++ docs/20260701_크리에이터_채널_홈/prd.md | 362 ++++++++++++ 3 files changed, 929 insertions(+), 15 deletions(-) create mode 100644 docs/20260701_크리에이터_채널_홈/plan-task.md create mode 100644 docs/20260701_크리에이터_채널_홈/prd.md diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 60316f0c..23c95316 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -2836,21 +2836,6 @@ path = Following; sourceTree = ""; }; - 401C10402FEF500000A5BBA3 /* Components */ = { - isa = PBXGroup; - children = ( - 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */, - 401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */, - 401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */, - 401C10502FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.swift */, - 401C10542FEFC00000A5BBA3 /* MainHomeFollowingRankingNewsCard.swift */, - 401C10592FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift */, - 401C10582FEFD00000A5BBA3 /* MainHomeFollowingNewsSection.swift */, - 401C105D2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift */, - ); - path = Components; - sourceTree = ""; - }; 401A3FC02FEEB00100A5BBA3 /* Components */ = { isa = PBXGroup; children = ( @@ -2901,6 +2886,21 @@ path = Repository; sourceTree = ""; }; + 401C10402FEF500000A5BBA3 /* Components */ = { + isa = PBXGroup; + children = ( + 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */, + 401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */, + 401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */, + 401C10502FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.swift */, + 401C10542FEFC00000A5BBA3 /* MainHomeFollowingRankingNewsCard.swift */, + 401C10592FEFE00000A5BBA3 /* MainHomeFollowingContentNewsCard.swift */, + 401C10582FEFD00000A5BBA3 /* MainHomeFollowingNewsSection.swift */, + 401C105D2FEFF00000A5BBA3 /* MainHomeFollowingEmptyStateView.swift */, + ); + path = Components; + sourceTree = ""; + }; 40235DEA2E3797B000B09A87 /* Recent */ = { isa = PBXGroup; children = ( diff --git a/docs/20260701_크리에이터_채널_홈/plan-task.md b/docs/20260701_크리에이터_채널_홈/plan-task.md new file mode 100644 index 00000000..a36454ac --- /dev/null +++ b/docs/20260701_크리에이터_채널_홈/plan-task.md @@ -0,0 +1,552 @@ +# 크리에이터 채널 홈 구현 계획 + +## 기준 문서 + +- PRD: `docs/20260701_크리에이터_채널_홈/prd.md` +- 코드 스타일: `docs/agent-guides/code-style.md` +- 빌드/검증: `docs/agent-guides/build-test-verification.md` +- Figma 전체 화면: `296:14890` +- Figma Unfollow title bar: `296:14287` +- Figma Follow + 알림 설정 title bar: `296:14288` +- Figma Follow + 알림 해제 title bar: `296:14289` + +## 구현 원칙 + +- API, Response model, Repository, ViewModel을 먼저 구현한 뒤 UI phase를 진행한다. +- 신규 크리에이터 채널 홈 구현은 `SodaLive/Sources/V2/CreatorChannel/Home/**` 아래에 둔다. +- 크리에이터 채널 홈 내부에서만 쓰는 UI는 `SodaLive/Sources/V2/CreatorChannel/Home/Components/**` 아래에 둔다. +- 여러 화면에서 재사용할 근거가 명확한 UI만 `SodaLive/Sources/V2/Component/**`로 올린다. +- 기존 V2 홈의 `TargetType`, Repository, ViewModel, `ApiResponse` 디코딩 패턴을 따른다. +- `Long` 계열 id/count는 Swift `Int`로 통일한다. +- 빈 섹션은 section title과 empty state까지 포함해 표시하지 않는다. +- API 실패 시 전체 화면 error state로 전환하지 않고 title bar, tab-bar, 홈 외 tab placeholder처럼 서버 데이터 없이 가능한 기본 shell을 유지한다. +- `화보` 탭, 화보 섹션, 활동의 화보 row는 구현하지 않는다. +- DM 버튼과 DM 진입은 구현하지 않는다. +- Figma localhost asset URL은 앱 코드에 사용하지 않는다. +- SNS 아이콘은 `ic_sns_instagram`, `ic_sns_youtube`, `ic_sns_x`, `ic_sns_kakao`, `ic_sns_fancimm` asset을 사용한다. +- 기존 `AppStep.creatorDetail(userId:)` 진입은 새 크리에이터 채널 홈 화면으로 연결한다. + +## 주요 대상 파일 + +### 생성 + +- `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeTab.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Repository/CreatorChannelHomeApi.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Repository/CreatorChannelHomeRepository.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeViewModel.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTitleBar.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelHeaderSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTalkActionSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTabBar.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelPlaceholderTabView.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelLatestAudioSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelScheduleSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSeriesSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelIntroduceSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelActivitySection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSnsSection.swift` + +### 수정 + +- `SodaLive/Sources/ContentView.swift` +- `SodaLive/Sources/I18n/I18n.swift` +- 필요 시 `SodaLive.xcodeproj/project.pbxproj` + +### 확인 + +- `SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingApi.swift` +- `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift` +- `SodaLive/Sources/V2/Main/Home/Following/Models/HomeFollowingTabResponse.swift` +- `SodaLive/Sources/V2/Main/MainView.swift` +- `SodaLive/Sources/Chat/ChatTabView.swift` +- `SodaLive/Sources/User/UserApi.swift` +- `SodaLive/Sources/Content/Series/Detail/SeriesDetailViewModel.swift` +- `SodaLive/Sources/V2/Component/SectionTitle.swift` +- `SodaLive/Sources/V2/Component/AudioContentCard.swift` +- `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` +- `SodaLive/Sources/Explorer/Profile/Detail/CreatorDetailDialogView.swift` + +## TASK 체크리스트 + +### Phase 1: API와 상태 모델 + +- [ ] **Task 1.1: 크리에이터 채널 홈 Response model 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/Following/Models/HomeFollowingTabResponse.swift` + - 작업 내용: + - PRD의 `CreatorChannelHomeResponse`와 하위 응답 모델을 Swift `Decodable` struct로 작성한다. + - Kotlin `Long` 필드는 Swift `Int`로 선언한다. + - list item은 필요한 경우 `Identifiable`을 채택하고 id는 서버 id 필드를 사용한다. + - `CreatorActivityType`은 기존 enum을 재사용하거나 공용 위치로 이동한다. 중복 enum을 만들지 않는다. + - nullable URL 필드는 PRD nullable 여부에 맞춰 optional로 선언한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelHomeResponse|struct CreatorChannelCreatorResponse|struct CreatorChannelLiveResponse|struct CreatorChannelAudioContentResponse|struct CreatorChannelDonationResponse|struct CreatorChannelScheduleResponse|struct CreatorChannelSeriesResponse|struct CreatorChannelCommunityPostResponse|struct CreatorChannelFanTalkSummaryResponse|struct CreatorChannelActivityResponse|struct CreatorChannelSnsResponse" SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift` + - 기대 결과: PRD의 응답 모델이 Swift 파일에 모두 존재하고 id/count 타입이 `Int`이다. + +- [ ] **Task 1.2: 홈 tab enum 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeTab.swift` + - 작업 내용: + - `CreatorChannelHomeTab` enum을 `CaseIterable, Hashable`로 만든다. + - case는 `home`, `live`, `audio`, `series`, `community`, `fanTalk`, `donation` 순서로 둔다. + - title은 `홈`, `라이브`, `오디오`, `시리즈`, `커뮤니티`, `팬Talk`, `후원`을 반환한다. + - `photo` 또는 `화보` case는 만들지 않는다. + - 검증 기준: + - 실행 명령: `rg "enum CreatorChannelHomeTab|case home|case live|case audio|case series|case community|case fanTalk|case donation|화보|photo" SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeTab.swift` + - 기대 결과: 7개 tab case가 있고 `화보`/`photo` case가 없다. + +- [ ] **Task 1.3: API endpoint 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Repository/CreatorChannelHomeApi.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingApi.swift` + - 작업 내용: + - `CreatorChannelHomeApi`를 `TargetType`으로 만든다. + - case는 `getHome(creatorId: Int)` 하나로 시작한다. + - path는 `/api/v2/creator-channels/{creatorId}/home` 형식으로 구성한다. + - method는 `.get`, task는 `.requestPlain`로 둔다. + - headers는 기존 V2 API와 동일하게 `Authorization: Bearer {token}` 패턴을 따른다. + - 검증 기준: + - 실행 명령: `rg "enum CreatorChannelHomeApi|getHome\\(creatorId: Int\\)|/api/v2/creator-channels/|/home|TargetType|requestPlain|Authorization" SodaLive/Sources/V2/CreatorChannel/Home/Repository/CreatorChannelHomeApi.swift` + - 기대 결과: 신규 endpoint와 인증 헤더가 확인된다. + +- [ ] **Task 1.4: Repository 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Repository/CreatorChannelHomeRepository.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingRepository.swift` + - 작업 내용: + - `CreatorChannelHomeRepository`를 만들고 `MoyaProvider`를 사용한다. + - `getHome(creatorId:) -> AnyPublisher` 메서드를 제공한다. + - 검증 기준: + - 실행 명령: `rg "final class CreatorChannelHomeRepository|MoyaProvider|getHome\\(creatorId: Int\\)|AnyPublisher" SodaLive/Sources/V2/CreatorChannel/Home/Repository/CreatorChannelHomeRepository.swift` + - 기대 결과: 기존 V2 Repository와 같은 형태가 확인된다. + +- [ ] **Task 1.5: ViewModel 생성 및 API 실패 placeholder 상태 정의** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeViewModel.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift` + - 확인: `SodaLive/Sources/User/UserApi.swift` + - 확인: `SodaLive/Sources/Content/Series/Detail/SeriesDetailViewModel.swift` + - 작업 내용: + - `CreatorChannelHomeViewModel`을 `ObservableObject`로 만든다. + - `@Published var isLoading`, `@Published var response`, `@Published var hasLoaded`, `@Published var selectedTab`, `@Published var isApiFailedPlaceholderVisible`를 둔다. + - `fetchHome(creatorId:)`에서 `ApiResponse`를 디코딩한다. + - 성공 시 `response`, `selectedTab = .home`, `isApiFailedPlaceholderVisible = false`를 갱신한다. + - 실패/디코딩 실패 시 `response = nil`, `isApiFailedPlaceholderVisible = true`, `hasLoaded = true`로 두어 기본 shell은 유지할 수 있게 한다. + - `follow(creatorId:notify:)`, `unfollow(creatorId:)`는 기존 `UserRepository.creatorFollow`, `UserRepository.creatorUnFollow` 패턴을 재사용한다. + - mutation 성공 시 `response?.creator.isFollow`, `response?.creator.isNotify`가 갱신될 수 있도록 모델 갱신 방식을 구현한다. struct 불변성 때문에 필요한 경우 creator 복사 helper를 둔다. + - 검증 기준: + - 실행 명령: `rg "final class CreatorChannelHomeViewModel|fetchHome\\(creatorId: Int\\)|ApiResponse|isApiFailedPlaceholderVisible|creatorFollow|creatorUnFollow|selectedTab|hasLoaded" SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeViewModel.swift` + - 기대 결과: 홈 API 디코딩, API 실패 placeholder 상태, 기존 follow/unfollow API 호출이 확인된다. + +### Phase 2: 라우팅과 기본 화면 shell + +- [ ] **Task 2.1: 기존 creatorDetail 라우팅을 신규 홈으로 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/ContentView.swift` + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/App/AppStep.swift` + - 작업 내용: + - `AppStep.creatorDetail(userId:)` case는 유지한다. + - `ContentView`에서 `.creatorDetail(let userId)` destination을 `CreatorChannelHomeView(creatorId: userId)`로 변경한다. + - 기존 `UserProfileView`를 삭제하거나 수정하지 않는다. + - `CreatorChannelHomeView`는 `creatorId: Int`를 받아 기본 shell을 렌더링한다. + - 검증 기준: + - 실행 명령: `rg "case \\.creatorDetail|CreatorChannelHomeView\\(creatorId: userId\\)|UserProfileView\\(userId:" SodaLive/Sources/ContentView.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: `.creatorDetail`이 신규 `CreatorChannelHomeView`로 연결되고 기존 `UserProfileView` 파일은 유지된다. + +- [ ] **Task 2.2: 기본 shell과 로딩/API 실패 placeholder 구성** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelPlaceholderTabView.swift` + - 작업 내용: + - `@StateObject private var viewModel = CreatorChannelHomeViewModel()`를 사용한다. + - 최초 `onAppear`에서 `hasLoaded == false`일 때 `fetchHome(creatorId:)`를 호출한다. + - root background는 black으로 둔다. + - `response == nil && isApiFailedPlaceholderVisible == true`이면 title bar, tab-bar, 선택된 tab placeholder만 표시한다. + - 로딩 중에는 기존 `LoadingView` 또는 V2 관례에 맞는 loading view를 사용한다. + - 홈 외 tab 선택 시 `CreatorChannelPlaceholderTabView(title: selectedTab.title)`을 표시한다. + - 검증 기준: + - 실행 명령: `rg "@StateObject private var viewModel|fetchHome\\(creatorId: creatorId\\)|isApiFailedPlaceholderVisible|CreatorChannelPlaceholderTabView|LoadingView|Color.black" SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelPlaceholderTabView.swift` + - 기대 결과: API 호출, 실패 placeholder, 홈 외 tab placeholder shell이 확인된다. + +- [ ] **Task 2.3: I18n 문구 추가** + - 대상 파일: + - 수정: `SodaLive/Sources/I18n/I18n.swift` + - 작업 내용: + - `I18n.CreatorChannelHome` enum을 추가한다. + - 섹션명 `currentLive`, `latestAudio`, `donation`, `notice`, `schedule`, `audio`, `series`, `community`, `fanTalk`, `introduce`, `activity`, `sns`를 추가한다. + - 액션 문구 `follow`, `talk`, `viewAll`를 추가한다. + - 활동 row 문구 `debut`, `liveCount`, `liveDurationHours`, `liveContributorCount`, `audioContentCount`, `seriesCount`를 추가한다. + - ko/en/ja 문구를 모두 제공한다. + - 검증 기준: + - 실행 명령: `rg "enum CreatorChannelHome|currentLive|latestAudio|fanTalk|introduce|activity|viewAll|liveDurationHours|seriesCount" SodaLive/Sources/I18n/I18n.swift` + - 기대 결과: 신규 사용자 노출 문구가 `I18n`에 존재한다. + +### Phase 3: Header / Title Bar / Tab Bar / Sticky + +- [ ] **Task 3.1: title bar 컴포넌트 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTitleBar.swift` + - 확인: `SodaLive/Sources/Common/FollowButtonImageAsset.swift` + - 작업 내용: + - back 버튼, 팔로우/팔로잉 상태 버튼, 알림 상태 아이콘, 더보기 버튼을 표시한다. + - `isFollow == false`이면 `팔로우` capsule을 표시한다. + - `isFollow == true && isNotify == true`이면 팔로잉 상태 capsule과 알림 설정 아이콘을 표시한다. + - `isFollow == true && isNotify == false`이면 팔로잉 상태 capsule과 알림 해제 아이콘을 표시한다. + - 더보기 버튼은 항상 표시한다. + - 배경 opacity는 외부에서 전달받는 `backgroundProgress`로 제어한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelTitleBar|isFollow|isNotify|backgroundProgress|onTapBack|onTapFollow|onTapUnfollow|onTapMore|팔로우" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTitleBar.swift` + - 기대 결과: Figma 3개 title bar 상태를 표현하는 조건 분기가 확인된다. + +- [ ] **Task 3.2: 크리에이터 이미지/프로필 헤더 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelHeaderSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 작업 내용: + - `creator.profileImageUrl`을 큰 배경 이미지와 프로필 이미지에 동일하게 사용한다. + - 이미지 영역은 OS status bar까지 확장되도록 top safe area를 무시한다. + - `creator.nickname`, `creator.followerCount`를 표시한다. + - 이미지 로드 실패 시 기존 `DownsampledKFImage` 또는 `KFImage` placeholder 관례를 따른다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelHeaderSection|profileImageUrl|ignoresSafeArea\\(.*top|followerCount|DownsampledKFImage|KFImage" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelHeaderSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: profile image가 헤더 큰 이미지로 쓰이고 top safe area 확장이 확인된다. + +- [ ] **Task 3.3: horizontal tab-bar 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTabBar.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/V2/Component/TextTabBar.swift` + - 작업 내용: + - `CreatorChannelHomeTab.allCases`를 horizontal scroll로 표시한다. + - 선택된 tab은 white text와 soda color bottom indicator로 표시한다. + - 비선택 tab은 gray text로 표시한다. + - `화보` tab은 표시하지 않는다. + - sticky 상태일 때도 같은 컴포넌트를 사용한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelTabBar|ScrollView\\(\\.horizontal|CreatorChannelHomeTab.allCases|selectedTab|soda|indicator|화보" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTabBar.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 7개 tab만 표시되고 선택 상태 UI가 확인된다. + +- [ ] **Task 3.4: scroll progress와 sticky 계산 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTitleBar.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTabBar.swift` + - 작업 내용: + - scroll offset 또는 geometry preference로 tab-bar와 title bar의 거리를 계산한다. + - 거리에 따라 title bar background progress를 0...1로 clamp한다. + - tab-bar가 title bar에 닿으면 tab-bar를 sticky 상태로 고정한다. + - sticky 이후 OS status bar 영역이 title bar와 동일한 black 배경으로 보이게 상단 overlay를 둔다. + - 검증 기준: + - 실행 명령: `rg "PreferenceKey|GeometryReader|backgroundProgress|isTabBarSticky|clamp|safeAreaInsets|CreatorChannelTitleBar|CreatorChannelTabBar" SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTitleBar.swift SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTabBar.swift` + - 기대 결과: scroll 기반 title bar opacity와 sticky tab-bar 상태 계산이 확인된다. + +- [ ] **Task 3.5: Header/TitleBar/TabBar만으로 sticky 조기 검증** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 작업 내용: + - `#if DEBUG` preview 전용 sticky scaffold를 `CreatorChannelHomeView_Previews` 또는 같은 파일의 private preview view로 둔다. + - preview scaffold는 sample creator, `CreatorChannelHeaderSection`, `CreatorChannelTitleBar`, `CreatorChannelTabBar`, 충분한 높이의 dummy rows를 사용한다. + - production runtime에는 preview 전용 dummy rows가 노출되지 않게 `#if DEBUG` 범위에만 둔다. + - Xcode Preview 또는 simulator preview route에서 스크롤하며 title bar black 전환과 tab-bar sticky 고정을 수동 확인한다. + - 검증 기준: + - 실행 명령: `rg "#if DEBUG|CreatorChannelHomeView_Previews|CreatorChannelHeaderSection|CreatorChannelTitleBar|CreatorChannelTabBar|dummy|isTabBarSticky|backgroundProgress" SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: API와 하위 섹션 구현 전에도 Header/TitleBar/TabBar sticky 동작을 확인할 수 있는 debug-only preview scaffold가 존재한다. + +### Phase 4: 홈 외 placeholder + +- [ ] **Task 4.1: 홈 외 tab placeholder 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelPlaceholderTabView.swift` + - 작업 내용: + - `selectedTab != .home`이면 실제 목록 대신 `CreatorChannelPlaceholderTabView(title: selectedTab.title)`을 표시한다. + - 홈 섹션의 전체보기 action 중 별도 기존 route가 없는 경우 대응 tab을 선택하도록 연결한다. + - 팬Talk 전체보기는 `selectedTab = .fanTalk`로 이동한다. + - 검증 기준: + - 실행 명령: `rg "selectedTab != \\.home|CreatorChannelPlaceholderTabView\\(title: selectedTab.title\\)|selectedTab = \\.fanTalk|selectedTab = \\.audio|selectedTab = \\.series|selectedTab = \\.community|selectedTab = \\.donation" SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 홈 외 tab과 전체보기 placeholder 전환이 확인된다. + +### Phase 5: 현재 라이브 섹션 + +- [ ] **Task 5.1: 현재 라이브 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 작업 내용: + - `currentLive == nil`이면 섹션을 표시하지 않는다. + - `liveId`, `title`, `coverImageUrl`, `beginDateTimeUtc`, `price`, `isAdult`를 표시한다. + - `coverImageUrl`이 없으면 기존 이미지 placeholder 패턴을 사용한다. + - `isAdult == true`이면 기존 성인 콘텐츠 표시 asset/pattern을 사용한다. + - card tap은 기존 live detail/live room 진입 흐름이 확인되면 연결하고, 없으면 action closure만 둔다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelCurrentLiveSection|CreatorChannelLiveResponse|currentLive|liveId|coverImageUrl|isAdult|onTapLive" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: `currentLive` optional 기반 숨김과 live item 표시가 확인된다. + +### Phase 6: 최신 오디오 콘텐츠 섹션 + +- [ ] **Task 6.1: 최신 오디오 콘텐츠 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelLatestAudioSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/V2/Component/AudioContentCard.swift` + - 작업 내용: + - `latestAudioContent == nil`이면 섹션을 표시하지 않는다. + - `AudioContentCard` 재사용이 맞으면 사용하고, 태그/잠금 표시가 부족하면 채널 홈 전용 thumbnail wrapper를 만든다. + - `isAdult`, `isPointAvailable`, `isFirstContent`, `seriesName`, `isOriginalSeries` 표시를 기존 태그 관례에 맞춘다. + - card tap은 기존 `AppStep.contentDetail(contentId:)` 흐름이 맞으면 연결한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelLatestAudioSection|latestAudioContent|AudioContentCard|isPointAvailable|isFirstContent|isOriginalSeries|contentDetail" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelLatestAudioSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 최신 오디오 optional 기반 숨김, 태그 표시, 상세 진입이 확인된다. + +### Phase 7: 채널 후원 섹션 + +- [ ] **Task 7.1: 채널 후원 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/App/AppStep.swift` + - 확인: `SodaLive/Sources/ContentView.swift` + - 작업 내용: + - `channelDonations.isEmpty`이면 섹션을 표시하지 않는다. + - 각 item의 `nickname`, `profileImageUrl`, `can`, `message`, `createdAtUtc`를 표시한다. + - 전체보기는 기존 `AppStep.channelDonationAll(creatorId:)`로 연결한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelDonationSection|channelDonations|CreatorChannelDonationResponse|channelDonationAll|createdAtUtc|can" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 빈 후원 섹션 숨김과 기존 후원 전체보기 route 연결이 확인된다. + +### Phase 8: 공지 섹션 + +- [ ] **Task 8.1: 공지 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` + - 작업 내용: + - `notices.isEmpty`이면 섹션을 표시하지 않는다. + - `CreatorChannelCommunityPostResponse`를 사용한다. + - 공지 섹션은 커뮤니티와 별도 component로 둔다. + - `imageUrl`, `audioUrl`, `price`, `existOrdered`, `likeCount`, `commentCount`를 기존 커뮤니티 카드 패턴으로 표시한다. + - item tap/댓글/구매 action은 기존 route가 확인되는 범위에서만 연결하고, 불명확한 action은 빈 closure로 둔다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelNoticeSection|notices|CreatorChannelCommunityPostResponse|CommunityPostCard|existOrdered|commentCount" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 공지 섹션이 커뮤니티 섹션과 분리되어 있고 빈 배열 숨김이 확인된다. + +### Phase 9: 스케줄 섹션 + +- [ ] **Task 9.1: 스케줄 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelScheduleSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/V2/Main/MainView.swift` + - 작업 내용: + - `schedules.isEmpty`이면 섹션을 표시하지 않는다. + - 각 item의 `scheduledAtUtc`, `title`, `type`, `targetId`를 표시한다. + - tap action은 closure로 분리하고, `.live`, `.liveReplay`, `.audio`, `.community` 분기는 기존 `MainView.handleFollowingScheduleTap`과 같은 기준을 따른다. + - `.unknown`은 crash 없이 무시한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelScheduleSection|schedules|scheduledAtUtc|CreatorActivityType|targetId|unknown|onTapSchedule" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelScheduleSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 스케줄 표시와 type별 tap closure가 확인된다. + +### Phase 10: 오디오 섹션 + +- [ ] **Task 10.1: 오디오 목록 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/V2/Component/AudioContentCard.swift` + - 작업 내용: + - `audioContents.isEmpty`이면 섹션을 표시하지 않는다. + - Figma 홈 노출 개수만 표시한다. + - `AudioContentCard`와 채널 홈 전용 thumbnail wrapper를 재사용한다. + - 전체보기는 `selectedTab = .audio`로 연결한다. + - item tap은 기존 `AppStep.contentDetail(contentId:)` 흐름으로 연결한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelAudioSection|audioContents|AudioContentCard|prefix|selectedTab = \\.audio|contentDetail" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 빈 배열 숨김, 홈 노출 개수 제한, 오디오 tab 전체보기 연결이 확인된다. + +### Phase 11: 시리즈 섹션 + +- [ ] **Task 11.1: 시리즈 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSeriesSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 작업 내용: + - `series.isEmpty`이면 섹션을 표시하지 않는다. + - 각 item의 `seriesId`, `title`, `coverImageUrl`, `numberOfContent`, `isNew`, `isOriginal`을 표시한다. + - 전체보기는 `selectedTab = .series`로 연결한다. + - item tap은 기존 `AppStep.seriesDetail(seriesId:)` 흐름으로 연결한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelSeriesSection|seriesId|coverImageUrl|numberOfContent|isNew|isOriginal|selectedTab = \\.series|seriesDetail" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSeriesSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 시리즈 카드, 전체보기, 상세 진입이 확인된다. + +### Phase 12: 커뮤니티 섹션 + +- [ ] **Task 12.1: 커뮤니티 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` + - 작업 내용: + - `communities.isEmpty`이면 섹션을 표시하지 않는다. + - `CommunityPostCard`가 요구사항과 맞으면 재사용한다. + - 유료/잠금 게시물은 기존 `price`, `existOrdered` 처리 패턴을 따른다. + - 전체보기는 `selectedTab = .community`로 연결한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelCommunitySection|communities|CommunityPostCard|existOrdered|onTapPurchase|selectedTab = \\.community" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 커뮤니티 섹션과 전체보기 tab 전환이 확인된다. + +### Phase 13: 팬Talk 섹션 + +- [ ] **Task 13.1: 팬Talk 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 작업 내용: + - `fanTalk.latestFanTalk == nil`이면 섹션을 표시하지 않는다. + - `totalCount`, `nickname`, `profileImageUrl`, `content`, `languageCode`, `createdAtUtc`를 표시한다. + - 전체보기는 `selectedTab = .fanTalk`로 연결한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelFanTalkSection|latestFanTalk|totalCount|languageCode|createdAtUtc|selectedTab = \\.fanTalk" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 최신 팬Talk가 있을 때만 표시되고 전체보기는 팬Talk tab으로 이동한다. + +### Phase 14: 소개 섹션 + +- [ ] **Task 14.1: 소개 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelIntroduceSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 작업 내용: + - `introduce`가 빈 문자열이면 섹션을 표시하지 않는다. + - 긴 텍스트는 화면 폭을 넘지 않게 줄바꿈한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelIntroduceSection|introduce|trimmingCharacters|fixedSize|lineLimit" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelIntroduceSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 빈 소개 숨김과 긴 텍스트 줄바꿈 처리가 확인된다. + +### Phase 15: 활동 섹션 + +- [ ] **Task 15.1: 활동 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelActivitySection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 작업 내용: + - `debutDateUtc`와 `dDay`를 `2026.06.11(D+1)` 형식으로 표시한다. + - `liveCount`, `liveDurationHours`, `liveContributorCount`, `audioContentCount`, `seriesCount`를 표시한다. + - `화보` row는 만들지 않는다. + - 숫자는 기존 `.comma()` 등 프로젝트 formatting 패턴을 따른다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelActivitySection|debutDateUtc|dDay|liveCount|liveDurationHours|liveContributorCount|audioContentCount|seriesCount|화보|comma" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelActivitySection.swift` + - 기대 결과: 활동 6개 항목만 표시되고 화보 row가 없다. + +### Phase 16: SNS 섹션 + +- [ ] **Task 16.1: SNS 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSnsSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/Explorer/Profile/Detail/CreatorDetailDialogView.swift` + - 작업 내용: + - `instagramUrl`, `youtubeUrl`, `xUrl`, `kakaoOpenChatUrl`, `fancimmUrl` 중 유효한 URL만 표시한다. + - 표시 순서는 `Instagram`, `YouTube`, `X`, `KakaoTalk`, `Fancimm(팬심M)`이다. + - 아이콘은 각각 `ic_sns_instagram`, `ic_sns_youtube`, `ic_sns_x`, `ic_sns_kakao`, `ic_sns_fancimm`을 사용한다. + - tap 시 `openURL` 또는 기존 in-app web 흐름을 따른다. + - 유효한 SNS URL이 하나도 없으면 섹션을 표시하지 않는다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelSnsSection|ic_sns_instagram|ic_sns_youtube|ic_sns_x|ic_sns_kakao|ic_sns_fancimm|openURL|fancimmUrl" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSnsSection.swift` + - 기대 결과: SNS asset과 URL 필터링, tap 처리 방식이 확인된다. + +### Phase 17: 대화하기 액션 영역 + +- [ ] **Task 17.1: 대화하기 액션 섹션 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTalkActionSection.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 작업 내용: + - `isAiChatAvailable == true`일 때만 대화하기 버튼을 표시한다. + - 버튼 아이콘은 `ic_new_talk`를 사용한다. + - 버튼은 영역 중앙에 배치한다. + - 버튼이 없어도 영역 높이와 vertical spacing을 유지한다. + - `isDmAvailable`과 `ic_new_dm`은 이번 UI에 사용하지 않는다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelTalkActionSection|isAiChatAvailable|ic_new_talk|ic_new_dm|frame\\(height|onTapTalk" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTalkActionSection.swift` + - 기대 결과: 대화하기만 조건부 표시되고 DM 버튼 구현이 없다. + +- [ ] **Task 17.2: 기존 AI 채팅방 진입 guard 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/Chat/ChatTabView.swift` + - 확인: `SodaLive/Sources/App/AppStep.swift` + - 작업 내용: + - token이 없으면 `.login`으로 이동한다. + - 국가 코드를 trim/uppercase 처리하고, 비어 있거나 `KR`이면 한국으로 취급한다. + - 한국 사용자는 `auth == false`일 때 기존 본인인증 confirm/fullScreenCover 흐름을 재사용하거나 같은 조건으로 연결한다. + - 비한국 사용자는 기존 `ChatTabView`의 비한국 분기와 동일하게 `auth` 조건 비교를 적용한다. + - 본인인증과 콘텐츠 보기 설정 guard를 통과하면 `creator.characterId`로 기존 AI 채팅방/캐릭터 상세 진입 흐름을 연결한다. + - 검증 기준: + - 실행 명령: `rg "countryCode|uppercased\\(\\)|isKoreanCountry|auth == false|isShowAuthConfirmView|characterId|characterDetail|setAppStep\\(step: \\.login\\)" SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: 기존 AI 채팅 guard와 동일한 국가/본인인증 분기가 확인된다. + +### Phase 18: 최종 조립과 검증 + +- [ ] **Task 18.1: 홈 섹션 순서 조립** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 작업 내용: + - 홈 탭 섹션 순서를 PRD와 동일하게 조립한다. + - 순서: 헤더, 대화하기, 현재 라이브, 최신 오디오, 채널 후원, 공지, 스케줄, 오디오, 시리즈, 커뮤니티, 팬Talk, 소개, 활동, SNS. + - 각 섹션은 자신의 데이터가 없으면 숨긴다. + - API 실패 placeholder 상태에서는 데이터 기반 섹션을 표시하지 않는다. + - 검증 기준: + - 실행 명령: `rg -n "CreatorChannelHeaderSection|CreatorChannelTalkActionSection|CreatorChannelCurrentLiveSection|CreatorChannelLatestAudioSection|CreatorChannelDonationSection|CreatorChannelNoticeSection|CreatorChannelScheduleSection|CreatorChannelAudioSection|CreatorChannelSeriesSection|CreatorChannelCommunitySection|CreatorChannelFanTalkSection|CreatorChannelIntroduceSection|CreatorChannelActivitySection|CreatorChannelSnsSection" SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 기대 결과: PRD의 홈 섹션 순서대로 조립되어 있다. + +- [ ] **Task 18.2: 프로젝트 파일 포함 여부 확인** + - 대상 파일: + - 확인/필요 시 수정: `SodaLive.xcodeproj/project.pbxproj` + - 작업 내용: + - 신규 Swift 파일이 Xcode target에 포함되는지 확인한다. + - 자동 포함되지 않는 프로젝트 구조이면 `project.pbxproj`에 신규 파일을 추가한다. + - 기존 사용자 변경이 있는 경우 해당 변경을 되돌리지 않는다. + - 검증 기준: + - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - 기대 결과: 신규 파일 누락으로 인한 compile error가 없다. + +- [ ] **Task 18.3: PRD 성공 기준 회귀 점검** + - 대상 파일: + - 확인: `docs/20260701_크리에이터_채널_홈/prd.md` + - 확인: `SodaLive/Sources/V2/CreatorChannel/Home/**` + - 작업 내용: + - PRD `Success Criteria`를 한 줄씩 대조한다. + - `화보`, DM 버튼, API 실패 전체 error state가 구현에 들어가지 않았는지 검색한다. + - `ic_sns_*`, `ic_new_talk`, follow/unfollow, AI 채팅 guard가 구현에 포함됐는지 검색한다. + - 검증 기준: + - 실행 명령: `rg -n "화보|photo|ic_new_dm|DM|ic_new_talk|ic_sns_|creatorFollow|creatorUnFollow|isApiFailedPlaceholderVisible|countryCode|auth == false" SodaLive/Sources/V2/CreatorChannel/Home SodaLive/Sources/ContentView.swift` + - 기대 결과: 제외 범위는 UI 구현에 없고, 필수 요구사항 키워드는 확인된다. + +- [ ] **Task 18.4: 빌드 검증** + - 대상 파일: + - 확인: `docs/agent-guides/build-test-verification.md` + - 작업 내용: + - 공식 검증 명령으로 Debug build를 실행한다. + - 실패 시 에러 로그를 기준으로 최소 수정하고 같은 명령을 다시 실행한다. + - 검증 기준: + - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - 기대 결과: exit code 0으로 빌드가 완료된다. + +## 검증 기록 + +- 2026-07-01: `docs/20260701_크리에이터_채널_홈/prd.md`를 기준으로 API 선구현, Figma 섹션별 phase, placeholder tab, API 실패 placeholder, DM/화보 제외 범위를 계획에 반영했다. +- 2026-07-01: `docs/agent-guides/documentation-policy.md`를 확인해 `plan-task.md` 경로와 phase/task/checklist 형식을 검증했다. +- 2026-07-01: `docs/agent-guides/build-test-verification.md`를 확인해 최종 빌드 검증 명령을 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 반영했다. +- 2026-07-01: 기존 V2 홈 API/Repository/ViewModel, AI 채팅 guard, follow/unfollow API, SNS asset 사용처를 확인해 대상 파일과 검증 기준에 반영했다. +- 2026-07-01: 사용자 요청에 따라 기존 Phase 4 `대화하기 액션 영역`을 Phase 17로 이동하고, 기존 Phase 5~17을 Phase 4~16으로 한 칸씩 당겼다. +- 2026-07-01: Header, TitleBar, TabBar만으로 sticky 동작을 먼저 확인할 수 있도록 Phase 3에 `CreatorChannelHomeView_Previews` 기반 debug-only sticky scaffold 검증 task를 추가했다. diff --git a/docs/20260701_크리에이터_채널_홈/prd.md b/docs/20260701_크리에이터_채널_홈/prd.md new file mode 100644 index 00000000..ad710058 --- /dev/null +++ b/docs/20260701_크리에이터_채널_홈/prd.md @@ -0,0 +1,362 @@ +# PRD: 크리에이터 채널 홈 + +## 1. Overview +크리에이터 채널 홈 화면에서 신규 API `GET /api/v2/creator-channels/{creatorId}/home` 응답을 사용해 크리에이터 프로필, 현재 라이브, 최신 오디오 콘텐츠, 후원, 공지, 스케줄, 오디오, 시리즈, 커뮤니티, 팬Talk, 소개, 활동, SNS 정보를 표시한다. + +API 연동을 먼저 구현하고, UI는 API 응답 데이터를 기준으로 Figma 섹션 단위로 나누어 순차 구현한다. + +Figma 전체 화면 기준 홈 탭 섹션 순서는 아래를 따른다. + +1. 크리에이터 이미지/프로필 헤더 +2. 대화하기 액션 영역 +3. 현재 라이브 +4. 최신 오디오 콘텐츠 +5. 채널 후원 +6. 공지 +7. 스케줄 +8. 오디오 +9. 시리즈 +10. 커뮤니티 +11. 팬Talk +12. 소개 +13. 활동 +14. SNS + +상단 tab-bar 항목은 `홈`, `라이브`, `오디오`, `시리즈`, `커뮤니티`, `팬Talk`, `후원`으로 구성한다. Figma에는 `화보` 탭과 활동 항목이 보이지만 이번 구현 범위에서는 제외한다. + +## 2. Problem +- 기존 크리에이터 상세/채널 화면은 신규 `creator-channels` 홈 API 응답 구조와 Figma의 채널 홈 구성을 그대로 반영하지 못한다. +- 홈 화면은 여러 도메인 데이터를 한 endpoint에서 받지만, 각 섹션의 UI와 전체보기 진입 대상이 달라 한 파일에 직접 구현하면 유지보수와 검증이 어려워진다. +- 크리에이터 이미지 영역이 OS status bar 영역까지 확장되고, scroll 위치에 따라 title bar/tab-bar/status bar 상태가 함께 변해야 하므로 일반적인 정적 navigation bar로 처리하기 어렵다. +- tab-bar는 title bar와 붙는 순간 sticky 상태가 되어야 하고, sticky 이후에는 아래 콘텐츠만 스크롤되어야 한다. +- `isAiChatAvailable` 값에 따라 대화하기 버튼 노출이 달라지지만, 버튼이 없어도 액션 영역 높이는 유지해야 한다. +- 선택된 tab 및 홈 섹션의 `전체보기` 진입 화면은 전체 기능 구현 전에도 placeholder destination이 필요하다. + +## 3. Goals +- `creatorId`로 `GET /api/v2/creator-channels/{creatorId}/home`을 호출하고 응답 데이터로 홈 화면을 구성한다. +- 신규 API, Repository, ViewModel, Response model은 `SodaLive/Sources/V2/**` 하위에 둔다. +- API 구현을 UI보다 먼저 완료해 홈 섹션들이 동일한 response model을 사용하도록 한다. +- Figma 기준 섹션을 이후 `plan-task.md`에서 phase 단위로 나눌 수 있도록 요구사항을 분리한다. +- 크리에이터 대표 이미지는 화면 최상단에서 OS status bar 영역까지 확장해 표시한다. +- scroll 진행도에 따라 title bar 배경색을 점진적으로 black에 가깝게 변경한다. +- tab-bar가 title bar와 맞닿으면 sticky 상태로 고정하고, sticky 이후 OS status bar 배경도 title bar와 동일하게 보이도록 처리한다. +- title bar는 `isFollow`, `isNotify` 상태에 따라 Figma의 Unfollow, Follow+알림 설정, Follow+알림 해제 상태를 표시한다. +- 홈 tab 외 tab-bar 항목 및 홈 섹션의 전체보기 destination은 placeholder 형태로 먼저 연결한다. +- V2 하위의 재사용 가능한 공용 컴포넌트는 우선 재사용한다. +- 빈 섹션은 section title과 empty state를 표시하지 않고 숨긴다. + +## 4. Non-Goals +- `화보` 탭과 화보 섹션은 구현하지 않는다. +- 이번 API 스펙에 없는 페이지네이션, 무한 스크롤, 섹션별 필터/정렬 기능을 추가하지 않는다. +- 후원 작성, 댓글/좋아요, 커뮤니티 구매, 콘텐츠 구매의 실제 mutation 구현은 이번 PRD의 필수 범위로 보지 않는다. +- DM 버튼 및 DM 보내기 진입은 추후 작업으로 미루고 이번 범위에서 구현하지 않는다. +- 라이브룸, 오디오 상세, 시리즈 상세, 커뮤니티 상세, 팬Talk 전체, 후원 전체의 실제 상세 기능을 새로 구현하지 않는다. 기존 진입 흐름이 있으면 연결하고, 없으면 placeholder로 둔다. +- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다. +- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다. + +## 5. Target Users +- 크리에이터 채널에서 라이브, 콘텐츠, 커뮤니티, 후원, 팬Talk 정보를 한 번에 확인하려는 사용자 +- 크리에이터를 팔로우하거나 알림 상태를 확인하려는 사용자 +- 크리에이터와 AI 대화로 이동하려는 사용자 +- 특정 섹션의 전체 목록으로 이동해 더 많은 콘텐츠를 탐색하려는 사용자 + +## 6. User Stories +- 사용자는 크리에이터 채널 홈에서 크리에이터 이미지, 닉네임, 팔로워 수를 확인하고 싶다. +- 사용자는 팔로우하지 않은 크리에이터를 title bar에서 팔로우하고 싶다. +- 사용자는 팔로우한 크리에이터의 알림 설정 상태를 title bar에서 구분하고 싶다. +- 사용자는 현재 진행 중인 라이브가 있으면 홈 상단에서 바로 확인하고 싶다. +- 사용자는 최신 오디오 콘텐츠와 오디오 목록을 홈에서 빠르게 탐색하고 싶다. +- 사용자는 채널 후원 메시지와 후원 전체 목록을 확인하고 싶다. +- 사용자는 공지, 스케줄, 커뮤니티, 팬Talk를 홈에서 요약 형태로 확인하고 전체보기로 이동하고 싶다. +- 사용자는 크리에이터 소개, 활동 통계, SNS 링크를 채널 홈 하단에서 확인하고 싶다. + +## 7. Core Requirements + +### 7.1 크리에이터 채널 홈 데이터 조회 + +#### API +- Method: `GET` +- Path: `/api/v2/creator-channels/{creatorId}/home` +- Path parameter: `creatorId` +- 인증: 기존 V2 API 인증 헤더 패턴을 따른다. +- 응답 래퍼: 기존 관례대로 `ApiResponse` 디코딩을 우선한다. + +#### Response 기준 +```kotlin +data class CreatorChannelHomeResponse( + val creator: CreatorChannelCreatorResponse, + val currentLive: CreatorChannelLiveResponse?, + val latestAudioContent: CreatorChannelAudioContentResponse?, + val channelDonations: List, + val notices: List, + val schedules: List, + val audioContents: List, + val series: List, + val communities: List, + val fanTalk: CreatorChannelFanTalkSummaryResponse, + val introduce: String, + val activity: CreatorChannelActivityResponse, + val sns: CreatorChannelSnsResponse +) +``` + +#### Swift 모델 기준 +```swift +struct CreatorChannelHomeResponse: Decodable { + let creator: CreatorChannelCreatorResponse + let currentLive: CreatorChannelLiveResponse? + let latestAudioContent: CreatorChannelAudioContentResponse? + let channelDonations: [CreatorChannelDonationResponse] + let notices: [CreatorChannelCommunityPostResponse] + let schedules: [CreatorChannelScheduleResponse] + let audioContents: [CreatorChannelAudioContentResponse] + let series: [CreatorChannelSeriesResponse] + let communities: [CreatorChannelCommunityPostResponse] + let fanTalk: CreatorChannelFanTalkSummaryResponse + let introduce: String + let activity: CreatorChannelActivityResponse + let sns: CreatorChannelSnsResponse +} +``` + +- Kotlin `Long`으로 정의된 id/count 계열 값은 Swift에서 `Int`로 통일한다. +- `CreatorActivityType`은 기존 `SodaLive/Sources/V2/Main/Home/Following/Models/HomeFollowingTabResponse.swift`의 enum 재사용 또는 공용 이동 여부를 계획 단계에서 결정한다. +- `profileImageUrl`, `coverImageUrl`, `imageUrl`, `audioUrl` 등 URL 문자열은 서버 스펙의 nullable 여부를 따른다. 빈 문자열은 이미지 없음과 동일하게 처리할지 구현 전 확인한다. + +### 7.2 화면 상태 + +- 최초 진입 시 홈 API를 호출한다. +- 로딩 중에는 기존 V2 화면 패턴에 맞는 loading state를 표시한다. +- API 실패 시에는 전체 화면 error state로 전환하지 않고 placeholder 기반으로 일부 UI를 유지한다. +- API 실패 placeholder는 title bar, tab-bar, 홈 외 tab placeholder destination처럼 서버 데이터 없이 표시 가능한 기본 shell을 유지하는 방식으로 처리한다. +- 응답을 성공적으로 받은 뒤 각 섹션은 자신의 데이터가 비어 있으면 section title과 empty state를 표시하지 않고 섹션 전체를 숨긴다. +- 동일 화면 생명주기 안에서 불필요한 중복 호출은 피한다. +- pull-to-refresh가 기존 크리에이터 상세 화면에 있다면 같은 패턴을 검토한다. 기존 패턴이 없으면 이번 범위에서 새로 추가하지 않는다. + +### 7.3 Header / Title Bar / Status Bar + +Figma 참조: +- 전체 화면: `node-id=296:14890` +- Unfollow title bar: `node-id=296:14287` +- Follow + 알림 설정 title bar: `node-id=296:14288` +- Follow + 알림 해제 title bar: `node-id=296:14289` + +요구사항: +- 크리에이터 이미지 영역은 OS status bar 영역까지 확장한다. +- title bar는 화면 최상단에 overlay 형태로 배치한다. +- 초기 title bar 배경은 투명 또는 이미지 위 overlay 상태로 시작한다. +- tab-bar와 title bar가 가까워질수록 title bar 배경색은 점진적으로 black으로 변한다. +- tab-bar가 sticky 상태가 되면 OS status bar 영역도 title bar 배경색과 동일하게 보이도록 한다. +- title bar 좌측에는 back 버튼을 둔다. +- title bar 우측에는 팔로우/알림 상태 버튼과 더보기 버튼을 둔다. +- `isFollow == false`이면 `팔로우` capsule 버튼을 표시한다. +- `isFollow == true && isNotify == true`이면 팔로우 상태 아이콘 capsule과 알림 설정 아이콘을 표시한다. +- `isFollow == true && isNotify == false`이면 팔로우 상태 아이콘 capsule과 알림 해제 아이콘을 표시한다. +- 더보기 버튼은 항상 표시한다. +- 팔로우/언팔로우 mutation은 기존 follow, unfollow API를 사용한다. +- 알림 상태 버튼은 신규 API를 만들지 않고 기존 follow/unfollow 및 알림 상태 처리 흐름에서 제공되는 범위 안에서 연결한다. + +### 7.4 크리에이터 프로필 헤더 + +- `creator.profileImageUrl`은 프로필 이미지와 크리에이터 이미지 헤더의 큰 배경 이미지에 동일하게 사용한다. +- `creator.nickname`, `creator.followerCount`를 Figma 위치와 계층에 맞게 표시한다. +- `creator.characterId`가 있는 경우 캐릭터/AI 대화 연결 여부를 판단하는 데이터로 사용한다. +- 이미지 로드 실패 시 기존 프로젝트의 기본 프로필 이미지 처리 패턴을 따른다. +- 텍스트가 길어도 이미지/버튼/탭 영역과 겹치지 않도록 한 줄 말줄임 또는 기존 nickname 표시 관례를 따른다. + +### 7.5 대화하기 액션 영역 + +- `creator.isAiChatAvailable == true`이면 대화하기 버튼을 표시한다. +- 대화하기 아이콘은 `ic_new_talk` asset을 사용한다. +- 대화하기 버튼은 영역 중앙에 배치한다. +- `creator.isAiChatAvailable == false`여도 액션 영역의 높이와 vertical spacing은 보존한다. +- 대화하기 버튼 action은 기존 AI 채팅방 페이지로 이동한다. +- 기존 AI 채팅방 진입에 필요한 본인인증 guard를 동일하게 적용한다. +- 본인인증 guard에는 한국이 아닌 국가에서 다른 값으로 비교하는 기존 분기 조건도 포함한다. +- `creator.isDmAvailable`과 `ic_new_dm`은 response/asset 확인 대상이지만, DM 버튼 UI와 DM 진입은 추후 작업으로 미루고 이번 범위에서 구현하지 않는다. + +### 7.6 Tab Bar + +- tab-bar 항목은 `홈`, `라이브`, `오디오`, `시리즈`, `커뮤니티`, `팬Talk`, `후원` 순서로 표시한다. +- `화보` 탭은 표시하지 않는다. +- tab-bar는 horizontal scroll 가능 구조로 구현한다. +- 선택된 탭은 Figma처럼 하단 indicator와 white text로 표시한다. +- 선택되지 않은 탭은 gray text로 표시한다. +- tab-bar가 title bar와 맞닿으면 sticky 상태가 되고, 이후에는 tab-bar 아래 콘텐츠만 스크롤된다. +- 선택된 tab이 `홈`이 아닐 때는 실제 목록 화면 대신 선택된 탭 제목을 표시하는 placeholder 페이지를 표시한다. +- 홈 섹션의 `전체보기` chevron/button으로 이동해야 하는 destination도 해당 tab placeholder 또는 별도 placeholder 화면으로 연결한다. + +### 7.7 현재 라이브 섹션 + +- `currentLive != nil`이면 현재 라이브 섹션을 표시한다. +- `liveId`, `title`, `coverImageUrl`, `beginDateTimeUtc`, `price`, `isAdult`를 사용한다. +- `coverImageUrl`이 없으면 기존 라이브 카드의 기본 이미지 처리 패턴을 따른다. +- `isAdult == true`이면 기존 19금/성인 콘텐츠 표시 asset 또는 패턴을 따른다. +- card tap 시 기존 라이브 상세/라이브룸 진입 흐름이 있으면 연결하고, 없으면 placeholder action으로 둔다. + +### 7.8 최신 오디오 콘텐츠 섹션 + +- `latestAudioContent != nil`이면 최신 오디오 콘텐츠 섹션을 표시한다. +- `audioContentId`, `title`, `duration`, `imageUrl`, `price`, `isAdult`, `isPointAvailable`, `isFirstContent`, `seriesName`, `isOriginalSeries`를 사용한다. +- V2 공용 `AudioContentCard`가 요구사항과 맞으면 재사용한다. +- `isPointAvailable`, `isFirstContent`, `isOriginalSeries` 표시는 기존 오디오 카드/태그 패턴을 우선 따른다. +- card tap 시 기존 오디오 콘텐츠 상세 진입 흐름이 있으면 연결하고, 없으면 placeholder action으로 둔다. + +### 7.9 채널 후원 섹션 + +- `channelDonations`를 사용해 채널 후원 요약 목록을 표시한다. +- 각 item은 `nickname`, `profileImageUrl`, `can`, `message`, `createdAtUtc`를 표시한다. +- `channelDonations`가 비어 있으면 채널 후원 섹션을 숨긴다. +- 전체보기 action은 기존 `AppStep.channelDonationAll(creatorId:)` 또는 동일한 기존 흐름이 있으면 연결한다. +- 기존 흐름 연결이 어렵다면 후원 tab placeholder로 이동한다. + +### 7.10 공지 섹션 + +- `notices`를 사용해 공지 게시물을 표시한다. +- 응답 모델은 `CreatorChannelCommunityPostResponse`를 재사용한다. +- 공지 섹션은 일반 커뮤니티 섹션과 시각 구성이 다를 수 있으므로 별도 section component로 분리한다. +- `imageUrl`, `audioUrl`, `price`, `existOrdered`, `likeCount`, `commentCount` 처리는 기존 커뮤니티 카드 패턴을 따른다. +- 공지 전체보기 또는 item tap destination은 기존 커뮤니티 상세/목록 흐름이 있으면 연결하고, 없으면 placeholder로 둔다. + +### 7.11 스케줄 섹션 + +- `schedules`를 사용해 예정 항목을 표시한다. +- 각 item은 `scheduledAtUtc`, `title`, `type`, `targetId`를 사용한다. +- `type`은 `LIVE`, `AUDIO`, `COMMUNITY`, `LIVE_REPLAY` 등 기존 `CreatorActivityType` 처리 범위를 따른다. +- schedule tap 시 기존 `MainView.handleFollowingScheduleTap(type:targetId:)`와 같은 분기 패턴을 검토해 재사용한다. +- 지원하지 않는 type은 crash 없이 무시하거나 placeholder로 둔다. + +### 7.12 오디오 섹션 + +- `audioContents`를 사용해 오디오 콘텐츠 목록을 표시한다. +- V2 공용 `AudioContentCard` 재사용을 우선 검토한다. +- 홈 섹션에서는 Figma 기준 노출 개수만 표시하고, 전체보기는 오디오 tab placeholder 또는 기존 오디오 목록으로 이동한다. +- `audioContents`가 비어 있으면 오디오 섹션을 숨긴다. + +### 7.13 시리즈 섹션 + +- `series`를 사용해 시리즈 목록을 표시한다. +- 각 item은 `seriesId`, `title`, `coverImageUrl`, `numberOfContent`, `isNew`, `isOriginal`을 표시한다. +- `isNew`, `isOriginal` 표시는 Figma 태그와 기존 시리즈 카드 패턴을 따른다. +- 전체보기는 시리즈 tab placeholder 또는 기존 시리즈 목록으로 이동한다. +- item tap 시 기존 시리즈 상세 흐름이 있으면 연결하고, 없으면 placeholder action으로 둔다. + +### 7.14 커뮤니티 섹션 + +- `communities`를 사용해 커뮤니티 게시물 목록을 표시한다. +- V2 공용 `CommunityPostCard`가 요구사항과 맞으면 재사용한다. +- `imageUrl`, `audioUrl`, `price`, `existOrdered`, `likeCount`, `commentCount`를 표시 규칙에 반영한다. +- 유료/잠금 게시물은 기존 커뮤니티 잠금/구매 표시 패턴을 따른다. +- 홈에서는 Figma 기준 노출 개수만 표시하고 `전체보기` 버튼으로 커뮤니티 tab placeholder 또는 기존 커뮤니티 목록으로 이동한다. + +### 7.15 팬Talk 섹션 + +- `fanTalk.totalCount`와 `fanTalk.latestFanTalk`를 사용한다. +- `latestFanTalk == nil`이면 팬Talk 섹션을 숨긴다. +- 최신 팬Talk가 있으면 `nickname`, `profileImageUrl`, `content`, `languageCode`, `createdAtUtc`를 표시한다. +- 전체보기 action은 크리에이터 채널의 팬Talk 탭으로 이동한다. + +### 7.16 소개 섹션 + +- `introduce` 문자열을 표시한다. +- 빈 문자열이면 소개 섹션을 숨긴다. +- 긴 텍스트는 Figma 기준으로 전체 표시하되, 화면 폭을 넘지 않게 줄바꿈 처리한다. + +### 7.17 활동 섹션 + +- `activity`를 사용해 아래 항목을 표시한다. +- `debutDateUtc`와 `dDay`를 조합해 `2026.06.11(D+1)` 형식으로 표시한다. +- `liveCount`는 `라이브 총 진행 수`로 표시한다. +- `liveDurationHours`는 `라이브 누적 진행 시간`으로 표시한다. +- `liveContributorCount`는 `라이브 누적 참여자`로 표시한다. +- `audioContentCount`는 `오디오`로 표시한다. +- `seriesCount`는 `시리즈`로 표시한다. +- Figma에는 `화보` 활동 항목이 있지만 response 초안에 필드가 없고 이번 범위에서 화보 제외이므로 표시하지 않는다. +- 숫자 formatting은 기존 프로젝트의 count formatting 패턴을 따른다. + +### 7.18 SNS 섹션 + +- `sns.instagramUrl`, `sns.fancimmUrl`, `sns.xUrl`, `sns.youtubeUrl`, `sns.kakaoOpenChatUrl`을 사용한다. +- 빈 URL 또는 유효하지 않은 URL은 해당 SNS 아이콘을 숨긴다. +- 표시 순서는 Figma 기준 `Instagram`, `YouTube`, `X`, `KakaoTalk`, `Fancimm(팬심M)` 순서를 따른다. +- SNS 아이콘은 `ic_sns_*` asset을 사용한다. +- tap 시 외부 브라우저 또는 기존 in-app web 흐름을 따른다. + +## 8. UX / UI Expectations + +### 8.1 포함하는 UI +- OS status bar 영역까지 확장되는 크리에이터 이미지 헤더 +- scroll progress에 따라 black으로 변하는 overlay title bar +- 팔로우/알림 상태별 title bar +- 대화하기 액션 영역 +- sticky horizontal tab-bar +- 홈 탭의 섹션별 summary UI +- 홈 외 탭의 placeholder page +- 홈 섹션 전체보기 placeholder destination + +### 8.2 제외하는 UI +- 화보 탭 +- 화보 섹션 +- 활동 섹션의 화보 count row +- DM 버튼 및 DM 보내기 진입 +- 새 외부 라이브러리 추가 + +### 8.3 시각 규칙 +- 전체 화면은 Figma처럼 dark background를 기준으로 한다. +- Figma의 fixed width/height 값은 참고하되, SwiftUI 구현에서는 화면 width, safe area, dynamic type에 대응 가능한 layout을 우선한다. +- card/list root container는 필요한 경우를 제외하고 고정 숫자 width/height로 제한하지 않는다. +- section title은 V2 공용 `SectionTitle` 재사용을 우선 검토한다. +- chevron이 있는 section title 또는 전체보기 버튼은 tap 가능한 영역을 충분히 확보한다. +- profile image, cover image, content image는 기존 이미지 로딩/placeholder 패턴을 따른다. +- nickname/title/content가 길어도 인접 UI와 겹치지 않도록 말줄임 또는 줄바꿈을 적용한다. + +## 9. Technical Constraints +- 신규 View, ViewModel, Repository 및 연결 하위 코드는 `SodaLive/Sources/V2/**` 아래에 작성한다. +- 여러 페이지에서 재사용 가능한 공용 컴포넌트는 `SodaLive/Sources/V2/Component/**` 아래에 둔다. +- 특정 채널 홈 내부에서만 쓰는 컴포넌트는 채널 홈 폴더 하위 `Components`에 둔다. +- API/Repository/ViewModel 구성은 기존 `SodaLive/Sources/V2/Main/Home/Following/**`, `Ranking/**`, `Recommendation/**` 패턴을 따른다. +- `SodaLive/Sources/V2/Component/SectionTitle.swift`, `AudioContentCard.swift`, `CommunityPostCard.swift`, `CapsuleTabBar.swift` 등 기존 V2 컴포넌트 재사용 가능성을 먼저 검토한다. +- `CreatorActivityType`은 중복 선언하지 않고 기존 enum 재사용 또는 공용 위치 이동을 검토한다. +- 신규 asset은 프로젝트 asset catalog에 이미 존재하는 `ic_new_talk`을 사용한다. `ic_new_dm`은 추후 DM 버튼 작업에서 사용한다. +- 신규 문구가 다국어 대상이면 `SodaLive/Sources/I18n/I18n.swift`에 추가한다. +- Figma에서 제공되는 localhost asset URL은 앱 코드에 넣지 않는다. +- 프로젝트 설정 변경은 신규 파일이 Xcode project에 자동 포함되지 않는 구조일 때만 수행한다. +- Kotlin `Long`으로 정의된 id/count 계열 값은 Swift에서 `Int`로 통일한다. +- SNS 섹션 아이콘은 Figma localhost asset이 아니라 프로젝트 asset catalog의 `ic_sns_*` 아이콘을 사용한다. + +## 10. Success Criteria +- 크리에이터 채널 홈 진입 시 `GET /api/v2/creator-channels/{creatorId}/home` 호출이 발생한다. +- response model이 PRD의 API 초안 필드를 디코딩한다. +- API 실패 시 전체 화면 error state 대신 placeholder 기반으로 표시 가능한 기본 UI가 유지된다. +- `creator` 정보가 header/title/action/tab UI 상태에 반영된다. +- 크리에이터 이미지가 OS status bar 영역까지 표시된다. +- scroll 위치에 따라 title bar 배경이 점진적으로 black으로 변한다. +- tab-bar가 title bar와 맞닿으면 sticky 상태가 되고 아래 콘텐츠만 스크롤된다. +- sticky 상태에서 OS status bar 영역도 title bar와 동일한 black 배경으로 보인다. +- `isFollow`, `isNotify` 조합에 따라 title bar의 팔로우/알림 상태가 Figma 3개 상태와 일치한다. +- 팔로우/언팔로우 action은 기존 follow, unfollow API를 호출한다. +- `isAiChatAvailable`에 따라 대화하기 버튼이 조건부 표시된다. +- 대화하기 버튼은 기존 AI 채팅방 페이지로 이동하고, 기존 본인인증 guard와 비한국 국가 분기 guard를 적용한다. +- 대화하기 버튼이 없어도 액션 영역 높이는 유지된다. +- DM 버튼은 표시되지 않는다. +- 홈 tab에는 현재 라이브, 최신 오디오 콘텐츠, 채널 후원, 공지, 스케줄, 오디오, 시리즈, 커뮤니티, 팬Talk, 소개, 활동, SNS 섹션이 API 데이터 기반으로 표시된다. +- SNS 섹션은 `ic_sns_*` asset 아이콘과 유효한 SNS URL만 사용해 표시된다. +- 데이터가 비어 있는 섹션은 표시되지 않는다. +- `화보` 탭, 화보 섹션, 활동의 화보 row가 표시되지 않는다. +- 홈 외 tab-bar 항목 선택 시 선택한 탭 제목을 표시하는 placeholder page가 표시된다. +- 홈 섹션의 전체보기 action은 기존 화면 또는 placeholder destination으로 이동한다. +- 기존 V2 공용 컴포넌트를 재사용 가능한 곳에서 재사용하고, 채널 홈 전용 UI만 전용 `Components`로 분리한다. + +## 11. Open Questions +해당 없음. + +## 12. Verification Notes +- `docs/agent-guides/documentation-policy.md`를 확인해 신규 PRD 경로와 필수 섹션 기준을 검증했다. +- `docs/prd/sample-prd.md`, `docs/20260630_메인_홈_랭킹_탭/prd.md`, `docs/20260630_메인_홈_팔로잉_탭/prd.md`를 확인해 저장소의 PRD 작성 스타일을 맞췄다. +- Figma `node-id=296:14890`의 design context와 screenshot을 확인해 홈 화면 섹션, tab-bar 항목, sticky 대상 구조를 검토했다. +- Figma `node-id=296:14287`, `296:14288`, `296:14289`의 design context와 screenshot을 확인해 title bar의 Unfollow, Follow+알림 설정, Follow+알림 해제 상태를 검토했다. +- `SodaLive/Sources/V2/**` 구조를 확인해 기존 V2 홈 API/Repository/ViewModel/Components 배치 패턴을 검토했다. +- `SodaLive/Sources/V2/Component/SectionTitle.swift`, `AudioContentCard.swift`, `CommunityPostCard.swift`, `CapsuleTabBar.swift` 등 재사용 후보 파일 존재를 확인했다. +- 사용자 확인 사항을 반영해 `creator.profileImageUrl`을 헤더 큰 이미지로 사용, 빈 섹션 숨김, 기존 follow/unfollow API 사용, 대화하기의 기존 AI 채팅방 및 본인인증 guard 적용, DM 버튼 추후 작업, 홈 외 탭 제목 placeholder, 팬Talk 전체보기의 팬Talk 탭 이동, Swift `Int` 통일 정책을 확정 요구사항으로 이동했다. +- 사용자 확인 사항을 반영해 API 실패 시 placeholder 기반으로 일부 UI를 유지하고, SNS 섹션 아이콘은 `ic_sns_*` asset을 사용하도록 확정 요구사항으로 이동했다. +- 저장소의 `ic_sns_fancimm`, `fancimmUrl`, `I18n.ProfileUpdate.fancimm`을 확인해 Figma의 Fansim 표기를 `Fancimm(팬심M)` 기준으로 정리했다.