Files
sodalive-android/docs/20260619_크리에이터_채널_오디오_탭/plan-task.md

35 KiB

크리에이터 채널 오디오 탭 구현 계획/TASK

For agentic workers: 각 단계는 체크박스(- [ ])로 추적하고, 완료 즉시 - [x]로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.

Goal: GET /api/v2/creator-channels/{creatorId}/audio 응답을 기반으로 크리에이터 채널의 오디오 탭에 테마 필터, 정렬, 소장률, 오디오 콘텐츠 목록, empty 상태, 본인 채널 전용 하단 오디오 올리기 CTA와 pagination을 표시한다.

Architecture: 기존 CreatorChannelActivityViewPager2/CreatorChannelPagerAdapter 구조를 유지하고, CreatorChannelTab.Audio의 placeholder를 신규 CreatorChannelAudioFragment로 교체한다. 오디오 탭 전용 Fragment/ViewModel/DTO/mapper/UI model/adapter는 kr.co.vividnext.sodalive.v2.creator.channel.audio 하위에 두되, API/Repository는 기존 채널 공통 CreatorChannelApi/CreatorChannelRepository에 endpoint만 추가한다. 라이브 탭에서 검증된 sort popup, replay item 상태 표시, 하단 CTA inset 처리, 상세 이동 경로는 가능한 한 재사용하거나 동일 패턴으로 최소 구현한다.

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이다.
  • 첫 페이지 page0, 기본 size20, 기본 sortContentSort.LATEST이다.
  • 최초 조회와 전체 테마 선택 상태에서는 themeId query parameter를 보내지 않는다.
  • CreatorChannelAudioTabResponse.themes에는 전체가 포함되지 않는다.
  • 클라이언트가 themes 맨 앞에 전체 synthetic tab을 추가한다.
  • CreatorChannelAudioTabResponse.themeId == null이면 전체 tab 선택 상태로 표시한다.
  • ContentSortCreatorChannelAudioContentResponse는 기존 타입을 재사용한다.
  • duration == null인 오디오 콘텐츠는 서버에서 내려오지 않는 계약이며, 예외적으로 내려오면 해당 item을 숨긴다.
  • seriesName이 있으면 duration • seriesName, 없으면 duration만 표시한다.
  • 내 채널이 아니고 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
    • 오디오 탭 scroll-to-bottom, owner CTA 표시/클릭, 오디오 콘텐츠 클릭 callback을 연결한다.
  • 수정: app/src/main/res/layout/activity_creator_channel.xml
    • 라이브 CTA와 동일한 하단 고정 방식으로 오디오 CTA를 추가하거나, 기존 CTA 영역을 현재 탭에 맞게 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, 소장률, item, 상태, 화면 상태 UI model을 정의한다.
  • 생성: 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, theme click, pagination, host callback 연결을 담당한다.
  • 생성: app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/ui/CreatorChannelAudioContentAdapter.kt
    • 오디오 콘텐츠 목록 RecyclerView adapter를 담당한다.
  • 생성 후보: app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/ui/CreatorChannelAudioThemeAdapter.kt
    • theme tab-bar를 RecyclerView로 구성할 때만 추가한다. 단순 LinearLayout 동적 view로 충분하면 생성하지 않는다.
  • 재사용 후보: app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveSortPopup.kt
    • 이름이 라이브 전용이라 오디오에서 직접 사용하기 어렵다면 creator/channel/ui/CreatorChannelSortPopup.kt로 이동/rename하고 라이브/오디오가 함께 사용한다.
  • 생성: app/src/main/res/layout/fragment_creator_channel_audio.xml
    • theme tab-bar, Sort-bar, 소장률 카드, RecyclerView, empty/error/retry 영역을 포함한다.
  • 생성: app/src/main/res/layout/item_creator_channel_audio_content.xml
    • 오디오 콘텐츠 item layout이다. 라이브 다시듣기 item과 동일 구조를 유지하되 id prefix는 audio로 둔다.
  • 생성 후보: app/src/main/res/layout/item_creator_channel_audio_theme.xml
    • theme tab-bar를 RecyclerView로 구성할 때만 추가한다.
  • 수정 또는 생성: app/src/main/res/layout/view_creator_channel_live_sort_menu.xml
    • sort popup을 공용화하면 view_creator_channel_sort_menu.xml로 rename하고 라이브/오디오 참조를 함께 갱신한다.
  • 수정: 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: 기존 구조 확인과 작업 경계 고정

  • 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에 그대로 사용할 수 있는지 확인한다.
      • 라이브 탭 파일을 무리하게 리팩터링하지 않고 오디오 탭 구현에 필요한 최소 공용화만 계획에 반영한다.
    • 검증:
      • 라이브 탭 기존 테스트가 공용화 후에도 유지되어야 할 검증 목록을 기록한다.
  • 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 리소스 참조가 확인된다.
  • 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에 오디오 탭 연결 테스트를 추가할 준비가 되었는지 기록한다.

Phase 2: API/DTO/Repository/ViewModel 계약 추가

  • 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한다.
  • 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와 충돌 없이 컴파일된다.
  • 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 실패한다.
  • 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 상태로 둔다.
      • 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한다.

Phase 3: Mapper/UI model 정책 구현

  • 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 상태다.
      • seriesName이 있으면 secondary text가 duration • seriesName이다.
      • seriesName == null 또는 blank이면 secondary text가 duration이다.
      • duration == null이면 item mapper 결과에서 제외된다.
      • isOwned == trueisRented == 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 실패한다.
  • 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한다.
  • Task 3.3: 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
    • 생성 후보:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelSortPopup.kt
      • app/src/main/res/layout/view_creator_channel_sort_menu.xml
    • 작업:
      • 라이브 전용 이름이 오디오 참조를 어렵게 만들면 공용 CreatorChannelSortPopup으로 이동한다.
      • 공용화하더라도 기존 라이브 탭 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한다.

Phase 4: 오디오 탭 레이아웃과 Fragment 구성

  • 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에 theme tab container, 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은 88dp thumbnail, title, secondary text, play/price/status 영역을 가진다.
    • 검증 명령:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"
    • 기대 결과:
      • layout 미구현 상태에서 RED 실패한다.
  • 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, theme 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한다.
  • Task 4.3: 오디오 콘텐츠 item layout 작성

    • 생성:
      • app/src/main/res/layout/item_creator_channel_audio_content.xml
    • 작업:
      • item_creator_channel_live_replay.xml과 동일한 구조를 따르되 id prefix를 creator_channel_audio_content로 둔다.
      • thumbnail 88dp, radius 14dp clip, adult badge, original/first/point/free tag, title, secondary text, play/price/status 영역을 포함한다.
      • secondary text는 duration • seriesName 또는 duration이 한 줄 말줄임 처리되도록 한다.
    • 검증 명령:
      • ./gradlew :app:mergeDebugResources
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"
    • 기대 결과:
      • item layout test가 PASS한다.
  • 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 골격이 컴파일된다.

Phase 5: Adapter, item bind, theme tab, sort UI 구현

  • Task 5.1: 오디오 콘텐츠 adapter 구현

    • 생성:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/ui/CreatorChannelAudioContentAdapter.kt
    • 작업:
      • CreatorChannelAudioContentUiModel list를 bind한다.
      • 이미지 로딩은 기존 loadUrl/placeholder 정책을 따른다.
      • adult badge, original/first/point/free tag, play/owned/rented/price 상태를 라이브 다시듣기 item과 동일하게 bind한다.
      • item 클릭 시 audioContentId를 host callback으로 전달한다.
    • 검증 명령:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragmentLayoutTest"
      • ./gradlew :app:compileDebugKotlin
    • 기대 결과:
      • layout/source 테스트와 컴파일이 PASS한다.
  • Task 5.2: theme tab-bar 구현

    • 수정:
      • 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/ui/CreatorChannelAudioThemeAdapter.kt
      • app/src/main/res/layout/item_creator_channel_audio_theme.xml
    • 작업:
      • CreatorChannelAudioThemeUiModel을 화면에 표시한다.
      • 첫 번째 전체 tab은 mapper가 만든 synthetic model을 사용한다.
      • 선택 tab은 white background/black text, 미선택 tab은 black background/gray border/white text로 표시한다.
      • tab click은 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한다.
  • Task 5.3: Sort-bar와 sort popup 연결

    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioFragment.kt
    • 작업:
      • Sort-bar count는 audioContentCount를 표시한다.
      • sort label은 ContentSort.toLabelResId()를 사용한다.
      • sort button click 시 라이브 탭과 동일한 sort popup을 표시한다.
      • 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한다.
  • 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한다.

Phase 6: 탭 연결, Activity callback, owner CTA, pagination 통합

  • 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한다.
  • 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)로 연결한다.
      • 스크롤 하단 도달 시 현재 탭이 Audio이면 findAudioFragment()?.onCreatorChannelAudioScrolledToBottom()를 호출한다.
      • content height 변화 시 기존 live tab content changed 처리와 같은 방식으로 sticky/viewport 보정을 호출한다.
    • 검증 명령:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"
    • 기대 결과:
      • Activity source test가 PASS한다.
  • Task 6.3: 내 채널 오디오 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/res/values/strings.xml
      • app/src/main/res/values-en/strings.xml
      • app/src/main/res/values-ja/strings.xml
    • 작업:
      • 라이브 CTA와 동일한 하단 고정 방식으로 오디오 CTA를 표시한다.
      • label은 오디오 올리기, icon은 ic_new_upload_audio를 사용한다.
      • 현재 tab이 Audio이고 currentHeader?.isOwner == true일 때만 표시한다.
      • CTA click은 기존 onOwnerFabAudioClicked()를 호출한다.
      • 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한다.
  • 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 callback에서 Fragment onCreatorChannelAudioScrolledToBottom()가 호출되면 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한다.

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

  • 아직 통합 검증을 실행하지 않았다. 구현 단계에서 각 Phase/Task의 검증 기록과 최종 통합 검증 결과를 누적한다.