28 KiB
콘텐츠 랭킹 위젯 컴포넌트 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Figma 20:3715, 20:3718, 20:3721, 20:3724 기준으로 순위 구간별 콘텐츠 랭킹 위젯 컴포넌트를 추가한다.
Architecture: 랭킹 항목의 순위/차단 관계/텍스트 표시 상태를 순수 Kotlin contract로 먼저 분리하고, 순위 변동 타입은 크리에이터 랭킹과 콘텐츠 랭킹이 공유하는 공용 contract로 둔다. Android custom view와 RecyclerView adapter가 이 contract를 바인딩한다. 카드 UI는 Large, MediumGrid, SmallGrid, Horizontal 4개 variant로 나누며, 실제 크기는 row count와 부모 폭으로 계산한다.
Tech Stack: Android XML Views, Kotlin custom View, RecyclerView, ViewBinding/resource merge, JUnit4 local unit test.
작업 목표
- 1위는
Large전용 카드로 구현한다. - 2위~7위는
MediumGrid카드로 구현하고 한 줄 2개로 배치한다. - 8위~10위는
SmallGrid카드로 구현하고 한 줄 3개로 배치한다. - 11위 이후는
Horizontal카드로 구현한다. - 콘텐츠명은 순위 구간별 글자 수 제한과 한 줄 말줄임을 적용한다.
- rank delta 상태에 따라 상승/하락/동일/신규 진입 UI를 표시한다.
- 차단 관계인 크리에이터의 콘텐츠는 이미지 블러, 정보 비노출 또는 대체문구, 터치 불가 상태로 표시한다.
- 이미지 크기는 고정하지 않고 row container 폭과 row count로 계산한다.
파일 구조
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItem.kt- 콘텐츠 랭킹 UI에 필요한 순수 데이터 모델과 차단 관계 상태 계산을 정의한다.
- Rename/Move: creator ranking에서 사용하는 change type ->
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/ranking/RankingChangeType.ktIncrease,Decrease,Stay,New순위 변동 타입을 크리에이터/콘텐츠 랭킹 공용 타입으로 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardVariant.ktLarge,MediumGrid,SmallGrid,Horizontal카드 UI variant를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingPlacement.kt- rank 기준 variant와 row count를 함께 결정한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLayoutCalculator.kt- 부모 폭, horizontal gap, row count 기준으로 item width/height를 계산한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingDeltaPresentation.kt- 공용
RankingChangeType별 아이콘과 숫자 표시 여부를 정의한다.Stay와New는 숫자를 표시하지 않는다.
- 공용
- Create:
app/src/main/res/layout/view_content_ranking_large_card.xml- 1위 전용 큰 카드 layout을 정의한다.
- Create:
app/src/main/res/layout/view_content_ranking_medium_grid_card.xml- 2위~7위 2열 카드 layout을 정의한다.
- Create:
app/src/main/res/layout/view_content_ranking_small_grid_card.xml- 8위~10위 3열 카드 layout을 정의한다.
- Create:
app/src/main/res/layout/view_content_ranking_horizontal_card.xml- 11위 이후 가로형 카드 layout을 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt- 1위 전용 rank, delta, 콘텐츠명, 크리에이터명, access state를 바인딩한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingMediumGridCardView.kt- 2위~7위 rank, delta, 콘텐츠명, 크리에이터명, access state를 바인딩한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingSmallGridCardView.kt- 8위~10위 rank, delta, 콘텐츠명, 크리에이터명, access state를 바인딩한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt- 11위 이후 가로형 카드의 rank, delta, 콘텐츠명, 크리에이터명, access state를 바인딩한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt- rank별 viewType과 터치 가능 여부를 처리한다.
- Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingPlacementTest.kt- rank별 variant/row count 계약을 검증한다.
- Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItemTest.kt- 차단 관계 상태와 표시 텍스트 정책을 검증한다.
- Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLayoutCalculatorTest.kt- 부모 폭 기반 크기 계산을 검증한다.
- Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingDeltaPresentationTest.kt- 상승/하락/동일/신규 진입 표시 계약을 검증한다.
- Modify:
app/src/main/res/values/strings.xml- 접근 불가 대체문구
접근할 수 없는 정보입니다.를 추가한다.
- 접근 불가 대체문구
- Modify:
app/src/main/res/values-en/strings.xml,app/src/main/res/values-ja/strings.xml- 기존 다국어 정책에 맞춰 접근 불가 대체문구를 추가한다.
- Add if missing:
app/src/main/res/drawable/ic_rank_caret_increase.xml,ic_rank_caret_decrease.xml,ic_rank_caret_stay.xml,ic_rank_new.xml- Figma 에셋이 프로젝트에 없으면 디자인 에셋을 추가한다.
- Modify:
docs/plan-task/20260520_콘텐츠랭킹위젯컴포넌트.md- 구현 중 체크박스와 검증 기록을 누적한다.
구현 계획
Task 1: 기존 리소스 및 유사 UI 확인
Files:
-
Read:
docs/prd/20260520_콘텐츠랭킹위젯컴포넌트_prd.md -
Read:
docs/prd/20260520_크리에이터랭킹위젯컴포넌트_prd.md -
Read:
app/src/main/java/kr/co/vividnext/sodalive/common/image/BlurTransformation.kt -
Read:
app/src/main/res/values/colors.xml -
Read:
app/src/main/res/values/dimens.xml -
Read:
app/src/main/res/values/typography.xml -
Step 1: 현재 랭킹/블러/에셋 사용처 확인
Run: rg -n "ContentRanking|CreatorRanking|ic_rank_caret|ic_rank_new|BlurTransformation|rank" app/src/main app/src/test docs
Expected: 기존 랭킹 contract, blur 구현, rank 이미지 리소스 사용처를 확인한다.
- Step 2: Figma 세부 컨텍스트 재확인
Run tools:
Figma_get_design_context(20:3715)Figma_get_design_context(20:3718)Figma_get_design_context(20:3721)Figma_get_design_context(20:3724)
Expected: typography, color, radius, spacing, icon asset name을 확인한다. 도구가 timeout이면 현재 PRD의 screenshot 기준으로 진행하고 검증 기록에 남긴다.
- Step 3: Figma token을 구현 기준으로 정리
Expected token contract:
- 공통 카드 image radius:
radius_14또는14dp - 공통 dim gradient: top transparent, bottom black, opacity
50%, transition start64.423% - 공통
rank-num: backgroundgray_900(#202020), radius4dp, horizontal padding4dp, gap2dp - 공통
rank-num숫자: Pretendard Variable Medium,16sp, line-height1.45, white - 공통 caret icon:
14dp x 14dp - 순위 숫자: Pattaya Regular, white~
#EEEEEEgradient,0px 0px 4px rgba(0,0,0,0.48)shadow Largetitle: Pretendard Variable Bold,22sp, line-height1.45, whiteLargecreator: Pretendard Variable Regular,12sp, line-height normal, whiteMediumGridtitle: Pretendard Variable Bold,22sp, line-height1.45, whiteMediumGridcreator: Pretendard Variable Regular,12sp, line-height normal, whiteSmallGridtitle: Pretendard Variable Bold,14sp, line-height normal, whiteSmallGridcreator: Pretendard Variable Regular,12sp, line-height normal, whiteHorizontaltitle: Pretendard Variable Bold,18sp, line-height1.45, whiteHorizontalcreator: Pretendard Variable Regular,14sp, line-height1.45, white
Task 2: Rank placement contract TDD
Files:
-
Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingPlacementTest.kt -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardVariant.kt -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingPlacement.kt -
Step 1: RED - rank별 variant와 row count 테스트 추가
package kr.co.vividnext.sodalive.v2.widget.contentranking
import org.junit.Assert.assertEquals
import org.junit.Test
class ContentRankingPlacementTest {
@Test
fun `rank 1 uses large variant and one item row`() {
val placement = ContentRankingPlacement.fromRank(1)
assertEquals(ContentRankingCardVariant.Large, placement.variant)
assertEquals(1, placement.itemsPerRow)
}
@Test
fun `rank 2 to 7 uses medium grid variant and two item row`() {
(2..7).forEach { rank ->
val placement = ContentRankingPlacement.fromRank(rank)
assertEquals(ContentRankingCardVariant.MediumGrid, placement.variant)
assertEquals(2, placement.itemsPerRow)
}
}
@Test
fun `rank 8 to 10 uses small grid variant and three item row`() {
(8..10).forEach { rank ->
val placement = ContentRankingPlacement.fromRank(rank)
assertEquals(ContentRankingCardVariant.SmallGrid, placement.variant)
assertEquals(3, placement.itemsPerRow)
}
}
@Test
fun `rank 11 or greater uses horizontal variant and one item row`() {
listOf(11, 12, 100).forEach { rank ->
val placement = ContentRankingPlacement.fromRank(rank)
assertEquals(ContentRankingCardVariant.Horizontal, placement.variant)
assertEquals(1, placement.itemsPerRow)
}
}
@Test(expected = IllegalArgumentException::class)
fun `rank less than 1 is invalid`() {
ContentRankingPlacement.fromRank(0)
}
}
- Step 2: RED 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingPlacementTest"
Expected: Unresolved reference 'ContentRankingPlacement'로 실패한다.
- Step 3: GREEN - 최소 placement contract 추가
package kr.co.vividnext.sodalive.v2.widget.contentranking
enum class ContentRankingCardVariant {
Large,
MediumGrid,
SmallGrid,
Horizontal
}
package kr.co.vividnext.sodalive.v2.widget.contentranking
data class ContentRankingPlacement(
val variant: ContentRankingCardVariant,
val itemsPerRow: Int
) {
companion object {
fun fromRank(rank: Int): ContentRankingPlacement {
require(rank >= 1) { "rank must be greater than or equal to 1." }
return when (rank) {
1 -> ContentRankingPlacement(ContentRankingCardVariant.Large, itemsPerRow = 1)
in 2..7 -> ContentRankingPlacement(ContentRankingCardVariant.MediumGrid, itemsPerRow = 2)
in 8..10 -> ContentRankingPlacement(ContentRankingCardVariant.SmallGrid, itemsPerRow = 3)
else -> ContentRankingPlacement(ContentRankingCardVariant.Horizontal, itemsPerRow = 1)
}
}
}
}
- Step 4: GREEN 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingPlacementTest"
Expected: BUILD SUCCESSFUL
Task 3: Ranking item state contract TDD
Files:
-
Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItemTest.kt -
Rename/Move: creator ranking에서 사용하는 change type ->
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/ranking/RankingChangeType.kt -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingItem.kt -
Step 1: RED - 접근 가능/불가 및 텍스트 표시 정책 테스트 추가
package kr.co.vividnext.sodalive.v2.widget.contentranking
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ContentRankingItemTest {
@Test
fun `blocked item is inaccessible`() {
val item = sampleItem(isBlocked = true)
assertTrue(item.isInaccessible)
assertFalse(item.isTouchable)
}
@Test
fun `accessible item is touchable`() {
val item = sampleItem()
assertFalse(item.isInaccessible)
assertTrue(item.isTouchable)
}
@Test
fun `top ten blocked item hides content and creator names`() {
val item = sampleItem(rank = 10, isBlocked = true)
assertEquals("", item.displayContentName(inaccessibleMessage = "접근할 수 없는 정보입니다."))
assertEquals("", item.displayCreatorName())
}
@Test
fun `rank 11 blocked item shows inaccessible message as single line`() {
val item = sampleItem(rank = 11, isBlocked = true)
assertEquals("접근할 수 없는 정보입니다.", item.displayContentName(inaccessibleMessage = "접근할 수 없는 정보입니다."))
assertEquals("", item.displayCreatorName())
}
@Test
fun `accessible item shows original names`() {
val item = sampleItem(contentName = "콘텐츠 이름", creatorName = "크리에이터 이름")
assertEquals("콘텐츠 이름", item.displayContentName(inaccessibleMessage = "접근할 수 없는 정보입니다."))
assertEquals("크리에이터 이름", item.displayCreatorName())
}
@Test
fun `content title max length follows rank range`() {
assertEquals(16, sampleItem(rank = 1).contentNameMaxLength)
assertEquals(8, sampleItem(rank = 2).contentNameMaxLength)
assertEquals(8, sampleItem(rank = 10).contentNameMaxLength)
assertEquals(12, sampleItem(rank = 11).contentNameMaxLength)
}
private fun sampleItem(
rank: Int = 1,
contentName: String = "콘텐츠 이름",
creatorName: String = "크리에이터 이름",
isBlocked: Boolean = false
) = ContentRankingItem(
contentId = "content-1",
creatorId = "creator-1",
rank = rank,
previousRank = 5,
rankChangeType = RankingChangeType.Increase,
rankChangeAmount = 4,
contentName = contentName,
creatorName = creatorName,
imageUrl = "https://example.com/image.png",
isBlocked = isBlocked
)
}
- Step 2: RED 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItemTest"
Expected: Unresolved reference 'ContentRankingItem'로 실패한다.
- Step 3: GREEN - 공용 ranking change type과 최소 item contract 추가
package kr.co.vividnext.sodalive.v2.widget.ranking
enum class RankingChangeType {
Increase,
Decrease,
Stay,
New
}
package kr.co.vividnext.sodalive.v2.widget.contentranking
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
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 isInaccessible: Boolean = isBlocked
val isTouchable: Boolean = !isBlocked
val contentNameMaxLength: Int = when (rank) {
1 -> 16
in 2..10 -> 8
else -> 12
}
fun displayContentName(inaccessibleMessage: String): String = when {
!isBlocked -> contentName
rank <= 10 -> ""
else -> inaccessibleMessage
}
fun displayCreatorName(): String = if (isBlocked) "" else creatorName
}
- Step 4: GREEN 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItemTest"
Expected: BUILD SUCCESSFUL
Task 4: Rank delta presentation contract TDD
Files:
-
Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingDeltaPresentationTest.kt -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingDeltaPresentation.kt -
Step 1: RED - 변동 상태별 표시 테스트 추가
package kr.co.vividnext.sodalive.v2.widget.contentranking
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ContentRankingDeltaPresentationTest {
@Test
fun `increase shows amount and increase caret`() {
val presentation = ContentRankingDeltaPresentation.from(RankingChangeType.Increase, amount = 4)
assertEquals(R.drawable.ic_rank_caret_increase, presentation.iconRes)
assertEquals("4", presentation.amountText)
assertTrue(presentation.showAmount)
assertFalse(presentation.replaceWithNewIcon)
}
@Test
fun `decrease shows amount and decrease caret`() {
val presentation = ContentRankingDeltaPresentation.from(RankingChangeType.Decrease, amount = 2)
assertEquals(R.drawable.ic_rank_caret_decrease, presentation.iconRes)
assertEquals("2", presentation.amountText)
assertTrue(presentation.showAmount)
assertFalse(presentation.replaceWithNewIcon)
}
@Test
fun `stay shows only stay icon`() {
val presentation = ContentRankingDeltaPresentation.from(RankingChangeType.Stay, amount = 0)
assertEquals(R.drawable.ic_rank_caret_stay, presentation.iconRes)
assertEquals("", presentation.amountText)
assertFalse(presentation.showAmount)
assertFalse(presentation.replaceWithNewIcon)
}
@Test
fun `new replaces rank num with new icon`() {
val presentation = ContentRankingDeltaPresentation.from(RankingChangeType.New, amount = null)
assertEquals(R.drawable.ic_rank_new, presentation.iconRes)
assertEquals("", presentation.amountText)
assertFalse(presentation.showAmount)
assertTrue(presentation.replaceWithNewIcon)
}
}
- Step 2: RED 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingDeltaPresentationTest"
Expected: Unresolved reference 'ContentRankingDeltaPresentation' 또는 missing drawable reference로 실패한다.
- Step 3: GREEN - delta presentation 추가
package kr.co.vividnext.sodalive.v2.widget.contentranking
import androidx.annotation.DrawableRes
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
data class ContentRankingDeltaPresentation(
@DrawableRes val iconRes: Int,
val amountText: String,
val showAmount: Boolean,
val replaceWithNewIcon: Boolean
) {
companion object {
fun from(type: RankingChangeType, amount: Int?): ContentRankingDeltaPresentation = when (type) {
RankingChangeType.Increase -> ContentRankingDeltaPresentation(
iconRes = R.drawable.ic_rank_caret_increase,
amountText = requireNotNull(amount).toString(),
showAmount = true,
replaceWithNewIcon = false
)
RankingChangeType.Decrease -> ContentRankingDeltaPresentation(
iconRes = R.drawable.ic_rank_caret_decrease,
amountText = requireNotNull(amount).toString(),
showAmount = true,
replaceWithNewIcon = false
)
RankingChangeType.Stay -> ContentRankingDeltaPresentation(
iconRes = R.drawable.ic_rank_caret_stay,
amountText = "",
showAmount = false,
replaceWithNewIcon = false
)
RankingChangeType.New -> ContentRankingDeltaPresentation(
iconRes = R.drawable.ic_rank_new,
amountText = "",
showAmount = false,
replaceWithNewIcon = true
)
}
}
}
- Step 4: GREEN 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingDeltaPresentationTest"
Expected: BUILD SUCCESSFUL
Task 5: Layout calculator contract TDD
Files:
-
Create:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLayoutCalculatorTest.kt -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLayoutCalculator.kt -
Step 1: RED - 부모 폭 기반 크기 계산 테스트 추가
package kr.co.vividnext.sodalive.v2.widget.contentranking
import org.junit.Assert.assertEquals
import org.junit.Test
class ContentRankingLayoutCalculatorTest {
@Test
fun `square item width divides available width by items per row`() {
val size = ContentRankingLayoutCalculator.calculateSquareItemSize(
parentWidthPx = 374,
horizontalGapPx = 4,
itemsPerRow = 2
)
assertEquals(185, size.widthPx)
assertEquals(185, size.heightPx)
}
@Test
fun `three column square item subtracts two gaps`() {
val size = ContentRankingLayoutCalculator.calculateSquareItemSize(
parentWidthPx = 374,
horizontalGapPx = 4,
itemsPerRow = 3
)
assertEquals(122, size.widthPx)
assertEquals(122, size.heightPx)
}
@Test
fun `horizontal item keeps figma ratio`() {
val size = ContentRankingLayoutCalculator.calculateHorizontalItemSize(parentWidthPx = 374)
assertEquals(374, size.widthPx)
assertEquals(100, size.heightPx)
}
}
- Step 2: RED 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingLayoutCalculatorTest"
Expected: Unresolved reference 'ContentRankingLayoutCalculator'로 실패한다.
- Step 3: GREEN - 최소 layout calculator 추가
package kr.co.vividnext.sodalive.v2.widget.contentranking
data class ContentRankingItemSize(
val widthPx: Int,
val heightPx: Int
)
object ContentRankingLayoutCalculator {
fun calculateSquareItemSize(parentWidthPx: Int, horizontalGapPx: Int, itemsPerRow: Int): ContentRankingItemSize {
require(parentWidthPx > 0) { "parentWidthPx must be greater than 0." }
require(horizontalGapPx >= 0) { "horizontalGapPx must be greater than or equal to 0." }
require(itemsPerRow > 0) { "itemsPerRow must be greater than 0." }
val totalGap = horizontalGapPx * (itemsPerRow - 1)
val size = (parentWidthPx - totalGap) / itemsPerRow
return ContentRankingItemSize(widthPx = size, heightPx = size)
}
fun calculateHorizontalItemSize(parentWidthPx: Int): ContentRankingItemSize {
require(parentWidthPx > 0) { "parentWidthPx must be greater than 0." }
val height = (parentWidthPx * 100f / 374f).toInt()
return ContentRankingItemSize(widthPx = parentWidthPx, heightPx = height)
}
}
- Step 4: GREEN 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingLayoutCalculatorTest"
Expected: BUILD SUCCESSFUL
Task 6: XML 카드 레이아웃과 custom view 추가
Files:
-
Create:
app/src/main/res/layout/view_content_ranking_large_card.xml -
Create:
app/src/main/res/layout/view_content_ranking_medium_grid_card.xml -
Create:
app/src/main/res/layout/view_content_ranking_small_grid_card.xml -
Create:
app/src/main/res/layout/view_content_ranking_horizontal_card.xml -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingMediumGridCardView.kt -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingSmallGridCardView.kt -
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt -
Modify:
app/src/main/res/values/strings.xml -
Modify:
app/src/main/res/values-en/strings.xml -
Modify:
app/src/main/res/values-ja/strings.xml -
Step 1: 접근 불가 문자열 추가
Expected:
-
strings.xml:<string name="content_ranking_inaccessible_info">접근할 수 없는 정보입니다.</string> -
values-en/strings.xml: 동일 의미의 영문 문자열을 추가한다. -
values-ja/strings.xml: 동일 의미의 일본어 문자열을 추가한다. -
Step 2: 4개 XML layout 추가
Expected:
-
모든 title/creator
TextView는android:maxLines="1",android:ellipsize="end"를 가진다. -
1위~10위 layout은 이름 영역을
gone으로 전환해도 dim gradient view가 남도록 overlay와 label container를 분리한다. -
11위 이후 layout은 접근 불가 상태에서 단일
TextView만 표시할 수 있도록 title/creator 영역과 inaccessible message 영역을 분리한다. -
이미지 view는
centerCrop, radius14dp, blur 적용 가능 구조를 가진다. -
Step 3: 4개 custom view 추가
Expected:
-
각 custom view는
bind(item: ContentRankingItem)API를 제공한다. -
각 custom view는
ContentRankingDeltaPresentation을 사용해rank-num또는ic_rank_new를 표시한다. -
각 custom view는
item.isBlocked일 때 이미지 blur와 터치 불가 상태를 적용한다. -
1위~10위 custom view는 차단 상태에서 콘텐츠명/크리에이터명 영역만 숨기고 gradient는 유지한다.
-
11위 이후 custom view는 차단 상태에서
content_ranking_inaccessible_info만 한 줄로 표시한다. -
Step 4: resource merge 확인
Run: ./gradlew :app:assembleDebug
Expected: BUILD SUCCESSFUL
Task 7: Adapter 추가 및 통합 계약 검증
Files:
-
Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt -
Modify:
docs/plan-task/20260520_콘텐츠랭킹위젯컴포넌트.md -
Step 1: Adapter 추가
Expected:
-
getItemViewType은ContentRankingPlacement.fromRank(item.rank).variant를 기준으로 view type을 반환한다. -
onCreateViewHolder는 4개 custom view 중 하나를 생성한다. -
onBindViewHolder는 item을 bind하고,item.isTouchable이 false이면 click listener를 제거한다. -
외부 클릭 동작은
onItemClick: (ContentRankingItem) -> Unit형태로 호출부에 위임한다. -
Step 2: 단위 테스트 전체 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"
Expected: BUILD SUCCESSFUL
- Step 3: 관련 빌드 실행
Run: ./gradlew :app:assembleDebug
Expected: BUILD SUCCESSFUL
- Step 4: 계획 문서 검증 기록 누적
Expected:
- 실행한 명령, 결과, 실패 시 원인과 후속 조치를 이 문서 하단
검증 기록에 누적한다.
검증 기록
- 2026-05-20: 문서만 먼저 작성하는 요청이므로 구현/빌드/테스트는 실행하지 않았다. Figma
20:3715,20:3718,20:3721,20:3724의 design context와 screenshot을 확인해 PRD 및 구현 계획에 반영했다. - 2026-05-20:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingItemTest" --tests "kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingDeltaPresentationTest" --tests "kr.co.vividnext.sodalive.v2.widget.contentranking.*"를 먼저 실행해RankingChangeType및 콘텐츠 랭킹 contract 미구현으로 실패하는 RED를 확인했다. - 2026-05-20: 공용
RankingChangeType, 콘텐츠 랭킹 placement/item/delta/layout calculator, XML/custom view/adapter를 구현한 뒤 동일 단위 테스트 명령을 재실행해BUILD SUCCESSFUL을 확인했다. - 2026-05-20:
./gradlew :app:assembleDebug를 실행해 Android resource merge, Kotlin compile, debug APK assemble이BUILD SUCCESSFUL임을 확인했다. Kotlin LSP는 현재 환경에 서버가 없어lsp_diagnostics를 실행할 수 없었다.