14 KiB
14 KiB
PRD: 콘텐츠 전체보기 API
1. Overview
콘텐츠 섹션에서 노출되는 오디오 목록의 전체보기 화면을 위해 NEW_AND_HOT_AUDIO, FIRST_AUDIO_CONTENT 두 타입을 페이징으로 조회하는 v2 API를 제공한다.
2. Problem
- 기존
GET /api/v2/audio/recommendations는 추천 탭 첫 화면의 섹션별 기본 개수만 내려주며, New & Hot 섹션 전체보기/페이징 API가 없다. - 기존
GET /api/v2/home/recommendations/first-audio-contents는 아직 배포되지 않은 홈 추천 하위 개별 endpoint이며, 콘텐츠 전체보기 API가 추가되면 별도 유지할 이유가 없다. GET /api/v2/audio/recommendations/contents는 추천 API 하위 리소스처럼 보이므로, 콘텐츠 전체보기 API라는 의미와 맞지 않는다.GET /api/v2/audio/contents는 이미 메인 콘텐츠 전체 탭 API가 사용 중이므로, 새 섹션 전체보기 API 경로로 재사용하지 않는다.- 클라이언트는 전체보기 화면에서 동일한 페이징 응답 형태로 섹션 타입만 바꿔 조회할 수 있어야 한다.
- V2 패키지에는
AudioRecommendationQueryService,HomeRecommendationQueryService,AudioCardResponse,HomeFirstAudioContentItem,HomeRecommendationPageResponse등 재사용 가능한 조회/응답 패턴이 있으므로, 새 API는 기존 패턴을 우선 재사용해야 한다.
3. Goals
- 콘텐츠 전체보기 API를
kr.co.vividnext.sodalive.v2하위 코드로 제공한다. - 기존 패턴과 동일하게 API 조립 계층과 도메인 조회 계층을 분리한다.
- 조회 타입은
NEW_AND_HOT_AUDIO,FIRST_AUDIO_CONTENT를 지원한다. NEW_AND_HOT_AUDIO는AudioRecommendationQueryService의 New & Hot 스냅샷 조회 흐름을 재사용한다.FIRST_AUDIO_CONTENT는HomeRecommendationQueryService.findFirstAudioContents조회 흐름을 재사용한다.- 하나의 endpoint에서
typequery parameter로 두 타입을 분리한다. - 비회원 조회를 허용하지 않는다.
- 인증 회원의 차단/성인 콘텐츠 노출 가능 여부 등 기존 사용자 조건을 반영한다.
- 아직 배포되지 않은
GET /api/v2/home/recommendations/first-audio-contents는 제거한다. - PRD에 API endpoint와 Response data class 초안을 포함한다.
4. Non-Goals
- 기존
GET /api/v2/audio/recommendations공개 응답 스키마를 변경하지 않는다. - 기존
GET /api/v2/home/recommendations공개 응답 스키마를 변경하지 않는다. - 기존
GET /api/v2/home/recommendations/first-audio-contentsendpoint는 배포 전 기능이므로 하위 호환 대상으로 보지 않는다. - New & Hot 점수 산식, 스냅샷 생성 주기, lazy refresh 정책을 변경하지 않는다.
- 첫 번째 오디오 콘텐츠 판정 기준과 정렬 정책을 변경하지 않는다.
RECENT_DEBUT_CREATOR,AI_CHARACTER등 다른 홈 추천 전체보기 타입은 이번 범위에 포함하지 않는다.- 새로운 DB 테이블, 배치 작업, 관리자 기능은 이번 범위에 포함하지 않는다.
5. Target Users
- 회원: 콘텐츠 섹션의 전체보기에서 더 많은 오디오 콘텐츠를 탐색하는 사용자
- 앱 클라이언트: 동일한 전체보기 화면에서 타입, page, size, hasNext를 기반으로 목록을 구성하려는 클라이언트
6. User Stories
- 사용자는 New & Hot 섹션에서 첫 화면에 보이는 개수보다 더 많은 오디오를 보고 싶다.
- 사용자는 처음부터 함께 성장 섹션의 첫 번째 오디오 콘텐츠를 전체보기로 더 탐색하고 싶다.
- 앱 클라이언트는 전체보기 화면에서
type만 바꿔 동일한 페이징 응답을 처리하고 싶다. - 앱 클라이언트는 인증 회원 기준으로 서버가 기존 성인 콘텐츠/차단 정책을 반영한 결과를 받길 원한다.
7. Core Features
Feature A. 콘텐츠 전체보기 통합 조회 API
Requirements
- 신규 API endpoint는
GET /api/v2/contents로 정의한다. - 응답 wrapper는 기존 패턴과 동일하게
ApiResponse.ok(...)를 사용한다. - 비회원 조회를 허용하지 않는다.
- Security 설정은
GET /api/v2/contents를 인증 필요 endpoint로 둔다. - 회원 조회 시
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?패턴과requireMember(...)가드절을 사용한다. - 요청 query parameter는
type,page,size를 사용한다. type값은 아래 enum으로 정의한다.NEW_AND_HOT_AUDIO: 콘텐츠 추천 탭 New & Hot 오디오 전체보기FIRST_AUDIO_CONTENT: 메인 홈 처음부터 함께 성장 오디오 전체보기
type을 보내지 않으면NEW_AND_HOT_AUDIO를 기본값으로 사용한다.- 지원하지 않는
type값이 들어오면 400 오류 대신NEW_AND_HOT_AUDIO로 fallback한다. page는 0부터 시작하는 page index로 처리한다.page를 보내지 않으면 기본값0을 사용한다.size를 보내지 않으면 기본값20을 사용한다.page가 0보다 작으면0으로 fallback한다.size가 1보다 작으면 기본값20으로 fallback한다.size가 50보다 크면50으로 fallback한다.- 다음 page 존재 여부는
size + 1개 조회 또는 동등한 방식으로 판단하되, 응답 목록에는 최대size개만 내려준다.
Edge Cases
- 조회 결과가 없으면
items는 빈 배열,hasNext는false로 내려준다. - 요청한 page 범위에 콘텐츠가 없으면
items는 빈 배열,hasNext는false로 내려준다. - 특정 타입 조회 중 필터링으로 스냅샷 대상 상세 데이터가 제거될 수 있으며, 이 경우 가능한 항목만 내려준다.
Feature B. NEW_AND_HOT_AUDIO 전체보기
Requirements
type=NEW_AND_HOT_AUDIO는AudioRecommendationQueryService의 New & Hot 조회 정책을 재사용한다.- 인증 회원의 성인 콘텐츠 노출 가능 여부에 따라
AudioRecommendationVisibility.SAFE또는AudioRecommendationVisibility.ALL을 결정한다. SAFE는RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,ALL은RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL스냅샷을 조회한다.- New & Hot 첫 화면 노출 수는 기존과 동일하게 12개로 유지한다.
- New & Hot 스냅샷 저장 수는 전체보기 페이징을 위해 visibility별 100개로 확장한다.
- 스냅샷 저장 수 100개는
SAFE와ALL각각에 적용한다. RecommendationSnapshotPort.findLatestSnapshots(sectionType, offset, limit)로 page offset과size + 1limit을 적용한다.- 스냅샷이 없으면 기존
AudioRecommendationQueryService의 New & Hot lazy refresh 정책을 재사용한다. - 스냅샷 target id 목록을
AudioRecommendationQueryPort.findAudioCardsByIds(...)로 상세 조회한다. - 응답 item은 기존
AudioCardResponse필드 의미를 유지한다.
Edge Cases
- lazy refresh 후에도 스냅샷이 없으면 빈 배열로 내려준다.
- 스냅샷에는 있지만 비활성/예약 공개/차단/성인 콘텐츠 정책으로 상세 조회에서 제외된 항목은 응답하지 않는다.
Feature C. FIRST_AUDIO_CONTENT 전체보기
Requirements
type=FIRST_AUDIO_CONTENT는HomeRecommendationQueryService.findFirstAudioContents(...)를 재사용한다.offset = page * size,limit = size + 1로 조회한다.member.id와MemberContentPreferenceService.canViewAdultContent(member)결과를 전달한다.- 응답 item은
NEW_AND_HOT_AUDIO와 동일한ContentOverviewItemResponse필드를 모두 채운다. - 기존
HomeFirstAudioContentRecord에 공통 응답 구성을 위해 필요한isAdult,isOriginalSeries값을 보강한다. FIRST_AUDIO_CONTENT응답의isFirstContent는 첫 번째 콘텐츠 섹션 특성상true로 내려준다.
Edge Cases
- 첫 번째 오디오 콘텐츠 판정은 기존 홈 추천 PRD와 현재
HomeRecommendationQueryService.findFirstAudioContents구현을 따른다. - 예약 공개 콘텐츠는 기존 조회 서비스 정책에 따라 공개 전에는 노출하지 않는다.
Feature D. 공통 콘텐츠 정책
Requirements
- 모든 타입은 공개 가능한 콘텐츠만 조회한다.
- 회원이 차단했거나 회원을 차단한 크리에이터의 콘텐츠는 노출하지 않는다.
- 인증 회원은 기존 콘텐츠 조회 설정에 따라 19금 콘텐츠 노출 가능 여부를 반영한다.
- 이미지 경로와 기본 프로필 이미지는 기존 각 조회 서비스/Facade 변환 정책을 따른다.
Feature E. 미배포 홈 하위 전체보기 API 제거
Requirements
HomeRecommendationController의GET /api/v2/home/recommendations/first-audio-contentsendpoint를 제거한다.- 해당 endpoint만을 위한
HomeRecommendationFacade.getFirstAudioContents(...)조립 메서드는 새 콘텐츠 전체보기 Facade로 책임을 옮긴 뒤 제거한다. - 관련 Controller/Facade 테스트는 새
GET /api/v2/contents?type=FIRST_AUDIO_CONTENT테스트로 대체한다. SecurityConfig에 홈 하위 전체보기 endpoint를 위한 별도 설정이 있다면 제거하거나 더 이상 영향이 없게 정리한다.
Edge Cases
HomeRecommendationQueryService.findFirstAudioContents(...)는 새 API에서 재사용하므로 제거하지 않는다.
8. API Endpoint
GET /api/v2/contents?type=NEW_AND_HOT_AUDIO&page=0&size=20
Authorization: Bearer {accessToken}
- 비회원 조회를 허용하지 않는다.
SecurityConfig에서GET /api/v2/contents는 인증 필요 endpoint로 둔다.type미지정 또는 invalid 값은NEW_AND_HOT_AUDIO로 fallback한다.FIRST_AUDIO_CONTENT조회 예시는 아래와 같다.
GET /api/v2/contents?type=FIRST_AUDIO_CONTENT&page=0&size=20
Authorization: Bearer {accessToken}
9. Response Data Class
data class ContentOverviewPageResponse(
val type: ContentOverviewType,
val items: List<ContentOverviewItemResponse>,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
)
enum class ContentOverviewType {
NEW_AND_HOT_AUDIO,
FIRST_AUDIO_CONTENT
}
data class ContentOverviewItemResponse(
val contentId: Long,
val title: String,
val coverImage: String?,
val price: Int,
@JsonProperty("isPointAvailable")
val isPointAvailable: Boolean,
val creatorNickname: String,
@JsonProperty("isAdult")
val isAdult: Boolean,
@JsonProperty("isFirstContent")
val isFirstContent: Boolean,
@JsonProperty("isOriginalSeries")
val isOriginalSeries: Boolean
)
NEW_AND_HOT_AUDIO,FIRST_AUDIO_CONTENT모두 동일한 item 필드를 채우며 타입별 nullable 전용 필드를 두지 않는다.- 기존
audioContentId,imageUrl공개 필드명은 각각contentId,coverImage로 사용한다. duration,creatorId,creatorProfileImage는 콘텐츠 전체보기 응답에 포함하지 않는다.
10. Technical Constraints
패키지 구조
- 공개 API 조립 계층은 콘텐츠 전체보기 API 의미가 드러나도록
kr.co.vividnext.sodalive.v2.api.content.overview하위에 둔다.- Controller:
...adapter.in.web.ContentOverviewController - Facade:
...application.ContentOverviewFacade - Response DTO:
...dto.ContentOverviewPageResponse
- Controller:
- 도메인 조회 계층은 기존 서비스 재사용을 우선한다.
- New & Hot:
kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService - 첫 번째 오디오 콘텐츠:
kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
- New & Hot:
- 신규 도메인 모델/정책이 필요하면
kr.co.vividnext.sodalive.v2.content.recommendation.domain에 최소 범위로 추가한다. - 의존 방향은
v2.api.content.overview -> v2.content.recommendation,v2.api.content.overview -> v2.recommendation만 허용한다.
V2 공통화/재사용 대상
AudioRecommendationQueryService.resolveVisibility(...)AudioRecommendationQueryService.newAndHotSectionType(...)RecommendationSnapshotPort.findLatestSnapshots(...)AudioRecommendationQueryPort.findAudioCardsByIds(...)HomeRecommendationQueryService.findFirstAudioContents(...)AudioCardResponse의 응답 필드 의미와JsonProperty네이밍 패턴HomeFirstAudioContentItem의 응답 필드 의미와 이미지 URL 변환 패턴HomeRecommendationFacade의 page/size 보정,size + 1기반hasNext계산 패턴
스냅샷 저장 정책
- New & Hot은 첫 화면 조회 limit과 스냅샷 저장 limit을 분리한다.
- 첫 화면 조회 limit은
NEW_AND_HOT_HOME_LIMIT = 12로 유지한다. - 스냅샷 저장 limit은
NEW_AND_HOT_SNAPSHOT_LIMIT = 100으로 정의한다. AudioRecommendationSnapshotRefreshService는findNewAndHotSnapshots(..., limit = NEW_AND_HOT_SNAPSHOT_LIMIT)로SAFE,ALL각각 최대 100개를 저장한다.AudioRecommendationQueryService.getRecommendations(...)는 첫 화면 응답 조립 시 최신 스냅샷에서 12개만 조회한다.- 콘텐츠 전체보기 API는 저장된 100개 스냅샷 범위 안에서
offset,size + 1로 페이징한다.
구현 판단
- 별도 endpoint 2개보다 typed endpoint 1개를 기본안으로 한다.
- 이유는 두 요구가 모두 “오디오 콘텐츠 목록 전체보기”이고, page/size/hasNext 응답 계약이 동일하며,
MainContentAllController도type기반 단일 endpoint 패턴을 이미 사용하기 때문이다. - endpoint는
GET /api/v2/contents를 사용한다. - 이유는
GET /api/v2/audio/recommendations/contents가 추천 하위 리소스처럼 읽혀 콘텐츠 전체보기 API 의미와 맞지 않고,GET /api/v2/audio/contents는 이미 메인 콘텐츠 전체 탭 API가 사용 중이기 때문이다. - 기존
GET /api/v2/home/recommendations/first-audio-contents는 배포 전 endpoint이므로 제거하고, 새 API의type=FIRST_AUDIO_CONTENT로 대체한다.
11. Decisions
GET /api/v2/contents는 인증 회원만 호출할 수 있다.- 기존 홈 하위 전체보기 endpoint는 배포 전 기능이므로 제거한다.
- New & Hot 스냅샷은 전체보기 지원을 위해 visibility별 100개 저장한다.