Files

403 lines
29 KiB
Markdown

# 메인 홈 랭킹 탭 구현 계획
## 기준 문서
- PRD: `docs/20260630_메인_홈_랭킹_탭/prd.md`
- 코드 스타일: `docs/agent-guides/code-style.md`
- 빌드/검증: `docs/agent-guides/build-test-verification.md`
- Figma 전체 화면: `24:5654`
- Figma 1위 카드: `24:5659`
- Figma 2~7위 카드: `24:5661`
- Figma 8~10위 카드: `24:5667`
- Figma 11위 이상 row: `24:5670`
- Figma 상승 badge: `24:5674`
- Figma 하락 badge: `24:5675`
- Figma 변동 없음 badge 참고: `24:5678`
## 구현 원칙
- `MainHomeView`는 홈 상단 title bar, `추천`/`랭킹`/`팔로잉` Text tab shell만 담당한다.
- 랭킹 탭의 API, 모델, ViewModel, 세부 UI는 `SodaLive/Sources/V2/Main/Home/Ranking/**` 아래에 둔다.
- 기존 추천 탭 API/ViewModel/Moya 패턴을 따르고, 기존 구 홈 API/랭킹 구현은 수정하지 않는다.
- Capsule Tab bar와 랭킹 필터는 구현하지 않는다.
- 서버가 내려준 `items[].rank`를 화면 순위로 사용하고 클라이언트에서 index 기반 순위를 재계산하지 않는다.
- item은 최대 20개로 취급한다.
- 1~10위 카드는 모양이 동일하므로 `MainHomeRankingCard` 하나를 재사용하고, 1열/2열/3열 차이는 부모 grid/layout에서 처리한다.
- 11위 이상은 row 방식으로 모양이 다르므로 `MainHomeRankingRow`로 분리한다.
- 카드/row 컴포넌트의 루트 레이아웃은 고정 숫자 width/height로 제한하지 않고, grid column, parent width, `aspectRatio`, relative sizing 기반으로 화면 크기에 대응한다.
- 랭킹 페이지 노출 자체는 로그인 guard 없이 허용하고, item 탭으로 상세 진입할 때만 기존 creator 상세 guard를 재사용한다.
- 외부 라이브러리는 추가하지 않는다.
## 주요 대상 파일
### 생성
- `SodaLive/Sources/V2/Main/Home/Ranking/Models/MainHomeCreatorRankingResponse.swift`
- `SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingApi.swift`
- `SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingRepository.swift`
- `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift`
- `SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankChangeBadge.swift`
- `SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingCard.swift`
- `SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingRow.swift`
- `SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingEmptyStateView.swift`
### 수정
- `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`
- `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
- `SodaLive/Sources/I18n/I18n.swift`
- 필요 시 `SodaLive.xcodeproj/project.pbxproj`
### 확인
- `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationApi.swift`
- `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationRepository.swift`
- `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift`
- `SodaLive/Sources/V2/Main/MainView.swift`
- `SodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_decrease.imageset`
- `SodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_increase.imageset`
- `SodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_stay.imageset`
- `SodaLive/Resources/Assets.xcassets/v2/ic_rank_new.imageset`
## TASK 체크리스트
### Phase 1: 기준점 점검
- [x] **Task 1.1: PRD와 기존 랭킹 placeholder 확인**
- 대상 파일:
- 확인: `docs/20260630_메인_홈_랭킹_탭/prd.md`
- 확인: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`
- 확인: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
- 작업 내용:
- PRD 성공 기준과 제외 범위를 다시 읽고 구현 범위를 확정한다.
- `MainHomeRankingView`가 placeholder인지 확인한다.
- `MainHomeView`가 랭킹 탭에 `onTapCreator`를 아직 전달하지 않는지 확인한다.
- 검증 기준:
- 실행 명령: `sed -n '1,140p' SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift && sed -n '1,120p' SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
- 기대 결과: 랭킹 탭은 placeholder이고, `MainHomeView`는 탭 shell 역할만 담당한다.
- [x] **Task 1.2: 순위 변화 아이콘 asset 확인**
- 대상 파일:
- 확인: `SodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_decrease.imageset`
- 확인: `SodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_increase.imageset`
- 확인: `SodaLive/Resources/Assets.xcassets/v2/ic_rank_caret_stay.imageset`
- 확인: `SodaLive/Resources/Assets.xcassets/v2/ic_rank_new.imageset`
- 작업 내용:
- PRD에서 지정한 순위 변화 아이콘 asset이 존재하는지 확인한다.
- asset이 없으면 구현을 진행하지 말고 PRD/사용자 확인을 먼저 받는다.
- 검증 기준:
- 실행 명령: `find SodaLive/Resources/Assets.xcassets/v2 -maxdepth 2 -name 'ic_rank_*' | sort`
- 기대 결과: `ic_rank_caret_decrease`, `ic_rank_caret_increase`, `ic_rank_caret_stay`, `ic_rank_new` imageset이 모두 확인된다.
### Phase 2: API와 응답 모델
- [x] **Task 2.1: 랭킹 응답 모델 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/Main/Home/Ranking/Models/MainHomeCreatorRankingResponse.swift`
- 작업 내용:
- `MainHomeCreatorRankingResponse``MainHomeCreatorRankingItem`을 PRD 필드명 그대로 만든다.
- `profileImageUrl`은 optional로 둔다.
- `MainHomeCreatorRankingItem``Identifiable`을 채택하고 `id``creatorId`를 사용한다.
- 검증 기준:
- 실행 명령: `rg "struct MainHomeCreatorRankingResponse|struct MainHomeCreatorRankingItem|showRankChange|rankChange|profileImageUrl|var id" SodaLive/Sources/V2/Main/Home/Ranking/Models/MainHomeCreatorRankingResponse.swift`
- 기대 결과: PRD의 응답 필드와 `Identifiable` id 기준이 확인된다.
- [x] **Task 2.2: 랭킹 API endpoint 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingApi.swift`
- 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationApi.swift`
- 작업 내용:
- `MainHomeRankingApi``TargetType`으로 만든다.
- case는 `getCreatorRankings` 하나만 둔다.
- path는 `/api/v2/home/rankings/creators`, method는 `.get`, task는 `.requestPlain`로 둔다.
- headers는 기존 V2 홈 추천 API와 동일하게 `Authorization: Bearer {token}` 패턴을 사용한다.
- 검증 기준:
- 실행 명령: `rg "/api/v2/home/rankings/creators|getCreatorRankings|Authorization|requestPlain" SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingApi.swift`
- 기대 결과: 신규 endpoint와 인증 헤더가 랭킹 전용 API에만 존재한다.
- [x] **Task 2.3: 랭킹 Repository 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingRepository.swift`
- 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationRepository.swift`
- 작업 내용:
- `MainHomeRankingRepository`를 만들고 `MoyaProvider<MainHomeRankingApi>`를 사용한다.
- `getCreatorRankings() -> AnyPublisher<Response, MoyaError>` 메서드를 제공한다.
- 검증 기준:
- 실행 명령: `rg "final class MainHomeRankingRepository|MoyaProvider<MainHomeRankingApi>|getCreatorRankings\\(\\)|AnyPublisher<Response, MoyaError>" SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingRepository.swift`
- 기대 결과: 추천 탭과 같은 Repository 패턴이 확인된다.
### Phase 3: ViewModel 상태와 데이터 로딩
- [x] **Task 3.1: 랭킹 ViewModel 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift`
- 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift`
- 작업 내용:
- `MainHomeRankingViewModel``ObservableObject`로 만든다.
- `isLoading`, `items`, `showRankChange`, `emptyMessage`, `hasLoaded` 상태를 둔다.
- `fetchRankings()`에서 `ApiResponse<MainHomeCreatorRankingResponse>`를 디코딩한다.
- 성공 시 `items`는 서버 순서를 유지하되 최대 20개만 보관한다.
- 빈 배열 또는 API 실패/디코딩 실패 시 `emptyMessage``I18n.HomeRanking.emptyStateMessage`를 설정한다.
- 실패 시 toast 대신 empty state를 표시한다.
- 검증 기준:
- 실행 명령: `rg "final class MainHomeRankingViewModel|fetchRankings|ApiResponse<MainHomeCreatorRankingResponse>|prefix\\(20\\)|emptyStateMessage|hasLoaded" SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift`
- 기대 결과: 최대 20개 처리, empty state 처리, API 디코딩 로직이 확인된다.
- [x] **Task 3.2: 랭킹 empty state I18n 추가**
- 대상 파일:
- 수정: `SodaLive/Sources/I18n/I18n.swift`
- 작업 내용:
- `I18n.HomeRanking.emptyStateMessage`를 추가한다.
- ko는 `순위 집계 중입니다.\n잠시 후 다시 시도해 주세요.`로 둔다.
- en/ja는 기능 검증을 막지 않는 자연스러운 대응 문구로 추가한다.
- 검증 기준:
- 실행 명령: `rg "enum HomeRanking|emptyStateMessage|순위 집계 중입니다|잠시 후 다시 시도해 주세요" SodaLive/Sources/I18n/I18n.swift`
- 기대 결과: 랭킹 empty state 문구가 I18n에 존재한다.
### Phase 4: 순위 변화 UI 컴포넌트
- [x] **Task 4.1: 순위 변화 badge 컴포넌트 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankChangeBadge.swift`
- 작업 내용:
- 입력값은 `showRankChange: Bool`, `rankChange: Int?`, `isNew: Bool`로 둔다.
- `showRankChange == false`이면 아무것도 표시하지 않는다.
- `isNew == true`이면 `Image("ic_rank_new")`만 표시한다.
- `rankChange > 0`이면 절댓값 숫자와 `Image("ic_rank_caret_increase")`를 표시한다.
- `rankChange < 0`이면 절댓값 숫자와 `Image("ic_rank_caret_decrease")`를 표시한다.
- `rankChange == 0`이면 숫자 없이 `Image("ic_rank_caret_stay")`만 표시한다.
- `rankChange == nil && isNew == false`이면 아무것도 표시하지 않는다.
- Figma 기준 rank 변화 badge는 컴포넌트 내부에서 `Color.gray900`(`#202020`) 배경, horizontal padding 4, corner radius 4를 적용한다.
- Preview에는 상승, 하락, 변동 없음, 신규 진입, `showRankChange == false`, `rankChange == nil && isNew == false` 상태를 모두 배치해 한 화면에서 UI 차이를 확인할 수 있게 한다.
- 검증 기준:
- 실행 명령: `rg "struct MainHomeRankChangeBadge|ic_rank_new|ic_rank_caret_increase|ic_rank_caret_decrease|ic_rank_caret_stay|abs\\(|Color.gray900|cornerRadius\\(4\\)|padding\\(\\.horizontal, 4\\)|PreviewProvider|showRankChange: false|rankChange: nil|isNew: true|rankChange: 0" SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankChangeBadge.swift`
- 기대 결과: PRD의 순위 변화 표시 규칙, Figma badge 배경/라운드/패딩, 아이콘명, 조건별 Preview가 모두 확인된다.
### Phase 5: 순위별 랭킹 UI 컴포넌트
- [x] **Task 5.1: 1~10위 공용 카드 컴포넌트 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingCard.swift`
- 확인: `SodaLive/Sources/V2/Component/Creator/CreatorProfileItem.swift`
- 확인: `SodaLive/Sources/Home/HomeCreatorRankingItemView.swift`
- 작업 내용:
- `rank >= 1 && rank <= 10` 카드형 item을 모두 담당하는 공용 컴포넌트로 만든다.
- Figma 기준으로 1위, 2~7위, 8~10위 카드는 크기만 다르고 모양은 동일하므로 별도 카드 파일을 만들지 않는다.
- 1열/2열/3열 배치와 카드 크기는 부모 `MainHomeRankingView`의 grid/layout이 결정한다.
- `DownsampledKFImage``profileImageUrl`을 표시한다.
- 카드 하단에 dim gradient를 적용하고 nickname을 한 줄 말줄임으로 표시한다.
- rank 숫자는 `item.rank`를 사용한다.
- 순위 변화 표시는 `MainHomeRankChangeBadge`를 사용한다.
- 카드 tap 시 `onTapCreator(item.creatorId)`를 호출한다.
- 루트 container에 고정 숫자 `frame(width:)`, `frame(height:)`를 사용하지 않는다.
- 전체 카드 비율은 부모 width 기반 `aspectRatio` 또는 container-relative 제약으로 잡는다.
- Preview에는 1위 크기, 2열 크기, 3열 크기 예시를 함께 배치한다.
- Preview에는 상승, 하락, 변동 없음, 신규 진입, 순위 변화 숨김 상태를 모두 배치해 한 화면에서 UI 차이를 확인할 수 있게 한다.
- 검증 기준:
- 실행 명령: `rg "struct MainHomeRankingCard|DownsampledKFImage|LinearGradient|item.rank|MainHomeRankChangeBadge|onTapCreator\\(item.creatorId\\)|aspectRatio|PreviewProvider|rankChange: 5|rankChange: -|rankChange: 0|isNew: true|showRankChange: false" SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingCard.swift`
- 기대 결과: 1~10위 카드가 하나의 공용 컴포넌트로 구현되고, 서버 rank와 공용 badge를 사용하며, 전체 크기는 부모 layout에 의해 결정되고 조건별 Preview가 확인된다.
- [x] **Task 5.2: 11위 이상 row 컴포넌트 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingRow.swift`
- 작업 내용:
- `rank >= 11` item을 row 방식으로 표시한다.
- rank 숫자, profile image, nickname, 순위 변화 badge를 Figma row 구조에 맞춰 배치한다.
- rank 숫자는 `item.rank`를 사용한다.
- 순위 변화 표시는 `MainHomeRankChangeBadge`를 사용한다.
- row tap 시 `onTapCreator(item.creatorId)`를 호출한다.
- 루트 container에 고정 숫자 `frame(width:)`, `frame(height:)`를 사용하지 않는다.
- row의 전체 높이는 고정 숫자로 박지 않고 내부 padding, image aspect ratio, parent width 기반 제약으로 구성한다.
- Preview에는 11위 이상 row의 상승, 하락, 변동 없음, 신규 진입, 순위 변화 숨김 상태를 모두 배치해 한 화면에서 UI 차이를 확인할 수 있게 한다.
- 검증 기준:
- 실행 명령: `rg "struct MainHomeRankingRow|DownsampledKFImage|item.rank|MainHomeRankChangeBadge|onTapCreator\\(item.creatorId\\)|aspectRatio|PreviewProvider|rankChange: 5|rankChange: -|rankChange: 0|isNew: true|showRankChange: false" SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingRow.swift`
- 기대 결과: row형 순위 UI가 서버 rank와 공용 badge를 사용하고, 전체 크기는 반응형 제약으로 구성되며 조건별 Preview가 확인된다.
- [x] **Task 5.3: empty state 컴포넌트 생성**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingEmptyStateView.swift`
- 작업 내용:
- `I18n.HomeRanking.emptyStateMessage`를 중앙 또는 랭킹 콘텐츠 영역 안에 표시한다.
- 배경은 검정, 텍스트는 기존 V2 empty state 관례와 맞춘다.
- 검증 기준:
- 실행 명령: `rg "struct MainHomeRankingEmptyStateView|I18n.HomeRanking.emptyStateMessage|Color.black" SodaLive/Sources/V2/Main/Home/Ranking/Components/MainHomeRankingEmptyStateView.swift`
- 기대 결과: empty/API 실패 공통 문구 표시 컴포넌트가 확인된다.
### Phase 6: 랭킹 탭 화면 조립과 상세 진입 연결
- [x] **Task 6.1: MainHomeRankingView 구현**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`
- 작업 내용:
- placeholder를 제거한다.
- `@StateObject private var viewModel = MainHomeRankingViewModel()`를 사용한다.
- 최초 `onAppear`에서 `hasLoaded == false`일 때만 `fetchRankings()`를 호출한다.
- loading, empty, content 상태를 분기한다.
- content 상태에서는 1위 단독, 2~7위 2열, 8~10위 3열, 11위 이상 row로 조립한다.
- 1~10위는 공용 `MainHomeRankingCard`, 11위 이상은 `MainHomeRankingRow`를 사용한다.
- 2열/3열 grid는 고정 숫자 card width가 아니라 `GridItem(.flexible())` 기반 column으로 구성한다.
- `MainHomeRankingView`에서 카드/row 컴포넌트의 루트 크기를 고정 숫자로 강제하지 않는다.
- Capsule Tab bar는 추가하지 않는다.
- 생성한 카드/row 컴포넌트에 `showRankChange``onTapCreator`를 전달한다.
- 검증 기준:
- 실행 명령: `rg "MainHomeRankingViewModel|fetchRankings|MainHomeRankingCard|MainHomeRankingRow|MainHomeRankingEmptyStateView|GridItem\\(\\.flexible\\(\\)\\)|onTapCreator|MainPlaceholderTabView|CapsuleTabBar" SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`
- 기대 결과: `MainPlaceholderTabView``CapsuleTabBar`는 검색되지 않고, 랭킹 ViewModel/공용 카드/row 조립과 flexible grid가 확인된다.
- [x] **Task 6.2: MainHomeView에서 creator tap callback 전달**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
- 확인: `SodaLive/Sources/V2/Main/MainView.swift`
- 작업 내용:
- 랭킹 탭에서 `MainHomeRankingView(onTapCreator: onTapCreator)`를 호출하도록 변경한다.
- `MainView`의 기존 `handleRecommendationCreatorTap` 로그인 guard 흐름을 그대로 재사용한다.
- 랭킹 페이지 노출 자체에는 별도 guard를 추가하지 않는다.
- 검증 기준:
- 실행 명령: `rg "MainHomeRankingView\\(onTapCreator: onTapCreator\\)|handleRecommendationCreatorTap|setAppStep\\(step: \\.login\\)|creatorDetail" SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift`
- 기대 결과: 랭킹 item tap이 기존 creator 상세 guard callback으로 연결된다.
### Phase 7: 프로젝트 연결과 전체 검증
- [x] **Task 7.1: Xcode 프로젝트 포함 여부 확인**
- 대상 파일:
- 확인/필요 시 수정: `SodaLive.xcodeproj/project.pbxproj`
- 확인: `SodaLive/Sources/V2/Main/Home/Ranking/**`
- 작업 내용:
- 신규 Swift 파일이 빌드 대상에 포함되는지 확인한다.
- 현재 프로젝트 구조에서 파일 시스템 동기화가 자동이 아니면 `project.pbxproj`에 필요한 file reference/build phase를 추가한다.
- 검증 기준:
- 실행 명령: `rg "MainHomeCreatorRankingResponse|MainHomeRankingApi|MainHomeRankingRepository|MainHomeRankingViewModel|MainHomeRankChangeBadge|MainHomeRankingCard|MainHomeRankingRow|MainHomeRankingEmptyStateView" SodaLive.xcodeproj/project.pbxproj SodaLive/Sources/V2/Main/Home/Ranking`
- 기대 결과: 신규 Swift 파일이 소스 경로와 필요 시 Xcode 프로젝트에서 확인된다.
- [x] **Task 7.2: 정적 검색 검증**
- 대상 파일:
- 확인: `SodaLive/Sources/V2/Main/Home/Ranking/**`
- 확인: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
- 확인: `SodaLive/Sources/I18n/I18n.swift`
- 작업 내용:
- endpoint, 아이콘명, empty state, 공용 카드/row 컴포넌트 분리, Capsule Tab bar 제외, rank index 미사용 여부를 검색으로 확인한다.
- 카드/row 컴포넌트의 루트 container가 고정 숫자 `frame(width:)`/`frame(height:)`로 제한되지 않는지는 코드 리뷰로 확인한다. 단, 아이콘/내부 이미지의 작은 고정 크기는 이 금지 대상이 아니다.
- 검증 기준:
- 실행 명령: `rg "/api/v2/home/rankings/creators|ic_rank_caret_decrease|ic_rank_caret_increase|ic_rank_caret_stay|ic_rank_new|순위 집계 중입니다|MainHomeRankingCard|MainHomeRankingRow|CapsuleTabBar|enumerated\\(\\)" SodaLive/Sources/V2/Main/Home/Ranking SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/I18n/I18n.swift`
- 기대 결과: endpoint/아이콘/문구/공용 카드/row 컴포넌트는 확인되고, 랭킹 UI에서 `CapsuleTabBar`와 rank 계산용 `enumerated()` 사용은 없어야 한다.
- [x] **Task 7.3: 빌드 검증**
- 대상 파일:
- 확인: `SodaLive.xcworkspace`
- 확인: `SodaLive.xcodeproj`
- 작업 내용:
- 공식 빌드 명령으로 컴파일 오류를 확인한다.
- 테스트 번들 타깃이 확인되지 않는 저장소 상태이므로, 우선 Debug build를 필수 검증으로 둔다.
- 검증 기준:
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- 기대 결과: build succeeded
- [x] **Task 7.4: 수동 QA 체크**
- 대상 파일:
- 확인: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`
- 확인: `SodaLive/Sources/V2/Main/Home/Ranking/Components/**`
- 작업 내용:
- 랭킹 탭 진입 시 API가 1회 호출되는지 확인한다.
- 1위, 2~7위, 8~10위, 11위 이상 배치가 Figma 기준과 맞는지 확인한다.
- 1~10위가 `MainHomeRankingCard` 하나를 재사용하고, 11위 이상만 `MainHomeRankingRow`를 사용하는지 확인한다.
- 카드/row 컴포넌트의 루트 레이아웃이 화면 폭에 따라 자연스럽게 대응하는지 확인한다.
- Phase 4/5 컴포넌트 Preview에서 상승, 하락, 변동 없음, 신규 진입, 순위 변화 숨김 상태가 한 화면에서 모두 보이는지 확인한다.
- `showRankChange == false`일 때 순위 변화 UI가 숨겨지는지 확인한다.
- `isNew == true`일 때 `ic_rank_new`가 표시되는지 확인한다.
- `rankChange > 0`, `< 0`, `== 0` 각각 increase/decrease/stay 아이콘 규칙을 확인한다.
- 빈 배열 또는 API 실패 시 empty state 문구가 표시되는지 확인한다.
- 랭킹 item tap 시 로그인 guard 이후 creator 상세로 이동하는지 확인한다.
- 검증 기준:
- 수동 확인: PRD Success Criteria 전 항목을 체크한다.
## 검증 기록
- 2026-06-30 문서 생성:
- 목적: `prd.md`를 기반으로 메인 홈 랭킹 탭 구현 전 plan-task 문서 생성
- 확인한 기준 문서:
- `docs/20260630_메인_홈_랭킹_탭/prd.md`
- `docs/agent-guides/documentation-policy.md`
- `docs/agent-guides/build-test-verification.md`
- `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md`
- 확인한 코드:
- `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`가 placeholder 상태임
- `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`가 랭킹 탭에 `MainHomeRankingView()`를 조합 중임
- `SodaLive/Sources/V2/Main/Home/Recommendation/**`의 API/Repository/ViewModel 패턴
- `SodaLive/Sources/V2/Main/MainView.swift`의 기존 creator 상세 guard callback
- `SodaLive/Resources/Assets.xcassets/v2/ic_rank_*` 아이콘 asset 존재
- 2026-06-30 문서 보완:
- 목적: 순위별 UI 검증 Task를 명확히 하고, 카드/row 루트 레이아웃의 고정 숫자 width/height 금지 제약을 반영
- 변경 내용:
- PRD에 1~10위 공용 카드와 11위 이상 row 분리 기준 추가
- PRD에 카드/row 컴포넌트 루트 레이아웃의 반응형 제약 기준 추가
- plan-task Phase 5를 `MainHomeRankingCard`, `MainHomeRankingRow` Task로 정리
- Phase 4/5 Preview에서 상승, 하락, 변동 없음, 신규 진입, 순위 변화 숨김 상태를 모두 확인하도록 검증 기준 추가
- 2026-06-30 문서 보완:
- 목적: Figma상 1~10위 카드는 크기만 다르고 모양이 동일하므로 카드 컴포넌트를 하나로 통합
- 변경 내용:
- 순위 구간별로 나뉘어 있던 카드 컴포넌트 계획을 제거
- `MainHomeRankingCard` 단일 카드 컴포넌트와 부모 grid 기반 1열/2열/3열 배치로 계획 수정
- 2026-06-30 Phase 1~3 진행:
- 목적: 랭킹 탭 API 연동을 위한 기준점 확인, 응답 모델/API/Repository/ViewModel/I18n 추가
- 확인한 기준:
- `MainHomeRankingView.swift`는 placeholder 상태 유지
- `MainHomeView.swift`는 랭킹 탭에 `onTapCreator`를 아직 전달하지 않는 Phase 1 기준 상태 유지
- `ic_rank_caret_decrease`, `ic_rank_caret_increase`, `ic_rank_caret_stay`, `ic_rank_new` asset 존재 확인
- 변경 내용:
- `MainHomeCreatorRankingResponse`, `MainHomeCreatorRankingItem` 추가
- `MainHomeRankingApi`, `MainHomeRankingRepository` 추가
- `MainHomeRankingViewModel` 추가
- `I18n.HomeRanking.emptyStateMessage` 추가
- 검증 메모:
- 현재 저장소 가이드상 테스트 번들 타깃이 확인되지 않아 신규 단위 테스트는 추가하지 않고 정적 검색 및 빌드 검증으로 확인
- 2026-06-30 Phase 4 진행:
- 목적: 랭킹 카드/row에서 재사용할 순위 변화 badge 컴포넌트 추가
- 변경 내용:
- `MainHomeRankChangeBadge` 추가
- `showRankChange`, `isNew`, `rankChange` 조건에 따라 신규/상승/하락/변동 없음/미표시 상태 분기
- Phase 4 Preview에 상승, 하락, 변동 없음, 신규 진입, 순위 변화 숨김, `rankChange == nil && isNew == false` 상태 추가
- 검증 메모:
- 문서의 Phase 4 정적 검색 기준으로 아이콘명, 조건별 Preview, `abs()` 사용을 확인
- Figma badge 배경 기준 확인 후 `Color.gray900`(`#202020`) 배경, horizontal padding 4, corner radius 4를 `MainHomeRankChangeBadge`에 반영
- 2026-06-30 Phase 5 진행:
- 목적: 랭킹 순위별 UI 조립에 사용할 카드/row/empty state 컴포넌트 추가
- 확인한 기준:
- Figma `24:5659`, `24:5670` 기준 카드/row 구조 확인
- 기존 V2 추천 탭의 `DownsampledKFImage`, `LinearGradient`, `SodaSpacing`, `Color.gray*`, `.buttonStyle(.plain)` 사용 관례 확인
- 변경 내용:
- `MainHomeRankingCard` 추가: 1~10위 공용 카드, 서버 `item.rank`, 하단 dim gradient, `MainHomeRankChangeBadge`, creator tap callback 사용
- `MainHomeRankingRow` 추가: 11위 이상 row, 서버 `item.rank`, 프로필 이미지, nickname, `MainHomeRankChangeBadge`, creator tap callback 사용
- `MainHomeRankingEmptyStateView` 추가: `I18n.HomeRanking.emptyStateMessage`를 검정 배경 위에 표시
- Phase 5 Preview에 상승, 하락, 변동 없음, 신규 진입, 순위 변화 숨김 상태 추가
- 검증 메모:
- 문서의 Phase 5 정적 검색 기준으로 카드/row/empty state 필수 심볼과 Preview 상태를 확인
- 카드/row 루트 container에 고정 숫자 `frame(width:)`, `frame(height:)`를 두지 않고, 카드 전체 비율은 `aspectRatio` 기반으로 구성
- 2026-06-30 Phase 6 진행:
- 목적: 랭킹 탭 화면을 ViewModel 상태 기반으로 조립하고, 랭킹 item tap을 기존 creator 상세 진입 callback에 연결
- 변경 내용:
- `MainHomeRankingView` placeholder 제거
- `MainHomeRankingViewModel``@StateObject`로 연결하고 최초 `onAppear`에서 `hasLoaded == false`일 때만 `fetchRankings()` 호출
- loading, empty, content 상태 분기 추가
- content 상태에서 1위 단독, 2~7위 2열, 8~10위 3열, 11위 이상 row로 조립
- 1~10위는 `MainHomeRankingCard`, 11위 이상은 `MainHomeRankingRow`를 사용하고 `showRankChange`, `onTapCreator` 전달
- `MainHomeView`의 랭킹 탭에서 `MainHomeRankingView(onTapCreator: onTapCreator)`를 호출하도록 변경
- 검증 메모:
- Phase 6 정적 검색 기준으로 ViewModel, fetch, 공용 카드/row, empty state, flexible grid, creator tap callback 연결을 확인
- `MainHomeRankingView`에서 `MainPlaceholderTabView`, `CapsuleTabBar`가 사용되지 않음을 확인
- 2026-06-30 Phase 7 진행:
- 목적: 랭킹 탭 신규 파일의 Xcode 프로젝트 포함 여부, 정적 검색 기준, 빌드 가능 여부 확인
- 확인한 기준:
- `SodaLive.xcodeproj/project.pbxproj`에서 `MainHomeCreatorRankingResponse`, `MainHomeRankingApi`, `MainHomeRankingRepository`, `MainHomeRankingViewModel`, `MainHomeRankChangeBadge`, `MainHomeRankingCard`, `MainHomeRankingRow`, `MainHomeRankingEmptyStateView``PBXFileReference``PBXBuildFile` 등록 확인
- 정적 검색으로 endpoint, 순위 변화 아이콘명, empty state 문구, 공용 카드/row 컴포넌트 사용 확인
- 정적 검색 결과 랭킹 UI 범위에서 `CapsuleTabBar`, rank 계산용 `enumerated()` 사용 없음 확인
- `MainHomeRankingCard`, `MainHomeRankingRow`, `MainHomeRankingView` 코드 리뷰로 루트 레이아웃이 `aspectRatio`, flexible grid, parent width 기반임을 확인
- 검증 메모:
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행 결과 `** BUILD SUCCEEDED **`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "id=C012DC2D-6F85-4C9F-AF22-A10621385D53" build` 실행 결과 `** BUILD SUCCEEDED **`
- 빌드 로그 검색 결과 duplicate build file warning은 없었고, AppIntents metadata 및 Alamofire/BootpayUI dependency scan warning만 확인
- booted simulator에 `SodaLive-dev.app` 설치 후 `xcrun simctl launch booted kr.co.vividnext.sodalive.debug2` 실행 결과 PID `21238`로 launch 확인
- Task 7.4의 Figma 육안 대조, 랭킹 API 1회 호출, item tap 후 상세 진입은 CLI 검증만으로 확정할 수 없어 별도 수동 QA 대상으로 남김