docs(home): 크리에이터 랭킹 계획을 추가한다

This commit is contained in:
2026-06-08 14:01:27 +09:00
parent ecc59376a3
commit 9600147240
2 changed files with 488 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
# 크리에이터 랭킹 페이지 Plan / Task
> **For agentic workers:** 구현 시 `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`를 사용해 task 단위로 진행한다. 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다.
**Goal:** `HomeMainFragment`의 Text Tab bar에서 `랭킹` 선택 시 `GET /api/v2/home/rankings/creators` 응답을 기존 `creatorranking` 위젯으로 표시한다.
**Architecture:** 신규 홈 크리에이터 랭킹 API, Repository, ViewModel, DTO/UI model/mapper는 기존 홈 추천 패턴과 같은 `kr.co.vividnext.sodalive.v2.main.home` 하위에 둔다. 화면은 `HomeMainFragment``fragment_v2_main_home.xml`을 최소 확장하고, 기존 `kr.co.vividnext.sodalive.v2.widget.creatorranking` 위젯은 `showRankChange=false` 숨김 옵션만 필요한 만큼 확장한다.
**Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, RxJava3, Retrofit, Gson, Koin, Coil, Robolectric/local unit test.
---
## 전제와 성공 기준
- PRD: `docs/20260608_크리에이터_랭킹_페이지/prd.md`
- Figma: `24:5654`
- Capsule Tab bar(`주간 인기`, `지금 뜨는 중`, `남성 인기`, `여성 인기`)는 배치하지 않는다.
- `rankChange``null`/`0` -> `Stay`, 양수 -> `Increase`, 음수 -> `Decrease`로 매핑한다.
- `isNew=true``rankChange`보다 우선해 `New`로 매핑한다.
- `showRankChange=false`이면 모든 rank-num 영역을 완전히 숨긴다.
- API 응답은 서버에서 `rank` 오름차순으로 내려오지만, 클라이언트에서도 한 번 더 `rank` 기준 오름차순 정렬한다.
- `creatorId=0`은 차단 관계로 보고 `isBlocked=true`, 클릭 불가로 처리한다.
- `creatorId>0` item만 `UserProfileActivity`로 이동하며, 별도 analytics/logging은 추가하지 않는다.
- 구현 완료 후 최소 다음 명령을 실행한다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.creatorranking.*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*"`
- `./gradlew :app:mergeDebugResources`
- `./gradlew :app:compileDebugKotlin`
- `./gradlew :app:ktlintCheck`
---
## 파일 구조
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingModels.kt`
- `HomeCreatorRankingResponse`, `HomeCreatorRankingItemResponse` DTO를 정의한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingApi.kt`
- `GET /api/v2/home/rankings/creators` Retrofit endpoint를 정의한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingRepository.kt`
- API 호출을 감싸고 기존 repository 패턴과 동일하게 token을 전달한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeCreatorRankingMappers.kt`
- API 응답을 `CreatorRankingItem` 목록으로 변환한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeCreatorRankingUiState.kt`
- `Loading`, `Content`, `Empty`, `Error` 상태를 정의한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeCreatorRankingViewModel.kt`
- 랭킹 API 호출, loading, toast, state를 관리한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
- 신규 API, Repository, ViewModel을 Koin에 등록한다.
- 수정: `app/src/main/res/layout/fragment_v2_main_home.xml`
- `TextTabBarView` 아래에 랭킹 전용 `RecyclerView`를 추가한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt`
- `랭킹` 탭 전환, 랭킹 adapter, ViewModel observe, 프로필 이동을 연결한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingItem.kt`
- `showRankChange` 표시 계약을 추가한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingLargeCardView.kt`
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingCompactCardView.kt`
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingHorizontalCardView.kt`
- `showRankChange=false`일 때 rank-num 영역을 `GONE` 처리한다.
- 테스트 수정/생성:
- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingItemTest.kt`
- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingAdapterLayoutTest.kt`
- 생성: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeCreatorRankingMapperTest.kt`
- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
---
### Phase 1: 기존 구조 확인과 작업 경계 고정
- [ ] **Task 1.1: 기존 홈/랭킹 위젯/DI 구조 확인**
- 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt`
- 확인: `app/src/main/res/layout/fragment_v2_main_home.xml`
- 확인: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
- 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeRecommendationApi.kt`
- 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeRecommendationViewModel.kt`
- 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingAdapter.kt`
- 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingItem.kt`
- 검증: 기존 추천 content는 `nsv_home_recommendation_content` 아래에 있고, 랭킹 content는 별도 container가 필요함을 확인한다.
- [ ] **Task 1.2: 구현 제외 범위 재확인**
- 확인: `docs/20260608_크리에이터_랭킹_페이지/prd.md`
- 제외:
- Capsule Tab bar
- 팔로잉 탭 content
- analytics/logging
- ViewPager2/swipe 전환
- 검증: 계획 문서의 모든 phase가 위 제외 범위를 침범하지 않는지 확인한다.
---
### Phase 2: `creatorranking` 위젯 rank-num 숨김 계약 확장
- [ ] **Task 2.1: `CreatorRankingItem` 표시 계약 테스트 추가**
- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingItemTest.kt`
- 추가 테스트:
- 기본 `showRankChange``true`
- `showRankChange=false`인 item은 rank change 표시 대상이 아님
- `creatorId=0`, `isBlocked=true` item은 `isTouchable=false`
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingItemTest"`
- 기대 결과: `showRankChange` 속성 미구현으로 RED 실패.
- [ ] **Task 2.2: `CreatorRankingItem`에 `showRankChange` 추가**
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingItem.kt`
- 구현 내용:
- `val showRankChange: Boolean = true`를 기본값 있는 마지막 파라미터로 추가한다.
- 기존 테스트/호출부가 깨지지 않도록 기본값을 유지한다.
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingItemTest"`
- 기대 결과: PASS.
- [ ] **Task 2.3: rank-num 숨김 view 테스트 추가**
- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingAdapterLayoutTest.kt`
- 추가 테스트:
- `Large`, `Compact`, `Horizontal` card에 `showRankChange=false` item을 bind하면 `ll_creator_ranking_delta``GONE`
- `showRankChange=true` item을 bind하면 기존처럼 `ll_creator_ranking_delta``VISIBLE`
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingAdapterLayoutTest"`
- 기대 결과: view bind 미구현으로 RED 실패.
- [ ] **Task 2.4: rank-num 숨김 구현**
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingLargeCardView.kt`
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingCompactCardView.kt`
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingHorizontalCardView.kt`
- 구현 내용:
- `bindDelta(item)` 시작부에서 `item.showRankChange == false`이면 delta container를 `View.GONE`으로 설정하고 return한다.
- `item.showRankChange == true`이면 delta container를 `View.VISIBLE`로 복구한 뒤 기존 presentation 적용을 유지한다.
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.creatorranking.*"`
- 기대 결과: PASS.
---
### Phase 3: 홈 크리에이터 랭킹 API/DTO/mapper 작성
- [ ] **Task 3.1: mapper RED 테스트 작성**
- 생성: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeCreatorRankingMapperTest.kt`
- 테스트 케이스:
- 응답 item을 `rank` 오름차순으로 재정렬한다.
- `isNew=true``RankingChangeType.New`, amount `0`으로 매핑한다.
- `rankChange=null` 또는 `0``RankingChangeType.Stay`, amount `0`으로 매핑한다.
- `rankChange=5``RankingChangeType.Increase`, amount `5`로 매핑한다.
- `rankChange=-3``RankingChangeType.Decrease`, amount `3`으로 매핑한다.
- `showRankChange=false`이면 모든 `CreatorRankingItem.showRankChange=false`로 매핑한다.
- `creatorId=0``isBlocked=true`, `isTouchable=false`로 매핑한다.
- `rank < 1` item은 제외한다.
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeCreatorRankingMapperTest"`
- 기대 결과: DTO/mapper 미구현으로 RED 실패.
- [ ] **Task 3.2: API DTO와 mapper 구현**
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingModels.kt`
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeCreatorRankingMappers.kt`
- 구현 내용:
- `data class HomeCreatorRankingResponse(val showRankChange: Boolean, val items: List<HomeCreatorRankingItemResponse>)`
- `data class HomeCreatorRankingItemResponse(val rank: Int, val rankChange: Int?, val isNew: Boolean, val creatorId: Long, val nickname: String, val profileImageUrl: String)`
- `fun HomeCreatorRankingResponse.toCreatorRankingItems(): List<CreatorRankingItem>`
- `rank >= 1` filtering, `rank` sorting, `RankingChangeType` mapping, `abs(rankChange)` amount mapping
- `creatorId == 0L` -> `isBlocked=true`
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeCreatorRankingMapperTest"`
- 기대 결과: PASS.
- [ ] **Task 3.3: API/Repository 작성**
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingApi.kt`
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeCreatorRankingRepository.kt`
- 구현 내용:
- `@GET("/api/v2/home/rankings/creators")`
- `fun getCreatorRankings(@Header("Authorization") authHeader: String): Single<ApiResponse<HomeCreatorRankingResponse>>`
- repository는 `getCreatorRankings(token: String)`으로 API를 위임한다.
- 검증 명령: `./gradlew :app:compileDebugKotlin`
- 기대 결과: 신규 API/Repository 컴파일 성공.
---
### Phase 4: ViewModel과 DI 등록
- [ ] **Task 4.1: UI state와 ViewModel 작성**
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeCreatorRankingUiState.kt`
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeCreatorRankingViewModel.kt`
- 구현 내용:
- `HomeCreatorRankingUiState.Loading`
- `HomeCreatorRankingUiState.Content(val items: List<CreatorRankingItem>)`
- `HomeCreatorRankingUiState.Empty`
- `HomeCreatorRankingUiState.Error(val message: String?)`
- `rankingStateLiveData`, `isLoading`, `toastLiveData`
- `loadCreatorRankings()`
- 기존 `HomeRecommendationViewModel`과 동일하게 `SharedPreferenceManager.token`, RxJava scheduler, unknown error toast 패턴 사용
- 검증 명령: `./gradlew :app:compileDebugKotlin`
- 기대 결과: ViewModel 컴파일 성공.
- [ ] **Task 4.2: Koin DI 등록**
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
- 구현 내용:
- import 추가: `HomeCreatorRankingApi`, `HomeCreatorRankingRepository`, `HomeCreatorRankingViewModel`
- `networkModule``single { ApiBuilder().build(get(), HomeCreatorRankingApi::class.java) }`
- `repositoryModule``factory { HomeCreatorRankingRepository(get()) }`
- `viewModelModule``viewModel { HomeCreatorRankingViewModel(get()) }`
- 검증 명령: `./gradlew :app:compileDebugKotlin`
- 기대 결과: Koin 등록과 import 컴파일 성공.
---
### Phase 5: 홈 레이아웃과 탭 전환 UI 연결
- [ ] **Task 5.1: 홈 레이아웃에 랭킹 목록 추가 RED 테스트 작성**
- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
- 추가 테스트:
- `fragment_v2_main_home.xml``rv_home_creator_rankings`가 존재한다.
- `rv_home_creator_rankings``TextTabBarView` 아래에 직접 constraint 된다.
- Capsule Tab bar 관련 view id가 존재하지 않는다.
- 초기 visibility는 `GONE`이다.
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home ranking layout*"`
- 기대 결과: 신규 RecyclerView 미존재로 RED 실패.
- [ ] **Task 5.2: `fragment_v2_main_home.xml`에 랭킹 RecyclerView 추가**
- 수정: `app/src/main/res/layout/fragment_v2_main_home.xml`
- 구현 내용:
- `androidx.recyclerview.widget.RecyclerView` 추가
- id: `@+id/rv_home_creator_rankings`
- width/height: `0dp`
- top: `@id/text_tab_bar_home` bottom
- bottom/start/end: parent
- `android:visibility="gone"`
- `android:clipToPadding="false"`
- `android:paddingHorizontal="@dimen/spacing_14"`
- `android:paddingTop="@dimen/spacing_14"`
- `android:paddingBottom="@dimen/spacing_28"`
- 제외: Capsule Tab bar view 추가 금지.
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home ranking layout*"``./gradlew :app:mergeDebugResources`
- 기대 결과: PASS.
- [ ] **Task 5.3: `HomeMainFragment` 탭 전환/adapter 연결 테스트 작성**
- 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt`
- 추가 테스트:
- `CreatorRankingAdapter.createGridLayoutManager()`가 랭킹 RecyclerView에 사용되는 소스 계약
- `랭킹` index 선택 시 추천 content는 `GONE`, 랭킹 RecyclerView는 `VISIBLE`
- `추천` index 선택 시 추천 content는 `VISIBLE`, 랭킹 RecyclerView는 `GONE`
- `creatorId=0` item은 프로필 이동 intent를 만들지 않는다.
- `creatorId>0` item은 `UserProfileActivity` + `Constants.EXTRA_USER_ID` intent를 만든다.
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home ranking*"`
- 기대 결과: Fragment 연결 미구현으로 RED 실패.
- [ ] **Task 5.4: `HomeMainFragment`에 랭킹 탭 content 연결**
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt`
- 구현 내용:
- `private val homeCreatorRankingViewModel: HomeCreatorRankingViewModel by viewModel()`
- `private val creatorRankingAdapter = CreatorRankingAdapter { openCreatorRankingProfile(it) }`
- `rvHomeCreatorRankings.layoutManager = CreatorRankingAdapter.createGridLayoutManager(requireContext())`
- `rvHomeCreatorRankings.adapter = creatorRankingAdapter`
- tab index 상수: `HOME_TAB_RECOMMENDATION = 0`, `HOME_TAB_RANKING = 1`, `HOME_TAB_FOLLOWING = 2`
- `setOnTabSelectedListener`에서 추천/랭킹만 content 전환 처리한다.
- 랭킹 최초 선택 시 `homeCreatorRankingViewModel.loadCreatorRankings()`를 호출한다.
- `HomeCreatorRankingUiState.Content`이면 `creatorRankingAdapter.submitItems(items)`
- `Empty`/`Error`이면 빈 목록을 submit한다.
- `openCreatorRankingProfile(item)``item.creatorId > 0`일 때만 `UserProfileActivity``Constants.EXTRA_USER_ID`와 함께 실행한다.
- 별도 analytics/logging은 추가하지 않는다.
- 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home ranking*"``./gradlew :app:compileDebugKotlin`
- 기대 결과: PASS.
---
### Phase 6: 통합 검증과 문서 기록
- [ ] **Task 6.1: targeted test 실행**
- 실행:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.creatorranking.*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeCreatorRankingMapperTest"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`
- 기대 결과: 모두 `BUILD SUCCESSFUL`.
- 실패 시: 실패 원인을 이 문서의 Verification Log에 누적하고, 수정 전 관련 task 체크박스를 되돌린다.
- [ ] **Task 6.2: compile/resource/lint 검증**
- 실행:
- `./gradlew :app:mergeDebugResources`
- `./gradlew :app:compileDebugKotlin`
- `./gradlew :app:ktlintCheck`
- 기대 결과: 모두 `BUILD SUCCESSFUL`.
- 참고: 기존 `.editorconfig disabled_rules` deprecation warning은 신규 실패가 아니면 별도 수정하지 않는다.
- [ ] **Task 6.3: 최종 문서 검증 기록 누적**
- 수정: `docs/20260608_크리에이터_랭킹_페이지/plan-task.md`
- 수정: `docs/20260608_크리에이터_랭킹_페이지/prd.md`
- 구현 내용:
- 실행한 명령, 성공/실패 결과, 실패 시 원인과 보완 내용을 Verification Log에 누적한다.
- 기존 Verification Log는 삭제하거나 덮어쓰지 않는다.
- 검증: PRD와 plan-task의 완료 상태와 실제 구현 상태가 일치한다.
---
## Verification Log
- 2026-06-08: `superpowers:writing-plans` 지침, PRD `docs/20260608_크리에이터_랭킹_페이지/prd.md`, 기존 홈 추천 계획 문서, `fragment_v2_main_home.xml`, `HomeMainFragment`, `AppDI`, 기존 홈 추천 API/ViewModel, `creatorranking` 위젯 구조와 테스트 위치를 확인했다.
- 2026-06-08: 이번 단계는 계획 문서 작성만 수행했으며 구현/빌드/테스트는 실행하지 않았다.

View File

@@ -0,0 +1,204 @@
# PRD: 크리에이터 랭킹 페이지
## 1. Overview
`HomeMainFragment`의 Text Tab bar에서 `랭킹`을 선택했을 때 `GET /api/v2/home/rankings/creators` 응답을 기존 `kr.co.vividnext.sodalive.v2.widget.creatorranking` 위젯으로 표시하는 홈 랭킹 페이지를 구현한다.
---
## 2. Problem
- 홈 추천 화면에는 `추천`, `랭킹`, `팔로잉` Text Tab bar가 이미 배치되어 있지만, `랭킹` 선택 시 표시할 실제 페이지가 없다.
- Figma `24:5654`에는 홈 랭킹 화면과 크리에이터 랭킹 목록이 정의되어 있으나, 3번째 줄의 Capsule Tab bar는 이번 요구사항에서 제외해야 한다.
- 기존 크리에이터 랭킹 위젯은 `previousRank`, `rankChangeType`, `rankChangeAmount`, `creatorName`, `imageUrl`, `isBlocked` 계약을 사용한다.
- 신규 API 응답은 `showRankChange`, `items[].rankChange`, `items[].isNew`, `items[].nickname`, `items[].profileImageUrl` 형태이므로 기존 위젯 계약으로 변환하는 mapper가 필요하다.
---
## 3. Goals
- `HomeMainFragment`에서 Text Tab bar의 `랭킹` 선택 시 크리에이터 랭킹 페이지를 표시한다.
- API `GET /api/v2/home/rankings/creators`를 호출해 응답 item을 기존 `CreatorRankingAdapter`/`CreatorRankingItem` 기반 UI로 표시한다.
- Figma `24:5654` 기준 상단 TitleBar와 Text Tab bar 아래에 랭킹 목록을 배치한다.
- Figma에 존재하더라도 3번째 줄 Capsule Tab bar(`주간 인기`, `지금 뜨는 중`, `남성 인기`, `여성 인기`)는 배치하지 않는다.
- 기존 `kr.co.vividnext.sodalive.v2.widget.creatorranking` 위젯을 우선 재사용하고, API 응답과 Figma 정합에 필요한 최소 변경만 수행한다.
- 랭킹 item 터치 시 접근 가능한 크리에이터의 프로필 화면으로 이동할 수 있도록 한다.
---
## 4. Non-Goals
- Capsule Tab bar 및 기간/성별/인기 유형 필터는 구현하지 않는다.
- `팔로잉` 탭 페이지는 구현하지 않는다.
- 크리에이터 랭킹 위젯의 순위별 카드 variant 정책 자체를 새로 설계하지 않는다.
- Figma에 없는 skeleton, shimmer, pagination, pull-to-refresh, 추가 배너, 광고 영역은 구현하지 않는다.
- 서버 API 스키마를 클라이언트에서 임의로 변경하지 않는다.
- Compose 전환, ViewPager2 기반 tab swipe 전환, tab별 신규 Fragment 대량 분리는 이번 범위에 포함하지 않는다.
---
## 5. Target Users
- 홈에서 인기 크리에이터 순위를 확인하려는 앱 사용자.
- `HomeMainFragment`와 v2 위젯 기반 홈 화면을 유지보수하는 Android 개발자.
---
## 6. User Stories
- 사용자는 홈의 `랭킹` 탭을 눌러 크리에이터 순위를 바로 보고 싶다.
- 사용자는 1위, 2~10위, 11위 이후 순위가 시각적으로 구분된 카드로 표시되길 기대한다.
- 사용자는 신규 진입 또는 순위 변동 정보를 랭킹 카드에서 확인하고 싶다.
- 사용자는 랭킹 item을 눌러 해당 크리에이터 프로필로 이동하고 싶다.
- 개발자는 API 응답을 기존 랭킹 위젯 계약으로 명확히 변환해 재사용하고 싶다.
---
## 7. Core Features
### Home Ranking Tab Page
`HomeMainFragment`의 Text Tab bar에서 `랭킹` 선택 상태일 때 크리에이터 랭킹 목록을 표시한다.
#### Requirements
- Text Tab bar 항목은 기존과 동일하게 `추천`, `랭킹`, `팔로잉` 순서를 유지한다.
- `랭킹` 선택 시 Text Tab bar selected index가 `랭킹`으로 갱신되어야 한다.
- TitleBar와 Text Tab bar는 기존 홈 추천 화면과 동일하게 상단에 유지한다.
- Text Tab bar 아래 content 영역만 랭킹 페이지 content로 교체한다.
- 랭킹 페이지 content는 세로 스크롤 가능한 목록으로 구성한다.
- Figma `24:5654`의 3번째 줄 Capsule Tab bar는 배치하지 않으므로, 랭킹 목록은 Text Tab bar 아래에서 바로 시작한다.
- 추천 탭으로 다시 전환하면 기존 추천 페이지 content가 표시되어야 한다.
#### Edge Cases
- API loading 중에는 기존 홈 추천 API loading 처리와 일관된 정책을 따른다.
- API error 또는 empty 응답이면 랭킹 목록 영역을 비우거나 기존 홈 empty/error 정책에 맞춰 처리한다.
- `items`가 빈 배열이면 `CreatorRankingAdapter`에는 빈 목록을 전달하고, 불필요한 placeholder UI는 추가하지 않는다.
### Creator Ranking API Integration
홈 랭킹 페이지에서 `GET /api/v2/home/rankings/creators`를 호출하고 응답을 UI model로 변환한다.
#### API Endpoint
- `GET /api/v2/home/rankings/creators`
#### Response Contract
```json
{
"showRankChange": true,
"items": [
{
"rank": 1,
"rankChange": 5,
"isNew": false,
"creatorId": 123,
"nickname": "creator",
"profileImageUrl": "https://cdn.example.com/profile.png"
}
]
}
```
#### Mapping Requirements
| Response field | UI/widget field | Requirement |
| --- | --- | --- |
| `items[].creatorId` | `CreatorRankingItem.creatorId` | 그대로 전달한다. |
| `items[].rank` | `CreatorRankingItem.rank` | `1` 이상인 item만 표시한다. |
| `items[].nickname` | `CreatorRankingItem.creatorName` | 그대로 전달한다. |
| `items[].profileImageUrl` | `CreatorRankingItem.imageUrl` | 그대로 전달한다. |
| `items[].isNew` | `CreatorRankingItem.rankChangeType` | `true`이면 `RankingChangeType.New`로 매핑한다. |
| `items[].rankChange` | `CreatorRankingItem.rankChangeType`, `CreatorRankingItem.rankChangeAmount` | `null` 또는 `0`이면 `Stay`, 양수이면 `Increase`, 음수이면 `Decrease`로 매핑하고 표시 숫자는 절대값을 사용한다. |
| `showRankChange` | 순위 변동 표시 여부 | `false`이면 모든 item의 rank-num 영역을 완전히 숨긴다. |
#### Rank Change Rules
- `isNew=true`이면 `rankChange` 값과 무관하게 `RankingChangeType.New`로 표시한다.
- `isNew=false && (rankChange=null || rankChange=0)`이면 `RankingChangeType.Stay`로 표시한다.
- `isNew=false && rankChange > 0`이면 `RankingChangeType.Increase`로 표시한다.
- `isNew=false && rankChange < 0`이면 `RankingChangeType.Decrease`로 표시한다.
- `Increase``Decrease`의 표시 숫자는 `abs(rankChange)`를 사용한다.
- `showRankChange=false`이면 `isNew`, `rankChange` 값과 무관하게 rank-num 영역을 완전히 숨긴다.
- API 응답 item은 서버에서 항상 `rank` 오름차순으로 내려오지만, 클라이언트에서도 한 번 더 `rank` 기준 오름차순 정렬 후 표시한다.
#### Click Requirements
- `creatorId > 0`인 item만 클릭 이벤트가 동작한다.
- `creatorId > 0`인 item을 터치하면 `UserProfileActivity`로 이동하고 `Constants.EXTRA_USER_ID``creatorId`를 전달한다.
- `creatorId = 0`인 item은 차단 관계인 크리에이터로 간주하며 클릭 이벤트가 동작하지 않는다.
- 랭킹 item 터치 시 크리에이터 프로필 이동 외 별도 analytics/logging은 추가하지 않는다.
### Creator Ranking Widget Reuse
기존 `kr.co.vividnext.sodalive.v2.widget.creatorranking` 컴포넌트를 랭킹 페이지 목록에 사용한다.
#### Existing Widget Fit
- Figma `24:5654`의 카드 구조는 기존 크리에이터 랭킹 위젯 PRD의 순위 구간과 일치한다.
- 1위: `Large`
- 2위~7위: 2열 `Compact`
- 8위~10위: 3열 `Compact`
- 11위 이후: `Horizontal`
- 이미지 radius, dim gradient, Pattaya 순위 숫자, rank-num, New badge는 기존 위젯 계약을 따른다.
- API 응답에서 차단 관계인 크리에이터는 `creatorId=0`으로 내려오므로 `creatorId=0` item은 `isBlocked=true`, `creatorId>0` item은 `isBlocked=false`로 매핑한다.
#### Required Change Candidates
- `showRankChange=false`일 때 rank-num 영역을 완전히 숨기는 옵션이 기존 위젯에 없으면 최소 확장이 필요하다.
- 기존 `CreatorRankingItem.rankChangeAmount`는 non-null `Int`이므로 `rankChange=null` 응답을 `Stay``0`으로 안전하게 변환해야 한다.
- 기존 위젯은 `creatorName`/`imageUrl` 이름을 사용하므로 API DTO와 UI model mapper를 분리한다.
- 기존 Adapter의 span size는 position 기반으로 계산하므로 서버 정렬 보장과 별개로 mapper 또는 UI state에서 `rank` 기준으로 한 번 더 정렬한다.
- `creatorId=0` item은 기존 위젯의 차단 상태 UI를 적용하고, 클릭 listener가 동작하지 않도록 보장한다.
#### Edge Cases
- `rank < 1` item은 기존 `CreatorRankingItem` 생성 조건에 맞지 않으므로 표시하지 않는다.
- `nickname`이 빈 값이면 빈 문자열 그대로 표시하되, 별도 대체 문구는 추가하지 않는다.
- `profileImageUrl`이 빈 값이거나 이미지 로딩에 실패하면 기존 `loadUrl`/이미지 로딩 정책을 따른다.
- `creatorId=0`인 차단 관계 item은 상세 이동을 막고, 기존 차단 상태 위젯 정책에 따라 이미지 블러와 이름 비노출/대체문구를 적용한다.
- 동일 rank가 중복되면 서버 데이터 오류로 보고 클라이언트는 받은 순서 또는 `rank` 정렬 결과를 그대로 표시한다. 중복 보정 UI는 추가하지 않는다.
---
## 8. UX / UI Expectations
- 전체 배경은 기존 홈과 동일한 black 계열을 유지한다.
- TitleBar와 Text Tab bar는 Figma `24:5654` 및 기존 홈 추천 구현과 동일한 위치/스타일을 유지한다.
- Capsule Tab bar를 제거한 상태에서도 Text Tab bar 아래 여백이 과도하게 남지 않아야 한다.
- 랭킹 목록 좌우 margin과 item gap은 기존 `CreatorRankingAdapter`와 Figma 목록 폭을 기준으로 맞춘다.
- `랭킹` 탭 선택 상태가 흰색 텍스트로 명확히 드러나야 한다.
- 1위~20위까지 응답이 내려오면 Figma 예시처럼 20위까지 자연스럽게 스크롤로 확인할 수 있어야 한다.
- 긴 닉네임은 기존 위젯의 ellipsize/line 제한 정책을 따른다.
---
## 9. Technical Constraints
- Android XML Views, ViewBinding, RecyclerView 기반 기존 구조를 유지한다.
- 신규 API DTO, Repository, ViewModel, mapper, UI model은 기존 홈 추천 패턴을 확인해 `kr.co.vividnext.sodalive.v2.main.home` 하위에 둔다.
- 신규 `Activity`, `Fragment`, `ViewModel` 및 연결 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
- 화면은 우선 `HomeMainFragment``fragment_v2_main_home.xml`의 기존 상단 구조를 확장해 구현한다.
- 기존 `CreatorRankingAdapter.createGridLayoutManager()`를 사용해 span 정책을 유지한다.
- 크리에이터 프로필 이동은 `creatorId > 0`일 때만 기존 `UserProfileActivity``Constants.EXTRA_USER_ID` 사용 패턴을 재사용한다.
- 네트워크/API 등록은 기존 `AppDI.kt`의 홈 추천 API/Repository/ViewModel 등록 패턴을 따른다.
- 구현 전 `docs/20260608_크리에이터_랭킹_페이지/plan-task.md`를 작성한 뒤 해당 계획에 따라 최소 구현한다.
---
## 10. Metrics
- `랭킹` 탭 선택 시 `GET /api/v2/home/rankings/creators` 응답 item이 `CreatorRankingAdapter`에 전달된다.
- Figma `24:5654`의 Capsule Tab bar는 화면에 존재하지 않는다.
- 1위, 2~7위, 8~10위, 11위 이후 variant가 기존 위젯 정책과 일치한다.
- `isNew=true` item은 New badge로 표시된다.
- `isNew=false && (rankChange=null || rankChange=0)` item은 유지 상태로 표시된다.
- `rankChange > 0` item은 상승, `rankChange < 0` item은 하락으로 표시된다.
- `showRankChange=false` 응답에서는 rank-num 영역이 완전히 숨겨진다.
- `rank < 1` item은 표시되지 않는다.
- `creatorId=0` item은 클릭되지 않고, `creatorId>0` item만 `UserProfileActivity`로 이동한다.
- API 응답은 클라이언트에서 `rank` 기준 오름차순으로 한 번 더 정렬된다.
- 빈 응답, API 실패, 이미지 실패가 crash 없이 처리된다.
- 관련 mapper/unit test, `HomeMainFragment` layout test, `compileDebugKotlin`, `ktlintCheck`가 성공한다.
---
## 11. Open Questions
- 없음.
---
## 12. References
- Figma: 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-5654&m=dev
- 기존 크리에이터 랭킹 위젯 PRD: `docs/prd/20260520_크리에이터랭킹위젯컴포넌트_prd.md`
- 기존 홈 추천 PRD: `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md`
- 기존 위젯 패키지: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking`
---
## 13. Verification Log
- 2026-06-08: `docs/prd/sample-prd.md`, `docs/agent-guides/work-plan-docs.md`, 기존 크리에이터 랭킹 위젯 PRD, `CreatorRankingItem`, `CreatorRankingAdapter`, `CreatorRankingDeltaPresentation`, `RankingChangeType`, `HomeMainFragment`를 확인했다.
- 2026-06-08: Figma `24:5654`의 design context와 screenshot을 확인했다. 화면에는 Capsule Tab bar가 포함되어 있으나 사용자 요구에 따라 PRD 범위에서 제외했다.
- 2026-06-08: 이번 단계는 PRD 작성만 수행했으며 구현/빌드/테스트는 실행하지 않았다.
- 2026-06-08: 사용자 추가 제공 정보에 따라 `rankChange`의 양수/음수/0 매핑, `showRankChange=false` 시 rank-num 완전 숨김, 클라이언트 `rank` 재정렬, `creatorId=0` 차단 관계 및 클릭 불가 정책을 반영했다.
- 2026-06-08: 사용자 추가 제공 정보에 따라 랭킹 item 터치 시 `UserProfileActivity` 이동 외 별도 analytics/logging을 추가하지 않는 것으로 확정하고 Open Questions를 없음으로 정리했다.