Files
sodalive-backend-spring-boot/docs/20260627_콘텐츠_전체보기_API/prd.md

243 lines
14 KiB
Markdown

# 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에서 `type` query 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-contents` endpoint는 배포 전 기능이므로 하위 호환 대상으로 보지 않는다.
- 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 + 1` limit을 적용한다.
- 스냅샷이 없으면 기존 `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-contents` endpoint를 제거한다.
- 해당 endpoint만을 위한 `HomeRecommendationFacade.getFirstAudioContents(...)` 조립 메서드는 새 콘텐츠 전체보기 Facade로 책임을 옮긴 뒤 제거한다.
- 관련 Controller/Facade 테스트는 새 `GET /api/v2/contents?type=FIRST_AUDIO_CONTENT` 테스트로 대체한다.
- `SecurityConfig`에 홈 하위 전체보기 endpoint를 위한 별도 설정이 있다면 제거하거나 더 이상 영향이 없게 정리한다.
#### Edge Cases
- `HomeRecommendationQueryService.findFirstAudioContents(...)`는 새 API에서 재사용하므로 제거하지 않는다.
---
## 8. API Endpoint
```http
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` 조회 예시는 아래와 같다.
```http
GET /api/v2/contents?type=FIRST_AUDIO_CONTENT&page=0&size=20
Authorization: Bearer {accessToken}
```
---
## 9. Response Data Class
```kotlin
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`
- 도메인 조회 계층은 기존 서비스 재사용을 우선한다.
- New & Hot: `kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService`
- 첫 번째 오디오 콘텐츠: `kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService`
- 신규 도메인 모델/정책이 필요하면 `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개 저장한다.