18 KiB
18 KiB
PRD: 크리에이터 채널 라이브 API
1. Overview
크리에이터 채널의 라이브 탭에서 현재 진행 중인 라이브와 다시듣기 콘텐츠를 한 번에 조회하는 API를 제공한다.
2. Problem
- 크리에이터 채널 홈 API는 홈 화면에 필요한 요약 데이터를 제공하지만, 라이브 탭은 현재 라이브와
다시듣기콘텐츠 목록/개수를 함께 조회해야 한다. - 클라이언트는 라이브 탭 진입 시 현재 진행 중인 라이브,
다시듣기콘텐츠 목록, 전체 개수, 적용된 정렬 순서를 일관된 계약으로 받아야 한다. 다시듣기콘텐츠 정렬 기준이 여러 개이고 이후 오디오 콘텐츠, 시리즈, 화보 등 채널 내 다른 콘텐츠 목록에서도 같은 정렬 기준을 사용할 예정이므로 서버와 클라이언트가 공유할 명시적인 enum 계약이 필요하다.- 응답 필드는 기존
CreatorChannelLiveResponse,CreatorChannelAudioContentResponse와 의미가 어긋나지 않아야 한다.
3. Goals
- 크리에이터 채널 라이브 탭 조회 API를 제공한다.
- 클라이언트에서 호출하는 공개 API는
kr.co.vividnext.sodalive.v2.api.creator.channel.live하위 조립 계층에 둔다. - 라이브, 다시듣기 콘텐츠, 시리즈/소장 상태처럼 재사용 가능한 조회 책임은 API 패키지 밖의 도메인 패키지에 둔다.
- 요청은
creatorId와 정렬 순서를 받는다. - 정렬 순서를 보내지 않으면 최신순을 기본값으로 사용한다.
- 응답에는
다시듣기콘텐츠 개수, 현재 진행 중인 라이브,다시듣기콘텐츠 목록, 실제 적용된 정렬 순서를 포함한다. - 현재 진행 중인 라이브 응답은 기존
CreatorChannelLiveResponse와 동일한 필드/의미를 사용한다. 다시듣기콘텐츠 응답은 기존CreatorChannelAudioContentResponse에 유료 콘텐츠의 소장/대여 상태를 추가해 사용한다.다시듣기콘텐츠는 기존 프로젝트에서 사용하는AudioContentTheme.theme == "다시듣기"기준을 따른다.- 정렬 순서는 enum으로 정의해 공개 API 계약을 고정한다.
4. Non-Goals
- 이번 범위는 크리에이터 채널
라이브탭 조회 API만 포함한다. - 기존 크리에이터 채널 홈 API endpoint와 기존 응답 필드의 의미는 변경하지 않는다.
- 기존 크리에이터 채널 홈 API를
v2.api.*조립 계층 + 도메인 패키지 구조로 옮기는 리팩토링은 이번 범위에서 구현하지 않는다. - 라이브 생성, 예약, 입장, 종료 API는 포함하지 않는다.
- 오디오 콘텐츠 구매, 소장, 대여, 결제 API는 포함하지 않는다.
다시듣기테마명 관리 화면 또는 테마 마이그레이션은 포함하지 않는다.- 앱 표시용 다국어 문구, 날짜 포맷, 가격 단위 표시는 서버에서 처리하지 않는다.
5. Target Users
- 회원: 크리에이터 채널 라이브 탭에서 현재 라이브와 다시듣기 콘텐츠를 탐색하는 사용자
- 앱 클라이언트: 라이브 탭 구성에 필요한 데이터를 단일 API 응답으로 표시하려는 클라이언트
- 크리에이터: 자신의 현재 라이브와 다시듣기 콘텐츠가 적절한 정렬로 노출되기를 원하는 사용자
6. User Stories
- 사용자는 크리에이터 채널 라이브 탭에 들어가면 현재 진행 중인 라이브가 있는지 바로 확인하고 싶다.
- 사용자는 크리에이터의
다시듣기콘텐츠를 최신순으로 보고 싶다. - 사용자는 인기순, 소장순, 높은 가격순, 낮은 가격순으로
다시듣기콘텐츠를 바꿔 보고 싶다. - 앱 클라이언트는 현재 적용된 정렬 순서를 응답에서 확인해 화면 상태와 서버 조회 결과를 맞추고 싶다.
- 앱 클라이언트는
다시듣기콘텐츠 전체 개수를 받아 탭/헤더/빈 상태 UI에 표시하고 싶다.
7. Core Features
Feature A. 크리에이터 채널 라이브 조회 API
Requirements
- 신규 API는 크리에이터 채널 전용 v2 API로 작성한다.
- 클라이언트 공개 API controller/facade/response DTO는
kr.co.vividnext.sodalive.v2.api.creator.channel.live하위에 작성한다. - API 조립 계층은 필요한 도메인 조회 서비스를 호출해 라이브 탭 응답을 조립한다.
- API 조립 계층이 호출하는 도메인 조회 코드는
kr.co.vividnext.sodalive.v2.live,kr.co.vividnext.sodalive.v2.content,kr.co.vividnext.sodalive.v2.series또는 채널 문맥이 필요한 경우kr.co.vividnext.sodalive.v2.creator.channel.live하위에 둔다. - 도메인 패키지는
kr.co.vividnext.sodalive.v2.api.*패키지에 의존하지 않는다. - API endpoint는
GET /api/v2/creator-channels/{creatorId}/live를 기본안으로 한다. creatorId는 path variable로 받는다.- 정렬 순서는 query parameter로 받는다.
- 정렬 순서 query parameter 이름은
sort를 기본안으로 한다. sort를 보내지 않으면LATEST를 기본값으로 사용한다.다시듣기콘텐츠 추가 로딩을 위해page,sizequery parameter를 받는다.page는 0부터 시작하는 page index로 처리한다.size를 보내지 않으면 기본값20을 사용한다.page를 보내지 않으면 기본값0을 사용한다.- API는 인증 회원만 조회할 수 있어야 한다.
- 비회원이 조회하면 기존 인증 필요 API와 동일하게
common.error.bad_credentials계열 오류를 반환한다. - 조회 대상 회원이 존재하지 않으면 기존 정책과 동일하게
member.validation.user_not_found계열 오류를 반환한다. - 조회 대상 회원이 크리에이터가 아니면 기존 정책과 동일하게
member.validation.creator_not_found계열 오류를 반환한다. - 조회자와 크리에이터 사이에 차단 관계가 있으면 구버전 채널 접근 정책과 동일하게 접근 차단 오류를 반환한다.
- 현재 진행 중인 라이브가 없거나
다시듣기콘텐츠가 없어도 전체 API는 성공 처리한다.
Edge Cases
- 조회자 본인이 크리에이터인 경우에도 같은 응답 스키마를 사용한다.
- 알 수 없는
sort값은 Spring enum binding 실패 또는 기존 validation 오류 흐름에 맞춰 400 계열 오류로 처리한다. page가 0보다 작거나size가 20보다 작거나 50보다 크면 기존 validation 오류 흐름에 맞춰 400 계열 오류로 처리한다.
Feature B. 응답 스키마
Requirements
- 응답 DTO는 구현 전에 명시하고 공개 API 계약으로 관리한다.
- 응답 최상위 DTO 이름은
CreatorChannelLiveTabResponse를 기본안으로 한다. - 응답에는 다음 값을 포함한다.
liveReplayContentCount:다시듣기카테고리 콘텐츠 전체 개수currentLive: 현재 진행 중인 라이브, 없으면nullliveReplayContents:다시듣기콘텐츠 목록sort: 콘텐츠 조회에 실제 적용한 정렬 순서page: 현재 응답의 page indexsize: 현재 응답의 page sizehasNext: 다음 page 존재 여부
currentLive는 기존CreatorChannelLiveResponse와 동일한 필드/의미를 사용한다.liveReplayContents의 각 item은 기존CreatorChannelAudioContentResponse를 사용한다.CreatorChannelAudioContentResponse에는 다음 범위의 오디오 콘텐츠 조회 API에서도 재사용할 수 있도록isOwned,isRented를 추가한다.sort는 요청값이 없으면 기본값LATEST를 내려준다.page,size는 실제 적용된 값을 내려준다.hasNext는 같은 필터/정렬 조건에서 다음 page에 노출할다시듣기콘텐츠가 있으면true로 내려준다.- 응답 스키마 예시는 다음과 같다.
data class CreatorChannelLiveTabResponse(
val liveReplayContentCount: Int,
val currentLive: CreatorChannelLiveResponse?,
val liveReplayContents: List<CreatorChannelAudioContentResponse>,
val sort: ContentSort,
val page: Int,
val size: Int,
val hasNext: Boolean
)
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
- 현재 진행 중인 라이브가 없으면
currentLive는null로 내려준다. 다시듣기콘텐츠가 없으면liveReplayContentCount는0,liveReplayContents는 빈 배열,hasNext는false로 내려준다.
Feature C. 현재 진행 중인 라이브
Requirements
- 크리에이터가 현재 진행 중인 라이브를 내려준다.
- 현재 진행 중인 라이브는 기존 라이브 도메인의
LiveRoomStatus.NOW의미와 동일하게 판단한다. - 응답 필드는 기존
CreatorChannelLiveResponse와 동일하게 다음 값을 포함한다.liveIdtitlecoverImageUrlbeginDateTimeUtcpriceisAdult
- 조회자의 성인 콘텐츠 노출 정책과 차단 정책을 반영한다.
- 현재 라이브 노출은 기존 라이브 목록 정책과 동일하게 성별 제한(
LiveRoom.genderRestriction)과 크리에이터 입장 제한(LiveRoom.isAvailableJoinCreator)을 반영한다. - 성별 제한 판단에 사용하는 조회자 성별은 기존 라이브 목록과 동일하게
Auth.gender가 있으면 이를 우선하고, 없으면Member.gender를 사용하는 effective gender다.
Edge Cases
- 현재 진행 중인 라이브 후보가 여러 개이면 기존 라이브 목록/홈 API의 현재 라이브 선택 정책을 따른다.
- 성인 콘텐츠 노출 정책상 볼 수 없는 라이브만 있으면
currentLive는null로 내려준다.
Feature D. 다시듣기 콘텐츠 목록과 개수
Requirements
다시듣기콘텐츠는AudioContentTheme.theme == "다시듣기"인 오디오 콘텐츠를 의미한다.AudioContentTheme.isActive == true인 테마만 대상으로 한다.- 조회 대상은 지정한
creatorId의 콘텐츠로 제한한다. - 공개된 콘텐츠만 조회한다.
- 예약 공개 전 콘텐츠는 포함하지 않는다.
releaseDate == null인 오디오 콘텐츠는 삭제/미공개 데이터로 보고 조회에서 제외한다.- 응답 item 필드는 기존
CreatorChannelAudioContentResponse와 동일하게 다음 값을 포함한다.audioContentIdtitledurationimageUrlpriceisAdultisPointAvailableisFirstContentseriesNameisOriginalSeries
CreatorChannelAudioContentResponse에는 유료 콘텐츠 상태 표시를 위해 다음 값을 추가한다.isOwned: 조회자가 해당 콘텐츠를 소장 중이면trueisRented: 조회자가 해당 콘텐츠를 대여 중이고 대여 기간이 유효하면true
- 무료 콘텐츠 또는 조회자가 구매/대여하지 않은 콘텐츠는
isOwned == false,isRented == false로 내려준다. - 일반적으로
isOwned == true와isRented == true가 동시에 발생하지 않지만, 데이터상 동시에 유효한 소장/대여 주문이 있으면 둘 다true로 내려준다. - 콘텐츠 개수는 같은 필터를 적용한
다시듣기콘텐츠 전체 개수로 계산한다. - 콘텐츠 목록은
page,size기준으로 페이징 조회한다. - 기본 page size는 20개다.
- 클라이언트는
hasNext == true이면 같은creatorId,sort,size와 다음page값으로 추가 로딩할 수 있어야 한다. - 다음 page 존재 여부는
size + 1개를 조회하거나 동등한 방식으로 판단하되, 응답 목록에는 최대size개만 내려준다. - 목록 조회와 개수 조회는 성인 콘텐츠 노출 정책, 차단 정책, 공개 여부 필터가 서로 어긋나지 않아야 한다.
- 조회자의 성인 콘텐츠 노출 정책이 false이면 성인 콘텐츠는 목록과 개수에서 제외한다.
isFirstContent,seriesName,isOriginalSeries, 구매/대여/포인트 사용 가능 여부의 의미는 기존 오디오 콘텐츠/시리즈 콘텐츠 응답과 동일하게 유지한다.isFirstContent는다시듣기테마 안에서 첫 콘텐츠인지가 아니라, 크리에이터의 전체 공개 오디오 콘텐츠 중 첫 콘텐츠인지로 판단한다.
Edge Cases
- 시리즈에 속하지 않은 콘텐츠는
seriesName,isOriginalSeries를null로 내려준다. - 공개된
다시듣기콘텐츠가 없으면 빈 배열을 내려준다. - 요청한 page 범위에 콘텐츠가 없으면
liveReplayContents는 빈 배열,hasNext는false로 내려주되liveReplayContentCount는 전체 개수를 유지한다.
Feature E. 콘텐츠 정렬
Requirements
- 정렬 순서는 enum으로 처리한다.
- enum 이름은
ContentSort를 기본안으로 한다. ContentSort는 크리에이터 채널에 한정하지 않고, 서비스 전반에서 콘텐츠 목록 정렬이 필요할 때 재사용할 수 있는 공용 정렬 enum으로 둔다.ContentSort파일 위치는 구현 시kr.co.vividnext.sodalive.v2.common.domain.ContentSort를 기본 후보로 한다.ContentSort는 라이브 탭의다시듣기콘텐츠뿐 아니라 다음 범위의 오디오 콘텐츠, 시리즈, 화보 등 콘텐츠형 목록에서 같은 정렬 의미를 공유한다.- 공개 요청/응답 값은 다음을 사용한다.
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)를 기준으로 하며, 포인트 사용액은 매출 기준에 포함하지 않는다. - 환불되었거나 비활성 처리된 구매 내역은 기존 콘텐츠 구매/매출 정책과 동일하게 제외한다.
- 소장순은 조회자가 해당 콘텐츠를 유효하게 소장 또는 구매한 상태를 기준으로 한다.
- 같은 1차/2차 정렬 값을 가진 항목은
audioContent.id desc로 안정적으로 정렬한다.
Edge Cases
- 매출이 없는 콘텐츠의 인기순 매출값은 0으로 처리한다.
- 조회자가 소장한 콘텐츠가 없으면
OWNED정렬은 최신순 +audioContent.id desc보조 정렬과 같은 결과가 될 수 있다. - 가격이 같은 콘텐츠는 각 정렬의 2차/3차 기준을 따른다.
8. Technical Constraints
- 빌드 도구는 Gradle Wrapper(
./gradlew)를 사용한다. - Kotlin + Spring Boot 2.7.14 기존 스타일을 따른다.
- 신규 공개 API 스키마는 구현 전에 PRD와 구현 계획/TASK 문서에 명시한다.
- 기존
CreatorChannelLiveResponse,CreatorChannelAudioContentResponse와 필드/의미가 어긋나지 않도록 라이브 탭 API 응답 DTO를 작성한다. - 라이브 탭 API의
CreatorChannelAudioContentResponse에는isOwned,isRented를 포함하고, 다음 범위의 오디오 콘텐츠 조회 API에서도 같은 의미를 재사용할 수 있게 한다. - 기존 크리에이터 채널 홈 API의 인증, 예외, 성인 콘텐츠 노출, 차단 관계 정책은 재사용하되, 신규 공개 API 파일 위치는
v2.api.*조립 계층을 따른다. - 기존 크리에이터 채널 홈 API가
v2.creator.channel.adapter.in.web에 위치한 것은 현재 구조의 예외로 보고, 이번 라이브 탭 구현에서는 같은 예외를 확장하지 않는다. - 페이징 응답은 기존 v2 홈 추천 페이지 응답과 같은
page,size,hasNext패턴을 따른다. 다시듣기테마명은 기존 코드의 문자열 상수와 중복되지 않도록 구현 시 공용 상수 또는 정책 객체로 관리하는 방안을 검토한다.ContentSort는 API binding, service 정책, 테스트에서 같은 타입을 사용한다.
9. Metrics
- 라이브 탭 API 성공/실패 건수
- 라이브 탭 API 응답 시간
- 정렬 순서별 요청 건수
currentLive가 있는 응답 비율다시듣기콘텐츠 개수와 실제 목록 노출 개수
10. Resolved Decisions
- 인기순 매출 기준은 대여/소장 여부와 관계없이 순수하게 결제된 캔 매출만 사용한다.
- 라이브 탭 신규 API는 기존 크리에이터 채널 홈 API 위치를 따라가지 않고,
v2.api.creator.channel.live공개 API 조립 계층으로 작성한다. - 기존 크리에이터 채널 홈 API의 패키지 구조 정렬은 이번 라이브 탭 구현과 분리해 다음 범위에서 별도 리팩토링한다.
isOwned == true와isRented == true가 동시에 발생할 가능성은 없지만, 만약 그런 상황이 발생하면 둘 다true로 내려준다.