Files

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 값이 없거나 기존 ContentSort enum 값에 없으면 LATEST로 fallback한다.
  • 시리즈 추가 로딩을 위해 page, size query 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 index
    • size: 현재 응답의 page size
    • hasNext: 다음 page 존재 여부
  • seriesCount는 sort-bar에 표시할 전체 개수이며, 콘텐츠 개수가 아니라 시리즈 개수다.
  • sort는 요청값이 없거나 알 수 없는 값이면 실제 적용값인 LATEST를 내려준다.
  • page, size는 fallback 보정 이후 실제 적용된 값을 내려준다.
  • hasNext는 같은 정렬 조건에서 다음 page에 노출할 시리즈가 있으면 true로 내려준다.
  • 조회자가 해당 시리즈의 크리에이터인 경우 purchasedContentCount, paidContentCount, purchasedPaidContentRatenull로 내려준다.
  • 조회자가 해당 시리즈의 크리에이터가 아닌 경우 purchasedContentCount, paidContentCount, purchasedPaidContentRate를 계산해 내려준다.
  • purchasedPaidContentRate는 정수 퍼센트 값으로 내려준다.
  • purchasedPaidContentRatepaidContentCount == 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

  • 공개된 시리즈가 없으면 seriesCount0, series는 빈 배열, hasNextfalse로 내려준다.
  • 요청한 page 범위에 시리즈가 없으면 series는 빈 배열, hasNextfalse로 내려주되 seriesCount는 전체 개수를 유지한다.
  • 무료 콘텐츠만 포함한 시리즈는 비크리에이터 조회 시 paidContentCount0, purchasedContentCount0, purchasedPaidContentRate0으로 내려준다.

Feature C. 시리즈 목록과 필드

Requirements

  • 조회 대상은 지정한 creatorId의 시리즈로 제한한다.
  • 공개 가능한 활성 시리즈만 조회한다.
  • 조회자의 성인 콘텐츠 노출 정책이 false이면 19금 시리즈는 목록과 개수에서 제외한다.
  • 시리즈에 속한 콘텐츠 통계는 공개된 오디오 콘텐츠만 기준으로 계산한다.
  • 예약 공개 전 콘텐츠는 콘텐츠 통계에서 제외한다.
  • releaseDate == null인 오디오 콘텐츠는 삭제/미공개 데이터로 보고 콘텐츠 통계에서 제외한다.
  • 시리즈 제목은 호출 유저 언어코드에 맞는 번역값이 있으면 번역명을 사용하고, 없으면 원문 시리즈명을 사용한다.
  • coverImageUrl은 시리즈의 커버 이미지 경로를 CDN URL로 변환해 내려준다.
  • 시리즈 커버 이미지 경로가 없거나 빈 문자열이면 coverImageUrlnull로 내려준다.
  • 호출 유저 언어코드는 기존 LangContext.lang.code 값을 사용한다.
  • 지원 언어코드는 ko, en, ja를 기준으로 한다.
  • isProceedingSeriesState.PROCEEDING이면 true, 그 외 상태이면 false로 내려준다.
  • contentCount는 조회 가능한 공개 콘텐츠 개수다.
  • paidContentCount는 같은 필터를 적용한 콘텐츠 중 price > 0인 콘텐츠 개수다.
  • purchasedContentCount는 같은 필터를 적용한 유료 콘텐츠 중 조회자가 유효하게 소장하거나 대여 중인 콘텐츠 개수다.
  • 대여 중인 콘텐츠는 구매한 콘텐츠 개수와 purchasedPaidContentRate 계산에 포함한다.
  • 유효 구매/대여 조건은 기존 오디오 탭과 동일하게 orders.is_active = true이며, 대여는 만료되지 않은 주문만 포함한다.

Edge Cases

  • 제목 번역 데이터는 있지만 빈 문자열이면 원문 시리즈명을 fallback으로 사용한다.
  • 콘텐츠가 없는 활성 시리즈는 시리즈 목록에 포함하되 contentCount, paidContentCount, purchasedContentCount0으로 계산한다.
  • 일반적으로 동일 콘텐츠의 소장과 대여가 동시에 발생하지 않지만, 데이터상 동시에 유효한 소장/대여 주문이 있으면 콘텐츠 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

  • 정렬 순서는 기존 공용 ContentSort enum을 사용한다.
  • 공개 요청/응답 값은 다음을 사용한다.
    • 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 계열 모델을 둔다.
  • 기존 ContentSort enum을 재사용하고, API binding, service 정책, 테스트에서 같은 타입을 사용한다.
  • 기존 크리에이터 채널 홈/라이브/오디오 API의 인증, 예외, 성인 콘텐츠 노출, 차단 관계 정책은 재사용한다.
  • 페이징 응답은 기존 오디오 탭 API와 같은 page, size, hasNext 패턴을 따른다.
  • 시리즈명 다국어 처리는 기존 SeriesTranslation 구조를 따른다.
  • 연재 요일 문구 다국어 처리는 서버 코드의 명시적 매핑으로 처리한다.

9. Metrics

  • 시리즈 탭 API 성공/실패 건수
  • 시리즈 탭 API 응답 시간
  • 정렬 기준별 조회 건수
  • 시리즈 탭에서 추가 로딩 요청 건수

10. Open Questions

  • 없음.