docs(creator): 라이브 탭 구현 계획을 추가한다
This commit is contained in:
412
docs/20260617_크리에이터_채널_라이브_탭/plan-task.md
Normal file
412
docs/20260617_크리에이터_채널_라이브_탭/plan-task.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# 크리에이터 채널 라이브 탭 구현 계획/TASK
|
||||||
|
|
||||||
|
> **For agentic workers:** 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.
|
||||||
|
|
||||||
|
**Goal:** `GET /api/v2/creator-channels/{creatorId}/live` 응답을 기반으로 크리에이터 채널의 `라이브` 탭에 현재 라이브, 라이브 다시듣기 목록, 정렬, pagination, 본인 채널 전용 하단 `라이브 시작하기` CTA를 표시한다.
|
||||||
|
|
||||||
|
**Architecture:** 기존 `CreatorChannelActivity`의 `ViewPager2`/`CreatorChannelPagerAdapter` 구조를 유지하고, `CreatorChannelTab.Live`의 placeholder를 신규 `CreatorChannelLiveFragment`로 교체한다. API/Repository/ViewModel/mapper/UI model은 기존 `kr.co.vividnext.sodalive.v2.creator.channel` 패키지 하위에 최소 추가하고, 홈 탭에서 이미 쓰는 `CreatorChannelLiveResponse`, `CreatorChannelAudioContentResponse`, 본인 판정, 라이브 시작 진입 흐름은 가능한 한 재사용한다.
|
||||||
|
|
||||||
|
**Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 전제와 성공 기준
|
||||||
|
- PRD: `docs/20260617_크리에이터_채널_라이브_탭/prd.md`
|
||||||
|
- 기존 채널 컨테이너: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- 기존 탭 adapter: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
|
||||||
|
- 기존 홈 API/DTO/Repository:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeApi.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeRepository.kt`
|
||||||
|
- Figma:
|
||||||
|
- 전체: `290:8945`
|
||||||
|
- Sort-bar: `290:8949`
|
||||||
|
- 현재 진행 중인 라이브: `290:8950`
|
||||||
|
- 라이브 다시듣기 item: `290:8954`, `290:8956`
|
||||||
|
- 정렬 컨텍스트 메뉴: `290:9041`
|
||||||
|
- 본인 채널 하단 CTA: `665:19359`, `665:19371`
|
||||||
|
- 첫 페이지 `page`는 `0`이다.
|
||||||
|
- 다음 페이지는 마지막 성공 응답의 `page + 1`로 요청한다.
|
||||||
|
- `ContentSort` 기본값은 `LATEST`이다.
|
||||||
|
- `isOwned == true`와 `isRented == true`가 동시에 내려오면 `소장중`을 우선 표시한다.
|
||||||
|
- `seriesName`은 라이브 다시듣기 item에 표시하지 않는다.
|
||||||
|
- `isFirstContent`, `isOriginalSeries`는 기존 오디오 item 정책과 동일하게 매핑한다.
|
||||||
|
- drawable 리소스는 기존 파일을 재사용한다.
|
||||||
|
- `ic_new_sort`
|
||||||
|
- `ic_new_shield_small`
|
||||||
|
- `ic_new_player_play`
|
||||||
|
- `ic_new_create_live`
|
||||||
|
- 구현 완료 후 최소 다음 명령을 실행한다.
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"`
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`
|
||||||
|
- `./gradlew :app:mergeDebugResources`
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- `./gradlew :app:ktlintCheck`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Figma 참조 필요 Phase
|
||||||
|
- Phase 1: 부분 참조
|
||||||
|
- 기존 코드 경계와 리소스 존재 여부 확인이 중심이며, Figma는 PRD 기준만 확인한다.
|
||||||
|
- Phase 2: Figma 참조 불필요
|
||||||
|
- API/DTO/Repository/ViewModel 상태 모델은 서버 계약과 기존 코드 패턴을 따른다.
|
||||||
|
- Phase 3: 부분 참조
|
||||||
|
- mapper와 presenter 정책은 PRD와 Figma item variant의 상태 표시를 함께 확인한다.
|
||||||
|
- Phase 4: 필수 참조
|
||||||
|
- `fragment_creator_channel_live.xml`, sort-bar, current live card, replay item layout은 Figma `290:8949`, `290:8950`, `290:8954`, `290:8956`을 기준으로 구현한다.
|
||||||
|
- Phase 5: 필수 참조
|
||||||
|
- 정렬 컨텍스트 메뉴는 Figma `290:9041` 기준으로 구현한다.
|
||||||
|
- Phase 6: 필수 참조
|
||||||
|
- 본인 채널 하단 CTA는 Figma `665:19359`, `665:19371` 기준으로 구현한다.
|
||||||
|
- Phase 7: 부분 참조
|
||||||
|
- 탭 연결, 화면 이동, pagination 동작은 기존 코드 패턴 중심으로 검증한다.
|
||||||
|
- Phase 8: 필수 참조
|
||||||
|
- 최종 수동 화면 검증은 PRD의 모든 Figma 노드와 실제 화면을 대조한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 파일 구조
|
||||||
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
|
||||||
|
- `CreatorChannelTab.Live`를 신규 `CreatorChannelLiveFragment`로 연결한다.
|
||||||
|
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveFragment.kt`
|
||||||
|
- 라이브 탭 UI, adapter, sort menu, pagination, CTA click 연결을 담당한다.
|
||||||
|
- 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveViewModel.kt`
|
||||||
|
- 라이브 탭 API 호출, 정렬 변경, pagination 상태, loading/error/content 상태를 관리한다.
|
||||||
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeApi.kt`
|
||||||
|
- `GET /api/v2/creator-channels/{creatorId}/live` endpoint를 추가하거나, 필요 시 파일명을 공용 API 역할에 맞춰 최소 변경한다.
|
||||||
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeRepository.kt`
|
||||||
|
- 라이브 탭 API 호출 method를 추가하거나, 필요 시 공용 Repository 역할에 맞춰 최소 변경한다.
|
||||||
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt`
|
||||||
|
- `CreatorChannelLiveTabResponse`, `ContentSort`를 추가하고 `CreatorChannelAudioContentResponse`에 라이브 탭 전용 필드 `isAdult`, `isOwned`, `isRented`를 서버 계약에 맞춰 추가한다.
|
||||||
|
- 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelLiveUiModels.kt`
|
||||||
|
- sort option, replay item status, pagination 상태, CTA 노출 여부를 순수 UI model로 정의한다.
|
||||||
|
- 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelLiveMappers.kt`
|
||||||
|
- DTO를 UI model로 변환하고 소장/대여/무료/가격/포인트/19금 상태를 결정한다.
|
||||||
|
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelLiveReplayAdapter.kt`
|
||||||
|
- 라이브 다시듣기 목록 RecyclerView adapter를 담당한다.
|
||||||
|
- 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelLiveSortPopup.kt`
|
||||||
|
- Sort-bar anchor 기준 컨텍스트 메뉴 표시를 담당한다.
|
||||||
|
- 생성: `app/src/main/res/layout/fragment_creator_channel_live.xml`
|
||||||
|
- sort-bar, current live card container, replay RecyclerView, owner CTA를 포함한다.
|
||||||
|
- 생성: `app/src/main/res/layout/item_creator_channel_live_replay.xml`
|
||||||
|
- 라이브 다시듣기 item layout이다.
|
||||||
|
- 생성 후보: `app/src/main/res/layout/view_creator_channel_live_sort_menu.xml`
|
||||||
|
- 정렬 컨텍스트 메뉴를 XML로 분리할 때만 추가한다.
|
||||||
|
- 수정: `app/src/main/res/values/strings.xml`
|
||||||
|
- 정렬 label, `소장중`, `대여중`, `라이브 시작하기`, empty/error 문구를 추가한다.
|
||||||
|
- 수정: `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- 신규 문자열의 다국어 값을 추가한다.
|
||||||
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
|
||||||
|
- 신규 ViewModel/API/Repository binding이 필요하면 추가한다.
|
||||||
|
- 테스트 생성:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveMapperTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLivePaginationTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveViewModelTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveFragmentLayoutTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: 기존 구조 확인과 작업 경계 고정
|
||||||
|
|
||||||
|
- [ ] **Task 1.1: 크리에이터 채널 탭 구조와 본인 판정 경로 확인**
|
||||||
|
- 확인:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeMappers.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt`
|
||||||
|
- 작업:
|
||||||
|
- `CreatorChannelTab.Live`가 현재 placeholder로 연결되는 지점을 확인한다.
|
||||||
|
- 본인 여부(`isOwner`)가 Activity와 탭 Fragment로 전달되는 기존 경로를 확인한다.
|
||||||
|
- 라이브 시작 진입이 기존 `CreatorChannelActivity`의 `onOwnerFabLiveClicked()` 또는 `LiveRoomCreateActivity` 경로를 재사용할 수 있는지 확인한다.
|
||||||
|
- 검증:
|
||||||
|
- 라이브 탭 구현이 홈 탭 section adapter를 변경하지 않고 독립 Fragment로 추가 가능한지 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 1.2: 기존 오디오 item 정책과 drawable/string 리소스 확인**
|
||||||
|
- 확인:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeAudioContentCardView.kt`
|
||||||
|
- `app/src/main/res/layout/item_creator_channel_home_audio_content.xml`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTag.kt`
|
||||||
|
- `app/src/main/res/drawable-mdpi/ic_new_sort.png`
|
||||||
|
- `app/src/main/res/drawable-mdpi/ic_new_shield_small.png`
|
||||||
|
- `app/src/main/res/drawable-mdpi/ic_new_player_play.png`
|
||||||
|
- `app/src/main/res/drawable-mdpi/ic_new_create_live.png`
|
||||||
|
- 작업:
|
||||||
|
- `isFirstContent`, `isOriginalSeries`, point/free/original tag의 기존 표시 정책을 정리한다.
|
||||||
|
- 라이브 다시듣기 item에서 `seriesName`은 표시하지 않는다는 PRD 결정을 재확인한다.
|
||||||
|
- 검증:
|
||||||
|
- 위 4개 drawable 리소스가 존재함을 확인한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: API/DTO/Repository/ViewModel 계약 추가
|
||||||
|
|
||||||
|
- [ ] **Task 2.1: 라이브 탭 DTO와 `ContentSort` 추가**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeModels.kt`
|
||||||
|
- 작업:
|
||||||
|
- `CreatorChannelLiveTabResponse`를 `@Keep`, `@SerializedName` 기반 data class로 추가한다.
|
||||||
|
- `ContentSort` enum에 `LATEST`, `POPULAR`, `OWNED`, `PRICE_HIGH`, `PRICE_LOW`를 추가한다.
|
||||||
|
- 기존 `CreatorChannelAudioContentResponse`에 `isAdult`, `isOwned`, `isRented` 필드를 추가할 때 홈 탭 API 하위 호환성이 깨지지 않는지 확인한다.
|
||||||
|
- 하위 호환이 필요하면 라이브 탭 전용 DTO 분리 또는 nullable/default 정책을 테스트로 고정한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- 기대 결과:
|
||||||
|
- DTO 추가 후 컴파일된다.
|
||||||
|
|
||||||
|
- [ ] **Task 2.2: 라이브 탭 endpoint와 Repository method 추가**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeApi.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelHomeRepository.kt`
|
||||||
|
- 작업:
|
||||||
|
- `@GET("/api/v2/creator-channels/{creatorId}/live")` endpoint를 추가한다.
|
||||||
|
- query parameter `page`, `size`, `sort`를 전달한다.
|
||||||
|
- `sort`는 `ContentSort.name` 또는 Retrofit enum 변환의 기존 정책을 확인해 서버 값이 `LATEST` 등 대문자로 나가도록 한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- 기대 결과:
|
||||||
|
- API/Repository가 기존 Koin graph와 충돌 없이 컴파일된다.
|
||||||
|
|
||||||
|
- [ ] **Task 2.3: ViewModel RED 테스트 작성**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveViewModelTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLivePaginationTest.kt`
|
||||||
|
- 작업:
|
||||||
|
- 최초 로딩이 `page=0`, `sort=LATEST`로 호출되는지 검증한다.
|
||||||
|
- `hasNext == true`일 때 `page + 1`로 다음 페이지를 요청하는지 검증한다.
|
||||||
|
- loading 중 중복 load-more 요청이 막히는지 검증한다.
|
||||||
|
- 정렬 변경 시 목록과 page가 초기화되는지 검증한다.
|
||||||
|
- 선택 중인 정렬을 다시 선택하면 API를 재호출하지 않는지 검증한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLiveViewModelTest"`
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLivePaginationTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- ViewModel 미구현 상태에서 RED 실패한다.
|
||||||
|
|
||||||
|
- [ ] **Task 2.4: `CreatorChannelLiveViewModel` 구현**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveViewModel.kt`
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
|
||||||
|
- 작업:
|
||||||
|
- initial loading, content, empty, error, pagination loading, pagination error 상태를 정의한다.
|
||||||
|
- `page=0`, `size` 기본값, `sort=LATEST` 초기값을 적용한다.
|
||||||
|
- 정렬 변경과 pagination 요청 중복 방지 로직을 구현한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLiveViewModelTest"`
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLivePaginationTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- ViewModel/pagination 테스트가 PASS한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: UI model과 상태 표시 mapper 구현
|
||||||
|
|
||||||
|
- [ ] **Task 3.1: 라이브 다시듣기 표시 정책 RED 테스트 작성**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveMapperTest.kt`
|
||||||
|
- 작업:
|
||||||
|
- `isOwned && isRented` 동시 true면 `소장중` 상태가 선택되는지 검증한다.
|
||||||
|
- `price == 0`이면 무료 tag와 play CTA 상태가 선택되는지 검증한다.
|
||||||
|
- `isAdult == true`이면 `ic_new_shield_small` 표시 상태가 선택되는지 검증한다.
|
||||||
|
- `isPointAvailable == true`이면 point tag 표시 상태가 선택되는지 검증한다.
|
||||||
|
- `seriesName`은 secondary text에 포함되지 않는지 검증한다.
|
||||||
|
- `isFirstContent`, `isOriginalSeries`는 기존 오디오 item 정책과 동일한 tag 상태로 매핑되는지 검증한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLiveMapperTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- mapper 미구현 상태에서 RED 실패한다.
|
||||||
|
|
||||||
|
- [ ] **Task 3.2: UI model/mapper 구현**
|
||||||
|
- 생성 후보:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelLiveUiModels.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelLiveMappers.kt`
|
||||||
|
- 작업:
|
||||||
|
- sort label resource, replay item CTA/status, tag 상태, current live 상태를 UI model로 분리한다.
|
||||||
|
- 가격/포인트/소장중/대여중/play button 상태 우선순위를 순수 함수로 고정한다.
|
||||||
|
- `ContentSort`와 문자열 리소스 매핑을 구현한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLiveMapperTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- mapper 테스트가 PASS한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: 라이브 탭 Fragment와 목록 UI 구현
|
||||||
|
|
||||||
|
- [ ] **Task 4.1: Fragment layout과 replay item layout 추가**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/res/layout/fragment_creator_channel_live.xml`
|
||||||
|
- `app/src/main/res/layout/item_creator_channel_live_replay.xml`
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/res/values/strings.xml`
|
||||||
|
- `app/src/main/res/values-en/strings.xml`
|
||||||
|
- `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- 작업:
|
||||||
|
- Figma 기준 sort-bar, current live card, replay list, owner CTA 영역을 배치한다.
|
||||||
|
- replay item은 88dp 썸네일, title 최대 2줄, duration 1줄, 우측 price/play/status 영역을 포함한다.
|
||||||
|
- `ic_new_shield_small`, `ic_new_player_play`, `ic_new_sort`, `ic_new_create_live`를 layout 또는 binding에서 사용 가능하게 준비한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:mergeDebugResources`
|
||||||
|
- 기대 결과:
|
||||||
|
- 신규 layout/string/drawable 참조가 resource merge에 성공한다.
|
||||||
|
|
||||||
|
- [ ] **Task 4.2: Adapter와 Fragment 구현**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveFragment.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelLiveReplayAdapter.kt`
|
||||||
|
- 작업:
|
||||||
|
- ViewModel 상태를 관찰해 sort-bar, current live card, replay list, loading/empty/error를 갱신한다.
|
||||||
|
- replay item click은 기존 오디오 컨텐츠 상세/재생 진입 정책을 확인해 연결한다.
|
||||||
|
- current live card click은 기존 홈 탭 라이브 진입 정책을 재사용한다.
|
||||||
|
- RecyclerView scroll listener로 하단 접근 시 ViewModel load-more를 호출한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- 기대 결과:
|
||||||
|
- Fragment/adapter가 컴파일된다.
|
||||||
|
|
||||||
|
- [ ] **Task 4.3: Fragment layout 테스트 추가**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveFragmentLayoutTest.kt`
|
||||||
|
- 작업:
|
||||||
|
- `fragment_creator_channel_live.xml`에 sort-bar, RecyclerView, owner CTA가 존재하는지 검증한다.
|
||||||
|
- `item_creator_channel_live_replay.xml`에 19금 badge, point/free tag container, price/play/status 영역이 존재하는지 검증한다.
|
||||||
|
- `ic_new_*` 리소스 참조가 layout 또는 코드 상수로 연결되는지 검증한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLiveFragmentLayoutTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- layout 테스트가 PASS한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: 정렬 컨텍스트 메뉴 구현
|
||||||
|
|
||||||
|
- [ ] **Task 5.1: Sort popup 구현**
|
||||||
|
- 생성 후보:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/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/CreatorChannelLiveFragment.kt`
|
||||||
|
- 작업:
|
||||||
|
- Sort-bar 정렬 영역 아래에 Figma `290:9041` 형태의 컨텍스트 메뉴를 표시한다.
|
||||||
|
- 현재 선택 항목은 focused 배경으로 표시한다.
|
||||||
|
- 메뉴 외부 터치, 같은 정렬 재선택, 새 정렬 선택 시 메뉴를 닫는다.
|
||||||
|
- 새 정렬 선택 시 ViewModel에 정렬 변경을 전달한다.
|
||||||
|
- 검증:
|
||||||
|
- 작은 화면에서 메뉴가 화면 밖으로 벗어나지 않도록 위치 보정 기준을 코드에 남긴다.
|
||||||
|
|
||||||
|
- [ ] **Task 5.2: Sort 동작 테스트 보강**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveViewModelTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveMapperTest.kt`
|
||||||
|
- 작업:
|
||||||
|
- `ContentSort`별 label resource 매핑을 검증한다.
|
||||||
|
- 정렬 변경 시 첫 페이지가 다시 로딩되는지 검증한다.
|
||||||
|
- 같은 정렬 재선택은 API 재호출을 하지 않는지 검증한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"`
|
||||||
|
- 기대 결과:
|
||||||
|
- sort 관련 테스트가 PASS한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: 본인 채널 하단 CTA와 라이브 시작 진입 연결
|
||||||
|
|
||||||
|
- [ ] **Task 6.1: 본인 CTA 노출과 inset 처리 구현**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveFragment.kt`
|
||||||
|
- `app/src/main/res/layout/fragment_creator_channel_live.xml`
|
||||||
|
- 작업:
|
||||||
|
- 본인 채널일 때만 하단 CTA를 표시한다.
|
||||||
|
- 타인 채널 또는 본인 여부 로딩 전에는 CTA를 숨긴다.
|
||||||
|
- CTA 표시 시 RecyclerView 하단 padding을 추가해 마지막 item이 CTA에 가려지지 않도록 한다.
|
||||||
|
- navigation bar bottom inset을 CTA 영역에 반영한다.
|
||||||
|
- 버튼 icon은 `ic_new_create_live`, label은 `라이브 시작하기` 문자열 리소스를 사용한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:mergeDebugResources`
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- 기대 결과:
|
||||||
|
- CTA resource와 inset 코드가 컴파일된다.
|
||||||
|
|
||||||
|
- [ ] **Task 6.2: 기존 라이브 시작 플로우 연결**
|
||||||
|
- 확인:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt`
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveFragment.kt`
|
||||||
|
- 필요 시 `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- 작업:
|
||||||
|
- `CreatorChannelActivity`의 기존 owner FAB live action과 같은 진입 경로를 재사용한다.
|
||||||
|
- 중복 터치를 방지한다.
|
||||||
|
- 라이브 생성 완료 후 기존 홈 refresh/라이브룸 진입 정책과 충돌하지 않도록 처리한다.
|
||||||
|
- 검증:
|
||||||
|
- 라이브 시작 진입 대상 Activity/Fragment와 전달 extra를 계획 문서 해당 Task의 `검증 기록`에 남긴다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 7: 탭 연결과 통합 동작
|
||||||
|
|
||||||
|
- [ ] **Task 7.1: `CreatorChannelTab.Live`를 실제 Fragment로 연결**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
|
||||||
|
- 생성:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt`
|
||||||
|
- 작업:
|
||||||
|
- Live 탭 position에서 `CreatorChannelLiveFragment.newInstance(creatorId)`를 반환한다.
|
||||||
|
- 나머지 탭은 기존 placeholder 정책을 유지한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- Live 탭만 실제 Fragment로 연결되고 나머지 placeholder 정책은 유지된다.
|
||||||
|
|
||||||
|
- [ ] **Task 7.2: DI와 lifecycle 통합**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveFragment.kt`
|
||||||
|
- 작업:
|
||||||
|
- 신규 ViewModel/API/Repository injection이 기존 홈 탭과 충돌하지 않는지 확인한다.
|
||||||
|
- Fragment 재생성 후 정렬, page, 목록 상태가 ViewModel에 유지되는지 확인한다.
|
||||||
|
- 탭 전환 시 중복 최초 API 호출이 발생하지 않도록 초기 로딩 조건을 정리한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"`
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- 기대 결과:
|
||||||
|
- Live 탭 관련 테스트와 컴파일이 PASS한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 8: 최종 검증과 문서 기록
|
||||||
|
|
||||||
|
- [ ] **Task 8.1: 자동 검증 실행**
|
||||||
|
- 실행 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"`
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`
|
||||||
|
- `./gradlew :app:mergeDebugResources`
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- `./gradlew :app:ktlintCheck`
|
||||||
|
- 기대 결과:
|
||||||
|
- 모든 명령이 `BUILD SUCCESSFUL`로 통과한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 실행 일시, 명령, 결과, 기존 경고 여부를 이 Task 아래에 누적 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 8.2: 수동 확인**
|
||||||
|
- 확인 항목:
|
||||||
|
- `라이브` 탭 진입 시 `page=0`, `sort=LATEST`로 최초 조회된다.
|
||||||
|
- Sort-bar에 `전체 {count}`와 현재 정렬 label, `ic_new_sort`가 표시된다.
|
||||||
|
- 정렬 컨텍스트 메뉴가 Sort-bar 아래에 표시되고 선택/닫힘 동작이 정상이다.
|
||||||
|
- `currentLive != null`일 때 현재 라이브 카드가 표시된다.
|
||||||
|
- 라이브 다시듣기 item의 19금/포인트/무료/소장중/대여중/가격 상태가 DTO와 일치한다.
|
||||||
|
- `isOwned && isRented` 동시 true item은 `소장중`으로 표시된다.
|
||||||
|
- `seriesName`은 표시되지 않는다.
|
||||||
|
- `hasNext == true`일 때 하단 스크롤로 다음 페이지가 append된다.
|
||||||
|
- 본인 채널에서만 하단 `라이브 시작하기` CTA가 표시된다.
|
||||||
|
- 타인 채널에서는 하단 CTA가 표시되지 않는다.
|
||||||
|
- CTA가 마지막 item을 가리지 않는다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 실제 확인한 시나리오와 결과를 이 Task 아래에 한국어로 누적 기록한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
- 아직 구현 전 문서 생성 단계이므로 실행한 빌드/테스트 검증은 없다.
|
||||||
294
docs/20260617_크리에이터_채널_라이브_탭/prd.md
Normal file
294
docs/20260617_크리에이터_채널_라이브_탭/prd.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# PRD: 크리에이터 채널 라이브 탭
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
크리에이터 채널의 `라이브` 탭에서 현재 진행 중인 라이브와 라이브 다시듣기 목록을 표시하고, 정렬 및 스크롤 pagination을 제공한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Problem
|
||||||
|
- 크리에이터 채널 상단/탭 구조는 이미 홈 탭 PRD에서 정의되었지만, `라이브` 탭 하위 컨텐츠 조회와 표시 요구사항은 별도 정의가 필요하다.
|
||||||
|
- 사용자는 크리에이터 채널에서 현재 진행 중인 라이브를 빠르게 확인하고, 과거 라이브 다시듣기 컨텐츠를 정렬해 탐색할 수 있어야 한다.
|
||||||
|
- 라이브 다시듣기 item은 가격, 포인트 사용 가능 여부, 19금 콘텐츠, 소장중, 대여중 등 구매/접근 상태를 명확히 표시해야 한다.
|
||||||
|
- 목록이 길어질 수 있으므로 `CreatorChannelLiveTabResponse.hasNext == true`일 때 다음 페이지를 자동 로딩해야 한다.
|
||||||
|
- 정렬 label은 앱 다국어 정책에 맞춰 문자열 리소스로 관리해야 한다.
|
||||||
|
- Sort-bar의 정렬 메뉴 표시 방식은 Figma 컨텍스트 메뉴와 BottomSheet 중 선택이 필요하며, 모바일 UX 흐름에 맞는 기준이 필요하다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
- Figma `290:8945` 기준으로 크리에이터 채널 `라이브` 탭 UI 요구사항을 정의한다.
|
||||||
|
- Sort-bar에는 전체 라이브 다시듣기 수와 현재 정렬 label을 표시한다.
|
||||||
|
- 정렬 기본값은 `LATEST`이며 UI label은 `최신순`이다.
|
||||||
|
- 사용자가 정렬을 변경하면 선택한 `ContentSort` 값으로 `GET /api/v2/creator-channels/{creatorId}/live`를 다시 조회한다.
|
||||||
|
- 정렬 메뉴는 Sort-bar 바로 아래에 표시되는 컨텍스트 메뉴 방식을 채택한다.
|
||||||
|
- 현재 진행 중인 라이브가 있으면 Sort-bar 아래에 현재 라이브 카드로 표시한다.
|
||||||
|
- 라이브 다시듣기 목록은 이미지, title, duration, 가격/포인트/무료/재생/소장중/대여중 상태, 19금 badge를 표시한다.
|
||||||
|
- 응답의 `hasNext`가 `true`이면 현재 `page + 1` 페이지를 추가 로딩한다.
|
||||||
|
- 정렬 label과 상태 문구는 다국어 문자열 리소스로 관리한다.
|
||||||
|
- 로그인 사용자가 해당 크리에이터 본인이면 라이브 탭 하단에 고정된 `라이브 시작하기` 버튼을 표시한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
- 크리에이터 채널 상단 header, title bar, tab-bar 구조 자체를 재설계하지 않는다.
|
||||||
|
- `홈`, `오디오`, `시리즈`, `커뮤니티`, `팬Talk`, `후원` 탭의 상세 구현은 이번 범위에서 제외한다.
|
||||||
|
- 라이브룸 진입, 오디오 상세/결제/대여/소장 플로우 내부 동작 변경은 이번 범위에서 제외한다.
|
||||||
|
- API schema를 임의 변경하거나 서버 응답 필드명을 클라이언트에서 새로 정의하지 않는다.
|
||||||
|
- 정렬 옵션 외 별도 필터, 검색, pull-to-refresh, skeleton/shimmer는 이번 범위에서 제외한다.
|
||||||
|
- BottomSheet 방식의 정렬 메뉴는 이번 범위에서 채택하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Target Users
|
||||||
|
- 크리에이터 채널에서 현재 라이브와 라이브 다시듣기를 탐색하는 앱 사용자.
|
||||||
|
- 구매 가능/보유/대여 상태를 확인한 뒤 라이브 다시듣기를 재생하거나 상세로 이동하려는 앱 사용자.
|
||||||
|
- 본인 크리에이터 채널의 라이브 탭에서 라이브를 시작하려는 크리에이터.
|
||||||
|
- `kr.co.vividnext.sodalive.v2` 하위 크리에이터 채널 탭을 구현/유지보수하는 Android 개발자.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. User Stories
|
||||||
|
- 사용자는 크리에이터 채널의 `라이브` 탭에서 현재 진행 중인 라이브를 바로 확인하고 싶다.
|
||||||
|
- 사용자는 라이브 다시듣기 총 개수와 정렬 기준을 확인하고 싶다.
|
||||||
|
- 사용자는 최신순, 인기순, 소장순, 가격 높은 순, 가격 낮은 순으로 다시듣기 목록을 정렬하고 싶다.
|
||||||
|
- 사용자는 19금 콘텐츠, 포인트 사용 가능 콘텐츠, 무료 콘텐츠, 소장중/대여중 콘텐츠를 목록에서 구분하고 싶다.
|
||||||
|
- 사용자는 목록 하단까지 스크롤하면 다음 페이지가 자연스럽게 이어서 로딩되길 기대한다.
|
||||||
|
- 크리에이터 본인은 본인 채널의 `라이브` 탭에서 하단 고정 버튼으로 라이브 시작 화면에 진입하고 싶다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Core Features
|
||||||
|
|
||||||
|
### Creator Channel Live Tab API
|
||||||
|
`라이브` 탭 진입 및 정렬/추가 로딩 시 크리에이터별 라이브 탭 데이터를 조회한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/live`이다.
|
||||||
|
- Query parameters는 `page`, `size`, `sort`를 사용한다.
|
||||||
|
- `sort`는 `ContentSort` enum 값을 그대로 전달한다.
|
||||||
|
- 최초 조회의 정렬 기본값은 `ContentSort.LATEST`이다.
|
||||||
|
- 최초 조회의 `page` 시작 값은 `0`이다.
|
||||||
|
- 최초 조회 실패, 정렬 변경 실패, 다음 페이지 로딩 실패는 기존 프로젝트의 에러 표시/재시도 패턴을 구현 계획 단계에서 확인해 따른다.
|
||||||
|
- `hasNext == true`일 때 다음 페이지 요청은 현재 응답의 `page + 1` 값을 사용한다.
|
||||||
|
- 중복 pagination 요청이 발생하지 않도록 loading 중 추가 요청을 막아야 한다.
|
||||||
|
- 정렬 변경 시 기존 목록과 page 상태를 초기화하고 첫 페이지부터 다시 조회한다.
|
||||||
|
|
||||||
|
#### Response Contract
|
||||||
|
```kotlin
|
||||||
|
data class CreatorChannelLiveTabResponse(
|
||||||
|
val liveReplayContentCount: Int,
|
||||||
|
val currentLive: CreatorChannelLiveResponse?,
|
||||||
|
val liveReplayContents: List<CreatorChannelAudioContentResponse>,
|
||||||
|
val sort: ContentSort,
|
||||||
|
val page: Int,
|
||||||
|
val size: Int,
|
||||||
|
val hasNext: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelAudioContentResponse(
|
||||||
|
val audioContentId: Long,
|
||||||
|
val title: String,
|
||||||
|
val duration: String?,
|
||||||
|
val imageUrl: String?,
|
||||||
|
val price: Int,
|
||||||
|
val isAdult: Boolean,
|
||||||
|
val isPointAvailable: Boolean,
|
||||||
|
val isFirstContent: Boolean,
|
||||||
|
val seriesName: String?,
|
||||||
|
val isOriginalSeries: Boolean?,
|
||||||
|
val isOwned: Boolean,
|
||||||
|
val isRented: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class ContentSort {
|
||||||
|
LATEST,
|
||||||
|
POPULAR,
|
||||||
|
OWNED,
|
||||||
|
PRICE_HIGH,
|
||||||
|
PRICE_LOW
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- `currentLive == null`이면 현재 라이브 카드 영역을 표시하지 않고 라이브 다시듣기 목록이 Sort-bar 아래에 이어진다.
|
||||||
|
- `liveReplayContents`가 비어 있고 `currentLive == null`이면 기존 앱 패턴에 맞는 empty 상태를 표시한다.
|
||||||
|
- `duration == null`이면 duration 영역은 빈 문자열 노출 대신 숨김 또는 기존 duration placeholder 정책을 따른다.
|
||||||
|
- `imageUrl == null` 또는 이미지 로딩 실패 시 기존 이미지 placeholder 정책을 따른다.
|
||||||
|
- 다음 페이지 응답의 `liveReplayContents`가 비어 있어도 `hasNext` 값 기준으로 이후 로딩 가능 여부를 갱신한다.
|
||||||
|
|
||||||
|
### Sort Bar and Sort Menu
|
||||||
|
Sort-bar는 라이브 다시듣기 총 개수와 현재 정렬 상태를 표시하고, 정렬 메뉴를 연다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- Sort-bar Figma 기준 노드는 `290:8949`이다.
|
||||||
|
- 좌측에는 `전체`와 `liveReplayContentCount`를 표시한다.
|
||||||
|
- 우측에는 현재 정렬 label과 정렬 icon을 표시한다.
|
||||||
|
- 정렬 icon 리소스는 `ic_new_sort`를 사용한다.
|
||||||
|
- 정렬 label은 `ContentSort`에 따라 다국어 문자열 리소스로 표시한다.
|
||||||
|
- 정렬 기본값은 `LATEST`이며 label은 한국어 기준 `최신순`이다.
|
||||||
|
- 정렬 옵션은 아래 순서로 표시한다.
|
||||||
|
|
||||||
|
| ContentSort | 한국어 label |
|
||||||
|
| --- | --- |
|
||||||
|
| `LATEST` | 최신순 |
|
||||||
|
| `POPULAR` | 인기순 |
|
||||||
|
| `OWNED` | 소장순 |
|
||||||
|
| `PRICE_HIGH` | 높은 가격순 |
|
||||||
|
| `PRICE_LOW` | 낮은 가격순 |
|
||||||
|
|
||||||
|
- Figma `290:9041`의 컨텍스트 메뉴를 정렬 메뉴 방식으로 채택한다.
|
||||||
|
- Sort-bar 정렬 영역을 터치하면 해당 UI 아래에 컨텍스트 메뉴를 표시한다.
|
||||||
|
- 현재 선택된 정렬 옵션은 focused 배경으로 표시한다.
|
||||||
|
- 메뉴 외부 영역 터치 또는 정렬 옵션 선택 시 메뉴를 닫는다.
|
||||||
|
- 선택 중인 정렬 옵션을 다시 선택하면 API 재호출 없이 메뉴만 닫는다.
|
||||||
|
|
||||||
|
#### UX Decision
|
||||||
|
- 이번 화면의 정렬 옵션은 5개이며, 사용자가 Sort-bar의 현재 정렬 위치를 기준으로 빠르게 바꾸는 보조 액션이다.
|
||||||
|
- 2026년 모바일 UX 기준으로도 전체 화면 흐름을 끊는 BottomSheet보다, 앵커 위치를 유지하는 작은 컨텍스트 메뉴가 현재 요구에 적합하다.
|
||||||
|
- 따라서 BottomSheet는 옵션 수가 증가하거나 긴 설명/복수 선택/필터 조합이 필요한 후속 범위에서 재검토한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 작은 화면에서 메뉴가 화면 우측 또는 하단을 벗어나지 않도록 위치를 보정한다.
|
||||||
|
- 스크롤 중 메뉴가 열린 상태에서 화면이 이동하면 메뉴 위치가 Sort-bar와 어긋나지 않도록 닫거나 재배치한다.
|
||||||
|
- 다국어 label 길이가 길어져도 Sort-bar text와 icon이 겹치지 않아야 한다.
|
||||||
|
|
||||||
|
### Current Live Card
|
||||||
|
현재 진행 중인 라이브가 있으면 Sort-bar 아래에 라이브 카드로 표시한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- 현재 라이브 Figma 기준 노드는 `290:8950`이다.
|
||||||
|
- `currentLive != null`일 때만 표시한다.
|
||||||
|
- 카드에는 `LIVE` badge, 라이브 경과/시간 텍스트, 라이브 제목, 가격을 표시한다.
|
||||||
|
- 가격 표시는 기존 can/point/cash icon 및 가격 표시 정책을 구현 계획 단계에서 확인해 재사용한다.
|
||||||
|
- 카드 터치 시 라이브룸 또는 라이브 상세로 이동하는 기존 크리에이터 채널 홈 탭 정책이 있으면 동일하게 따른다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 라이브 제목이 긴 경우 한 줄 말줄임 처리한다.
|
||||||
|
- 가격이 0인 경우 무료 표시 정책을 기존 라이브/오디오 카드와 맞춘다.
|
||||||
|
- `currentLive`의 세부 DTO는 크리에이터 채널 홈 탭에서 이미 사용하는 `CreatorChannelLiveResponse`와 호환되는지 구현 계획 단계에서 확인한다.
|
||||||
|
|
||||||
|
### Live Replay List
|
||||||
|
라이브 다시듣기 목록은 Figma item variant를 기준으로 오디오 컨텐츠 item을 표시한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- 라이브 다시듣기 item Figma 기준 노드는 `290:8954`, `290:8956` 및 전체 화면 내 `290:8952`~`290:8957`이다.
|
||||||
|
- 각 item은 `imageUrl`, `title`, `duration`을 표시한다.
|
||||||
|
- `isAdult == true`이면 이미지 우상단에 19금 badge를 표시한다.
|
||||||
|
- 19금 badge의 방패 이미지 리소스는 `ic_new_shield_small`을 사용한다.
|
||||||
|
- `isPointAvailable == true`이면 이미지 하단 tag 영역에 point tag를 표시한다.
|
||||||
|
- `price == 0`이면 무료 tag와 우측 재생 버튼을 표시한다.
|
||||||
|
- `price > 0`이고 소장/대여 상태가 아니면 가격을 표시한다.
|
||||||
|
- `isOwned == true`이면 구매/가격 CTA 대신 우측 재생 버튼과 `소장중` 상태를 표시한다.
|
||||||
|
- `isRented == true`이면 우측 재생 버튼과 `대여중` 상태를 표시한다.
|
||||||
|
- `isOwned == true`와 `isRented == true`가 동시에 내려오면 `소장중`을 우선 표시한다.
|
||||||
|
- 무료/소장중/대여중 콘텐츠의 우측 재생 버튼 icon 리소스는 `ic_new_player_play`를 사용한다.
|
||||||
|
- `seriesName`은 라이브 다시듣기 item에 표시하지 않는다.
|
||||||
|
- `isFirstContent`와 `isOriginalSeries`는 기존 오디오 item 정책과 동일하게 매핑한다.
|
||||||
|
- item 터치 시 오디오 컨텐츠 상세 또는 재생 진입은 기존 오디오 컨텐츠 item 정책을 재사용한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- `isOwned == true`와 `isRented == true`가 동시에 내려오는 경우 `소장중`을 대여중보다 상위 상태로 본다.
|
||||||
|
- `isPointAvailable == true`이면서 `isAdult == true`인 경우 point tag와 19금 badge가 동시에 표시되어야 한다.
|
||||||
|
- `title`이 긴 경우 Figma처럼 최대 2줄까지 표시하고 이후 말줄임 처리한다.
|
||||||
|
- `duration`이 긴 경우 한 줄 말줄임 처리한다.
|
||||||
|
|
||||||
|
### Owner Live Start CTA
|
||||||
|
로그인 사용자가 해당 크리에이터 본인인 경우 라이브 탭 하단에 고정된 라이브 시작 버튼을 표시한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- 본인 채널 라이브 탭 Figma 기준 노드는 `665:19359`이다.
|
||||||
|
- 하단 CTA Figma 기준 노드는 `665:19371`이다.
|
||||||
|
- 로그인 사용자가 현재 크리에이터 채널의 본인이면 하단 고정 CTA 영역을 표시한다.
|
||||||
|
- 로그인 사용자가 현재 크리에이터 채널의 본인이 아니면 하단 고정 CTA 영역을 표시하지 않는다.
|
||||||
|
- CTA 영역은 화면 하단에 고정하고, 목록 스크롤과 함께 움직이지 않는다.
|
||||||
|
- CTA 영역 배경은 black이며, 내부 버튼은 Figma 기준 cyan capsule 형태를 따른다.
|
||||||
|
- 버튼 label은 `라이브 시작하기`이며 다국어 문자열 리소스로 관리한다.
|
||||||
|
- 버튼 icon은 `ic_new_create_live` drawable 리소스를 사용한다.
|
||||||
|
- 버튼 터치 시 기존 라이브 시작/생성 진입 플로우로 이동한다.
|
||||||
|
- CTA가 표시되는 경우 목록 마지막 item이 CTA에 가려지지 않도록 하단 padding 또는 inset을 추가한다.
|
||||||
|
- Android gesture navigation, soft navigation bar, display cutout 환경에서 CTA가 system navigation 영역과 겹치지 않도록 bottom inset을 반영한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 본인 여부 판정 데이터가 아직 로딩 중이면 CTA를 먼저 노출하지 않는다.
|
||||||
|
- 본인 여부 판정 실패 또는 API 실패 상태에서는 기존 에러 상태 정책을 따르고 CTA를 임의로 노출하지 않는다.
|
||||||
|
- 라이브 시작 플로우 진입 중 중복 터치를 막는다.
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
라이브 다시듣기 목록은 스크롤 하단 접근 시 다음 페이지를 로딩한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `CreatorChannelLiveTabResponse.hasNext == true`일 때만 다음 페이지를 요청한다.
|
||||||
|
- 다음 페이지는 마지막 성공 응답의 `page + 1`로 요청한다.
|
||||||
|
- 다음 페이지 로딩 중에는 추가 page 요청을 중복으로 보내지 않는다.
|
||||||
|
- 다음 페이지 성공 시 기존 `liveReplayContents` 뒤에 append한다.
|
||||||
|
- 다음 페이지 실패 시 기존 목록은 유지하고, 기존 프로젝트 패턴에 맞는 retry UI 또는 toast/snackbar 정책을 따른다.
|
||||||
|
- 정렬 변경 시 pagination 상태를 초기화한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 빠른 스크롤로 load-more trigger가 반복 발생해도 page가 중복 append되지 않아야 한다.
|
||||||
|
- Fragment/View 재생성 후 현재 목록, 정렬, page 상태는 ViewModel 상태 보존 정책에 따라 유지되어야 한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. UX / UI Expectations
|
||||||
|
- 전체 배경은 Figma 기준 black이다.
|
||||||
|
- Sort-bar 높이, 좌우 여백, 텍스트 스타일은 Figma `290:8949`를 따른다.
|
||||||
|
- 현재 라이브 카드는 cyan 계열 gradient capsule 형태를 유지한다.
|
||||||
|
- 라이브 다시듣기 item은 88dp 정사각 썸네일, title 영역, 우측 CTA/status 영역의 가로형 목록 구조를 따른다.
|
||||||
|
- 컨텍스트 메뉴는 dark surface, 14dp radius, border, focused item 배경 차이를 유지한다.
|
||||||
|
- 정렬 메뉴는 BottomSheet처럼 화면 하단에서 올라오지 않고 Sort-bar 정렬 영역 아래에 떠야 한다.
|
||||||
|
- 본인 채널의 `라이브 시작하기` 버튼은 Figma `665:19359`처럼 하단 고정 CTA로 표시하고, 목록 컨텐츠 위를 덮지 않도록 스크롤 하단 여백을 확보한다.
|
||||||
|
- 다국어 문자열 길이 증가 시 title, 정렬 label, 상태 문구가 겹치지 않도록 말줄임 또는 최소 폭 정책을 적용한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Technical Constraints
|
||||||
|
- Android Gradle 단일 `:app` 모듈에서 구현한다.
|
||||||
|
- 신규 `Fragment`, `ViewModel`, adapter, DTO, Repository 등은 `kr.co.vividnext.sodalive.v2` 하위에 작성한다.
|
||||||
|
- 크리에이터 채널 홈 탭에서 이미 정의된 공통 모델/컴포넌트가 있으면 우선 재사용한다.
|
||||||
|
- API DTO는 서버 계약과 동일한 필드명을 사용한다.
|
||||||
|
- `ContentSort`는 API 전송 값과 UI label을 분리하고, UI label은 문자열 리소스로 관리한다.
|
||||||
|
- 구현 시 `ic_new_shield_small`, `ic_new_player_play`, `ic_new_sort` 리소스 존재 여부를 먼저 확인하고 기존 drawable 리소스를 재사용한다.
|
||||||
|
- 본인 채널 판정은 크리에이터 채널 홈 탭 또는 공통 채널 컨테이너에서 사용하는 기존 본인 페이지 판정 값을 우선 재사용한다.
|
||||||
|
- `BuildConfig` 값이나 토큰/URL 같은 민감값은 로그, Toast, crash message에 노출하지 않는다.
|
||||||
|
- 구현 전 `docs/20260617_크리에이터_채널_라이브_탭/plan-task.md`를 작성한 뒤 해당 계획에 따라 최소 구현한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Metrics
|
||||||
|
- `라이브` 탭 최초 진입 시 첫 페이지 API가 1회 호출된다.
|
||||||
|
- 정렬 변경 시 선택한 `ContentSort` 값으로 첫 페이지 API가 1회 재호출된다.
|
||||||
|
- `hasNext == true` 상태에서 목록 하단 접근 시 `page + 1` API가 호출된다.
|
||||||
|
- `hasNext == false` 이후에는 추가 page API가 호출되지 않는다.
|
||||||
|
- 라이브 다시듣기 item의 가격/포인트/19금/소장중/대여중 표시가 DTO 값과 일치한다.
|
||||||
|
- 본인 크리에이터 채널에서만 하단 `라이브 시작하기` CTA가 표시된다.
|
||||||
|
- 타인 크리에이터 채널에서는 하단 `라이브 시작하기` CTA가 표시되지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Open Questions
|
||||||
|
- `CreatorChannelLiveResponse`의 정확한 필드 계약은 홈 탭 DTO와 동일한지 구현 계획 단계에서 확인한다.
|
||||||
|
- `라이브 시작하기` 버튼 터치 시 진입할 기존 라이브 시작 플로우의 Activity/Fragment는 구현 계획 단계에서 기존 코드 기준으로 확인한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Confirmed Decisions
|
||||||
|
- `page` query parameter의 시작 값은 `0`이다.
|
||||||
|
- 다음 페이지는 응답의 `page + 1`을 사용한다.
|
||||||
|
- `isOwned == true`와 `isRented == true`가 동시에 내려오는 경우 `소장중`을 우선 표시한다.
|
||||||
|
- `seriesName`은 라이브 다시듣기 item에 표시하지 않는다.
|
||||||
|
- `isFirstContent`, `isOriginalSeries`는 기존 오디오 item 정책과 동일하게 매핑한다.
|
||||||
|
- `라이브 시작하기` 버튼 icon drawable 리소스명은 `ic_new_create_live`이다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
- 전체 Figma: `290:8945`
|
||||||
|
- Sort-bar Figma: `290:8949`
|
||||||
|
- 현재 진행 중인 라이브 Figma: `290:8950`
|
||||||
|
- 라이브 다시듣기 item Figma: `290:8954`, `290:8956`
|
||||||
|
- 정렬 컨텍스트 메뉴 Figma: `290:9041`
|
||||||
|
- 본인 채널 라이브 탭 및 하단 CTA Figma: `665:19359`, `665:19371`
|
||||||
|
- 기존 크리에이터 채널 홈 탭 PRD: `docs/20260611_크리에이터_채널_홈_탭/prd.md`
|
||||||
|
- PRD template: `docs/prd/sample-prd.md`
|
||||||
|
- 문서 규칙: `docs/agent-guides/work-plan-docs.md`
|
||||||
Reference in New Issue
Block a user