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

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_AUDIOAudioRecommendationQueryService의 New & Hot 스냅샷 조회 흐름을 재사용한다.
  • FIRST_AUDIO_CONTENTHomeRecommendationQueryService.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는 빈 배열, hasNextfalse로 내려준다.
  • 요청한 page 범위에 콘텐츠가 없으면 items는 빈 배열, hasNextfalse로 내려준다.
  • 특정 타입 조회 중 필터링으로 스냅샷 대상 상세 데이터가 제거될 수 있으며, 이 경우 가능한 항목만 내려준다.

Feature B. NEW_AND_HOT_AUDIO 전체보기

Requirements

  • type=NEW_AND_HOT_AUDIOAudioRecommendationQueryService의 New & Hot 조회 정책을 재사용한다.
  • 인증 회원의 성인 콘텐츠 노출 가능 여부에 따라 AudioRecommendationVisibility.SAFE 또는 AudioRecommendationVisibility.ALL을 결정한다.
  • SAFERecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE, ALLRecommendedSectionType.NEW_AND_HOT_AUDIO_ALL 스냅샷을 조회한다.
  • New & Hot 첫 화면 노출 수는 기존과 동일하게 12개로 유지한다.
  • New & Hot 스냅샷 저장 수는 전체보기 페이징을 위해 visibility별 100개로 확장한다.
  • 스냅샷 저장 수 100개는 SAFEALL 각각에 적용한다.
  • 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_CONTENTHomeRecommendationQueryService.findFirstAudioContents(...)를 재사용한다.
  • offset = page * size, limit = size + 1로 조회한다.
  • member.idMemberContentPreferenceService.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

  • HomeRecommendationControllerGET /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

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
  • 도메인 조회 계층은 기존 서비스 재사용을 우선한다.
    • 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으로 정의한다.
  • AudioRecommendationSnapshotRefreshServicefindNewAndHotSnapshots(..., 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 응답 계약이 동일하며, MainContentAllControllertype 기반 단일 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개 저장한다.