feat(creator): 라이브 탭 기반을 추가한다
This commit is contained in:
308
docs/20260703_크리에이터_채널_라이브_탭/plan-task.md
Normal file
308
docs/20260703_크리에이터_채널_라이브_탭/plan-task.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# 크리에이터 채널 라이브 탭 구현 계획
|
||||
|
||||
## 기준 문서
|
||||
|
||||
- PRD: `docs/20260703_크리에이터_채널_라이브_탭/prd.md`
|
||||
- 기존 크리에이터 채널 홈 PRD: `docs/20260701_크리에이터_채널_홈/prd.md`
|
||||
- 기존 크리에이터 채널 홈 구현 계획: `docs/20260701_크리에이터_채널_홈/plan-task.md`
|
||||
- 본인 채널 플로팅 버튼 PRD: `docs/20260703_크리에이터_채널_본인_진입_플로팅_버튼/prd.md`
|
||||
- 코드 스타일: `docs/agent-guides/code-style.md`
|
||||
- 빌드/검증: `docs/agent-guides/build-test-verification.md`
|
||||
- Figma 전체 화면: `290:8945`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-8945&m=dev`
|
||||
- Figma Sort-bar: `290:8949`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-8949&m=dev`
|
||||
- Figma 라이브 다시듣기 아이템: `290:8954`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-8954&m=dev`
|
||||
- Figma 라이브 다시듣기 아이템 대여/무료 상태: `290:8956`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-8956&m=dev`
|
||||
- Figma 본인 채널 하단 CTA: `665:19359`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=665-19359&m=dev`
|
||||
- Figma 정렬 컨텍스트 메뉴 예시: `290:9041`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9041&m=dev`
|
||||
|
||||
## 구현 원칙
|
||||
|
||||
- 기존 `CreatorChannelView`의 공통 shell, header, sticky tab-bar, title bar 동작은 유지한다.
|
||||
- `CreatorChannelTab.live` 콘텐츠만 신규 `CreatorChannelLiveTabView`로 교체한다.
|
||||
- 홈 탭의 `CreatorChannelCurrentLiveSection`과 기존 `showLiveDetail(_:)`, `showContentDetail(_:)`, `showCreateLive()` 흐름을 재사용한다.
|
||||
- `CreatorChannelLiveResponse`와 `CreatorChannelAudioContentResponse`는 기존 `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift` 타입을 우선 재사용한다. 타입 이동은 이번 구현에 필수일 때만 수행한다.
|
||||
- `ContentSort`는 라이브 탭 외 콘텐츠 탭에서 재사용 가능하도록 `SodaLive/Sources/V2/CreatorChannel/Models/**` 아래에 둔다.
|
||||
- sort-bar와 정렬 BottomSheet는 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 둔다.
|
||||
- 라이브 탭 전용 API, Repository, ViewModel, View, item UI는 `SodaLive/Sources/V2/CreatorChannel/Live/**` 아래에 둔다.
|
||||
- 최초 요청은 `page = 0`, `size = 20`, `sort = LATEST`로 고정한다.
|
||||
- `hasNext == true`일 때만 `page + 1` 추가 요청을 수행하고, 추가 요청 중 중복 호출을 막는다.
|
||||
- 본인 채널 라이브 탭의 하단 CTA는 `ic_new_create_live` asset과 `I18n.CreatorChannelLive.startLive` 문구를 사용한다.
|
||||
- Figma localhost asset URL은 앱 코드에 사용하지 않는다.
|
||||
|
||||
## 주요 대상 파일
|
||||
|
||||
### 생성
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Models/ContentSort.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Live/Models/CreatorChannelLiveTabResponse.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveApi.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveRepository.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveViewModel.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveReplayListItem.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveStartButton.swift`
|
||||
|
||||
### 수정
|
||||
- `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||
- `SodaLive/Sources/I18n/I18n.swift`
|
||||
- 필요 시 `SodaLive.xcodeproj/project.pbxproj`
|
||||
|
||||
### 확인
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift`
|
||||
- `SodaLive/Sources/App/AppStep.swift`
|
||||
- `SodaLive/Sources/ContentView.swift`
|
||||
- `SodaLive/Resources/Assets.xcassets/v2/ic_new_create_live.imageset`
|
||||
|
||||
## TASK 체크리스트
|
||||
|
||||
### Phase 1: PRD 결정 반영과 API 모델
|
||||
|
||||
- [x] **Task 1.1: 라이브 시작하기 CTA 아이콘 결정 반영 확인**
|
||||
- 대상 파일:
|
||||
- 확인: `docs/20260703_크리에이터_채널_라이브_탭/prd.md`
|
||||
- 확인: `SodaLive/Resources/Assets.xcassets/v2/ic_new_create_live.imageset`
|
||||
- 작업 내용:
|
||||
- PRD의 본인 채널 하단 CTA 요구사항, Success Criteria, Decisions에 `ic_new_create_live` 사용이 명시되어 있는지 확인한다.
|
||||
- asset 경로가 존재하는지 확인한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "ic_new_create_live|라이브 시작하기" docs/20260703_크리에이터_채널_라이브_탭/prd.md SodaLive/Resources/Assets.xcassets/v2`
|
||||
- 기대 결과: PRD와 asset 경로에서 `ic_new_create_live`가 확인된다.
|
||||
|
||||
- [x] **Task 1.2: ContentSort enum 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Models/ContentSort.swift`
|
||||
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
||||
- 작업 내용:
|
||||
- `enum ContentSort: String, Decodable, CaseIterable`을 생성한다.
|
||||
- case는 `latest = "LATEST"`, `popular = "POPULAR"`, `owned = "OWNED"`, `priceHigh = "PRICE_HIGH"`, `priceLow = "PRICE_LOW"`로 둔다.
|
||||
- `var title: String`에서 `I18n.CreatorChannelLive.Sort.latest`, `popular`, `owned`, `priceHigh`, `priceLow`를 반환한다.
|
||||
- `I18n.CreatorChannelLive.Sort`에 ko/en/ja 문구를 추가한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "enum ContentSort|case latest = \"LATEST\"|case popular = \"POPULAR\"|case owned = \"OWNED\"|case priceHigh = \"PRICE_HIGH\"|case priceLow = \"PRICE_LOW\"|var title" SodaLive/Sources/V2/CreatorChannel/Models/ContentSort.swift`
|
||||
- 기대 결과: API enum 값 5개와 title mapping이 존재한다.
|
||||
- 실행 명령: `rg "enum CreatorChannelLive|enum Sort|latest|popular|owned|priceHigh|priceLow" SodaLive/Sources/I18n/I18n.swift`
|
||||
- 기대 결과: 라이브 탭 정렬 다국어 문구가 존재한다.
|
||||
|
||||
- [x] **Task 1.3: 라이브 탭 Response model 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Live/Models/CreatorChannelLiveTabResponse.swift`
|
||||
- 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelLiveTabResponse`를 `Decodable`로 생성한다.
|
||||
- 필드는 `liveReplayContentCount`, `currentLive`, `liveReplayContents`, `sort`, `page`, `size`, `hasNext`로 둔다.
|
||||
- `currentLive`는 기존 `CreatorChannelLiveResponse?`, `liveReplayContents`는 기존 `[CreatorChannelAudioContentResponse]`를 사용한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "struct CreatorChannelLiveTabResponse|liveReplayContentCount|currentLive: CreatorChannelLiveResponse\\?|liveReplayContents: \\[CreatorChannelAudioContentResponse\\]|sort: ContentSort|page: Int|size: Int|hasNext: Bool" SodaLive/Sources/V2/CreatorChannel/Live/Models/CreatorChannelLiveTabResponse.swift`
|
||||
- 기대 결과: PRD 기준 응답 필드가 모두 존재한다.
|
||||
|
||||
### Phase 2: API, Repository, ViewModel
|
||||
|
||||
- [x] **Task 2.1: 라이브 탭 API endpoint 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveApi.swift`
|
||||
- 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Repository/CreatorChannelHomeApi.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelLiveApi`를 `TargetType`으로 생성한다.
|
||||
- case는 `getLive(creatorId: Int, page: Int, size: Int, sort: ContentSort)`로 둔다.
|
||||
- path는 `/api/v2/creator-channels/{creatorId}/live`로 구성한다.
|
||||
- method는 `.get`으로 둔다.
|
||||
- task는 `.requestParameters(parameters: ["page": page, "size": size, "sort": sort.rawValue], encoding: URLEncoding.queryString)`로 둔다.
|
||||
- headers는 기존 V2 API와 동일하게 `Authorization: Bearer {token}` 패턴을 따른다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "enum CreatorChannelLiveApi|getLive\\(creatorId: Int, page: Int, size: Int, sort: ContentSort\\)|/api/v2/creator-channels/|/live|requestParameters|URLEncoding.queryString|sort.rawValue|Authorization" SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveApi.swift`
|
||||
- 기대 결과: live endpoint, query parameters, 인증 헤더가 확인된다.
|
||||
|
||||
- [x] **Task 2.2: 라이브 탭 Repository 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveRepository.swift`
|
||||
- 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Repository/CreatorChannelHomeRepository.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelLiveRepository`를 만들고 `MoyaProvider<CreatorChannelLiveApi>`를 사용한다.
|
||||
- `getLive(creatorId:page:size:sort:) -> AnyPublisher<Response, MoyaError>` 메서드를 제공한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "final class CreatorChannelLiveRepository|MoyaProvider<CreatorChannelLiveApi>|getLive\\(creatorId: Int, page: Int, size: Int, sort: ContentSort\\)|AnyPublisher<Response, MoyaError>" SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveRepository.swift`
|
||||
- 기대 결과: 기존 V2 Repository와 같은 형태가 확인된다.
|
||||
|
||||
- [x] **Task 2.3: 라이브 탭 ViewModel 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveViewModel.swift`
|
||||
- 확인: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelViewModel.swift`
|
||||
- 확인: `SodaLive/Sources/Content/All/ContentAllViewModel.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelLiveViewModel`을 `ObservableObject`로 생성한다.
|
||||
- 상태는 `isLoading`, `isLoadingNextPage`, `response`, `liveReplayContents`, `selectedSort`, `page`, `size`, `hasNext`, `hasLoaded`, `errorMessage`, `isShowPopup`를 둔다.
|
||||
- `size` 기본값은 `20`, `selectedSort` 기본값은 `.latest`, 최초 `page`는 `0`으로 둔다.
|
||||
- `fetchFirstPage(creatorId:)`는 `page = 0` 첫 페이지를 조회하되 요청 전 기존 목록과 pagination 상태를 비우지 않는다.
|
||||
- `fetchNextPageIfNeeded(creatorId:currentItem:)`는 마지막 item 노출, `hasNext == true`, 추가 로딩 아님 조건에서 `page + 1`을 조회한다.
|
||||
- `selectSort(_:creatorId:)`는 sort가 바뀔 때만 `selectedSort`를 갱신하고 첫 페이지를 다시 조회한다.
|
||||
- 첫 페이지 성공 시 `response`, `page`, `size`, `hasNext`를 갱신하고 `liveReplayContents`를 조회 결과로 교체한다. 다음 페이지 성공 시 같은 상태를 갱신하고 `liveReplayContents` 뒤에 append한다.
|
||||
- 첫 페이지 실패와 다음 페이지 실패 모두 기존 목록과 pagination 상태를 유지하고 `errorMessage`, `isShowPopup`를 갱신한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "final class CreatorChannelLiveViewModel|isLoadingNextPage|liveReplayContents|selectedSort|private let pageSize|fetchFirstPage\\(creatorId: Int\\)|fetchNextPageIfNeeded|selectSort|ApiResponse<CreatorChannelLiveTabResponse>|page \\+ 1|hasNext" SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveViewModel.swift`
|
||||
- 기대 결과: 첫 페이지 로딩, 정렬 변경, 다음 페이지 로딩 상태와 성공 시에만 목록 교체/append하는 동작이 확인된다.
|
||||
|
||||
### Phase 3: sort-bar와 정렬 BottomSheet
|
||||
|
||||
- [x] **Task 3.1: sort-bar 공통 컴포넌트 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift`
|
||||
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelSortBar`를 `View`로 생성한다.
|
||||
- 입력값은 `totalCount: Int`, `selectedSort: ContentSort`, `onTapSort: () -> Void`로 둔다.
|
||||
- 좌측에는 `I18n.CreatorChannelLive.totalLabel`과 `totalCount`를 표시한다.
|
||||
- 우측에는 `selectedSort.title`과 정렬 아이콘을 표시한다.
|
||||
- 높이는 Figma 기준 `52pt`로 둔다.
|
||||
- 배경은 black, 텍스트는 기존 `Color.gray400`, `Color.gray500`, white 토큰을 따른다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "struct CreatorChannelSortBar|totalCount: Int|selectedSort: ContentSort|onTapSort|I18n.CreatorChannelLive.totalLabel|selectedSort.title|frame\\(height: 52" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift`
|
||||
- 기대 결과: 전체 개수, 현재 정렬, 터치 action이 있는 sort-bar가 확인된다.
|
||||
|
||||
- [x] **Task 3.2: 정렬 BottomSheet 컴포넌트 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelSortBottomSheet`를 `View`로 생성한다.
|
||||
- 입력값은 `selectedSort: ContentSort`, `sorts: [ContentSort] = ContentSort.allCases`, `onSelect: (ContentSort) -> Void`로 둔다.
|
||||
- 항목은 `LATEST`, `POPULAR`, `OWNED`, `PRICE_HIGH`, `PRICE_LOW` 순서로 표시한다.
|
||||
- Figma context menu의 `추천순`은 표시하지 않는다.
|
||||
- 선택된 항목은 배경색 또는 체크 아이콘 없이도 충분히 구분되는 foreground/background 상태를 적용한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "struct CreatorChannelSortBottomSheet|selectedSort: ContentSort|ContentSort.allCases|onSelect|ForEach|sort.title" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
||||
- 기대 결과: ContentSort 5개 항목 기반 BottomSheet가 확인된다.
|
||||
- 실행 명령: `rg "추천순|recommend" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
||||
- 기대 결과: 검색 결과가 없다.
|
||||
|
||||
### Phase 4: 라이브 탭 UI
|
||||
|
||||
- [ ] **Task 4.1: 라이브 다시듣기 item 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveReplayListItem.swift`
|
||||
- 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift`
|
||||
- 확인: `SodaLive/Sources/I18n/I18n.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelLiveReplayListItem`을 `View`로 생성한다.
|
||||
- 입력값은 `audioContent: CreatorChannelAudioContentResponse`, `action: () -> Void`로 둔다.
|
||||
- 88pt 썸네일, 제목, duration, 우측 가격/상태 영역을 구성한다.
|
||||
- `price == 0`이면 무료 태그를 표시한다.
|
||||
- `isPointAvailable == true`이면 point 태그 또는 point 가격 표시를 적용한다.
|
||||
- `isAdult == true`이면 19금 shield 태그를 표시한다.
|
||||
- `isOwned == true`이면 우측 상태 영역에 play button과 `I18n.Content.Status.owned` 또는 신규 `I18n.CreatorChannelLive.owned` 문구를 표시한다.
|
||||
- `isRented == true`이고 `isOwned == false`이면 play button과 `I18n.Content.Status.rented` 또는 신규 `I18n.CreatorChannelLive.rented` 문구를 표시한다.
|
||||
- `isOwned == true && isRented == true`이면 `소장중`을 우선 표시한다.
|
||||
- tap 시 `action()`을 호출한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "struct CreatorChannelLiveReplayListItem|CreatorChannelAudioContentResponse|audioContent.isOwned|audioContent.isRented|audioContent.isAdult|audioContent.isPointAvailable|audioContent.price == 0|소장중|대여중|action\\(\\)" SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveReplayListItem.swift`
|
||||
- 기대 결과: 다시듣기 item의 상태 표시 조건과 tap action이 확인된다.
|
||||
|
||||
- [ ] **Task 4.2: 라이브 시작하기 하단 CTA 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveStartButton.swift`
|
||||
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
||||
- 확인: `SodaLive/Resources/Assets.xcassets/v2/ic_new_create_live.imageset`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelLiveStartButton`을 `View`로 생성한다.
|
||||
- 입력값은 `action: () -> Void`로 둔다.
|
||||
- full-width capsule button으로 표시한다.
|
||||
- 아이콘은 `Image("ic_new_create_live")`를 사용한다.
|
||||
- 문구는 `I18n.CreatorChannelLive.startLive`를 사용한다.
|
||||
- safe area bottom과 겹치지 않도록 부모 view에서 bottom padding을 줄 수 있게 고정 높이를 안정적으로 유지한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "struct CreatorChannelLiveStartButton|ic_new_create_live|I18n.CreatorChannelLive.startLive|Button\\(action: action\\)|Capsule|frame\\(maxWidth: \\.infinity" SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveStartButton.swift`
|
||||
- 기대 결과: 지정 아이콘과 문구를 사용하는 CTA 컴포넌트가 확인된다.
|
||||
|
||||
- [ ] **Task 4.3: 라이브 탭 화면 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift`
|
||||
- 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelLiveTabView`를 `View`로 생성한다.
|
||||
- 입력값은 `creatorId: Int`, `isOwnCreatorChannel: Bool`, `onTapLive: (Int) -> Void`, `onTapContent: (Int) -> Void`, `onTapCreateLive: () -> Void`로 둔다.
|
||||
- 내부에서 `@StateObject private var viewModel = CreatorChannelLiveViewModel()`을 사용한다.
|
||||
- `onAppear`에서 `hasLoaded == false`이면 `fetchFirstPage(creatorId:)`를 호출한다.
|
||||
- sort-bar 터치 시 `.sheet` 또는 기존 앱 sheet 패턴으로 `CreatorChannelSortBottomSheet`를 표시한다.
|
||||
- `currentLive != nil`이면 `CreatorChannelCurrentLiveSection(currentLive:onTapLive:)`를 표시한다.
|
||||
- `liveReplayContents`는 `LazyVStack`으로 표시하고 각 item의 `onAppear`에서 `fetchNextPageIfNeeded`를 호출한다.
|
||||
- item tap은 `onTapContent(audioContent.audioContentId)`로 연결한다.
|
||||
- `isOwnCreatorChannel == true`이면 하단 overlay 또는 safe area inset에 `CreatorChannelLiveStartButton(action: onTapCreateLive)`를 표시한다.
|
||||
- CTA가 목록을 가리지 않도록 scroll content bottom padding을 추가한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "struct CreatorChannelLiveTabView|CreatorChannelLiveViewModel|fetchFirstPage\\(creatorId: creatorId\\)|CreatorChannelSortBar|CreatorChannelSortBottomSheet|CreatorChannelCurrentLiveSection|CreatorChannelLiveReplayListItem|fetchNextPageIfNeeded|onTapContent\\(audioContent.audioContentId\\)|CreatorChannelLiveStartButton|isOwnCreatorChannel" SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift`
|
||||
- 기대 결과: 라이브 탭 화면, 정렬, 현재 라이브, 다시듣기 목록, pagination, 본인 CTA가 확인된다.
|
||||
|
||||
### Phase 5: CreatorChannelView 연결
|
||||
|
||||
- [ ] **Task 5.1: 라이브 탭 콘텐츠 연결**
|
||||
- 대상 파일:
|
||||
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||
- 작업 내용:
|
||||
- `selectedTabContent`에서 `viewModel.selectedTab == .live`일 때 `CreatorChannelLiveTabView`를 표시한다.
|
||||
- `creatorId`, `isOwnCreatorChannel`, `showLiveDetail`, `showContentDetail`, `showCreateLive`를 전달한다.
|
||||
- 기존 홈 탭 분기는 유지한다.
|
||||
- 홈 외 준비 중 화면 분기는 `live`를 제외한 탭에만 적용한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelLiveTabView|viewModel\\.selectedTab == \\.live|isOwnCreatorChannel|onTapLive: showLiveDetail|onTapContent: showContentDetail|onTapCreateLive: showCreateLive" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||
- 기대 결과: 라이브 탭이 준비 중 화면이 아니라 신규 view로 연결된다.
|
||||
|
||||
- [ ] **Task 5.2: 본인 채널 홈 플로팅 메뉴와 라이브 CTA 조건 분리 확인**
|
||||
- 대상 파일:
|
||||
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||
- 확인: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift`
|
||||
- 작업 내용:
|
||||
- 기존 `CreatorChannelFloatingActionMenu`는 본인 채널 `home` 탭에서만 유지한다.
|
||||
- 라이브 탭에서는 `CreatorChannelLiveTabView` 내부 하단 CTA만 표시되도록 한다.
|
||||
- tab 변경 시 기존 `isCreatorActionMenuPresented = false` 동작은 유지한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "isOwnCreatorChannel && viewModel\\.selectedTab == \\.home|isCreatorActionMenuPresented = false|CreatorChannelLiveTabView" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||
- 기대 결과: 홈 floating menu와 라이브 하단 CTA 표시 조건이 분리되어 있다.
|
||||
|
||||
### Phase 6: 검증
|
||||
|
||||
- [ ] **Task 6.1: 정적 검색 검증**
|
||||
- 대상 파일:
|
||||
- 확인: `SodaLive/Sources/V2/CreatorChannel`
|
||||
- 확인: `SodaLive/Sources/I18n/I18n.swift`
|
||||
- 작업 내용:
|
||||
- API endpoint, query parameter, 정렬 enum, `page=0`, `size=20`, sort-bar, BottomSheet, current live 재사용, 다시듣기 item, `ic_new_create_live`, 오디오 상세 이동 연결을 검색한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelLiveApi|/api/v2/creator-channels/|/live|URLEncoding.queryString|ContentSort|pageSize|20|page = 0|CreatorChannelSortBar|CreatorChannelSortBottomSheet|CreatorChannelCurrentLiveSection|CreatorChannelLiveReplayListItem|ic_new_create_live|contentDetail\\(contentId:" SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift`
|
||||
- 기대 결과: PRD 핵심 구현 지점이 모두 검색된다.
|
||||
|
||||
- [ ] **Task 6.2: 빌드 검증**
|
||||
- 대상 파일:
|
||||
- 확인: `SodaLive.xcworkspace`
|
||||
- 작업 내용:
|
||||
- Debug 빌드를 실행한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||
- 기대 결과: `** BUILD SUCCEEDED **`
|
||||
|
||||
- [ ] **Task 6.3: 수동 동작 검증**
|
||||
- 대상 화면:
|
||||
- 타인 크리에이터 채널 라이브 탭
|
||||
- 본인 크리에이터 채널 라이브 탭
|
||||
- 작업 내용:
|
||||
- 라이브 탭 진입 시 첫 요청이 `page=0`, `size=20`, `sort=LATEST`로 호출되는지 확인한다.
|
||||
- sort-bar에 전체 개수와 `최신순`이 표시되는지 확인한다.
|
||||
- sort-bar 터치 시 BottomSheet가 표시되고 5개 정렬만 보이는지 확인한다.
|
||||
- 정렬 변경 시 첫 페이지부터 다시 조회되는지 확인한다.
|
||||
- `currentLive != nil`인 경우 홈 탭 현재 라이브 UI와 동일한 카드가 표시되는지 확인한다.
|
||||
- 마지막 item 노출 시 `hasNext == true`이면 `page + 1`이 호출되고 목록이 append되는지 확인한다.
|
||||
- `hasNext == false`이면 추가 호출이 없는지 확인한다.
|
||||
- 다시듣기 item의 무료, 가격, 포인트, 19금, 소장중, 대여중 상태가 조건에 맞게 표시되는지 확인한다.
|
||||
- 다시듣기 item tap 시 오디오 상세 화면으로 이동하는지 확인한다.
|
||||
- 본인 채널 라이브 탭에서 `ic_new_create_live` 아이콘이 포함된 `라이브 시작하기` 버튼이 하단에 표시되는지 확인한다.
|
||||
- 타인 채널 또는 라이브 외 탭에서는 `라이브 시작하기` 버튼이 표시되지 않는지 확인한다.
|
||||
- 검증 기준:
|
||||
- 기대 결과: PRD Success Criteria를 모두 만족한다.
|
||||
|
||||
## 검증 기록
|
||||
|
||||
- 2026-07-03: 문서 작성 단계. 구현/빌드 검증은 아직 수행하지 않음.
|
||||
- 2026-07-03: Phase 1~3 구현 완료. `rg` 기반 정적 검증, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -list`를 확인함.
|
||||
- 2026-07-03: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`는 실행했으나, DerivedData `manifest.json` 작성 중 디스크 용량 부족(`No space left on device`, 여유 598Mi)으로 Swift 컴파일 전 실패함.
|
||||
- 2026-07-03: Phase 2.3 보완. 첫 페이지/정렬 변경/다음 페이지 조회 모두 성공 시에만 목록 교체 또는 append를 수행하고, 실패 시 기존 목록과 pagination 상태를 유지하도록 PRD/계획/구현을 정렬함.
|
||||
Reference in New Issue
Block a user