diff --git a/docs/20260617_크리에이터_채널_라이브_탭/plan-task.md b/docs/20260617_크리에이터_채널_라이브_탭/plan-task.md index 3ef0537c..c8c7dad0 100644 --- a/docs/20260617_크리에이터_채널_라이브_탭/plan-task.md +++ b/docs/20260617_크리에이터_채널_라이브_탭/plan-task.md @@ -21,6 +21,7 @@ - 별도 `CreatorChannelLiveApi`, `CreatorChannelLiveRepository`는 생성하지 않는다. - Figma: - 전체: `290:8945` + - 전체 empty: `290:8959` - Sort-bar: `290:8949` - 현재 진행 중인 라이브: `290:8950` - 라이브 다시듣기 item: `290:8954`, `290:8956` @@ -32,6 +33,11 @@ - `isOwned == true`와 `isRented == true`가 동시에 내려오면 `소장중`을 우선 표시한다. - `seriesName`은 라이브 다시듣기 item에 표시하지 않는다. - `isFirstContent`, `isOriginalSeries`는 기존 오디오 item 정책과 동일하게 매핑한다. +- `currentLive == null`이고 `liveReplayContents.isEmpty()`인 전체 empty 상태에서는 Sort-bar, 현재 라이브 카드, 라이브 다시듣기 리스트를 제거하고 중앙에 `크리에이터가 라이브를 준비 중입니다.\n기대해 주세요!` 문구를 표시한다. +- 전체 empty 상태가 본인 채널에 표시되는 경우에도 empty 문구는 동일하게 표시하고, 하단 `라이브 시작하기` CTA는 본인 채널 정책에 따라 계속 표시한다. +- 같은 `creatorId`로 `loadLive()`가 재호출되면 Fragment/View 재생성 또는 탭 재바인딩으로 보고 기존 ViewModel 상태를 유지한다. 명시적 새로고침은 후속 필요 시 별도 refresh API로 분리한다. +- 최초 조회 실패로 `Error` 상태인 경우 `retryLive()`를 통해 현재 `creatorId`의 첫 페이지를 다시 요청한다. +- 전체 error 상태에서는 안내 문구 아래 retry 버튼을 표시하고, retry 버튼은 `CreatorChannelLiveViewModel.retryLive()`를 호출한다. - drawable 리소스는 기존 파일을 재사용한다. - `ic_new_sort` - `ic_new_shield_small` @@ -55,7 +61,7 @@ - Phase 3: 부분 참조 - mapper와 presenter 정책은 PRD와 Figma item variant의 상태 표시를 함께 확인한다. - Phase 4: 필수 참조 - - `fragment_creator_channel_live.xml`, sort-bar, current live card, replay item layout은 Figma `290:8949`, `290:8950`, `290:8954`, `290:8956`을 기준으로 구현한다. + - `fragment_creator_channel_live.xml`, sort-bar, current live card, replay item layout, 전체 empty 상태는 Figma `290:8949`, `290:8950`, `290:8954`, `290:8956`, `290:8959`를 기준으로 구현한다. - Phase 5: 필수 참조 - 정렬 컨텍스트 메뉴는 Figma `290:9041` 기준으로 구현한다. - Phase 6: 필수 참조 @@ -79,7 +85,11 @@ - rename: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeRepository.kt` -> `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt` - 홈 API, 라이브 API, 팔로우, 대화, 후원, 신고 등 채널 공통 동작을 제공한다. - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt` - - `CreatorChannelLiveTabResponse`, `ContentSort`를 추가하고 `CreatorChannelAudioContentResponse`에 라이브 탭 전용 필드 `isAdult`, `isOwned`, `isRented`를 서버 계약에 맞춰 추가한다. + - `CreatorChannelAudioContentResponse`에 라이브 탭 전용 필드 `isAdult`, `isOwned`, `isRented`를 서버 계약에 맞춰 추가한다. +- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/common/data/ContentSort.kt` + - v2 API 공용 정렬 enum으로 `ContentSort`를 둔다. +- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/data/CreatorChannelLiveTabResponse.kt` + - `CreatorChannelLiveTabResponse`를 크리에이터 채널 라이브 탭 응답 전용 모델로 둔다. - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/model/CreatorChannelLiveUiModels.kt` - sort option, replay item status, pagination 상태, CTA 노출 여부를 순수 UI model로 정의한다. - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/model/CreatorChannelLiveMappers.kt` @@ -177,20 +187,54 @@ ### Phase 2: API/DTO/Repository/ViewModel 계약 추가 -- [ ] **Task 2.1: 라이브 탭 DTO와 `ContentSort` 추가** +- [x] **Task 2.1: 라이브 탭 DTO와 `ContentSort` 추가** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt` + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/common/data/ContentSort.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/data/CreatorChannelLiveTabResponse.kt` - 작업: - - `CreatorChannelLiveTabResponse`를 `@Keep`, `@SerializedName` 기반 data class로 추가한다. - - `ContentSort` enum에 `LATEST`, `POPULAR`, `OWNED`, `PRICE_HIGH`, `PRICE_LOW`를 추가한다. + - `CreatorChannelLiveTabResponse`를 라이브 탭 `data/CreatorChannelLiveTabResponse.kt`에 `@Keep`, `@SerializedName` 기반 data class로 추가한다. + - `ContentSort` enum에 `LATEST`, `POPULAR`, `OWNED`, `PRICE_HIGH`, `PRICE_LOW`를 추가하고 `v2.common.data` 패키지에 둔다. - 기존 `CreatorChannelAudioContentResponse`에 `isAdult`, `isOwned`, `isRented` 필드를 추가할 때 홈 탭 API 하위 호환성이 깨지지 않는지 확인한다. - 하위 호환이 필요하면 라이브 탭 전용 DTO 분리 또는 nullable/default 정책을 테스트로 고정한다. - 검증 명령: - `./gradlew :app:compileDebugKotlin` - 기대 결과: - DTO 추가 후 컴파일된다. + - 검증 기록: + - 2026-06-17: `CreatorChannelHomeModels.kt`에 `CreatorChannelLiveTabResponse`와 `ContentSort(LATEST, POPULAR, OWNED, PRICE_HIGH, PRICE_LOW)`를 추가했다. 기존 홈 탭 테스트 fixture의 positional constructor 호환을 유지하기 위해 `CreatorChannelAudioContentResponse`의 라이브 탭 전용 `isAdult`, `isOwned`, `isRented` 필드는 기본값이 있는 trailing field로 추가했다. + - 2026-06-17: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. -- [ ] **Task 2.2: 라이브 탭 endpoint와 Repository method 추가** +- [x] **Task 2.1.1: 라이브 탭 응답 모델 파일 경계 분리** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/common/data/ContentSort.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/data/CreatorChannelLiveTabResponse.kt` + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt` + - `docs/20260617_크리에이터_채널_라이브_탭/prd.md` + - 작업: + - `ContentSort`는 v2 API 공용 정렬 enum이므로 `v2.common.data` 패키지로 분리한다. + - `CreatorChannelLiveTabResponse`는 크리에이터 채널 라이브 탭 응답 계약이므로 `v2.creator.channel.live.data` 패키지로 분리한다. + - 홈 탭과 라이브 탭이 함께 쓰는 `CreatorChannelLiveResponse`, `CreatorChannelAudioContentResponse`는 기존 공용 DTO로 유지한다. + - 별도 `CreatorChannelLiveApi`/`CreatorChannelLiveRepository`는 만들지 않고 기존 공통 `CreatorChannelApi`/`CreatorChannelRepository`를 유지한다. + - 검증 명령: + - `rg -n "CreatorChannelLiveTabResponse|enum class ContentSort" app/src/main/java/kr/co/vividnext/sodalive/v2` + - `./gradlew :app:compileDebugKotlin` + - 기대 결과: + - `ContentSort`는 `v2/common/data/ContentSort.kt`에 정의된다. + - `CreatorChannelLiveTabResponse`는 `v2/creator/channel/live/data/CreatorChannelLiveTabResponse.kt`에 정의된다. + - 모델 파일 분리 후 Kotlin 컴파일이 PASS한다. + - 검증 기록: + - 2026-06-17: 사용자 리뷰에 따라 `CreatorChannelLiveTabResponse`와 `ContentSort`를 `CreatorChannelHomeModels.kt`에서 라이브 탭 응답 전용 파일로 이동했다. 같은 `data` 패키지의 타입 이동이라 API/Repository/ViewModel의 패키지 import 계약은 유지된다. + - 2026-06-17: `rg -n "CreatorChannelLiveTabResponse|enum class ContentSort" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data` 실행 결과, 타입 정의가 라이브 탭 응답 전용 파일에만 있고 `CreatorChannelApi.kt`의 응답 타입 참조만 남아 있음을 확인했다. + - 2026-06-17: `./gradlew :app:compileDebugKotlin` 최초 실행은 sandbox가 `~/.gradle` lock 파일 생성을 막아 실패했다. 동일 명령을 권한 승인 후 재실행해 `BUILD SUCCESSFUL in 24s`로 통과했다. + - 2026-06-17: 사용자 추가 지시에 따라 `ContentSort`는 `v2.common.data.ContentSort`, `CreatorChannelLiveTabResponse`는 `v2.creator.channel.live.data.CreatorChannelLiveTabResponse`로 이동하도록 계획과 PRD의 파일 경계를 갱신했다. + - 2026-06-17: `rg -n "creator\\.channel\\.data\\.ContentSort|creator\\.channel\\.data\\.CreatorChannelLiveTabResponse|ContentSort|CreatorChannelLiveTabResponse" app/src/main/java app/src/test/java`로 이전 패키지 import가 남지 않았고, `ContentSort`는 `v2.common.data`, `CreatorChannelLiveTabResponse`는 `v2.creator.channel.live.data` 참조로 갱신됐음을 확인했다. + - 2026-06-17: `./gradlew :app:compileDebugKotlin` 최초 실행은 sandbox가 `~/.gradle` lock 파일 생성을 막아 실패했다. 동일 명령을 권한 승인 후 재실행해 `BUILD SUCCESSFUL in 12s`로 통과했다. + - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL in 32s`로 통과했다. + +- [x] **Task 2.2: 라이브 탭 endpoint와 Repository method 추가** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt` @@ -202,8 +246,12 @@ - `./gradlew :app:compileDebugKotlin` - 기대 결과: - API/Repository가 기존 Koin graph와 충돌 없이 컴파일된다. + - 검증 기록: + - 2026-06-17: `CreatorChannelApi`에 `GET /api/v2/creator-channels/{creatorId}/live` endpoint를 추가하고 `page`, `size`, `sort` query와 `Authorization` header를 전달하도록 했다. `CreatorChannelRepository.getLive()`는 별도 live repository를 만들지 않고 기존 공통 repository에서 API를 얇게 위임한다. + - 2026-06-17: `sort`는 `ContentSort` enum query로 전달해 `LATEST` 등 enum name이 Retrofit query 값으로 사용되도록 했다. + - 2026-06-17: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. -- [ ] **Task 2.3: ViewModel RED 테스트 작성** +- [x] **Task 2.3: ViewModel RED 테스트 작성** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveViewModelTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLivePaginationTest.kt` @@ -218,8 +266,13 @@ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLivePaginationTest"` - 기대 결과: - ViewModel 미구현 상태에서 RED 실패한다. + - 검증 기록: + - 2026-06-17: `CreatorChannelLiveViewModelTest`와 `CreatorChannelLivePaginationTest`를 추가했다. 최초 로딩 `page=0`/`sort=LATEST`, `hasNext` 기반 `page + 1` pagination, loading 중 중복 load-more 차단, 정렬 변경 시 page/list 초기화, 같은 정렬 재선택 no-op, 실패/empty 상태를 검증한다. + - 2026-06-17: production 구현 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModelTest"` 실행 결과 `ContentSort`, `CreatorChannelLiveTabResponse`, `CreatorChannelLiveViewModel`, `CreatorChannelRepository.getLive` 미구현으로 `:app:compileDebugUnitTestKotlin FAILED`가 발생해 RED를 확인했다. + - 2026-06-17: `최초 로드 전 정렬 변경은 기본 최신순 최초 로드 계약을 바꾸지 않는다` 테스트를 추가하고 production 보정 전 실행한 결과, `changeSort(POPULAR)`가 최초 로드 기본값을 오염시켜 테스트가 실패함을 확인한 뒤 guard를 구현했다. + - 2026-06-17: 리뷰 코멘트에 따라 같은 `creatorId`로 `loadLive()`가 재호출되면 기존 목록/page/load-more 결과를 유지해야 한다는 테스트를 추가했다. production 보정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModelTest"` 실행 결과 해당 테스트가 실패함을 확인했다. -- [ ] **Task 2.4: `CreatorChannelLiveViewModel` 구현** +- [x] **Task 2.4: `CreatorChannelLiveViewModel` 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveViewModel.kt` - 수정: @@ -233,6 +286,17 @@ - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLivePaginationTest"` - 기대 결과: - ViewModel/pagination 테스트가 PASS한다. + - 검증 기록: + - 2026-06-17: `CreatorChannelLiveViewModel`을 추가하고 `Loading`, `Empty`, `Error`, `Content` 상태를 정의했다. 초기 로드는 `page=0`, `size=10`, `sort=LATEST`로 요청하고, pagination은 마지막 성공 응답의 `page + 1`로 append하며 `isLoadingMore`로 중복 요청을 차단한다. + - 2026-06-17: 정렬 변경은 기존 목록/page를 초기화해 첫 페이지를 다시 요청하고, 같은 정렬 재선택은 API를 재호출하지 않는다. 최초 로드 전 정렬 변경은 기본 `LATEST` 최초 로드 계약을 바꾸지 않도록 no-op 처리했다. + - 2026-06-17: 리뷰 게이트에서 정렬 변경 중 이전 첫 페이지/load-more 응답이 최신 상태를 덮어쓸 수 있다는 지적이 있어 RED 테스트를 추가했다. production 보정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `이전 첫 페이지 응답은 이후 정렬 변경 결과를 덮어쓰지 않는다`, `이전 load-more 응답은 이후 정렬 변경 목록에 append되지 않는다` 2개 테스트가 실패함을 확인했다. + - 2026-06-17: `requestGeneration` guard를 추가해 오래된 첫 페이지/load-more 성공 또는 실패 응답을 무시하도록 보정했다. 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. + - 2026-06-17: `AppDI`에 `CreatorChannelLiveViewModel(get())` binding을 추가했다. 기존 `CreatorChannelApi`/`CreatorChannelRepository` binding은 재사용했다. + - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. + - 2026-06-17: 같은 `creatorId`와 기존 상태가 있는 `loadLive()` 재호출은 no-op 하도록 보정해 Fragment/View 재생성 또는 탭 재바인딩 시 기존 정렬/page/list 상태를 유지한다. 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModelTest"` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. + - 2026-06-17: 리뷰 코멘트의 empty 정책을 반영해 PRD/계획 문서에 Figma `290:8959` 기준 전체 empty 상태를 기록했다. `CreatorChannelLiveUiState.Empty`는 Sort-bar/list를 제거하고 중앙 문구를 표시하기 위한 상태로 유지하며, 본인 채널 하단 CTA는 Phase 6의 본인 채널 정책대로 유지한다. + - 2026-06-17: 리뷰 코멘트에 따라 `Error` 상태에서 retry 버튼이 현재 `creatorId` 첫 페이지를 재요청해야 한다는 테스트를 추가했다. production 보정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModelTest"` 실행 결과 `retryLive` 미구현으로 `:app:compileDebugUnitTestKotlin FAILED`가 발생해 RED를 확인했다. + - 2026-06-17: Android 샘플과 기존 retry 의미를 따라 `loadLive()`의 같은 `creatorId` 상태 보존 guard는 유지하고, 명시적 `retryLive()` API를 추가했다. Phase 4 retry 버튼은 `retryLive()`를 호출하면 된다. 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModelTest"` 및 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. --- @@ -293,6 +357,8 @@ - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt` - 작업: - ViewModel 상태를 관찰해 sort-bar, current live card, replay list, loading/empty/error를 갱신한다. + - error 상태에서는 Sort-bar, current live card, replay list를 숨기고 중앙 안내 문구와 그 아래 retry 버튼을 표시한다. + - retry 버튼 클릭은 `CreatorChannelLiveViewModel.retryLive()`를 호출해 현재 `creatorId` 첫 페이지를 재요청한다. - replay item click은 기존 오디오 컨텐츠 상세/재생 진입 정책을 확인해 연결한다. - current live card click은 기존 홈 탭 라이브 진입 정책을 재사용한다. - RecyclerView scroll listener로 하단 접근 시 ViewModel load-more를 호출한다. @@ -448,3 +514,31 @@ - 2026-06-17: Phase 1 진행. `CreatorChannelHomeApi`/`CreatorChannelHomeRepository`를 `CreatorChannelApi`/`CreatorChannelRepository`로 rename하고 기존 홈 endpoint/repository method 동작은 유지했다. - 2026-06-17: `./gradlew :app:compileDebugKotlin` PASS. 최초 병렬 실행은 KSP incremental cache 손상으로 실패했으나 `app/build/kspCaches/debug` 생성물 캐시 삭제 후 순차 재실행에서 통과했다. - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"` PASS. +- 2026-06-17: Phase 2 진행. 라이브 탭 DTO/API/Repository/ViewModel 계약과 RED/GREEN 테스트를 추가했다. UI/layout/mapper/Fragment/tab 연결은 Phase 3 이후 범위라 변경하지 않았다. +- 2026-06-17: RED 확인. production 구현 전 live ViewModel 테스트 실행 시 `ContentSort`, `CreatorChannelLiveTabResponse`, `CreatorChannelLiveViewModel`, `CreatorChannelRepository.getLive` 미구현으로 `:app:compileDebugUnitTestKotlin FAILED`가 발생했다. +- 2026-06-17: 추가 RED 확인. 최초 로드 전 `changeSort(POPULAR)` 호출이 기본 `LATEST` 최초 로드 계약을 오염시키는 실패를 확인한 뒤 guard를 적용했다. +- 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS. +- 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"` PASS. +- 2026-06-17: `./gradlew :app:compileDebugKotlin` PASS. +- 2026-06-17: 리뷰 게이트 지적으로 stale async response 보강. production 보정 전 stale first-page/load-more 테스트 2개가 실패했고, `requestGeneration` guard 적용 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS. +- 2026-06-17: stale async response 보강 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"` PASS. +- 2026-06-17: stale async response 보강 후 `./gradlew :app:compileDebugKotlin` PASS. +- 2026-06-17: 리뷰 코멘트 반영. Figma `290:8959` 기준 전체 empty 상태는 Sort-bar/list 없이 중앙 문구를 표시하고, 본인 채널 하단 CTA는 유지하는 것으로 PRD/계획 문서에 기록했다. +- 2026-06-17: 같은 `creatorId`의 `loadLive()` 재호출 상태 보존 RED 테스트를 추가했다. 보정 전 테스트 실패 확인 후 기존 상태가 있으면 no-op 하도록 구현했고, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModelTest"` PASS. +- 2026-06-17: 리뷰 코멘트 반영 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS. +- 2026-06-17: 리뷰 코멘트 반영 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"` PASS. 병렬 실행 중 Robolectric 임시 DataStore 정리 경고가 한 차례 출력되었으나, 동일 명령 순차 재실행 결과 `BUILD SUCCESSFUL`로 통과했다. +- 2026-06-17: 리뷰 코멘트 반영 후 `./gradlew :app:compileDebugKotlin` PASS. +- 2026-06-17: 리뷰 코멘트 반영. `loadLive()`의 같은 `creatorId` 상태 보존 guard는 유지하고, Error 상태 재시도는 명시적 `retryLive()`로 분리했다. production 보정 전 `retryLive` 미구현 컴파일 실패를 확인했고, 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModelTest"` PASS. +- 2026-06-17: Phase 4 UI 작업에 error 안내 문구 아래 retry 버튼을 표시하고, retry 버튼이 `CreatorChannelLiveViewModel.retryLive()`를 호출한다는 요구사항을 추가했다. +- 2026-06-17: `retryLive()` 추가 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS. +- 2026-06-17: `retryLive()` 추가 후 `./gradlew :app:compileDebugKotlin` PASS. +- 2026-06-17: `retryLive()` 추가 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"` 최초 병렬 실행에서 `채널 후원 성공은 기존 후원 API를 호출하고 홈을 다시 로드한다`가 실패했으나, 변경 범위와 무관한 SharedPreferenceManager 상태 간섭 가능성을 분리하기 위해 동일 명령을 순차 재실행했고 `BUILD SUCCESSFUL`로 통과했다. +- 2026-06-17: 전체 회귀 명령 실행. 최초 sandbox 실행은 `~/.gradle` wrapper lock 접근 제한으로 실패해 권한 승인 후 Gradle 명령을 재실행했다. +- 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` PASS. +- 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS. +- 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` FAIL. `CreatorChannelActivitySourceTest`의 기존 XML/source 문자열 검증 3개가 실패했다: `activity_creator_channel.xml`의 `ic_new_talk`/`ic_new_dm` 기대, `item_creator_channel_home_donation.xml`의 `196dp` 기대, `item_creator_channel_home_series_content.xml`의 `android:text="Only"` 기대. +- 2026-06-17: `./gradlew :app:mergeDebugResources` PASS. +- 2026-06-17: `./gradlew :app:compileDebugKotlin` PASS. +- 2026-06-17: `./gradlew :app:ktlintCheck` 최초 실행에서 신규 live test 줄바꿈과 `CreatorChannelLiveModels.kt` 파일명 규칙 위반으로 FAIL. 테스트 helper 줄바꿈을 정리하고 응답 모델 파일명을 `CreatorChannelLiveTabResponse.kt`로 변경한 뒤 재실행해 PASS. +- 2026-06-17: ktlint 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` PASS. +- 2026-06-17: ktlint 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` 재실행 결과 동일한 `CreatorChannelActivitySourceTest` 3개 실패가 재현됐다. diff --git a/docs/20260617_크리에이터_채널_라이브_탭/prd.md b/docs/20260617_크리에이터_채널_라이브_탭/prd.md index 7b9b19b7..9361aac3 100644 --- a/docs/20260617_크리에이터_채널_라이브_탭/prd.md +++ b/docs/20260617_크리에이터_채널_라이브_탭/prd.md @@ -69,6 +69,7 @@ - 최초 조회의 정렬 기본값은 `ContentSort.LATEST`이다. - 최초 조회의 `page` 시작 값은 `0`이다. - 최초 조회 실패, 정렬 변경 실패, 다음 페이지 로딩 실패는 기존 프로젝트의 에러 표시/재시도 패턴을 구현 계획 단계에서 확인해 따른다. +- 최초 조회 실패 후 사용자가 retry 버튼을 누르면 `retryLive()`로 현재 `creatorId`의 첫 페이지를 다시 요청할 수 있어야 한다. - `hasNext == true`일 때 다음 페이지 요청은 현재 응답의 `page + 1` 값을 사용한다. - 중복 pagination 요청이 발생하지 않도록 loading 중 추가 요청을 막아야 한다. - 정렬 변경 시 기존 목록과 page 상태를 초기화하고 첫 페이지부터 다시 조회한다. @@ -111,7 +112,10 @@ enum class ContentSort { #### Edge Cases - `currentLive == null`이면 현재 라이브 카드 영역을 표시하지 않고 라이브 다시듣기 목록이 Sort-bar 아래에 이어진다. -- `liveReplayContents`가 비어 있고 `currentLive == null`이면 기존 앱 패턴에 맞는 empty 상태를 표시한다. +- `liveReplayContents`가 비어 있고 `currentLive == null`이면 Figma `290:8959` 기준의 전체 empty 상태를 표시한다. +- 전체 empty 상태에서는 Sort-bar, 현재 라이브 카드, 라이브 다시듣기 리스트를 제거하고 화면 중앙에 `크리에이터가 라이브를 준비 중입니다.\n기대해 주세요!` 문구를 표시한다. +- 전체 empty 상태가 본인 채널에 표시되는 경우에도 empty 문구는 동일하게 표시하고, 하단 `라이브 시작하기` CTA는 본인 채널 정책에 따라 계속 표시한다. +- 최초 조회 실패로 전체 error 상태가 표시되는 경우 안내 문구 아래에 retry 버튼을 표시한다. retry 버튼은 `retryLive()`를 호출해 현재 `creatorId`의 첫 페이지를 다시 요청한다. - `duration == null`이면 duration 영역은 빈 문자열 노출 대신 숨김 또는 기존 duration placeholder 정책을 따른다. - `imageUrl == null` 또는 이미지 로딩 실패 시 기존 이미지 placeholder 정책을 따른다. - 다음 페이지 응답의 `liveReplayContents`가 비어 있어도 `hasNext` 값 기준으로 이후 로딩 가능 여부를 갱신한다. @@ -238,6 +242,7 @@ Sort-bar는 라이브 다시듣기 총 개수와 현재 정렬 상태를 표시 - 컨텍스트 메뉴는 dark surface, 14dp radius, border, focused item 배경 차이를 유지한다. - 정렬 메뉴는 BottomSheet처럼 화면 하단에서 올라오지 않고 Sort-bar 정렬 영역 아래에 떠야 한다. - 본인 채널의 `라이브 시작하기` 버튼은 Figma `665:19359`처럼 하단 고정 CTA로 표시하고, 목록 컨텐츠 위를 덮지 않도록 스크롤 하단 여백을 확보한다. +- 전체 empty 상태는 Figma `290:8959`처럼 Sort-bar와 리스트 없이 중앙 안내 문구를 표시한다. - 다국어 문자열 길이 증가 시 title, 정렬 label, 상태 문구가 겹치지 않도록 말줄임 또는 최소 폭 정책을 적용한다. --- @@ -248,6 +253,8 @@ Sort-bar는 라이브 다시듣기 총 개수와 현재 정렬 상태를 표시 - 라이브 탭 전용 `Fragment`, `ViewModel`, UI model, mapper, adapter, popup/helper는 `kr.co.vividnext.sodalive.v2.creator.channel.live` 하위에 작성한다. - 크리에이터 채널 API와 Repository는 홈 탭과 라이브 탭이 함께 사용하는 공통 계층으로 보고, 기존 `CreatorChannelHomeApi`/`CreatorChannelHomeRepository`는 `CreatorChannelApi`/`CreatorChannelRepository`로 rename한다. - 라이브 탭용 별도 `CreatorChannelLiveApi`/`CreatorChannelLiveRepository`는 만들지 않는다. 기존 Repository가 홈 API 외 팔로우, 대화, 후원, 신고 등 채널 공통 액션을 이미 담당하므로 공통 채널 Repository로 명명하는 편이 역할에 맞다. +- `ContentSort`는 v2 API에서 공통적으로 사용하는 정렬 enum이므로 `kr.co.vividnext.sodalive.v2.common.data` 패키지에 둔다. +- `CreatorChannelLiveTabResponse`는 크리에이터 채널 라이브 탭 응답 전용 모델이므로 `kr.co.vividnext.sodalive.v2.creator.channel.live.data` 패키지에 둔다. - 크리에이터 채널 홈 탭에서 이미 정의된 공통 모델/컴포넌트가 있으면 우선 재사용한다. - API DTO는 서버 계약과 동일한 필드명을 사용한다. - `ContentSort`는 API 전송 값과 UI label을 분리하고, UI label은 문자열 리소스로 관리한다. @@ -255,6 +262,8 @@ Sort-bar는 라이브 다시듣기 총 개수와 현재 정렬 상태를 표시 - 본인 채널 판정은 크리에이터 채널 홈 탭 또는 공통 채널 컨테이너에서 사용하는 기존 본인 페이지 판정 값을 우선 재사용한다. - `BuildConfig` 값이나 토큰/URL 같은 민감값은 로그, Toast, crash message에 노출하지 않는다. - 구현 전 `docs/20260617_크리에이터_채널_라이브_탭/plan-task.md`를 작성한 뒤 해당 계획에 따라 최소 구현한다. +- Fragment/View 재생성 또는 탭 재바인딩으로 같은 `creatorId`의 `loadLive()`가 다시 호출되면 기존 ViewModel 상태를 유지하고 첫 페이지를 재호출하지 않는다. 명시적 새로고침이 필요하면 별도 refresh API로 분리한다. +- 최초 조회 실패로 인한 error 상태의 재시도는 `retryLive()`로 분리한다. `loadLive()`는 같은 `creatorId`와 기존 상태가 있으면 상태 보존을 위해 no-op으로 유지한다. --- @@ -284,6 +293,7 @@ Sort-bar는 라이브 다시듣기 총 개수와 현재 정렬 상태를 표시 - `라이브 시작하기` 버튼 icon drawable 리소스명은 `ic_new_create_live`이다. - 라이브 탭 전용 코드는 `kr.co.vividnext.sodalive.v2.creator.channel.live` 하위에 둔다. - 채널 API/Repository는 `CreatorChannelApi`/`CreatorChannelRepository`로 rename해 홈 탭과 라이브 탭이 함께 사용한다. +- `currentLive == null`이고 `liveReplayContents.isEmpty()`인 전체 empty 상태에서는 Sort-bar/list를 숨기고 중앙 empty 문구를 표시한다. 본인 채널의 하단 `라이브 시작하기` CTA는 empty 상태와 무관하게 본인 채널 정책대로 유지한다. --- @@ -294,6 +304,7 @@ Sort-bar는 라이브 다시듣기 총 개수와 현재 정렬 상태를 표시 - 라이브 다시듣기 item Figma: `290:8954`, `290:8956` - 정렬 컨텍스트 메뉴 Figma: `290:9041` - 본인 채널 라이브 탭 및 하단 CTA Figma: `665:19359`, `665:19371` +- 전체 empty 상태 Figma: `290:8959` - 기존 크리에이터 채널 홈 탭 PRD: `docs/20260611_크리에이터_채널_홈_탭/prd.md` - PRD template: `docs/prd/sample-prd.md` - 문서 규칙: `docs/agent-guides/work-plan-docs.md`