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