17 KiB
17 KiB
PRD: 크리에이터 채널 시리즈 탭 API
1. Overview
크리에이터 채널의 시리즈 탭에서 정렬별 시리즈 개수와 시리즈 목록을 페이징 조회하는 API를 제공한다.
2. Problem
- 크리에이터 채널 시리즈 탭은 전체 시리즈 개수, 정렬 상태, 시리즈 목록을 함께 표시해야 한다.
- 기존 홈 API의
CreatorChannelSeries는 홈 화면용 요약 모델이라 시리즈 탭에서 필요한 연재 요일, 연재 상태, 콘텐츠 개수, 구매/유료 콘텐츠 통계를 모두 표현하지 못한다. - 클라이언트는 시리즈 탭 진입과 추가 로딩 시 별도 API 조합 없이 일관된 계약으로 시리즈 목록을 받아야 한다.
- 연재 요일 문구는 서버에서 조합하고, 호출 유저의 언어에 맞게 반환해야 한다.
- 기존 크리에이터 채널 홈/라이브/오디오 탭 API endpoint와 응답 필드의 의미는 변경하지 않아야 한다.
3. Goals
- 크리에이터 채널 시리즈 탭 조회 API를 제공한다.
- 클라이언트에서 호출하는 공개 API controller/facade/response DTO는
kr.co.vividnext.sodalive.v2.api.creator.channel.series하위 조립 계층에 둔다. - 시리즈 목록, 시리즈 개수, 구매/유료 콘텐츠 통계, 연재 요일 조합처럼 재사용 가능한 조회 책임은 API 패키지 밖의 도메인 패키지에 둔다.
- 기존 홈 API의
CreatorChannelSeries는 확장하지 않고, 시리즈 탭 전용 도메인 모델과 응답 DTO를 새로 둔다. - 요청은
creatorId, 정렬 순서, 페이징 값을 받는다. - 정렬 순서를 보내지 않으면 최신순을 기본값으로 사용한다.
- 페이징 동작은 크리에이터 채널 오디오 탭 API와 같은 방식으로 처리한다.
- 응답에는 전체 시리즈 개수, 시리즈 목록, 실제 적용된 정렬 순서, page, size, hasNext를 포함한다.
- 시리즈 목록 item에는 시리즈 id, 제목, 커버 이미지 URL, 연재 요일 문구, 오리지널 여부, 19금 여부, 연재 중 여부, 전체 콘텐츠 개수를 포함한다.
- 조회자가 해당 시리즈의 크리에이터가 아닌 경우에는 구매한 콘텐츠 개수, 유료 콘텐츠 개수, 유료 콘텐츠 중 구매한 콘텐츠 비율도 포함한다.
- 시리즈 제목과 연재 요일 문구는 호출 유저 언어코드에 맞게 반환한다.
4. Non-Goals
- 이번 범위는 크리에이터 채널
시리즈탭 조회 API만 포함한다. - 기존 크리에이터 채널 홈 API, 라이브 탭 API, 오디오 탭 API endpoint와 응답 필드의 의미는 변경하지 않는다.
- 시리즈 상세 조회 API는 포함하지 않는다.
- 시리즈 생성/수정/삭제 API는 포함하지 않는다.
- 오디오 콘텐츠 구매, 소장, 대여, 결제 API는 포함하지 않는다.
- 시리즈 번역 데이터가 없는 항목을 새로 번역하거나 생성하는 배치 작업은 포함하지 않는다.
- 앱 표시용 날짜 포맷, 가격 단위 표시는 서버에서 처리하지 않는다.
5. Target Users
- 회원: 크리에이터 채널 시리즈 탭에서 크리에이터의 시리즈를 탐색하는 사용자
- 앱 클라이언트: 시리즈 탭 구성에 필요한 개수/목록/구매 통계를 단일 API 응답으로 표시하려는 클라이언트
- 크리에이터: 자신의 시리즈가 정렬 기준에 따라 적절히 노출되기를 원하는 사용자
6. User Stories
- 사용자는 크리에이터 채널 시리즈 탭에 들어가면 전체 시리즈 개수를 확인하고 싶다.
- 사용자는 최신순, 인기순, 소장순, 높은 가격순, 낮은 가격순으로 시리즈 목록을 바꿔 보고 싶다.
- 사용자는 시리즈의 연재 요일과 연재 중 여부를 확인하고 싶다.
- 사용자는 유료 콘텐츠 중 자신이 구매한 콘텐츠 비율을 확인하고 싶다.
- 앱 클라이언트는 현재 적용된 정렬 순서를 응답에서 확인해 화면 상태와 서버 조회 결과를 맞추고 싶다.
- 앱 클라이언트는 호출 유저 언어코드에 맞는 시리즈 제목과 연재 요일 문구를 받아 화면에 표시하고 싶다.
7. Core Features
Feature A. 크리에이터 채널 시리즈 탭 조회 API
Requirements
- 신규 API는 크리에이터 채널 전용 v2 API로 작성한다.
- API endpoint는
GET /api/v2/creator-channels/{creatorId}/series를 기본안으로 한다. creatorId는 path variable로 받는다.- 정렬 순서는 query parameter로 받는다.
- 정렬 순서 query parameter 이름은
sort를 기본안으로 한다. sort를 보내지 않으면LATEST를 기본값으로 사용한다.sort값이 없거나 기존ContentSortenum 값에 없으면LATEST로 fallback한다.- 시리즈 추가 로딩을 위해
page,sizequery parameter를 받는다. page는 0부터 시작하는 page index로 처리한다.page를 보내지 않으면 기본값0을 사용한다.size를 보내지 않으면 기본값20을 사용한다.page가 0보다 작으면0으로 fallback한다.size가 20보다 작으면20으로 fallback한다.size가 50보다 크면50으로 fallback한다.- API는 인증 회원만 조회할 수 있어야 한다.
- 비회원이 조회하면 기존 인증 필요 API와 동일하게
common.error.bad_credentials계열 오류를 반환한다. - 조회 대상 회원이 존재하지 않으면 기존 정책과 동일하게
member.validation.user_not_found계열 오류를 반환한다. - 조회 대상 회원이 크리에이터가 아니면 기존 정책과 동일하게
member.validation.creator_not_found계열 오류를 반환한다. - 조회자와 크리에이터 사이에 차단 관계가 있으면 기존 크리에이터 채널 접근 정책과 동일하게 접근 차단 오류를 반환한다.
- 공개된 시리즈가 없어도 전체 API는 성공 처리한다.
Edge Cases
- 조회자 본인이 크리에이터인 경우에도 같은 응답 스키마를 사용한다.
- 알 수 없는
sort값은 400 오류를 반환하지 않고LATEST로 fallback한다. page가 0보다 작거나size가 허용 범위를 벗어나도 400 오류를 반환하지 않고 실제 적용값으로 보정한다.
Feature B. 응답 스키마
Requirements
- 응답 DTO는 구현 전에 명시하고 공개 API 계약으로 관리한다.
- 응답 최상위 DTO 이름은
CreatorChannelSeriesTabResponse를 기본안으로 한다. - 응답에는 다음 값을 포함한다.
seriesCount: 조회 가능한 전체 시리즈 개수series: 시리즈 목록sort: 시리즈 조회에 실제 적용한 정렬 순서page: 현재 응답의 page indexsize: 현재 응답의 page sizehasNext: 다음 page 존재 여부
seriesCount는 sort-bar에 표시할 전체 개수이며, 콘텐츠 개수가 아니라 시리즈 개수다.sort는 요청값이 없거나 알 수 없는 값이면 실제 적용값인LATEST를 내려준다.page,size는 fallback 보정 이후 실제 적용된 값을 내려준다.hasNext는 같은 정렬 조건에서 다음 page에 노출할 시리즈가 있으면true로 내려준다.- 조회자가 해당 시리즈의 크리에이터인 경우
purchasedContentCount,paidContentCount,purchasedPaidContentRate는null로 내려준다. - 조회자가 해당 시리즈의 크리에이터가 아닌 경우
purchasedContentCount,paidContentCount,purchasedPaidContentRate를 계산해 내려준다. purchasedPaidContentRate는 정수 퍼센트 값으로 내려준다.purchasedPaidContentRate는paidContentCount == 0이면0으로 내려준다.purchasedPaidContentRate는(purchasedContentCount * 100) / paidContentCount를 기준으로 계산하고 소수점 이하는 버린다.- 응답 스키마 예시는 다음과 같다.
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
- 공개된 시리즈가 없으면
seriesCount는0,series는 빈 배열,hasNext는false로 내려준다. - 요청한 page 범위에 시리즈가 없으면
series는 빈 배열,hasNext는false로 내려주되seriesCount는 전체 개수를 유지한다. - 무료 콘텐츠만 포함한 시리즈는 비크리에이터 조회 시
paidContentCount를0,purchasedContentCount를0,purchasedPaidContentRate를0으로 내려준다.
Feature C. 시리즈 목록과 필드
Requirements
- 조회 대상은 지정한
creatorId의 시리즈로 제한한다. - 공개 가능한 활성 시리즈만 조회한다.
- 조회자의 성인 콘텐츠 노출 정책이 false이면 19금 시리즈는 목록과 개수에서 제외한다.
- 시리즈에 속한 콘텐츠 통계는 공개된 오디오 콘텐츠만 기준으로 계산한다.
- 예약 공개 전 콘텐츠는 콘텐츠 통계에서 제외한다.
releaseDate == null인 오디오 콘텐츠는 삭제/미공개 데이터로 보고 콘텐츠 통계에서 제외한다.- 시리즈 제목은 호출 유저 언어코드에 맞는 번역값이 있으면 번역명을 사용하고, 없으면 원문 시리즈명을 사용한다.
coverImageUrl은 시리즈의 커버 이미지 경로를 CDN URL로 변환해 내려준다.- 시리즈 커버 이미지 경로가 없거나 빈 문자열이면
coverImageUrl은null로 내려준다. - 호출 유저 언어코드는 기존
LangContext.lang.code값을 사용한다. - 지원 언어코드는
ko,en,ja를 기준으로 한다. isProceeding은SeriesState.PROCEEDING이면true, 그 외 상태이면false로 내려준다.contentCount는 조회 가능한 공개 콘텐츠 개수다.paidContentCount는 같은 필터를 적용한 콘텐츠 중price > 0인 콘텐츠 개수다.purchasedContentCount는 같은 필터를 적용한 유료 콘텐츠 중 조회자가 유효하게 소장하거나 대여 중인 콘텐츠 개수다.- 대여 중인 콘텐츠는 구매한 콘텐츠 개수와
purchasedPaidContentRate계산에 포함한다. - 유효 구매/대여 조건은 기존 오디오 탭과 동일하게
orders.is_active = true이며, 대여는 만료되지 않은 주문만 포함한다.
Edge Cases
- 제목 번역 데이터는 있지만 빈 문자열이면 원문 시리즈명을 fallback으로 사용한다.
- 콘텐츠가 없는 활성 시리즈는 시리즈 목록에 포함하되
contentCount,paidContentCount,purchasedContentCount를0으로 계산한다. - 일반적으로 동일 콘텐츠의 소장과 대여가 동시에 발생하지 않지만, 데이터상 동시에 유효한 소장/대여 주문이 있으면 콘텐츠 1개로 중복 없이 계산한다.
Feature D. 연재 요일 문구
Requirements
publishedDaysOfWeek는 서버에서 조합한 문자열로 내려준다.- 일요일부터 토요일까지 7개 요일이 모두 있으면 호출 유저 언어에 맞는
매일문구를 내려준다. - 7개 요일이 모두 있지 않으면 호출 유저 언어에 맞는
매주 {요일 목록}문구를 내려준다. - 요일 목록은
SUN,MON,TUE,WED,THU,FRI,SAT순서로 정렬한다. - 한국어 예시는
매일,매주 월, 목, 토다. - 영어 예시는
Every day,Every Mon, Thu, Sat다. - 일본어 예시는
毎日,毎週 月, 木, 土다. SeriesPublishedDaysOfWeek.RANDOM이 포함된 경우에는 다른 요일 값을 모두 무시하고 호출 유저 언어에 맞는 랜덤 문구만 내려준다.- 랜덤 문구도 다국어 처리한다.
- 랜덤 문구는 한국어
랜덤, 영어Random, 일본어ランダム을 기본안으로 한다.
Edge Cases
- 연재 요일이 비어 있으면 빈 문자열 대신 호출 유저 언어에 맞는 랜덤 문구를 fallback으로 내려준다.
RANDOM과 다른 요일이 동시에 저장된 데이터는RANDOM을 우선해 다른 요일을 제거한 것과 같은 결과로 랜덤 문구만 내려준다.
Feature E. 시리즈 정렬
Requirements
- 정렬 순서는 기존 공용
ContentSortenum을 사용한다. - 공개 요청/응답 값은 다음을 사용한다.
LATEST: 최신순, 기본값POPULAR: 인기순OWNED: 소장순PRICE_HIGH: 높은 가격순PRICE_LOW: 낮은 가격순
LATEST는 시리즈에 속한 콘텐츠의releaseDate desc를 1차 정렬로 사용한다.LATEST의 2차 정렬은 시리즈에 속한 콘텐츠의price desc다.LATEST의 3차 정렬은series.id desc다.POPULAR은 시리즈에 속한 콘텐츠에 순수하게 결제된 캔 매출 합계(orders.can)가 높은 시리즈를 먼저 노출한다.POPULAR의 매출 합계에는orders.is_active = true인 주문만 포함한다.POPULAR의 2차 정렬은 시리즈에 속한 콘텐츠의releaseDate desc다.POPULAR의 3차 정렬은series.id desc다.OWNED는 조회자가 시리즈에 속한 콘텐츠 중 유효하게 소장하거나 대여 중인 콘텐츠 개수가 많은 시리즈를 먼저 노출한다.OWNED의 2차 정렬은 시리즈에 속한 콘텐츠의releaseDate desc다.OWNED의 3차 정렬은series.id desc다.PRICE_HIGH는 시리즈에 속한 콘텐츠의price desc를 1차 정렬로 사용한다.PRICE_HIGH의 2차 정렬은 시리즈에 속한 콘텐츠의releaseDate desc다.PRICE_HIGH의 3차 정렬은series.id desc다.PRICE_LOW는 시리즈에 속한 콘텐츠의price asc를 1차 정렬로 사용한다.PRICE_LOW의 2차 정렬은 시리즈에 속한 콘텐츠의releaseDate desc다.PRICE_LOW의 3차 정렬은series.id desc다.- 시리즈에 여러 콘텐츠가 속한 경우 정렬은 시리즈 단위 집계 대표값을 사용한다.
- 정렬용
releaseDate는 항상 내림차순 정렬에만 사용하므로 각 시리즈에 속한 공개 콘텐츠 중 가장 최근releaseDate를 대표값으로 사용한다. price desc정렬에 사용하는 가격 대표값은 각 시리즈에 속한 공개 콘텐츠 중 가장 높은 가격이다.price asc정렬에 사용하는 가격 대표값은 각 시리즈에 속한 공개 콘텐츠 중 가장 낮은 가격이다.- 따라서
LATEST의 2차 정렬과PRICE_HIGH의 1차 정렬은 시리즈별 최고 가격을 사용하고,PRICE_LOW의 1차 정렬은 시리즈별 최저 가격을 사용한다.
Edge Cases
- 매출이 없는 시리즈의 인기순 매출값은 0으로 처리한다.
- 조회자가 유효하게 소장하거나 대여 중인 콘텐츠가 없으면
OWNED정렬은 최신순 +series.id desc보조 정렬과 같은 결과가 될 수 있다. - 콘텐츠가 없는 시리즈는 정렬 대표값이 없는 항목으로 처리해 같은 정렬 내 마지막에 노출한다.
- 가격이 같은 시리즈는 각 정렬의 2차/3차 기준을 따른다.
8. Technical Constraints
- 빌드 도구는 Gradle Wrapper(
./gradlew)를 사용한다. - Kotlin + Spring Boot 2.7.14 기존 스타일을 따른다.
- 신규 공개 API 스키마는 구현 전에 PRD와 구현 계획/TASK 문서에 명시한다.
- 공개 API controller/facade/response DTO는
kr.co.vividnext.sodalive.v2.api.creator.channel.series하위에 둔다. - API 조립 계층은 HTTP 계약과 공개 응답 변환만 담당한다.
- 도메인 조회 코드는
kr.co.vividnext.sodalive.v2.creator.channel.series또는 재사용 범위가 더 넓은 기존 도메인 패키지 하위에 둔다. - 도메인 패키지는
kr.co.vividnext.sodalive.v2.api.*패키지에 의존하지 않는다. - 기존 홈 API의
CreatorChannelSeries는 홈 응답 전용 요약 모델로 유지하고, 시리즈 탭 API에서는 별도CreatorChannelSeriesTab,CreatorChannelSeries계열 모델을 둔다. - 기존
ContentSortenum을 재사용하고, API binding, service 정책, 테스트에서 같은 타입을 사용한다. - 기존 크리에이터 채널 홈/라이브/오디오 API의 인증, 예외, 성인 콘텐츠 노출, 차단 관계 정책은 재사용한다.
- 페이징 응답은 기존 오디오 탭 API와 같은
page,size,hasNext패턴을 따른다. - 시리즈명 다국어 처리는 기존
SeriesTranslation구조를 따른다. - 연재 요일 문구 다국어 처리는 서버 코드의 명시적 매핑으로 처리한다.
9. Metrics
- 시리즈 탭 API 성공/실패 건수
- 시리즈 탭 API 응답 시간
- 정렬 기준별 조회 건수
- 시리즈 탭에서 추가 로딩 요청 건수
10. Open Questions
- 없음.