feat(content): 랭킹 탭을 추가한다

This commit is contained in:
Yu Sung
2026-07-06 20:19:27 +09:00
parent bae92eba4b
commit d129583d3b
15 changed files with 1322 additions and 1 deletions

View File

@@ -0,0 +1,173 @@
# PRD: 메인 콘텐츠 탭 내부 랭킹 탭
## 1. Overview
메인 하단 `콘텐츠` 탭 안의 내부 `랭킹` 탭에서 `GET /api/v2/audio/rankings` 응답을 사용해 오디오 콘텐츠 순위를 제공한다.
상단 title bar와 내부 `추천/랭킹/전체` tab bar는 기존 콘텐츠 탭 구조를 유지한다. 내부 `랭킹` 탭 화면에서는 서버가 내려준 순위와 순위 변동 정보를 기준으로 1위 전용 UI, 2~20위 row UI를 구성한다. 신규 기능이므로 API, 모델, ViewModel, 화면, 랭킹 전용 컴포넌트는 `SodaLive/Sources/V2/Main/Content/Ranking/**` 하위에 둔다.
Figma 참조:
- 전체: `24:6857`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-6857&m=dev`
- 1위: `24:6861`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-6861&m=dev`
- 2~10위: `24:6867`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-6867&m=dev`
- 11~20위: `24:6872`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-6872&m=dev`
## 2. Problem
- 현재 V2 메인 콘텐츠 탭의 내부 `랭킹` 탭은 placeholder 상태라 사용자가 콘텐츠 랭킹을 볼 수 없다.
- 기존 메인 홈 `랭킹` 탭은 크리에이터 순위용 응답과 UI에 맞춰져 있어 오디오 콘텐츠 랭킹의 `title`, `creatorNickname`, `coverImageUrl`, `contentId` 요구사항을 그대로 수용하기 어렵다.
- Figma에서 1위 UI는 2~20위와 형태가 다르므로 순위별 컴포넌트 경계를 명확히 나눠야 한다.
- 2~20위는 표시 데이터만 다르고 UI 형태가 메인 홈 랭킹 탭의 크리에이터 순위 row와 매우 유사하므로, 전용 컴포넌트를 만들더라도 `MainHomeRankingRow` 구조를 거의 그대로 복사해 사용할 수 있는지 먼저 검토한다.
- 2~20위 row에서 달라지는 핵심 영역은 텍스트 영역이며, 위에는 콘텐츠 타이틀, 아래에는 크리에이터 닉네임을 표시한다.
- 고정 width/height를 직접 박으면 다양한 기기 폭에서 Figma 구조가 깨질 수 있으므로 부모 width, aspect ratio, padding 기반의 반응형 제약이 필요하다.
## 3. Goals
- 메인 콘텐츠 탭 내부 `랭킹` 탭에서 `GET /api/v2/audio/rankings` 응답 기반 UI를 표시한다.
- `AudioRankingResponse`, `AudioRankingType`, `AudioRankingItemResponse`를 Swift `Decodable` 모델로 반영한다.
- 서버가 내려준 `items[].rank`를 화면 순위로 사용하고 클라이언트에서 index 기반 순위를 재계산하지 않는다.
- item은 최대 20개까지 표시한다.
- 1위는 Figma `24:6861` 기준 전용 UI로 구현한다.
- 2~20위는 단일 row 컴포넌트로 구현하고, 메인 홈 랭킹 탭의 크리에이터 순위 row 구조를 거의 그대로 복사한 뒤 오디오 콘텐츠 필드에 맞게 치환하는 방식을 우선한다.
- 2~20위 row의 텍스트 영역은 위에 콘텐츠 타이틀, 아래에 크리에이터 닉네임을 표시한다.
- `showRankChange`, `rankChange`, `isNew` 값에 따라 순위 변화 UI를 표시하거나 숨긴다.
- 오디오 item 탭 시 기존 콘텐츠 상세 진입 callback을 재사용한다.
- V2 패키지 하위에 구현된 재사용 가능한 위젯 후보를 확인하고, 직접 재사용이 어렵더라도 구조 복사에 가까운 재활용이 가능한 부분을 명시한다.
## 4. Non-Goals
- 실제 구현과 Xcode 프로젝트 수정은 이 PRD 작성 범위에 포함하지 않는다.
- API 스펙에 없는 랭킹 타입 filter UI를 임의로 추가하지 않는다.
- 서버 응답의 `rank`를 무시하고 클라이언트 index로 순위를 재계산하지 않는다.
- 20위를 초과한 item 노출 정책을 새로 만들지 않는다.
- 기존 메인 홈 `랭킹` 탭 동작을 변경하지 않는다.
- 기존 legacy `SodaLive/Sources/Content/**` 화면을 리팩터링하지 않는다.
- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다.
- 외부 라이브러리를 추가하지 않는다.
- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다.
## 5. Target Users
- 메인 콘텐츠 탭에서 인기 오디오 콘텐츠 순위를 빠르게 확인하려는 사용자
- 순위 변화, 신규 진입 여부를 보고 콘텐츠를 선택하려는 사용자
- 콘텐츠 타이틀과 크리에이터 닉네임을 함께 확인한 뒤 오디오 상세로 이동하려는 사용자
## 6. User Stories
- 사용자는 콘텐츠 탭의 내부 `랭킹` 탭을 누르면 현재 오디오 랭킹을 보고 싶다.
- 사용자는 1위 콘텐츠를 다른 순위보다 더 큰 시각적 강조로 확인하고 싶다.
- 사용자는 2~20위 콘텐츠를 같은 row 형식으로 빠르게 훑어보고 싶다.
- 사용자는 각 row에서 콘텐츠 타이틀과 크리에이터 닉네임을 구분해 보고 싶다.
- 사용자는 순위 상승, 하락, 유지, 신규 진입 상태를 확인하고 싶다.
- 사용자는 랭킹 item을 눌러 기존 오디오 상세 화면으로 이동하고 싶다.
## 7. Core Requirements
### 7.1 API
- Method: `GET`
- Path: `/api/v2/audio/rankings`
- 인증: 기존 V2 API 인증 헤더 패턴을 따른다.
- Request query: 현재 제공 명세 기준 없음.
- 응답 래퍼: 기존 관례대로 `ApiResponse<AudioRankingResponse>` 디코딩을 우선한다.
### 7.2 Response model
```kotlin
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?
)
```
Swift 모델은 기존 V2 관례에 맞춰 `Decodable` 구조체와 `String` raw value enum으로 선언한다. Kotlin `Long` 대응 값은 프로젝트 기존 모델 관례에 맞춰 Swift `Int`를 우선 사용한다. `AudioRankingItemResponse` 대응 모델은 `Identifiable`을 채택하고 `id == contentId`로 둔다.
### 7.3 Ranking display
- 서버 응답 `items` 순서를 유지한다.
- 화면 순위 숫자는 `item.rank`를 사용한다.
- 클라이언트에서 index 기반으로 순위를 다시 계산하지 않는다.
- 표시 대상은 최대 20개로 제한한다.
- item이 없으면 empty state를 표시한다.
### 7.4 Rank change display
- `showRankChange == false`이면 순위 변화 영역을 표시하지 않는다.
- `showRankChange == true`이고 `isNew == true`이면 신규 진입 표시를 우선한다.
- `rankChange > 0`이면 상승 표시와 절댓값 숫자를 표시한다.
- `rankChange < 0`이면 하락 표시와 절댓값 숫자를 표시한다.
- `rankChange == 0`이면 변동 없음 표시를 사용한다.
- `rankChange == nil && isNew == false`이면 순위 변화 영역을 표시하지 않는다.
- 기존 asset 후보는 `ic_rank_new`, `ic_rank_caret_increase`, `ic_rank_caret_decrease`, `ic_rank_caret_stay`를 우선 확인한다.
### 7.5 UI layout
- 내부 tab bar에서 `랭킹`이 선택된 상태로 표시된다.
- title bar와 내부 tab bar는 기존 `MainContentView` shell을 유지한다.
- 랭킹 탭 본문은 자체 `ScrollView`로 구성한다.
- 1위 item은 Figma `24:6861` 기준 전용 card로 표시한다.
- 2~20위 item은 Figma `24:6867`, `24:6872` 기준 row로 표시한다.
- 2~20위 row는 `MainHomeRankingRow`의 순위 컬럼, 썸네일, padding, `aspectRatio`, 터치 영역 구조를 최대한 유지하고, 데이터 모델과 텍스트 영역만 오디오 랭킹에 맞게 바꾼다.
- 2~20위 row의 텍스트 영역은 위 `title`, 아래 `creatorNickname` 순서로 표시한다.
- 텍스트가 길면 Figma 기준 line limit 안에서 말줄임 처리한다.
- 루트 layout과 반복 item에는 고정 숫자 width/height를 사용하지 않는다.
- 필요한 숫자 치수는 아이콘, 내부 padding, 최소 글자 크기 같은 내부 요소에만 제한적으로 사용한다.
- card/row 전체 크기는 parent width, `GeometryReader`, `aspectRatio`, flexible layout, padding 기반으로 정한다.
### 7.6 Navigation
- 랭킹 item 탭은 `contentId`로 기존 content detail 진입 흐름을 사용한다.
- `MainContentView`는 기존 `onTapContent` callback을 `MainContentRankingView`에 전달한다.
- 기존 `MainView.handleRecommendationContentTap`의 로그인/성인 콘텐츠 guard 흐름을 재사용한다.
### 7.7 Empty and error state
- 첫 로드 중에는 loading 상태를 표시한다.
- 첫 페이지 응답이 비어 있으면 empty state를 표시한다.
- API 실패 또는 디코딩 실패 시 사용자 노출 문구를 `I18n.MainContentRanking`에 둔다.
- 기존 목록이 없는 실패 상태에서는 랭킹 탭 내부 안내를 표시한다.
## 8. UX / UI Expectations
- 전체 배경은 기존 V2 dark UI 기준을 따른다.
- 1위는 Figma 기준으로 다른 순위보다 시각적 우선순위가 높게 보이도록 구성한다.
- 2~20위는 반복 row로 스캔하기 쉬워야 한다.
- 순위 숫자와 순위 변화 표시는 홈 랭킹 탭과 시각적 일관성을 유지한다.
- 썸네일은 `coverImageUrl`을 사용하고, 값이 없거나 비어 있으면 기존 V2 이미지 fallback 배경을 따른다.
- 콘텐츠 타이틀은 크리에이터 닉네임보다 우선되는 텍스트 계층으로 표시한다.
- 카드와 row의 터치 영역은 사용자가 오디오 상세 진입 가능성을 인지할 수 있게 item 전체 영역으로 둔다.
## 9. Technical Constraints
- 신규 기능 파일은 `SodaLive/Sources/V2/Main/Content/Ranking/**` 하위에 둔다.
- 콘텐츠 탭 shell은 기존 `SodaLive/Sources/V2/Main/Content/MainContentView.swift` 구조를 유지한다.
- 여러 화면에서 재사용할 명확한 근거가 생긴 컴포넌트만 `SodaLive/Sources/V2/Component/**`로 승격한다.
- 기존 홈 랭킹 row 컴포넌트는 모델 결합이 있으므로 직접 재사용은 어렵지만, 2~20위 오디오 row 전용 컴포넌트 작성 시 레이아웃 구조는 거의 그대로 복사하는 것을 우선 검토한다.
- 기존 `TextTabBar`, `HomeTitleBar`, 홈 랭킹 badge/row/card, `AudioContentThumbnailCard`, `AudioContentListRow`를 재사용 후보로 먼저 확인한다.
- 신규 사용자 노출 문구는 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja로 추가한다.
- 외부 라이브러리는 추가하지 않는다.
## 10. Success Criteria
- `docs/20260706_메인_콘텐츠_내부_랭킹_탭/plan-task.md`가 본 PRD를 기준 문서로 참조한다.
- `.content` 탭 내부 `랭킹` 분기에서 placeholder 대신 랭킹 화면을 표시하는 구현 계획이 있다.
- `GET /api/v2/audio/rankings` endpoint와 응답 모델이 문서화되어 있다.
- Figma 전체, 1위, 2~10위, 11~20위 URL이 PRD와 계획 문서에 기록되어 있다.
- 1위 전용 UI와 2~20위 row UI가 분리되어 계획되어 있다.
- 2~20위 row의 텍스트 영역이 `title`/`creatorNickname` 구조임이 명시되어 있다.
- 고정 width/height 금지와 반응형 layout 원칙이 명시되어 있다.
- V2 재사용 가능한 위젯 후보가 계획 문서에 기록되어 있다.
- 구현 단계에서는 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -sdk iphonesimulator build`가 성공해야 한다.
## 11. Open Questions
- 현재 제공 요구사항 기준으로 미결정 사항은 없다.
## 12. Verification Notes
- 2026-07-06: 사용자 요청으로 최초 `plan-task.md`가 먼저 작성되었고, 이후 PRD 누락 지적에 따라 본 PRD를 추가했다.
- 2026-07-06: `docs/prd/sample-prd.md`, `docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md`, `docs/20260706_메인_콘텐츠_내부_랭킹_탭/plan-task.md`를 확인해 PRD 형식과 계획 문서 내용을 맞췄다.
- 2026-07-06: 사용자 요청 범위가 문서 생성이므로 Swift 소스, Xcode 프로젝트, asset은 수정하지 않는다.