17 KiB
17 KiB
PRD: 크리에이터 채널 오디오 탭 API
1. Overview
크리에이터 채널의 오디오 탭에서 테마 목록, 정렬별 오디오 콘텐츠 개수, 소장률, 오디오 콘텐츠 목록을 한 번에 조회하는 API를 제공한다.
2. Problem
- 크리에이터 채널 오디오 탭은 테마 필터, 정렬 상태, 콘텐츠 개수, 소장률, 콘텐츠 목록을 함께 표시해야 한다.
- 기존 라이브 탭 API는
다시듣기콘텐츠에 한정되어 있고, 오디오 탭은 전체 오디오 콘텐츠와 선택한 테마별 콘텐츠를 조회해야 한다. - 클라이언트는 오디오 탭 진입 시 테마 리스트와 콘텐츠 목록을 별도 API 조합 없이 일관된 계약으로 받아야 한다.
- 테마명은 호출하는 유저의 언어코드에 따라 한글, 영문, 일본어로 표시되어야 한다.
- 기존 API endpoint와 응답 필드의 의미는 변경하지 않아야 한다.
3. Goals
- 크리에이터 채널 오디오 탭 조회 API를 제공한다.
- 클라이언트에서 호출하는 공개 API controller/facade/response DTO는
kr.co.vividnext.sodalive.v2.api.creator.channel.audio하위 조립 계층에 둔다. - 오디오 리스트, 오디오 개수, 소장률 계산, 테마 조회처럼 재사용 가능한 조회 책임은 API 패키지 밖의 도메인 패키지에 둔다.
- 요청은
creatorId, 정렬 순서, 테마를 받는다. - 정렬 순서를 보내지 않으면 최신순을 기본값으로 사용한다.
- 테마를 보내지 않으면 전체 테마의 오디오 콘텐츠를 조회한다.
- 응답에는 오디오 콘텐츠 개수, 유료 콘텐츠 개수, 호출자가 구매한 콘텐츠 개수, 호출자가 구매한 콘텐츠 개수의 비율, 크리에이터의 콘텐츠 목록, 실제 적용된 정렬 순서, 테마 목록을 포함한다.
- 콘텐츠 목록 item은 기존
CreatorChannelAudioContentResponse와 같은 필드/의미를 사용한다. - 오디오 콘텐츠 목록은 라이브 탭의
다시듣기목록과 같은 조회/정렬/소장 상태 의미를 따르되, 시리즈 이름이 표시되어야 한다. - 테마 목록은 테마 id와 호출 유저 언어코드에 맞는 테마명을 내려준다.
4. Non-Goals
- 이번 범위는 크리에이터 채널
오디오탭 조회 API만 포함한다. - 기존 크리에이터 채널 홈 API, 라이브 탭 API endpoint와 응답 필드의 의미는 변경하지 않는다.
- 오디오 콘텐츠 구매, 소장, 대여, 결제 API는 포함하지 않는다.
- 오디오 콘텐츠 생성/수정/삭제 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}/audio를 기본안으로 한다. creatorId는 path variable로 받는다.- 정렬 순서는 query parameter로 받는다.
- 정렬 순서 query parameter 이름은
sort를 기본안으로 한다. sort를 보내지 않으면LATEST를 기본값으로 사용한다.sort값이 없거나 기존ContentSortenum 값에 없으면LATEST로 fallback한다.- 테마는 query parameter로 받는다.
- 테마 query parameter 이름은
themeId를 기본안으로 한다. themeId를 보내지 않으면 전체 테마의 오디오 콘텐츠를 조회한다.- 오디오 콘텐츠 추가 로딩을 위해
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한다. themeId가 존재하지 않거나 비활성 테마이면 오류를 반환하지 않고 전체 테마 조회로 fallback한다.page가 0보다 작거나size가 허용 범위를 벗어나도 400 오류를 반환하지 않고 실제 적용값으로 보정한다.
Feature B. 응답 스키마
Requirements
- 응답 DTO는 구현 전에 명시하고 공개 API 계약으로 관리한다.
- 응답 최상위 DTO 이름은
CreatorChannelAudioTabResponse를 기본안으로 한다. - 응답에는 다음 값을 포함한다.
audioContentCount: 선택한 테마 필터를 적용한 오디오 콘텐츠 전체 개수paidAudioContentCount: 선택한 테마 필터를 적용한 유료 오디오 콘텐츠 전체 개수purchasedAudioContentCount: 선택한 테마 필터를 적용한 유료 오디오 콘텐츠 중 호출자가 구매한 콘텐츠 개수purchasedAudioContentRate:paidAudioContentCount대비purchasedAudioContentCount의 퍼센트 값themes: 활성 테마 목록audioContents: 오디오 콘텐츠 목록sort: 콘텐츠 조회에 실제 적용한 정렬 순서themeId: 콘텐츠 조회에 실제 적용한 테마 id, 전체 조회이면nullpage: 현재 응답의 page indexsize: 현재 응답의 page sizehasNext: 다음 page 존재 여부
audioContents의 각 item은 기존CreatorChannelAudioContentResponse와 같은 필드/의미를 사용한다.sort는 요청값이 없거나 알 수 없는 값이면 실제 적용값인LATEST를 내려준다.themeId는 요청값이 없거나 비활성/미존재 테마라 전체 조회로 fallback하면null을 내려준다.page,size는 fallback 보정 이후 실제 적용된 값을 내려준다.hasNext는 같은 필터/정렬 조건에서 다음 page에 노출할 오디오 콘텐츠가 있으면true로 내려준다.purchasedAudioContentRate는paidAudioContentCount == 0이면0.0으로 내려준다.purchasedAudioContentRate는(purchasedAudioContentCount / paidAudioContentCount) * 100을 기준으로 계산한 퍼센트 값으로 내려준다.- 응답 스키마 예시는 다음과 같다.
data class CreatorChannelAudioTabResponse(
val audioContentCount: Int,
val paidAudioContentCount: Int,
val purchasedAudioContentCount: Int,
val purchasedAudioContentRate: Double,
val themes: List<CreatorChannelAudioThemeResponse>,
val audioContents: List<CreatorChannelAudioContentResponse>,
val sort: ContentSort,
val themeId: Long?,
val page: Int,
val size: Int,
val hasNext: Boolean
)
data class CreatorChannelAudioThemeResponse(
val themeId: Long,
val themeName: String
)
data class CreatorChannelAudioContentResponse(
val audioContentId: Long,
val title: String,
val duration: String?,
val imageUrl: String?,
val price: Int,
val isAdult: Boolean,
val isPointAvailable: Boolean,
val isFirstContent: Boolean,
val seriesName: String?,
val isOriginalSeries: Boolean?,
val isOwned: Boolean,
val isRented: Boolean
)
enum class ContentSort {
LATEST,
POPULAR,
OWNED,
PRICE_HIGH,
PRICE_LOW
}
Edge Cases
- 공개된 오디오 콘텐츠가 없으면
audioContentCount,paidAudioContentCount,purchasedAudioContentCount는0,purchasedAudioContentRate는0.0,audioContents는 빈 배열,hasNext는false로 내려준다. - 요청한 page 범위에 콘텐츠가 없으면
audioContents는 빈 배열,hasNext는false로 내려주되 개수 필드는 전체 개수를 유지한다.
Feature C. 테마 목록
Requirements
- 테마 목록은
AudioContentTheme.isActive == true인 테마만 내려준다. - 테마 목록은 기존 테마 정렬 정책인
AudioContentTheme.orders를 따른다. - 테마 응답은 테마 id와 테마명을 포함한다.
- 테마명은 호출하는 유저의 언어코드에 따라 한글, 영문, 일본어로 반환한다.
- 호출 유저 언어코드는 기존
LangContext.lang.code값을 사용한다. - 지원 언어코드는
ko,en,ja를 기준으로 한다. ko는AudioContentTheme.theme원문을 기본으로 사용한다.en,ja는ContentThemeTranslation.locale에 해당하는 번역값이 있으면 해당theme을 사용한다.- 요청 언어의 번역값이 없으면
AudioContentTheme.theme원문을 fallback으로 사용한다. - 테마 목록은 선택한
themeId와 무관하게 활성 테마 중 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 1개 이상 있는 테마만 내려준다.
Edge Cases
- 활성 테마가 없으면
themes는 빈 배열로 내려준다. - 활성 테마가 있어도 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 없는 테마는
themes에서 제외한다. - 조회자의 성인 콘텐츠 노출 정책이 false이고 특정 테마의 조회 가능한 콘텐츠가 성인 콘텐츠뿐이면 해당 테마는
themes에서 제외한다. - 번역 데이터는 있지만 빈 문자열이면 원문 테마명을 fallback으로 사용한다.
Feature D. 오디오 콘텐츠 목록과 개수
Requirements
- 조회 대상은 지정한
creatorId의 오디오 콘텐츠로 제한한다. - 공개된 콘텐츠만 조회한다.
- 예약 공개 전 콘텐츠는 포함하지 않는다.
releaseDate == null인 오디오 콘텐츠는 삭제/미공개 데이터로 보고 조회에서 제외한다.themeId가 있고 활성 테마이면 해당 테마의 오디오 콘텐츠만 조회한다.themeId가 없거나 비활성/미존재 테마이면 전체 활성 테마의 오디오 콘텐츠를 조회한다.- 콘텐츠 목록은
page,size기준으로 페이징 조회한다. - 다음 page 존재 여부는
size + 1개를 조회하거나 동등한 방식으로 판단하되, 응답 목록에는 최대size개만 내려준다. - 오디오 콘텐츠 개수는 목록 조회와 같은 공개 여부, 예약 공개, 성인 콘텐츠, 차단 정책, 테마 필터를 적용해 계산한다.
- 유료 콘텐츠 개수는 같은 필터를 적용한 콘텐츠 중
price > 0인 콘텐츠 개수로 계산한다. - 호출자가 구매한 콘텐츠 개수는 같은 필터를 적용한 유료 콘텐츠 중 조회자가 유효하게 소장하거나 대여 중인 콘텐츠 개수로 계산한다.
- 대여 중인 콘텐츠는 호출자가 구매한 콘텐츠 개수와
purchasedAudioContentRate계산에 포함한다. - 조회자의 성인 콘텐츠 노출 정책이 false이면 성인 콘텐츠는 목록과 개수에서 제외한다.
- 응답 item 필드는 기존
CreatorChannelAudioContentResponse와 같은 의미를 유지한다. seriesName은 콘텐츠가 속한 시리즈 이름을 내려준다.- 시리즈 이름은 호출 유저 언어코드에 맞는 번역값이 있으면 번역명을 사용하고, 없으면 원문 시리즈명을 사용한다.
isFirstContent,seriesName,isOriginalSeries, 구매/대여/포인트 사용 가능 여부의 의미는 기존 오디오 콘텐츠/시리즈 콘텐츠 응답과 동일하게 유지한다.isFirstContent는 선택한 테마 안에서 첫 콘텐츠인지가 아니라, 크리에이터의 전체 공개 오디오 콘텐츠 중 첫 콘텐츠인지로 판단한다.
Edge Cases
- 시리즈에 속하지 않은 콘텐츠는
seriesName,isOriginalSeries를null로 내려준다. - 무료 콘텐츠는 구매한 콘텐츠 개수와 구매 비율 계산에서 제외한다.
- 일반적으로
isOwned == true와isRented == true가 동시에 발생하지 않지만, 데이터상 동시에 유효한 소장/대여 주문이 있으면 둘 다true로 내려준다.
Feature E. 콘텐츠 정렬
Requirements
- 정렬 순서는 기존 공용
ContentSortenum을 사용한다. - 공개 요청/응답 값은 다음을 사용한다.
LATEST: 최신순, 기본값POPULAR: 인기순OWNED: 소장순PRICE_HIGH: 높은 가격순PRICE_LOW: 낮은 가격순
LATEST는 공개일 최신순을 1차 정렬로 사용한다.LATEST의 2차 정렬은 높은 가격순이다.LATEST의 3차 정렬은audioContent.id desc다.POPULAR은 매출이 많은 콘텐츠를 먼저 노출한다.OWNED는 조회자가 소장 또는 대여 중인 콘텐츠를 먼저 노출한다.PRICE_HIGH는 가격이 높은 콘텐츠를 먼저 노출한다.PRICE_LOW는 가격이 낮은 콘텐츠를 먼저 노출한다.POPULAR,OWNED,PRICE_HIGH,PRICE_LOW의 2차 정렬은 최신순이다.POPULAR,OWNED,PRICE_HIGH,PRICE_LOW의 3차 정렬은audioContent.id desc다.- 최신순 기준에 사용하는 날짜는 기존
CreatorChannelAudioContentResponse목록 정책과 동일하게 공개 시각(releaseDate)을 기준으로 한다. - 인기순의 매출은 대여/소장 여부와 관계없이 해당 콘텐츠에 순수하게 결제된 캔 매출 합계(
orders.can)를 기준으로 하며, 포인트 사용액은 매출 기준에 포함하지 않는다. - 환불되었거나 비활성 처리된 구매 내역은 기존 콘텐츠 구매/매출 정책과 동일하게 제외한다.
Edge Cases
- 매출이 없는 콘텐츠의 인기순 매출값은 0으로 처리한다.
- 조회자가 소장 또는 대여 중인 콘텐츠가 없으면
OWNED정렬은 최신순 +audioContent.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.audio하위에 둔다. - API 조립 계층은 HTTP 계약과 공개 응답 변환만 담당한다.
- 도메인 조회 코드는
kr.co.vividnext.sodalive.v2.creator.channel.audio또는 재사용 범위가 더 넓은 기존 도메인 패키지 하위에 둔다. - 도메인 패키지는
kr.co.vividnext.sodalive.v2.api.*패키지에 의존하지 않는다. - 기존 라이브 탭의
CreatorChannelAudioContentResponse와 필드/의미가 어긋나지 않도록 오디오 탭 응답 DTO를 작성한다. - 기존
ContentSortenum을 재사용하고, API binding, service 정책, 테스트에서 같은 타입을 사용한다. - 기존 크리에이터 채널 홈/라이브 API의 인증, 예외, 성인 콘텐츠 노출, 차단 관계 정책은 재사용한다.
- 페이징 응답은 기존 라이브 탭 API와 같은
page,size,hasNext패턴을 따른다. - 테마명 다국어 처리는 기존
LangContext,ContentThemeTranslation구조를 따른다. - 시리즈명 다국어 처리는 기존
SeriesTranslation구조를 따른다.
9. Metrics
- 오디오 탭 API 성공/실패 건수
- 오디오 탭 API 응답 시간
- 테마별 조회 건수
- 정렬 기준별 조회 건수
- 오디오 탭에서 콘텐츠 추가 로딩 요청 건수