19 KiB
19 KiB
PRD: 메인 콘텐츠 전체 탭 API
1. Overview
메인 콘텐츠 탭의 내부 전체 탭에서 오디오, 시리즈, 오리지널, 무료, 포인트 구분별 공개 콘텐츠를 정렬과 페이징으로 조회하는 v2 API를 제공한다.
2. Problem
- 기존 메인 콘텐츠 추천 탭 API는 여러 추천 섹션을 한 번에 조립하지만, 전체 탭은 사용자가 선택한 구분별 전체 콘텐츠 목록과 전체 개수, 정렬 상태, 페이징 상태를 제공해야 한다.
- 기존 크리에이터 채널 오디오/시리즈 탭 API는 특정 크리에이터 기준 조회라서, 전체 탭처럼 차단 관계가 아닌 모든 크리에이터의 공개 콘텐츠를 대상으로 하기 어렵다.
- 정렬 기준은 기존 공용
ContentSortenum과 의미를 공유해야 하며, 인기순 매출 산식은 포인트 사용액을 제외한orders.can합계로 명확히 고정해야 한다. - V2 패키지에는 API 조립 계층과 도메인 조회 계층 분리, 공통 오디오 카드 DTO, 차단/성인 콘텐츠/공개 콘텐츠 필터, 시리즈 정렬 패턴이 이미 있으므로 재사용 범위를 먼저 명시해야 한다.
3. Goals
- 메인 콘텐츠 전체 탭 조회 API를
kr.co.vividnext.sodalive.v2하위 신규 코드로 제공한다. - 기존 패턴과 동일하게 API 조립 계층과 도메인 조회 계층을 분리한다.
- 구분은
AUDIO,SERIES,ORIGINAL,FREE,POINT를 지원한다. - 공개된 콘텐츠만 조회한다.
- 회원이 차단했거나 회원을 차단한 크리에이터의 콘텐츠는 노출하지 않는다.
- 비회원은 19금 콘텐츠를 노출하지 않는다.
- 인증 회원은 기존 콘텐츠 조회 설정에 따라 19금 콘텐츠 노출 가능 여부를 반영한다.
- 전체 콘텐츠 개수와 페이징 목록을 함께 응답한다.
- 정렬 순서는 기존 공용
ContentSortenum을 사용한다. SERIES구분은 legacy 시리즈 메인 요일별 조회와 동일하게 요일 선택을 지원한다.- PRD에 API endpoint와 Response data class 초안을 포함한다.
4. Non-Goals
- 기존
content.main.tab.*legacy API 스키마를 변경하지 않는다. - 기존 메인 콘텐츠 추천 탭 API와 랭킹 탭 API의 공개 스키마를 변경하지 않는다.
- 기존 크리에이터 채널 오디오/시리즈 탭 API의 endpoint, 응답 필드, 인증 정책을 변경하지 않는다.
- 신규 스냅샷 테이블이나 배치 집계는 이번 범위에 포함하지 않는다.
- 개인화 추천, 랜덤 노출, 운영자 고정/제외 기능은 포함하지 않는다.
- 구매, 대여, 소장, 포인트 결제 API는 포함하지 않는다.
ContentSortenum에 신규 값을 추가하지 않는다.OWNED정렬은 전체 탭 요구사항에 포함하지 않는다.
5. Target Users
- 회원: 메인 콘텐츠 전체 탭에서 원하는 구분의 공개 콘텐츠를 정렬해 탐색하는 사용자
- 비회원: 인증 없이 조회 가능한 공개 콘텐츠를 탐색하는 사용자
- 앱 클라이언트: 전체 탭의 구분, 전체 개수, 정렬 상태, 페이징 목록을 단일 계약으로 구성하려는 클라이언트
6. User Stories
- 사용자는 오디오 콘텐츠 전체 목록을 최신순, 인기순, 가격순으로 보고 싶다.
- 사용자는 선택한 요일의 시리즈 목록을 보고 싶다.
- 사용자는 오리지널 시리즈만 따로 보고 싶다.
- 사용자는 무료 오디오만 따로 보고 싶다.
- 사용자는 포인트를 사용할 수 있는 오디오만 따로 보고 싶다.
- 앱 클라이언트는 현재 적용된 구분, 정렬, page, size, hasNext를 응답에서 확인해 화면 상태와 서버 결과를 맞추고 싶다.
7. Core Features
Feature A. 메인 콘텐츠 전체 탭 조회 API
Requirements
- 신규 API endpoint는
GET /api/v2/audio/contents를 기본안으로 한다. - 응답 wrapper는 기존 패턴과 동일하게
ApiResponse.ok(...)를 사용한다. - 비회원 조회를 허용한다.
- 회원 조회 시
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?패턴을 사용한다. - 요청 query parameter는
type,sort,dayOfWeek,page,size를 사용한다. type값은 아래 enum으로 정의한다.AUDIO: 오디오SERIES: 시리즈ORIGINAL: 오리지널FREE: 무료POINT: 포인트
type을 보내지 않으면AUDIO를 기본값으로 사용한다.sort를 보내지 않으면LATEST를 기본값으로 사용한다.sort값이 없거나 기존ContentSortenum 값에 없으면LATEST로 fallback한다.- 전체 탭에서 지원하는 정렬 값은
LATEST,POPULAR,PRICE_HIGH,PRICE_LOW다. OWNED가 들어오면 전체 탭 요구사항에 없는 정렬이므로LATEST로 fallback한다.dayOfWeek는type=SERIES일 때만 적용한다.dayOfWeek값은 legacySeriesMainController.getDayOfWeekSeriesList(...)와 동일하게SeriesPublishedDaysOfWeekenum 값을 사용한다.dayOfWeek지원 값은SUN,MON,TUE,WED,THU,FRI,SAT,RANDOM이다.dayOfWeek를 보내지 않으면 전체 요일의 시리즈를 조회한다.type이SERIES가 아니면dayOfWeek는 조회 조건에 적용하지 않고 응답에서는null로 내려준다.page는 0부터 시작하는 page index로 처리한다.page를 보내지 않으면 기본값0을 사용한다.size를 보내지 않으면 기본값20을 사용한다.page가 0보다 작으면0으로 fallback한다.size가 20보다 작으면20으로 fallback한다.size가 50보다 크면50으로 fallback한다.- 응답에는 같은 필터 조건의 전체 콘텐츠 개수와 현재 page 목록을 포함한다.
- 다음 page 존재 여부는
size + 1개 조회 또는 동등한 방식으로 판단하되, 응답 목록에는 최대size개만 내려준다.
Edge Cases
- 공개된 콘텐츠가 없으면
totalCount는0, 목록은 빈 배열,hasNext는false로 내려준다. - 요청한 page 범위에 콘텐츠가 없으면 목록은 빈 배열,
hasNext는false로 내려주되totalCount는 전체 개수를 유지한다. - 특정 구분에서 지원하지 않는 응답 목록 필드는 빈 배열로 내려준다.
Feature B. 공통 공개/차단/성인 콘텐츠 정책
Requirements
- 모든 구분은 공개 가능한 콘텐츠만 조회한다.
- 오디오 콘텐츠는
isActive == true,duration != null,releaseDate != null,releaseDate <= now, 활성 테마, 활성 크리에이터 조건을 만족해야 한다. - 시리즈는
isActive == true, 활성 크리에이터 조건을 만족해야 한다. - 시리즈의 콘텐츠 통계와 정렬 대표값은 공개 가능한 오디오 콘텐츠만 기준으로 계산한다.
- 회원이 차단했거나 회원을 차단한 크리에이터의 오디오/시리즈는 제외한다.
- 비회원은 19금 오디오/시리즈를 제외한다.
- 인증 회원은
MemberContentPreferenceService의 기존 성인 콘텐츠 노출 가능 여부를 반영한다. - 이미지 경로는 기존
v2.common.domain.CdnUrlExtensions의 CDN URL 변환 패턴을 따른다.
Edge Cases
- 차단 관계가 있는 크리에이터의 시리즈에 속한 오디오도 조회 대상에서 제외한다.
- 예약 공개 전 오디오는 모든 구분의 목록, 개수, 정렬 대표값, 매출 집계에서 제외한다.
- 비활성 크리에이터의 콘텐츠는 모든 구분에서 제외한다.
Feature C. 오디오 구분
Requirements
type=AUDIO는 차단 관계가 아닌 모든 크리에이터의 공개 오디오 콘텐츠를 조회한다.- 전체 개수는 같은 공개/차단/성인 콘텐츠 조건을 적용한 오디오 콘텐츠 개수다.
- 응답 목록은
audios에 내려주고series는 빈 배열로 내려준다. - 응답 item은 기존 추천 탭의
AudioCardResponse필드 의미를 우선 재사용한다.
Edge Cases
- 시리즈에 속하지 않은 오디오도 목록에 포함한다.
- 오디오의 오리지널 여부는 기존 추천 탭과 동일하게 해당 오디오가 속한 시리즈의
isOriginal기준으로 판단한다.
Feature D. 시리즈 구분
Requirements
type=SERIES는 차단 관계가 아닌 모든 크리에이터의 요일별 시리즈 콘텐츠를 조회한다.- 활성 시리즈를 조회 대상으로 한다.
dayOfWeek가 있으면series.publishedDaysOfWeek에 해당 값이 포함된 시리즈만 조회한다.- 요일 필터는 legacy
GET /audio-content/series/main/day-of-week와 동일하게 query parameter 이름dayOfWeek와SeriesPublishedDaysOfWeekenum 값을 사용한다. dayOfWeek가 없으면 요일 조건 없이 전체 시리즈를 조회한다.- 전체 개수는 같은 공개/차단/성인 콘텐츠 조건을 적용한 시리즈 개수다.
- 응답 목록은
series에 내려주고audios는 빈 배열로 내려준다. - 시리즈 제목은 호출 유저 언어코드에 맞는 번역값이 있으면 번역명을 사용하고, 없으면 원문 시리즈명을 사용한다.
- 응답 최상위
dayOfWeek에는 실제 적용된 요일 값을 내려준다.
Edge Cases
- 콘텐츠가 없는 활성 시리즈는 시리즈 목록에 포함할 수 있다.
dayOfWeek=RANDOM요청은 legacy와 동일하게SeriesPublishedDaysOfWeek.RANDOM이 포함된 시리즈만 조회한다.dayOfWeek가 지원 enum 값이 아니면 400 오류 대신 요일 조건을 적용하지 않는 fallback을 기본안으로 한다.
Feature E. 오리지널 구분
Requirements
type=ORIGINAL은 차단 관계가 아닌 모든 크리에이터의isOriginal == true인 시리즈를 조회한다.- 정렬, 페이징, 전체 개수, 성인 콘텐츠 정책은
SERIES와 동일하다. - 단,
dayOfWeek요일 필터는type=ORIGINAL에 적용하지 않는다. - 응답 목록은
series에 내려주고audios는 빈 배열로 내려준다.
Edge Cases
- 오리지널 시리즈에 공개 가능한 오디오 콘텐츠가 없어도 활성 시리즈이면 목록에 포함한다.
- 19금 오리지널 시리즈는 조회자의 성인 콘텐츠 노출 가능 여부를 따른다.
Feature F. 무료 구분
Requirements
type=FREE는 차단 관계가 아닌 모든 크리에이터의 무료 오디오 콘텐츠를 조회한다.- 무료 오디오는
price == 0인 공개 오디오로 정의한다. - 정렬, 페이징, 전체 개수, 성인 콘텐츠 정책은
AUDIO와 동일하다. - 응답 목록은
audios에 내려주고series는 빈 배열로 내려준다.
Edge Cases
- 무료 콘텐츠의
PRICE_HIGH와PRICE_LOW는 가격이 모두 0일 수 있으므로 2차/3차 정렬인releaseDate desc,id desc가 실제 순서를 결정할 수 있다.
Feature G. 포인트 구분
Requirements
type=POINT는 차단 관계가 아닌 모든 크리에이터의 포인트 사용 가능 오디오 콘텐츠를 조회한다.- 포인트 오디오는
isPointAvailable == true인 공개 오디오로 정의한다. - 정렬, 페이징, 전체 개수, 성인 콘텐츠 정책은
AUDIO와 동일하다. - 응답 목록은
audios에 내려주고series는 빈 배열로 내려준다.
Edge Cases
- 포인트 사용 가능 여부는 결제 가능 여부 필터일 뿐이며, 인기순 매출 산식에는 포인트 사용액을 포함하지 않는다.
Feature H. 콘텐츠 정렬
Requirements
- 정렬 순서는 기존 공용
ContentSortenum을 사용한다. - 공개 요청/응답 값은 다음을 사용한다.
LATEST: 최신순, 기본값POPULAR: 인기순PRICE_HIGH: 높은 가격순PRICE_LOW: 낮은 가격순
LATEST는releaseDate desc,id desc순으로 정렬한다.POPULAR은 인기순 매출 내림차순,releaseDate desc,id desc순으로 정렬한다.- 인기순의 매출은 대여/소장 여부와 관계없이 해당 콘텐츠에 순수하게 결제된 캔 매출 합계(
orders.can)를 기준으로 한다. - 인기순 매출에는 포인트 사용액(
orders.point)을 포함하지 않는다. - 인기순 매출에는
orders.isActive == true인 주문만 포함한다. PRICE_HIGH는price desc,releaseDate desc,id desc순으로 정렬한다.PRICE_LOW는price asc,releaseDate desc,id desc순으로 정렬한다.- 시리즈 정렬에서
releaseDate는 시리즈에 속한 공개 오디오 콘텐츠 중 가장 최근releaseDate를 대표값으로 사용한다. - 시리즈 정렬에서
price desc는 시리즈에 속한 공개 오디오 콘텐츠 중 가장 높은 가격을 대표값으로 사용한다. - 시리즈 정렬에서
price asc는 시리즈에 속한 공개 오디오 콘텐츠 중 가장 낮은 가격을 대표값으로 사용한다. - 시리즈 인기순 매출은 시리즈에 속한 공개 오디오 콘텐츠의
orders.can합계를 사용한다.
Edge Cases
- 매출이 없는 오디오 또는 시리즈의 인기순 매출값은 0으로 처리한다.
- 콘텐츠가 없는 시리즈는 정렬 대표값이 없는 항목으로 처리해 같은 정렬 내 마지막에 노출한다.
- 가격이 같은 콘텐츠는 각 정렬의 2차/3차 기준을 따른다.
8. API Endpoint
GET /api/v2/audio/contents?type=AUDIO&sort=LATEST&page=0&size=20
Authorization: Bearer {accessToken} (optional)
- 비회원 조회를 허용한다.
SecurityConfig에GET /api/v2/audio/contentspermitAll 설정을 추가한다.type미지정 시AUDIO를 기본값으로 사용한다.sort미지정 또는 invalid 값은LATEST로 fallback한다.type=SERIES에서 요일 선택이 필요하면dayOfWeek를 함께 보낸다.- 예:
GET /api/v2/audio/contents?type=SERIES&dayOfWeek=MON&sort=LATEST&page=0&size=20 page,size는 기존 크리에이터 채널 오디오/시리즈 탭과 같은 보정 정책을 따른다.
9. Response Data Class
data class MainContentAllTabResponse(
val type: MainContentAllType,
val totalCount: Int,
val audios: List<MainContentAudioResponse>,
val series: List<MainContentSeriesResponse>,
val sort: ContentSort,
val dayOfWeek: SeriesPublishedDaysOfWeek?,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
)
enum class MainContentAllType {
AUDIO,
SERIES,
ORIGINAL,
FREE,
POINT
}
data class MainContentAudioResponse(
val audioContentId: Long,
val title: String,
val imageUrl: String?,
val price: Int,
@JsonProperty("isAdult")
val isAdult: Boolean,
@JsonProperty("isPointAvailable")
val isPointAvailable: Boolean,
@JsonProperty("isFirstContent")
val isFirstContent: Boolean,
@JsonProperty("isOriginalSeries")
val isOriginalSeries: Boolean,
val creatorNickname: String
)
data class MainContentSeriesResponse(
val seriesId: Long,
val title: String,
val coverImageUrl: String?,
val creatorNickname: String,
@JsonProperty("isOriginal")
val isOriginal: Boolean,
@JsonProperty("isAdult")
val isAdult: Boolean
)
10. Technical Constraints
패키지 구조
- 공개 API 조립 계층은
kr.co.vividnext.sodalive.v2.api.content.all하위에 둔다.- Controller:
...adapter.in.web - Facade:
...application - Response DTO:
...dto
- Controller:
- 도메인 조회 계층은
kr.co.vividnext.sodalive.v2.content.all하위에 둔다.- Query service:
...application - 조회 정책/domain model:
...domain - 조회 port:
...port.out - QueryDSL/JPA 구현:
...adapter.out.persistence
- Query service:
- 의존 방향은
v2.api.content.all -> v2.content.all만 허용한다. - 도메인 패키지는
kr.co.vividnext.sodalive.v2.api.*패키지에 의존하지 않는다.
V2 공통화/재사용 대상
v2.common.domain.ContentSort: 정렬 enum 재사용creator.admin.content.series.SeriesPublishedDaysOfWeek: legacy와 같은 요일 query parameter enum 재사용content.series.main.SeriesMainController.getDayOfWeekSeriesList(...): legacy 요일별 시리즈 조회 API 계약 참고content.series.ContentSeriesService.getDayOfWeekSeriesList(...): legacy 요일별 시리즈 조회 service 흐름 참고v2.api.content.recommendation.adapter.in.web.AudioRecommendationController: 비회원 허용 controller와ApiResponse.ok(...)패턴v2.api.content.recommendation.application.AudioRecommendationFacade: API 조립 계층에서 domain 결과를 response DTO로 변환하는 패턴v2.content.recommendation.application.AudioRecommendationQueryService: 회원 성인 콘텐츠 노출 가능 여부 계산과 전체 추천 조회 service 흐름v2.content.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepository: 전체 공개 오디오 조회, 차단 크리에이터 제외, CDN URL 변환,AudioCard조립 패턴v2.api.content.recommendation.dto.AudioCardResponse: 오디오 카드 응답 필드와JsonProperty네이밍 패턴v2.api.creator.channel.series.dto.CreatorChannelSeriesResponse: 시리즈 응답 필드와JsonProperty네이밍 패턴 참고v2.creator.channel.series.domain.CreatorChannelSeriesQueryPolicy:sort,page,sizefallback 정책 참고v2.creator.channel.series.adapter.out.persistence.DefaultCreatorChannelSeriesQueryRepository: 시리즈 정렬 대표값, 시리즈 콘텐츠 통계,orders.can매출 합산 패턴 참고v2.common.domain.CdnUrlExtensions: 이미지 URL 변환 공통 함수MemberContentPreferenceService: 성인 콘텐츠 노출 가능 여부 판단LangContext: 시리즈 제목 다국어 처리
구현 주의사항
- 기존 추천 탭의 무료/포인트 오디오는 랜덤 조회지만, 전체 탭은 사용자가 선택한
sort기준으로 조회한다. - 기존 legacy 요일별 시리즈 API는
dayOfWeekquery parameter로SeriesPublishedDaysOfWeekenum을 받으므로 v2 전체 탭도 같은 parameter 이름과 enum 값을 사용한다. - 기존 v2 채널 오디오/시리즈 탭처럼 invalid parameter fallback을 유지하려면 controller에서는
dayOfWeek: String?으로 받고 policy/service 경계에서SeriesPublishedDaysOfWeek로 보정한다. - 기존 채널 오디오/시리즈 탭의
OWNED정렬은 전체 탭 요구사항에 포함하지 않으므로 전체 탭 policy에서 제외하거나LATEST로 fallback한다. POPULAR정렬은 기존 채널 탭 코드와 유사하되, 명시적으로orders.point를 더하지 않고orders.can만 집계한다.- 오디오와 시리즈가 다른 응답 item 구조를 가지므로 최상위 응답은
audios와series를 분리한다. - 신규 Entity나 DDL은 필요하지 않다.
11. Metrics
- 전체 탭 API 성공/실패 건수
- 전체 탭 API 응답 시간
type별 조회 건수sort별 조회 건수- 추가 로딩 요청 건수
12. Open Questions
- 없음. endpoint는 기존 메인 콘텐츠 v2 endpoint 축에 맞춰
GET /api/v2/audio/contents로 확정한다.