# 메인 콘텐츠 탭 내부 랭킹 탭 구현 계획/TASK > **For agentic workers:** 구현 시 `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`를 사용해 task 단위로 진행한다. 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다. **Goal:** `GET /api/v2/audio/rankings?type={AudioRankingType}` 응답을 기반으로 메인 콘텐츠 탭 내부 `랭킹` 탭에 오디오 콘텐츠 랭킹 목록을 표시한다. **Architecture:** 기존 `ContentMainFragment`와 `fragment_v2_main_content.xml`을 최소 확장해 `추천`, `랭킹`, `전체` Text Tab bar와 랭킹 전용 Capsule Tab bar/RecyclerView를 연결한다. 신규 API/Repository/DTO/UI state/mapper/ViewModel은 `kr.co.vividnext.sodalive.v2.main.content` 하위에 둔다. 기존 `kr.co.vividnext.sodalive.v2.widget.contentranking` 위젯은 `showRankChange=false`일 때 rank-num `GONE` 처리만 필요한 만큼 확장한다. **Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test. --- ## 전제와 성공 기준 - PRD: `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md` - Figma 전체 화면: `cont_002` `24:6857` - API endpoint는 `GET /api/v2/audio/rankings`이고 선택 타입은 `type` query parameter로 전달한다. - 예: `GET /api/v2/audio/rankings?type=WEEKLY_POPULAR` - 내부 Text Tab bar 항목은 `추천`, `랭킹`, `전체` 순서다. - Text Tab bar 영역의 `전체` 탭은 항목만 표시하고, 실제 content 화면/API 연동은 이번 범위에서 제외한다. - 랭킹 유형 Capsule Tab bar 항목은 `주간 인기`, `지금 뜨는 중`, `매출`, `판매량`, `댓글수`, `좋아요` 순서다. - 초기 랭킹 유형은 `WEEKLY_POPULAR`/`주간 인기`다. - `rankChange=null` 또는 `0`은 `RankingChangeType.Stay`, amount `0`으로 매핑한다. - `rankChange=5`는 `RankingChangeType.Increase`, amount `5`로 매핑한다. - `rankChange=-3`은 `RankingChangeType.Decrease`, amount `3`으로 매핑한다. - `isNew=true`는 `rankChange`보다 우선해 `RankingChangeType.New`로 매핑한다. - `showRankChange=false`이면 모든 rank-num 영역을 `GONE` 처리한다. - `rank < 1` item은 표시하지 않고, `contentId <= 0` item은 클릭 이동을 무시한다. - API 응답은 서버에서 `rank` 오름차순으로 내려오지만, 클라이언트에서도 한 번 더 `rank` 기준 오름차순 정렬한다. - `coverImageUrl=null`은 빈 문자열로 변환하고 기존 이미지 로딩/placeholder 정책을 따른다. - 레거시 `AudioContentRankingAllActivity` 또는 기존 오디오 메인 화면 파일은 직접 수정하지 않는다. - 구현 완료 후 최소 다음 명령을 실행한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - `git diff --check` --- ## Figma 참조 필요 Phase - Phase 1: 제한 참조 - 기존 콘텐츠 추천 탭, DI, 위젯 구조 확인 중심으로 진행한다. - Phase 2: 제한 참조 - 기존 콘텐츠 랭킹 위젯 구조와 Figma의 rank-num 숨김 요구를 대조한다. - Phase 3: Figma 참조 불필요 - API/DTO/mapper/ViewModel은 PRD 서버 계약과 기존 V2 추천 패턴을 따른다. - Phase 4: 필수 참조 - Text Tab bar, Capsule Tab bar, 랭킹 목록 위치와 visibility 전환은 Figma `24:6857`, `24:6882`를 기준으로 확인한다. - Phase 5: 제한 참조 - 클릭 routing, loading/error/empty는 기존 `ContentMainFragment` 패턴 중심으로 검증한다. - Phase 6: 필수 참조 - 최종 수동 화면 검증은 PRD의 모든 포함/제외 항목과 실제 화면을 대조한다. --- ## 파일 구조 - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItem.kt` - `showRankChange` 표시 계약을 기본값 `true`로 추가한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt` - `showRankChange=false`일 때 `ll_content_ranking_delta`를 `GONE` 처리한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.kt` - 2~10위 grid card의 rank-num `GONE` 처리를 추가한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt` - 11위 이후 horizontal card의 rank-num `GONE` 처리를 추가한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt` - rank-num visibility와 클릭 가능 상태를 Robolectric으로 검증한다. - Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItemTest.kt` - `showRankChange` 기본값과 false 계약 테스트를 추가한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsApi.kt` - `GET /api/v2/audio/rankings?type=...` Retrofit endpoint를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsModels.kt` - `AudioRankingResponse`, `AudioRankingType`, `AudioRankingItemResponse` DTO를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsRepository.kt` - API 호출을 Repository method로 감싼다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRankingsUiState.kt` - `Loading`, `Content`, `Empty`, `Error` 상태와 선택 type 상태를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRankingsMappers.kt` - DTO를 `ContentRankingItem` 목록으로 변환한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentRankingViewModel.kt` - 랭킹 API 호출, selected type, loading/error/content/toast 상태를 관리한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - `AudioRankingsApi`, `AudioRankingsRepository`, `ContentRankingViewModel`을 Koin에 등록한다. - Modify: `app/src/main/res/layout/fragment_v2_main_content.xml` - Text Tab bar에 `추천`, `랭킹`, `전체`가 표시될 수 있도록 기존 구조를 유지하면서 랭킹 Capsule Tab bar와 RecyclerView를 추가한다. - Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - Text Tab 전환, ranking type tab, ranking adapter, ViewModel observe, 오디오 상세 routing을 연결한다. - Modify: `app/src/main/res/values/strings.xml`, `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml` - `추천`, `랭킹`, `전체`, 랭킹 유형 라벨 문자열을 추가하거나 기존 문자열을 재사용한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRankingsMapperTest.kt` - DTO -> `ContentRankingItem` 변환 규칙을 검증한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRankingTypeTest.kt` - type별 query value와 UI label 순서를 검증한다. - Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt` - 랭킹 layout id, tab label, adapter/ViewModel observe, routing source를 검증한다. --- ### Phase 1: 기존 구조 확인과 작업 경계 고정 - [x] **Task 1.1: 기존 콘텐츠 추천 탭 구조 확인** - 확인: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainViewModel.kt` - `app/src/main/res/layout/fragment_v2_main_content.xml` - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 작업: - 추천 탭 `NestedScrollView`와 section visibility 구조를 확인한다. - 랭킹 탭은 기존 추천 section을 리팩터링하지 않고 별도 `RecyclerView` container로 추가하는 경계로 고정한다. - `ContentMainViewModel`은 추천 API 전용으로 유지하고, 랭킹은 신규 `ContentRankingViewModel`로 분리한다. - 검증: - Run: `rg -n "ContentMainViewModel|loadRecommendations|nsv_content_recommendation_content|textTabBarContent|AudioRecommendationsApi" app/src/main/java app/src/main/res` - Expected: 추천 탭 구조와 DI 등록 지점이 확인된다. - 검증 기록: - 2026-06-24: `rg -n "ContentMainViewModel|loadRecommendations|nsv_content_recommendation_content|textTabBarContent|AudioRecommendationsApi" app/src/main/java app/src/main/res` 실행. `ContentMainFragment`, `ContentMainViewModel`, `fragment_v2_main_content.xml`, `AppDI.kt`, `AudioRecommendationsApi`/Repository 참조가 확인되어 추천 탭 구조와 DI 등록 지점을 확인했다. - [x] **Task 1.2: 콘텐츠 랭킹 위젯 확장 지점 확인** - 확인: - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItem.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt` - 작업: - `showRankChange` 필드가 없음을 확인한다. - 세 카드 View의 `bindDelta(item)` 시작부가 rank-num visibility 확장 지점임을 확인한다. - `ContentRankingAdapter`의 mixed layout 정책은 그대로 재사용하는 것으로 고정한다. - 검증: - Run: `rg -n "showRankChange|bindDelta|ll_content_ranking_delta|ContentRankingPlacement" app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking` - Expected: `showRankChange`는 없고, `bindDelta`와 `ll_content_ranking_delta` 참조가 확인된다. - 검증 기록: - 2026-06-24: `rg -n "showRankChange|bindDelta|ll_content_ranking_delta|ContentRankingPlacement" app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking` 실행. 기존 `showRankChange`는 없고 `bindDelta`, `ll_content_ranking_delta`, `ContentRankingPlacement` 참조가 확인되어 확장 지점을 고정했다. - [x] **Task 1.3: 구현 제외 범위 재확인** - 확인: - `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md` - 제외: - Text Tab bar `전체`의 실제 content 화면/API 연동 - 레거시 `AudioContentRankingAllActivity` 수정 - pagination, pull-to-refresh, skeleton/shimmer - analytics/logging 추가 - 추천 탭 section 구조 리팩터링 - 검증: - Run: `rg -n "전체|Non-Goals|AudioContentRankingAllActivity|pagination|skeleton" docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md` - Expected: 제외 범위가 PRD와 일치한다. - 검증 기록: - 2026-06-24: `rg -n "전체|Non-Goals|AudioContentRankingAllActivity|pagination|skeleton" docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md` 실행. `전체` 탭 API/content 제외, legacy `AudioContentRankingAllActivity` 미수정, pagination/skeleton 제외 범위를 재확인했다. --- ### Phase 2: `contentranking` 위젯 rank-num 숨김 계약 확장 - [x] **Task 2.1: `ContentRankingItem` 표시 계약 테스트 추가** - 수정: - `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItemTest.kt` - 추가 테스트: - `showRankChange` 기본값은 `true` - `showRankChange=false`인 item은 순위 변동 표시 대상이 아님 - 기존 `isBlocked`/`isTouchable` 동작은 유지 - 구현 예시: ```kotlin @Test fun `showRankChange 기본값은 true다`() { val item = sampleItem() assertTrue(item.showRankChange) } @Test fun `showRankChange false item은 rank change를 표시하지 않는다`() { val item = sampleItem(showRankChange = false) assertFalse(item.showRankChange) } ``` - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItemTest"` - Expected: `showRankChange` 속성 미구현으로 RED 실패. - 검증 기록: - 2026-06-24 RED: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItemTest"` 실행 시 `Unresolved reference 'showRankChange'`, `No parameter with name 'showRankChange' found`로 실패해 미구현 상태를 확인했다. 같은 실행에서 Phase 3 RED 테스트의 `AudioRankingType` 미구현 실패도 함께 확인되었다. - [x] **Task 2.2: `ContentRankingItem`에 `showRankChange` 추가** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItem.kt` - 구현: - 기존 생성자 마지막에 `val showRankChange: Boolean = true`를 추가한다. - 기본값을 둬 기존 호출부 변경을 최소화한다. - 코드 형태: ```kotlin data class ContentRankingItem( val contentId: String, val creatorId: String, val rank: Int, val previousRank: Int?, val rankChangeType: RankingChangeType, val rankChangeAmount: Int?, val contentName: String, val creatorName: String, val imageUrl: String, val isBlocked: Boolean, val showRankChange: Boolean = true ) ``` - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItemTest"` - Expected: PASS. - 검증 기록: - 2026-06-24 GREEN: `ContentRankingItem`에 `showRankChange: Boolean = true`를 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` 실행 결과 `BUILD SUCCESSFUL`을 확인했다. - [x] **Task 2.3: rank-num `GONE` view 테스트 추가** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt` - 테스트 케이스: - `ContentRankingLargeCardView`에 `showRankChange=false` item bind 시 `ll_content_ranking_delta`가 `GONE` - `ContentRankingMediumGridCardView`에 `showRankChange=false` item bind 시 `ll_content_ranking_delta`가 `GONE` - `ContentRankingSmallGridCardView`에 `showRankChange=false` item bind 시 `ll_content_ranking_delta`가 `GONE` - `ContentRankingHorizontalCardView`에 `showRankChange=false` item bind 시 `ll_content_ranking_delta`가 `GONE` - `showRankChange=true` item bind 시 `ll_content_ranking_delta`가 `VISIBLE` - 구현 예시: ```kotlin @RunWith(RobolectricTestRunner::class) class ContentRankingCardViewTest { @Test fun `large card는 showRankChange false이면 rank num을 숨긴다`() { val context = ApplicationProvider.getApplicationContext() val view = LayoutInflater.from(context) .inflate(R.layout.view_content_ranking_large_card, null, false) as ContentRankingLargeCardView view.bind(sampleItem(showRankChange = false)) assertEquals(View.GONE, view.findViewById(R.id.ll_content_ranking_delta).visibility) } } ``` - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingCardViewTest"` - Expected: card View rank-num 숨김 미구현으로 RED 실패. - 검증 기록: - 2026-06-24: `ContentRankingCardViewTest`를 추가해 large/medium grid/small grid/horizontal card의 `showRankChange=false` 시 `ll_content_ranking_delta` `GONE`, `true` 시 `VISIBLE` 계약을 검증하도록 했다. 기존 Phase 3 RED 테스트가 전체 unit-test 컴파일을 막아 card 테스트 단독 RED는 별도 분리 실행하지 못했고, 구현 후 widget 패키지 검증에서 함께 GREEN을 확인했다. - [x] **Task 2.4: rank-num `GONE` 처리 구현** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt` - 구현: - 각 `bindDelta(item)` 시작부에서 `item.showRankChange == false`이면 delta container를 `View.GONE`으로 설정하고 return한다. - `item.showRankChange == true`이면 delta container를 `View.VISIBLE`로 복구한 뒤 기존 presentation 적용을 유지한다. - 코드 형태: ```kotlin private fun bindDelta(item: ContentRankingItem) { val deltaView = requireNotNull(deltaGroup) if (!item.showRankChange) { deltaView.visibility = View.GONE return } deltaView.visibility = View.VISIBLE val presentation = ContentRankingDeltaPresentation.from(item.rankChangeType, item.rankChangeAmount) applyDeltaContainer(presentation) ... } ``` - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` - Expected: PASS. - 검증 기록: - 2026-06-24: `ContentRankingLargeCardView`, `ContentRankingGridCardView`, `ContentRankingHorizontalCardView`의 `bindDelta(item)` 시작부에서 `showRankChange=false`이면 delta container를 `View.GONE` 처리하고 return하도록 구현했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` 실행 결과 `BUILD SUCCESSFUL`. --- ### Phase 3: 랭킹 API/DTO/mapper 작성 - [x] **Task 3.1: `AudioRankingType` 라벨/query 테스트 작성** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRankingTypeTest.kt` - 테스트 케이스: - `AudioRankingType.entries` 순서가 `WEEKLY_POPULAR`, `RISING`, `REVENUE`, `SALES_COUNT`, `COMMENT_COUNT`, `LIKE_COUNT` - query value는 enum name 그대로 사용 - label은 `주간 인기`, `지금 뜨는 중`, `매출`, `판매량`, `댓글수`, `좋아요` 순서 - 구현 예시: ```kotlin @Test fun `랭킹 타입은 PRD 순서를 유지한다`() { assertEquals( listOf( AudioRankingType.WEEKLY_POPULAR, AudioRankingType.RISING, AudioRankingType.REVENUE, AudioRankingType.SALES_COUNT, AudioRankingType.COMMENT_COUNT, AudioRankingType.LIKE_COUNT ), AudioRankingType.entries ) } ``` - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRankingTypeTest"` - Expected: DTO/type 미구현으로 RED 실패. - 검증 기록: - 2026-06-24 RED: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRankingTypeTest"` 실행 시 `Unresolved reference 'AudioRankingType'`로 실패해 type 미구현 상태를 확인했다. - [x] **Task 3.2: mapper RED 테스트 작성** - 생성: - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRankingsMapperTest.kt` - 테스트 케이스: - 응답 item을 `rank` 오름차순으로 정렬한다. - `rank < 1` item은 제외한다. - `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`이면 모든 `ContentRankingItem.showRankChange=false`로 매핑한다. - `coverImageUrl=null`은 `imageUrl=""`로 매핑한다. - `creatorId`는 API에 없으므로 `ContentRankingItem.creatorId=""`로 매핑한다. - `isBlocked=false`로 매핑한다. - 구현 예시: ```kotlin @Test fun `rankChange 음수는 decrease와 절대값 amount로 매핑된다`() { val item = response( items = listOf(rankingItem(rank = 1, rankChange = -3, isNew = false)) ).toContentRankingItems().single() assertEquals(RankingChangeType.Decrease, item.rankChangeType) assertEquals(3, item.rankChangeAmount) } ``` - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRankingsMapperTest"` - Expected: DTO/mapper 미구현으로 RED 실패. - 검증 기록: - 2026-06-24: `AudioRankingsMapperTest`를 추가해 rank 정렬/필터, `isNew` 우선, `rankChange` null/0/양수/음수, `showRankChange=false`, `coverImageUrl=null`, `creatorId=""`, `isBlocked=false` 매핑 계약을 검증하도록 했다. 직전 `AudioRankingTypeTest` RED에서 DTO/type 미구현 컴파일 실패를 확인한 뒤 같은 미구현 상태를 전제로 작성했다. - [x] **Task 3.3: API DTO와 mapper 구현** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsModels.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRankingsMappers.kt` - 구현: - `@Keep data class AudioRankingResponse(...)` - `enum class AudioRankingType(...)` - `@Keep data class AudioRankingItemResponse(...)` - PRD 응답 계약에 맞춰 `AudioRankingResponse`에 `type: AudioRankingType`을 포함한다. - Gson `@SerializedName("isNew")`를 사용하고 Jackson `@JsonProperty`는 사용하지 않는다. - `fun AudioRankingResponse.toContentRankingItems(): List`를 추가한다. - `fun AudioRankingType.labelResId(): Int` 또는 `label(context)` helper는 UI 연결 Phase에서 문자열 리소스와 함께 확정한다. - 코드 형태: ```kotlin @Keep data class AudioRankingItemResponse( @SerializedName("contentId") val contentId: Long, @SerializedName("title") val title: String, @SerializedName("creatorNickname") val creatorNickname: String, @SerializedName("rank") val rank: Int, @SerializedName("rankChange") val rankChange: Int?, @SerializedName("isNew") val isNew: Boolean, @SerializedName("coverImageUrl") val coverImageUrl: String? ) ``` - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRankingTypeTest"` - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRankingsMapperTest"` - Expected: PASS. - 검증 기록: - 2026-06-24: `AudioRankingsModels.kt`와 `AudioRankingsMappers.kt`를 추가했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24 리뷰 보완: 리뷰 게이트에서 PRD 응답 계약의 `type` 누락이 확인되어 `AudioRankingResponse.type: AudioRankingType`을 추가했다. - [x] **Task 3.4: API/Repository 작성** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsApi.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsRepository.kt` - 구현: - `@GET("/api/v2/audio/rankings")` - `@Query("type") type: AudioRankingType` - `@Header("Authorization") authHeader: String` - 반환 타입은 `Single>` - 코드 형태: ```kotlin interface AudioRankingsApi { @GET("/api/v2/audio/rankings") fun getRankings( @Header("Authorization") authHeader: String, @Query("type") type: AudioRankingType ): Single> } ``` - 검증: - Run: `./gradlew :app:compileDebugKotlin` - Expected: 신규 API/Repository 컴파일 성공. - 검증 기록: - 2026-06-24: `AudioRankingsApi.kt`, `AudioRankingsRepository.kt`를 추가했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`에서 신규 API/Repository를 포함한 debug/test 컴파일과 관련 테스트 `BUILD SUCCESSFUL`을 확인했다. --- ### Phase 4: ViewModel과 DI 등록 - [x] **Task 4.1: UI state와 ViewModel 작성** - 생성: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRankingsUiState.kt` - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentRankingViewModel.kt` - 구현: - `AudioRankingsUiState.Loading` - `AudioRankingsUiState.Content(val type: AudioRankingType, val items: List)` - `AudioRankingsUiState.Empty(val type: AudioRankingType)` - `AudioRankingsUiState.Error(val type: AudioRankingType, val message: String?)` - `rankingStateLiveData`, `toastLiveData`, `isLoading`, `selectedTypeLiveData` - `loadRankings(type: AudioRankingType, force: Boolean = false)` - 같은 type을 이미 성공 로드했고 `force=false`이면 중복 호출하지 않는다. - 응답 `type`이 현재 선택 type과 다르면 화면 반영을 무시한다. - 기존 `ContentMainViewModel`과 동일하게 `SharedPreferenceManager.token`, RxJava scheduler, unknown error toast 패턴을 사용한다. - 검증: - Run: `./gradlew :app:compileDebugKotlin` - Expected: ViewModel 컴파일 성공. - 검증 기록: - 2026-06-24 RED: `ContentRankingViewModelTest`를 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentRankingViewModelTest"` 실행 시 `Unresolved reference 'AudioRankingsUiState'`, `Unresolved reference 'ContentRankingViewModel'`, `Unresolved reference 'loadRankings'` 등으로 실패해 UI state/ViewModel 미구현 상태를 확인했다. - 2026-06-24 GREEN: `AudioRankingsUiState`, `ContentRankingViewModel`을 추가해 초기 `WEEKLY_POPULAR`, token/type API 호출, content/empty/error/throwable, 중복 호출 방지, force reload, stale response 무시를 구현했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentRankingViewModelTest"` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: Kotlin LSP 진단은 환경에 `kotlin-lsp`가 설치되어 있지 않아 실행 불가(`Command not found: kotlin-lsp`). Gradle test/compile로 대체 검증했다. - 2026-06-24 리뷰 보완 RED: 리뷰 게이트에서 이미 로드한 랭킹 타입 재선택 시 API 호출은 막히지만 캐시된 상태를 재방출하지 않아 이전 타입 목록이 남을 수 있음을 지적했다. `이미 로드한 type을 다시 선택하면 API 재호출 없이 캐시된 상태를 emit한다` 테스트를 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentRankingViewModelTest"` 실행 시 `ContentRankingViewModelTest.kt:146` assertion 실패로 RED를 확인했다. - 2026-06-24 리뷰 보완 GREEN: `ContentRankingViewModel`의 `loadedTypes`를 `cachedStates`로 변경해 `Content`/`Empty` 성공 상태를 타입별로 저장하고, 이미 로드한 타입 재선택 시 API 재호출 없이 `_rankingStateLiveData`에 캐시 상태를 emit하도록 수정했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentRankingViewModelTest"` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24 Phase 4/5 리뷰 보완 RED: 이전 type 요청 응답이 늦게 도착하면 화면 상태 반영은 막히지만 `_isLoading=false`가 먼저 emit되어 현재 type 요청의 로딩 다이얼로그가 조기 종료될 수 있음을 확인했다. `이전 type 응답이 늦게 도착해도 현재 로딩 상태를 끄지 않는다` 테스트를 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentRankingViewModelTest"` 실행 시 `ContentRankingViewModelTest.kt:193` assertion 실패로 RED를 확인했다. - 2026-06-24 Phase 4/5 리뷰 보완 GREEN: `ContentRankingViewModel`에 `latestRequestId`와 `isCurrentRequest(...)`를 추가해 최신 요청만 loading/state/toast를 마감하도록 수정했다. 캐시 상태 재방출 시에도 이전 pending 요청을 무효화하고 `_isLoading=false`를 emit하도록 정리했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentRankingViewModelTest"` 실행 결과 `BUILD SUCCESSFUL`. - [x] **Task 4.2: Koin DI 등록** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` - 구현: - import 추가: `AudioRankingsApi`, `AudioRankingsRepository`, `ContentRankingViewModel` - API 등록: `single { ApiBuilder().build(get(), AudioRankingsApi::class.java) }` - Repository 등록: `factory { AudioRankingsRepository(get()) }` - ViewModel 등록: `viewModel { ContentRankingViewModel(get()) }` - 검증: - Run: `./gradlew :app:compileDebugKotlin` - Expected: Koin 등록과 import 컴파일 성공. - 검증 기록: - 2026-06-24: `AppDI.kt`에 `AudioRankingsApi`, `AudioRankingsRepository`, `ContentRankingViewModel` 등록을 추가했다. `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`. --- ### Phase 5: 레이아웃과 탭 전환 UI 연결 - [x] **Task 5.1: 랭킹 layout/source RED 테스트 추가** - 수정: - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt` - 추가 테스트: - `fragment_v2_main_content.xml`에 `view_content_ranking_type_tabs`가 존재한다. - `fragment_v2_main_content.xml`에 `rv_content_rankings`가 존재한다. - `ContentMainFragment` source에 `ContentRankingViewModel by viewModel()`이 존재한다. - `ContentMainFragment` source에 `ContentRankingAdapter.createGridLayoutManager(requireContext())`가 존재한다. - `ContentMainFragment` source에 `AudioRankingType.WEEKLY_POPULAR` 초기 로드가 존재한다. - `ContentMainFragment` source에 Text Tab 메뉴 `추천`, `랭킹`, `전체` 설정이 존재한다. - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` - Expected: layout id/ViewModel/adapter 연결 미구현으로 RED 실패. - 검증 기록: - 2026-06-24 RED: `ContentMainFragmentSourceTest`에 랭킹 layout id, capsule tab include, `ContentRankingViewModel`, `ContentRankingAdapter.createGridLayoutManager(requireContext())`, `AudioRankingType.WEEKLY_POPULAR`, Text Tab/Capsule Tab 연결 source assertion을 추가했다. 같은 시점의 테스트 컴파일은 Phase 4 RED 테스트의 `ContentRankingViewModel` 미구현 오류로 먼저 실패해 Fragment source 미구현 상태를 함께 확인했다. - 2026-06-24 GREEN: Phase 5 production 연결 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` 실행 결과 `BUILD SUCCESSFUL`. - [x] **Task 5.2: strings 추가** - 수정: - `app/src/main/res/values/strings.xml` - `app/src/main/res/values-en/strings.xml` - `app/src/main/res/values-ja/strings.xml` - 구현: - `screen_content_tab_recommendation` - `screen_content_tab_ranking` - `screen_content_tab_all` - `screen_content_ranking_type_weekly_popular` - `screen_content_ranking_type_rising` - `screen_content_ranking_type_revenue` - `screen_content_ranking_type_sales_count` - `screen_content_ranking_type_comment_count` - `screen_content_ranking_type_like_count` - 검증: - Run: `./gradlew :app:mergeDebugResources` - Expected: 문자열 리소스 merge 성공. - 검증 기록: - 2026-06-24: `values`, `values-en`, `values-ja`에 콘텐츠 Text Tab `랭킹`/`전체`와 랭킹 유형 6개 라벨 문자열을 추가했다. `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL`. - [x] **Task 5.3: `fragment_v2_main_content.xml`에 랭킹 container 추가** - 수정: - `app/src/main/res/layout/fragment_v2_main_content.xml` - 구현: - 기존 `text_tab_bar_content` 아래에 랭킹 type tab include를 추가한다. - `@+id/view_content_ranking_type_tabs`, `layout="@layout/view_capsule_tab_bar"`, 초기 `android:visibility="gone"`로 둔다. - 랭킹 RecyclerView `@+id/rv_content_rankings`를 추가한다. - `rv_content_rankings`는 `view_content_ranking_type_tabs` 아래부터 parent bottom까지 constraint하고 초기 `gone`으로 둔다. - 기존 `nsv_content_recommendation_content`는 추천 탭 선택 시만 visible로 유지한다. - XML 형태: ```xml ``` - 검증: - Run: `./gradlew :app:mergeDebugResources` - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` - Expected: resource merge 성공, source test는 Fragment 연결 전 일부 RED 유지 가능. - 검증 기록: - 2026-06-24: `fragment_v2_main_content.xml`의 `text_tab_bar_content` 아래에 `view_content_ranking_type_tabs` include와 `rv_content_rankings`를 초기 `gone` 상태로 추가했다. `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL`, `ContentMainFragmentSourceTest`에서 layout id/source assertion `BUILD SUCCESSFUL`. - [x] **Task 5.4: `ContentMainFragment`에 Text Tab 전환과 랭킹 탭 UI 연결** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - 구현: - `private val contentRankingViewModel: ContentRankingViewModel by viewModel()` 추가 - `ContentRankingAdapter { openAudioContentDetail(it.contentId.toLongOrNull() ?: 0L) }` 추가 - `binding.textTabBarContent.root.setMenus(listOf(...추천, ...랭킹, ...전체), selectedIndex = 0)`로 변경 - Text Tab index 상수 추가: - `CONTENT_TAB_RECOMMENDATION = 0` - `CONTENT_TAB_RANKING = 1` - `CONTENT_TAB_ALL = 2` - `랭킹` 선택 시 추천 scroll을 `GONE`, ranking type tab과 ranking RecyclerView를 `VISIBLE`로 전환한다. - `전체` 선택 시 항목만 선택되지만 실제 content 구현은 제외하므로 추천/랭킹 content를 모두 숨기거나 기존 화면 정책에 맞춰 빈 화면을 유지한다. 이 동작은 계획 실행 중 사용자 확인 없이 추가 API 연동으로 확장하지 않는다. - `랭킹` 최초 선택 시 `AudioRankingType.WEEKLY_POPULAR`를 로드한다. - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` - Expected: Fragment source 연결 관련 assertion PASS. - 검증 기록: - 2026-06-24: `ContentMainFragment`에 `ContentRankingViewModel`, content tab 상수, `추천`/`랭킹`/`전체` Text Tab 메뉴와 visibility 전환을 연결했다. `랭킹` 최초 선택 시 `contentRankingViewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR)`를 호출하고, `전체`는 별도 API/content 없이 추천/랭킹 surface를 숨기도록 유지했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` 실행 결과 `BUILD SUCCESSFUL`. - [x] **Task 5.5: 랭킹 유형 Capsule Tab 연결** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - 구현: - `view_content_ranking_type_tabs.root.setMenus(...)`로 6개 라벨을 설정한다. - `setOnTabSelectedListener`에서 index를 `AudioRankingType.entries[index]`로 변환한다. - 같은 type 재선택은 `CapsuleTabBarView.selectTab()` 내부에서 무시되므로 별도 API 호출을 만들지 않는다. - 탭 전환 시 `contentRankingViewModel.loadRankings(type)`를 호출한다. - 검증: - Run: `./gradlew :app:compileDebugKotlin` - Expected: Capsule Tab 연결 컴파일 성공. - 검증 기록: - 2026-06-24: `view_content_ranking_type_tabs.root.setMenus(...)`에 `AudioRankingType.entries` 순서대로 6개 문자열 리소스 라벨을 설정하고, `setOnTabSelectedListener`에서 `AudioRankingType.entries[index]`를 `loadRankings(type)`로 연결했다. `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`. - [x] **Task 5.6: ranking observer와 adapter submit 연결** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - 구현: - `rv_content_rankings.layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())` - `rv_content_rankings.adapter = contentRankingAdapter` - `rankingStateLiveData.observe(viewLifecycleOwner)`에서 `Content`이면 `submitItems(state.items)`, `Empty`/`Error`이면 `submitItems(emptyList())` - `isLoading`은 기존 `LoadingDialog`와 충돌하지 않도록 추천/랭킹 ViewModel의 loading 상태를 모두 관찰하거나, 랭킹 loading만 별도 observer에서 같은 dialog를 show/dismiss한다. - `toastLiveData`는 기존 `showToast` helper를 재사용한다. - 검증: - Run: `./gradlew :app:compileDebugKotlin` - Expected: Fragment/adapter/ViewModel 연결 컴파일 성공. - 검증 기록: - 2026-06-24: `rv_content_rankings`에 `ContentRankingAdapter.createGridLayoutManager(requireContext())`와 `ContentRankingAdapter`를 연결하고, `rankingStateLiveData`의 `Content`는 `submitItems(state.items)`, `Empty`/`Error`는 `submitItems(emptyList())`로 처리했다. 추천/랭킹 loading 상태는 `updateLoadingDialog()`에서 함께 반영하고, ranking `toastLiveData`는 기존 `showToast` helper를 재사용했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `./gradlew :app:compileDebugKotlin` 실행 결과 모두 `BUILD SUCCESSFUL`. --- ### Phase 6: routing, 통합 검증, 문서 검증 기록 - [x] **Task 6.1: 랭킹 item 클릭 routing guard 연결** - 수정: - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - 구현: - `ContentRankingItem.contentId`를 `Long`으로 변환한다. - `contentId > 0`이면 기존 `openAudioContentDetail(audioContentId)`를 호출한다. - 변환 실패 또는 `contentId <= 0`이면 아무 동작하지 않는다. - 코드 형태: ```kotlin private fun openRankingAudioContentDetail(item: ContentRankingItem) { val audioContentId = item.contentId.toLongOrNull()?.takeIf { it > 0L } ?: return openAudioContentDetail(audioContentId) } ``` - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` - Expected: routing source assertion PASS. - 검증 기록: - 2026-06-24 RED: `ContentMainFragmentSourceTest`에 랭킹 routing guard source assertion을 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` 실행 시 `ContentMainFragmentSourceTest.kt:97` assertion 실패로 `openRankingAudioContentDetail` 내부의 명시적 `contentId > 0` guard 미반영 상태를 확인했다. - 2026-06-24 GREEN: `openRankingAudioContentDetail`에서 `item.contentId.toLongOrNull()?.takeIf { it > 0L } ?: return`으로 변환 실패와 `contentId <= 0`을 모두 무시하도록 보강했다. 같은 테스트 재실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: Kotlin LSP 진단은 환경에 `kotlin-lsp`가 설치되어 있지 않아 실행 불가(`Command not found: kotlin-lsp`). `ContentMainFragmentSourceTest`, package test, compile 검증으로 대체했다. - 2026-06-24: Phase 6 코드 리뷰에서 `openRankingAudioContentDetail`의 `toLongOrNull()?.takeIf { it > 0L } ?: return` guard가 PRD의 `contentId <= 0` 클릭 무시 요구와 일치함을 재확인했다. - [x] **Task 6.2: mapper/widget/content package 단위 테스트 실행** - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` - Expected: PASS. - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` - Expected: PASS. - 검증 기록: - 2026-06-24: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: Phase 6 재검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` 재실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: Phase 6 재검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` 재실행 결과 `BUILD SUCCESSFUL`. - [x] **Task 6.3: 리소스/컴파일/스타일 검증** - 검증: - Run: `./gradlew :app:mergeDebugResources` - Expected: `BUILD SUCCESSFUL` - Run: `./gradlew :app:compileDebugKotlin` - Expected: `BUILD SUCCESSFUL` - Run: `./gradlew :app:ktlintCheck` - Expected: `BUILD SUCCESSFUL` - Run: `git diff --check` - Expected: trailing whitespace 또는 conflict marker 없음 - 검증 기록: - 2026-06-24: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: `./gradlew :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`. `.editorconfig`의 `disabled_rules` deprecation 경고는 기존 설정 경고로 남아 있다. - 2026-06-24: `git diff --check` 실행 결과 출력 없음. trailing whitespace 또는 conflict marker 없음. - 2026-06-24: Phase 6 재검증에서 `./gradlew :app:mergeDebugResources`는 샌드박스 내 Gradle wrapper lock 접근 제한으로 1차 실패 후 권한 상승 재실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: Phase 6 재검증으로 `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 실행 결과 모두 통과했다. - [ ] **Task 6.4: 수동 화면 검증 [blocked]** - 확인: - 메인 콘텐츠 탭 진입 시 기본 `추천` 탭 화면이 기존처럼 표시된다. - Text Tab bar에 `추천`, `랭킹`, `전체`이 보인다. - `랭킹` 선택 시 Capsule Tab bar 6개 항목이 Figma 순서로 보인다. - 초기 선택은 `주간 인기`이고 API는 `type=WEEKLY_POPULAR`로 호출된다. - 랭킹 목록은 1위 대형, 2~7위 2열, 8~10위 3열, 11위 이후 가로형으로 표시된다. - `showRankChange=false` 응답에서 rank-num 영역이 보이지 않는다. - `contentId > 0` item 터치 시 오디오 상세로 이동한다. - `전체` 탭은 항목만 표시하고 별도 API/content 구현이 없다. - 검증 기록: - 2026-06-24: Figma `24:6857` design context와 screenshot으로 Text Tab `추천`/`랭킹`/`전체`, Capsule Tab `주간 인기`/`지금 뜨는 중`/`매출`/`판매량`/`댓글수`/`좋아요` 순서, 랭킹 목록의 1위 대형/2~7위 2열/8~10위 3열/11위 이후 가로형 배치를 대조했다. - 2026-06-24: source/test 기준으로 `랭킹` 최초 선택 시 `AudioRankingType.WEEKLY_POPULAR` 로드, `ContentRankingAdapter` mixed layout 연결, `showRankChange=false` rank-num `GONE`, `contentId > 0` routing과 invalid id 무시, `전체` 탭의 별도 API/content 미구현 상태를 확인했다. - 2026-06-24 [blocked]: ADB 권한 상승 조회에서 연결 device 1대와 기존 `kr.co.vividnext.sodalive.debug` 설치 상태를 확인했고, 기존 설치 앱 실행 스크린샷에서 콘텐츠 추천 화면 진입 상태를 확인했다. - 2026-06-24 [blocked]: 현재 로컬 변경 기준 최신 APK 수동 QA를 위해 `./gradlew :app:installDebug`를 실행했으나 Gradle install 단계에서 `No connected devices!`로 실패했다. 재확인 시 `adb devices -l`도 빈 목록을 반환해 최신 debug APK 설치/실행 기반 화면 검증은 완료하지 못했다. - 2026-06-24: Phase 6 재검증에서 Figma `24:6857` design context와 screenshot을 다시 확인했다. 최신 APK가 설치된 device/emulator 기반 터치 검증은 계속 blocked 상태로 유지한다. --- ### Phase 7: rank-num 간격 시각 보정 - [x] **Task 7.1: rank 텍스트 하단 보정 회귀 테스트 추가** - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt` - 구현 내용: - Large, Medium grid, Small grid, Horizontal variant의 순위 `TextView`가 `includeFontPadding=false`를 유지하는지 검증한다. - 순위 `TextView`에 variant별 하단 padding이 적용되어 Pattaya glyph가 rank-num 영역에 붙어 보이지 않도록 검증한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingCardViewTest"` - 기대 결과: production 수정 전 padding assertion이 실패한다. - [x] **Task 7.2: rank 텍스트 내부 하단 padding 보정** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt` - 구현 내용: - 기존 Figma 좌표값과 rank-num top/left는 변경하지 않는다. - 순위 숫자 `TextView` 내부에 scale 기반 하단 padding을 적용해 폰트 glyph가 위쪽으로 보정되도록 한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingCardViewTest"` - 기대 결과: RED 테스트가 `BUILD SUCCESSFUL`로 전환된다. - [x] **Task 7.3: 후속 검증과 문서 기록 누적** - 실행: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:ktlintCheck` - `git diff --check` - 수정: `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md` - 수정: `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md` - 기대 결과: 모두 통과, Verification Log 누적. --- ### Phase 8: 랭킹 간격 8dp와 1위 이미지 중복 제거 - [x] **Task 8.1: 후속 UI 요구 문서 반영** - 수정: `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md` - 수정: `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md` - 구현 내용: - 카드 간 gap 정책을 4dp에서 8dp로 갱신한다. - 1위 대형 카드는 `coverImageUrl`을 blur 배경으로만 사용하고 전경 1:1 이미지는 표시하지 않는 요구를 기록한다. - 검증 명령: 문서 diff 확인 - 기대 결과: 기존 문서에 후속 요구가 누적되고 신규 문서는 생성하지 않는다. - 검증 기록: - 2026-06-25: 기존 `prd.md`와 `plan-task.md`에 카드 간 8dp gap, 1위 대형 카드 foreground 1:1 이미지 제거, background blur 유지 요구를 누적했다. 신규 docs 문서는 생성하지 않았다. - [x] **Task 8.2: 랭킹 간격 8dp 계약과 RecyclerView 세로 간격 적용** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLayoutCalculatorTest.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt` - 구현 내용: - `ContentRankingAdapter`의 gap 상수를 8dp로 변경한다. - 현재 랭킹 RecyclerView에 세로 item 간격이 없으므로 8dp 하단 간격 `ItemDecoration`을 랭킹 목록에만 추가한다. - 374px 기준 2열 width 183px, 3열 width 119px를 테스트로 고정한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` - 기대 결과: width/gap 계약 테스트가 통과한다. - 검증 기록: - 2026-06-25: `ContentRankingAdapter` gap 상수를 8dp로 변경하고, `ContentMainFragment`의 `rvContentRankings`에 랭킹 item 간 8dp bottom spacing `ItemDecoration`을 연결했다. `ContentRankingLayoutCalculatorTest`에서 374px 기준 2열 183px, 3열 119px 계약을 검증하도록 갱신했다. - [x] **Task 8.3: 1위 대형 카드 전경 이미지 제거와 배경 blur 유지** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt` - 수정: `app/src/main/res/layout/view_content_ranking_large_card.xml` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingBlurTest.kt` - 구현 내용: - LargeViewHolder는 `iv_content_ranking_image`에 이미지를 로드하지 않는다. - `iv_content_ranking_image`는 large card에서 `GONE` 상태로 유지한다. - `iv_content_ranking_background`에는 접근 가능 item도 blur background로 로드하고, S+에서는 background RenderEffect를 유지한다. - 검증 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` - 기대 결과: foreground 이미지 비표시/미로드와 background blur 계약 테스트가 통과한다. - 검증 기록: - 2026-06-25: LargeViewHolder에서 `view.imageView().loadContentImage(item)` 호출을 제거하고 `view.backgroundImageView().loadContentImage(item, blurEnabled = true)`만 수행하도록 변경했다. `iv_content_ranking_image`는 XML과 `ContentRankingLargeCardView`에서 `GONE`으로 유지하고, S+ background RenderEffect는 접근 가능 item에도 적용되도록 했다. `ContentRankingCardViewTest`와 `ContentRankingBlurTest`로 foreground 비표시/미로드와 blur helper 계약을 검증했다. - [x] **Task 8.4: 후속 Gradle 검증과 기록 누적** - 실행: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - `git diff --check` - 수정: `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md` - 기대 결과: 요청된 순차 검증이 통과하거나 정확한 실패가 Verification Log에 누적된다. - 검증 기록: - 2026-06-25: 요청된 Gradle 검증을 순차 실행했다. `contentranking.*`, `ContentMainFragmentSourceTest`, `mergeDebugResources`, `compileDebugKotlin`, `ktlintCheck`, `git diff --check`는 최종 모두 통과했다. `ktlintCheck`는 중간에 변경 파일 `ContentRankingAdapter.kt`의 긴 inflate 라인 3곳 max line length 위반으로 실패했고, 줄바꿈만 정리한 뒤 재실행해 `BUILD SUCCESSFUL`을 확인했다. - 2026-06-25: 연결 device `SM_G960N`에 `./gradlew :app:installDebug`로 최신 debug APK 설치가 성공했고, `adb shell monkey -p kr.co.vividnext.sodalive.debug 1` 후 `SplashActivity` 포커스를 확인했다. 로그인/초기 진입 상태 때문에 랭킹 탭까지의 터치 기반 화면 검증은 완료하지 못했다. --- ### Phase 8 Follow-up: 1위 대형 카드 전경 이미지 유지 정정 - [x] **Task 8.5: 사용자 정정 요구 문서 반영** - 수정: `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md` - 수정: `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md` - 구현 내용: - Phase 8의 '전경 1:1 이미지 숨김' 문구를 최신 사용자 정정으로 supersede한다. - 1위 대형 카드 배경 blur는 유지하고, 동일 `coverImageUrl` 전경 1:1 이미지는 표시하며 접근 가능한 item에서는 전경 이미지를 blur 처리하지 않는 요구를 기록한다. - 기존 8dp ranking spacing 요구는 변경하지 않는다. - 검증 기록: - 2026-06-25: 기존 Phase 8 검증 기록은 삭제하지 않고 유지했다. 최신 사용자 정정에 따라 배경 blur 유지, 전경 1:1 이미지 표시 및 accessible 전경 non-blur 계약을 본 follow-up으로 추가했다. - [x] **Task 8.6: 1위 대형 카드 전경 이미지 복구와 non-blur 로드 계약 반영** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt` - 수정: `app/src/main/res/layout/view_content_ranking_large_card.xml` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt` - 구현 내용: - LargeViewHolder는 `iv_content_ranking_background`에 `blurEnabled = true`로 이미지를 로드한다. - LargeViewHolder는 `iv_content_ranking_image`에도 동일 item 이미지를 `blurEnabled = false`로 로드한다. - large card의 전경 `iv_content_ranking_image` 강제 `GONE` 처리를 제거해 기본 `VISIBLE` 상태를 유지한다. - S+ background RenderEffect blur는 유지하고, foreground RenderEffect는 inaccessible item에만 적용되는 기존 접근 제한 계약을 유지한다. - 검증 기록: - 2026-06-25: production/test를 수정해 1위 대형 카드 전경 이미지가 `VISIBLE`이고, LargeViewHolder source가 background blur load와 foreground non-blur load를 모두 포함하도록 갱신했다. - [x] **Task 8.7: 최신 정정 순차 검증과 기록 누적** - 실행: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:compileDebugKotlin` - `./gradlew :app:ktlintCheck` - `git diff --check` - 수정: `docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md` - 기대 결과: 요청된 순차 검증 결과를 이 문서와 Verification Log에 누적한다. - 검증 기록: - 2026-06-25: 최신 정정 반영 후 요청된 검증을 순차 실행했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`는 모두 `BUILD SUCCESSFUL`로 통과했다. `git diff --check`는 출력 없이 통과했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고와 Gradle deprecation 경고는 기존 설정 경고로 남아 있다. --- ## Verification Log - 구현 중 여러 Phase에 걸친 통합 검증, 회귀 검증, 최종 수동 확인 기록을 이 아래에 누적한다. - 2026-06-24: Phase 1~3 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: Phase 1~3 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`. - 2026-06-24: `./gradlew :app:ktlintCheck` 1차 실행에서 변경 파일의 기존 긴 `LayoutParams` 라인이 ktlint max line length를 위반해 실패했고, 기능 변경 없이 줄바꿈만 정리한 뒤 재실행 결과 `BUILD SUCCESSFUL`. `.editorconfig`의 `disabled_rules` deprecation 경고는 기존 설정 경고로 남아 있다. - 2026-06-24: `git diff --check` 실행 결과 출력 없음. trailing whitespace 또는 conflict marker 없음. - 2026-06-24: Kotlin LSP 진단은 환경에 `kotlin-lsp`가 설치되어 있지 않아 실행 불가(`Command not found: kotlin-lsp`). Gradle compile/test/ktlint로 대체 검증했다. - 2026-06-24: 리뷰 게이트 보완 후 `AudioRankingResponse.type` 추가. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 재실행 결과 모두 통과했다. - 2026-06-24: Phase 1~3 코드 리뷰 및 현재 워크트리 기준 재검증을 수행했다. Phase 1 구조 확인용 `rg` 3개 명령에서 기대 참조를 확인했고, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 실행 결과 모두 통과했다. `./gradlew :app:mergeDebugResources`는 샌드박스 내 Gradle wrapper lock 접근 제한으로 1차 실패 후 권한 상승 실행에서 `BUILD SUCCESSFUL`을 확인했다. - 2026-06-24: Phase 4~5 구현 중 병렬 Gradle 실행에서 KSP/Kotlin incremental cache 접근 충돌(`NoSuchFileException .../kspCaches/debug/backups`, `Storage ... lookups.tab is already registered`)이 발생했으나, 동일 명령을 순차 재실행해 실제 코드 오류가 아님을 분리했다. - 2026-06-24: Phase 4~5 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 실행 결과 모두 통과했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고로 남아 있다. - 2026-06-24: Figma `24:6857`, `24:6882`를 참조해 Text Tab `추천`/`랭킹`/`전체`, Capsule Tab `주간 인기`/`지금 뜨는 중`/`매출`/`판매량`/`댓글수`/`좋아요` 순서와 랭킹 목록 위치를 확인했다. 에뮬레이터 실기 화면 수동 검증은 Phase 6 범위로 남겨두었다. - 2026-06-24: 리뷰 게이트 보완 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 재실행 결과 모두 통과했다. - 2026-06-24: Phase 4~5 코드 리뷰에서 stale response loading 조기 종료 문제를 발견해 RED/GREEN 테스트로 보완했다. Figma `24:6857` 스크린샷으로 Text Tab `추천`/`랭킹`/`전체`, Capsule Tab `주간 인기`/`지금 뜨는 중`/`매출`/`판매량`/`댓글수`/`좋아요` 순서와 랭킹 목록 위치를 재확인했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 실행 결과 모두 통과했다. `:app:mergeDebugResources`는 샌드박스 내 Gradle wrapper lock 접근 제한으로 1차 실패 후 권한 상승 실행에서 `BUILD SUCCESSFUL`을 확인했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고로 남아 있다. - 2026-06-24: Phase 4~5 현재 워크트리 기준 코드 리뷰 및 재검증을 수행했다. `ContentRankingViewModel`의 캐시 재방출, stale response 무시, loading 종료 조건과 `ContentMainFragment`의 Text Tab/Capsule Tab 연결, visibility 전환, ranking adapter 연결, loading dialog 통합, routing guard를 확인했고 추가 결함은 발견하지 못했다. Figma `24:6857`, `24:6882` design context와 screenshot으로 `추천`/`랭킹`/`전체`, `주간 인기`/`지금 뜨는 중`/`매출`/`판매량`/`댓글수`/`좋아요` 순서와 랭킹 목록 위치를 대조했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 실행 결과 모두 통과했다. `:app:mergeDebugResources`는 샌드박스 내 Gradle wrapper lock 접근 제한으로 1차 실패 후 권한 상승 실행에서 `BUILD SUCCESSFUL`을 확인했다. - 2026-06-24: Phase 6에서 랭킹 item 클릭 routing guard를 명시적으로 보강했다. RED는 `ContentMainFragmentSourceTest.kt:97` assertion 실패로 확인했고, GREEN은 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` `BUILD SUCCESSFUL`로 확인했다. - 2026-06-24: Phase 6 최종 자동 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 실행 결과 모두 통과했다. Kotlin LSP 진단은 환경에 `kotlin-lsp`가 설치되어 있지 않아 실행 불가했고, `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고는 기존 설정 경고로 남아 있다. - 2026-06-24: Phase 6 수동 검증 중 Figma `24:6857` design context/screenshot과 source/test 대조로 Text Tab/Capsule Tab 순서, 랭킹 목록 배치, 초기 `WEEKLY_POPULAR`, `showRankChange=false`, routing guard, `전체` 탭 제외 범위를 확인했다. ADB 권한 상승 조회에서 일시적으로 연결 device와 기존 debug 앱 실행 화면은 확인했으나, 현재 로컬 변경 기준 최신 APK 설치를 위한 `./gradlew :app:installDebug`는 `No connected devices!`로 실패했다. 최신 APK가 설치된 device/emulator 기반 최종 화면 터치 검증은 후속 환경에서 확인 필요하므로 Task 6.4는 [blocked]로 남긴다. - 2026-06-24: Phase 6 코드 리뷰 및 재검증을 수행했다. `ContentMainFragment` routing guard, `ContentRankingViewModel` stale response/loading 처리, mapper의 rank 필터/정렬과 `showRankChange` 전달, widget rank-num `GONE` 처리를 PRD/계획과 대조했고 추가 수정이 필요한 결함은 발견하지 못했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 재실행 결과 모두 통과했다. `:app:mergeDebugResources`는 샌드박스 내 Gradle wrapper lock 접근 제한으로 1차 실패 후 권한 상승 재실행에서 통과했다. - 2026-06-25: Phase 7.1로 `ContentRankingCardViewTest`에 순위 `TextView`의 `includeFontPadding=false` 유지와 variant별 하단 padding(1위 10px, 2~7위 6px, 8~10위 5px, 11위 이후 4px) 검증을 추가했다. Phase 7.2로 기존 Figma 좌표와 rank-num top/left는 유지하고 콘텐츠 랭킹 Large/Grid/Horizontal 카드의 순위 `TextView` 내부 하단 padding만 scale 기반으로 적용했다. - 2026-06-25: Phase 7 검증 중 최초 병렬 Gradle 실행에서 Kotlin incremental cache 충돌과 timeout이 발생해 `./gradlew --stop`, `./gradlew clean` 후 순차 재실행했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingCardViewTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`은 모두 `BUILD SUCCESSFUL`로 통과했다. `./gradlew :app:ktlintCheck`는 변경 파일이 아닌 기존 전역 위반(`Agora.kt`, `audio_content` package-name, 기존 `ContentRankingAdapter.kt` 긴 줄 등)으로 실패했으며, `git diff --check`는 출력 없이 통과했다. - 2026-06-25: 리뷰 게이트에서 콘텐츠 랭킹 horizontal rank `TextView`가 고정 박스 없이 bottom padding만 적용되어 rank-num 위치가 밀릴 수 있고, 콘텐츠 rank `TextView`의 중앙 정렬 계약이 부족하다는 blocking 이슈를 확인했다. 후속 보완으로 `ContentRankingHorizontalCardView`에 `48x52` 고정 rank box를 적용하고, 콘텐츠 랭킹 rank XML 3종에 `android:gravity="center"`를 추가했으며, `ContentRankingCardViewTest`가 rank box 크기/위치, gravity, `includeFontPadding=false`, bottom padding을 함께 검증하도록 확장했다. 보완 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingCardViewTest"`가 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-25: Phase 8 후속 UI 변경으로 랭킹 카드 간 gap 계약을 8dp로 변경하고, 랭킹 RecyclerView에 8dp item bottom spacing을 추가했다. 1위 대형 카드는 `coverImageUrl`을 background에만 blur enabled로 로드하고 foreground `iv_content_ranking_image`는 `GONE`으로 유지하도록 변경했다. - 2026-06-25: Phase 8 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `git diff --check` 실행 결과 모두 통과했다. `./gradlew :app:ktlintCheck`는 변경 파일 `ContentRankingAdapter.kt`의 긴 inflate 라인 3곳 max line length 위반으로 중간 실패 후 줄바꿈 정리 및 재실행 결과 `BUILD SUCCESSFUL`로 통과했다. `.editorconfig disabled_rules` deprecation 경고와 Gradle deprecation 경고는 기존 설정 경고로 남아 있다. - 2026-06-25: 수동 surface 확인으로 `adb devices -l`에서 `SM_G960N` 연결을 확인하고 `./gradlew :app:installDebug`로 최신 APK 설치가 `BUILD SUCCESSFUL`로 완료되었다. `adb shell monkey -p kr.co.vividnext.sodalive.debug 1` 실행 후 `dumpsys window`에서 `kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.splash.SplashActivity` 포커스를 확인했다. 로그인/초기 진입 상태 때문에 랭킹 탭 화면까지의 실기 터치 검증은 완료하지 못했다. - 2026-06-25: Phase 8 Follow-up 최신 정정 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 순차 실행해 모두 `BUILD SUCCESSFUL`을 확인했다. `git diff --check`는 출력 없이 통과했다. `ktlintCheck`의 `.editorconfig disabled_rules` deprecation 경고와 Gradle deprecation 경고는 기존 설정 경고로 이번 변경과 무관하다.