docs(creator): 오디오 탭 구현 계획을 기록한다
This commit is contained in:
546
docs/20260619_크리에이터_채널_오디오_탭/plan-task.md
Normal file
546
docs/20260619_크리에이터_채널_오디오_탭/plan-task.md
Normal file
@@ -0,0 +1,546 @@
|
||||
# 크리에이터 채널 오디오 탭 구현 계획/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/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`이다.
|
||||
- 첫 페이지 `page`는 `0`, 기본 `size`는 `20`, 기본 `sort`는 `ContentSort.LATEST`이다.
|
||||
- 최초 조회와 전체 테마 선택 상태에서는 `themeId` query parameter를 보내지 않는다.
|
||||
- `CreatorChannelAudioTabResponse.themes`에는 `전체`가 포함되지 않는다.
|
||||
- 클라이언트가 `themes` 맨 앞에 `전체` synthetic tab을 추가한다.
|
||||
- `CreatorChannelAudioTabResponse.themeId == null`이면 `전체` tab 선택 상태로 표시한다.
|
||||
- `ContentSort`와 `CreatorChannelAudioContentResponse`는 기존 타입을 재사용한다.
|
||||
- `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 == 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 실패한다.
|
||||
|
||||
- [ ] **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의 검증 기록과 최종 통합 검증 결과를 누적한다.
|
||||
322
docs/20260619_크리에이터_채널_오디오_탭/prd.md
Normal file
322
docs/20260619_크리에이터_채널_오디오_탭/prd.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# PRD: 크리에이터 채널 오디오 탭
|
||||
|
||||
## 1. Overview
|
||||
크리에이터 채널의 `오디오` 탭에서 오디오 콘텐츠 소장률, 테마 필터, 정렬, 오디오 콘텐츠 목록, 내 채널 업로드 CTA와 스크롤 pagination을 제공한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. Problem
|
||||
- 크리에이터 채널 컨테이너와 `라이브` 탭은 별도 문서에서 정의되었지만, `오디오` 탭의 API 계약과 Figma 기반 UI 요구사항은 별도 정의가 필요하다.
|
||||
- 사용자는 크리에이터 채널에서 전체 오디오 수, 구매/소장 현황, 테마별 콘텐츠 목록을 한 화면에서 탐색할 수 있어야 한다.
|
||||
- 오디오 콘텐츠 item은 기존 라이브 다시듣기 item과 동일한 구매/재생/보유 상태를 유지하되, `duration` 뒤에 `seriesName`이 표시될 수 있어야 한다.
|
||||
- 테마 필터와 정렬이 함께 존재하므로 query parameter 상태와 UI 선택 상태가 일관되게 유지되어야 한다.
|
||||
- 사용자가 크리에이터 본인인 경우 소장률 대신 오디오 업로드 진입 CTA를 제공해야 한다.
|
||||
- 목록이 길어질 수 있으므로 `CreatorChannelAudioTabResponse.hasNext == true`일 때 다음 페이지를 자동 로딩해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Goals
|
||||
- Figma `290:9015` 기준으로 크리에이터 채널 `오디오` 탭 UI 요구사항을 정의한다.
|
||||
- API endpoint `GET /api/v2/creator-channels/{creatorId}/audio`를 기준으로 최초 조회, 테마 변경, 정렬 변경, pagination 요구사항을 정의한다.
|
||||
- 최초 조회 query parameter 기본값은 `page=0`, `size=20`, `sort=LATEST`로 둔다.
|
||||
- 최초 조회 시 `themeId` query parameter는 보내지 않는다.
|
||||
- 응답의 `themes`에는 `전체` 항목이 포함되지 않으므로, 클라이언트가 맨 앞에 `전체` tab을 추가한다.
|
||||
- 응답의 `themeId == null` 상태는 클라이언트가 추가한 `전체` tab 선택 상태로 표현한다.
|
||||
- Sort-bar에는 전체 오디오 콘텐츠 수와 현재 정렬 label을 표시한다.
|
||||
- 정렬 선택 방식과 옵션은 `라이브` 탭과 동일하게 적용한다.
|
||||
- 소장률 섹션은 내 채널이 아니고 `전체` 테마 선택 상태일 때만 표시한다.
|
||||
- 소장률 섹션에는 전체 유료 오디오 중 소장한 비율과 `purchasedAudioContentCount/paidAudioContentCount`를 표시한다.
|
||||
- 내 채널인 경우 소장률 섹션을 숨기고 하단 고정 `오디오 올리기` CTA를 표시한다.
|
||||
- 오디오 콘텐츠 목록은 기존 라이브 다시듣기 item UI를 재사용하되, `seriesName`이 있으면 `duration • seriesName` 형식으로 표시한다.
|
||||
- `seriesName`이 없으면 시리즈 이름 UI를 숨기고 `duration`만 표시한다.
|
||||
- 오디오 콘텐츠가 0개이면 오디오 콘텐츠가 있을 때 표시하는 UI를 모두 숨기고 empty 문구만 표시한다.
|
||||
- 응답의 `hasNext`가 `true`이면 현재 `page + 1` 페이지를 추가 로딩한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. Non-Goals
|
||||
- 크리에이터 채널 상단 header, title bar, 공통 main tab-bar 구조 자체를 재설계하지 않는다.
|
||||
- `홈`, `라이브`, `시리즈`, `화보`, `커뮤니티`, `팬Talk`, `후원` 탭의 상세 구현은 이번 범위에서 제외한다.
|
||||
- 오디오 상세, 결제, 대여, 소장, 재생 플로우 내부 동작 변경은 이번 범위에서 제외한다.
|
||||
- 오디오 업로드/등록 화면 내부 구현은 이번 범위에서 제외하고 CTA 진입점만 정의한다.
|
||||
- API schema를 임의 변경하거나 서버 응답 필드명을 클라이언트에서 새로 정의하지 않는다.
|
||||
- 정렬/테마 외 별도 검색, 복수 필터, pull-to-refresh, skeleton/shimmer는 이번 범위에서 제외한다.
|
||||
- Figma asset을 localhost URL 그대로 앱 코드에 직접 의존하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 5. Target Users
|
||||
- 크리에이터 채널에서 오디오 콘텐츠를 테마별로 탐색하는 앱 사용자.
|
||||
- 구매 가능/보유/대여 상태를 확인한 뒤 오디오 콘텐츠를 재생하거나 상세로 이동하려는 앱 사용자.
|
||||
- 특정 크리에이터의 오디오 콘텐츠 중 자신이 얼마나 소장했는지 확인하려는 앱 사용자.
|
||||
- 본인 채널에서 오디오 업로드 화면으로 진입하려는 크리에이터.
|
||||
- `kr.co.vividnext.sodalive.v2` 하위 크리에이터 채널 탭을 구현/유지보수하는 Android 개발자.
|
||||
|
||||
---
|
||||
|
||||
## 6. User Stories
|
||||
- 사용자는 크리에이터 채널의 `오디오` 탭에서 전체 오디오 콘텐츠 수를 확인하고 싶다.
|
||||
- 사용자는 전체 오디오 중 자신이 소장한 비율과 소장 개수를 확인하고 싶다.
|
||||
- 사용자는 테마 tab을 선택해 특정 테마의 오디오 콘텐츠만 보고 싶다.
|
||||
- 사용자는 최신순 등 라이브 탭과 동일한 방식으로 오디오 콘텐츠를 정렬하고 싶다.
|
||||
- 사용자는 오디오 콘텐츠의 제목, 재생 시간, 시리즈 이름, 가격/무료/소장중/대여중 상태를 목록에서 구분하고 싶다.
|
||||
- 사용자는 시리즈 이름이 없는 콘텐츠에서는 불필요한 빈 구분자나 빈 텍스트가 보이지 않길 기대한다.
|
||||
- 사용자는 목록 하단까지 스크롤하면 다음 페이지가 자연스럽게 이어서 로딩되길 기대한다.
|
||||
- 사용자는 크리에이터가 아직 오디오를 준비 중인 경우 불필요한 필터/정렬 UI 없이 empty 문구만 보고 싶다.
|
||||
- 크리에이터 본인은 본인 채널의 `오디오` 탭에서 하단 고정 버튼으로 오디오 업로드 화면에 진입하고 싶다.
|
||||
|
||||
---
|
||||
|
||||
## 7. Core Features
|
||||
|
||||
### Creator Channel Audio Tab API
|
||||
`오디오` 탭 진입, 테마 변경, 정렬 변경, 추가 로딩 시 크리에이터별 오디오 탭 데이터를 조회한다.
|
||||
|
||||
#### Requirements
|
||||
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/audio`이다.
|
||||
- `creatorId`는 path variable로 전달한다.
|
||||
- Query parameters는 `sort`, `page`, `size`, `themeId`를 사용한다.
|
||||
- 최초 조회 기본값은 `page=0`, `size=20`, `sort=ContentSort.LATEST`이다.
|
||||
- 최초 조회 시 `themeId`는 보내지 않는다.
|
||||
- `sort`는 기존 `ContentSort` enum 값을 그대로 전달한다.
|
||||
- `ContentSort`는 기존에 만들어져 있는 타입을 재사용한다.
|
||||
- `CreatorChannelAudioContentResponse`는 기존에 만들어져 있는 타입을 재사용한다.
|
||||
- `hasNext == true`일 때 다음 페이지 요청은 현재 응답의 `page + 1` 값을 사용한다.
|
||||
- 중복 pagination 요청이 발생하지 않도록 loading 중 추가 요청을 막아야 한다.
|
||||
- 정렬 변경 시 기존 목록과 page 상태를 초기화하고 첫 페이지부터 다시 조회한다.
|
||||
- 테마 변경 시 기존 목록과 page 상태를 초기화하고 첫 페이지부터 다시 조회한다.
|
||||
- `themeId == null` 또는 전체 테마 선택 상태에서는 `themeId` query parameter를 보내지 않는다.
|
||||
- 서버 응답의 `themes`에는 `전체` 항목이 포함되지 않는다.
|
||||
- 클라이언트는 `themes` 응답 앞에 `전체` tab을 synthetic item으로 추가해 표시한다.
|
||||
- 서버 응답의 `themeId == null`은 `전체` tab 선택 상태로 해석한다.
|
||||
|
||||
#### Response Contract
|
||||
```kotlin
|
||||
data class CreatorChannelAudioTabResponse(
|
||||
val audioContentCount: Int,
|
||||
val paidAudioContentCount: Int,
|
||||
val purchasedAudioContentCount: Int,
|
||||
val purchasedAudioContentRate: Double,
|
||||
val themes: List<CreatorChannelAudioThemeResponse>,
|
||||
val audioContents: List<CreatorChannelAudioContentResponse>,
|
||||
val sort: ContentSort,
|
||||
val themeId: Long?,
|
||||
val page: Int,
|
||||
val size: Int,
|
||||
val hasNext: Boolean
|
||||
)
|
||||
|
||||
data class CreatorChannelAudioThemeResponse(
|
||||
val themeId: Long,
|
||||
val themeName: String
|
||||
)
|
||||
```
|
||||
|
||||
#### Edge Cases
|
||||
- 최초 조회 실패 시 기존 크리에이터 채널 탭의 error/retry 패턴을 따른다.
|
||||
- 정렬 변경 실패 또는 테마 변경 실패 시 현재 프로젝트의 에러 표시/재시도 패턴을 구현 계획 단계에서 확인해 따른다.
|
||||
- 다음 페이지 로딩 실패 시 기존 목록은 유지하고 기존 pagination 실패 표시 정책을 따른다.
|
||||
- 다음 페이지 응답의 `audioContents`가 비어 있어도 `hasNext` 값 기준으로 이후 로딩 가능 여부를 갱신한다.
|
||||
- 서버 응답의 `sort`, `themeId`, `page`, `size`가 요청 상태와 다를 경우 구현 계획 단계에서 기존 ViewModel 상태 동기화 패턴을 확인해 따른다.
|
||||
- `duration == null`인 오디오 콘텐츠는 서버에서 내려오지 않는 것이 계약이다.
|
||||
- 예외적으로 `duration == null`인 오디오 콘텐츠가 내려오면 해당 콘텐츠 item 자체를 목록에서 숨긴다.
|
||||
|
||||
### Theme Tab Bar
|
||||
오디오 탭 상단의 테마 tab-bar는 클라이언트가 추가한 `전체` tab과 서버가 내려준 테마 목록을 표시하고 선택된 테마로 목록을 필터링한다.
|
||||
|
||||
#### Requirements
|
||||
- Figma 전체 화면 기준 노드는 `290:9015`이며, 테마 tab-bar는 main tab-bar 아래에 위치한다.
|
||||
- 첫 번째 tab은 클라이언트가 추가하는 `전체`이며 `themeId == null` 상태를 나타낸다.
|
||||
- `CreatorChannelAudioTabResponse.themes`에는 `전체`가 포함되지 않는다.
|
||||
- `themes` 응답 순서를 유지하되, 클라이언트에서 추가한 `전체` tab 뒤에 서버 테마 tab을 표시한다.
|
||||
- 각 theme tab은 `CreatorChannelAudioThemeResponse.themeId`, `themeName`을 사용한다.
|
||||
- `CreatorChannelAudioTabResponse.themeId == null`이면 `전체` tab을 선택 상태로 표시한다.
|
||||
- `CreatorChannelAudioTabResponse.themeId`가 특정 theme id이면 해당 `themeId`를 가진 서버 테마 tab을 선택 상태로 표시한다.
|
||||
- 선택된 tab은 Figma처럼 white background와 black text로 표시한다.
|
||||
- 선택되지 않은 tab은 black background, gray border, white text로 표시한다.
|
||||
- theme tab을 선택하면 `page=0`, 현재 `sort`, 선택한 `themeId`로 API를 다시 조회한다.
|
||||
- 이미 선택된 theme tab을 다시 선택하면 API를 재호출하지 않는다.
|
||||
- theme tab-bar는 가로 스크롤을 지원한다.
|
||||
|
||||
#### Edge Cases
|
||||
- `themes`가 비어도 `전체` tab은 표시한다.
|
||||
- 서버 응답 `themes`에 실수로 `전체`와 동일한 이름의 테마가 포함되어도 클라이언트가 추가한 `전체` tab과 서버 테마 tab을 별개로 취급한다.
|
||||
- `themeName`이 긴 경우 tab 내부에서 텍스트와 주변 tab이 겹치지 않도록 기존 capsule tab 정책을 따른다.
|
||||
- 선택된 `themeId`가 응답의 `themes`에 없으면 `전체` 선택 상태로 fallback하는지 여부는 구현 계획 단계에서 기존 탭 상태 정책을 확인해 결정한다.
|
||||
|
||||
### Sort Bar and Sort Menu
|
||||
Sort-bar는 전체 오디오 콘텐츠 수와 현재 정렬 상태를 표시하고, 라이브 탭과 동일한 정렬 메뉴를 연다.
|
||||
|
||||
#### Requirements
|
||||
- Figma 전체 화면 기준 Sort-bar는 `290:9015` 내 `sort-bar`이다.
|
||||
- 좌측에는 `전체`와 `audioContentCount`를 표시한다.
|
||||
- 우측에는 현재 정렬 label과 정렬 icon을 표시한다.
|
||||
- 정렬 선택 방식은 `라이브` 탭과 동일하다.
|
||||
- 정렬 기본값은 `ContentSort.LATEST`이며 label은 한국어 기준 `최신순`이다.
|
||||
- 정렬 옵션, label, 선택 표시, 메뉴 닫힘 동작은 `라이브` 탭 구현과 동일하게 재사용한다.
|
||||
- 정렬 옵션을 선택하면 `page=0`, 선택된 `sort`, 현재 `themeId`로 API를 다시 조회한다.
|
||||
- 선택 중인 정렬 옵션을 다시 선택하면 API 재호출 없이 메뉴만 닫는다.
|
||||
|
||||
#### Edge Cases
|
||||
- 작은 화면에서 정렬 메뉴가 화면 우측 또는 하단을 벗어나지 않도록 라이브 탭과 동일한 위치 보정 정책을 적용한다.
|
||||
- 다국어 label 길이가 길어져도 Sort-bar text와 icon이 겹치지 않아야 한다.
|
||||
|
||||
### Purchased Audio Rate Section
|
||||
소장률 섹션은 사용자가 전체 유료 오디오 중 소장한 비율과 개수를 확인할 수 있게 한다.
|
||||
|
||||
#### Requirements
|
||||
- Figma 기준 노드는 `290:9029`이다.
|
||||
- Sort-bar 아래, 오디오 콘텐츠 목록 위에 표시한다.
|
||||
- 내 채널이 아닌 경우에만 표시한다.
|
||||
- `themeId == null`인 전체 테마 선택 상태에서만 표시한다.
|
||||
- 특정 테마가 선택된 필터 상태에서는 표시하지 않는다.
|
||||
- 카드 배경은 Figma 기준 `gray/900 #202020`, radius 14dp, 내부 padding 14dp를 따른다.
|
||||
- 좌측 문구는 `전체 오디오의 n%를 소장하고 있어요.` 형식으로 표시한다.
|
||||
- `purchasedAudioContentRate`는 서버에서 percent 값으로 내려오며, `n%`에 그대로 사용자 표시용 포맷만 적용한다.
|
||||
- 우측에는 `purchasedAudioContentCount/paidAudioContentCount개`를 표시한다.
|
||||
- 하단 progress bar는 `purchasedAudioContentRate`를 기준으로 채움 비율을 표시한다.
|
||||
- `paidAudioContentCount`, `purchasedAudioContentCount`, `purchasedAudioContentRate`는 응답이 의미하는 서버 기준 값을 그대로 사용한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 내 채널인 경우 `themeId == null`이어도 소장률 섹션을 표시하지 않는다.
|
||||
- `paidAudioContentCount == 0`이면 0 나누기 계산을 클라이언트에서 수행하지 않고, 서버의 `purchasedAudioContentRate` 또는 0% 표시 정책을 따른다.
|
||||
- 긴 숫자나 다국어 문구에서도 좌측 문구와 우측 카운트가 겹치지 않아야 한다.
|
||||
|
||||
### Audio Content List
|
||||
오디오 콘텐츠 목록은 기존 라이브 다시듣기 item UI와 동일하게 표시하되, 시리즈 이름 표시 규칙을 추가한다.
|
||||
|
||||
#### Requirements
|
||||
- Figma 콘텐츠 item 기준 노드는 `290:9026`이다.
|
||||
- `audioContents`를 세로 목록으로 표시한다.
|
||||
- 각 item은 기존 라이브 다시듣기 item UI, 상태 표시, tag 표시, 가격/재생 CTA 정책을 재사용한다.
|
||||
- 각 item은 `imageUrl`, `title`, `duration`, `seriesName`을 표시한다.
|
||||
- `seriesName`이 있으면 secondary text를 `duration • seriesName` 형식으로 표시한다.
|
||||
- `seriesName`이 없으면 시리즈 이름 UI를 숨기고 `duration`만 표시한다.
|
||||
- `duration`은 필수 표시 값이며, `duration == null`인 콘텐츠는 목록에서 숨긴다.
|
||||
- `isAdult`, `isPointAvailable`, `isFirstContent`, `isOriginalSeries`, `price`, `isOwned`, `isRented`는 기존 라이브 다시듣기/오디오 item 정책과 동일하게 매핑한다.
|
||||
- item 터치 시 오디오 콘텐츠 상세 또는 재생 진입은 기존 오디오 콘텐츠 item 정책을 재사용한다.
|
||||
|
||||
#### Edge Cases
|
||||
- `seriesName`이 blank string이면 없는 값으로 취급한다.
|
||||
- `duration == null`이면 `seriesName` 존재 여부와 관계없이 해당 콘텐츠를 숨긴다.
|
||||
- `isOwned == true`와 `isRented == true`가 동시에 내려오면 기존 라이브 다시듣기 정책과 동일하게 우선순위를 적용한다.
|
||||
- `title`이 긴 경우 Figma처럼 최대 2줄까지 표시하고 이후 말줄임 처리한다.
|
||||
- secondary text가 긴 경우 한 줄 말줄임 처리한다.
|
||||
- `imageUrl == null` 또는 이미지 로딩 실패 시 기존 이미지 placeholder 정책을 따른다.
|
||||
|
||||
### Pagination
|
||||
오디오 콘텐츠 목록은 스크롤 하단 접근 시 다음 페이지를 로딩한다.
|
||||
|
||||
#### Requirements
|
||||
- `CreatorChannelAudioTabResponse.hasNext == true`일 때만 다음 페이지를 요청한다.
|
||||
- 다음 페이지는 마지막 성공 응답의 `page + 1`로 요청한다.
|
||||
- 다음 페이지 요청에는 현재 `sort`, 현재 `themeId`, `size=20`을 유지한다.
|
||||
- `themeId == null`이면 다음 페이지 요청에서도 `themeId`를 보내지 않는다.
|
||||
- 다음 페이지 로딩 중에는 추가 page 요청을 중복으로 보내지 않는다.
|
||||
- 다음 페이지 성공 시 기존 `audioContents` 뒤에 append한다.
|
||||
- 정렬 변경 또는 테마 변경 시 pagination 상태를 초기화한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 빠른 스크롤로 load-more trigger가 반복 발생해도 page가 중복 append되지 않아야 한다.
|
||||
- Fragment/View 재생성 후 현재 목록, 정렬, 테마, page 상태는 ViewModel 상태 보존 정책에 따라 유지되어야 한다.
|
||||
|
||||
### Empty State
|
||||
오디오 콘텐츠가 없으면 오디오 콘텐츠가 있을 때 표시하는 UI를 모두 숨기고 empty 문구만 표시한다.
|
||||
|
||||
#### Requirements
|
||||
- Figma 기준 노드는 `290:8965`이다.
|
||||
- `audioContentCount == 0` 또는 표시 가능한 `audioContents`가 없는 전체 empty 상태이면 empty 상태를 표시한다.
|
||||
- empty 문구는 `크리에이터가 오디오를 준비 중입니다.\n기대해 주세요!`이다.
|
||||
- empty 상태에서는 테마 tab-bar, Sort-bar, 소장률 섹션, 오디오 콘텐츠 목록을 표시하지 않는다.
|
||||
- empty 상태 표시 방식은 라이브 탭 empty 상태와 동일하게 적용한다.
|
||||
- empty 문구는 화면 중앙 영역에 gray text로 표시하고 다국어 문자열 리소스로 관리한다.
|
||||
- 내 채널 empty 상태에서도 empty 문구는 동일하게 표시한다.
|
||||
- 내 채널 empty 상태에서 하단 CTA 표시 여부는 내 채널 CTA 정책을 따른다.
|
||||
|
||||
#### Edge Cases
|
||||
- API 최초 조회 실패 상태는 empty 상태로 취급하지 않고 기존 error/retry 패턴을 따른다.
|
||||
- `duration == null`인 콘텐츠를 숨긴 결과 표시 가능한 콘텐츠가 0개가 되면 empty 상태 정책을 적용한다.
|
||||
|
||||
### Owner Audio Upload CTA
|
||||
로그인 사용자가 해당 크리에이터 본인인 경우 오디오 탭 하단에 고정된 오디오 업로드 버튼을 표시한다.
|
||||
|
||||
#### Requirements
|
||||
- Figma 기준 노드는 `665:19008`이다.
|
||||
- CTA 버튼 표시 방식은 라이브 탭의 `라이브 시작하기` CTA와 동일하게 적용한다.
|
||||
- 로그인 사용자가 현재 크리에이터 채널의 본인이면 하단 고정 CTA 영역을 표시한다.
|
||||
- 로그인 사용자가 현재 크리에이터 채널의 본인이 아니면 하단 고정 CTA 영역을 표시하지 않는다.
|
||||
- 내 채널인 경우 소장률 섹션은 표시하지 않는다.
|
||||
- CTA 영역은 화면 하단에 고정하고, 목록 스크롤과 함께 움직이지 않는다.
|
||||
- CTA 버튼 label은 `오디오 올리기`이며 다국어 문자열 리소스로 관리한다.
|
||||
- CTA 버튼 icon은 `ic_new_upload_audio` drawable 리소스를 사용한다.
|
||||
- CTA가 표시되는 경우 목록 마지막 item 또는 empty 문구가 CTA에 가려지지 않도록 하단 padding 또는 inset을 추가한다.
|
||||
- Android gesture navigation, soft navigation bar, display cutout 환경에서 CTA가 system navigation 영역과 겹치지 않도록 bottom inset을 반영한다.
|
||||
- 버튼 터치 시 기존 또는 신규 오디오 업로드 진입 플로우로 이동한다. 업로드 화면 내부 구현은 이번 PRD 범위에서 제외한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 본인 여부 판정 데이터가 아직 로딩 중이면 CTA를 먼저 노출하지 않는다.
|
||||
- 본인 여부 판정 실패 또는 API 실패 상태에서는 기존 에러 상태 정책을 따르고 CTA를 임의로 노출하지 않는다.
|
||||
- 오디오 업로드 플로우 진입 중 중복 터치를 막는다.
|
||||
|
||||
### Suggested Implementation Phases
|
||||
Figma 섹션 기준으로 구현 계획/TASK 문서를 작성할 때 아래 단위로 Phase를 나누는 것을 권장한다.
|
||||
|
||||
#### Phase Candidates
|
||||
- Phase 1: API/DTO/Repository 계약 추가
|
||||
- Phase 2: ViewModel 상태, sort/theme/pagination 로직
|
||||
- Phase 3: Theme tab-bar 및 Sort-bar UI
|
||||
- Phase 4: 소장률 섹션 UI와 표시 조건
|
||||
- Phase 5: 오디오 콘텐츠 item/list UI와 시리즈 이름 표시
|
||||
- Phase 6: empty 상태와 내 채널 CTA UI
|
||||
- Phase 7: error/loading/pagination 검증 및 통합 확인
|
||||
|
||||
---
|
||||
|
||||
## 8. UX / UI Expectations
|
||||
- 전체 배경은 Figma 기준 black이다.
|
||||
- 공통 크리에이터 채널 header와 main tab-bar는 기존 크리에이터 채널 화면 정책을 따른다.
|
||||
- 오디오 탭 선택 상태는 main tab-bar에서 cyan underline과 white text로 표시한다.
|
||||
- 테마 tab-bar는 main tab-bar 아래에 위치하고, 선택된 tab은 white capsule로 표시한다.
|
||||
- Sort-bar 높이, 좌우 여백, 텍스트 스타일은 라이브 탭 Sort-bar와 동일하게 맞춘다.
|
||||
- 소장률 카드는 Sort-bar 아래에 14dp 좌우 여백을 두고, progress bar는 cyan fill과 dark gray track을 사용한다.
|
||||
- 소장률 카드는 내 채널 또는 특정 테마 필터 상태에서는 표시하지 않는다.
|
||||
- 오디오 콘텐츠 item은 88dp 정사각 썸네일, 14dp corner, title 영역, 우측 CTA/status 영역의 가로형 목록 구조를 따른다.
|
||||
- empty 상태에서는 콘텐츠 UI를 모두 숨기고 Figma `290:8965`처럼 중앙 안내 문구만 표시한다.
|
||||
- 내 채널 CTA는 라이브 탭 하단 CTA와 동일한 고정 방식으로 표시하고, cyan capsule 버튼과 `ic_new_upload_audio` icon을 사용한다.
|
||||
- item 간 세로 간격과 좌우 여백은 Figma `290:9015`, `290:9026` 기준을 따른다.
|
||||
- 문자열은 다국어 문자열 리소스로 관리한다.
|
||||
- 텍스트와 아이콘은 작은 화면과 긴 다국어 문구에서도 겹치지 않아야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 9. Technical Constraints
|
||||
- Android Gradle 프로젝트의 `:app` 단일 모듈 구조를 유지한다.
|
||||
- 신규 `Activity`, `Fragment`, `ViewModel` 및 연결된 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
||||
- 기존 크리에이터 채널 컨테이너, 탭 전환, sticky 동작을 불필요하게 재설계하지 않는다.
|
||||
- 기존 `ContentSort`와 `CreatorChannelAudioContentResponse`를 재사용한다.
|
||||
- 정렬 메뉴는 라이브 탭과 동일한 구현/리소스/문자열 매핑을 우선 재사용한다.
|
||||
- 내 채널 하단 CTA는 라이브 탭 CTA 구현/레이아웃/inset 처리 방식을 우선 재사용한다.
|
||||
- 이미지 placeholder, 가격/포인트/무료/소장중/대여중/19금/original tag 표시 정책은 기존 라이브 다시듣기 및 오디오 item 구현과 맞춘다.
|
||||
- API 실패, retry, pagination 실패 표시는 기존 프로젝트 패턴을 구현 계획 단계에서 확인해 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 10. Metrics
|
||||
- 오디오 탭 최초 진입 시 API 요청이 `page=0`, `size=20`, `sort=LATEST`, `themeId` 미전송으로 발생한다.
|
||||
- 테마 선택 시 선택한 `themeId`로 첫 페이지가 재조회된다.
|
||||
- 전체 테마 선택 시 `themeId` query parameter가 전송되지 않는다.
|
||||
- 서버 응답 `themes`에 `전체`가 없어도 화면 맨 앞에 `전체` tab이 표시된다.
|
||||
- 서버 응답 `themeId == null`이면 `전체` tab이 선택 상태로 표시된다.
|
||||
- 정렬 선택 시 라이브 탭과 동일한 정렬 옵션과 UI 동작이 적용된다.
|
||||
- 내 채널이 아니고 전체 테마 상태일 때 `purchasedAudioContentRate`, `purchasedAudioContentCount`, `paidAudioContentCount`가 소장률 섹션에 정확히 표시된다.
|
||||
- 내 채널 또는 특정 테마 필터 상태에서는 소장률 섹션이 표시되지 않는다.
|
||||
- `seriesName`이 있는 콘텐츠는 `duration • seriesName`, 없는 콘텐츠는 `duration`만 표시된다.
|
||||
- `duration == null`인 콘텐츠는 목록에서 제외된다.
|
||||
- 오디오 콘텐츠가 0개이면 `크리에이터가 오디오를 준비 중입니다.\n기대해 주세요!` 문구만 표시된다.
|
||||
- 내 채널에서는 하단 고정 `오디오 올리기` CTA가 `ic_new_upload_audio` icon과 함께 표시된다.
|
||||
- `hasNext == true`일 때 다음 페이지가 한 번씩만 append된다.
|
||||
|
||||
---
|
||||
|
||||
## 11. Open Questions
|
||||
- 현재 PRD 기준 추가 확인이 필요한 사항은 없다.
|
||||
Reference in New Issue
Block a user