Files
sodalive-android/docs/20260620_크리에이터_채널_시리즈_탭/prd.md

275 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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와 전달 파라미터는 구현 계획 단계에서 기존 프로젝트 코드를 확인해 확정한다.