From c030cbabd3430f71426d61f06e2ed5ab401e85aa Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 25 Jun 2026 18:30:55 +0900 Subject: [PATCH] =?UTF-8?q?fix(content):=20=EB=9E=AD=ED=82=B9=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EA=B0=84=EA=B2=A9=EA=B3=BC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EB=B3=B4=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/main/content/ContentMainFragment.kt | 1 + .../contentranking/ContentRankingAdapter.kt | 81 ++++++++++---- .../ContentRankingLargeCardView.kt | 7 +- .../content/ContentMainFragmentSourceTest.kt | 1 + .../contentranking/ContentRankingBlurTest.kt | 24 +++++ .../ContentRankingCardViewTest.kt | 35 ++++++ .../ContentRankingLayoutCalculatorTest.kt | 16 +-- .../plan-task.md | 102 ++++++++++++++++++ 8 files changed, 238 insertions(+), 29 deletions(-) create mode 100644 app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingBlurTest.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt index 0699427a..cb132d52 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt @@ -274,6 +274,7 @@ class ContentMainFragment : BaseFragment( 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 { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt index b8a14614..fadad840 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt @@ -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 } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt index 000e5259..ccd62faa 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt @@ -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) } } diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt index 026f48dd..d6ee1f24 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt @@ -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()")) diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingBlurTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingBlurTest.kt new file mode 100644 index 00000000..4ea864db --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingBlurTest.kt @@ -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)) + } +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt index 44b68df5..56510966 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt @@ -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(R.id.ll_content_ranking_delta).visibility) } + @Test + fun `large card는 전경 1대1 이미지와 배경 이미지를 함께 유지한다`() { + val view = inflateView(R.layout.view_content_ranking_large_card) + + view.bind(sampleItem()) + + assertEquals(View.VISIBLE, view.findViewById(R.id.iv_content_ranking_image).visibility) + assertEquals(View.VISIBLE, view.findViewById(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 inflateView(layoutResId: Int): T { val context = ApplicationProvider.getApplicationContext() 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") + } } diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLayoutCalculatorTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLayoutCalculatorTest.kt index d8c6d198..94f6b19d 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLayoutCalculatorTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLayoutCalculatorTest.kt @@ -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) ) diff --git a/docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md b/docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md index c7a30b6c..c0ac0d7b 100644 --- a/docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md +++ b/docs/20260623_메인_콘텐츠_탭_내부_랭킹_탭/plan-task.md @@ -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 경고는 기존 설정 경고로 이번 변경과 무관하다.