# 크리에이터 채널 오디오 탭 구현 계획/TASK > **For agentic workers:** 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다. **Goal:** `GET /api/v2/creator-channels/{creatorId}/audio` 응답을 기반으로 크리에이터 채널의 `오디오` 탭에 테마 필터, 정렬, 소장률, 오디오 콘텐츠 목록, empty 상태, 본인 채널 전용 하단 `오디오 올리기` CTA와 pagination을 표시한다. **Architecture:** 기존 `CreatorChannelActivity`의 `ViewPager2`/`CreatorChannelPagerAdapter` 구조를 유지하고, `CreatorChannelTab.Audio`의 placeholder를 신규 `CreatorChannelAudioFragment`로 교체한다. 오디오 탭 전용 Fragment/ViewModel/DTO/mapper는 `kr.co.vividnext.sodalive.v2.creator.channel.audio` 하위에 두되, API/Repository는 기존 채널 공통 `CreatorChannelApi`/`CreatorChannelRepository`에 endpoint만 추가한다. 테마 tab-bar는 v2 공통 `CapsuleTabBarView`를 사용하고, 라이브 탭에서 검증된 replay item layout/adapter/model/status, sort popup, 하단 owner CTA, scroll-to-bottom hook, 상세 이동 경로는 크리에이터 채널 공통 이름으로 rename/move해 재사용한다. **Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test. --- ## 전제와 성공 기준 - PRD: `docs/20260619_크리에이터_채널_오디오_탭/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: - `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` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.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/live/CreatorChannelLiveViewModel.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/model/CreatorChannelLiveMappers.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveSortPopup.kt` - Figma: - 전체: `290:9015` - 소장률: `290:9029` - 콘텐츠 item: `290:9026` - 전체 empty: `290:8965` - 본인 채널 하단 CTA: `665:19008` - API endpoint는 `GET /api/v2/creator-channels/{creatorId}/audio`이다. - 첫 페이지 `page`는 `0`, 기본 `size`는 `20`, 기본 `sort`는 `ContentSort.LATEST`이다. - 최초 조회와 전체 테마 선택 상태에서는 `themeId` query parameter를 보내지 않는다. - `CreatorChannelAudioTabResponse.themes`에는 `전체`가 포함되지 않는다. - 클라이언트가 `themes` 맨 앞에 `전체` synthetic tab을 추가한다. - `CreatorChannelAudioTabResponse.themeId == null`이면 `전체` tab 선택 상태로 표시한다. - `ContentSort`와 `CreatorChannelAudioContentResponse`는 기존 타입을 재사용한다. - theme tab-bar는 v2 공통 `CapsuleTabBarView`를 재사용하고 별도 theme adapter/item layout을 만들지 않는다. - original/first/point/free tag 표현과 순서는 v2 공통 `AudioContentTag`를 재사용한다. - `duration == null`인 오디오 콘텐츠는 서버에서 내려오지 않는 계약이며, 예외적으로 내려오면 해당 item을 숨긴다. - `seriesName`이 있으면 `duration • seriesName`, 없으면 `duration`만 표시한다. - 라이브 다시듣기 item과 오디오 콘텐츠 item은 동일한 UI를 사용하므로, 별도 중복 layout/adapter를 만들지 않고 기존 `item_creator_channel_live_replay.xml`, `CreatorChannelLiveReplayAdapter`, live replay UI model/status를 creator channel 공통 오디오 콘텐츠 item으로 rename/move해 재사용한다. - 기존 `CreatorChannelLiveSortPopup`과 `view_creator_channel_live_sort_menu.xml`은 creator channel 공통 sort popup 이름으로 rename/move해 라이브/오디오가 함께 사용한다. - 기존 live owner CTA container는 creator channel 공통 owner CTA로 rename하고 현재 탭에 따라 label/icon/click action만 바꿔 재사용한다. - 기존 live scroll-to-bottom hook은 creator channel 공통 scroll hook으로 확장해 현재 탭이 Live/Audio일 때 각 Fragment의 load-more entry를 호출한다. - 내 채널이 아니고 `themeId == null`인 전체 테마 상태에서만 소장률 UI를 표시한다. - 소장률 UI는 `purchasedAudioContentRate` percent 값, `purchasedAudioContentCount`, `paidAudioContentCount`를 사용한다. - 내 채널이면 소장률 UI를 숨기고 하단 고정 `오디오 올리기` CTA를 표시한다. - 오디오 콘텐츠가 0개이거나 표시 가능한 콘텐츠가 0개이면 테마 tab-bar, Sort-bar, 소장률, 목록을 숨기고 `크리에이터가 오디오를 준비 중입니다.\n기대해 주세요!`만 표시한다. - 본인 채널 empty 상태에서도 empty 문구는 동일하며, CTA는 본인 채널 정책에 따라 표시한다. - item 클릭은 기존 `CreatorChannelActivity.startAudioContentDetail(audioContentId)` 경로를 재사용한다. - `오디오 올리기` CTA 클릭은 기존 `onOwnerFabAudioClicked()`의 `AudioContentUploadActivity` 진입 경로를 재사용한다. - 구현 완료 후 최소 다음 명령을 실행한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Audio*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - `git diff --check` --- ## Figma 참조 필요 Phase - Phase 1: 제한 참조 - 기존 코드 경계, endpoint, 리소스 존재 여부 확인이 중심이며 Figma는 PRD 기준만 확인한다. - Phase 2: Figma 참조 불필요 - API/DTO/Repository/ViewModel 상태 모델은 서버 계약과 기존 라이브 탭 패턴을 따른다. - Phase 3: 제한 참조 - mapper는 PRD와 Figma item variant의 상태 표시를 함께 확인한다. - Phase 4: 필수 참조 - 테마 tab-bar, Sort-bar, 소장률 카드, empty, CTA는 Figma `290:9015`, `290:9029`, `290:8965`, `665:19008`을 기준으로 구현한다. - Phase 5: 필수 참조 - 오디오 콘텐츠 item은 Figma `290:9026`과 공통화 전 기존 `item_creator_channel_live_replay.xml` 구조를 대조한다. - Phase 6: 제한 참조 - 탭 연결, pagination, navigation, owner CTA 동작은 기존 코드 패턴 중심으로 검증한다. - Phase 7: 필수 참조 - 최종 수동 화면 검증은 PRD의 모든 Figma 노드와 실제 화면을 대조한다. --- ## 파일 구조 - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt` - `CreatorChannelTab.Audio`를 신규 `CreatorChannelAudioFragment`로 연결한다. - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - 기존 live 전용 scroll-to-bottom hook과 owner CTA 표시/클릭을 creator channel 공통 처리로 확장하고, 오디오 콘텐츠 클릭 callback을 연결한다. - 수정: `app/src/main/res/layout/activity_creator_channel.xml` - 기존 `layout_creator_channel_live_owner_cta`/`btn_creator_channel_live_owner_cta`를 creator channel 공통 owner CTA 이름으로 rename하고 현재 탭에 맞게 label/icon을 바꿔 재사용한다. - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt` - 오디오 탭 endpoint를 추가한다. - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt` - 오디오 탭 repository method를 추가한다. - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/data/CreatorChannelAudioTabResponse.kt` - `CreatorChannelAudioTabResponse`, `CreatorChannelAudioThemeResponse`를 정의한다. - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModel.kt` - 최초 조회, 정렬 변경, 테마 변경, retry, pagination, loading/error/empty/content 상태를 관리한다. - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/model/CreatorChannelAudioUiModels.kt` - theme tab, 소장률, 화면 상태 UI model을 정의한다. item model/status는 공통 오디오 콘텐츠 item model/status를 재사용한다. - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/model/CreatorChannelAudioMappers.kt` - DTO를 UI model로 변환하고 `전체` synthetic theme, series secondary text, 소장률 표시 여부, item status를 결정한다. - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt` - 오디오 탭 UI, 공통 오디오 콘텐츠 adapter, 공통 sort popup, `CapsuleTabBarView` theme click, pagination, host callback 연결을 담당한다. - move/rename: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt` → `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelAudioContentAdapter.kt` - 라이브 다시듣기와 오디오 콘텐츠 목록 RecyclerView adapter를 함께 담당하며, 공통 오디오 콘텐츠 item layout을 inflate한다. - move/rename: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveSortPopup.kt` → `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelSortPopup.kt` - 기존 라이브 sort popup 구현을 크리에이터 채널 공통 sort popup으로 사용한다. - move/rename: `ContentSort.toLabelResId()`/`toSortOptionUiModel()`과 sort option UI model - `creator/channel/live/model`에서 creator channel 공통 model/ui 위치로 이동해 라이브/오디오가 함께 사용한다. - 생성: `app/src/main/res/layout/fragment_creator_channel_audio.xml` - `@layout/view_capsule_tab_bar` 기반 theme tab-bar, Sort-bar, 소장률 카드, RecyclerView, empty/error/retry 영역을 포함한다. - rename: `app/src/main/res/layout/item_creator_channel_live_replay.xml` → `app/src/main/res/layout/item_creator_channel_audio_content.xml` - 크리에이터 채널 공통 오디오 콘텐츠 item layout이다. 기존 라이브 다시듣기 item과 동일 UI를 유지하고 id prefix는 `creator_channel_audio_content`로 둔다. - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt` - 공통 adapter로 move/rename하면서 live Fragment 참조를 함께 갱신한다. - rename: `app/src/main/res/layout/view_creator_channel_live_sort_menu.xml` → `app/src/main/res/layout/view_creator_channel_sort_menu.xml` - 기존 live sort popup layout을 creator channel 공통 sort popup layout으로 사용한다. - 수정: `app/src/main/res/values/strings.xml` - 오디오 탭 empty/error/retry/CTA/소장률 문구를 추가한다. - 수정: `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` - `CreatorChannelAudioViewModel` binding을 추가한다. - 테스트 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioMapperTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModelTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioPaginationTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragmentLayoutTest.kt` - 테스트 수정: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` --- ### Phase 1: 기존 구조 확인과 작업 경계 고정 - [x] **Task 1.1: 라이브 탭 재사용 경계 확인** - 확인: - `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/CreatorChannelLiveViewModel.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/model/CreatorChannelLiveMappers.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveSortPopup.kt` - 작업: - Sort popup을 공용화할지, 오디오 전용으로 얇게 복제할지 결정한다. - replay item adapter의 상태 표시 정책을 오디오 item에 그대로 사용할 수 있는지 확인한다. - 라이브 탭 파일은 오디오 탭과 동일하게 쓰는 item/sort/CTA/scroll-to-bottom 영역만 creator channel 공통 이름으로 rename/move한다. - 검증: - 라이브 탭 기존 테스트가 공용화 후에도 유지되어야 할 검증 목록을 기록한다. - 검증 기록: - 2026-06-19 확인 완료. `CreatorChannelLiveFragment`는 `CreatorChannelLiveSortPopup`, `CreatorChannelLiveReplayAdapter`, `toReplayUiModel()`을 직접 사용하고, sort label은 `ContentSort.toLabelResId()` 확장으로 분리되어 있다. - `CreatorChannelLiveSortPopup`은 class/package명과 `view_creator_channel_live_sort_menu`, `layout_creator_channel_live_sort_options`, `tv_creator_channel_live_sort_option_sample` id가 모두 live 전용이므로 Phase 3에서 `creator/channel/ui/CreatorChannelSortPopup.kt`와 공용 layout으로 이동하는 방식이 오디오 참조에 적합하다. - replay item 상태 정책은 mapper의 `Owned > Rented > Play(무료) > Price` 우선순위와 adapter의 badge/tag/status bind를 오디오 item에 동일 적용할 수 있다. 후속 재검토 결과 item UI가 동일하므로 live replay adapter/model/status/layout을 creator channel 공통 오디오 콘텐츠 item 이름으로 rename/move해 라이브와 오디오가 함께 사용한다. - 공용화 후 유지해야 할 회귀 검증은 `CreatorChannelLiveFragmentLayoutTest`, `CreatorChannelActivitySourceTest`의 live sort/pagination/owner CTA 관련 source 검증, `CreatorChannelPagerAdapterTest`이다. - [x] **Task 1.2: 오디오 업로드 CTA와 기존 진입점 확인** - 확인: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/res/layout/activity_creator_channel.xml` - `app/src/main/java/kr/co/vividnext/sodalive/audio_content/upload/AudioContentUploadActivity.kt` - 작업: - 기존 `onOwnerFabAudioClicked()`가 `AudioContentUploadActivity`로 이동하는지 확인한다. - 하단 CTA 클릭도 같은 method를 호출하도록 연결 계획을 고정한다. - `ic_new_upload_audio` drawable이 존재하는지 확인한다. - 검증: - `rg -n "onOwnerFabAudioClicked|ic_new_upload_audio|AudioContentUploadActivity" app/src/main/java app/src/main/res` - 기대 결과: 기존 오디오 업로드 진입점과 icon 리소스 참조가 확인된다. - 검증 기록: - 2026-06-19 `rg -n "onOwnerFabAudioClicked|ic_new_upload_audio|AudioContentUploadActivity|CreatorChannelTab\.Audio|CreatorChannelPlaceholderFragment|CreatorChannelPagerAdapterTest|CreatorChannelActivitySourceTest" app/src/main/java app/src/main/res app/src/test/java` 실행으로 기존 진입점과 icon 참조를 확인했다. - `CreatorChannelActivity`는 `binding.ownerFabAudioButton.setOnClickListener { onOwnerFabAudioClicked() }`를 가지고, `onOwnerFabAudioClicked()`에서 `collapseOwnerFab(animate = false)` 후 `AudioContentUploadActivity`를 `startActivity`로 연다. - `activity_creator_channel.xml`에 `owner_fab_audio_button`과 `@drawable/ic_new_upload_audio`가 이미 존재한다. 오디오 탭 하단 CTA는 새 업로드 경로를 만들지 말고 기존 `onOwnerFabAudioClicked()`를 호출하도록 연결한다. - 기존 source test는 `AudioContentUploadActivity`, `onOwnerFabAudioClicked()`, `startActivity(Intent(this, AudioContentUploadActivity::class.java))`, `@drawable/ic_new_upload_audio`를 검증하고 있으므로 Phase 6에서 하단 CTA 추가 시 이 검증을 유지/확장한다. - [x] **Task 1.3: 오디오 탭 placeholder 연결 지점 확인** - 확인: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt` - 작업: - `CreatorChannelTab.Audio`가 현재 `CreatorChannelPlaceholderFragment`로 연결되는지 확인한다. - 신규 `CreatorChannelAudioFragment.newInstance(creatorId)`로 교체할 수 있는지 확인한다. - 검증: - `CreatorChannelPagerAdapterTest`에 오디오 탭 연결 테스트를 추가할 준비가 되었는지 기록한다. - 검증 기록: - 2026-06-19 확인 완료. `CreatorChannelPagerAdapter`는 현재 `CreatorChannelTab.Home`과 `CreatorChannelTab.Live`만 실제 Fragment로 연결하고 `else -> CreatorChannelPlaceholderFragment.newInstance(tab)`로 Audio 포함 후속 탭을 placeholder 처리한다. - `CreatorChannelActivitySourceTest`는 현재 `CreatorChannelTab.Audio ->`가 source에 없어야 한다고 검증하고, `CreatorChannelPagerAdapterTest`는 Home/Live 외 탭이 placeholder임을 검증한다. - Phase 6에서 `CreatorChannelTab.Audio -> CreatorChannelAudioFragment.newInstance(creatorId)`를 추가하면서 `CreatorChannelPagerAdapterTest`는 Audio 실제 Fragment 검증으로 변경하고, source test의 기존 placeholder/no-op 기대값도 오디오 연결 정책에 맞게 갱신한다. --- ### Phase 2: API/DTO/Repository/ViewModel 계약 추가 - [x] **Task 2.1: 오디오 탭 DTO 추가** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/data/CreatorChannelAudioTabResponse.kt` - 작업: - `@Keep`, `@SerializedName` 기반으로 `CreatorChannelAudioTabResponse`, `CreatorChannelAudioThemeResponse`를 추가한다. - `ContentSort`는 기존 `kr.co.vividnext.sodalive.v2.common.data.ContentSort`를 import해 사용한다. - `CreatorChannelAudioContentResponse`는 기존 `creator.channel.data` 타입을 import해 사용한다. - 검증 명령: - `./gradlew :app:compileDebugKotlin` - 기대 결과: - 신규 DTO 추가 후 컴파일이 PASS한다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioTabResponse`, `CreatorChannelAudioThemeResponse`를 `audio/data` 하위에 추가했다. `ContentSort`와 기존 `CreatorChannelAudioContentResponse` 타입을 재사용한다. - 2026-06-19 `./gradlew :app:compileDebugKotlin` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 리뷰 후속 수정으로 PRD 계약의 `themeName` 필드가 ViewModel theme title 매핑에서도 사용되도록 연결부를 점검한다. - [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}/audio")` endpoint를 추가한다. - query parameter `sort`, `page`, `size`, `themeId`를 전달한다. - `themeId`는 nullable `Long?`로 받고, `null`이면 Retrofit query가 전송되지 않도록 한다. - Repository method는 `getAudio(creatorId, page, size, sort, themeId, token)` 형태로 둔다. - 검증 명령: - `./gradlew :app:compileDebugKotlin` - 기대 결과: - API/Repository 추가 후 기존 Koin graph와 충돌 없이 컴파일된다. - 검증 기록: - 2026-06-19 `CreatorChannelApi.getAudio()`에 `GET /api/v2/creator-channels/{creatorId}/audio` endpoint를 추가하고, `page`, `size`, `sort`, nullable `themeId`, `Authorization` header를 전달하도록 했다. - 2026-06-19 `CreatorChannelRepository.getAudio(creatorId, page, size, sort, themeId, token)` method를 추가했다. `themeId`는 nullable 값 그대로 Retrofit query로 전달한다. - 2026-06-19 `./gradlew :app:compileDebugKotlin` 실행 결과 BUILD SUCCESSFUL. - [x] **Task 2.3: ViewModel RED 테스트 작성** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModelTest.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioPaginationTest.kt` - 테스트 케이스: - 최초 로딩이 `page=0`, `size=20`, `sort=LATEST`, `themeId=null`로 호출된다. - 응답 `themes` 앞에 `전체` UI tab이 추가되고, 응답 `themeId == null`이면 `전체`가 선택된다. - 테마 선택 시 `page=0`, 현재 sort, 선택한 `themeId`로 재조회된다. - `전체` 선택 시 `themeId=null`로 재조회되고 query parameter가 전송되지 않는 repository 계약을 사용한다. - 정렬 변경 시 `page=0`, 선택된 sort, 현재 themeId로 재조회된다. - 같은 정렬 또는 같은 테마를 다시 선택하면 API를 재호출하지 않는다. - `hasNext == true`일 때 다음 페이지는 마지막 응답의 `page + 1`로 요청한다. - loading 중 중복 load-more 요청은 무시된다. - `duration == null` item은 목록에서 제외된다. - 표시 가능한 item이 0개이면 `Empty` 상태가 된다. - 내 채널이 아니고 전체 테마일 때만 소장률 UI model이 생성된다. - 내 채널 또는 특정 themeId 선택 상태에서는 소장률 UI model이 `null`이다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModelTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioPaginationTest"` - 기대 결과: - production 구현 전 `CreatorChannelAudioViewModel` 미구현으로 RED 실패한다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioViewModelTest`, `CreatorChannelAudioPaginationTest`를 추가해 최초 로드, synthetic `전체` tab, 테마/정렬 변경, no-op, `duration == null` item 제외, empty, 소장률 표시 조건, pagination append/중복 방지/error consume/stale response 무시 계약을 고정했다. - 2026-06-19 production ViewModel 구현 전 RED 확인을 위해 두 테스트 명령을 병렬 실행했으나 Kotlin daemon/incremental compile 충돌로 정상적인 단일 테스트 실패 로그 대신 compile failure가 발생했다. 이후 daemon을 정리하고 production 구현 후 동일 테스트 명령을 단일 실행해 GREEN을 확인했다. - 2026-06-19 리뷰 후속 수정으로 `audioContentCount == 0`이면 표시 가능한 item이 있어도 Empty 상태가 되는 테스트를 추가한다. - [x] **Task 2.4: `CreatorChannelAudioViewModel` 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModel.kt` - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 작업: - `DEFAULT_PAGE_SIZE = 20`, `FIRST_PAGE = 0`, 기본 `selectedSort = ContentSort.LATEST`, 기본 `selectedThemeId = null`로 둔다. - `loadAudio(creatorId: Long, isOwner: Boolean)`는 같은 `creatorId`와 기존 state가 있으면 중복 호출하지 않는다. - `changeSort(sort: ContentSort)`는 같은 sort면 no-op, 다르면 첫 페이지를 재조회한다. - `changeTheme(themeId: Long?)`는 같은 themeId면 no-op, 다르면 첫 페이지를 재조회한다. - `retryAudio()`는 현재 sort/theme 상태로 첫 페이지를 재조회한다. - `loadMore()`는 `hasNext == true`, `isLoadingMore == false`일 때만 실행한다. - request generation 값을 두어 오래된 응답이 최신 상태를 덮어쓰지 않도록 한다. - `duration == null` item은 상태 생성 전에 제외한다. - 첫 페이지 성공 후 표시 가능한 item이 0개이면 `Empty` 상태로 둔다. - 첫 페이지 성공 후 `audioContentCount == 0`이면 표시 가능한 item 존재 여부와 관계없이 `Empty` 상태로 둔다. - pagination 성공 시 기존 item 뒤에 append한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModelTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioPaginationTest"` - 기대 결과: - ViewModel 테스트가 PASS한다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioViewModel`을 추가해 최초 로드, sort/theme 변경, retry, pagination, request generation stale response 방지, `duration == null` item 제외, empty/content/error 상태를 구현했다. - 2026-06-19 `AppDI.kt`에 `CreatorChannelAudioViewModel` Koin binding을 추가했다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModelTest"` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioPaginationTest"` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 리뷰 후속 수정으로 theme title 매핑은 `theme.themeName`을 사용하고, Empty 조건은 `audioContents.isEmpty() || audioContentCount == 0`으로 보강한다. --- ### Phase 3: Mapper/UI model 정책 구현 - [x] **Task 3.1: 오디오 UI model과 mapper RED 테스트 작성** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioMapperTest.kt` - 테스트 케이스: - `themes` 응답 앞에 `전체` tab이 추가된다. - `themeId == null`이면 `전체` tab이 selected 상태다. - 응답 `themeId`가 특정 id이면 해당 서버 theme tab만 selected 상태다. - 응답 `themeId`가 서버 `themes`에 없으면 UI 무선택 상태를 피하기 위해 `전체` tab selected 상태로 fallback하고, effective selected themeId를 `null`로 정규화한다. - `seriesName`이 있으면 secondary text가 `duration • seriesName`이다. - `seriesName == null` 또는 blank이면 secondary text가 `duration`이다. - `duration == null`이면 item mapper 결과에서 제외된다. - `isOwned == true`와 `isRented == true`가 동시에 내려오면 `Owned` 상태가 우선이다. - `price == 0`이면 play 상태, `price > 0`이고 미보유면 price 상태다. - `isAdult`, `isPointAvailable`, `isFirstContent`, `isOriginalSeries` tag/badge가 기존 라이브 item 정책과 동일하게 매핑된다. - 소장률은 내 채널이 아니고 전체 테마일 때만 생성된다. - 소장률 count는 `purchasedAudioContentCount/paidAudioContentCount`, percent는 `purchasedAudioContentRate`를 사용한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest"` - 기대 결과: - mapper 미구현 상태에서 RED 실패한다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioMapperTest`를 추가해 synthetic `전체` tab, 선택 theme, `duration • seriesName`, blank/null series, `duration == null` 제외, 상태 우선순위, tag/badge, 소장률 생성 조건을 고정했다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest"` 실행 결과 `audio/model` 패키지와 mapper 심볼 미존재로 `compileDebugUnitTestKotlin` 실패를 확인했다. - 2026-06-19 리뷰 후속으로 응답 `themeId`가 서버 `themes`에 없는 경우 `전체` selected fallback 테스트와 effective selected themeId `null` 정규화 테스트를 추가했다. 구현 전 `effectiveSelectedThemeId` 심볼 미존재로 RED 실패를 확인했다. - [x] **Task 3.2: 오디오 UI model과 mapper 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/model/CreatorChannelAudioUiModels.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/model/CreatorChannelAudioMappers.kt` - 작업: - `CreatorChannelAudioThemeUiModel(themeId: Long?, title: String, isSelected: Boolean)`를 정의한다. - `themeId == null`, title `전체`인 synthetic tab을 mapper에서 맨 앞에 추가한다. - `CreatorChannelAudioRateUiModel(ratePercent: Double, purchasedCount: Int, paidCount: Int)`를 정의한다. - `CreatorChannelAudioContentUiModel`은 라이브 replay UI model과 같은 필드에 `secondaryText`만 오디오 정책으로 둔다. - status sealed interface는 라이브 replay status와 동일한 상태를 제공한다. - 정렬 label mapping은 기존 `ContentSort.toLabelResId()`를 재사용하거나 공용 위치로 이동한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest"` - 기대 결과: - mapper 테스트가 PASS한다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioUiModels.kt`와 `CreatorChannelAudioMappers.kt`를 추가하고, `CreatorChannelAudioViewModel`의 임시 theme/rate/content 매핑을 audio model mapper로 이동했다. - 2026-06-19 `CreatorChannelAudioContentUiModel`은 live replay item과 동일한 상태/tag 정책을 따르되, secondary text는 PRD 계약대로 `duration • seriesName` 또는 `duration`으로 매핑한다. - 2026-06-19 리뷰 후속으로 선택된 `themeId`가 응답 `themes`에 없을 때 UI 무선택 상태를 피하기 위해 synthetic `전체` tab 선택 상태로 fallback하도록 결정하고 mapper에 반영했다. - 2026-06-19 fallback 정책은 표시 상태뿐 아니라 내부 선택 상태에도 적용한다. `effectiveSelectedThemeId()`가 없는 themeId를 `null`로 정규화하고, `CreatorChannelAudioViewModel.Content.selectedThemeId`, 소장률 표시 조건, 정렬 변경 후속 요청이 모두 정규화된 `null` 기준을 사용하도록 보강했다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest"` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModelTest"` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioPaginationTest"` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 후속 리뷰 지적으로 선택 테마 요청 뒤 서버가 없는 `themeId`를 응답하면 ViewModel 내부 `selectedThemeId`가 이전 선택값으로 남는 문제가 확인됐다. `CreatorChannelAudioViewModelTest`에 정렬 후속 요청이 정규화된 `null` themeId를 사용하는 회귀 테스트를 추가하고, 첫 페이지 성공 응답 처리 시 private `selectedThemeId`도 `effectiveSelectedThemeId()`로 갱신하도록 수정했다. - [x] **Task 3.3: sort popup 공통 rename/move 결정 반영** - move/rename: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveSortPopup.kt` → `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelSortPopup.kt` - `app/src/main/res/layout/view_creator_channel_live_sort_menu.xml` → `app/src/main/res/layout/view_creator_channel_sort_menu.xml` - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - 작업: - 라이브 전용 sort popup을 `CreatorChannelSortPopup`으로 rename/move해 라이브/오디오가 함께 사용한다. - popup layout id prefix도 live 전용 이름에서 creator channel sort 공통 이름으로 변경한다. - `ContentSort.toLabelResId()`와 sort option UI model/mapping이 live 전용 파일에 있으면 creator channel 공통 model/ui 위치로 이동한다. - 기존 라이브 탭 UI와 테스트 기대값을 깨지 않는다. - 검증 명령: - `./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.CreatorChannelAudioMapperTest"` - 기대 결과: - 라이브 탭 정렬 테스트와 오디오 mapper 테스트가 모두 PASS한다. - 검증 기록: - 2026-06-19 탐색 결과 `CreatorChannelLiveSortPopup`과 `view_creator_channel_live_sort_menu.xml`은 class/layout/id/test가 live 전용 이름에 강하게 묶여 있고, Phase 3 mapper 구현에는 즉시 필요하지 않음을 확인했다. - 2026-06-19 Phase 3에서는 sort popup 공통 rename/move를 실제 코드에 적용하지 않았다. 후속 재사용 검토 결과 오디오 Sort-bar에서 동일 UI/동작을 사용하므로 Phase 5에서 `CreatorChannelSortPopup` 공통 이름으로 rename/move해 적용한다. - 2026-06-19 Task 3.3 재작업 요청에 따라 완료 체크를 해제했다. 실제 sort popup 공통 rename/move를 적용하고 리뷰/검증 완료 후 다시 체크한다. - 2026-06-19 RED 확인: `CreatorChannelLiveFragmentLayoutTest`, `CreatorChannelLiveMapperTest`를 공통 sort 이름 기준으로 먼저 갱신한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`를 실행했고, `view_creator_channel_sort_menu`, `layout_creator_channel_sort_options`, 공통 `toLabelResId()` 미존재로 `:app:compileDebugUnitTestKotlin` 실패를 확인했다. - 2026-06-19 `CreatorChannelLiveSortPopup`을 `CreatorChannelSortPopup`으로 `creator/channel/ui`에 move/rename하고, `view_creator_channel_sort_menu.xml`, `bg_creator_channel_sort_popup.xml`, `bg_creator_channel_sort_selected.xml`, 공통 sort option UI model/mapping을 추가했다. `CreatorChannelLiveFragment`는 공통 popup과 `creator/channel/model/toLabelResId()`를 사용하도록 갱신했다. - 2026-06-19 GREEN 확인: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest"` 실행 결과 모두 BUILD SUCCESSFUL. - 2026-06-19 stale 참조 확인: `rg -n "CreatorChannelLiveSortPopup|view_creator_channel_live_sort_menu|layout_creator_channel_live_sort_options|tv_creator_channel_live_sort_option_sample|bg_creator_channel_live_sort" app/src/main app/src/test` 실행 결과 app production/test 코드에 잔여 참조가 없음을 확인했다. - 2026-06-19 리뷰 게이트에서 지적된 문서 체크 미반영 blocker를 수정하고, Task 3.3을 다시 완료 체크했다. --- ### Phase 4: 오디오 탭 레이아웃과 Fragment 구성 - [x] **Task 4.1: Fragment layout RED 테스트 작성** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragmentLayoutTest.kt` - 테스트 케이스: - `fragment_creator_channel_audio.xml`에 `CapsuleTabBarView`, Sort-bar, 소장률 card, RecyclerView, empty container, error message, retry button이 존재한다. - empty message text는 `@string/creator_channel_audio_empty_message`를 사용한다. - Sort-bar는 `전체`, count, sort label, `ic_new_sort` icon을 가진다. - 소장률 card는 percent 문구, count 문구, progress bar track/fill view를 가진다. - 공통 item layout `item_creator_channel_audio_content.xml`은 88dp thumbnail, title, secondary text, play/price/status 영역을 가진다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - 기대 결과: - layout 미구현 상태에서 RED 실패한다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioFragmentLayoutTest`를 먼저 추가하고 production 변경 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"`를 실행했다. `fragment_creator_channel_audio`, 오디오 section id, `item_creator_channel_audio_content` 리소스가 없어 `:app:compileDebugUnitTestKotlin`에서 RED 실패를 확인했다. - [x] **Task 4.2: `fragment_creator_channel_audio.xml` 작성** - 생성: - `app/src/main/res/layout/fragment_creator_channel_audio.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 `290:9015` 기준으로 black background, `@layout/view_capsule_tab_bar`, Sort-bar, 소장률 card, list, empty/error 영역을 배치한다. - 소장률 card는 `themeId == null`/내 채널 여부에 따라 Fragment에서 visibility를 제어할 수 있도록 별도 container id를 둔다. - empty 상태에서 콘텐츠 UI를 모두 숨길 수 있도록 각 section id를 분리한다. - `creator_channel_audio_empty_message`, `creator_channel_audio_error_message`, `creator_channel_audio_upload_button`, `creator_channel_audio_total_label`, `creator_channel_audio_owned_rate_message` 문자열을 추가한다. - 검증 명령: - `./gradlew :app:mergeDebugResources` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - 기대 결과: - 리소스 merge와 layout 테스트가 PASS한다. - 검증 기록: - 2026-06-19 `fragment_creator_channel_audio.xml`에 black background, `@layout/view_capsule_tab_bar` include, 52dp sort bar, 14dp margin 소장률 card, RecyclerView, empty/error/retry 영역을 추가했다. `values`, `values-en`, `values-ja`에 오디오 empty/error/upload/total/rate 문자열을 추가했다. - 2026-06-19 `./gradlew :app:mergeDebugResources` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 리뷰 지적 반영: Figma `290:9029` 기준과 다르던 소장률 card를 한 줄 문장형 `전체 오디오의 n%를 소장하고 있어요.`, 우측 `purchased/paid개`, 14dp padding, 4dp progress 구조로 수정했다. Figma `290:8965` 기준 empty/error 중앙 정렬을 위해 empty container와 error message에 parent 상하 constraint를 추가했다. - 2026-06-19 RED 확인: `CreatorChannelAudioFragmentLayoutTest`에 소장률 card/empty 중앙 정렬 기대값을 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` 실행 결과 기존 Fragment가 `tvCreatorChannelAudioRateMessage`를 사용하지 않아 실패했다. - 2026-06-19 GREEN 확인: 소장률 card layout, rate message/count binding, `%` 부분 `soda_400` 강조 span, 다국어 문자열을 수정한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` 실행 결과 BUILD SUCCESSFUL. - [x] **Task 4.3: 라이브 다시듣기 item layout을 공통 오디오 콘텐츠 item layout으로 rename** - rename: - `app/src/main/res/layout/item_creator_channel_live_replay.xml` → `app/src/main/res/layout/item_creator_channel_audio_content.xml` - 작업: - 별도 중복 layout을 생성하지 않고 기존 라이브 다시듣기 item layout을 크리에이터 채널 공통 오디오 콘텐츠 item layout으로 사용한다. - layout id prefix를 `creator_channel_live_replay`에서 `creator_channel_audio_content`로 변경한다. - thumbnail 88dp, radius 14dp clip, adult badge, original/first/point/free tag, title, secondary text, play/price/status 영역을 포함한다. - 기존 duration TextView는 별도 seriesName TextView를 추가하지 않고 공통 secondary text TextView로 rename한다. - secondary text TextView는 라이브 다시듣기에서 `duration`, 오디오 탭에서 `duration • seriesName` 또는 `duration`을 한 줄 말줄임 처리한다. - 검증 명령: - `./gradlew :app:mergeDebugResources` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - 기대 결과: - 공통 item layout test와 기존 라이브 다시듣기 layout test가 모두 PASS한다. - 검증 기록: - 2026-06-19 `item_creator_channel_live_replay.xml`을 `item_creator_channel_audio_content.xml`로 rename하고 id prefix를 `creator_channel_audio_content`로 변경했다. `duration` TextView는 `secondary_text`로 rename했다. - 2026-06-19 `CreatorChannelLiveReplayAdapter`는 `ItemCreatorChannelAudioContentBinding`과 신규 id를 사용하도록 갱신했고, `fragment_creator_channel_live.xml`의 `tools:listitem`과 live layout test 기대값도 공통 item 이름으로 갱신했다. - 2026-06-19 `./gradlew :app:mergeDebugResources` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./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.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 BUILD SUCCESSFUL. - [x] **Task 4.4: `CreatorChannelAudioFragment` 골격 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt` - 작업: - `newInstance(creatorId: Long)`를 제공한다. - `CreatorChannelAudioViewModel`을 Koin으로 주입한다. - state observer에서 `Loading`, `Empty`, `Error`, `Content`를 분기한다. - `Content` 상태에서 theme tab, Sort-bar, 소장률 card, list visibility를 bind한다. - `Empty` 상태에서 theme tab-bar, Sort-bar, 소장률 card, list를 모두 숨긴다. - retry button은 `viewModel.retryAudio()`를 호출한다. - 검증 명령: - `./gradlew :app:compileDebugKotlin` - 기대 결과: - Fragment 골격이 컴파일된다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioFragment` 골격을 추가했다. `BaseFragment`, Koin `by viewModel()`, `newInstance(creatorId)`, `audioStateLiveData` observer, Loading/Empty/Error/Content visibility 분기, retryAudio 연결, Content의 sort count/label 및 rate text/progress bind를 포함했다. Phase 5/6 범위인 theme click, sort popup, adapter bind, pager 연결, pagination 연결은 구현하지 않았다. - 2026-06-19 `./gradlew :app:compileDebugKotlin` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 리뷰 지적 반영으로 `CreatorChannelAudioFragment.bindRate()`에서 소장률 문장과 count를 분리하고, 문장 내 `%` 구간에 `ForegroundColorSpan`으로 `soda_400` 강조를 적용했다. - 2026-06-19 Phase 4 코드 리뷰 중 progress fill이 왼쪽 기준으로 채워져야 하는 회귀 조건을 확인했고, `viewCreatorChannelAudioRateFill.pivotX = 0f` source 검증을 `CreatorChannelAudioFragmentLayoutTest`에 추가했다. 기존 구현이 이미 조건을 만족해 테스트는 BUILD SUCCESSFUL. --- ### Phase 5: Adapter, item bind, theme tab, sort UI 구현 - [x] **Task 5.1: 라이브 다시듣기 adapter를 공통 오디오 콘텐츠 adapter로 rename/move** - move/rename: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt` → `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelAudioContentAdapter.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/live/model/CreatorChannelLiveUiModels.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/model/CreatorChannelLiveMappers.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/model/CreatorChannelAudioUiModels.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/model/CreatorChannelAudioMappers.kt` - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` - 작업: - 별도 오디오 전용 adapter를 만들지 않고 기존 live replay adapter를 creator channel 공통 `CreatorChannelAudioContentAdapter`로 사용한다. - live replay UI model/status 중 item bind에 필요한 필드는 creator channel 공통 오디오 콘텐츠 item model/status로 rename/move한다. - 오디오 mapper와 라이브 mapper가 같은 공통 item model/status를 반환하도록 조정한다. - `ItemCreatorChannelAudioContentBinding`을 사용한다. - 이미지 로딩은 기존 `loadUrl`/placeholder 정책을 따른다. - adult badge, original/first/point/free tag, play/owned/rented/price 상태를 라이브 다시듣기 item과 동일하게 bind한다. - 오디오 탭의 `secondaryText`는 mapper 결과 그대로 표시하고, 라이브 다시듣기는 기존처럼 `duration`만 표시한다. - item 클릭 시 `audioContentId`를 host callback으로 전달한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - 오디오 layout/source 테스트, 기존 라이브 다시듣기 layout/source 테스트, 컴파일이 모두 PASS한다. - 검증 기록: - 2026-06-19 `CreatorChannelLiveReplayAdapter`를 `creator/channel/ui/CreatorChannelAudioContentAdapter`로 move/rename하고, item bind model/status를 `creator/channel/model/CreatorChannelAudioContentUiModel`, `CreatorChannelAudioContentStatus`로 공통화했다. - 2026-06-19 `CreatorChannelLiveMappers.toReplayUiModel()`과 `CreatorChannelAudioMappers.toAudioContentUiModels()`가 공통 item model/status를 반환하도록 수정했고, live/audio Fragment layout/source test의 adapter 경로와 mapper test import를 공통 이름 기준으로 갱신했다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:compileDebugKotlin --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - [x] **Task 5.2: `CapsuleTabBarView`로 theme tab-bar 연결** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt` - 작업: - 별도 theme adapter/item layout을 만들지 않고 v2 공통 `CapsuleTabBarView`를 사용한다. - `CreatorChannelAudioThemeUiModel.title` 목록을 `CapsuleTabBarView.setMenus()`에 전달한다. - `isSelected == true`인 theme의 index를 selected index로 전달한다. - 첫 번째 `전체` tab은 mapper가 만든 synthetic model을 사용한다. - `setOnTabSelectedListener`의 index를 `CreatorChannelAudioThemeUiModel.themeId`로 역매핑해 `viewModel.changeTheme(themeId)`를 호출한다. - 같은 tab click은 ViewModel에서 no-op 처리한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModelTest"` - `./gradlew :app:mergeDebugResources` - 기대 결과: - theme 선택 테스트와 resource merge가 PASS한다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioFragment`에서 `CapsuleTabBarView.setMenus()` 표시 상태에 더해 `setOnTabSelectedListener`를 연결하고, 선택 index를 `CreatorChannelAudioThemeUiModel.themeId`로 역매핑해 `viewModel.changeTheme(themeId)`를 호출하도록 구현했다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:mergeDebugResources --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - [x] **Task 5.3: Sort-bar와 sort popup 연결** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` - move/rename: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveSortPopup.kt` → `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelSortPopup.kt` - `app/src/main/res/layout/view_creator_channel_live_sort_menu.xml` → `app/src/main/res/layout/view_creator_channel_sort_menu.xml` - 작업: - Sort-bar count는 `audioContentCount`를 표시한다. - sort label은 `ContentSort.toLabelResId()`를 사용한다. - sort button click 시 라이브 탭에서 쓰던 popup을 공통 `CreatorChannelSortPopup` 이름으로 rename/move해 표시한다. - popup option 선택 시 `viewModel.changeSort(sort)`를 호출한다. - 같은 sort 선택은 ViewModel에서 no-op 처리하고 popup만 닫는다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModelTest"` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - 정렬 변경 테스트와 컴파일이 PASS한다. - 검증 기록: - 2026-06-19 Phase 3에서 공통화된 `CreatorChannelSortPopup`을 오디오 Sort-bar에 연결했다. Sort-bar 클릭 시 현재 `Content` 상태 기준 popup을 표시하고, option 선택 시 `viewModel.changeSort(sort)`를 호출한다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:compileDebugKotlin --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - [x] **Task 5.4: 소장률 card bind 구현** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt` - 작업: - `rateUiModel == null`이면 소장률 card를 숨긴다. - `rateUiModel != null`이면 `전체 오디오의 n%를 소장하고 있어요.` 문구와 `purchased/paid개`를 표시한다. - progress bar fill width는 percent 값을 기준으로 설정한다. - 내 채널 또는 특정 테마 필터 상태에서는 표시하지 않는다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest"` - `./gradlew :app:compileDebugKotlin` - 기대 결과: - 소장률 mapper 테스트와 컴파일이 PASS한다. - 검증 기록: - 2026-06-19 Phase 4에서 작성된 소장률 card bind를 유지하면서, 오디오 Fragment가 Activity Host의 `isCreatorChannelOwner()` 값을 `viewModel.loadAudio(creatorId, isOwner)`에 전달하도록 연결해 본인 채널 소장률 숨김 정책이 실제 탭 진입에도 반영되도록 했다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:compileDebugKotlin --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. --- ### Phase 6: 탭 연결, Activity callback, owner CTA, pagination 통합 - [x] **Task 6.1: 오디오 탭을 pager에 연결** - 수정: - `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` - 작업: - `CreatorChannelTab.Audio`에서 `CreatorChannelAudioFragment.newInstance(creatorId)`를 반환한다. - 기존 Home/Live 연결은 유지한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"` - 기대 결과: - pager adapter 테스트가 PASS한다. - 검증 기록: - 2026-06-19 `CreatorChannelPagerAdapter`에서 `CreatorChannelTab.Audio -> CreatorChannelAudioFragment.newInstance(creatorId)` 분기를 추가하고, `CreatorChannelPagerAdapterTest`는 Home/Live/Audio 실제 Fragment와 나머지 placeholder 정책을 검증하도록 갱신했다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveMapperTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - [x] **Task 6.2: Activity host callback 연결** - 수정: - `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` - 작업: - `CreatorChannelAudioFragment.Host`를 구현한다. - item click은 `startAudioContentDetail(audioContentId)`로 연결한다. - 기존 live 전용 scroll-to-bottom hook을 creator channel 공통 scroll hook 이름으로 rename하고, 현재 탭이 Live/Audio인지에 따라 해당 Fragment의 load-more 진입점을 호출한다. - content height 변화 시 기존 live tab content changed 처리를 creator channel 공통 content changed 처리로 rename해 Live/Audio가 같은 sticky/viewport 보정을 사용한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - 기대 결과: - Activity source test가 PASS한다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioFragment.Host`를 추가하고 `CreatorChannelActivity`가 이를 구현하도록 연결했다. 오디오 item click은 `startAudioContentDetail(audioContentId)`로 이동하고, 오디오 content changed는 `updateViewPagerHeight()`와 현재 탭 load-more 재평가를 호출한다. - 2026-06-19 live 전용 scroll bottom helper를 `calculateCreatorChannelRemainingScroll`, `postCheckCreatorChannelCurrentTabNeedsMore`, `checkCreatorChannelCurrentTabNeedsMore`, `notifyCurrentCreatorChannelTabScrolledToBottom` 등 공통 이름으로 확장하고 Live/Audio 현재 탭에 따라 각 Fragment load-more hook을 호출하도록 했다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioPaginationTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - [x] **Task 6.3: owner CTA를 공통 이름으로 rename하고 오디오 탭에 재사용** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` - `app/src/main/res/layout/activity_creator_channel.xml` - `app/src/main/res/values/strings.xml` - `app/src/main/res/values-en/strings.xml` - `app/src/main/res/values-ja/strings.xml` - 작업: - 기존 `layout_creator_channel_live_owner_cta`/`btn_creator_channel_live_owner_cta`를 `layout_creator_channel_owner_cta`/`btn_creator_channel_owner_cta` 같은 creator channel 공통 owner CTA 이름으로 rename한다. - CTA 내부 icon/text view도 live 전용 id이면 creator channel owner CTA 공통 id로 rename한다. - 현재 tab이 Live이면 기존 라이브 생성 label/icon/action을 유지하고, Audio이면 label `오디오 올리기`, icon `ic_new_upload_audio`, action `onOwnerFabAudioClicked()`를 사용한다. - 현재 tab이 Audio이고 `currentHeader?.isOwner == true`일 때 오디오 CTA를 표시한다. - CTA 표시 시 오디오 Fragment list/empty 하단 padding을 업데이트한다. - API error 또는 본인 여부 미확정 상태에서는 임의로 CTA를 노출하지 않는다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - `./gradlew :app:mergeDebugResources` - 기대 결과: - source test와 resource merge가 PASS한다. - 검증 기록: - 2026-06-19 `activity_creator_channel.xml`의 live 전용 owner CTA id를 `layout_creator_channel_owner_cta`, `btn_creator_channel_owner_cta`, `iv_creator_channel_owner_cta`, `tv_creator_channel_owner_cta` 공통 id로 rename했다. - 2026-06-19 `CreatorChannelActivity.updateOwnerCtaVisibility()`가 현재 탭에 따라 Live이면 `ic_new_create_live`/`creator_channel_live_start_button`/`onOwnerFabLiveClicked()`, Audio이면 `ic_new_upload_audio`/`creator_channel_audio_upload_button`/`onOwnerFabAudioClicked()`를 사용하도록 구현했다. CTA visibility 변경은 Live/Audio Fragment의 하단 padding hook으로 전달한다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioPaginationTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 `./gradlew :app:mergeDebugResources --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - [x] **Task 6.4: pagination 통합** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModel.kt` - 작업: - Activity의 공통 scroll-to-bottom hook에서 현재 탭이 Audio일 때 오디오 Fragment의 load-more 진입점이 호출되면 `viewModel.loadMore()`를 실행한다. - 다음 페이지 요청에는 현재 sort/themeId/size를 유지한다. - load-more 실패 시 기존 목록은 유지하고 pagination error message를 toast/snackbar로 표시한 뒤 consume한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioPaginationTest"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` - 기대 결과: - pagination 테스트와 Activity source test가 PASS한다. - 검증 기록: - 2026-06-19 `CreatorChannelAudioFragment.onCreatorChannelAudioScrolledToBottom()`을 추가해 Activity 공통 scroll-to-bottom hook에서 현재 탭이 Audio일 때 `viewModel.loadMore()`를 호출하도록 연결했다. - 2026-06-19 `CreatorChannelAudioFragment`가 pagination error message를 Toast로 표시한 뒤 `viewModel.consumePaginationErrorMessage()`를 호출하도록 구현해 기존 목록 유지 + error consume 계약을 UI에 연결했다. - 2026-06-19 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioPaginationTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. --- ### Phase 7: 통합 검증과 문서 갱신 - [ ] **Task 7.1: 오디오 탭 단위 테스트 실행** - 실행 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Audio*"` - 기대 결과: - 오디오 탭 관련 테스트가 모두 PASS한다. - 검증 기록: - 실행 후 명령, 결과, 실패 시 조치 내용을 이 위치에 누적한다. - [ ] **Task 7.2: 크리에이터 채널 회귀 테스트 실행** - 실행 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` - 기대 결과: - 홈/라이브/오디오 탭 관련 크리에이터 채널 테스트가 모두 PASS한다. - 검증 기록: - 실행 후 명령, 결과, 실패 시 조치 내용을 이 위치에 누적한다. - [ ] **Task 7.3: 리소스/컴파일/스타일 검증** - 실행 명령: - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - `git diff --check` - 기대 결과: - resource merge, Kotlin compile, ktlint, whitespace check가 모두 PASS한다. - 검증 기록: - 실행 후 명령, 결과, 기존 경고 여부를 이 위치에 누적한다. - [ ] **Task 7.4: 수동 확인** - 확인 항목: - 최초 진입 요청이 `page=0`, `size=20`, `sort=LATEST`, `themeId` 미전송으로 발생한다. - 서버 `themes`에 `전체`가 없어도 첫 tab으로 `전체`가 표시된다. - 응답 `themeId == null`이면 `전체` tab이 선택된다. - theme tab 선택 시 첫 페이지가 선택 themeId로 재조회된다. - `전체` 선택 시 `themeId`가 전송되지 않는다. - 정렬 메뉴와 옵션은 라이브 탭과 동일하게 동작한다. - 내 채널이 아니고 전체 테마일 때만 소장률 card가 표시된다. - 내 채널 또는 특정 테마 선택 상태에서는 소장률 card가 숨겨진다. - `seriesName`이 있으면 `duration • seriesName`, 없으면 `duration`만 표시된다. - 오디오 콘텐츠가 0개이면 empty 문구만 표시된다. - 내 채널에서는 하단 `오디오 올리기` CTA가 표시되고 `AudioContentUploadActivity`로 이동한다. - 목록 하단 도달 시 다음 페이지가 중복 없이 append된다. - 검증 기록: - 수동 확인 일시, 기기/해상도, 결과를 이 위치에 누적한다. --- ## Verification Log - 2026-06-19 Phase 3 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*"` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 Phase 3 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Audio*"` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 Phase 3 검증으로 `./gradlew :app:compileDebugKotlin` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 Phase 3 검증으로 `./gradlew :app:ktlintCheck` 실행 결과 BUILD SUCCESSFUL. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 출력됐으나 실패는 발생하지 않았다. - 2026-06-19 Phase 3 검증으로 `git diff --check` 실행 결과 whitespace error 없음. - 2026-06-19 리뷰 게이트에서 PRD의 `themeId`가 응답 `themes`에 없을 때 fallback 결정/검증 누락이 발견되어, `전체` tab selected fallback 테스트와 구현을 추가했다. 구현 전 해당 테스트 RED 실패를 확인했고, 수정 후 오디오 테스트/컴파일/ktlint/diff check와 후속 컨텍스트 리뷰가 PASS했다. - 2026-06-19 후속 리뷰 지적으로 fallback 시 UI selected 상태와 내부 `selectedThemeId`/소장률/후속 요청 기준이 불일치할 수 있어 `effectiveSelectedThemeId()` 정규화 정책을 추가했다. 구현 전 mapper/ViewModel 테스트 RED 실패를 확인했고, 수정 후 `CreatorChannelAudioMapperTest`, `CreatorChannelAudioViewModelTest`, `compileDebugKotlin --no-daemon`이 BUILD SUCCESSFUL. - 2026-06-19 정규화 후 최종 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*" --no-daemon`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Audio*" --no-daemon`, `./gradlew :app:ktlintCheck --no-daemon`, `git diff --check` 실행 결과 모두 PASS했다. - 2026-06-19 후속 리뷰 지적(`selectedThemeId` 정규화가 ViewModel 내부 상태까지 반영되지 않음)에 대해 선택 테마 응답이 서버 themes에 없는 `themeId`를 반환하는 RED 테스트를 추가했고, `CreatorChannelAudioViewModel` 첫 페이지 성공 경로에서 private `selectedThemeId`를 `effectiveSelectedThemeId()`로 갱신하도록 수정했다. - 2026-06-19 위 수정 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModelTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. 동일 실행에서 `compileDebugKotlin`도 PASS했다. `git diff --check` 실행 결과 whitespace error 없음. - 2026-06-19 Phase 3 코드 리뷰 재검증으로 `CreatorChannelAudioMappers.kt`, `CreatorChannelAudioUiModels.kt`, `CreatorChannelAudioViewModel.kt`, `CreatorChannelAudioMapperTest.kt`, `CreatorChannelAudioViewModelTest.kt`를 확인했다. 응답 `themeId` fallback 정규화가 theme UI, rate UI, `Content.selectedThemeId`, ViewModel private `selectedThemeId`, 후속 정렬 요청에 일관되게 반영되어 추가 수정 이슈는 발견하지 않았다. - 2026-06-19 Phase 3 코드 리뷰 재검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioMapperTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModelTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioPaginationTest"` 실행 결과 모두 BUILD SUCCESSFUL. 최초 샌드박스 실행은 `~/.gradle` wrapper lock 파일 접근 권한으로 실패해 승인 후 재실행했다. - 2026-06-19 Phase 3 코드 리뷰 재검증으로 `git diff --check` 실행 결과 whitespace error 없음. - 2026-06-19 Phase 4 코드 리뷰로 `CreatorChannelAudioFragment.kt`, `fragment_creator_channel_audio.xml`, `item_creator_channel_audio_content.xml`, `CreatorChannelSortPopup.kt`, `CreatorChannelSortModels.kt`, 오디오/라이브 layout test 변경을 확인했다. Phase 4 범위에서 추가 blocker는 발견하지 않았다. - 2026-06-19 Phase 4 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 실행 결과 모두 BUILD SUCCESSFUL 또는 whitespace error 없음. - 2026-06-19 Phase 5/6 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 Phase 5/6 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Audio*" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 Phase 5/6 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false` 실행 결과 BUILD SUCCESSFUL. - 2026-06-19 Phase 5/6 리소스/컴파일/스타일 검증으로 `./gradlew :app:mergeDebugResources --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:compileDebugKotlin --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:ktlintCheck --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `git diff --check` 실행 결과 모두 BUILD SUCCESSFUL 또는 whitespace error 없음. ktlint 실행 중 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 출력됐으나 실패는 발생하지 않았다. - 2026-06-19 Phase 5/6 리뷰 게이트에서 오디오 탭이 header 확정 전 `isOwner=false`로 선로드될 수 있는 문제와 Audio empty/CTA viewport 보정 및 pagination error 재평가 loop 가능성이 blocker로 지적됐다. 후속 수정으로 `CreatorChannelAudioFragment`의 즉시 로드를 제거하고 `onCreatorChannelAudioTabSelected()`에서 현재 Host owner 상태로 로드하도록 변경했으며, `CreatorChannelActivity`는 Audio 탭 선택 및 header 변경 시 해당 진입점을 호출한다. - 2026-06-19 Phase 5/6 리뷰 후속 수정으로 `CreatorChannelAudioFragment`에 empty min-height hook, `CreatorChannelAudioContentLayoutKey` 기반 content changed guard, Audio viewport height 전달을 추가해 empty/CTA 보정과 pagination error consume 후 즉시 재요청 가능성을 차단했다. - 2026-06-19 Phase 5/6 리뷰 후속 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Audio*" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:mergeDebugResources --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:compileDebugKotlin --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:ktlintCheck --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `git diff --check` 실행 결과 모두 BUILD SUCCESSFUL 또는 whitespace error 없음. - 2026-06-19 Phase 5/6 후속 리뷰에서 같은 `creatorId`로 `isOwner=false` 선로드 후 header 확정으로 `isOwner=true`가 전달돼도 `CreatorChannelAudioViewModel.loadAudio()`가 early return하는 owner race가 추가 blocker로 지적됐다. `loadAudio()`의 skip 조건을 `creatorId`, `isOwner`, 기존 state가 모두 동일한 경우로 제한하고, `CreatorChannelAudioViewModelTest`에 같은 creatorId라도 owner 상태가 바뀌면 첫 페이지를 재조회하고 소장률이 `null`로 갱신되는 회귀 테스트를 추가했다. - 2026-06-19 owner race 후속 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModelTest" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.*" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Audio*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.*" --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:mergeDebugResources --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:compileDebugKotlin --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `./gradlew :app:ktlintCheck --no-daemon -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false`, `git diff --check` 실행 결과 모두 BUILD SUCCESSFUL 또는 whitespace error 없음.