docs(creator): 시리즈 탭 구현 계획을 기록한다
This commit is contained in:
534
docs/20260620_크리에이터_채널_시리즈_탭/plan-task.md
Normal file
534
docs/20260620_크리에이터_채널_시리즈_탭/plan-task.md
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
# 크리에이터 채널 시리즈 탭 구현 계획/TASK
|
||||||
|
|
||||||
|
> **For agentic workers:** 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.
|
||||||
|
|
||||||
|
**Goal:** `GET /api/v2/creator-channels/{creatorId}/series` 응답을 기반으로 크리에이터 채널의 `시리즈` 탭에 정렬, 시리즈 목록, 조건부 소장 진행 정보, empty 상태와 pagination을 표시한다.
|
||||||
|
|
||||||
|
**Architecture:** 기존 `CreatorChannelActivity`의 `ViewPager2`/`CreatorChannelPagerAdapter` 구조를 유지하고, `CreatorChannelTab.Series`의 placeholder를 신규 `CreatorChannelSeriesFragment`로 교체한다. 시리즈 탭 전용 Fragment/ViewModel/DTO/mapper/adapter는 `kr.co.vividnext.sodalive.v2.creator.channel.series` 하위에 두되, API/Repository는 기존 채널 공통 `CreatorChannelApi`/`CreatorChannelRepository`에 endpoint만 추가한다. 정렬 UI는 오디오 탭에서 사용하는 `CreatorChannelSortPopup`과 `ContentSort.toLabelResId()`를 재사용하고, 목록 하단 감지와 ViewPager 높이 갱신은 기존 Live/Audio 탭 패턴에 Series를 추가한다.
|
||||||
|
|
||||||
|
**Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 전제와 성공 기준
|
||||||
|
- PRD: `docs/20260620_크리에이터_채널_시리즈_탭/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/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/CreatorChannelActivity.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/detail/SeriesDetailActivity.kt`
|
||||||
|
- `Constants.EXTRA_SERIES_ID`
|
||||||
|
- 기존 오디오 탭 참조:
|
||||||
|
- `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`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/model/CreatorChannelAudioMappers.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelSortPopup.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelSortModels.kt`
|
||||||
|
- 기존 홈 시리즈 item 참조:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSeriesCardView.kt`
|
||||||
|
- `app/src/main/res/layout/item_creator_channel_home_series.xml`
|
||||||
|
- `app/src/main/res/layout/item_creator_channel_home_series_content.xml`
|
||||||
|
- Figma:
|
||||||
|
- 전체: `290:9031`
|
||||||
|
- 시리즈 item: `290:9036`
|
||||||
|
- 시리즈 콘텐츠 소장률: `290:9038`
|
||||||
|
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/series`이다.
|
||||||
|
- 첫 페이지 `page`는 `0`, 기본 `size`는 `20`, 기본 `sort`는 `ContentSort.LATEST`이다.
|
||||||
|
- 정렬 옵션은 `ContentSort.LATEST`, `ContentSort.POPULAR`, `ContentSort.OWNED`, `ContentSort.PRICE_HIGH`, `ContentSort.PRICE_LOW` 5개만 표시한다.
|
||||||
|
- Figma에 보이는 `추천순`은 표시하지 않는다.
|
||||||
|
- `coverImageUrl`은 시리즈 썸네일 이미지로 사용한다.
|
||||||
|
- 시리즈 item 우측의 `전체소장`/play button 영역은 표시하지 않는다.
|
||||||
|
- 썸네일처럼 크기 제한이 필요한 영역 외에는 불필요한 고정 width/height를 만들지 않는다.
|
||||||
|
- 내 채널이 아닌 경우에만 `purchasedContentCount`, `paidContentCount`, `purchasedPaidContentRate` 기반 소장 진행 정보를 표시한다.
|
||||||
|
- 내 채널인 경우 item에는 제목, 발행 요일, 총 콘텐츠 수, 연재/완결 상태만 표시한다.
|
||||||
|
- 시리즈 empty 문구는 다음 다국어 문자열 리소스로 제공한다.
|
||||||
|
- 한국어: `크리에이터가 시리즈를 준비 중입니다.\n기대해 주세요!`
|
||||||
|
- 영어: `The creator is preparing a series.\nPlease look forward to it!`
|
||||||
|
- 일본어: `クリエイターがシリーズを準備中です。\n楽しみにお待ちください!`
|
||||||
|
- 구현 완료 후 최소 다음 명령을 실행한다.
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.*"`
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Series*"`
|
||||||
|
- `./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: 제한 참조
|
||||||
|
- 기존 코드 경계, 시리즈 상세 진입, 공통 sort/pagination 패턴 확인이 중심이며 Figma는 PRD 기준만 확인한다.
|
||||||
|
- Phase 2: Figma 참조 불필요
|
||||||
|
- API/DTO/Repository/ViewModel 상태 모델은 서버 계약과 기존 오디오 탭 패턴을 따른다.
|
||||||
|
- Phase 3: 제한 참조
|
||||||
|
- mapper는 PRD와 Figma item variant의 정보 표시/소장률 조건을 함께 확인한다.
|
||||||
|
- Phase 4: 필수 참조
|
||||||
|
- Sort-bar, empty, 시리즈 item layout, 우측 버튼 제거 후 info 영역 확장은 Figma `290:9031`, `290:9036`, `290:9038` 기준으로 구현한다.
|
||||||
|
- Phase 5: 제한 참조
|
||||||
|
- 탭 연결, pagination, navigation, ViewPager 높이 갱신은 기존 코드 패턴 중심으로 검증한다.
|
||||||
|
- Phase 6: 필수 참조
|
||||||
|
- 최종 수동 화면 검증은 PRD의 모든 Figma 노드와 실제 화면을 대조한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 파일 구조
|
||||||
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
|
||||||
|
- `CreatorChannelTab.Series`를 신규 `CreatorChannelSeriesFragment`로 연결한다.
|
||||||
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- `CreatorChannelSeriesFragment.Host` 구현, 시리즈 탭 선택 시 최초 로드, pagination trigger, ViewPager 높이 갱신, 시리즈 상세 이동을 연결한다.
|
||||||
|
- 수정: `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/series/data/CreatorChannelSeriesTabResponse.kt`
|
||||||
|
- `CreatorChannelSeriesTabResponse`, `CreatorChannelSeriesResponse`를 정의한다.
|
||||||
|
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesViewModel.kt`
|
||||||
|
- 최초 조회, 정렬 변경, retry, pagination, loading/error/empty/content 상태를 관리한다.
|
||||||
|
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/model/CreatorChannelSeriesUiModels.kt`
|
||||||
|
- series item, possession progress, 화면 상태 UI model을 정의한다.
|
||||||
|
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/model/CreatorChannelSeriesMappers.kt`
|
||||||
|
- DTO를 UI model로 변환하고 subtitle, original/adult tag, owner별 progress 표시 여부를 결정한다.
|
||||||
|
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesFragment.kt`
|
||||||
|
- 시리즈 탭 UI, adapter, 공통 sort popup, retry, pagination error toast, host callback 연결을 담당한다.
|
||||||
|
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/ui/CreatorChannelSeriesAdapter.kt`
|
||||||
|
- 시리즈 목록 RecyclerView adapter를 담당한다.
|
||||||
|
- 생성: `app/src/main/res/layout/fragment_creator_channel_series.xml`
|
||||||
|
- Sort-bar, RecyclerView, empty/error/retry 영역을 포함한다.
|
||||||
|
- 생성: `app/src/main/res/layout/item_creator_channel_series.xml`
|
||||||
|
- Figma 시리즈 item을 구현한다. 우측 버튼 영역은 포함하지 않는다.
|
||||||
|
- 수정: `app/src/main/res/values/strings.xml`
|
||||||
|
- 시리즈 탭 empty/error/retry/상태/소장률 문구를 추가한다.
|
||||||
|
- 수정: `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- 신규 empty/error/retry/상태 문구의 다국어 값을 추가한다.
|
||||||
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
|
||||||
|
- `CreatorChannelSeriesViewModel` binding을 추가한다.
|
||||||
|
- 테스트 생성:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesMapperTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesViewModelTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesPaginationTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesFragmentLayoutTest.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/audio/CreatorChannelAudioFragment.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModel.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelSortModels.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelSortPopup.kt`
|
||||||
|
- 작업:
|
||||||
|
- Series 탭도 `CreatorChannelSortPopup`을 그대로 사용한다.
|
||||||
|
- sort option은 `ContentSort.entries`를 그대로 쓰되 enum 5개만 노출되는지 확인한다.
|
||||||
|
- 오디오 탭의 `loadMore`, `requestGeneration`, `paginationErrorMessage`, `consumePaginationErrorMessage` 패턴을 시리즈 ViewModel에 동일하게 적용한다.
|
||||||
|
- 검증:
|
||||||
|
- `rg -n "CreatorChannelSortPopup|toLabelResId|loadMore|paginationErrorMessage" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel`
|
||||||
|
- 기대 결과: 공통 sort popup과 오디오 pagination 패턴이 확인된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 1.2: 시리즈 상세 진입 경로 확인**
|
||||||
|
- 확인:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/detail/SeriesDetailActivity.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt`
|
||||||
|
- 작업:
|
||||||
|
- 기존 `onSeriesClicked(series)`가 `SeriesDetailActivity`에 `Constants.EXTRA_SERIES_ID`를 전달하는지 확인한다.
|
||||||
|
- 시리즈 탭 item 클릭은 신규 경로를 만들지 않고 같은 Activity method를 재사용한다.
|
||||||
|
- 검증:
|
||||||
|
- `rg -n "SeriesDetailActivity|EXTRA_SERIES_ID|onSeriesClicked" app/src/main/java`
|
||||||
|
- 기대 결과: 기존 시리즈 상세 진입점이 확인된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 1.3: Series 탭 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.Series`가 현재 `CreatorChannelPlaceholderFragment`로 연결되는지 확인한다.
|
||||||
|
- 신규 `CreatorChannelSeriesFragment.newInstance(creatorId)`로 교체할 수 있는지 확인한다.
|
||||||
|
- 검증:
|
||||||
|
- `CreatorChannelPagerAdapterTest`에 Series 탭 연결 테스트를 추가할 준비가 되었는지 기록한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 1.4: 기존 시리즈 썸네일/tag 리소스 확인**
|
||||||
|
- 확인:
|
||||||
|
- `app/src/main/res/layout/item_creator_channel_home_series.xml`
|
||||||
|
- `app/src/main/res/layout/item_creator_channel_home_series_content.xml`
|
||||||
|
- `app/src/main/res/layout/view_series_original_tag.xml`
|
||||||
|
- `app/src/main/res/drawable`
|
||||||
|
- 작업:
|
||||||
|
- original tag와 adult tag를 기존 리소스로 표현할 수 있는지 확인한다.
|
||||||
|
- image placeholder는 기존 시리즈/오디오 콘텐츠 item의 placeholder 정책을 확인해 따른다.
|
||||||
|
- 검증:
|
||||||
|
- `rg -n "view_series_original_tag|isAdult|adult|original" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel app/src/main/res/layout app/src/main/res/drawable`
|
||||||
|
- 기대 결과: 기존 tag/placeholder 재사용 경계가 확인된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: API/DTO/Repository/ViewModel 계약 추가
|
||||||
|
|
||||||
|
- [ ] **Task 2.1: 시리즈 탭 DTO 추가**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/data/CreatorChannelSeriesTabResponse.kt`
|
||||||
|
- 작업:
|
||||||
|
- `@Keep`, `@SerializedName` 기반으로 `CreatorChannelSeriesTabResponse`, `CreatorChannelSeriesResponse`를 추가한다.
|
||||||
|
- `ContentSort`는 기존 `kr.co.vividnext.sodalive.v2.common.data.ContentSort`를 import해 사용한다.
|
||||||
|
- `coverImageUrl` 필드를 포함한다.
|
||||||
|
- 홈 API의 `CreatorChannelSeriesResponse`와 이름이 충돌하지 않도록 package 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}/series")` endpoint를 추가한다.
|
||||||
|
- query parameter `sort`, `page`, `size`를 전달한다.
|
||||||
|
- Repository method는 `getSeries(creatorId, page, size, sort, token)` 형태로 둔다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- 기대 결과:
|
||||||
|
- API/Repository 추가 후 기존 Koin graph와 충돌 없이 컴파일된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 2.3: ViewModel RED 테스트 작성**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesViewModelTest.kt`
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesPaginationTest.kt`
|
||||||
|
- 테스트 케이스:
|
||||||
|
- 최초 로딩이 `page=0`, `size=20`, `sort=LATEST`로 호출된다.
|
||||||
|
- 정렬 변경 시 `page=0`, 선택된 sort로 재조회된다.
|
||||||
|
- 같은 정렬을 다시 선택하면 API를 재호출하지 않는다.
|
||||||
|
- `hasNext == true`일 때 다음 페이지는 마지막 응답의 `page + 1`로 요청한다.
|
||||||
|
- load-more 요청에는 현재 sort와 `size=20`을 유지한다.
|
||||||
|
- loading 중 중복 load-more 요청은 무시된다.
|
||||||
|
- 다음 페이지 성공 시 기존 series 뒤에 append한다.
|
||||||
|
- 다음 페이지 실패 시 기존 목록은 유지하고 pagination error message만 설정한다.
|
||||||
|
- `seriesCount == 0`이면 `Empty` 상태가 된다.
|
||||||
|
- 표시 가능한 series가 0개이면 `Empty` 상태가 된다.
|
||||||
|
- 내 채널이면 item progress UI model이 `null`이다.
|
||||||
|
- 내 채널이 아니고 progress 관련 nullable field가 모두 있으면 progress UI model이 생성된다.
|
||||||
|
- 내 채널이 아니어도 progress 관련 nullable field 중 하나라도 `null`이면 progress UI model이 `null`이다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesViewModelTest"`
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesPaginationTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- production 구현 전 `CreatorChannelSeriesViewModel` 미구현으로 RED 실패한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 2.4: `CreatorChannelSeriesViewModel` 구현**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesViewModel.kt`
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
|
||||||
|
- 작업:
|
||||||
|
- `DEFAULT_PAGE_SIZE = 20`, `FIRST_PAGE = 0`, 기본 `selectedSort = ContentSort.LATEST`로 둔다.
|
||||||
|
- `loadSeries(creatorId, isOwner)`는 같은 creatorId/isOwner로 이미 상태가 있으면 중복 최초 조회를 막는다.
|
||||||
|
- `changeSort(sort)`는 같은 sort이면 API를 재호출하지 않는다.
|
||||||
|
- `retrySeries()`는 현재 sort로 첫 페이지를 다시 조회한다.
|
||||||
|
- `loadMore()`는 content 상태, `hasNext`, `isLoadingMore`, creatorId를 확인해 중복 요청을 막는다.
|
||||||
|
- `requestGeneration`으로 오래된 응답이 최신 상태를 덮어쓰지 않게 한다.
|
||||||
|
- 성공 응답의 `seriesCount == 0` 또는 표시 가능한 item이 0개이면 `Empty` 상태로 전환한다.
|
||||||
|
- pagination 실패는 기존 content를 유지하고 `paginationErrorMessage`에만 반영한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesViewModelTest"`
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesPaginationTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- ViewModel 테스트가 GREEN이다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Mapper/UI model 계약 추가
|
||||||
|
|
||||||
|
- [ ] **Task 3.1: Mapper RED 테스트 작성**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesMapperTest.kt`
|
||||||
|
- 테스트 케이스:
|
||||||
|
- `isProceeding == true`이면 subtitle에 `연재`가 포함된다.
|
||||||
|
- `isProceeding == false`이면 subtitle에 `완결`이 포함된다.
|
||||||
|
- `publishedDaysOfWeek`, `contentCount`, 진행 상태를 `매주 월 • 총 45화 • 연재` 형식으로 조합한다.
|
||||||
|
- `publishedDaysOfWeek`가 blank이면 빈 bullet 없이 `총 45화 • 연재` 형식으로 조합한다.
|
||||||
|
- `isOriginal == true`이면 original tag 표시 flag가 true이다.
|
||||||
|
- `isAdult == true`이면 adult tag 표시 flag가 true이다.
|
||||||
|
- 내 채널이면 progress가 생성되지 않는다.
|
||||||
|
- 내 채널이 아니고 `purchasedContentCount=12`, `paidContentCount=45`, `purchasedPaidContentRate=40`이면 progress가 생성된다.
|
||||||
|
- rate가 0 미만이면 progress bar percent는 0으로 clamp된다.
|
||||||
|
- rate가 100 초과이면 progress bar percent는 100으로 clamp된다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesMapperTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- production mapper 미구현으로 RED 실패한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 3.2: UI model과 mapper 구현**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/model/CreatorChannelSeriesUiModels.kt`
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/model/CreatorChannelSeriesMappers.kt`
|
||||||
|
- 작업:
|
||||||
|
- `CreatorChannelSeriesItemUiModel`에는 `seriesId`, `title`, `subtitle`, `coverImageUrl`, `showOriginalTag`, `showAdultBadge`, `progress`를 둔다.
|
||||||
|
- `CreatorChannelSeriesProgressUiModel`에는 `purchasedCount`, `paidCount`, `ratePercent`, `progressScale`를 둔다.
|
||||||
|
- progress는 내 채널이면 생성하지 않는다.
|
||||||
|
- progress는 nullable field가 모두 있을 때만 생성한다.
|
||||||
|
- progress bar 표시값은 `purchasedPaidContentRate / 100f`를 0f..1f로 clamp한다.
|
||||||
|
- subtitle은 blank segment를 제외하고 ` • `로 join한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesMapperTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- Mapper 테스트가 GREEN이다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Fragment, Adapter, XML UI 구현
|
||||||
|
|
||||||
|
- [ ] **Task 4.1: Fragment/Layout RED 테스트 작성**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesFragmentLayoutTest.kt`
|
||||||
|
- 테스트 케이스:
|
||||||
|
- `fragment_creator_channel_series.xml`이 존재한다.
|
||||||
|
- `item_creator_channel_series.xml`이 존재한다.
|
||||||
|
- fragment layout에 Sort-bar, total count, sort label, RecyclerView, empty, error, retry view id가 존재한다.
|
||||||
|
- item layout에 thumbnail, title, subtitle, original tag, adult badge, progress container, progress count, progress percent, progress fill id가 존재한다.
|
||||||
|
- item layout source에 `전체소장` 또는 play button id/text가 포함되지 않는다.
|
||||||
|
- empty 문자열 resource key가 한국어/영어/일본어 파일에 모두 존재한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesFragmentLayoutTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- layout/string 미구현으로 RED 실패한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 4.2: 문자열 리소스 추가**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/res/values/strings.xml`
|
||||||
|
- `app/src/main/res/values-en/strings.xml`
|
||||||
|
- `app/src/main/res/values-ja/strings.xml`
|
||||||
|
- 작업:
|
||||||
|
- `creator_channel_series_empty_message`를 3개 언어에 추가한다.
|
||||||
|
- `creator_channel_series_error_message`, `creator_channel_series_retry_button`, `creator_channel_series_status_proceeding`, `creator_channel_series_status_completed`, `creator_channel_series_total_content_count`, `creator_channel_series_progress_count`, `creator_channel_series_progress_percent`를 추가한다.
|
||||||
|
- 기존 공통 문자열이 있으면 중복 생성하지 않고 재사용한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:mergeDebugResources`
|
||||||
|
- 기대 결과:
|
||||||
|
- 신규 문자열 리소스 병합이 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 4.3: 시리즈 탭 fragment layout 작성**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/res/layout/fragment_creator_channel_series.xml`
|
||||||
|
- 작업:
|
||||||
|
- 오디오 탭의 sort bar 구조를 참고하되 theme tab과 audio rate card는 만들지 않는다.
|
||||||
|
- 좌측 `전체` label과 `seriesCount` TextView를 둔다.
|
||||||
|
- 우측 sort label과 `ic_new_sort` ImageView를 둔다.
|
||||||
|
- RecyclerView는 vertical list로 사용한다.
|
||||||
|
- empty 영역에는 `creator_channel_series_empty_message`를 표시한다.
|
||||||
|
- error TextView와 retry Button은 기존 오디오 탭 error/retry 패턴을 따른다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:mergeDebugResources`
|
||||||
|
- 기대 결과:
|
||||||
|
- ViewBinding `FragmentCreatorChannelSeriesBinding`이 생성된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 4.4: 시리즈 item layout 작성**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/res/layout/item_creator_channel_series.xml`
|
||||||
|
- 작업:
|
||||||
|
- 썸네일은 Figma 기준 `122dp x 172dp`, radius 14dp 형태로 제한한다.
|
||||||
|
- item root는 `match_parent` width와 `wrap_content` height를 사용한다.
|
||||||
|
- 좌측 썸네일 외 info/progress 영역은 `0dp` width + weight 또는 ConstraintLayout constraint로 남은 공간을 채운다.
|
||||||
|
- 우측 `전체소장`/play button 영역은 만들지 않는다.
|
||||||
|
- original tag는 기존 `view_series_original_tag` 또는 기존 equivalent resource를 재사용한다.
|
||||||
|
- adult badge는 기존 adult icon/background 정책을 재사용한다.
|
||||||
|
- progress container는 내 채널이거나 progress가 null이면 adapter에서 `GONE` 처리할 수 있게 분리한다.
|
||||||
|
- progress fill은 scaleX 방식으로 0f..1f를 적용할 수 있게 pivot start 기준 구조로 만든다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:mergeDebugResources`
|
||||||
|
- 기대 결과:
|
||||||
|
- ViewBinding `ItemCreatorChannelSeriesBinding`이 생성된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 4.5: Adapter 구현**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/ui/CreatorChannelSeriesAdapter.kt`
|
||||||
|
- 작업:
|
||||||
|
- `RecyclerView.Adapter` 또는 기존 프로젝트 adapter 패턴을 따른다.
|
||||||
|
- `submitItems(items)`로 내부 목록을 갱신한다.
|
||||||
|
- item 클릭 시 `onSeriesClicked(seriesId)`를 호출한다.
|
||||||
|
- `title`, `subtitle`, `coverImageUrl`, original/adult tag, progress count/rate/progress fill을 bind한다.
|
||||||
|
- `coverImageUrl`은 기존 `loadUrl` extension과 placeholder/error 정책을 사용한다.
|
||||||
|
- progress가 null이면 progress container를 `GONE` 처리한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- 기대 결과:
|
||||||
|
- Adapter 컴파일이 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 4.6: Fragment 구현**
|
||||||
|
- 생성:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesFragment.kt`
|
||||||
|
- 작업:
|
||||||
|
- `newInstance(creatorId)`로 creatorId argument를 받는다.
|
||||||
|
- `Host` interface에는 `isCreatorChannelOwner()`, `onCreatorChannelSeriesClicked(seriesId: Long)`, `onCreatorChannelSeriesContentChanged()`를 둔다.
|
||||||
|
- `onCreatorChannelSeriesTabSelected()`에서 `viewModel.loadSeries(creatorId, isOwner = host.isCreatorChannelOwner())`를 호출한다.
|
||||||
|
- `onCreatorChannelSeriesScrolledToBottom()`에서 `viewModel.loadMore()`를 호출한다.
|
||||||
|
- `onCreatorChannelSeriesViewportHeightChanged(minHeight)`는 empty/error 영역 최소 높이 조정이 필요하면 기존 Live/Audio 패턴에 맞춰 구현한다.
|
||||||
|
- sort button 클릭 시 `CreatorChannelSortPopup`을 띄우고 선택 결과를 `viewModel.changeSort(sort)`로 전달한다.
|
||||||
|
- Loading/Empty/Error/Content 상태별 view visibility를 명확히 bind한다.
|
||||||
|
- content bind 시 total count, sort label, adapter items를 갱신한다.
|
||||||
|
- pagination error message는 Toast로 표시하고 consume한다.
|
||||||
|
- content layout key가 바뀔 때만 host에 content changed를 알린다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesFragmentLayoutTest"`
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- 기대 결과:
|
||||||
|
- Fragment layout 테스트와 Kotlin 컴파일이 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: 채널 탭 연결과 Activity 통합
|
||||||
|
|
||||||
|
- [ ] **Task 5.1: PagerAdapter RED 테스트 수정**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt`
|
||||||
|
- 작업:
|
||||||
|
- `CreatorChannelTab.Series`가 `CreatorChannelSeriesFragment`를 생성하는 테스트를 추가한다.
|
||||||
|
- 기존 placeholder 기대값에서 Series 탭을 제외한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- production 연결 전 RED 실패한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 5.2: PagerAdapter Series 연결**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
|
||||||
|
- 작업:
|
||||||
|
- `CreatorChannelTab.Series -> CreatorChannelSeriesFragment.newInstance(creatorId)` 분기를 추가한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- PagerAdapter 테스트가 GREEN이다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 5.3: Activity source RED 테스트 수정**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
|
||||||
|
- 작업:
|
||||||
|
- Activity가 `CreatorChannelSeriesFragment.Host`를 구현하는지 검증한다.
|
||||||
|
- Series 탭 선택 시 `onCreatorChannelSeriesTabSelected()`를 호출하는지 검증한다.
|
||||||
|
- Series 탭이 load-more 대상에 포함되는지 검증한다.
|
||||||
|
- `notifyCurrentCreatorChannelTabScrolledToBottom()`에서 Series fragment의 scroll bottom callback을 호출하는지 검증한다.
|
||||||
|
- `onCreatorChannelSeriesClicked(seriesId)`가 `SeriesDetailActivity`와 `Constants.EXTRA_SERIES_ID`를 사용하는지 검증한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
|
||||||
|
- 기대 결과:
|
||||||
|
- production 연결 전 RED 실패한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 5.4: Activity Series 통합**
|
||||||
|
- 수정:
|
||||||
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
||||||
|
- 작업:
|
||||||
|
- `CreatorChannelSeriesFragment.Host`를 구현한다.
|
||||||
|
- `findSeriesFragment()`를 추가한다.
|
||||||
|
- `onPageSelected`에서 Series 탭 선택 시 `onCreatorChannelSeriesTabSelected()`를 호출한다.
|
||||||
|
- `onCreatorChannelHeaderChanged`에서 현재 탭이 Series이면 owner 여부가 반영되도록 load를 호출한다.
|
||||||
|
- `notifyCurrentCreatorChannelTabScrolledToBottom()`에 Series load-more callback을 추가한다.
|
||||||
|
- `isCreatorChannelLoadMoreTab()`에 Series 탭을 추가한다.
|
||||||
|
- `updateCreatorChannelTabViewportHeight()`에 Series viewport callback을 추가한다.
|
||||||
|
- `onCreatorChannelSeriesContentChanged()`에서 `updateCreatorChannelTabViewportHeight()`, `updateViewPagerHeight()`, `postCheckCreatorChannelCurrentTabNeedsMore()`를 호출한다.
|
||||||
|
- `onCreatorChannelSeriesClicked(seriesId)`에서 `SeriesDetailActivity`에 `Constants.EXTRA_SERIES_ID`를 전달한다.
|
||||||
|
- 기존 홈 탭의 `onCreatorChannelSeriesClicked(series: CreatorChannelSeriesResponse)`는 기존 동작을 유지한다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- 기대 결과:
|
||||||
|
- Activity source 테스트와 Kotlin 컴파일이 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: 통합 검증과 회귀 확인
|
||||||
|
|
||||||
|
- [ ] **Task 6.1: 시리즈 탭 단위 테스트 실행**
|
||||||
|
- 실행:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.series.*"`
|
||||||
|
- 기대 결과:
|
||||||
|
- 신규 series 패키지 테스트가 모두 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 6.2: 크리에이터 채널 관련 테스트 실행**
|
||||||
|
- 실행:
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Series*"`
|
||||||
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`
|
||||||
|
- 기대 결과:
|
||||||
|
- 시리즈 연결 테스트와 기존 홈/라이브/오디오 회귀 테스트가 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 6.3: 리소스/컴파일/스타일 검증**
|
||||||
|
- 실행:
|
||||||
|
- `./gradlew :app:mergeDebugResources`
|
||||||
|
- `./gradlew :app:compileDebugKotlin`
|
||||||
|
- `./gradlew :app:ktlintCheck`
|
||||||
|
- `git diff --check`
|
||||||
|
- 기대 결과:
|
||||||
|
- 리소스 병합, Kotlin 컴파일, ktlint, whitespace 검증이 모두 PASS한다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
- [ ] **Task 6.4: 수동 확인**
|
||||||
|
- 확인:
|
||||||
|
- 시리즈 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/series?page=0&size=20&sort=LATEST`가 호출된다.
|
||||||
|
- Sort-bar 좌측에 전체 시리즈 수가 표시된다.
|
||||||
|
- 정렬 팝업에는 `추천순` 없이 5개 옵션만 표시된다.
|
||||||
|
- 정렬 변경 시 첫 페이지부터 재조회된다.
|
||||||
|
- 시리즈 item 우측 `전체소장`/play button이 표시되지 않는다.
|
||||||
|
- 내 채널이 아닌 경우 progress count, percent, progress bar가 표시된다.
|
||||||
|
- 내 채널인 경우 progress count, percent, progress bar가 표시되지 않고 제목/부제 info만 표시된다.
|
||||||
|
- empty 상태에서 `크리에이터가 시리즈를 준비 중입니다.\n기대해 주세요!`가 표시된다.
|
||||||
|
- 영어/일본어 locale에서 empty 문구가 각 번역으로 표시된다.
|
||||||
|
- 목록 하단 스크롤 시 다음 page가 중복 없이 append된다.
|
||||||
|
- item 터치 시 `SeriesDetailActivity`로 이동하고 `seriesId`가 전달된다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 구현 시 기록한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
- 구현 완료 후 여러 Phase에 걸친 통합 검증, 회귀 검증, 최종 수동 확인 기록을 이 섹션에 누적한다.
|
||||||
274
docs/20260620_크리에이터_채널_시리즈_탭/prd.md
Normal file
274
docs/20260620_크리에이터_채널_시리즈_탭/prd.md
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
# PRD: 크리에이터 채널 시리즈 탭
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
크리에이터 채널의 `시리즈` 탭에서 시리즈 수, 정렬, 시리즈 목록, 시리즈별 콘텐츠 소장 진행 정보와 스크롤 pagination을 제공한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Problem
|
||||||
|
- 크리에이터 채널 컨테이너와 `홈`, `라이브`, `오디오` 탭은 별도 문서에서 정의되었지만, `시리즈` 탭의 API 계약과 Figma 기반 UI 요구사항은 별도 정의가 필요하다.
|
||||||
|
- 사용자는 크리에이터 채널에서 전체 시리즈 수와 각 시리즈의 발행 요일, 총 콘텐츠 수, 연재/완결 상태를 한 화면에서 확인할 수 있어야 한다.
|
||||||
|
- 사용자는 내 채널이 아닌 크리에이터 채널에서 시리즈별 유료 콘텐츠 소장 진행률을 확인할 수 있어야 한다.
|
||||||
|
- 크리에이터 본인의 채널에서는 소장률 정보가 아니라 시리즈 기본 정보만 표시되어야 한다.
|
||||||
|
- 시리즈 목록은 길어질 수 있으므로 `CreatorChannelSeriesTabResponse.hasNext == true`일 때 다음 페이지를 자동 로딩해야 한다.
|
||||||
|
- 정렬 선택 방식은 이미 구현된 오디오 탭과 동일하게 유지해 탭 간 사용성이 일관되어야 한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
- Figma 전체 화면 `290:9031` 기준으로 크리에이터 채널 `시리즈` 탭 UI 요구사항을 정의한다.
|
||||||
|
- Figma 시리즈 item `290:9036`을 기준으로 목록 item 구조와 표시 규칙을 정의한다.
|
||||||
|
- Figma 시리즈 콘텐츠 소장률 `290:9038`을 기준으로 소장 진행 정보 표시 규칙을 정의한다.
|
||||||
|
- API endpoint `GET /api/v2/creator-channels/{creatorId}/series`를 기준으로 최초 조회, 정렬 변경, pagination 요구사항을 정의한다.
|
||||||
|
- 최초 조회 query parameter 기본값은 `page=0`, `size=20`, `sort=LATEST`로 둔다.
|
||||||
|
- 정렬 선택 방식은 오디오 탭과 동일하게 구현한다.
|
||||||
|
- 정렬 옵션은 기존 `ContentSort` enum에 존재하는 `LATEST`, `POPULAR`, `OWNED`, `PRICE_HIGH`, `PRICE_LOW` 5개만 사용한다.
|
||||||
|
- Figma 정렬 팝업에 보이는 `추천순`은 이번 시리즈 탭 정렬 옵션에서 제외한다.
|
||||||
|
- Sort-bar에는 전체 시리즈 수와 현재 정렬 label을 표시한다.
|
||||||
|
- 시리즈 item 우측 끝의 버튼 영역은 표시하지 않는다.
|
||||||
|
- 우측 버튼 왼쪽의 info/소장률 영역은 버튼이 사라진 공간까지 사용할 수 있어야 한다.
|
||||||
|
- 이미지처럼 크기 제한이 필요한 영역 외에는 불필요한 고정 상수를 두지 않고 `match_parent` 또는 `wrap_content`를 우선 사용한다.
|
||||||
|
- 내 채널이 아닌 경우 시리즈 item에 콘텐츠 소장 진행 정보와 progress bar를 표시한다.
|
||||||
|
- 내 채널인 경우 시리즈 item의 소장률 영역을 숨기고 상단 info만 표시한다.
|
||||||
|
- 응답의 `hasNext`가 `true`이면 현재 `page + 1` 페이지를 추가 로딩한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
- 크리에이터 채널 상단 header, title bar, 공통 main tab-bar 구조 자체를 재설계하지 않는다.
|
||||||
|
- `홈`, `라이브`, `오디오`, `화보`, `커뮤니티`, `팬Talk`, `후원` 탭의 상세 구현은 이번 범위에서 제외한다.
|
||||||
|
- 시리즈 상세, 오디오 상세, 결제, 대여, 소장, 재생 플로우 내부 동작 변경은 이번 범위에서 제외한다.
|
||||||
|
- 시리즈 생성/수정/삭제 또는 콘텐츠 업로드 진입점은 이번 범위에서 제외한다.
|
||||||
|
- API schema를 임의 변경하거나 서버 응답 필드명을 클라이언트에서 새로 정의하지 않는다.
|
||||||
|
- 정렬 외 별도 검색, 테마/카테고리 필터, pull-to-refresh, skeleton/shimmer는 이번 범위에서 제외한다.
|
||||||
|
- Figma asset을 localhost URL 그대로 앱 코드에 직접 의존하지 않는다.
|
||||||
|
- Figma 정렬 팝업에 보이는 `추천순` 정렬은 `ContentSort` 계약에 없으므로 이번 범위에서 제외한다.
|
||||||
|
- 시리즈 item 우측 버튼의 `전체소장` 또는 play button은 이번 범위에서 표시하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Target Users
|
||||||
|
- 크리에이터 채널에서 시리즈 목록을 탐색하는 앱 사용자.
|
||||||
|
- 특정 크리에이터의 시리즈별 콘텐츠 구성과 연재 상태를 확인하려는 앱 사용자.
|
||||||
|
- 특정 시리즈의 유료 콘텐츠 중 자신이 얼마나 소장했는지 확인하려는 앱 사용자.
|
||||||
|
- 본인 채널에서 사용자에게 보이는 시리즈 정보를 확인하려는 크리에이터.
|
||||||
|
- `kr.co.vividnext.sodalive.v2` 하위 크리에이터 채널 탭을 구현/유지보수하는 Android 개발자.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. User Stories
|
||||||
|
- 사용자는 크리에이터 채널의 `시리즈` 탭에서 전체 시리즈 수를 확인하고 싶다.
|
||||||
|
- 사용자는 시리즈 제목, 발행 요일, 총 콘텐츠 수, 연재/완결 상태를 목록에서 확인하고 싶다.
|
||||||
|
- 사용자는 오디오 탭과 동일한 방식으로 시리즈 목록 정렬을 변경하고 싶다.
|
||||||
|
- 사용자는 내 채널이 아닌 크리에이터의 시리즈별 소장 화수와 전체 유료 화수 대비 소장률을 확인하고 싶다.
|
||||||
|
- 사용자는 본인 채널의 `시리즈` 탭에서는 소장률 정보 없이 시리즈 기본 정보만 보고 싶다.
|
||||||
|
- 사용자는 시리즈 목록 하단까지 스크롤하면 다음 페이지가 자연스럽게 이어서 로딩되길 기대한다.
|
||||||
|
- 사용자는 크리에이터가 아직 시리즈를 준비 중인 경우 불필요한 정렬 UI 없이 empty 문구만 보고 싶다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Core Features
|
||||||
|
|
||||||
|
### Creator Channel Series Tab API
|
||||||
|
`시리즈` 탭 진입, 정렬 변경, 추가 로딩 시 크리에이터별 시리즈 탭 데이터를 조회한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/series`이다.
|
||||||
|
- `creatorId`는 path variable로 전달한다.
|
||||||
|
- Query parameters는 `sort`, `page`, `size`를 사용한다.
|
||||||
|
- 최초 조회 기본값은 `page=0`, `size=20`, `sort=ContentSort.LATEST`이다.
|
||||||
|
- `sort`는 기존 `ContentSort` enum 값을 그대로 전달한다.
|
||||||
|
- `ContentSort`는 기존에 만들어져 있는 타입을 재사용한다.
|
||||||
|
- 시리즈 item의 이미지 표시는 `CreatorChannelSeriesResponse.coverImageUrl`을 사용한다.
|
||||||
|
- `hasNext == true`일 때 다음 페이지 요청은 현재 응답의 `page + 1` 값을 사용한다.
|
||||||
|
- 중복 pagination 요청이 발생하지 않도록 loading 중 추가 요청을 막아야 한다.
|
||||||
|
- 정렬 변경 시 기존 목록과 page 상태를 초기화하고 첫 페이지부터 다시 조회한다.
|
||||||
|
|
||||||
|
#### Response Contract
|
||||||
|
```kotlin
|
||||||
|
data class CreatorChannelSeriesTabResponse(
|
||||||
|
val seriesCount: Int,
|
||||||
|
val series: List<CreatorChannelSeriesResponse>,
|
||||||
|
val sort: ContentSort,
|
||||||
|
val page: Int,
|
||||||
|
val size: Int,
|
||||||
|
val hasNext: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelSeriesResponse(
|
||||||
|
val seriesId: Long,
|
||||||
|
val title: String,
|
||||||
|
val coverImageUrl: String,
|
||||||
|
val publishedDaysOfWeek: String,
|
||||||
|
val isOriginal: Boolean,
|
||||||
|
val isAdult: Boolean,
|
||||||
|
val isProceeding: Boolean,
|
||||||
|
val contentCount: Int,
|
||||||
|
val purchasedContentCount: Int?,
|
||||||
|
val paidContentCount: Int?,
|
||||||
|
val purchasedPaidContentRate: Int?
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class ContentSort {
|
||||||
|
LATEST,
|
||||||
|
POPULAR,
|
||||||
|
OWNED,
|
||||||
|
PRICE_HIGH,
|
||||||
|
PRICE_LOW
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 최초 조회 실패 시 기존 크리에이터 채널 탭의 error/retry 패턴을 따른다.
|
||||||
|
- 정렬 변경 실패 시 현재 프로젝트의 에러 표시/재시도 패턴을 구현 계획 단계에서 확인해 따른다.
|
||||||
|
- 다음 페이지 로딩 실패 시 기존 목록은 유지하고 기존 pagination 실패 표시 정책을 따른다.
|
||||||
|
- 다음 페이지 응답의 `series`가 비어 있어도 `hasNext` 값 기준으로 이후 로딩 가능 여부를 갱신한다.
|
||||||
|
- 서버 응답의 `sort`, `page`, `size`가 요청 상태와 다를 경우 구현 계획 단계에서 기존 ViewModel 상태 동기화 패턴을 확인해 따른다.
|
||||||
|
- `coverImageUrl`이 비어 있거나 이미지 로딩에 실패하면 기존 이미지 placeholder 정책을 따른다.
|
||||||
|
|
||||||
|
### Sort Bar and Sort Menu
|
||||||
|
Sort-bar는 전체 시리즈 수와 현재 정렬 상태를 표시하고, 오디오 탭과 동일한 정렬 메뉴를 연다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- Figma 전체 화면 기준 Sort-bar는 `290:9031` 내 `sort-bar`이다.
|
||||||
|
- 좌측에는 `전체`와 `seriesCount`를 표시한다.
|
||||||
|
- 우측에는 현재 정렬 label과 정렬 icon을 표시한다.
|
||||||
|
- 정렬 선택 방식은 오디오 탭과 동일하다.
|
||||||
|
- 정렬 기본값은 `ContentSort.LATEST`이며 label은 한국어 기준 `최신순`이다.
|
||||||
|
- 정렬 옵션은 기존 `ContentSort` enum에 존재하는 5개만 사용한다.
|
||||||
|
- `LATEST` label은 `최신순`이다.
|
||||||
|
- `POPULAR` label은 기존 프로젝트의 `인기순` label을 따른다.
|
||||||
|
- `OWNED` label은 기존 프로젝트의 `소장순` label을 따른다.
|
||||||
|
- `PRICE_HIGH` label은 기존 프로젝트의 `높은 가격순` label을 따른다.
|
||||||
|
- `PRICE_LOW` label은 기존 프로젝트의 `낮은 가격순` label을 따른다.
|
||||||
|
- Figma 정렬 팝업에 보이는 `추천순`은 표시하지 않는다.
|
||||||
|
- 정렬 옵션을 선택하면 `page=0`, 선택된 `sort`로 API를 다시 조회한다.
|
||||||
|
- 선택 중인 정렬 옵션을 다시 선택하면 API 재호출 없이 메뉴만 닫는다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 작은 화면에서 정렬 메뉴가 화면 우측 또는 하단을 벗어나지 않도록 오디오 탭과 동일한 위치 보정 정책을 적용한다.
|
||||||
|
- 다국어 label 길이가 길어져도 Sort-bar text와 icon이 겹치지 않아야 한다.
|
||||||
|
|
||||||
|
### Series Content List
|
||||||
|
시리즈 목록은 Figma의 목록형 item으로 표시하고, 각 item에서 시리즈 기본 정보와 조건부 소장 진행 정보를 제공한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- Figma 콘텐츠 item 기준 노드는 `290:9036`이다.
|
||||||
|
- `series`를 세로 목록으로 표시한다.
|
||||||
|
- 각 item의 좌측에는 `coverImageUrl` 기반 썸네일을 표시한다.
|
||||||
|
- 썸네일은 Figma 기준 `122dp x 172dp`, radius 14dp 형태를 따른다.
|
||||||
|
- `isOriginal == true`이면 썸네일 좌상단에 original tag를 표시한다.
|
||||||
|
- `isAdult == true`이면 썸네일 우상단에 adult tag를 표시한다.
|
||||||
|
- 제목 영역에는 `title`을 표시한다.
|
||||||
|
- 부제 영역에는 `publishedDaysOfWeek`, `contentCount`, `isProceeding`을 조합해 표시한다.
|
||||||
|
- 부제 문구 형식은 Figma 기준 `매주 월 • 총 nn화 • 연재` 또는 `매주 월 • 총 nn화 • 완결`이다.
|
||||||
|
- `isProceeding == true`이면 `연재`로 표시한다.
|
||||||
|
- `isProceeding == false`이면 `완결`로 표시한다.
|
||||||
|
- item 우측 끝의 `전체소장` 또는 play button 영역은 표시하지 않는다.
|
||||||
|
- 우측 버튼이 사라진 공간은 info 영역과 소장률 영역이 사용할 수 있어야 한다.
|
||||||
|
- 이미지처럼 명확한 크기 제한이 필요한 썸네일 외에는 고정 width/height 상수를 최소화하고 `match_parent` 또는 `wrap_content`를 우선 사용한다.
|
||||||
|
- item 터치 시 시리즈 상세 진입은 기존 프로젝트의 시리즈 상세 진입 정책을 구현 계획 단계에서 확인해 따른다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- `title`이 긴 경우 Figma처럼 최대 2줄 또는 기존 목록 item 정책에 맞게 표시하고 이후 말줄임 처리한다.
|
||||||
|
- 부제 문구가 긴 경우 한 줄 말줄임 처리한다.
|
||||||
|
- `publishedDaysOfWeek`가 비어 있으면 빈 구분자나 불필요한 bullet이 보이지 않아야 한다.
|
||||||
|
- `contentCount == 0`이어도 `총 0화` 표시 정책을 유지한다.
|
||||||
|
- `coverImageUrl`이 비어 있거나 이미지 로딩 실패 시 기존 이미지 placeholder 정책을 따른다.
|
||||||
|
|
||||||
|
### Series Possession Progress
|
||||||
|
시리즈 소장 진행 정보는 내 채널이 아닌 경우에만 표시한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- Figma 기준 노드는 `290:9038`이다.
|
||||||
|
- 내 채널이 아닌 경우 item 하단에 소장 진행 정보를 표시한다.
|
||||||
|
- 내 채널인 경우 소장 진행 정보 전체를 숨기고 item 상단 info만 표시한다.
|
||||||
|
- 내 채널에서 표시하는 info는 제목, 발행 요일, 총 콘텐츠 수, 연재/완결 상태까지만 의미한다.
|
||||||
|
- 내 채널에서는 `purchasedContentCount`, `paidContentCount`, `purchasedPaidContentRate` 기반 숫자와 progress bar를 표시하지 않는다.
|
||||||
|
- 내 채널이 아닌 경우 `purchasedContentCount`, `paidContentCount`, `purchasedPaidContentRate`를 사용해 소장 진행 정보를 표시한다.
|
||||||
|
- 소장 화수 문구는 Figma 기준 `12/45화` 형식으로 표시한다.
|
||||||
|
- `purchasedContentCount`는 좌측 숫자로 표시한다.
|
||||||
|
- `paidContentCount`는 우측 전체 유료 화수로 표시한다.
|
||||||
|
- `purchasedPaidContentRate`는 우측 percent와 progress bar 채움 비율로 표시한다.
|
||||||
|
- `purchasedPaidContentRate`는 서버에서 percent 값으로 내려오며, 클라이언트는 사용자 표시용 `%` 포맷만 적용한다.
|
||||||
|
- progress bar는 Figma 기준 4dp height, soda color 채움, gray 배경을 따른다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 내 채널이 아닌데 `purchasedContentCount`, `paidContentCount`, `purchasedPaidContentRate` 중 하나라도 `null`이면 소장 진행 정보 전체를 숨긴다.
|
||||||
|
- `paidContentCount == 0`이면 0 나누기 계산을 클라이언트에서 수행하지 않고, 서버의 `purchasedPaidContentRate` 또는 소장 진행 정보 숨김 정책을 따른다.
|
||||||
|
- `purchasedPaidContentRate`가 0 미만 또는 100 초과로 내려오면 progress bar 표시값은 UI 안정성을 위해 0..100 범위로 clamp한다.
|
||||||
|
- 긴 숫자에서도 소장 화수와 percent가 겹치지 않아야 한다.
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
시리즈 목록은 스크롤 하단 접근 시 다음 페이지를 로딩한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `CreatorChannelSeriesTabResponse.hasNext == true`일 때만 다음 페이지를 요청한다.
|
||||||
|
- 다음 페이지는 마지막 성공 응답의 `page + 1`로 요청한다.
|
||||||
|
- 다음 페이지 요청에는 현재 `sort`, `size=20`을 유지한다.
|
||||||
|
- 다음 페이지 로딩 중에는 추가 page 요청을 중복으로 보내지 않는다.
|
||||||
|
- 다음 페이지 성공 시 기존 `series` 뒤에 append한다.
|
||||||
|
- 정렬 변경 시 pagination 상태를 초기화한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 빠른 스크롤로 load-more trigger가 반복 발생해도 page가 중복 append되지 않아야 한다.
|
||||||
|
- Fragment/View 재생성 후 현재 목록, 정렬, page 상태는 ViewModel 상태 보존 정책에 따라 유지되어야 한다.
|
||||||
|
- 마지막 페이지 응답 이후 `hasNext == false`이면 이후 load-more trigger를 무시한다.
|
||||||
|
|
||||||
|
### Empty State
|
||||||
|
시리즈가 없으면 시리즈가 있을 때 표시하는 UI를 숨기고 empty 문구만 표시한다.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `seriesCount == 0` 또는 표시 가능한 `series`가 없는 전체 empty 상태이면 empty 상태를 표시한다.
|
||||||
|
- empty 상태에서는 Sort-bar와 시리즈 목록을 표시하지 않는다.
|
||||||
|
- empty 문구는 `크리에이터가 시리즈를 준비 중입니다.\n기대해 주세요!`이다.
|
||||||
|
- empty 문구는 한국어/영어/일본어 다국어 문자열 리소스로 관리한다.
|
||||||
|
- 영어 empty 문구는 `The creator is preparing a series.\nPlease look forward to it!`이다.
|
||||||
|
- 일본어 empty 문구는 `クリエイターがシリーズを準備中です。\n楽しみにお待ちください!`이다.
|
||||||
|
- empty 상태 표시 방식은 라이브/오디오 탭 empty 상태와 동일하게 적용한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- API 최초 조회 실패 상태는 empty 상태로 취급하지 않고 기존 error/retry 패턴을 따른다.
|
||||||
|
- `seriesCount > 0`이지만 응답 `series`가 비어 있는 첫 페이지 응답은 서버 상태 불일치 가능성이 있으므로 기존 목록 탭의 empty/error 정책을 구현 계획 단계에서 확인해 따른다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. UX / UI Expectations
|
||||||
|
- 전체 화면은 기존 크리에이터 채널 컨테이너의 black background, sticky tab-bar, title-bar 동작을 유지한다.
|
||||||
|
- `시리즈` main tab은 선택 상태로 표시하고, 선택 underline과 텍스트 색상은 기존 tab-bar 정책을 따른다.
|
||||||
|
- Sort-bar 높이와 배치는 Figma `290:9031`을 기준으로 하되 기존 오디오 탭 구현과 가능한 한 동일한 공통 UI를 사용한다.
|
||||||
|
- 목록 item 간 간격은 Figma 기준 8dp 수준을 따른다.
|
||||||
|
- 시리즈 item은 좌측 썸네일, 중앙 info/소장 진행 정보의 2영역 구조로 표시한다.
|
||||||
|
- 사용자 요구에 따라 item 우측 버튼 영역은 표시하지 않는다.
|
||||||
|
- 우측 버튼을 제거한 뒤에도 title, subtitle, 소장 진행 숫자, progress bar가 서로 겹치지 않아야 한다.
|
||||||
|
- 이미지 썸네일처럼 크기 제한이 필요한 경우 외에는 고정 상수보다 `match_parent` 또는 `wrap_content`를 사용한다.
|
||||||
|
- 모든 사용자 표시 문구는 문자열 리소스로 관리한다.
|
||||||
|
- Figma localhost asset URL은 앱 코드에 직접 사용하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Technical Constraints
|
||||||
|
- Android Gradle 단일 모듈 `:app` 안에서 구현한다.
|
||||||
|
- 신규 `Fragment`, `ViewModel` 및 그와 연결된 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
||||||
|
- 기존 `CreatorChannelActivity`와 `ViewPager2` 기반 탭 구조를 유지한다.
|
||||||
|
- 기존 크리에이터 채널 API/Repository 패턴을 따른다.
|
||||||
|
- 기존 `ContentSort` 타입과 오디오 탭의 정렬 선택 UI/동작을 재사용한다.
|
||||||
|
- 서버 DTO 필드명과 타입은 PRD의 Response Contract를 따른다.
|
||||||
|
- `coverImageUrl`은 시리즈 썸네일 이미지 URL로 사용한다.
|
||||||
|
- API 기본값은 `page=0`, `size=20`, `sort=LATEST`이다.
|
||||||
|
- 네트워크, 이미지 로딩, error/retry, pagination 중복 방지 방식은 기존 라이브/오디오 탭 패턴을 우선 따른다.
|
||||||
|
- 비밀값, `BuildConfig` 값, 로컬 Figma asset URL을 로그/Toast/크래시 메시지에 노출하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Metrics
|
||||||
|
- 시리즈 탭 최초 API 조회 성공/실패 여부.
|
||||||
|
- 정렬 변경 후 첫 페이지 재조회 성공/실패 여부.
|
||||||
|
- pagination 추가 로딩 성공/실패 여부.
|
||||||
|
- 시리즈 item 클릭 후 상세 진입 성공 여부.
|
||||||
|
- 내 채널 여부에 따른 소장 진행 정보 노출/숨김 정확도.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Open Questions
|
||||||
|
- 시리즈 상세 진입 대상 Activity/Fragment와 전달 파라미터는 구현 계획 단계에서 기존 프로젝트 코드를 확인해 확정한다.
|
||||||
Reference in New Issue
Block a user