feat(widget): 콘텐츠 랭킹 위젯을 추가한다

This commit is contained in:
2026-05-20 12:00:23 +09:00
parent 01fea58e4c
commit 36ffbc6cdb
35 changed files with 2365 additions and 39 deletions

View File

@@ -21,8 +21,8 @@
## 파일 구조
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingItem.kt`
- 랭킹 UI에 필요한 순수 데이터 모델과 차단 관계 상태 계산을 정의한다.
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingChangeType.kt`
- `Increase`, `Decrease`, `Stay`, `New` 순위 변동 타입을 정의한다.
- Rename/Move: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingChangeType.kt` -> `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/ranking/RankingChangeType.kt`
- `Increase`, `Decrease`, `Stay`, `New` 순위 변동 타입을 크리에이터/콘텐츠 랭킹 공용 타입으로 정의한다.
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingCardVariant.kt`
- `Large`, `Compact`, `Horizontal` 카드 UI variant를 정의한다.
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingPlacement.kt`
@@ -30,7 +30,7 @@
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingLayoutCalculator.kt`
- 부모 폭, horizontal gap, row count 기준으로 item width/height를 계산한다.
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingDeltaPresentation.kt`
- 순위 변동 타입별 아이콘과 숫자 표시 여부를 정의한다. `Stay``New`는 숫자를 표시하지 않는다.
- 공용 `RankingChangeType`별 아이콘과 숫자 표시 여부를 정의한다. `Stay``New`는 숫자를 표시하지 않는다.
- Create: `app/src/main/res/layout/view_creator_ranking_large_card.xml`
- 1위 전용 큰 정사각형 카드 layout을 정의한다.
- Create: `app/src/main/res/layout/view_creator_ranking_compact_card.xml`
@@ -215,7 +215,7 @@ Expected: `BUILD SUCCESSFUL`
**Files:**
- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingItemTest.kt`
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingChangeType.kt`
- Rename/Move: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingChangeType.kt` -> `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/ranking/RankingChangeType.kt`
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingItem.kt`
- [x] **Step 1: RED - 접근 가능/불가 표시 정책 테스트 추가**
@@ -223,6 +223,7 @@ Expected: `BUILD SUCCESSFUL`
```kotlin
package kr.co.vividnext.sodalive.v2.widget.creatorranking
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -271,7 +272,7 @@ class CreatorRankingItemTest {
creatorId: Long = 1L,
rank: Int = 1,
previousRank: Int? = 5,
rankChangeType: CreatorRankingChangeType = CreatorRankingChangeType.Increase,
rankChangeType: RankingChangeType = RankingChangeType.Increase,
rankChangeAmount: Int = 4,
creatorName: String = "크리에이터 이름",
imageUrl: String = "https://example.com/image.png",
@@ -298,9 +299,9 @@ Expected: `Unresolved reference 'CreatorRankingItem'`로 실패한다.
- [x] **Step 3: GREEN - 순수 상태 모델 추가**
```kotlin
package kr.co.vividnext.sodalive.v2.widget.creatorranking
package kr.co.vividnext.sodalive.v2.widget.ranking
enum class CreatorRankingChangeType {
enum class RankingChangeType {
Increase,
Decrease,
Stay,
@@ -311,11 +312,13 @@ enum class CreatorRankingChangeType {
```kotlin
package kr.co.vividnext.sodalive.v2.widget.creatorranking
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
data class CreatorRankingItem(
val creatorId: Long,
val rank: Int,
val previousRank: Int?,
val rankChangeType: CreatorRankingChangeType,
val rankChangeType: RankingChangeType,
val rankChangeAmount: Int,
val creatorName: String,
val imageUrl: String,
@@ -354,6 +357,7 @@ Expected: `BUILD SUCCESSFUL`
package kr.co.vividnext.sodalive.v2.widget.creatorranking
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.assertNull
@@ -364,7 +368,7 @@ class CreatorRankingDeltaPresentationTest {
@Test
fun `increase shows caret and amount`() {
val presentation = CreatorRankingDeltaPresentation.from(CreatorRankingChangeType.Increase, amount = 4)
val presentation = CreatorRankingDeltaPresentation.from(RankingChangeType.Increase, amount = 4)
assertEquals(R.drawable.ic_rank_caret_increase, presentation.iconRes)
assertTrue(presentation.showAmount)
@@ -373,7 +377,7 @@ class CreatorRankingDeltaPresentationTest {
@Test
fun `decrease shows caret and amount`() {
val presentation = CreatorRankingDeltaPresentation.from(CreatorRankingChangeType.Decrease, amount = 4)
val presentation = CreatorRankingDeltaPresentation.from(RankingChangeType.Decrease, amount = 4)
assertEquals(R.drawable.ic_rank_caret_decrease, presentation.iconRes)
assertTrue(presentation.showAmount)
@@ -382,7 +386,7 @@ class CreatorRankingDeltaPresentationTest {
@Test
fun `stay shows stay icon without amount`() {
val presentation = CreatorRankingDeltaPresentation.from(CreatorRankingChangeType.Stay, amount = 0)
val presentation = CreatorRankingDeltaPresentation.from(RankingChangeType.Stay, amount = 0)
assertEquals(R.drawable.ic_rank_caret_stay, presentation.iconRes)
assertFalse(presentation.showAmount)
@@ -391,7 +395,7 @@ class CreatorRankingDeltaPresentationTest {
@Test
fun `new shows new image without amount`() {
val presentation = CreatorRankingDeltaPresentation.from(CreatorRankingChangeType.New, amount = 0)
val presentation = CreatorRankingDeltaPresentation.from(RankingChangeType.New, amount = 0)
assertEquals(R.drawable.ic_rank_new, presentation.iconRes)
assertFalse(presentation.showAmount)
@@ -413,6 +417,7 @@ package kr.co.vividnext.sodalive.v2.widget.creatorranking
import androidx.annotation.DrawableRes
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
data class CreatorRankingDeltaPresentation(
@DrawableRes val iconRes: Int,
@@ -420,23 +425,23 @@ data class CreatorRankingDeltaPresentation(
val amountText: String?
) {
companion object {
fun from(type: CreatorRankingChangeType, amount: Int): CreatorRankingDeltaPresentation = when (type) {
CreatorRankingChangeType.Increase -> CreatorRankingDeltaPresentation(
fun from(type: RankingChangeType, amount: Int): CreatorRankingDeltaPresentation = when (type) {
RankingChangeType.Increase -> CreatorRankingDeltaPresentation(
iconRes = R.drawable.ic_rank_caret_increase,
showAmount = true,
amountText = amount.toString()
)
CreatorRankingChangeType.Decrease -> CreatorRankingDeltaPresentation(
RankingChangeType.Decrease -> CreatorRankingDeltaPresentation(
iconRes = R.drawable.ic_rank_caret_decrease,
showAmount = true,
amountText = amount.toString()
)
CreatorRankingChangeType.Stay -> CreatorRankingDeltaPresentation(
RankingChangeType.Stay -> CreatorRankingDeltaPresentation(
iconRes = R.drawable.ic_rank_caret_stay,
showAmount = false,
amountText = null
)
CreatorRankingChangeType.New -> CreatorRankingDeltaPresentation(
RankingChangeType.New -> CreatorRankingDeltaPresentation(
iconRes = R.drawable.ic_rank_new,
showAmount = false,
amountText = null
@@ -619,8 +624,8 @@ Required common API:
Required behavior:
- `CreatorRankingDeltaPresentation`을 사용해 rank delta icon과 amount 표시 여부를 결정한다.
- `CreatorRankingChangeType.Stay`이면 숫자 없이 `ic_rank_caret_stay`만 표시한다.
- `CreatorRankingChangeType.New`이면 `ic_rank_new`를 표시하고 rank delta 숫자는 숨긴다.
- `RankingChangeType.Stay`이면 숫자 없이 `ic_rank_caret_stay`만 표시한다.
- `RankingChangeType.New`이면 `ic_rank_new`를 표시하고 rank delta 숫자는 숨긴다.
- `Increase`, `Decrease`는 change type별 caret icon과 `rankChangeAmount`를 표시한다.
- `Large``Compact`에서 `item.displayName(...)` 결과가 빈 문자열이면 name TextView를 숨기거나 빈 값으로 둔다.
- `Large``Compact`에서 name TextView를 숨겨도 dim gradient view는 숨기지 않는다.
@@ -794,7 +799,7 @@ Expected: 신규 layout의 ViewBinding 생성 파일이 출력된다.
- 결과:
- `CreatorRankingAdapter.GRID_SPAN_COUNT`, `createSpanSizeLookup()`, `createGridLayoutManager(context)`를 추가해 1위/11위 이후 full span, 2위~7위 2열, 8위~10위 3열 구성을 호출부가 적용할 수 있게 했다.
- API 31 미만 차단 이미지에는 기존 `BlurTransformation` 기반 Coil blur fallback을 적용하고, API 31 이상 `RenderEffect` 경로는 유지했다.
- `CreatorRankingChangeType.New`는 pill 배경 없이 `36dp x 23dp` 아이콘으로 표시하고, caret 계열은 기존 `14dp x 14dp` pill 표시를 유지했다.
- `RankingChangeType.New`는 pill 배경 없이 `36dp x 23dp` 아이콘으로 표시하고, caret 계열은 기존 `14dp x 14dp` pill 표시를 유지했다.
- creator ranking 단위 테스트와 `:app:assembleDebug`는 모두 `BUILD SUCCESSFUL`로 통과했다.
- Kotlin LSP는 현재 환경에 `.kt` 확장자 서버가 없어 `No LSP server configured for extension: .kt`로 진단을 수행할 수 없었다.
- 보강 후 재리뷰에서 기존 Important 3건은 모두 해소됐고 새 Critical/Important 이슈는 없음을 확인했다.