26 KiB
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위이면
rankChange는5다. - 예를 들어 직전 완료 주차 1위, 최신 완료 주차 10위이면
rankChange는-9다. - 직전 완료 주차에는 없고 최신 완료 주차에 진입한 콘텐츠는
isNew == true로 내려준다. - 신규 진입 콘텐츠의
rankChange는 비교 가능한 이전 순위가 없으므로null로 내려준다. - 직전 완료 주차 스냅샷이 없으면
showRankChange == false로 내려주고, 각 item의rankChange는null,isNew는false로 내려준다. - 직전 완료 주차 스냅샷이 있으면
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
- Release 3일 이내:
- 최종 정렬은 정규화 점수 내림차순,
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의 기준안은 모든 랭킹 타입을 스냅샷으로 저장하는 것이다.
- 이유는
rankChange와isNew가 모든 응답 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/Seoulzone을 명시한다. - 다중 서버 인스턴스에서 같은 스케줄이 동시에 실행되더라도 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 실행 전
FALLBACKtrigger의 작업 이력을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/recommendation의AudioRecommendationController,AudioRecommendationFacade, DTO 변환 패턴을 참고한다. - 도메인 조회 계층은
v2/content/recommendation/application/AudioRecommendationQueryService처럼 응답 조립에 필요한 도메인 모델을 반환한다. rankChange,isNew,showRankChange, fallback 로그/작업 이력 패턴은v2/ranking/application/CreatorRankingQueryService와CreatorRankingSnapshotJobService를 참고한다.- 주간 기간 계산, UTC 변환, Redisson lock은
v2/ranking/domain/CreatorRankingPeriodPolicy와 크리에이터 랭킹 스냅샷 job 구조를 재사용하거나 콘텐츠 랭킹용으로 동일 패턴을 만든다. - 상세 페이지 조회수는
v2/recommendation/adapter/out/persistence/CreatorContentViewHistory와 관련 port/repository를 재사용 후보로 검토한다. - CDN URL 조립은
v2/common/domain/CdnUrlExtensions.kt의toCdnUrl패턴을 우선 사용한다. 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를
nullmember로 처리한다. 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.kt의toCdnUrl정책을 따른다. 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으로 계산하고 좋아요/댓글 증가율은 점수에 반영한다. 콘텐츠 전체 후보 제외가 의도라면 구현 전에 수정해야 한다.