# 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` 디코딩을 우선한다. #### 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 금지 제약을 반영했다.