Files
sodalive-ios/docs/20260630_메인_홈_랭킹_탭/prd.md

248 lines
16 KiB
Markdown

# PRD: 메인 홈 랭킹 탭
## 1. Overview
메인 홈 화면의 `랭킹` 탭에서 신규 API `GET /api/v2/home/rankings/creators` 응답을 사용해 인기 크리에이터 랭킹을 표시한다.
기존 `MainHomeView`는 홈 상단 공통 shell과 `추천`/`랭킹`/`팔로잉` 탭 전환만 담당한다. 랭킹 탭의 API 호출, 상태 관리, 화면 조립은 `SodaLive/Sources/V2/Main/Home/Ranking/**` 하위에서 처리한다.
Figma 전체 화면의 3번째 줄에 있는 Capsule Tab bar는 이번 랭킹 탭 화면에 배치하지 않는다. 즉, `주간 인기`, `지금 뜨는 중`, `남성 인기`, `여성 인기` 필터 UI는 구현 범위에서 제외한다.
## 2. Problem
- 현재 `MainHomeRankingView`는 placeholder 상태라 홈의 `랭킹` 탭에서 실제 크리에이터 랭킹 데이터를 제공하지 못한다.
- 랭킹 화면은 순위별로 카드 크기와 배치 방식이 다르므로, 단순 리스트로 처리하면 Figma 기준 UI와 맞지 않는다.
- 서버는 순위 변화 표시 여부(`showRankChange`)와 각 item의 변화 값(`rankChange`, `isNew`)을 함께 내려주므로, 클라이언트가 이를 일관된 규칙으로 표현해야 한다.
- 신규 랭킹 API를 기존 추천 탭 API나 기존 구 홈 API에 섞으면 홈 탭별 책임이 흐려지고 회귀 위험이 커진다.
## 3. Goals
- `랭킹` 탭 진입 시 `GET /api/v2/home/rankings/creators`를 호출하고 응답 데이터로 화면을 구성한다.
- 랭킹 탭 전용 API, Repository, ViewModel, Response model을 `SodaLive/Sources/V2/Main/Home/Ranking/**` 아래에 둔다.
- Figma 기준으로 1위, 2~7위, 8~10위, 11위 이상 item의 배치와 시각 구조를 구분한다.
- `items[].rank` 값을 화면 순위로 사용하고 배열 index로 순위를 재계산하지 않는다.
- `showRankChange == true`일 때만 순위 변화 badge를 표시한다.
- `items[].isNew == true`이면 순위 변화 숫자 대신 `ic_rank_new` 아이콘을 표시한다.
- `items[].rankChange`가 양수이면 순위 상승, 음수이면 순위 하락으로 표시한다.
- 직전 공개 스냅샷이 없어 `showRankChange == false`이면 모든 item에서 순위 변화 UI를 숨긴다.
- 최대 item 개수는 20개로 고정한다.
- 크리에이터 item 탭 시 기존 크리에이터 상세 진입 흐름을 사용한다.
- Capsule Tab bar는 배치하지 않는다.
## 4. Non-Goals
- Figma 전체 화면 3번째 줄의 Capsule Tab bar를 구현하지 않는다.
- `주간 인기`, `지금 뜨는 중`, `남성 인기`, `여성 인기` 같은 랭킹 필터/세그먼트 기능은 구현하지 않는다.
- 이번 API 스펙에 없는 페이지네이션, 무한 스크롤, 정렬 변경, 성별 필터, 기간 필터를 추가하지 않는다.
- 추천 탭, 팔로잉 탭, 하단 메인 탭 구조를 변경하지 않는다.
- 기존 구 홈 화면의 `HomeCreatorRankingItemView`, `HomeTabViewModel.creatorRanking` 동작을 변경하지 않는다.
- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다.
- 서버가 내려주지 않는 팔로우 상태, 라이브 상태, 후원 정보, 카테고리 정보를 임의로 표시하지 않는다.
## 5. Target Users
- 홈에서 현재 인기 크리에이터를 빠르게 탐색하려는 사용자
- 크리에이터의 순위와 직전 공개 스냅샷 대비 변화 여부를 확인하려는 사용자
- 신규 진입 크리에이터를 랭킹 화면에서 구분해 보고 싶은 사용자
## 6. User Stories
- 사용자는 홈 `랭킹` 탭에서 인기 크리에이터 목록을 순위별 강조도에 맞게 보고 싶다.
- 사용자는 순위가 오른 크리에이터와 내려간 크리에이터를 구분하고 싶다.
- 사용자는 새로 랭킹에 진입한 크리에이터를 신규 진입 아이콘으로 확인하고 싶다.
- 사용자는 마음에 드는 크리에이터를 탭해 상세 화면으로 이동하고 싶다.
## 7. Core Requirements
### 7.1 랭킹 데이터 조회
#### API
- Method: `GET`
- Path: `/api/v2/home/rankings/creators`
- 인증: 기존 V2 홈 API 인증 헤더 패턴을 따른다.
- 응답 래퍼: 기존 관례대로 `ApiResponse<MainHomeCreatorRankingResponse>` 디코딩을 우선한다.
#### Response
```json
{
"showRankChange": true,
"items": [
{
"rank": 1,
"rankChange": 5,
"isNew": false,
"creatorId": 123,
"nickname": "creator",
"profileImageUrl": "https://cdn.example.com/profile.png"
},
{
"rank": 2,
"rankChange": null,
"isNew": true,
"creatorId": 456,
"nickname": "new creator",
"profileImageUrl": "https://cdn.example.com/profile-new.png"
}
]
}
```
#### Swift 모델 기준
```swift
struct MainHomeCreatorRankingResponse: Decodable {
let showRankChange: Bool
let items: [MainHomeCreatorRankingItem]
}
struct MainHomeCreatorRankingItem: Decodable, Identifiable {
let rank: Int
let rankChange: Int?
let isNew: Bool
let creatorId: Int
let nickname: String
let profileImageUrl: String?
var id: Int { creatorId }
}
```
`profileImageUrl`은 서버 예시에서 문자열로 내려오지만, 이미지가 없을 수 있는 운영 데이터를 고려해 Swift 모델에서는 optional로 선언한다. 이미지가 비어 있거나 로드 실패하면 기존 프로젝트의 기본 프로필 이미지 처리 패턴을 따른다.
### 7.2 순위 변화 표시 규칙
- `showRankChange``items`와 같은 레벨에 내려오는 화면 제어 값이다.
- `showRankChange == false`이면 모든 item에서 `rankChange`, `isNew` 값과 관계없이 순위 변화 UI를 표시하지 않는다.
- `showRankChange == true && item.isNew == true`이면 `ic_rank_new` 아이콘을 표시한다.
- `isNew == true`인 item은 비교 가능한 이전 순위가 없으므로 `rankChange == nil`로 내려오는 것을 정상으로 취급한다.
- `showRankChange == true && item.isNew == false && item.rankChange != nil`이면 순위 변화 숫자 badge를 표시한다.
- `rankChange > 0`이면 순위 상승으로 표시한다. 예: 직전 10위, 최신 5위는 `rankChange == 5`.
- `rankChange < 0`이면 순위 하락으로 표시한다. 예: 직전 1위, 최신 10위는 `rankChange == -9`.
- `rankChange == 0`이면 숫자 `0`은 표시하지 않고 `ic_rank_caret_stay` 아이콘으로 변동 없음 상태를 표시한다.
- 숫자 badge에는 변화량의 절댓값을 표시하고, 상승/하락 방향은 전용 caret 아이콘으로 구분한다.
- 상승 badge는 Figma `node-id=24-5674` 기준으로 표시하고 `ic_rank_caret_increase` 아이콘을 사용한다.
- 하락 badge는 Figma `node-id=24-5675` 기준으로 표시하고 `ic_rank_caret_decrease` 아이콘을 사용한다.
- 신규 진입 표시는 텍스트 `New` 대신 `ic_rank_new` 아이콘을 사용한다.
- `rankChange == nil && isNew == false`이면 순위 변화 badge를 표시하지 않는다.
직전 공개 스냅샷이 없을 때 서버 응답은 아래처럼 처리된다는 전제를 둔다.
- `showRankChange == false`
- 각 item의 `rankChange == nil`
- 각 item의 `isNew == false`
### 7.3 랭킹 item 레이아웃
Figma 참조:
- 전체 화면: `node-id=24-5654`
- 1위 카드: `node-id=24-5659`
- 2~7위 카드: `node-id=24-5661`
- 8~10위 카드: `node-id=24-5667`
- 11위 이상 row: `node-id=24-5670`
배치 규칙:
- `rank == 1`: 한 줄에 1개, 화면 width 기준 큰 카드로 표시한다.
- `rank >= 2 && rank <= 7`: 한 줄에 2개 grid 카드로 표시한다.
- `rank >= 8 && rank <= 10`: 한 줄에 3개 grid 카드로 표시한다.
- `rank >= 11`: row 방식으로 표시한다.
- `rank == 1`, `rank >= 2 && rank <= 7`, `rank >= 8 && rank <= 10`은 크기와 배치만 다르고 카드 모양은 동일하므로 하나의 카드 컴포넌트를 재사용한다.
- 1열/2열/3열 배치는 카드 컴포넌트가 아니라 부모 grid/layout이 결정한다.
- `rank >= 11`은 row 방식으로 모양이 다르므로 카드와 별도 row 컴포넌트로 분리한다.
- 카드/row 컴포넌트의 전체 레이아웃 크기는 고정 숫자 width/height로 제한하지 않고, 화면 크기에 대응할 수 있도록 부모 width, grid column, aspect ratio, relative sizing 기반으로 구성한다.
- API 응답 item은 최대 20개로 취급한다.
- item은 서버가 내려준 `items` 순서를 기본으로 렌더링하되, 화면에 표시하는 순위 숫자는 `item.rank`를 사용한다.
- 서버 순서와 `item.rank`가 불일치해도 클라이언트에서 임의 재정렬하지 않는다. 불일치는 서버 데이터 문제로 보고 QA/로그 확인 대상으로 남긴다.
- 카드/row는 전체적으로 검정 배경 위에 배치한다.
- profile image는 카드/row의 주요 이미지로 사용한다.
- nickname은 Figma의 `텍스트` 위치에 표시한다.
- rank 숫자는 Figma의 rank number 위치와 크기 계층을 따른다.
- 카드형 item은 이미지 하단에 dim gradient를 적용해 nickname 가독성을 확보한다.
### 7.4 화면 상태
- 최초 진입 또는 탭 전환으로 랭킹 탭이 처음 노출될 때 API를 호출한다.
- 동일 화면 생명주기 안에서 이미 성공적으로 로드한 데이터가 있으면 불필요한 중복 호출은 피한다.
- `items`가 빈 배열이면 empty state를 표시한다.
- API 실패 시에도 empty state 문구를 표시한다.
- empty state 문구는 `순위 집계 중입니다.\n잠시 후 다시 시도해 주세요.`로 표시한다.
- pull-to-refresh가 기존 `MainHomeView` 또는 추천 탭에 있다면 같은 패턴으로 재조회한다. 기존 패턴이 없다면 이번 범위에서 새로 추가하지 않는다.
### 7.5 상세 진입
- 랭킹 item 탭 시 `creatorId`로 기존 크리에이터 상세 화면에 진입한다.
- 랭킹 조회 자체는 로그인, 본인인증, 민감 콘텐츠 설정으로 막지 않는다.
- 랭킹 페이지 노출에는 로그인 guard가 필요하지 않다.
- 터치 등의 사용자 액션으로 상세 페이지에 진입할 때는 로그인 guard가 필요하다.
- 상세 진입 guard는 기존 메인/추천 탭에서 사용하는 guard 패턴을 재사용한다.
- 상세 진입 전 서버 응답에 없는 성인/민감 콘텐츠 여부를 클라이언트에서 임의 추정하지 않는다.
## 8. UX / UI Expectations
### 8.1 포함하는 UI
- 홈 공통 title bar
- 홈 상단 `추천`/`랭킹`/`팔로잉` Text tab bar
- 선택된 `랭킹` 탭 콘텐츠
- 크리에이터 랭킹 카드/row 목록
- 순위 변화 숫자 badge 또는 신규 진입 아이콘
- 하단 메인 tab bar는 기존 `MainView` 구조에서 유지
### 8.2 제외하는 UI
- Figma 전체 화면 3번째 줄의 Capsule Tab bar
- 랭킹 필터 chip/segment
- 화면 내 별도 섹션 타이틀
### 8.3 시각 규칙
- 선택된 홈 상단 탭 `랭킹`은 흰색 bold text로 표시한다.
- 비선택 탭 `추천`, `팔로잉`은 gray text로 표시한다.
- 카드 corner radius, 이미지 crop, dim gradient, badge 위치는 Figma 기준을 따른다.
- 카드/row의 루트 레이아웃에는 고정 숫자 width/height를 사용하지 않는다.
- 카드 비율과 row 높이는 `aspectRatio`, grid column width, container-relative size 등 반응형 제약을 우선 사용한다.
- 신규 진입 표시는 텍스트 `New` 대신 `ic_rank_new` 아이콘을 사용한다.
- rankChange 상승 badge는 Figma `node-id=24-5674` 기준으로 어두운 배경 위 숫자와 `ic_rank_caret_increase` 아이콘을 사용한다.
- rankChange 하락 badge는 Figma `node-id=24-5675` 기준으로 어두운 배경 위 숫자와 `ic_rank_caret_decrease` 아이콘을 사용한다.
- `rankChange == 0`은 Figma `node-id=24-5678` 예시가 있더라도 숫자 `0`을 표시하지 않고 `ic_rank_caret_stay` 아이콘으로 변동 없음 상태를 표시한다.
- nickname이 길면 카드/row 영역을 넘지 않도록 한 줄 말줄임 또는 기존 프로젝트의 nickname 표시 관례를 따른다.
### 8.4 순위 변화 아이콘
- 상승: `ic_rank_caret_increase`
- 하락: `ic_rank_caret_decrease`
- 변동 없음: `ic_rank_caret_stay`
- 신규 진입: `ic_rank_new`
## 9. Technical Constraints
- 신규 랭킹 탭 관련 파일은 `SodaLive/Sources/V2/Main/Home/Ranking/**` 아래에 둔다.
- `MainHomeView`에는 API 호출, ranking item 변환, 랭킹 세부 UI를 넣지 않는다.
- 기존 `MainHomeRecommendationView`와 추천 탭 API 동작을 변경하지 않는다.
- 기존 `TextTabBar` 또는 홈 상단 탭 컴포넌트를 재사용한다.
- profile image 로딩은 기존 프로젝트의 이미지 로딩 패턴을 따른다.
- 카드/row UI 컴포넌트의 루트 container는 고정 숫자 `frame(width:)`, `frame(height:)`로 제한하지 않는다.
- 외부 라이브러리를 추가하지 않는다.
- Figma에서 제공되는 localhost asset URL을 앱 코드에 직접 사용하지 않는다.
- 신규 순위 변화 아이콘은 `ic_rank_caret_decrease`, `ic_rank_caret_increase`, `ic_rank_caret_stay`, `ic_rank_new` asset을 사용한다.
- 다국어가 필요한 신규 문구는 `I18n`에 추가한다. 신규 진입 표시는 텍스트가 아니라 `ic_rank_new` 아이콘으로 처리한다.
## 10. Success Criteria
- `랭킹` 탭 진입 시 `GET /api/v2/home/rankings/creators` 호출이 발생한다.
- 응답의 `items`가 rank 규칙에 따라 1위 단독, 2~7위 2열, 8~10위 3열, 11위 이상 row로 표시된다.
- 1위, 2~7위, 8~10위는 하나의 카드 컴포넌트를 재사용하고, 11위 이상은 별도 row 컴포넌트를 사용한다.
- 카드/row 컴포넌트의 루트 레이아웃은 고정 숫자 width/height가 아니라 화면 크기 대응 가능한 제약으로 구성되어 있다.
- 최대 20개 item을 표시 대상으로 처리한다.
- 화면에 표시되는 순위 숫자는 배열 index가 아니라 `items[].rank`와 일치한다.
- `showRankChange == false`이면 모든 item에서 순위 변화 UI가 표시되지 않는다.
- `showRankChange == true && isNew == true`이면 `ic_rank_new` 아이콘이 표시된다.
- `showRankChange == true && rankChange > 0`이면 상승 변화량과 `ic_rank_caret_increase` 아이콘이 표시된다.
- `showRankChange == true && rankChange < 0`이면 하락 변화량과 `ic_rank_caret_decrease` 아이콘이 표시된다.
- `showRankChange == true && rankChange == 0`이면 숫자 `0`은 표시되지 않고 `ic_rank_caret_stay` 아이콘이 표시된다.
- 신규 진입 item의 `rankChange == nil`은 오류로 처리하지 않는다.
- `items`가 빈 배열이거나 API 호출이 실패하면 `순위 집계 중입니다.\n잠시 후 다시 시도해 주세요.` 문구가 표시된다.
- Figma 전체 화면 3번째 줄의 Capsule Tab bar가 화면에 배치되지 않는다.
- 랭킹 페이지 자체는 로그인 guard 없이 노출된다.
- item 탭 시 로그인 guard를 거친 뒤 해당 `creatorId`의 크리에이터 상세 화면으로 이동한다.
- `MainHomeView`는 탭 shell 역할만 유지하고 랭킹 탭 세부 구현은 `Ranking` 하위로 분리된다.
## 11. Open Questions
해당 없음.
## 12. Verification Notes
- `docs/agent-guides/documentation-policy.md`를 확인해 신규 PRD 경로와 필수 섹션 기준을 검증했다.
- `docs/prd/sample-prd.md``docs/20260602_메인_홈_추천_UI_API_연동/prd.md`를 확인해 저장소의 PRD 작성 스타일을 맞췄다.
- `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`가 현재 placeholder임을 확인했다.
- `SodaLive/Sources/V2/Main/Home/MainHomeView.swift``MainHomeRankingView`를 탭 콘텐츠로 조합하고 있음을 확인했다.
- Figma `node-id=24-5654`, `24-5659`, `24-5661`, `24-5667`, `24-5670`의 design context와 screenshot을 확인해 랭킹 화면 및 item 형태를 검증했다.
- 사용자 추가 결정 사항을 반영해 최대 item 개수, empty state 문구, API 실패 표시 방식, 로그인 guard 범위, 신규 진입 표시 정책을 확정 요구사항으로 이동했다.
- Figma `node-id=24-5674`, `24-5675`, `24-5678`의 design context와 screenshot을 확인해 상승/하락/0 변화 badge 기준을 검증했다.
- 순위 변화 아이콘 리소스명 `ic_rank_caret_decrease`, `ic_rank_caret_increase`, `ic_rank_caret_stay`, `ic_rank_new`를 요구사항에 반영했다.
- 1~10위 카드 컴포넌트 재사용, 11위 이상 row 분리, 루트 레이아웃의 고정 숫자 width/height 금지 제약을 반영했다.