fix(content): 랭킹 카드 간격과 이미지를 보정한다

This commit is contained in:
2026-06-25 18:30:55 +09:00
parent ab9d14598c
commit c030cbabd3
8 changed files with 238 additions and 29 deletions

View File

@@ -274,6 +274,7 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
binding.rvContentRankings.apply {
layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())
adapter = contentRankingAdapter
addItemDecoration(ContentRankingAdapter.createItemDecoration(requireContext()))
}
val contentAllGridLayoutManager = GridLayoutManager(requireContext(), CONTENT_ALL_GRID_SPAN_COUNT)
binding.rvContentAllItems.apply {

View File

@@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.v2.widget.contentranking
import android.content.Context
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.GridLayoutManager
@@ -27,19 +29,35 @@ class ContentRankingAdapter(
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_LARGE -> LargeViewHolder(
inflater.inflate(R.layout.view_content_ranking_large_card, parent, false) as ContentRankingLargeCardView,
inflater.inflate(
R.layout.view_content_ranking_large_card,
parent,
false
) as ContentRankingLargeCardView,
parent
)
VIEW_TYPE_MEDIUM_GRID -> MediumGridViewHolder(
inflater.inflate(R.layout.view_content_ranking_medium_grid_card, parent, false) as ContentRankingMediumGridCardView,
inflater.inflate(
R.layout.view_content_ranking_medium_grid_card,
parent,
false
) as ContentRankingMediumGridCardView,
parent
)
VIEW_TYPE_SMALL_GRID -> SmallGridViewHolder(
inflater.inflate(R.layout.view_content_ranking_small_grid_card, parent, false) as ContentRankingSmallGridCardView,
inflater.inflate(
R.layout.view_content_ranking_small_grid_card,
parent,
false
) as ContentRankingSmallGridCardView,
parent
)
VIEW_TYPE_HORIZONTAL -> HorizontalViewHolder(
inflater.inflate(R.layout.view_content_ranking_horizontal_card, parent, false) as ContentRankingHorizontalCardView,
inflater.inflate(
R.layout.view_content_ranking_horizontal_card,
parent,
false
) as ContentRankingHorizontalCardView,
parent
)
else -> error("Unknown viewType: $viewType")
@@ -71,8 +89,8 @@ class ContentRankingAdapter(
fun bind(item: ContentRankingItem) {
val size = calculateSize(item, parent)
view.setCardSize(size)
view.imageView().loadContentImage(item)
view.backgroundImageView().loadContentImage(item)
view.backgroundImageView().loadContentImage(item, blurEnabled = true)
view.imageView().loadContentImage(item, blurEnabled = false)
view.bind(item)
view.setOnContentClick(onClickItem)
}
@@ -121,9 +139,12 @@ class ContentRankingAdapter(
view.setOnContentClick(onClickItem)
}
private fun ImageView.loadContentImage(item: ContentRankingItem) {
private fun ImageView.loadContentImage(
item: ContentRankingItem,
blurEnabled: Boolean = item.isInaccessible
) {
loadUrl(item.imageUrl) {
val blurTransformations = ContentRankingBlur.transformations(context, item.isInaccessible)
val blurTransformations = ContentRankingBlur.transformations(context, blurEnabled)
if (blurTransformations.isNotEmpty()) {
transformations(blurTransformations)
}
@@ -148,23 +169,47 @@ class ContentRankingAdapter(
companion object {
const val GRID_SPAN_COUNT = 6
fun createGridLayoutManager(context: Context): GridLayoutManager = GridLayoutManager(context, GRID_SPAN_COUNT).apply {
spanSizeLookup = createSpanSizeLookup()
}
fun createGridLayoutManager(context: Context): GridLayoutManager =
GridLayoutManager(context, GRID_SPAN_COUNT).apply {
spanSizeLookup = createSpanSizeLookup()
}
fun createSpanSizeLookup(): GridLayoutManager.SpanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int = when (ContentRankingPlacement.fromRank(position + 1).itemsPerRow) {
1 -> GRID_SPAN_COUNT
2 -> GRID_SPAN_COUNT / 2
3 -> GRID_SPAN_COUNT / 3
else -> GRID_SPAN_COUNT
fun createItemDecoration(context: Context): RecyclerView.ItemDecoration {
val spacingPx = HORIZONTAL_GAP_DP.dpToPx(context)
return object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
val itemCount = parent.adapter?.itemCount ?: return
if (position != RecyclerView.NO_POSITION && position < itemCount - 1) {
outRect.bottom = spacingPx
}
}
}
}
fun createSpanSizeLookup(): GridLayoutManager.SpanSizeLookup =
object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int =
when (ContentRankingPlacement.fromRank(position + 1).itemsPerRow) {
1 -> GRID_SPAN_COUNT
2 -> GRID_SPAN_COUNT / 2
3 -> GRID_SPAN_COUNT / 3
else -> GRID_SPAN_COUNT
}
}
private const val VIEW_TYPE_LARGE = 1
private const val VIEW_TYPE_MEDIUM_GRID = 2
private const val VIEW_TYPE_SMALL_GRID = 3
private const val VIEW_TYPE_HORIZONTAL = 4
private const val HORIZONTAL_GAP_DP = 4
private fun Int.dpToPx(context: Context): Int =
(this * context.resources.displayMetrics.density).roundToInt()
private const val HORIZONTAL_GAP_DP = 8
}
}

View File

@@ -144,9 +144,10 @@ class ContentRankingLargeCardView @JvmOverloads constructor(
private fun applyBlur(enabled: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val effect = if (enabled) RenderEffect.createBlurEffect(16f, 16f, Shader.TileMode.CLAMP) else null
contentImageView().setRenderEffect(effect)
backgroundImageView().setRenderEffect(effect)
val contentEffect = if (enabled) RenderEffect.createBlurEffect(16f, 16f, Shader.TileMode.CLAMP) else null
val backgroundEffect = RenderEffect.createBlurEffect(16f, 16f, Shader.TileMode.CLAMP)
contentImageView().setRenderEffect(contentEffect)
backgroundImageView().setRenderEffect(backgroundEffect)
}
}

View File

@@ -41,6 +41,7 @@ class ContentMainFragmentSourceTest {
assertTrue(source.contains("ContentCommentedAudioAdapter"))
assertTrue(source.contains("ContentRankingAdapter"))
assertTrue(source.contains("ContentRankingAdapter.createGridLayoutManager(requireContext())"))
assertTrue(source.contains("ContentRankingAdapter.createItemDecoration(requireContext())"))
assertTrue(source.contains("recommendationsStateLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("rankingStateLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("contentMainViewModel.loadRecommendations()"))

View File

@@ -0,0 +1,24 @@
package kr.co.vividnext.sodalive.v2.widget.contentranking
import android.os.Build
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ContentRankingBlurTest {
@Test
fun `pre S blur는 enabled true이면 Coil transformation을 사용한다`() {
assertTrue(ContentRankingBlur.shouldUseCoilBlur(enabled = true, sdkInt = Build.VERSION_CODES.R))
}
@Test
fun `S 이상 blur는 RenderEffect를 사용하므로 Coil transformation을 사용하지 않는다`() {
assertFalse(ContentRankingBlur.shouldUseCoilBlur(enabled = true, sdkInt = Build.VERSION_CODES.S))
}
@Test
fun `pre S blur는 enabled false이면 Coil transformation을 사용하지 않는다`() {
assertFalse(ContentRankingBlur.shouldUseCoilBlur(enabled = false, sdkInt = Build.VERSION_CODES.R))
}
}

View File

@@ -11,10 +11,12 @@ import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.Increase
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.io.File
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
@@ -136,6 +138,33 @@ class ContentRankingCardViewTest {
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.ll_content_ranking_delta).visibility)
}
@Test
fun `large card는 전경 1대1 이미지와 배경 이미지를 함께 유지한다`() {
val view = inflateView<ContentRankingLargeCardView>(R.layout.view_content_ranking_large_card)
view.bind(sampleItem())
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.iv_content_ranking_image).visibility)
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.iv_content_ranking_background).visibility)
}
@Test
fun `large view holder는 배경 blur와 전경 non blur coverImage를 모두 로드한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt"
).readText()
val largeViewHolderSource = source
.substringAfter("private inner class LargeViewHolder")
.substringBefore("private inner class MediumGridViewHolder")
assertTrue(
largeViewHolderSource.contains("view.backgroundImageView().loadContentImage(item, blurEnabled = true)")
)
assertTrue(
largeViewHolderSource.contains("view.imageView().loadContentImage(item, blurEnabled = false)")
)
}
private inline fun <reified T : View> inflateView(layoutResId: Int): T {
val context = ApplicationProvider.getApplicationContext<Context>()
return LayoutInflater.from(context).inflate(layoutResId, null, false) as T
@@ -175,4 +204,10 @@ class ContentRankingCardViewTest {
isBlocked = false,
showRankChange = showRankChange
)
private fun projectFile(relativePath: String): File {
val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() }
?: error("Project file not found: $relativePath")
}
}

View File

@@ -9,7 +9,7 @@ class ContentRankingLayoutCalculatorTest {
fun `large item keeps figma large ratio`() {
val size = ContentRankingLayoutCalculator.calculate(
parentWidthPx = 374,
horizontalGapPx = 4,
horizontalGapPx = 8,
placement = ContentRankingPlacement(ContentRankingCardVariant.Large, itemsPerRow = 1)
)
@@ -21,31 +21,31 @@ class ContentRankingLayoutCalculatorTest {
fun `medium grid item width divides available width by items per row`() {
val size = ContentRankingLayoutCalculator.calculate(
parentWidthPx = 374,
horizontalGapPx = 4,
horizontalGapPx = 8,
placement = ContentRankingPlacement(ContentRankingCardVariant.MediumGrid, itemsPerRow = 2)
)
assertEquals(185, size.widthPx)
assertEquals(185, size.heightPx)
assertEquals(183, size.widthPx)
assertEquals(183, size.heightPx)
}
@Test
fun `small grid item subtracts two gaps`() {
val size = ContentRankingLayoutCalculator.calculate(
parentWidthPx = 374,
horizontalGapPx = 4,
horizontalGapPx = 8,
placement = ContentRankingPlacement(ContentRankingCardVariant.SmallGrid, itemsPerRow = 3)
)
assertEquals(122, size.widthPx)
assertEquals(122, size.heightPx)
assertEquals(119, size.widthPx)
assertEquals(119, size.heightPx)
}
@Test
fun `horizontal item keeps figma ratio`() {
val size = ContentRankingLayoutCalculator.calculate(
parentWidthPx = 374,
horizontalGapPx = 4,
horizontalGapPx = 8,
placement = ContentRankingPlacement(ContentRankingCardVariant.Horizontal, itemsPerRow = 1)
)

View File

@@ -675,6 +675,104 @@
---
### 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`.
@@ -699,3 +797,7 @@
- 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 경고는 기존 설정 경고로 이번 변경과 무관하다.