# 크리에이터 채널 라이브 탭 구현 계획/TASK > **For agentic workers:** 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다. **Goal:** `GET /api/v2/creator-channels/{creatorId}/live` 응답을 기반으로 크리에이터 채널의 `라이브` 탭에 현재 라이브, 라이브 다시듣기 목록, 정렬, pagination, 본인 채널 전용 하단 `라이브 시작하기` CTA를 표시한다. **Architecture:** 기존 `CreatorChannelActivity`의 `ViewPager2`/`CreatorChannelPagerAdapter` 구조를 유지하고, `CreatorChannelTab.Live`의 placeholder를 신규 `CreatorChannelLiveFragment`로 교체한다. 라이브 탭 전용 Fragment/ViewModel/mapper/UI model/adapter/popup은 `kr.co.vividnext.sodalive.v2.creator.channel.live` 하위에 둔다. API/Repository는 홈 탭과 라이브 탭이 함께 쓰는 채널 공통 계층으로 보고 기존 `CreatorChannelHomeApi`/`CreatorChannelHomeRepository`를 `CreatorChannelApi`/`CreatorChannelRepository`로 rename한다. 홈 탭에서 이미 쓰는 `CreatorChannelLiveResponse`, `CreatorChannelAudioContentResponse`, 본인 판정, 라이브 시작 진입 흐름은 가능한 한 재사용한다. **Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test. --- ## 전제와 성공 기준 - PRD: `docs/20260617_크리에이터_채널_라이브_탭/prd.md` - 기존 채널 컨테이너: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 기존 탭 adapter: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt` - 기존 채널 API/DTO/Repository: - rename 대상: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeApi.kt` -> `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt` - 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` - 유지/확장: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt` - 별도 `CreatorChannelLiveApi`, `CreatorChannelLiveRepository`는 생성하지 않는다. - Figma: - 전체: `290:8945` - 전체 empty: `290:8959` - Sort-bar: `290:8949` - 현재 진행 중인 라이브: `290:8950` - 라이브 다시듣기 item: `290:8954`, `290:8956` - 정렬 컨텍스트 메뉴: `290:9041` - 본인 채널 하단 CTA: `665:19359`, `665:19371` - 첫 페이지 `page`는 `0`이다. - 다음 페이지는 마지막 성공 응답의 `page + 1`로 요청한다. - `ContentSort` 기본값은 `LATEST`이다. - `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` - `ic_new_player_play` - `ic_new_create_live` - 구현 완료 후 최소 다음 명령을 실행한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` --- ## Figma 참조 필요 Phase - Phase 1: 부분 참조 - 기존 코드 경계와 리소스 존재 여부 확인이 중심이며, Figma는 PRD 기준만 확인한다. - Phase 2: Figma 참조 불필요 - API/DTO/Repository/ViewModel 상태 모델은 서버 계약과 기존 코드 패턴을 따른다. - Phase 3: 부분 참조 - mapper와 presenter 정책은 PRD와 Figma item variant의 상태 표시를 함께 확인한다. - Phase 4: 필수 참조 - `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: 필수 참조 - 본인 채널 하단 CTA는 Figma `665:19359`, `665:19371` 기준으로 구현한다. - Phase 7: 부분 참조 - 탭 연결, 화면 이동, pagination 동작은 기존 코드 패턴 중심으로 검증한다. - Phase 8: 필수 참조 - 최종 수동 화면 검증은 PRD의 모든 Figma 노드와 실제 화면을 대조한다. --- ## 파일 구조 - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt` - `CreatorChannelTab.Live`를 신규 `CreatorChannelLiveFragment`로 연결한다. - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - 라이브 탭 UI, adapter, sort menu, pagination, CTA click 연결을 담당한다. - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveViewModel.kt` - 라이브 탭 API 호출, 정렬 변경, pagination 상태, loading/error/content 상태를 관리한다. - rename: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeApi.kt` -> `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt` - 홈 API와 라이브 API endpoint를 함께 가진 채널 공통 API로 변경한다. - 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` - `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` - DTO를 UI model로 변환하고 소장/대여/무료/가격/포인트/19금 상태를 결정한다. - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt` - 라이브 다시듣기 목록 RecyclerView adapter를 담당한다. - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveSortPopup.kt` - Sort-bar anchor 기준 컨텍스트 메뉴 표시를 담당한다. - 생성: `app/src/main/res/layout/fragment_creator_channel_live.xml` - sort-bar, current live card container, replay RecyclerView, owner CTA를 포함한다. - 생성: `app/src/main/res/layout/item_creator_channel_live_replay.xml` - 라이브 다시듣기 item layout이다. - 생성 후보: `app/src/main/res/layout/view_creator_channel_live_sort_menu.xml` - 정렬 컨텍스트 메뉴를 XML로 분리할 때만 추가한다. - 수정: `app/src/main/res/values/strings.xml` - 정렬 label, `소장중`, `대여중`, `라이브 시작하기`, empty/error 문구를 추가한다. - 수정: `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml` - 신규 문자열의 다국어 값을 추가한다. - 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 신규 ViewModel/API/Repository binding이 필요하면 추가한다. - 테스트 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveMapperTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLivePaginationTest.kt` - `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/CreatorChannelLiveFragmentLayoutTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt` --- ### Phase 9: 라이브/오디오 탭 마지막 아이템 잘림 수정 - [x] **Task 9.1: ViewPager 높이 계산 회귀 테스트 추가** - 수정: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - 작업: - `ViewPager2` 자체 bottom padding이 있는 상태에서 현재 page 측정 높이만 layout height로 적용하면 하단 컨텐츠가 padding 영역만큼 잘릴 수 있음을 테스트로 고정한다. - `updateViewPagerHeight()`가 측정된 page 높이에 `ViewPager2` vertical padding을 포함해 layout height를 계산하는지 검증한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - 기대 결과: - 구현 전에는 padding 포함 계산이 없어 RED 실패한다. - 검증 기록: - 2026-06-19: `CreatorChannelActivitySourceTest`에 `ViewPager2` 자체 vertical padding을 height 계산에 포함해야 한다는 회귀 테스트를 추가했다. 구현 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행은 sandbox의 `~/.gradle` lock 접근 제한으로 실패했고, 권한 승인 후 재실행 결과 신규 테스트가 `CreatorChannelActivitySourceTest.kt:391`에서 실패해 RED를 확인했다. - [x] **Task 9.2: ViewPager 높이 계산 보정** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 작업: - `currentPage.measuredHeight`에 `binding.viewPager.paddingTop + binding.viewPager.paddingBottom`을 더한 값을 `ViewPager2` layout height로 적용한다. - 기존 `NestedScrollView` 소유 스크롤, sticky tabbar, 라이브/오디오 pagination 구조는 변경하지 않는다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - 기대 결과: - 라이브/오디오 탭의 마지막 아이템이 `ViewPager2` bottom padding만큼 잘리지 않도록 height 계산이 보정되고 관련 테스트가 PASS한다. - 검증 기록: - 2026-06-19: 원인 확인 결과, `setupOwnerFabInsets()`가 `ViewPager2` 자체에 96dp bottom padding을 적용하지만 `updateViewPagerHeight()`는 현재 page의 `measuredHeight`만 layout height로 반영하고 있었다. 이 때문에 `ViewPager2` 내부 표시 영역이 padding만큼 줄어 마지막 item이 1개뿐이어도 하단이 잘릴 수 있었다. - 2026-06-19: `updateViewPagerHeight()`에서 `currentPage.measuredHeight + binding.viewPager.paddingTop + binding.viewPager.paddingBottom`을 target height로 적용하도록 보정했다. 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 결과 `BUILD SUCCESSFUL in 22s`로 통과했다. - 2026-06-19: 추가 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"`, `./gradlew :app:compileDebugKotlin`이 모두 `BUILD SUCCESSFUL`로 통과했다. 최초 sandbox 실행은 `~/.gradle` wrapper lock 접근 제한으로 실패해 권한 승인 후 재실행했다. - [x] **Task 9.3: 라이브/오디오 empty 문구 위치 회귀 테스트 추가** - 수정: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragmentLayoutTest.kt` - 작업: - 라이브/오디오 empty container가 스크롤 전 화면에서 보이도록 상단 기준 여백 배치를 사용하는지 테스트로 고정한다. - empty container가 과도하게 낮은 중앙 배치가 되지 않도록 `FrameLayout` center gravity 의존을 제거하는지 검증한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - 기대 결과: - 구현 전에는 empty 문구 상단 배치 정책이 없어 RED 실패한다. - 검증 기록: - 2026-06-19: 라이브/오디오 empty 문구가 상단 가시 영역에 배치되어야 한다는 레이아웃 테스트를 추가했다. 구현 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`는 `CreatorChannelLiveFragmentLayoutTest.kt:85`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"`는 `CreatorChannelAudioFragmentLayoutTest.kt:76`에서 실패해 RED를 확인했다. - [x] **Task 9.4: 라이브/오디오 empty 문구 상단 가시 영역 배치** - 수정: - `app/src/main/res/layout/fragment_creator_channel_live.xml` - `app/src/main/res/layout/fragment_creator_channel_audio.xml` - 작업: - empty container를 상단 constraint 기준으로 두고, 안내 문구는 컨테이너 상단에서 적절한 margin을 둔 위치에 배치한다. - 기존 empty minHeight 전달 구조는 유지해 짧은 탭에서도 sticky scroll range가 부족하지 않게 한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - 기대 결과: - 라이브/오디오 empty 문구가 스크롤 없이 보이는 상단 가시 영역에 배치되고 레이아웃/컴파일/린트 검증이 PASS한다. - 검증 기록: - 2026-06-19: 원인 확인 결과, empty 상태는 컨텐츠가 없어도 Activity가 전달한 viewport `minimumHeight`를 가진 컨테이너 중앙에 문구를 배치하고 있었다. 탭 컨텐츠가 화면 아래에서 시작하거나 sticky 보정 전후 타이밍에서는 이 중앙 위치가 화면 하단 밖으로 밀려 스크롤 전 문구가 보이지 않을 수 있었다. - 2026-06-19: `fragment_creator_channel_live.xml`, `fragment_creator_channel_audio.xml`의 empty container 중앙 gravity 의존을 제거하고, container 상단 기준 `spacing_48` top padding과 문구 `top|center_horizontal` 배치로 보정했다. 오디오 empty container의 parent bottom constraint도 제거해 낮은 중앙 배치를 만들지 않도록 했다. - 2026-06-19: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`와 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"`가 모두 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-19: 후속 수정 전체 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`가 모두 `BUILD SUCCESSFUL`로 통과했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 계속 출력됐다. - [x] **Task 9.5: empty 상태 스크롤 이동 원인 회귀 테스트 추가** - 수정: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragmentLayoutTest.kt` - 작업: - 라이브/오디오 empty 상태가 Activity viewport `minimumHeight` 보정에 의존하지 않는지 테스트로 고정한다. - empty 상태에서 문구 표시만을 위해 추가 스크롤 영역을 만들지 않도록 `layoutCreatorChannel*Empty.minimumHeight` 적용이 제거됐는지 검증한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - 기대 결과: - 구현 전에는 empty minHeight 적용 코드가 남아 RED 실패한다. - 검증 기록: - 2026-06-19: empty 상태가 `minimumHeight` 보정에 의존하지 않아야 한다는 테스트로 라이브/오디오 Fragment layout test를 갱신했다. production 보정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`는 `CreatorChannelLiveFragmentLayoutTest.kt:74`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"`는 `CreatorChannelAudioFragmentLayoutTest.kt:145`에서 실패해 RED를 확인했다. 병렬 실행 중 Kotlin daemon incremental cache 경합 로그가 출력됐으나 실패 원인은 신규 테스트 assertion이었다. - [x] **Task 9.6: empty 상태 minHeight 보정 제거** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt` - 작업: - empty container에 Activity viewport height를 minimumHeight로 적용하지 않는다. - `onCreatorChannel*ViewportHeightChanged()` API는 Activity 호출 계약 유지를 위해 남기되 empty layout 높이를 변경하지 않도록 한다. - 기존 content/list height 보정과 마지막 item 잘림 보정은 유지한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - 기대 결과: - empty 상태에서 문구 자체를 위한 추가 scroll range가 없어지고, 기존 라이브/오디오 탭 회귀 테스트가 PASS한다. - 검증 기록: - 2026-06-19: 원인 확인 결과, 라이브/오디오 empty 문구가 Fragment 컨텐츠 내부에 있고 `layoutCreatorChannel*Empty.minimumHeight`가 Activity viewport 높이로 설정되어 empty 상태에서도 추가 scroll range가 만들어졌다. 따라서 스크롤 시 문구가 컨텐츠와 함께 움직였다. - 2026-06-19: `CreatorChannelLiveFragment`, `CreatorChannelAudioFragment`에서 `emptyMinHeight` 상태, `applyEmptyMinHeight()`, empty bind 시 minimumHeight 적용을 제거했다. Activity가 호출하는 `onCreatorChannel*ViewportHeightChanged(minHeight)`는 기존 호출 계약 유지를 위해 no-op으로 남겼다. - 2026-06-19: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`는 `BUILD SUCCESSFUL in 47s`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"`는 `BUILD SUCCESSFUL in 9s`로 통과했다. - 2026-06-19: 후속 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`가 모두 `BUILD SUCCESSFUL`로 통과했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 계속 출력됐다. - [x] **Task 9.7: 오디오 소장률 카운트 색상 회귀 테스트 추가** - Figma: - `290:9029` - 수정: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragmentLayoutTest.kt` - 작업: - 소장률 카드 오른쪽 `구매한 콘텐츠 수/전체 유료 콘텐츠 수` 중 구매한 수는 기본 white, `/전체 유료 콘텐츠 수`는 gray 계열 span으로 표시하는 계약을 테스트로 고정한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - 기대 결과: - 구현 전에는 오른쪽 count 부분 색상 span이 없어 RED 실패한다. - 검증 기록: - 2026-06-19: Figma `290:9029`를 확인해 오른쪽 count에서 구매한 콘텐츠 수는 white, `/전체 유료 콘텐츠 수`는 gray로 분리되어야 함을 확인했다. `CreatorChannelAudioFragmentLayoutTest`에 count span 계약을 추가했고, production 보정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` 실행 결과 `CreatorChannelAudioFragmentLayoutTest.kt:172`에서 실패해 RED를 확인했다. - [x] **Task 9.8: 오디오 소장률 카운트 부분 색상 적용** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt` - 작업: - `tvCreatorChannelAudioRateCount`에 표시하는 문자열을 `SpannableString`으로 변환한다. - 구매한 콘텐츠 수는 TextView 기본 white를 유지하고, `/전체 유료 콘텐츠 수` 범위에 `R.color.gray_500` `ForegroundColorSpan`을 적용한다. - 기존 소장률 퍼센트 `soda_400` highlight는 유지한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - 기대 결과: - Figma `290:9029`와 같이 소장률 오른쪽 카운트의 구매 수와 전체 유료 수 색상이 분리되고 관련 검증이 PASS한다. - 검증 기록: - 2026-06-19: `CreatorChannelAudioFragment.bindRate()`에서 `tvCreatorChannelAudioRateCount` 표시 문자열을 `SpannableString`으로 변환하고, 구매한 콘텐츠 수 이후 범위(`/전체 유료 콘텐츠 수`)에 `R.color.gray_500` `ForegroundColorSpan`을 적용했다. 구매한 콘텐츠 수는 TextView 기본 white를 유지한다. - 2026-06-19: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL in 25s`로 통과했다. - 2026-06-19: 후속 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`가 모두 `BUILD SUCCESSFUL`로 통과했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 계속 출력됐다. --- ### Phase 1: 기존 구조 확인과 작업 경계 고정 - [x] **Task 1.1: 크리에이터 채널 탭 구조와 본인 판정 경로 확인** - 확인: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeMappers.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt` - 작업: - `CreatorChannelTab.Live`가 현재 placeholder로 연결되는 지점을 확인한다. - 본인 여부(`isOwner`)가 Activity와 탭 Fragment로 전달되는 기존 경로를 확인한다. - 라이브 시작 진입이 기존 `CreatorChannelActivity`의 `onOwnerFabLiveClicked()` 또는 `LiveRoomCreateActivity` 경로를 재사용할 수 있는지 확인한다. - 검증: - 라이브 탭 구현이 홈 탭 section adapter를 변경하지 않고 독립 Fragment로 추가 가능한지 기록한다. - 검증 기록: - 2026-06-17: `CreatorChannelPagerAdapter`는 현재 `CreatorChannelTab.Home`만 `CreatorChannelHomeFragment.newInstance(creatorId)`로 연결하고, `CreatorChannelTab.Live`를 포함한 나머지 탭은 `CreatorChannelPlaceholderFragment.newInstance(tab)`로 처리함을 확인했다. 따라서 Phase 7에서 Live 탭만 신규 Fragment로 교체해도 홈 탭 section adapter 변경 없이 독립 추가 가능하다. - 2026-06-17: 본인 여부는 `CreatorChannelHomeMappers.toUiContent()`에서 `creator.creatorId == currentMemberId`로 계산되어 `CreatorChannelHeaderUiModel.isOwner`와 donation section에 전달되고, `CreatorChannelActivity`는 이 값을 owner FAB/상단 액션/DM/후원 분기 조건으로 사용함을 확인했다. - 2026-06-17: 라이브 시작 진입은 `CreatorChannelActivity.onOwnerFabLiveClicked()`가 `liveRoomCreateLauncher.launch(Intent(this, LiveRoomCreateActivity::class.java))`를 호출하는 기존 경로를 사용하며, 생성 결과는 `homeActionDelegate?.refreshHome()` 후 `Constants.EXTRA_ROOM_ID`, `Constants.EXTRA_ROOM_CHANNEL_NAME`에 따라 `CreatorChannelLiveCoordinator.enterLiveRoom()` 또는 생성 완료 toast로 처리됨을 확인했다. - [x] **Task 1.2: 기존 오디오 item 정책과 drawable/string 리소스 확인** - 확인: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeAudioContentCardView.kt` - `app/src/main/res/layout/item_creator_channel_home_audio_content.xml` - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTag.kt` - `app/src/main/res/drawable-mdpi/ic_new_sort.png` - `app/src/main/res/drawable-mdpi/ic_new_shield_small.png` - `app/src/main/res/drawable-mdpi/ic_new_player_play.png` - `app/src/main/res/drawable-mdpi/ic_new_create_live.png` - 작업: - `isFirstContent`, `isOriginalSeries`, point/free/original tag의 기존 표시 정책을 정리한다. - 라이브 다시듣기 item에서 `seriesName`은 표시하지 않는다는 PRD 결정을 재확인한다. - 검증: - 위 4개 drawable 리소스가 존재함을 확인한다. - 검증 기록: - 2026-06-17: 기존 홈 오디오 item은 `CreatorChannelHomeAudioContentCardView.bind()`에서 `isOriginalSeries == true`이면 original tag, `isPointAvailable`이면 point tag, `isFirstContent`이면 first tag, `price <= 0`이면 free tag를 표시한다. secondary text는 기존 홈 카드에서 `duration`과 `seriesName`을 조합하지만, 라이브 다시듣기 item에서는 PRD/계획 기준에 따라 `seriesName`을 표시하지 않는 것으로 재확인했다. - 2026-06-17: `AudioContentTag`의 top tag 순서는 `Original`, `First`, bottom tag 순서는 `Point`, `Free`로 정의되어 있음을 확인했다. - 2026-06-17: `test -f`로 `app/src/main/res/drawable-mdpi/ic_new_sort.png`, `ic_new_shield_small.png`, `ic_new_player_play.png`, `ic_new_create_live.png`가 모두 존재함을 확인했다. - [x] **Task 1.3: 채널 공통 API/Repository rename** - rename: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeApi.kt` -> `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/CreatorChannelHomeRepository.kt` -> `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt` - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModel.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModelTest.kt` - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 기타 `CreatorChannelHomeApi`, `CreatorChannelHomeRepository` import 참조 파일 - 작업: - class/interface 이름을 `CreatorChannelApi`, `CreatorChannelRepository`로 변경한다. - 기존 홈 탭 동작은 변경하지 않는다. - 별도 `CreatorChannelLiveApi`, `CreatorChannelLiveRepository`는 만들지 않는다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - rename 후 기존 홈 탭 ViewModel 테스트와 컴파일이 PASS한다. - 검증 기록: - 2026-06-17: rename-only 변경으로 `CreatorChannelHomeApi.kt`/`CreatorChannelHomeRepository.kt`를 각각 `CreatorChannelApi.kt`/`CreatorChannelRepository.kt`로 변경하고 interface/class/import/DI/test 참조를 갱신했다. 홈 endpoint `GET /api/v2/creator-channels/{creatorId}/home`와 기존 repository method 동작은 변경하지 않았다. - 2026-06-17: 신규 동작 추가가 없는 rename-only 작업이므로 RED 테스트 신규 작성 대상에서 제외했다. 기존 회귀 검증은 아래 명령 실행 결과로 누적한다. - 2026-06-17: 최초 병렬 검증 실행 중 `:app:kspDebugKotlin`이 KSP incremental cache(`app/build/kspCaches/debug`) 경합/손상으로 실패했다. 생성물 캐시만 삭제한 뒤 순차 재실행했다. - 2026-06-17: `./gradlew :app:compileDebugKotlin` 순차 재실행 결과 `BUILD SUCCESSFUL`로 통과했다. 기존 deprecation/annotation 경고는 출력되었으나 rename 변경과 무관하다. - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest"` 순차 재실행 결과 `BUILD SUCCESSFUL`로 통과했다. --- ### Phase 2: API/DTO/Repository/ViewModel 계약 추가 - [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`를 라이브 탭 `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`로 통과했다. - [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` - 작업: - `@GET("/api/v2/creator-channels/{creatorId}/live")` endpoint를 추가한다. - query parameter `page`, `size`, `sort`를 전달한다. - `sort`는 `ContentSort.name` 또는 Retrofit enum 변환의 기존 정책을 확인해 서버 값이 `LATEST` 등 대문자로 나가도록 한다. - 검증 명령: - `./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`로 통과했다. - [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` - 작업: - 최초 로딩이 `page=0`, `sort=LATEST`로 호출되는지 검증한다. - `hasNext == true`일 때 `page + 1`로 다음 페이지를 요청하는지 검증한다. - loading 중 중복 load-more 요청이 막히는지 검증한다. - 정렬 변경 시 목록과 page가 초기화되는지 검증한다. - 선택 중인 정렬을 다시 선택하면 API를 재호출하지 않는지 검증한다. - 검증 명령: - `./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.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"` 실행 결과 해당 테스트가 실패함을 확인했다. - [x] **Task 2.4: `CreatorChannelLiveViewModel` 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveViewModel.kt` - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 작업: - initial loading, content, empty, error, pagination loading, pagination error 상태를 정의한다. - `page=0`, `size` 기본값, `sort=LATEST` 초기값을 적용한다. - 정렬 변경과 pagination 요청 중복 방지 로직을 구현한다. - 검증 명령: - `./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.CreatorChannelLivePaginationTest"` - 기대 결과: - ViewModel/pagination 테스트가 PASS한다. - 검증 기록: - 2026-06-17: `CreatorChannelLiveViewModel`을 추가하고 `Loading`, `Empty`, `Error`, `Content` 상태를 정의했다. 초기 로드는 `page=0`, `size=20`, `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`로 통과했다. --- ### Phase 3: UI model과 상태 표시 mapper 구현 - [x] **Task 3.1: 라이브 다시듣기 표시 정책 RED 테스트 작성** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveMapperTest.kt` - 작업: - `isOwned && isRented` 동시 true면 `소장중` 상태가 선택되는지 검증한다. - `price == 0`이면 무료 tag와 play CTA 상태가 선택되는지 검증한다. - `isAdult == true`이면 `ic_new_shield_small` 표시 상태가 선택되는지 검증한다. - `isPointAvailable == true`이면 point tag 표시 상태가 선택되는지 검증한다. - `seriesName`은 secondary text에 포함되지 않는지 검증한다. - `isFirstContent`, `isOriginalSeries`는 기존 오디오 item 정책과 동일한 tag 상태로 매핑되는지 검증한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest"` - 기대 결과: - mapper 미구현 상태에서 RED 실패한다. - 검증 기록: - 2026-06-17: `CreatorChannelLiveMapperTest`를 추가해 `isOwned && isRented` 동시 true 시 `소장중` 우선, 무료 콘텐츠의 free tag/play CTA, 19금 shield, point tag, `seriesName` 미표시, `isFirstContent`/`isOriginalSeries` tag 정책, `ContentSort` label resource 매핑을 검증하도록 했다. - 2026-06-17: production mapper 구현 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest"` 실행 결과 `CreatorChannelLiveReplayStatus`, `toReplayUiModel`, `toLabelResId` 미구현으로 `:app:compileDebugUnitTestKotlin FAILED`가 발생해 RED를 확인했다. - [x] **Task 3.2: UI model/mapper 구현** - 생성 후보: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/model/CreatorChannelLiveUiModels.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/model/CreatorChannelLiveMappers.kt` - 작업: - sort label resource, replay item CTA/status, tag 상태, current live 상태를 UI model로 분리한다. - 가격/포인트/소장중/대여중/play button 상태 우선순위를 순수 함수로 고정한다. - `ContentSort`와 문자열 리소스 매핑을 구현한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest"` - 기대 결과: - mapper 테스트가 PASS한다. - 검증 기록: - 2026-06-17: `CreatorChannelLiveReplayUiModel`, `CreatorChannelLiveReplayStatus`, `CreatorChannelLiveSortOptionUiModel`을 추가하고 `CreatorChannelAudioContentResponse.toReplayUiModel()`, `ContentSort.toLabelResId()`, `ContentSort.toSortOptionUiModel()`을 구현했다. - 2026-06-17: 가격/상태 우선순위는 `소장중` > `대여중` > 무료 play > 가격 표시로 고정했다. tag는 기존 오디오 정책과 동일하게 `isOriginalSeries == true`이면 `Original`, `isFirstContent`이면 `First`, `isPointAvailable`이면 `Point`, `price == 0`이면 `Free`를 매핑한다. `secondaryText`는 `duration`만 사용해 `seriesName`을 표시하지 않는다. - 2026-06-17: `ContentSort` label은 `LATEST`/`POPULAR`/`PRICE_HIGH`/`PRICE_LOW`에 기존 리소스 `screen_audio_content_sort_newest`, `screen_audio_content_sort_popularity`, `screen_audio_content_sort_price_high`, `screen_audio_content_sort_price_low`를 재사용하고, 기존 리소스가 없는 `OWNED`의 `소장순` label은 `creator_channel_live_sort_owned` 문자열로 추가했다. - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest"` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-17: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-17: `./gradlew :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 출력되었으나 Phase 3 변경과 무관하다. - 2026-06-17: 리뷰 게이트에서 `LATEST`가 `최신 콘텐츠`, `OWNED`가 `소장중` 리소스에 매핑되는 문제가 지적되어 `LATEST`는 `screen_audio_content_sort_newest`, `OWNED`는 신규 `creator_channel_live_sort_owned`로 보정했다. - 2026-06-17: label 보정 후 `./gradlew :app:mergeDebugResources` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest"` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS. 병렬 실행 중 `:app:dataBindingGenBaseClassesDebug`가 Gradle 생성물 경합으로 `R-def.txt` 누락 실패했으나 동일 compile 명령 순차 재실행에서 PASS했다. --- ### Phase 4: 라이브 탭 Fragment와 목록 UI 구현 - [x] **Task 4.1: Fragment layout과 replay item layout 추가** - 생성: - `app/src/main/res/layout/fragment_creator_channel_live.xml` - `app/src/main/res/layout/item_creator_channel_live_replay.xml` - 수정: - `app/src/main/res/values/strings.xml` - `app/src/main/res/values-en/strings.xml` - `app/src/main/res/values-ja/strings.xml` - 작업: - Figma 기준 sort-bar, current live card, replay list, owner CTA 영역을 배치한다. - replay item은 88dp 썸네일, title 최대 2줄, duration 1줄, 우측 price/play/status 영역을 포함한다. - `ic_new_shield_small`, `ic_new_player_play`, `ic_new_sort`, `ic_new_create_live`를 layout 또는 binding에서 사용 가능하게 준비한다. - 검증 명령: - `./gradlew :app:mergeDebugResources` - 기대 결과: - 신규 layout/string/drawable 참조가 resource merge에 성공한다. - 검증 기록: - 2026-06-17: Figma `290:8949`, `290:8950`, `290:8954`, `290:8959` 기준으로 `fragment_creator_channel_live.xml`과 `item_creator_channel_live_replay.xml`을 추가했다. sort-bar는 52dp, current live card는 78dp pill, replay item은 88dp 썸네일/최대 2줄 title/1줄 duration/우측 action 영역으로 구성했다. - 2026-06-17: `ic_new_sort`, `ic_new_shield_small`, `ic_new_player_play`, `ic_new_create_live`는 기존 drawable을 재사용하고, live current/price/adult/retry 배경 drawable만 Phase 4 UI 표현에 필요한 최소 리소스로 추가했다. - 2026-06-17: `creator_channel_live_total_label`, empty/error/retry/owner CTA 문자열을 `values`, `values-en`, `values-ja`에 추가했다. - 2026-06-17: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. - [x] **Task 4.2: Adapter와 Fragment 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - `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를 호출한다. - 검증 명령: - `./gradlew :app:compileDebugKotlin` - 기대 결과: - Fragment/adapter가 컴파일된다. - 검증 기록: - 2026-06-17: `CreatorChannelLiveFragment`를 추가해 `CreatorChannelLiveViewModel.liveStateLiveData`의 `Loading`, `Empty`, `Error`, `Content` 상태에 따라 sort-bar/current live/replay list/empty/error/retry visibility를 갱신하도록 했다. - 2026-06-17: error 상태 retry 버튼은 `CreatorChannelLiveViewModel.retryLive()`를 호출한다. RecyclerView 하단 접근 시 `loadMore()`를 호출하며 중복/hasNext guard는 ViewModel 정책을 따른다. - 2026-06-17: `CreatorChannelLiveReplayAdapter`를 추가해 mapper의 `CreatorChannelLiveReplayUiModel`을 19금 shield, original/first/point/free tag, play/소장중/대여중/가격 상태로 바인딩한다. - 2026-06-17: replay item click은 `CreatorChannelLiveFragment.Host.onCreatorChannelLiveReplayClicked(audioContentId)`로 위임하고, `CreatorChannelActivity`에서 기존 `AudioContentDetailActivity` 진입 경로를 재사용하도록 연결했다. current live card click은 기존 홈 탭과 동일하게 `onCreatorChannelCurrentLiveClicked(live)`를 재사용한다. - 2026-06-17: Phase 5 범위인 sort popup/정렬 변경 동작은 구현하지 않고, Phase 4에서는 sort-bar 표시와 icon 준비까지만 유지했다. - 2026-06-17: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. 병렬 Gradle 실행 중 Kotlin incremental cache/daemon 경합 로그가 출력되었으나, fallback/최종 실행은 성공했다. - [x] **Task 4.3: Fragment layout 테스트 추가** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` - 작업: - `fragment_creator_channel_live.xml`에 sort-bar, RecyclerView, owner CTA가 존재하는지 검증한다. - `item_creator_channel_live_replay.xml`에 19금 badge, point/free tag container, price/play/status 영역이 존재하는지 검증한다. - `ic_new_*` 리소스 참조가 layout 또는 코드 상수로 연결되는지 검증한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - 기대 결과: - layout 테스트가 PASS한다. - 검증 기록: - 2026-06-17: production layout/code 추가 전 `CreatorChannelLiveFragmentLayoutTest`를 먼저 작성했다. RED 실행 결과 `fragment_creator_channel_live`, `item_creator_channel_live_replay` 및 필수 id 미존재로 `:app:compileDebugUnitTestKotlin FAILED`가 발생해 실패를 확인했다. - 2026-06-17: layout/source 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL`로 통과해 Phase 4 layout 테스트와 기존 live ViewModel/mapper/pagination 테스트 회귀를 함께 확인했다. --- ### Phase 5: 정렬 컨텍스트 메뉴 구현 - [x] **Task 5.1: Sort popup 구현** - 생성 후보: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveSortPopup.kt` - `app/src/main/res/layout/view_creator_channel_live_sort_menu.xml` - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - 작업: - Sort-bar 정렬 영역 아래에 Figma `290:9041` 형태의 컨텍스트 메뉴를 표시한다. - 현재 선택 항목은 focused 배경으로 표시한다. - 메뉴 외부 터치, 같은 정렬 재선택, 새 정렬 선택 시 메뉴를 닫는다. - 새 정렬 선택 시 ViewModel에 정렬 변경을 전달한다. - 검증: - 작은 화면에서 메뉴가 화면 밖으로 벗어나지 않도록 위치 보정 기준을 코드에 남긴다. - 검증 기록: - 2026-06-17: Figma `290:9041` 기준으로 `view_creator_channel_live_sort_menu.xml`, `bg_creator_channel_live_sort_popup.xml`, `bg_creator_channel_live_sort_selected.xml`을 추가했다. Popup 배경은 `gray_900` fill, `gray_700` 1dp stroke, `radius_14`로 구성하고, 선택 row는 `gray_800` 배경으로 표시한다. - 2026-06-17: `CreatorChannelLiveSortPopup`을 추가해 `ContentSort.entries`와 기존 `toSortOptionUiModel(selectedSort)`/`toLabelResId()` 매핑을 재사용하도록 했다. `PopupWindow`는 `isOutsideTouchable = true`, `isFocusable = true`로 외부 터치 dismiss를 지원하고, 같은 정렬 선택은 dismiss만 수행하며 새 정렬 선택은 `viewModel.changeSort(sort)`로 전달한다. - 2026-06-17: 작은 화면에서 popup 오른쪽이 visible display frame 밖으로 나가는 경우 `calculateHorizontalOffset()`으로 음수 x offset을 계산해 `showAsDropDown()`에 전달하도록 위치 보정 기준을 코드에 남겼다. - 2026-06-17: `CreatorChannelLiveFragment`는 `layoutCreatorChannelLiveSortButton` 클릭 시 현재 `Content` 상태의 `selectedSort`로 popup을 표시하고, `onDestroyView()`에서 `sortPopup?.dismiss()`로 window leak을 방지한다. - [x] **Task 5.2: Sort 동작 테스트 보강** - 수정: - `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/CreatorChannelLiveMapperTest.kt` - 작업: - `ContentSort`별 label resource 매핑을 검증한다. - 정렬 변경 시 첫 페이지가 다시 로딩되는지 검증한다. - 같은 정렬 재선택은 API 재호출을 하지 않는지 검증한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` - 기대 결과: - sort 관련 테스트가 PASS한다. - 검증 기록: - 2026-06-17: production 구현 전 `CreatorChannelLiveFragmentLayoutTest`에 popup layout/resource/source wiring 테스트를 먼저 추가했다. RED 실행 결과 `view_creator_channel_live_sort_menu`, `layout_creator_channel_live_sort_options` 미존재로 `:app:compileDebugUnitTestKotlin FAILED`가 발생해 실패를 확인했다. - 2026-06-17: 기존 `CreatorChannelLiveMapperTest`는 `ContentSort`별 label resource 매핑을 이미 검증하고, 기존 `CreatorChannelLiveViewModelTest`는 정렬 변경 첫 페이지 재로딩 및 같은 정렬 재선택 no-op을 이미 검증하고 있어 중복 테스트를 추가하지 않았다. - 2026-06-17: 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck` 실행 결과 모두 `BUILD SUCCESSFUL`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고와 기존 Kotlin deprecation/annotation 경고는 Phase 5 변경과 무관하다. --- ### Phase 6: 본인 채널 하단 CTA와 라이브 시작 진입 연결 - [x] **Task 6.1: 본인 CTA 노출과 inset 처리 구현** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - `app/src/main/res/layout/fragment_creator_channel_live.xml` - 작업: - 본인 채널일 때만 하단 CTA를 표시한다. - 타인 채널 또는 본인 여부 로딩 전에는 CTA를 숨긴다. - CTA 표시 시 RecyclerView 하단 padding을 추가해 마지막 item이 CTA에 가려지지 않도록 한다. - navigation bar bottom inset을 CTA 영역에 반영한다. - 버튼 icon은 `ic_new_create_live`, label은 `라이브 시작하기` 문자열 리소스를 사용한다. - 검증 명령: - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - CTA resource와 inset 코드가 컴파일된다. - 검증 기록: - 2026-06-18: Figma `665:19359`, `665:19371`을 확인해 본인 채널 하단 CTA가 검정 하단 영역 위 14dp inset, `soda_400` capsule, `ic_new_create_live`, `라이브 시작하기` label로 구성됨을 확인했다. 기존 `fragment_creator_channel_live.xml`의 CTA 골격과 `bg_creator_channel_owner_fab`, `ic_new_create_live`, `creator_channel_live_start_button` 리소스를 재사용했다. - 2026-06-18: `CreatorChannelLiveFragmentLayoutTest`에 owner CTA source/layout 계약 테스트를 추가했다. production 구현 전 실행은 신규 assertion까지 도달하기 전에 기존 KSP unit-test cache 손상으로 `:app:kspDebugUnitTestKotlin FAILED`, `java.io.UTFDataFormatException`이 발생해 `app/build/kspCaches/debugUnitTest`, `app/build/generated/ksp/debugUnitTest` 생성물 캐시를 삭제 후 재검증했다. - 2026-06-18: `CreatorChannelLiveFragment`에 `Host.isCreatorChannelOwner()`, `onCreatorChannelOwnerChanged(isOwner)`, `bindOwnerCta()`를 추가해 본인 채널에서만 `layoutCreatorChannelLiveOwnerCta`를 표시하고 타인 채널 또는 본인 여부 로딩 전에는 숨기도록 했다. - 2026-06-18: `ViewCompat.setOnApplyWindowInsetsListener(binding.layoutCreatorChannelLiveOwnerCta)`로 `WindowInsetsCompat.Type.navigationBars()` bottom inset을 받아 CTA bottom margin에 `14dp + navigationBottomInset`을 적용하고, CTA 표시 시 `rvCreatorChannelLiveReplays` bottom padding을 `102dp + navigationBottomInset`으로 조정해 마지막 item이 CTA에 가려지지 않도록 했다. CTA 미표시 시 기존 32dp bottom padding으로 복원한다. - 2026-06-18: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-18: `./gradlew :app:compileDebugKotlin` 단독 재실행 결과 `BUILD SUCCESSFUL`로 통과했다. 병렬 Gradle 실행 중 한 차례 extension import 해석 실패가 발생했으나 동일 compile 명령 단독 재실행에서 통과했으며, 기존 deprecation/annotation 경고는 Phase 6 변경과 무관하다. - [x] **Task 6.2: 기존 라이브 시작 플로우 연결** - 확인: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt` - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - 필요 시 `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 작업: - `CreatorChannelActivity`의 기존 owner FAB live action과 같은 진입 경로를 재사용한다. - 중복 터치를 방지한다. - 라이브 생성 완료 후 기존 홈 refresh/라이브룸 진입 정책과 충돌하지 않도록 처리한다. - 검증: - 라이브 시작 진입 대상 Activity/Fragment와 전달 extra를 계획 문서 해당 Task의 `검증 기록`에 남긴다. - 검증 기록: - 2026-06-18: `CreatorChannelLiveFragment.Host`에 `onCreatorChannelLiveStartClicked()`를 추가하고 CTA 클릭 시 `layoutCreatorChannelLiveOwnerCta.isEnabled = false` 후 host callback을 호출하도록 연결했다. `onResume()`에서 CTA enabled 상태를 복원해 라이브 생성 화면에서 돌아온 뒤 다시 클릭 가능하다. - 2026-06-18: `CreatorChannelActivity.onCreatorChannelLiveStartClicked()`는 기존 `onOwnerFabLiveClicked()`를 재사용한다. 진입 대상은 `LiveRoomCreateActivity`이며, 기존 `liveRoomCreateLauncher.launch(Intent(this, LiveRoomCreateActivity::class.java))` 경로를 그대로 사용하므로 별도 extra는 추가하지 않는다. - 2026-06-18: 라이브 생성 완료 후 기존 정책과 동일하게 `RESULT_OK`에서 `homeActionDelegate?.refreshHome()`를 호출하고, `Constants.EXTRA_ROOM_ID`, `Constants.EXTRA_ROOM_CHANNEL_NAME` 결과에 따라 `CreatorChannelLiveCoordinator.enterLiveRoom(roomId)` 또는 `creator_channel_live_created_message` toast를 사용한다. - 2026-06-18: `CreatorChannelActivitySourceTest`에 live tab owner CTA가 header 본인 여부를 Fragment로 전달하고 기존 라이브 생성 플로우를 재사용하는 source 계약 테스트를 추가했다. - 2026-06-18: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 재실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-18: `./gradlew :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 Phase 6 변경과 무관하다. #### Phase 6 코드 리뷰/검증 기록 - 2026-06-18: Phase 6 변경 범위를 코드 리뷰했다. `CreatorChannelActivity`의 `currentHeader?.isOwner == true` 기반 owner 상태 전달, `CreatorChannelLiveFragment`의 owner CTA visibility/inset/list bottom padding/click 중복 방지, 기존 `onOwnerFabLiveClicked()` 재사용 경로를 확인했으며 차단 이슈는 발견하지 못했다. - 2026-06-18: fresh verification으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`를 실행해 `BUILD SUCCESSFUL in 27s`로 통과했다. 최초 sandbox 실행은 `~/.gradle` wrapper lock 파일 권한 제한으로 실패해 권한 승인 후 재실행했다. - 2026-06-18: fresh verification으로 `./gradlew :app:mergeDebugResources`를 실행해 `BUILD SUCCESSFUL in 1s`로 통과했다. 최초 sandbox 실행은 `~/.gradle` wrapper lock 파일 권한 제한으로 실패해 권한 승인 후 재실행했다. - 2026-06-18: fresh verification으로 `./gradlew :app:compileDebugKotlin`을 실행해 `BUILD SUCCESSFUL in 1s`로 통과했다. - 2026-06-18: fresh verification으로 `./gradlew :app:ktlintCheck`를 실행해 `BUILD SUCCESSFUL in 11s`로 통과했다. Gradle deprecation 경고는 기존 빌드 설정 경고로 Phase 6 변경과 직접 관련 없다. --- ### Phase 7: 탭 연결과 통합 동작 - [x] **Task 7.1: `CreatorChannelTab.Live`를 실제 Fragment로 연결** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt` - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt` - 작업: - Live 탭 position에서 `CreatorChannelLiveFragment.newInstance(creatorId)`를 반환한다. - 나머지 탭은 기존 placeholder 정책을 유지한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"` - 기대 결과: - Live 탭만 실제 Fragment로 연결되고 나머지 placeholder 정책은 유지된다. - 검증 기록: - 2026-06-17: 리뷰 게이트 차단 이슈 보정으로 `CreatorChannelPagerAdapter`에서 `CreatorChannelTab.Live -> CreatorChannelLiveFragment.newInstance(creatorId)` 분기를 추가했다. Home/Live 탭은 실제 Fragment로 연결하고, 후속 탭은 기존 placeholder 정책을 유지한다. - 2026-06-17: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS. - [x] **Task 7.2: DI와 lifecycle 통합** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - 작업: - 신규 ViewModel/API/Repository injection이 기존 홈 탭과 충돌하지 않는지 확인한다. - Fragment 재생성 후 정렬, page, 목록 상태가 ViewModel에 유지되는지 확인한다. - 탭 전환 시 중복 최초 API 호출이 발생하지 않도록 초기 로딩 조건을 정리한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - Live 탭 관련 테스트와 컴파일이 PASS한다. - 검증 기록: - 2026-06-18: `AppDI`에서 `CreatorChannelApi`와 `CreatorChannelRepository`는 기존 홈 탭 공통 binding을 유지하고, `CreatorChannelLiveViewModel(get())`가 같은 repository를 주입받는 것을 확인했다. 별도 Live API/Repository binding은 추가하지 않았다. - 2026-06-18: `CreatorChannelLiveFragment.onViewCreated()`는 초기 API 호출을 수행하지 않고, `CreatorChannelActivity.onPageSelected()`가 Live 탭 선택 시 `findLiveFragment()?.onCreatorChannelLiveTabSelected()`를 호출해 `viewModel.loadLive(creatorId)`를 시작하는 lazy load 경로를 확인했다. - 2026-06-18: `CreatorChannelLiveViewModel.loadLive()`는 같은 `creatorId`와 기존 상태가 있으면 no-op 하므로 Fragment 재생성/탭 재바인딩 중 정렬, page, 목록 상태를 유지한다. 해당 계약은 `CreatorChannelLiveViewModelTest`의 `같은 creatorId로 다시 loadLive를 호출하면 기존 상태를 유지하고 API를 재호출하지 않는다` 테스트로 고정되어 있음을 확인했다. - 2026-06-18: `CreatorChannelActivity`의 `binding.viewPager.offscreenPageLimit = CreatorChannelTab.entries.size - 1` 설정과 Live 탭 선택 hook은 `CreatorChannelActivitySourceTest`에서 고정되어 있어 Home delegate 보존과 Live lazy load 통합 계약이 유지됨을 확인했다. - 2026-06-18: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL in 34s`로 통과했다. - 2026-06-18: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 7s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 Phase 7.2 변경과 직접 관련 없다. #### Phase 7 코드 리뷰/검증 기록 - 2026-06-18: Phase 7 전체 변경 범위를 코드 리뷰했다. `CreatorChannelPagerAdapter`는 `CreatorChannelTab.Live`에서 `CreatorChannelLiveFragment.newInstance(creatorId)`를 반환하고 Home/후속 탭 정책은 유지한다. `CreatorChannelActivity`는 Live 탭 선택 시 `onCreatorChannelLiveTabSelected()`로 lazy load를 시작하고, NestedScrollView 하단 접근 및 content 변경 후 bottom 재평가에서 `loadMore()` 경로를 호출한다. `AppDI`는 공통 `CreatorChannelApi`/`CreatorChannelRepository` binding과 `CreatorChannelLiveViewModel(get())` 주입을 사용하며 별도 Live API/Repository binding을 만들지 않는다. `CreatorChannelLiveViewModel.loadLive()`의 동일 `creatorId` no-op 조건과 관련 테스트로 Fragment 재생성/탭 재바인딩 중 상태 유지 계약을 확인했다. 차단 이슈는 발견하지 못했다. - 2026-06-18: fresh verification으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"`를 실행해 `BUILD SUCCESSFUL in 8s`로 통과했다. 최초 sandbox 실행은 `~/.gradle` wrapper lock 파일 권한 제한으로 실패해 권한 승인 후 재실행했다. - 2026-06-18: fresh verification으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"`를 실행해 `BUILD SUCCESSFUL in 11s`로 통과했다. - 2026-06-18: fresh verification으로 `./gradlew :app:compileDebugKotlin`을 실행해 `BUILD SUCCESSFUL in 2s`로 통과했다. - 2026-06-18: fresh verification으로 `./gradlew :app:ktlintCheck`를 실행해 `BUILD SUCCESSFUL in 1s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 Phase 7 변경과 직접 관련 없다. --- ### Phase 8: 최종 검증과 문서 기록 - [x] **Task 8.1: 자동 검증 실행** - 실행 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - 기대 결과: - 모든 명령이 `BUILD SUCCESSFUL`로 통과한다. - 검증 기록: - 실행 일시, 명령, 결과, 기존 경고 여부를 이 Task 아래에 누적 기록한다. - 2026-06-18: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL in 12s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - 2026-06-18: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` 실행 결과 `BUILD SUCCESSFUL in 11s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - 2026-06-18: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` 실행 결과 `BUILD SUCCESSFUL in 14s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - 2026-06-18: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 1s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - 2026-06-18: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 21s`로 통과했다. Gradle daemon 1개 busy로 신규 daemon이 시작됐고, Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - 2026-06-18: `./gradlew :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 16s`로 통과했다. Gradle daemon 1개 busy로 신규 daemon이 시작됐고, Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - [ ] **Task 8.2: 수동 확인** - 확인 항목: - `라이브` 탭 진입 시 `page=0`, `sort=LATEST`로 최초 조회된다. - Sort-bar에 `전체 {count}`와 현재 정렬 label, `ic_new_sort`가 표시된다. - 정렬 컨텍스트 메뉴가 Sort-bar 아래에 표시되고 선택/닫힘 동작이 정상이다. - `currentLive != null`일 때 현재 라이브 카드가 표시된다. - 라이브 다시듣기 item의 19금/포인트/무료/소장중/대여중/가격 상태가 DTO와 일치한다. - `isOwned && isRented` 동시 true item은 `소장중`으로 표시된다. - `seriesName`은 표시되지 않는다. - `hasNext == true`일 때 하단 스크롤로 다음 페이지가 append된다. - 본인 채널에서만 하단 `라이브 시작하기` CTA가 표시된다. - 타인 채널에서는 하단 CTA가 표시되지 않는다. - CTA가 마지막 item을 가리지 않는다. - 검증 기록: - 실제 확인한 시나리오와 결과를 이 Task 아래에 한국어로 누적 기록한다. - 2026-06-18: 연결 단말 확인을 위해 `adb devices`를 실행했고 `2cec640c34017ece device`가 연결되어 있음을 확인했다. `adb shell pm list packages | rg "sodalive"` 결과 `kr.co.vividnext.sodalive.debug`, `kr.co.vividnext.sodalive`가 설치되어 있음을 확인했다. - 2026-06-18: `CreatorChannelActivity`를 `extra_creator_id=1`로 직접 실행해 Live 탭 실제 화면 검증을 시도했으나, Activity가 exported 대상이 아니어서 `Permission Denial ... not exported` 보안 예외로 차단됐다. - 2026-06-18: `adb shell monkey -p kr.co.vividnext.sodalive.debug 1`로 디버그 앱 런처 실행은 성공했다. 실행 후 focus는 `kr.co.vividnext.sodalive.debug/com.gun0912.tedpermission.TedPermissionActivity`로 확인되어 권한 화면 진입까지만 실제 단말 표면으로 확인했다. - 2026-06-18: Live 탭 상세 화면은 로그인/권한/앱 내부 이동 및 서버 데이터가 필요한 경로라 현재 shell 직접 실행으로는 Figma 대비 실제 화면 대조를 완료하지 못했다. 따라서 Task 8.2는 남은 실제 수동 QA로 유지한다. - 2026-06-18: 자동 검증과 소스/테스트 근거로 `page=0`/`sort=LATEST` 최초 조회, 정렬 변경/동일 정렬 no-op, load-more append, `isOwned && isRented` 시 `소장중` 우선, `seriesName` 미표시, owner CTA visibility/inset/click 중복 방지, `ic_new_sort` 연결은 확인했다. 실제 화면의 Sort popup 표시/닫힘, current live 카드 렌더링, CTA가 마지막 item을 가리지 않는지는 후속 실제 단말 시나리오에서 확인이 필요하다. --- ### Phase 4 Review Fix: Lazy Load와 Empty/Error 중앙 정렬 보정 - [x] **Task RF4.1: Live 탭 선택 시점 lazy load 보정** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 작업: - Live Fragment 생성 시점인 `onViewCreated()`에서는 `loadLive()`를 호출하지 않는다. - `CreatorChannelTab.Live`가 선택된 시점에만 Live Fragment에 명시적으로 로딩을 요청한다. - 같은 creatorId/state에 대한 중복 호출 방지는 기존 `CreatorChannelLiveViewModel.loadLive()` guard를 유지한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - 기대 결과: - Live 탭을 보지 않은 Activity 최초 진입에서는 Live API 요청이 시작되지 않고, Live 탭 선택 시점에 첫 조회가 시작된다. - 검증 기록: - 2026-06-17: production 보정 전 source 테스트에서 `offscreenPageLimit = CreatorChannelTab.entries.size - 1`, `onViewCreated()` 즉시 `viewModel.loadLive(creatorId)` 호출, Live 선택 hook 미존재로 RED 실패를 확인했다. - 2026-06-17: `CreatorChannelLiveFragment.onViewCreated()`의 즉시 `loadLive()` 호출을 제거하고 `onCreatorChannelLiveTabSelected()`로 이동했다. `CreatorChannelActivity.onPageSelected()`는 Live 탭 선택 시 `viewPager.post { findLiveFragment()?.onCreatorChannelLiveTabSelected() }`를 호출하며, 전체 탭 선생성을 유발하던 `offscreenPageLimit = CreatorChannelTab.entries.size - 1` 설정을 제거했다. - 2026-06-17: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` PASS. - [x] **Task RF4.2: Empty/Error 상태 viewport 중앙 정렬 보정** - 수정: - `app/src/main/res/layout/fragment_creator_channel_live.xml` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 작업: - Live Fragment root가 `ViewPager2` page 높이를 채우도록 보장한다. - Empty/Error 상태 전환 시에도 `ViewPager2` 높이 재측정을 요청한다. - Activity의 `updateViewPagerHeight()`는 현재 page의 최소 높이를 탭 viewport 기준으로 보정한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:mergeDebugResources` - 기대 결과: - Live Empty 문구와 Error/Retry 그룹이 작은 wrap-content 영역이 아니라 탭 viewport 중앙에 표시된다. - 검증 기록: - 2026-06-17: production 보정 전 layout/source 테스트에서 Live root `wrap_content`, Empty/Error 상태 height callback 미보장, Activity page minimumHeight 보정 미존재로 RED 실패를 확인했다. - 2026-06-17: `fragment_creator_channel_live.xml` root 높이를 `match_parent`로 변경하고, `bindEmpty()`/`bindError()`에서도 `host.onCreatorChannelLiveContentChanged()`를 호출하도록 했다. `CreatorChannelActivity.updateViewPagerHeight()`는 현재 page 측정 전 `currentPage.minimumHeight = calculateCreatorChannelTabViewportHeight()`를 적용해 탭 viewport 기준 최소 높이를 보장한다. - 2026-06-17: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` PASS, `./gradlew :app:mergeDebugResources` PASS. - [x] **Task RF4.3: Home delegate 보존과 load-more 메타데이터 보존 보정** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveViewModel.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLivePaginationTest.kt` - 작업: - Live lazy load는 유지하되 Home 탭의 `HomeActionDelegate`가 후속 탭 이동 중 제거되지 않도록 `ViewPager2.offscreenPageLimit`을 기존처럼 전체 탭 수 기준으로 복구한다. - 다음 페이지 성공 시 기존 첫 페이지의 `currentLive`, `liveReplayContentCount`, `selectedSort`를 보존하고, 목록 append와 `page`/`size`/`hasNext`만 다음 응답 기준으로 갱신한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLivePaginationTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - Home 탭 delegate 기반 상단 액션/refresh 경로가 Live 및 후속 탭 이동 후에도 유지된다. - 다음 페이지 응답이 `currentLive == null`이거나 총 개수가 부분 값이어도 현재 라이브 카드와 총 개수/정렬 label이 첫 페이지 상태를 유지한다. - 검증 기록: - 2026-06-17: `CreatorChannelActivity.setupTabsAndPager()`에 `binding.viewPager.offscreenPageLimit = CreatorChannelTab.entries.size - 1`를 복구해 Home 탭 Fragment와 `HomeActionDelegate`가 후속 탭 이동 중 제거되지 않도록 했다. `CreatorChannelActivitySourceTest`의 기존 offscreenPageLimit 제거 기대는 복구 정책에 맞춰 갱신했다. - 2026-06-17: `CreatorChannelLiveViewModel.loadMore()` 성공 처리에서 `data.toContentState()`로 전체 content metadata를 덮어쓰지 않고, 기존 `Content`를 `copy()`해 `liveReplayContents` append와 `page`/`size`/`hasNext`만 다음 응답 기준으로 갱신하도록 변경했다. - 2026-06-17: `CreatorChannelLivePaginationTest`에 다음 페이지 응답이 `currentLive == null`, 다른 `liveReplayContentCount`, 다른 `sort`를 내려줘도 첫 페이지의 현재 라이브/count/sort를 보존하는 회귀 테스트를 추가했다. - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 최초 실행은 상충하던 기존 offscreenPageLimit 제거 assertion으로 실패했고, 테스트 계약 갱신 후 재실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLivePaginationTest"` 최초 실행은 신규 테스트 fixture의 `CreatorChannelLiveResponse` 필드명 오기로 컴파일 실패했고, `coverImageUrl`로 수정 후 재실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-17: 추가 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 `BUILD SUCCESSFUL`로 통과했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 계속 출력되었다. --- ### Phase 6 Follow-up: CTA Figma 고정 영역 보정 - [x] **Task RF6.1: 라이브 시작하기 CTA를 Figma `665:19371` 기준으로 보정** - 수정: - `app/src/main/res/layout/fragment_creator_channel_live.xml` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` - 작업: - CTA outer 영역은 화면 하단에 고정된 100dp black container로 유지한다. - 내부 버튼은 좌우 14dp, top 14dp 위치의 `soda_400` capsule로 표시한다. - CTA는 스크롤 컨텐츠가 아니라 Fragment root에 고정해 스크롤하지 않아도 보이게 한다. - CTA 표시 시 리스트 하단 padding은 CTA 100dp 영역과 navigation bar inset을 반영해 마지막 item이 가려지지 않게 한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - 기대 결과: - 본인 채널의 `라이브 시작하기` CTA가 Figma처럼 하단 고정 100dp 영역 안에 표시되고, 스크롤하지 않아도 보인다. - 검증 기록: - 2026-06-18: Figma `665:19359`, CTA layer `665:19371`을 재확인했다. CTA는 `bottom=0`, `height=100`, black 배경의 outer 영역이며, 내부 버튼은 `left/right=14`, `top=14`, `soda_400 #00BDF7`, 24dp icon, 18sp medium label 구조다. 기존 구현은 56dp capsule 자체를 parent 하단에 붙이는 구조라 Figma outer CTA 영역과 달라 보정한다. - 2026-06-18: production 보정 전 `CreatorChannelLiveFragmentLayoutTest`를 실행해 Activity root overlay CTA 계약 미충족으로 실패함을 확인했다. 최초 RED 시도는 테스트 문자열 escaping 오류로 컴파일 실패했고, 테스트 문자열을 수정한 뒤 `CreatorChannelLiveFragmentLayoutTest > 라이브 owner CTA는 Activity root overlay로 Figma 고정 영역을 제공한다`가 assertion 실패로 RED가 됐다. - 2026-06-18: CTA를 `fragment_creator_channel_live.xml`에서 제거하고 `activity_creator_channel.xml` root overlay로 이동했다. Activity overlay는 100dp black container, 내부 52dp `soda_400` capsule, 좌우/상단 14dp margin, `ic_new_create_live`, `creator_channel_live_start_button` 구조로 Figma `665:19371`을 맞춘다. - 2026-06-18: `CreatorChannelActivity`가 Live 탭이면서 본인 채널일 때만 CTA를 표시하도록 `updateLiveOwnerCtaVisibility()`를 추가했다. navigation bar inset은 CTA container 높이와 `nestedScrollView` bottom padding에 반영해 CTA가 화면 하단에 고정되어도 컨텐츠가 가려지지 않도록 했다. - 2026-06-18: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL in 1m 33s`로 GREEN을 확인했다. - 2026-06-18: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 1s`로 통과했다. - 2026-06-18: `./gradlew :app:ktlintCheck` 최초 실행은 제가 추가한 빈 줄 1개로 실패했고, 빈 줄 제거 후 재실행 결과 `BUILD SUCCESSFUL in 31s`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 계속 출력되었다. - 2026-06-18: 병렬 Gradle 실행 중 Kotlin incremental cache 경합으로 `compileDebugKotlin`이 실패/timeout 되었으나 `./gradlew --stop` 후 순차 재실행한 `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 2m 4s`로 통과했다. 기존 deprecation/annotation 경고는 변경 범위와 무관하다. - 2026-06-18: 최종 확인으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`를 순차 재실행해 `BUILD SUCCESSFUL in 27s`로 통과했다. - [x] **Task RF6.2: 실제 기기 bottom 위치와 짧은 Live 상태 스크롤 보정** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/res/layout/fragment_creator_channel_live.xml` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` - 작업: - `BaseActivity` root bottom padding과 `owner_fab_button`/`layout_creator_channel_live_owner_cta`의 navigation inset 중복 적용을 제거한다. - Live 탭의 empty/error처럼 짧은 상태에서는 `ViewPager2` page가 viewport 높이로 강제 확장되지 않아 스크롤 없이 안내 문구와 retry 버튼이 보이도록 한다. - CTA 표시 시 content bottom padding은 CTA 100dp 영역만 반영하고 navigation inset은 root padding에 맡긴다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - 기대 결과: - 실제 기기에서 owner FAB와 live CTA가 XML preview 기준 bottom 위치와 동일한 safe-area 기준으로 표시된다. - Live 탭 error/empty 같은 짧은 상태에서 불필요한 긴 스크롤이 생기지 않는다. - 검증 기록: - 2026-06-18: 사용자 실제 기기 확인 결과 `owner_fab_button`과 `layout_creator_channel_live_owner_cta`가 XML preview보다 위에 표시되고, Live 탭 error/empty 짧은 상태에서도 Fragment 높이가 길어 스크롤이 발생함을 확인 요청받았다. 코드 확인 결과 `BaseActivity`가 root에 system bar bottom padding을 이미 적용하는데 `CreatorChannelActivity`가 동일 navigation inset을 FAB margin과 CTA height/padding에 추가로 반영하고 있어 bottom 위치가 중복 보정될 수 있음을 확인했다. - 2026-06-18: RED 확인으로 `CreatorChannelActivitySourceTest`와 `CreatorChannelLiveFragmentLayoutTest`에 navigation inset 중복 제거, `currentPage.minimumHeight` 강제 제거, Live root `wrap_content` 계약을 추가한 뒤 실행했다. production 보정 전 `CreatorChannelActivitySourceTest > 라이브 탭 pagination과 높이 갱신은 NestedScrollView 소유 스크롤 경로에서 처리한다`, `CreatorChannelActivitySourceTest > 크리에이터 채널 하단 고정 UI는 BaseActivity root bottom padding과 navigation inset을 중복 적용하지 않는다`, `CreatorChannelLiveFragmentLayoutTest > 라이브 fragment layout은 sort current live list empty error owner CTA를 제공한다`, `CreatorChannelLiveFragmentLayoutTest > 라이브 fragment와 adapter source는 필수 drawable과 retry loadMore click 연결을 포함한다`가 실패함을 확인했다. - 2026-06-18: `CreatorChannelActivity.setupOwnerFabInsets()`에서 FAB margin에 navigation inset을 더하던 listener를 제거하고, ViewPager content bottom padding만 유지했다. Live CTA도 Activity root overlay의 XML 100dp height를 유지하고, `nestedScrollView` bottom padding에는 CTA 100dp만 반영하도록 변경했다. navigation bar 대응은 `BaseActivity` root bottom padding에 맡긴다. - 2026-06-18: 짧은 Live 상태에서 불필요하게 긴 스크롤이 생기지 않도록 `updateViewPagerHeight()`의 `currentPage.minimumHeight = calculateCreatorChannelTabViewportHeight()` 강제 설정을 제거하고, `fragment_creator_channel_live.xml` root를 `wrap_content`로 변경했다. empty/error 안내 영역은 parent bottom constraint를 제거해 실제 표시 컨텐츠 높이만 측정되도록 했다. - 2026-06-18: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL in 30s`로 통과했다. - 2026-06-18: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 2s`로 통과했다. - 2026-06-18: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 1s`로 통과했다. - 2026-06-18: `./gradlew :app:ktlintCheck` 최초 실행은 테스트 source assertion 2줄의 line length/argument wrapping 위반으로 실패했고, 줄바꿈 보정 후 재실행 결과 `BUILD SUCCESSFUL in 6s`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 계속 출력되었다. - 2026-06-18: 포맷 보정 후 최종 targeted 재검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`를 실행해 `BUILD SUCCESSFUL in 13s`로 통과했다. --- ### Phase 7 Follow-up: 탭 전환 시 sticky tabbar anchor 보정 - [x] **Task RF7.1: 탭 전환 시 sticky 위치로 먼저 정렬한 뒤 컨텐츠 표시** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - 판단: - 현재 구조는 `NestedScrollView`가 전체 스크롤을 소유하고, `ViewPager2` page 높이가 선택 탭 컨텐츠 높이에 맞춰 바뀐다. - 짧은 탭으로 전환하면 parent scroll range가 줄면서 기존 sticky 상태가 풀려 tabbar가 header 아래로 갑자기 되돌아가 보일 수 있다. - 탭을 선택한 순간 사용자의 목적은 header 재확인이 아니라 선택한 탭 컨텐츠 탐색이므로, tabbar를 sticky anchor로 고정한 뒤 컨텐츠를 보여주는 편이 전환감과 예측 가능성이 좋다. - 따라서 sticky 상태가 아니더라도 다른 탭 선택 시 `NestedScrollView`를 sticky threshold까지 보정하는 방식을 채택한다. - 작업: - sticky threshold 계산은 기존 `updateScrollState()`와 같은 기준을 사용한다. - `stickyTop = CreatorChannelScrollState.calculateStickyTop(statusBarHeight, baseTitleBarHeight)` - `stickyScrollY = (binding.headerContainer.height - stickyTop).coerceAtLeast(0)` - `ViewPager2.OnPageChangeCallback.onPageSelected(position)`에서 이전 탭과 다른 탭으로 전환될 때 sticky 보정을 수행한다. - 현재 `nestedScrollView.scrollY`가 `stickyScrollY`보다 작은 경우에만 `scrollTo(0, stickyScrollY)` 또는 동등한 즉시 보정을 수행한다. - 현재 `nestedScrollView.scrollY`가 이미 `stickyScrollY` 이상이면 사용자의 기존 scroll 위치를 낮추지 않는다. - 같은 탭 재선택 또는 초기 adapter attach 과정에서 불필요한 scroll 보정이 발생하지 않도록 마지막 선택 tab index 또는 초기 선택 여부를 관리한다. - Live 탭 lazy load, pagination bottom check, owner CTA visibility 갱신은 기존 흐름을 유지한다. - RED 테스트: - `CreatorChannelActivitySourceTest`에 탭 전환 시 sticky threshold helper와 `nestedScrollView.scrollTo(0, stickyScrollY)` 호출 계약을 추가한다. - 같은 탭 재선택/초기 선택에서는 sticky 보정을 하지 않는 guard 계약을 추가한다. - 이미 sticky 기준 이상으로 스크롤된 경우 scrollY를 낮추지 않는 guard 계약을 추가한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - 기대 결과: - Home 또는 Live 등 어느 탭에서든 다른 탭을 선택하면 tabbar가 sticky 위치로 유지된 상태에서 선택 탭 컨텐츠가 표시된다. - 선택 탭 컨텐츠가 empty/error처럼 짧아도 tabbar가 header 아래로 풀리는 전환이 보이지 않는다. - 이미 sticky보다 아래로 스크롤한 상태에서 탭을 바꿔도 scrollY가 위로 되돌아가지 않는다. - 검증 기록: - 2026-06-18: 사용자 피드백에 따라 탭 전환 시 짧은 컨텐츠 때문에 sticky tabbar가 풀려 보이는 현상을 검토했다. 현재 `updateViewPagerHeight()`가 선택 page 높이를 컨텐츠에 맞추고 parent `NestedScrollView`가 스크롤을 소유하므로, 짧은 탭 전환 시 scroll range 변화로 tabbar가 갑자기 header 아래 위치로 보일 수 있다고 판단했다. UX상 탭 전환 시점에는 sticky tabbar를 anchor로 유지하는 편이 더 자연스럽다고 판단해 PRD와 plan-task에 후속 작업을 추가했다. 구현은 아직 진행하지 않았다. - 2026-06-18: `CreatorChannelActivitySourceTest`에 탭 전환 시 sticky anchor 보정 source 계약을 추가했다. production 보정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 결과 `탭 전환은 sticky tabbar anchor 아래로 내려간 scroll 위치를 되돌리지 않고 부족할 때만 보정한다`가 assertion 실패로 RED가 됐다. - 2026-06-18: `CreatorChannelActivity`에 `lastSelectedCreatorChannelTabPosition`, `adjustCreatorChannelStickyAnchorOnTabSelected(position)`, `calculateCreatorChannelStickyScrollY()`를 추가했다. 초기 선택 또는 같은 탭 재선택은 무시하고, 다른 탭으로 전환할 때 현재 `NestedScrollView.scrollY`가 sticky threshold보다 작으면 `scrollTo(0, stickyScrollY)`로 보정하며 이미 sticky 기준 이상이면 기존 scroll 위치를 낮추지 않는다. - 2026-06-18: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 결과 `BUILD SUCCESSFUL in 26s`로 GREEN을 확인했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - 2026-06-18: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 11s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - 2026-06-18: `./gradlew :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 25s`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 계속 출력됐다. - 2026-06-18: 리뷰 게이트에서 `TabLayoutMediator.attach()` 이후 `OnPageChangeCallback`을 등록하므로 초기 Home 선택 이벤트를 받지 못하면 첫 실제 탭 전환이 `previousPosition == null`로 처리되어 sticky 보정이 누락될 수 있다는 차단 이슈가 발견됐다. - 2026-06-18: `CreatorChannelActivitySourceTest`에 `lastSelectedCreatorChannelTabPosition = binding.viewPager.currentItem` baseline 초기화 계약을 추가했다. production 보정 전 동일 테스트를 실행해 `탭 전환은 sticky tabbar anchor 아래로 내려간 scroll 위치를 되돌리지 않고 부족할 때만 보정한다`가 assertion 실패로 RED가 됐다. - 2026-06-18: `setupTabsAndPager()`에서 `TabLayoutMediator.attach()` 직후, `OnPageChangeCallback` 등록 전 `lastSelectedCreatorChannelTabPosition = binding.viewPager.currentItem`를 초기화했다. 이로써 첫 실제 탭 전환도 이전 탭과 다른 선택으로 판단되어 sticky anchor 보정이 수행된다. - 2026-06-18: 병렬/daemon Gradle 재검증 중 `processDebugResources`의 기본 resource not found, `packageDebugResources` 삭제 실패, `dataBindingGenBaseClassesDebug`의 `chk_fri` 누락, Kotlin incremental cache `lookups.tab` 중복 등록이 발생했다. 변경 코드 실패가 아니라 생성물/daemon cache 문제로 판단해 `app/build/intermediates`, `app/build/generated`, `app/build/kspCaches`, `app/build/tmp`, `app/build/kotlin` 생성물 캐시를 정리하고 Gradle/Kotlin daemon을 비활성화해 순차 재검증했다. - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 4m 19s`로 통과했다. 기존 Kotlin deprecation/annotation 경고는 변경 범위와 무관하게 출력됐다. - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 결과 `BUILD SUCCESSFUL in 1m 32s`로 통과했다. 기존 Gradle 9.0 호환성 deprecation 경고는 계속 출력됐다. - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:ktlintCheck` 실행 결과 `:app:ktlintMainSourceSetCheck`에서 실패했다. 보고서 확인 결과 `Agora.kt`, `SodaLiveApp.kt`, `audio_content` package-name 등 기존 main source 위반 574건이 출력됐고, `rg -n "CreatorChannelActivity|creator/channel|plan-task" app/build/reports/ktlint/ktlintMainSourceSetCheck/ktlintMainSourceSetCheck.txt` 결과 이번 RF7.1 변경 파일은 포함되지 않았다. --- ### Phase 8 Follow-up: Live empty 안내와 다시듣기 item Figma 보정 - [x] **Task RF8.1: Live empty 안내 메시지와 다시듣기 item 간격/썸네일 radius 보정** - 수정: - `app/src/main/res/layout/fragment_creator_channel_live.xml` - `app/src/main/res/layout/item_creator_channel_live_replay.xml` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` - 작업: - Live empty 상태에서 `크리에이터가 라이브를 준비 중입니다.\n기대해 주세요!` 안내 문구가 실제 탭 영역 안에서 보이도록 empty 영역 높이와 중앙 정렬을 보장한다. - 라이브 다시듣기 item 사이 간격을 Figma 기준 8dp로 적용한다. - 라이브 다시듣기 썸네일 이미지는 Figma 기준 14dp rounded corner로 실제 이미지가 clipping되도록 적용한다. - RED 테스트: - `CreatorChannelLiveFragmentLayoutTest`에 empty 메시지 표시 영역이 0dp match-constraints가 아닌 viewport 높이를 갖는지 확인하는 source/layout 계약을 추가한다. - `CreatorChannelLiveFragmentLayoutTest`에 replay item 8dp spacing과 thumbnail `radius_14` clipping 계약을 추가한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - Live empty 상태에서 안내 메시지가 보인다. - 라이브 다시듣기 item 사이 간격은 8dp이고 썸네일 이미지는 14dp rounded corner로 표시된다. - 검증 기록: - 2026-06-18: 현재 코드 확인 결과 Live empty 문구는 root `ConstraintLayout` top에 직접 붙은 `TextView`라 `wrap_content` page 높이/탭 전환 환경에서 보장된 표시 영역을 갖지 못할 수 있음을 확인했다. 라이브 다시듣기 item은 썸네일 background에 `radius_14`가 있으나 실제 이미지 clipping이 없고, item 간 8dp 간격도 적용되지 않았다. - 2026-06-18: `CreatorChannelLiveFragmentLayoutTest`에 empty container 표시 영역과 replay item 8dp spacing/thumbnail clipping 계약을 추가했다. production 보정 전 실행한 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`는 `layout_creator_channel_live_empty` 미존재로 `:app:compileDebugUnitTestKotlin FAILED`가 발생해 RED를 확인했다. - 2026-06-18: `fragment_creator_channel_live.xml`에 `layout_creator_channel_live_empty` `FrameLayout`을 추가해 `minHeight=360dp`, `gravity=center` 안에서 empty message를 중앙 표시하도록 변경했다. `CreatorChannelLiveFragment`는 empty/loading/error/content 상태에서 `layoutCreatorChannelLiveEmpty` visibility를 제어하도록 보정했다. - 2026-06-18: `item_creator_channel_live_replay.xml` root에 `android:layout_marginBottom="@dimen/spacing_8"`을 추가하고, 썸네일 container에 `android:clipToOutline="true"`, `android:outlineProvider="background"`를 적용했다. `CreatorChannelLiveReplayAdapter.ViewHolder`에서도 `ViewOutlineProvider`를 설정해 실제 썸네일 이미지를 `radius_14`로 clipping하도록 보강했다. - 2026-06-18: 보정 후 `CreatorChannelLiveFragmentLayoutTest` 최초 GREEN 시도는 테스트가 item root margin이 아니라 thumbnail margin을 확인해 실패했고, Figma 기준인 item 사이 간격을 source 계약으로 확인하도록 테스트를 바로잡았다. 재실행 결과 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`가 `BUILD SUCCESSFUL in 1m 7s`로 통과했다. - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 28s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 25s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - [x] **Task RF8.2: Live empty 최소 높이를 sticky 가능한 탭 viewport 기준으로 보정** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - `app/src/main/res/layout/fragment_creator_channel_live.xml` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` - 작업: - 고정 `360dp` empty 높이를 제거한다. - Activity가 sticky anchor 이후 실제 탭 viewport를 계산해 Live Fragment empty container의 최소 높이로 전달한다. - Live empty 상태에서 `NestedScrollView`의 최대 scroll range가 sticky threshold보다 작아져 tabbar가 header 아래로 풀리는 상황을 방지한다. - content/error 상태의 짧은 스크롤 정책은 기존 RF6.2 범위를 유지하고, empty 상태만 최소 높이를 보정한다. - RED 테스트: - `CreatorChannelActivitySourceTest`에 `calculateCreatorChannelLiveEmptyMinHeight()`와 `findLiveFragment()?.onCreatorChannelLiveViewportHeightChanged(...)` 호출 계약을 추가한다. - `CreatorChannelLiveFragmentLayoutTest`에 고정 `android:minHeight="360dp"` 제거와 runtime `layoutCreatorChannelLiveEmpty.minimumHeight` 적용 계약을 추가한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - Live empty 안내 문구가 실제 탭 viewport 중앙에 표시된다. - Live empty 탭 전환 시 sticky tabbar anchor가 scroll range 부족으로 풀리지 않는다. - 검증 기록: - 2026-06-18: 리뷰 지적에 따라 RF8.1의 고정 `360dp` empty 높이가 실제 탭 viewport 중앙/Sticky 유지 요구를 보장하지 못할 수 있음을 확인했다. 탭 컨텐츠가 너무 짧으면 `NestedScrollView.scrollTo(stickyScrollY)`가 최대 scroll range에 clamp되어 tabbar가 sticky anchor에 도달하지 못할 수 있으므로 Activity 계산값을 empty 상태에만 전달하도록 보정한다. - 2026-06-18: production 보정 전 `CreatorChannelActivitySourceTest`와 `CreatorChannelLiveFragmentLayoutTest`에 viewport min height 전달 계약과 고정 `360dp` 제거 계약을 추가했다. 실행 결과 `라이브 탭 pagination과 높이 갱신은 NestedScrollView 소유 스크롤 경로에서 처리한다`, `라이브 empty 최소 높이는 sticky anchor 이후 탭 viewport 기준으로 전달한다`, `라이브 fragment layout은 sort current live list empty error owner CTA를 제공한다`, `라이브 empty container 최소 높이는 Activity가 전달한 viewport 높이를 사용한다` 4개 테스트가 실패해 RED를 확인했다. - 2026-06-18: `fragment_creator_channel_live.xml`에서 `android:minHeight="360dp"`를 제거했다. `CreatorChannelLiveFragment`에는 `emptyMinHeight`, `onCreatorChannelLiveViewportHeightChanged(minHeight)`, `applyEmptyMinHeight()`를 추가해 empty 상태에서 Activity가 전달한 최소 높이를 `layoutCreatorChannelLiveEmpty.minimumHeight`에 적용하도록 했다. - 2026-06-18: `CreatorChannelActivity`는 Live 탭 선택/Live content 변경/ViewPager height 갱신 시 `updateCreatorChannelLiveViewportHeight()`를 호출한다. 최소 높이는 `visibleTabViewportHeight = nestedScrollView.height - tabLayout.height`와 `scrollRangeRequiredHeight = nestedScrollView.height + stickyScrollY - headerContainer.height` 중 큰 값으로 계산해, empty page가 실제 탭 viewport를 채우면서 sticky scroll range도 만족하도록 했다. - 2026-06-18: 보정 후 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL in 1m 50s`로 통과했다. - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 44s`로 통과했다. - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 56s`로 통과했다. - [x] **Task RF8.3: Live owner CTA 표시 시 다시듣기 마지막 item cut-off 보정** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` - 작업: - Activity overlay CTA 표시 여부를 Live Fragment에 전달한다. - CTA가 표시될 때 마지막 다시듣기 item이 하단 CTA 영역에 가려지지 않도록 `RecyclerView` 하단 padding을 CTA 높이만큼 추가한다. - CTA 미표시 상태에서는 기존 기본 list bottom padding만 유지한다. - RED 테스트: - `CreatorChannelLiveFragmentLayoutTest`에 CTA visibility callback과 replay list bottom padding 계약을 추가한다. - `CreatorChannelActivitySourceTest`에 owner CTA visibility 변경 시 Live Fragment로 CTA 표시 여부를 전달하는 계약을 추가한다. - 검증 명령: - `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources` - `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` - 기대 결과: - 본인 채널 Live 탭에서 하단 CTA가 표시되어도 다시듣기 마지막 item 전체가 CTA 위로 스크롤되어 보인다. - 검증 기록: - 실제 원인 확인과 검증 결과를 이 Task 아래에 한국어로 누적 기록한다. - 2026-06-18: 원인 확인 결과, Phase 6 Follow-up에서 `라이브 시작하기` CTA를 Activity root overlay로 이동하면서 기존 Fragment 내부 `RecyclerView` 하단 padding 보정이 제거됐고, Activity의 `NestedScrollView` bottom padding에만 의존하게 됐다. Phase 8 Follow-up에서 item 간격과 empty 높이를 보정한 뒤에도 실제 마지막 다시듣기 item을 소유한 `RecyclerView`에는 CTA 높이만큼의 list bottom padding이 없어 owner CTA 아래에서 마지막 item이 잘릴 수 있음을 확인했다. - 2026-06-18: RED 확인으로 `CreatorChannelActivitySourceTest`와 `CreatorChannelLiveFragmentLayoutTest`에 owner CTA visibility를 Live Fragment로 전달하고, Live Fragment가 replay list bottom padding을 기본 32dp 또는 CTA 표시 시 132dp로 적용하는 계약을 추가했다. production 보정 전 실행한 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`는 `CreatorChannelActivitySourceTest` 2건, `CreatorChannelLiveFragmentLayoutTest` 1건이 실패해 RED를 확인했다. - 2026-06-18: `CreatorChannelActivity.updateLiveOwnerCtaVisibility()`가 `findLiveFragment()?.onCreatorChannelLiveOwnerCtaVisibilityChanged(shouldShowLiveOwnerCta)`를 호출하도록 변경하고, 바깥 `NestedScrollView` bottom padding 조정은 제거했다. `CreatorChannelLiveFragment`에는 `onCreatorChannelLiveOwnerCtaVisibilityChanged(isVisible)`를 추가해 `rvCreatorChannelLiveReplays` bottom padding을 CTA 미표시 32dp, CTA 표시 132dp로 갱신하도록 했다. - 2026-06-18: 보정 후 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL in 1m 11s`로 통과했다. - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 14s`로 통과했다. - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 17s`로 통과했다. - 2026-06-18: 추가 검증으로 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 14s`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 계속 출력됐다. ## Verification Log - 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개 실패가 재현됐다. - 2026-06-17: Phase 3 진행. 라이브 다시듣기 UI model/mapper와 mapper RED/GREEN 테스트를 추가했다. Fragment/layout/sort popup/tab 연결은 Phase 4 이후 범위라 변경하지 않았다. - 2026-06-17: Phase 3 RED 확인. production mapper 구현 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest"` 실행 시 `CreatorChannelLiveReplayStatus`, `toReplayUiModel`, `toLabelResId` 미구현으로 `:app:compileDebugUnitTestKotlin FAILED`가 발생했다. - 2026-06-17: Phase 3 GREEN 확인. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest"` PASS. - 2026-06-17: Phase 3 회귀 검증. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS. ktlint 실행 중 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 출력되었다. - 2026-06-17: Phase 3 리뷰 게이트 보정. `LATEST` label은 `최신순` 리소스 `screen_audio_content_sort_newest`로, `OWNED` label은 신규 다국어 리소스 `creator_channel_live_sort_owned`로 변경했다. 보정 후 `./gradlew :app:mergeDebugResources` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest"` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS. - 2026-06-17: Phase 4 진행. `CreatorChannelLiveFragment`, `CreatorChannelLiveReplayAdapter`, `fragment_creator_channel_live.xml`, `item_creator_channel_live_replay.xml`, 최소 배경 drawable/string, layout RED/GREEN 테스트를 추가했다. Sort popup/정렬 변경 동작과 owner CTA 실제 노출/inset/라이브 시작 연결은 Phase 5/6 범위로 남겼다. - 2026-06-17: Phase 4 RED 확인. production layout/code 추가 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 시 신규 layout/id/source 미존재로 `:app:compileDebugUnitTestKotlin FAILED`가 발생했다. - 2026-06-17: Phase 4 GREEN 확인. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` PASS. - 2026-06-17: Phase 4 회귀 검증. `./gradlew :app:mergeDebugResources` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS, `./gradlew :app:ktlintCheck` PASS. Gradle 병렬 실행 중 Kotlin daemon/incremental cache 경합 로그가 일부 출력되었으나 fallback/최종 결과는 `BUILD SUCCESSFUL`이었다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고와 Kotlin/Android deprecation 경고는 변경 범위와 무관하게 계속 출력된다. - 2026-06-17: Phase 4 리뷰 보정 진행. `NestedScrollView`가 스크롤을 소유하는 구조에서 Live pagination을 Fragment 내부 `RecyclerView.OnScrollListener`가 아니라 `CreatorChannelActivity`의 parent scroll bottom detection으로 전달하도록 보정했다. Live content bind 후 `onCreatorChannelLiveContentChanged()`로 `ViewPager2` 높이 재측정을 요청하고, 현재 라이브 시간은 홈 탭의 `formatCreatorChannelLiveDateTime()`을 재사용하도록 변경했다. Owned/Rented 다시듣기 item은 PRD 요구대로 play icon과 상태 텍스트를 함께 표시한다. - 2026-06-17: Phase 4 리뷰 보정 RED 확인. production 보정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`는 신규 Live fragment source 계약 미충족으로 FAIL, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`는 신규 NestedScrollView pagination/height callback 계약 미충족으로 FAIL했다. 같은 실행에서 기존 오디오 상세 이동 source assertion도 `startAudioContentDetail()` helper 추출 이후의 현재 코드와 맞지 않아 테스트 계약만 갱신했다. - 2026-06-17: Phase 4 리뷰 보정 GREEN 확인. 병렬 Gradle 실행 중 Kotlin daemon/incremental cache 경합으로 `Constants.class` 누락 및 fallback timeout이 발생했으나, daemon 정리 후 순차 재실행한 `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL`로 통과했다. 이어서 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` PASS. - 2026-06-17: Phase 4 리뷰 보정 최종 검증. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS, `./gradlew :app:mergeDebugResources` PASS, `./gradlew :app:ktlintCheck` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` PASS. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 계속 출력되었으나 실패 원인은 아니었다. - 2026-06-17: Phase 4 리뷰 게이트에서 `CreatorChannelPagerAdapter`가 아직 Live 탭을 `CreatorChannelPlaceholderFragment`로 생성해 Live 보정 코드가 실제 사용자 경로에서 실행되지 않는다는 차단 이슈가 발견됐다. 최소 보정으로 `CreatorChannelTab.Live -> CreatorChannelLiveFragment.newInstance(creatorId)` 분기를 추가하고, 기존 placeholder-only source test를 홈/라이브 실제 Fragment + 후속 탭 placeholder 계약으로 갱신했다. - 2026-06-17: Live pager 연결 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 계속 출력되었다. - 2026-06-17: 리뷰 지적 2건 보정. Live 탭 API는 Live 탭 선택 시점 lazy load로 변경했고, Empty/Error는 Live page root와 Activity page minimumHeight 보정으로 탭 viewport 중앙 정렬을 보장했다. 최종 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` PASS, `./gradlew :app:mergeDebugResources` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS. 초기 병렬 RED 확인 중 Kotlin incremental cache 경합 로그가 출력됐으나 daemon 정리 후 순차 검증은 모두 통과했다. - 2026-06-17: 추가 리뷰 지적 2건 보정. `ViewPager2.offscreenPageLimit`을 복구해 Home 탭의 `HomeActionDelegate` 기반 상단 액션/refresh 경로를 유지하고, Live load-more 성공 시 첫 페이지의 current live/count/sort metadata를 보존하도록 변경했다. 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLivePaginationTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 계속 출력되었다. - 2026-06-17: Phase 5 코드 리뷰 및 검증. Figma `290:9041` 컨텍스트 메뉴와 스크린샷을 재확인해 `gray_900` 배경, `gray_700` stroke, `radius_14`, 선택 row `gray_800`, 16sp medium, 12/8 padding 기준이 구현에 반영됐음을 확인했다. PRD/계획의 서버 정렬 계약은 `ContentSort` 5개 옵션이므로 Figma의 `추천순` row는 이번 범위에서 제외했다. - 2026-06-17: Phase 5 코드 리뷰에서 수정 필요 결함은 발견하지 못했다. `CreatorChannelLiveSortPopup`은 외부 터치 dismiss, 같은 정렬 재선택 dismiss-only, 새 정렬 선택 시 `viewModel.changeSort(sort)` 전달, 우측 화면 밖 보정, `onDestroyView()` dismiss 경로를 갖는다. - 2026-06-17: Phase 5 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` PASS, `./gradlew :app:mergeDebugResources` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS를 확인했다. - 2026-06-17: 추가 상위 회귀 검증 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` 최초 실행은 drawable 3개 추가 후 resource id 캐시가 맞지 않아 `CreatorChannelHomeMapperTest`, `CreatorChannelTitleBarStateTest`의 drawable id assertion이 3씩 밀려 실패했다. 해당 클래스들을 `--rerun-tasks`로 강제 재컴파일하자 통과했고, 이후 동일 채널 전체 회귀 명령을 순차 재실행해 `BUILD SUCCESSFUL`로 통과했다. 병렬 `--rerun-tasks` 중 Kotlin incremental cache 경합 로그가 출력됐으나 최종 순차 검증은 통과했다. - 2026-06-18: Phase 8 자동 검증 실행. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck` 모두 `BUILD SUCCESSFUL`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - 2026-06-18: Phase 8 수동 검증 시도. 연결 단말과 설치 패키지는 확인했으나 `CreatorChannelActivity` 직접 실행은 exported 제한으로 차단됐고, 런처 실행은 TedPermissionActivity 권한 화면까지만 확인했다. Live 탭 실제 화면/Figma 대조는 후속 로그인/권한/서버 데이터가 준비된 단말 시나리오에서 계속 확인해야 한다. - 2026-06-18: Phase 6 Follow-up 진행. `라이브 시작하기` CTA를 Fragment 내부 스크롤 컨텐츠에서 Activity root overlay로 이동해 Figma `665:19371` 기준 100dp black 하단 고정 영역과 14dp inset cyan capsule 버튼 구조로 보정했다. RED/GREEN 확인으로 `CreatorChannelLiveFragmentLayoutTest` 실패 후 통과, `mergeDebugResources` PASS, `compileDebugKotlin` PASS, `ktlintCheck` PASS를 확인했다. 병렬 Gradle 실행 중 Kotlin incremental cache 경합은 daemon stop 후 순차 재실행으로 해소했다. - 2026-06-18: Phase 6 Follow-up RF6.2 진행. `BaseActivity` root bottom padding과 Activity 하단 UI navigation inset의 중복 적용을 제거해 실제 기기에서 preview보다 위로 뜨는 문제를 보정했다. Live 탭 empty/error 짧은 상태는 `ViewPager2` page minimum height 강제와 `fragment_creator_channel_live.xml` root `match_parent`를 제거해 불필요한 긴 스크롤이 생기지 않도록 했다. targeted RED/GREEN 테스트, `mergeDebugResources`, `compileDebugKotlin`, `ktlintCheck`가 최종 통과했다. - 2026-06-18: Live API 기본 `size`를 20으로 변경한 뒤 연결 지점을 점검했다. `CreatorChannelLiveViewModel.DEFAULT_PAGE_SIZE`를 참조하는 테스트 bytecode가 기존 `const val` 값 10을 inline해 mock stub이 production 호출 `size=20`과 불일치하는 문제를 확인했고, 기본값을 runtime `val`로 바꿔 stale inline 재발 가능성을 낮췄다. 문서의 초기 로드 기록도 `page=0`, `size=20`, `sort=LATEST`로 보정했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLivePaginationTest"` 실행 결과 `BUILD SUCCESSFUL in 27s`로 통과했다. - 2026-06-18: Live API 기본 `size=20` 보정 후 `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 1s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다.