56 KiB
메인 콘텐츠 탭 내부 랭킹 탭 구현 계획/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_00224:6857 - API endpoint는
GET /api/v2/audio/rankings이고 선택 타입은typequery 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, amount0으로 매핑한다.rankChange=5는RankingChangeType.Increase, amount5로 매핑한다.rankChange=-3은RankingChangeType.Decrease, amount3으로 매핑한다.isNew=true는rankChange보다 우선해RankingChangeType.New로 매핑한다.showRankChange=false이면 모든 rank-num 영역을GONE처리한다.rank < 1item은 표시하지 않고,contentId <= 0item은 클릭 이동을 무시한다.- 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:ktlintCheckgit 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를 기준으로 확인한다.
- Text Tab bar, Capsule Tab bar, 랭킹 목록 위치와 visibility 전환은 Figma
- Phase 5: 제한 참조
- 클릭 routing, loading/error/empty는 기존
ContentMainFragment패턴 중심으로 검증한다.
- 클릭 routing, loading/error/empty는 기존
- Phase 6: 필수 참조
- 최종 수동 화면 검증은 PRD의 모든 포함/제외 항목과 실제 화면을 대조한다.
파일 구조
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItem.ktshowRankChange표시 계약을 기본값true로 추가한다.
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.ktshowRankChange=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처리를 추가한다.
- 2~10위 grid card의 rank-num
- Modify:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt- 11위 이후 horizontal card의 rank-num
GONE처리를 추가한다.
- 11위 이후 horizontal card의 rank-num
- 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.ktshowRankChange기본값과 false 계약 테스트를 추가한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsApi.ktGET /api/v2/audio/rankings?type=...Retrofit endpoint를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsModels.ktAudioRankingResponse,AudioRankingType,AudioRankingItemResponseDTO를 정의한다.
- 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.ktLoading,Content,Empty,Error상태와 선택 type 상태를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRankingsMappers.kt- DTO를
ContentRankingItem목록으로 변환한다.
- DTO를
- 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.ktAudioRankingsApi,AudioRankingsRepository,ContentRankingViewModel을 Koin에 등록한다.
- Modify:
app/src/main/res/layout/fragment_v2_main_content.xml- Text Tab bar에
추천,랭킹,전체가 표시될 수 있도록 기존 구조를 유지하면서 랭킹 Capsule Tab bar와 RecyclerView를 추가한다.
- Text Tab bar에
- 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변환 규칙을 검증한다.
- DTO ->
- 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: 기존 구조 확인과 작업 경계 고정
-
Task 1.1: 기존 콘텐츠 추천 탭 구조 확인
- 확인:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainViewModel.ktapp/src/main/res/layout/fragment_v2_main_content.xmlapp/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
- 작업:
- 추천 탭
NestedScrollView와 section visibility 구조를 확인한다. - 랭킹 탭은 기존 추천 section을 리팩터링하지 않고 별도
RecyclerViewcontainer로 추가하는 경계로 고정한다. ContentMainViewModel은 추천 API 전용으로 유지하고, 랭킹은 신규ContentRankingViewModel로 분리한다.
- 추천 탭
- 검증:
- Run:
rg -n "ContentMainViewModel|loadRecommendations|nsv_content_recommendation_content|textTabBarContent|AudioRecommendationsApi" app/src/main/java app/src/main/res - Expected: 추천 탭 구조와 DI 등록 지점이 확인된다.
- Run:
- 검증 기록:
- 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 등록 지점을 확인했다.
- 2026-06-24:
- 확인:
-
Task 1.2: 콘텐츠 랭킹 위젯 확장 지점 확인
- 확인:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItem.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.ktapp/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참조가 확인된다.
- Run:
- 검증 기록:
- 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참조가 확인되어 확장 지점을 고정했다.
- 2026-06-24:
- 확인:
-
Task 1.3: 구현 제외 범위 재확인
- 확인:
docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md
- 제외:
- Text Tab bar
전체의 실제 content 화면/API 연동 - 레거시
AudioContentRankingAllActivity수정 - pagination, pull-to-refresh, skeleton/shimmer
- analytics/logging 추가
- 추천 탭 section 구조 리팩터링
- Text Tab bar
- 검증:
- Run:
rg -n "전체|Non-Goals|AudioContentRankingAllActivity|pagination|skeleton" docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md - Expected: 제외 범위가 PRD와 일치한다.
- Run:
- 검증 기록:
- 2026-06-24:
rg -n "전체|Non-Goals|AudioContentRankingAllActivity|pagination|skeleton" docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md실행.전체탭 API/content 제외, legacyAudioContentRankingAllActivity미수정, pagination/skeleton 제외 범위를 재확인했다.
- 2026-06-24:
- 확인:
Phase 2: contentranking 위젯 rank-num 숨김 계약 확장
-
Task 2.1:
ContentRankingItem표시 계약 테스트 추가- 수정:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItemTest.kt
- 추가 테스트:
showRankChange기본값은trueshowRankChange=false인 item은 순위 변동 표시 대상이 아님- 기존
isBlocked/isTouchable동작은 유지
- 구현 예시:
@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 실패.
- Run:
- 검증 기록:
- 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미구현 실패도 함께 확인되었다.
- 2026-06-24 RED:
- 수정:
-
Task 2.2:
ContentRankingItem에showRankChange추가- 수정:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItem.kt
- 구현:
- 기존 생성자 마지막에
val showRankChange: Boolean = true를 추가한다. - 기본값을 둬 기존 호출부 변경을 최소화한다.
- 기존 생성자 마지막에
- 코드 형태:
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.
- Run:
- 검증 기록:
- 2026-06-24 GREEN:
ContentRankingItem에showRankChange: Boolean = true를 추가한 뒤./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"실행 결과BUILD SUCCESSFUL을 확인했다.
- 2026-06-24 GREEN:
- 수정:
-
Task 2.3: rank-num
GONEview 테스트 추가- 생성:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt
- 테스트 케이스:
ContentRankingLargeCardView에showRankChange=falseitem bind 시ll_content_ranking_delta가GONEContentRankingMediumGridCardView에showRankChange=falseitem bind 시ll_content_ranking_delta가GONEContentRankingSmallGridCardView에showRankChange=falseitem bind 시ll_content_ranking_delta가GONEContentRankingHorizontalCardView에showRankChange=falseitem bind 시ll_content_ranking_delta가GONEshowRankChange=trueitem bind 시ll_content_ranking_delta가VISIBLE
- 구현 예시:
@RunWith(RobolectricTestRunner::class) class ContentRankingCardViewTest { @Test fun `large card는 showRankChange false이면 rank num을 숨긴다`() { val context = ApplicationProvider.getApplicationContext<Context>() 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<View>(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 실패.
- Run:
- 검증 기록:
- 2026-06-24:
ContentRankingCardViewTest를 추가해 large/medium grid/small grid/horizontal card의showRankChange=false시ll_content_ranking_deltaGONE,true시VISIBLE계약을 검증하도록 했다. 기존 Phase 3 RED 테스트가 전체 unit-test 컴파일을 막아 card 테스트 단독 RED는 별도 분리 실행하지 못했고, 구현 후 widget 패키지 검증에서 함께 GREEN을 확인했다.
- 2026-06-24:
- 생성:
-
Task 2.4: rank-num
GONE처리 구현- 수정:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.ktapp/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 적용을 유지한다.
- 각
- 코드 형태:
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.
- Run:
- 검증 기록:
- 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.
- 2026-06-24:
- 수정:
Phase 3: 랭킹 API/DTO/mapper 작성
-
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은
주간 인기,지금 뜨는 중,매출,판매량,댓글수,좋아요순서
- 구현 예시:
@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 실패.
- Run:
- 검증 기록:
- 2026-06-24 RED:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRankingTypeTest"실행 시Unresolved reference 'AudioRankingType'로 실패해 type 미구현 상태를 확인했다.
- 2026-06-24 RED:
- 생성:
-
Task 3.2: mapper RED 테스트 작성
- 생성:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRankingsMapperTest.kt
- 테스트 케이스:
- 응답 item을
rank오름차순으로 정렬한다. rank < 1item은 제외한다.isNew=true는RankingChangeType.New, amount0으로 매핑한다.rankChange=null또는0은RankingChangeType.Stay, amount0으로 매핑한다.rankChange=5는RankingChangeType.Increase, amount5로 매핑한다.rankChange=-3은RankingChangeType.Decrease, amount3으로 매핑한다.showRankChange=false이면 모든ContentRankingItem.showRankChange=false로 매핑한다.coverImageUrl=null은imageUrl=""로 매핑한다.creatorId는 API에 없으므로ContentRankingItem.creatorId=""로 매핑한다.isBlocked=false로 매핑한다.
- 응답 item을
- 구현 예시:
@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 실패.
- Run:
- 검증 기록:
- 2026-06-24:
AudioRankingsMapperTest를 추가해 rank 정렬/필터,isNew우선,rankChangenull/0/양수/음수,showRankChange=false,coverImageUrl=null,creatorId="",isBlocked=false매핑 계약을 검증하도록 했다. 직전AudioRankingTypeTestRED에서 DTO/type 미구현 컴파일 실패를 확인한 뒤 같은 미구현 상태를 전제로 작성했다.
- 2026-06-24:
- 생성:
-
Task 3.3: API DTO와 mapper 구현
- 생성:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsModels.ktapp/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<ContentRankingItem>를 추가한다.fun AudioRankingType.labelResId(): Int또는label(context)helper는 UI 연결 Phase에서 문자열 리소스와 함께 확정한다.
- 코드 형태:
@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.
- Run:
- 검증 기록:
- 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을 추가했다.
- 2026-06-24:
- 생성:
-
Task 3.4: API/Repository 작성
- 생성:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsApi.ktapp/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<ApiResponse<AudioRankingResponse>>
- 코드 형태:
interface AudioRankingsApi { @GET("/api/v2/audio/rankings") fun getRankings( @Header("Authorization") authHeader: String, @Query("type") type: AudioRankingType ): Single<ApiResponse<AudioRankingResponse>> } - 검증:
- Run:
./gradlew :app:compileDebugKotlin - Expected: 신규 API/Repository 컴파일 성공.
- Run:
- 검증 기록:
- 2026-06-24:
AudioRankingsApi.kt,AudioRankingsRepository.kt를 추가했다../gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"에서 신규 API/Repository를 포함한 debug/test 컴파일과 관련 테스트BUILD SUCCESSFUL을 확인했다.
- 2026-06-24:
- 생성:
Phase 4: ViewModel과 DI 등록
-
Task 4.1: UI state와 ViewModel 작성
- 생성:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRankingsUiState.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentRankingViewModel.kt
- 구현:
AudioRankingsUiState.LoadingAudioRankingsUiState.Content(val type: AudioRankingType, val items: List<ContentRankingItem>)AudioRankingsUiState.Empty(val type: AudioRankingType)AudioRankingsUiState.Error(val type: AudioRankingType, val message: String?)rankingStateLiveData,toastLiveData,isLoading,selectedTypeLiveDataloadRankings(type: AudioRankingType, force: Boolean = false)- 같은 type을 이미 성공 로드했고
force=false이면 중복 호출하지 않는다. - 응답
type이 현재 선택 type과 다르면 화면 반영을 무시한다. - 기존
ContentMainViewModel과 동일하게SharedPreferenceManager.token, RxJava scheduler, unknown error toast 패턴을 사용한다.
- 검증:
- Run:
./gradlew :app:compileDebugKotlin - Expected: ViewModel 컴파일 성공.
- Run:
- 검증 기록:
- 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:146assertion 실패로 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:193assertion 실패로 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.
- 2026-06-24 RED:
- 생성:
-
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()) }
- import 추가:
- 검증:
- Run:
./gradlew :app:compileDebugKotlin - Expected: Koin 등록과 import 컴파일 성공.
- Run:
- 검증 기록:
- 2026-06-24:
AppDI.kt에AudioRankingsApi,AudioRankingsRepository,ContentRankingViewModel등록을 추가했다../gradlew :app:compileDebugKotlin실행 결과BUILD SUCCESSFUL.
- 2026-06-24:
- 수정:
Phase 5: 레이아웃과 탭 전환 UI 연결
-
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가 존재한다.ContentMainFragmentsource에ContentRankingViewModel by viewModel()이 존재한다.ContentMainFragmentsource에ContentRankingAdapter.createGridLayoutManager(requireContext())가 존재한다.ContentMainFragmentsource에AudioRankingType.WEEKLY_POPULAR초기 로드가 존재한다.ContentMainFragmentsource에 Text Tab 메뉴추천,랭킹,전체설정이 존재한다.
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest" - Expected: layout id/ViewModel/adapter 연결 미구현으로 RED 실패.
- Run:
- 검증 기록:
- 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.
- 2026-06-24 RED:
- 수정:
-
Task 5.2: strings 추가
- 수정:
app/src/main/res/values/strings.xmlapp/src/main/res/values-en/strings.xmlapp/src/main/res/values-ja/strings.xml
- 구현:
screen_content_tab_recommendationscreen_content_tab_rankingscreen_content_tab_allscreen_content_ranking_type_weekly_popularscreen_content_ranking_type_risingscreen_content_ranking_type_revenuescreen_content_ranking_type_sales_countscreen_content_ranking_type_comment_countscreen_content_ranking_type_like_count
- 검증:
- Run:
./gradlew :app:mergeDebugResources - Expected: 문자열 리소스 merge 성공.
- Run:
- 검증 기록:
- 2026-06-24:
values,values-en,values-ja에 콘텐츠 Text Tab랭킹/전체와 랭킹 유형 6개 라벨 문자열을 추가했다../gradlew :app:mergeDebugResources실행 결과BUILD SUCCESSFUL.
- 2026-06-24:
- 수정:
-
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 형태:
<include android:id="@+id/view_content_ranking_type_tabs" layout="@layout/view_capsule_tab_bar" android:layout_width="0dp" android:layout_height="52dp" android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/text_tab_bar_content" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_content_rankings" android:layout_width="0dp" android:layout_height="0dp" android:clipToPadding="false" android:paddingHorizontal="@dimen/spacing_14" android:paddingTop="@dimen/spacing_14" android:paddingBottom="@dimen/spacing_28" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/view_content_ranking_type_tabs" /> - 검증:
- Run:
./gradlew :app:mergeDebugResources - Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest" - Expected: resource merge 성공, source test는 Fragment 연결 전 일부 RED 유지 가능.
- Run:
- 검증 기록:
- 2026-06-24:
fragment_v2_main_content.xml의text_tab_bar_content아래에view_content_ranking_type_tabsinclude와rv_content_rankings를 초기gone상태로 추가했다../gradlew :app:mergeDebugResources실행 결과BUILD SUCCESSFUL,ContentMainFragmentSourceTest에서 layout id/source assertionBUILD SUCCESSFUL.
- 2026-06-24:
- 수정:
-
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 = 0CONTENT_TAB_RANKING = 1CONTENT_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.
- Run:
- 검증 기록:
- 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.
- 2026-06-24:
- 수정:
-
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 연결 컴파일 성공.
- Run:
- 검증 기록:
- 2026-06-24:
view_content_ranking_type_tabs.root.setMenus(...)에AudioRankingType.entries순서대로 6개 문자열 리소스 라벨을 설정하고,setOnTabSelectedListener에서AudioRankingType.entries[index]를loadRankings(type)로 연결했다../gradlew :app:compileDebugKotlin실행 결과BUILD SUCCESSFUL.
- 2026-06-24:
- 수정:
-
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 = contentRankingAdapterrankingStateLiveData.observe(viewLifecycleOwner)에서Content이면submitItems(state.items),Empty/Error이면submitItems(emptyList())isLoading은 기존LoadingDialog와 충돌하지 않도록 추천/랭킹 ViewModel의 loading 상태를 모두 관찰하거나, 랭킹 loading만 별도 observer에서 같은 dialog를 show/dismiss한다.toastLiveData는 기존showToasthelper를 재사용한다.
- 검증:
- Run:
./gradlew :app:compileDebugKotlin - Expected: Fragment/adapter/ViewModel 연결 컴파일 성공.
- Run:
- 검증 기록:
- 2026-06-24:
rv_content_rankings에ContentRankingAdapter.createGridLayoutManager(requireContext())와ContentRankingAdapter를 연결하고,rankingStateLiveData의Content는submitItems(state.items),Empty/Error는submitItems(emptyList())로 처리했다. 추천/랭킹 loading 상태는updateLoadingDialog()에서 함께 반영하고, rankingtoastLiveData는 기존showToasthelper를 재사용했다../gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*",./gradlew :app:compileDebugKotlin실행 결과 모두BUILD SUCCESSFUL.
- 2026-06-24:
- 수정:
Phase 6: routing, 통합 검증, 문서 검증 기록
-
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이면 아무 동작하지 않는다.
- 코드 형태:
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.
- Run:
- 검증 기록:
- 2026-06-24 RED:
ContentMainFragmentSourceTest에 랭킹 routing guard source assertion을 추가한 뒤./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"실행 시ContentMainFragmentSourceTest.kt:97assertion 실패로openRankingAudioContentDetail내부의 명시적contentId > 0guard 미반영 상태를 확인했다. - 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 } ?: returnguard가 PRD의contentId <= 0클릭 무시 요구와 일치함을 재확인했다.
- 2026-06-24 RED:
- 수정:
-
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.
- Run:
- 검증 기록:
- 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.
- 2026-06-24:
- 검증:
-
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 없음
- Run:
- 검증 기록:
- 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_rulesdeprecation 경고는 기존 설정 경고로 남아 있다. - 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실행 결과 모두 통과했다.
- 2026-06-24:
- 검증:
-
Task 6.4: 수동 화면 검증 [blocked]
- 확인:
- 메인 콘텐츠 탭 진입 시 기본
추천탭 화면이 기존처럼 표시된다. - Text Tab bar에
추천,랭킹,전체이 보인다. 랭킹선택 시 Capsule Tab bar 6개 항목이 Figma 순서로 보인다.- 초기 선택은
주간 인기이고 API는type=WEEKLY_POPULAR로 호출된다. - 랭킹 목록은 1위 대형, 2
7위 2열, 810위 3열, 11위 이후 가로형으로 표시된다. showRankChange=false응답에서 rank-num 영역이 보이지 않는다.contentId > 0item 터치 시 오디오 상세로 이동한다.전체탭은 항목만 표시하고 별도 API/content 구현이 없다.
- 메인 콘텐츠 탭 진입 시 기본
- 검증 기록:
- 2026-06-24: Figma
24:6857design context와 screenshot으로 Text Tab추천/랭킹/전체, Capsule Tab주간 인기/지금 뜨는 중/매출/판매량/댓글수/좋아요순서, 랭킹 목록의 1위 대형/27위 2열/810위 3열/11위 이후 가로형 배치를 대조했다. - 2026-06-24: source/test 기준으로
랭킹최초 선택 시AudioRankingType.WEEKLY_POPULAR로드,ContentRankingAdaptermixed layout 연결,showRankChange=falserank-numGONE,contentId > 0routing과 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:6857design context와 screenshot을 다시 확인했다. 최신 APK가 설치된 device/emulator 기반 터치 검증은 계속 blocked 상태로 유지한다.
- 2026-06-24: Figma
- 확인:
Phase 7: rank-num 간격 시각 보정
-
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 영역에 붙어 보이지 않도록 검증한다.
- Large, Medium grid, Small grid, Horizontal variant의 순위
- 검증 명령:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingCardViewTest" - 기대 결과: production 수정 전 padding assertion이 실패한다.
- 수정:
-
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로 전환된다.
- 수정:
-
Task 7.3: 후속 검증과 문서 기록 누적
- 실행:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"./gradlew :app:mergeDebugResources./gradlew :app:ktlintCheckgit diff --check
- 수정:
docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/prd.md - 수정:
docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md - 기대 결과: 모두 통과, Verification Log 누적.
- 실행:
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:ktlintCheck1차 실행에서 변경 파일의 기존 긴LayoutParams라인이 ktlint max line length를 위반해 실패했고, 기능 변경 없이 줄바꿈만 정리한 뒤 재실행 결과BUILD SUCCESSFUL..editorconfig의disabled_rulesdeprecation 경고는 기존 설정 경고로 남아 있다. - 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 구조 확인용
rg3개 명령에서 기대 참조를 확인했고,./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_rulesdeprecation 경고는 기존 설정 경고로 남아 있다. - 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_rulesdeprecation 경고는 기존 설정 경고로 남아 있다. - 2026-06-24: Phase 4~5 현재 워크트리 기준 코드 리뷰 및 재검증을 수행했다.
ContentRankingViewModel의 캐시 재방출, stale response 무시, loading 종료 조건과ContentMainFragment의 Text Tab/Capsule Tab 연결, visibility 전환, ranking adapter 연결, loading dialog 통합, routing guard를 확인했고 추가 결함은 발견하지 못했다. Figma24:6857,24:6882design 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:97assertion 실패로 확인했고, 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_rulesdeprecation 경고는 기존 설정 경고로 남아 있다. - 2026-06-24: Phase 6 수동 검증 중 Figma
24:6857design 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 코드 리뷰 및 재검증을 수행했다.
ContentMainFragmentrouting guard,ContentRankingViewModelstale response/loading 처리, mapper의 rank 필터/정렬과showRankChange전달, widget rank-numGONE처리를 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, 27위 6px, 810위 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_contentpackage-name, 기존ContentRankingAdapter.kt긴 줄 등)으로 실패했으며,git diff --check는 출력 없이 통과했다. - 2026-06-25: 리뷰 게이트에서 콘텐츠 랭킹 horizontal rank
TextView가 고정 박스 없이 bottom padding만 적용되어 rank-num 위치가 밀릴 수 있고, 콘텐츠 rankTextView의 중앙 정렬 계약이 부족하다는 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로 통과했다.