Files
sodalive-backend-spring-boot/docs/20260617_크리에이터_채널_라이브_API/prd.md

272 lines
18 KiB
Markdown

# 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`, `size` query 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`: 현재 진행 중인 라이브, 없으면 `null`
- `liveReplayContents`: `다시듣기` 콘텐츠 목록
- `sort`: 콘텐츠 조회에 실제 적용한 정렬 순서
- `page`: 현재 응답의 page index
- `size`: 현재 응답의 page size
- `hasNext`: 다음 page 존재 여부
- `currentLive`는 기존 `CreatorChannelLiveResponse`와 동일한 필드/의미를 사용한다.
- `liveReplayContents`의 각 item은 기존 `CreatorChannelAudioContentResponse`를 사용한다.
- `CreatorChannelAudioContentResponse`에는 다음 범위의 오디오 콘텐츠 조회 API에서도 재사용할 수 있도록 `isOwned`, `isRented`를 추가한다.
- `sort`는 요청값이 없으면 기본값 `LATEST`를 내려준다.
- `page`, `size`는 실제 적용된 값을 내려준다.
- `hasNext`는 같은 필터/정렬 조건에서 다음 page에 노출할 `다시듣기` 콘텐츠가 있으면 `true`로 내려준다.
- 응답 스키마 예시는 다음과 같다.
```kotlin
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`와 동일하게 다음 값을 포함한다.
- `liveId`
- `title`
- `coverImageUrl`
- `beginDateTimeUtc`
- `price`
- `isAdult`
- 조회자의 성인 콘텐츠 노출 정책과 차단 정책을 반영한다.
- 현재 라이브 노출은 기존 라이브 목록 정책과 동일하게 성별 제한(`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`와 동일하게 다음 값을 포함한다.
- `audioContentId`
- `title`
- `duration`
- `imageUrl`
- `price`
- `isAdult`
- `isPointAvailable`
- `isFirstContent`
- `seriesName`
- `isOriginalSeries`
- `CreatorChannelAudioContentResponse`에는 유료 콘텐츠 상태 표시를 위해 다음 값을 추가한다.
- `isOwned`: 조회자가 해당 콘텐츠를 소장 중이면 `true`
- `isRented`: 조회자가 해당 콘텐츠를 대여 중이고 대여 기간이 유효하면 `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`로 내려준다.