Files
sodalive-backend-spring-boot/docs/20260623_메인_콘텐츠_랭킹_탭_API/prd.md

26 KiB

PRD: 메인 콘텐츠 랭킹 탭 API

1. Overview

메인 콘텐츠 탭의 내부 랭킹 탭에서 사용할 콘텐츠 랭킹을 조회하는 v2 API를 제공한다.

랭킹 구분은 주간 인기, 지금 뜨는 중, 매출, 판매량, 댓글 수, 좋아요이며, 각 랭킹은 최대 20위까지 표시한다.


2. Problem

  • 기존 콘텐츠 랭킹 조회는 RankingService.getContentRanking 기반의 정렬 조회를 제공하지만, 신규 랭킹 탭은 v2 스냅샷 기준으로 rank, rankChange, isNew를 크리에이터 랭킹과 같은 의미로 내려줘야 한다.
  • 주간 인기지금 뜨는 중은 신규 점수 산식, 유료/무료 콘텐츠별 정규화, 주간 스냅샷 갱신, fallback 실행 기록이 필요하다.
  • 매출, 판매량, 댓글 수, 좋아요는 기존 랭킹과 동일한 원천 지표를 사용하되, 순위 변화와 신규 진입 여부를 안정적으로 계산하려면 v2 스냅샷 생성 시점에 완료 주차 기준으로 직접 집계해야 한다.
  • 조회 시마다 모든 랭킹 타입의 원천 데이터를 집계하면 응답 지연과 계산 중복이 커지고, 운영 서버와 테스트 환경에서 같은 기준의 결과를 재현하기 어렵다.
  • 기존 v2 패키지에 크리에이터 랭킹 스냅샷/작업 이력/fallback 패턴과 콘텐츠 추천 탭의 API 조립 계층/도메인 조회 계층 분리 패턴이 있으므로 이를 우선 재사용해야 한다.
  • 2026-06-25 후속 확인 결과, 메인 콘텐츠 랭킹 탭 API의 coverImageUrl 응답이 cloud.aws.cloud-front.host가 포함된 완성 URL이 아니라 cover-*.png 같은 저장 path만 내려가는 버그가 확인되었다. 앱 클라이언트는 공개 API의 이미지 필드를 직접 렌더링 가능한 URL로 기대하므로, 다른 v2 콘텐츠/크리에이터 조회 API와 동일하게 CDN host를 포함해 반환해야 한다.

3. Goals

  • 메인 콘텐츠 랭킹 탭 조회 API를 kr.co.vividnext.sodalive.v2 하위 신규 코드로 제공한다.
  • 기존 패턴과 동일하게 API 조립 계층과 도메인 조회 계층을 분리한다.
  • 모든 랭킹 타입은 최대 20개 콘텐츠를 응답한다.
  • 모든 랭킹 타입의 동점자는 releaseDate desc, contentId desc 순으로 2차, 3차 정렬한다.
  • rank, rankChange, isNew의 의미는 크리에이터 랭킹과 동일하게 정의한다.
  • 주간 인기지금 뜨는 중은 매주 월요일 00:00 KST 기준으로 지난 주 데이터를 계산한다.
  • 매출, 판매량, 댓글 수, 좋아요도 완료된 지난 주 데이터를 기준으로 계산한다.
  • 스냅샷 생성은 부하 분산을 위해 매주 월요일 01:00:00 KST부터 07:30:00 KST 사이에 랭킹 타입별로 분산 실행한다.
  • 새로 생성된 스냅샷은 매주 월요일 09:00:00 KST부터 조회 API에 노출한다.
  • 스냅샷이 없어 조회할 수 없는 경우 스케줄러로 예약된 랭킹 계산 로직을 fallback으로 직접 실행한다.
  • fallback 실행은 스케줄 실행 기록처럼 저장하며, 동일 랭킹 타입과 동일 집계 기간 기준 최대 3회까지만 시도한다.
  • PRD에 API endpoint와 Response data class 초안을 포함한다.
  • 신규 Entity가 생성되는 경우 같은 작업 디렉터리에 대응 DB table 생성/수정 DDL을 기록한다.
  • coverImageUrl은 스냅샷 또는 DB에 저장된 path를 그대로 공개하지 않고, 공개 Response를 만들기 전에 cloud.aws.cloud-front.host를 포함한 URL로 변환한다.

4. Non-Goals

  • 기존 공개 API 스키마를 임의 변경하지 않는다.
  • 기존 공개 API의 RankingService.getContentRanking 동작과 정렬 산식은 이번 작업에서 변경하지 않는다.
  • 관리자 화면, 수동 보정 기능, 랭킹 결과 고정/제외 기능은 포함하지 않는다.
  • 개인화 랭킹, A/B 테스트, 머신러닝 기반 점수 산정은 포함하지 않는다.
  • 20위 이후 전체보기/페이징 API는 이번 요구사항에 포함하지 않는다.
  • 실시간 랭킹은 포함하지 않는다. 모든 랭킹 타입은 완료된 지난 주 데이터를 기준으로 한다.
  • 기존 크리에이터 랭킹의 다중 랭킹 타입 전환, 스냅샷 테이블 구조 변경, 계산 스케줄 분산 처리는 이번 PRD에 포함하지 않고 별도 PRD에서 다룬다.

5. Target Users

  • 회원: 콘텐츠 메인 탭에서 인기 콘텐츠와 상승 중인 콘텐츠를 탐색하는 사용자
  • 비회원: 인증 없이 조회 가능한 랭킹 콘텐츠를 탐색하는 사용자
  • 앱 클라이언트: 내부 랭킹 탭의 랭킹 타입별 목록과 순위 변화 UI를 구성하는 클라이언트
  • 운영자: 주간 콘텐츠 랭킹 계산 결과와 fallback 실행 이력을 확인하는 내부 사용자

6. User Stories

  • 사용자는 주간 인기 콘텐츠 상위 20개를 보고 싶다.
  • 사용자는 지난 주 대비 지금 뜨는 중인 콘텐츠를 보고 싶다.
  • 사용자는 매출, 판매량, 댓글 수, 좋아요 기준의 콘텐츠 랭킹을 보고 싶다.
  • 사용자는 각 콘텐츠의 현재 순위, 순위 변화, 신규 진입 여부를 보고 싶다.
  • 앱 클라이언트는 하나의 API endpoint에서 랭킹 타입만 바꿔 동일한 응답 구조로 화면을 구성하고 싶다.
  • 테스트 환경에서는 스냅샷이 비어 있어도 조회 API 호출만으로 fallback 랭킹 계산이 실행되기를 원한다.

7. Core Features

Feature A. 메인 콘텐츠 랭킹 탭 조회 API

Requirements

  • 신규 API endpoint는 GET /api/v2/audio/rankings로 정의한다.
  • 요청 query parameter는 type을 사용한다.
  • type 값은 아래 enum으로 정의한다.
    • WEEKLY_POPULAR: 주간 인기
    • RISING: 지금 뜨는 중
    • REVENUE: 매출
    • SALES_COUNT: 판매량
    • COMMENT_COUNT: 댓글 수
    • LIKE_COUNT: 좋아요
  • type이 없으면 WEEKLY_POPULAR를 기본값으로 사용한다.
  • 응답 wrapper는 기존 패턴과 동일하게 ApiResponse.ok(...)를 사용한다.
  • 조회 API는 visibleFromAt <= now이고 생성이 완료된 최신 스냅샷만 응답한다.
  • 월요일 09:00:00 KST 전에는 새 주차 스냅샷이 이미 생성되어 있어도 직전 공개 스냅샷을 응답한다.
  • 인증 회원이면 기존 콘텐츠 랭킹/추천 조회와 같은 방식으로 회원의 19금 노출 가능 여부와 차단 관계를 반영한다.
  • 비회원이면 19금 콘텐츠를 노출하지 않는다.
  • 비활성 콘텐츠, 공개 전 콘텐츠, 비활성 크리에이터의 콘텐츠는 노출하지 않는다.
  • 각 랭킹 타입은 최대 20개를 응답한다.
  • 정렬은 랭킹 점수 또는 정렬 지표 내림차순, releaseDate desc, contentId desc 순으로 적용한다.

Edge Cases

  • 랭킹 결과가 없으면 빈 배열로 성공 응답한다.
  • 후보가 20개 미만이면 가능한 개수만 내려준다.
  • 특정 랭킹 타입의 새 스냅샷 생성이 실패하면 해당 타입은 직전 공개 스냅샷을 유지한다.
  • 콘텐츠 제목, 크리에이터 닉네임, 커버 이미지가 기존 정책상 마스킹되어야 하는 경우 기존 콘텐츠 랭킹/추천 조회 정책을 따른다.
  • coverImageUrl은 스냅샷 저장값이 path 형태여도 공개 응답에서는 https://... 또는 http://...로 시작하는 완성 URL이어야 한다. 이미 완성 URL인 값은 중복 prefix를 붙이지 않는다.

Feature B. rank, rankChange, isNew 의미

Requirements

  • rank는 최신 완료 주차 스냅샷에서 해당 랭킹 타입의 정렬 결과 순위다.
  • rank는 1부터 시작한다.
  • rankChange직전 완료 주차 rank - 최신 완료 주차 rank로 계산한다.
  • 순위가 올라갔으면 양수, 순위가 내려갔으면 음수, 동일하면 0을 내려준다.
  • 예를 들어 직전 완료 주차 10위, 최신 완료 주차 5위이면 rankChange5다.
  • 예를 들어 직전 완료 주차 1위, 최신 완료 주차 10위이면 rankChange-9다.
  • 직전 완료 주차에는 없고 최신 완료 주차에 진입한 콘텐츠는 isNew == true로 내려준다.
  • 신규 진입 콘텐츠의 rankChange는 비교 가능한 이전 순위가 없으므로 null로 내려준다.
  • 직전 완료 주차 스냅샷이 없으면 showRankChange == false로 내려주고, 각 item의 rankChangenull, isNewfalse로 내려준다.
  • 직전 완료 주차 스냅샷이 있으면 showRankChange == true로 내려준다.

Edge Cases

  • fallback으로 최신 주차 스냅샷을 생성했지만 직전 완료 주차 스냅샷이 없으면 showRankChange == false를 유지한다.
  • 동점자는 releaseDate desc, contentId desc로 결정되므로 같은 스냅샷을 조회할 때 순위가 랜덤하게 바뀌지 않는다.

Feature C. 주간 인기 랭킹

Requirements

  • 갱신 기준은 매주 월요일 00:00 KST다.
  • 집계 대상 기간은 지난 주 월요일 00:00:00 KST 이상, 이번 주 월요일 00:00:00 KST 미만이다.
  • DB 조회 조건은 KST 집계 기간을 UTC로 변환해 사용한다.
  • 유료 콘텐츠와 무료 콘텐츠는 서로 다른 원천 지표와 가중치로 1차 점수를 산출한다.
  • 유료 콘텐츠 점수는 매출 45% + 판매량 35% + 좋아요 수 10% + 댓글 수 10%로 계산한다.
  • 무료 콘텐츠 점수는 조회수 50% + 좋아요 수 25% + 댓글 수 25%로 계산한다.
  • 조회수는 상세 페이지 조회 이력인 creator_content_view_history 기준으로 집계한다.
  • 유료 콘텐츠와 무료 콘텐츠는 각 그룹의 최고 점수를 기준으로 0~100 정규화한 뒤 비교한다.
  • 유료 정규화 점수는 (현재 유료 콘텐츠 점수 / 유료 콘텐츠 최고 점수) * 100으로 계산한다.
  • 무료 정규화 점수는 (현재 무료 콘텐츠 점수 / 무료 콘텐츠 최고 점수) * 100으로 계산한다.
  • 각 그룹의 최고 점수가 0 이하이면 해당 그룹의 정규화 점수는 0으로 처리한다.
  • 최종 정렬은 정규화 점수 내림차순, releaseDate desc, contentId desc 순으로 적용한다.

정규화 판단

  • (최고 점수 + 현재 콘텐츠 점수) * 100은 최고점 대비 상대 위치를 0~100 범위로 맞추지 못하므로 정규화 산식으로 부적절하다.
  • 유료/무료 콘텐츠를 별도 산식으로 계산한 뒤 한 목록에서 비교하려면 (현재 점수 / 그룹 최고 점수) * 100 방식이 더 적절하다.
  • 이 방식은 각 그룹의 1위 콘텐츠를 100점으로 맞추고 나머지 콘텐츠를 상대 점수로 비교한다.

Edge Cases

  • 유료 콘텐츠 후보가 없으면 유료 정규화는 수행하지 않고 무료 콘텐츠만 비교한다.
  • 무료 콘텐츠 후보가 없으면 무료 정규화는 수행하지 않고 유료 콘텐츠만 비교한다.
  • 원천 지표가 없으면 0으로 계산한다.

Feature D. 지금 뜨는 중 랭킹

Requirements

  • 갱신 기준은 매주 월요일 00:00 KST다.
  • 집계 대상 기간은 최근 7일과 직전 7일을 비교한다.
  • 기준 시점은 완료된 지난 주의 종료 시점으로 한다.
    • 최근 7일: 지난 주 월요일 00:00:00 KST 이상, 이번 주 월요일 00:00:00 KST 미만
    • 직전 7일: 2주 전 월요일 00:00:00 KST 이상, 지난 주 월요일 00:00:00 KST 미만
  • 콘텐츠 지금 뜨는 중 점수는 ((0.5 * 콘텐츠 성장 점수) + (0.25 * 좋아요 증가율) + (0.25 * 댓글 증가율)) * 신규 콘텐츠 부스트로 계산한다.
  • 유료 콘텐츠 성장 점수는 (0.6 * 판매 증가율) + (0.4 * 조회수 증가율)로 계산한다.
  • 무료 콘텐츠 성장 점수는 (0.5 * 조회수 증가율) + (0.25 * 좋아요 증가율) + (0.25 * 댓글 증가율)로 계산한다.
  • 판매 증가율은 (최근 7일 판매량 - 직전 7일 판매량) / max(직전 7일 판매량, 1)로 계산한다.
  • 조회수 증가율은 (최근 7일 조회수 - 직전 7일 조회수) / max(직전 7일 조회수, 1)로 계산한다.
  • 좋아요 증가율은 (최근 7일 좋아요 수 - 직전 7일 좋아요 수) / max(직전 7일 좋아요 수, 1)로 계산한다.
  • 댓글 증가율은 (최근 7일 댓글 수 - 직전 7일 댓글 수) / max(직전 7일 댓글 수, 1)로 계산한다.
  • 최근 7일 조회수 10회 미만이면 조회수 증가율 반영값은 0으로 처리한다.
  • 최근 7일 좋아요 수 3개 미만이면 좋아요 증가율 반영값은 0으로 처리한다.
  • 최근 7일 댓글 수 3개 미만이면 댓글 증가율 반영값은 0으로 처리한다.
  • 최근 7일 판매량 3건 미만이면 판매 증가율 반영값은 0으로 처리한다.
  • 유료 콘텐츠와 무료 콘텐츠는 각 그룹의 최고 점수를 기준으로 0~100 정규화한 뒤 비교한다.
  • 유료 정규화 점수는 (현재 유료 콘텐츠 점수 / 유료 콘텐츠 최고 점수) * 100으로 계산한다.
  • 무료 정규화 점수는 (현재 무료 콘텐츠 점수 / 무료 콘텐츠 최고 점수) * 100으로 계산한다.
  • 각 그룹의 최고 점수가 0 이하이면 해당 그룹의 정규화 점수는 0으로 처리한다.
  • 신규 콘텐츠 부스트는 집계 종료일 기준 releaseDate 경과 일수로 적용한다.
    • Release 3일 이내: 1.5
    • Release 7일 이내: 1.3
    • Release 14일 이내: 1.15
    • Release 14일 초과: 1.0
  • 최종 정렬은 정규화 점수 내림차순, releaseDate desc, contentId desc 순으로 적용한다.

Edge Cases

  • 모든 증가율 반영값이 0이면 지금 뜨는 중 원점수는 0으로 계산한다.
  • 증가율은 음수가 될 수 있으며, 음수 원점수는 정규화 전 후보 점수에 그대로 반영한다.
  • 그룹 최고 점수가 0 이하인 경우 해당 그룹 콘텐츠의 정규화 점수는 0으로 처리해 음수 최고점으로 인한 역전 현상을 피한다.

Feature E. 매출, 판매량, 댓글 수, 좋아요 랭킹

Requirements

  • REVENUE, SALES_COUNT, COMMENT_COUNT, LIKE_COUNT는 v2 스냅샷 생성 계층에서 완료 주차 기준으로 직접 집계한다.
  • 각 타입의 최종 점수는 원천 지표를 그대로 사용한다. REVENUE는 매출 can 합계, SALES_COUNT는 판매 건수, COMMENT_COUNT는 활성 댓글 수, LIKE_COUNT는 활성 좋아요 수다.
  • 각 랭킹 타입도 스냅샷으로 저장한다.
  • 스냅샷 생성 시 v2 집계 결과를 기반으로 전체 기준 상위 20개와 비성인 콘텐츠 기준 상위 20개의 합집합 후보를 저장한다.
  • 공개 응답은 조회자의 성인 콘텐츠 열람 가능 여부를 적용한 뒤 최대 20개 콘텐츠를 반환한다.
  • 동점자는 releaseDate desc, contentId desc 순으로 정렬한다.
  • 최종 응답의 rank, rankChange, isNew 계산은 주간 인기, 지금 뜨는 중과 동일한 공통 로직을 사용한다.

스냅샷 저장 판단

  • 이번 PRD의 기준안은 모든 랭킹 타입을 스냅샷으로 저장하는 것이다.
  • 이유는 rankChangeisNew가 모든 응답 item에 필요하고, 이 값은 최신 완료 주차와 직전 완료 주차의 같은 랭킹 타입 결과를 비교해야 안정적으로 계산할 수 있기 때문이다.
  • 주간 인기지금 뜨는 중만 스냅샷으로 저장하면 매출, 판매량, 댓글 수, 좋아요는 조회 때마다 최신/직전 주차를 동적으로 재계산해야 하며, 계산 비용과 late update에 따른 순위 흔들림이 생긴다.
  • 기존 랭킹과 같은 원천 지표를 사용하는 4개 랭킹도 v2 스냅샷 생성 시점에 직접 집계해, legacy 조회 조건과 v2 공개/제외 조건이 섞이지 않도록 한다.

Edge Cases

  • v2 집계 결과 또는 조회자에게 노출 가능한 후보가 20개 미만이면 해당 개수만 저장/응답한다.
  • legacy 랭킹 조회의 최소 개수 확보용 기간 확장 로직은 v2 스냅샷 생성에 적용하지 않는다.

Feature F. 랭킹 스냅샷 및 작업 이력

Requirements

  • 콘텐츠 랭킹 스냅샷은 랭킹 타입, 집계 시작/종료 시각, 콘텐츠 id, 순위, 점수 또는 정렬 지표, 표시용 콘텐츠 정보를 저장한다.
  • 신규 스냅샷 Entity와 작업 이력 Entity의 DB table DDL은 docs/20260623_메인_콘텐츠_랭킹_탭_API/create-content-ranking-tables.sql에 기록한다.
  • 스냅샷에는 visibleFromAt을 저장하며, 공개 조회는 이 시각이 지난 스냅샷만 대상으로 한다.
  • 스냅샷은 최신 완료 주차와 직전 완료 주차를 조회할 수 있어야 한다.
  • 같은 랭킹 타입과 같은 집계 기간의 스냅샷은 중복 저장하지 않는다.
  • 스냅샷 생성은 기존 크리에이터 랭킹과 동일하게 job service, refresh service, scheduler 책임으로 분리한다.
  • 집계 기준 시각은 매주 월요일 00:00:00 KST다.
  • 스냅샷 생성은 원천 데이터 적재 지연과 운영 부하를 고려해 매주 월요일 01:00:00 KST부터 07:30:00 KST 사이에 랭킹 타입별로 분산 실행한다.
  • 새 스냅샷의 기본 노출 전환 시각은 매주 월요일 09:00:00 KST다.
  • 스케줄러는 Asia/Seoul zone을 명시한다.
  • 다중 서버 인스턴스에서 같은 스케줄이 동시에 실행되더라도 Redisson lock으로 동일 기간/랭킹 타입은 한 번만 계산한다.
  • 작업 이력에는 trigger, status, 집계 시작/종료 시각, 랭킹 타입, 오류 메시지, 처리 시작/종료 시각을 저장한다.
  • trigger 값은 최소 SCHEDULED, MANUAL, FALLBACK을 지원한다.

Edge Cases

  • 특정 랭킹 타입 스냅샷 생성이 실패해도 다른 랭킹 타입 생성이 가능한 구조로 분리한다.
  • 일부 랭킹 타입만 스냅샷이 있으면 요청한 type 기준으로만 fallback 여부를 판단한다.
  • 월요일 09:00:00 KST 전에 새 스냅샷이 일부만 생성되어도 공개 조회에는 반영하지 않는다.
  • 월요일 09:00:00 KST 이후 특정 랭킹 타입의 새 스냅샷이 없거나 생성 실패 상태이면 해당 타입은 직전 공개 스냅샷을 응답한다.

Feature G. 크리에이터 랭킹과의 범위 경계

Requirements

  • 현재 크리에이터 랭킹 스냅샷 생성 스케줄은 매주 월요일 KST 07:30이다.
  • 크리에이터 랭킹도 향후 다중 랭킹 타입이 추가될 예정이므로, 콘텐츠 랭킹의 스냅샷/작업 이력 구조는 향후 크리에이터 랭킹에도 같은 운영 모델을 적용할 수 있도록 rankingType, 집계 기간, visibleFromAt, job trigger/status 축을 기준으로 설계한다.
  • 이번 PRD는 콘텐츠 랭킹 API와 콘텐츠 랭킹 스냅샷 생성만 구현한다.
  • 기존 크리에이터 랭킹의 스냅샷 테이블에 rankingType을 추가하거나, 크리에이터 랭킹 계산 스케줄을 01:00~07:30 분산 방식으로 변경하는 작업은 이번 PRD에 포함하지 않는다.
  • 크리에이터 랭킹 다중 타입 전환과 스케줄 분산 처리는 별도 PRD에서 기존 크리에이터 랭킹 PRD/DDL/구현 계획을 갱신해 다룬다.

Feature H. fallback 랭킹 계산

Requirements

  • 조회 시 요청한 랭킹 타입의 최신 완료 주차 스냅샷이 없으면 fallback 실행 가능 여부를 확인한다.
  • fallback은 스케줄러가 호출하는 랭킹 계산 로직과 동일한 refresh service를 직접 실행한다.
  • fallback 실행 전 FALLBACK trigger의 작업 이력을 PENDING 또는 PROCESSING 상태로 기록한다.
  • fallback 성공 시 DONE, 실패 시 FAILED로 작업 이력을 기록한다.
  • 동일 랭킹 타입과 동일 집계 기간의 fallback 실행 이력이 3회 이상이면 추가 fallback을 실행하지 않는다.
  • fallback으로 스냅샷이 생성되면 해당 스냅샷을 다시 조회해 응답한다.
  • fallback으로 생성된 스냅샷도 visibleFromAt <= now 조건을 만족해야 공개 조회에 노출한다.
  • fallback 실행 후에도 스냅샷이 없으면 빈 배열로 성공 응답한다.
  • fallback 여부는 공개 API response schema에 포함하지 않는다.

Edge Cases

  • 다른 요청이 같은 랭킹 타입/기간 fallback을 처리 중이면 lock 획득 실패를 정상 skip으로 간주하고, 현재 요청은 재조회 후 없으면 빈 배열로 응답한다.
  • fallback 계산 중 예외가 발생해도 공개 API는 내부 오류를 그대로 노출하지 않고 기존 예외/응답 정책을 따른다.
  • fallback 작업 이력 저장 실패와 랭킹 계산 실패의 트랜잭션 경계는 구현 계획 단계에서 크리에이터 랭킹 작업 이력 패턴을 따른다.

Feature I. v2 재사용 후보

Requirements

  • API 조립 계층은 v2/api/content/recommendationAudioRecommendationController, AudioRecommendationFacade, DTO 변환 패턴을 참고한다.
  • 도메인 조회 계층은 v2/content/recommendation/application/AudioRecommendationQueryService처럼 응답 조립에 필요한 도메인 모델을 반환한다.
  • rankChange, isNew, showRankChange, fallback 로그/작업 이력 패턴은 v2/ranking/application/CreatorRankingQueryServiceCreatorRankingSnapshotJobService를 참고한다.
  • 주간 기간 계산, UTC 변환, Redisson lock은 v2/ranking/domain/CreatorRankingPeriodPolicy와 크리에이터 랭킹 스냅샷 job 구조를 재사용하거나 콘텐츠 랭킹용으로 동일 패턴을 만든다.
  • 상세 페이지 조회수는 v2/recommendation/adapter/out/persistence/CreatorContentViewHistory와 관련 port/repository를 재사용 후보로 검토한다.
  • CDN URL 조립은 v2/common/domain/CdnUrlExtensions.kttoCdnUrl 패턴을 우선 사용한다.
  • REVENUE, SALES_COUNT, COMMENT_COUNT, LIKE_COUNT는 legacy adapter를 사용하지 않고 v2 집계 repository에서 직접 후보를 만든다.

8. API Endpoint

GET /api/v2/audio/rankings?type=WEEKLY_POPULAR
Authorization: Bearer {accessToken} (optional)
  • 비회원 조회를 허용한다.
  • 회원 조회 시 기존 v2 controller 패턴과 동일하게 anonymous user를 null member로 처리한다.
  • type이 없으면 WEEKLY_POPULAR를 기본값으로 사용한다.
  • 잘못된 type 값에 대한 오류 응답은 기존 enum request parameter 오류 처리 정책을 따른다.

9. Response Data Class

data class AudioRankingResponse(
    val showRankChange: Boolean,
    val type: AudioRankingType,
    val items: List<AudioRankingItemResponse>
)

enum class AudioRankingType {
    WEEKLY_POPULAR,
    RISING,
    REVENUE,
    SALES_COUNT,
    COMMENT_COUNT,
    LIKE_COUNT
}

data class AudioRankingItemResponse(
    val contentId: Long,
    val title: String,
    val creatorNickname: String,
    val rank: Int,
    val rankChange: Int?,
    @JsonProperty("isNew")
    val isNew: Boolean,
    val coverImageUrl: String?
)

coverImageUrl 응답 정책은 다음과 같다.

  • 스냅샷 테이블의 표시용 커버 이미지 값은 원천 audio_content.cover_image와 같은 path 형태로 저장될 수 있다.
  • 공개 API 응답의 coverImageUrl은 클라이언트가 바로 이미지 로딩에 사용할 수 있도록 cloud.aws.cloud-front.host를 prefix로 포함한다.
  • 변환은 v2/common/domain/CdnUrlExtensions.kttoCdnUrl 정책을 따른다.
  • null, 빈 문자열, blank 값은 null로 유지한다.
  • 이미 https:// 또는 http://로 시작하는 값은 외부/완성 URL로 보고 그대로 유지한다.
  • 이 정책은 스냅샷 생성, 정렬, rankChange, isNew, fallback 여부와 무관한 Response 조립 정책이며, 기존 스냅샷 데이터의 재생성이나 DDL 변경을 요구하지 않는다.

응답 예시는 다음과 같다.

{
  "showRankChange": true,
  "type": "WEEKLY_POPULAR",
  "items": [
    {
      "contentId": 123,
      "title": "Audio title",
      "creatorNickname": "creator",
      "rank": 1,
      "rankChange": 5,
      "isNew": false,
      "coverImageUrl": "https://cdn.example.com/audio-cover.png"
    },
    {
      "contentId": 456,
      "title": "New audio",
      "creatorNickname": "new creator",
      "rank": 2,
      "rankChange": null,
      "isNew": true,
      "coverImageUrl": "https://cdn.example.com/audio-cover-new.png"
    }
  ]
}

10. Technical Constraints

  • Kotlin + Spring Boot 2.7.14 기준으로 작성한다.
  • Java 17 런타임을 기준으로 한다.
  • 신규 코드는 kr.co.vividnext.sodalive.v2 하위에 배치한다.
  • 공개 API 조립 계층과 도메인 조회 계층을 분리한다.
  • 스냅샷과 작업 이력 저장은 MySQL 기준 DDL을 별도 문서 또는 구현 계획에서 작성한다.
  • 이번 PRD에서 예상하는 신규 Entity는 content_ranking_snapshot, content_ranking_snapshot_job 테이블에 대응하며, 초안 DDL은 docs/20260623_메인_콘텐츠_랭킹_탭_API/create-content-ranking-tables.sql에 둔다.
  • 시간 기준은 Asia/Seoul을 명시하고 DB 조회는 UTC 변환 범위를 사용한다.
  • 테스트는 순위 변화 계산, 정규화 산식, 동점 정렬, fallback 최대 3회 제한, 작업 이력 기록을 포함해야 한다.

11. Metrics

  • 랭킹 조회 API 응답 시간
  • 랭킹 타입별 스냅샷 생성 성공/실패 횟수
  • fallback 실행 횟수와 성공/실패 횟수
  • fallback 최대 3회 초과로 빈 응답한 횟수
  • 랭킹 타입별 응답 item 수

12. Open Questions

  • fallback 최대 3회 제한은 동일 랭킹 타입과 동일 집계 기간 기준으로 가정한다.
  • 지금 뜨는 중의 최소 반영 기준은 콘텐츠 전체 후보 제외가 아니라 지표별 점수 반영 제외로 해석한다. 예를 들어 최근 7일 조회수는 10회 미만이지만 좋아요 3개 이상, 댓글 3개 이상이면 조회수 증가율만 0으로 계산하고 좋아요/댓글 증가율은 점수에 반영한다. 콘텐츠 전체 후보 제외가 의도라면 구현 전에 수정해야 한다.