# 라이브 썸네일 컴포넌트 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 `24:4999`, `24:5017` 기준으로 현재 라이브 중인 상태를 표시하는 재사용 가능한 라이브 썸네일 컴포넌트를 추가한다. **Architecture:** 라이브 썸네일 표시 데이터와 variant를 순수 Kotlin contract로 먼저 정의하고, Android XML layout과 custom view가 해당 contract를 바인딩한다. 실제 이미지 로딩은 컴포넌트가 직접 수행하지 않고 `ImageView`를 노출해 기존 호출부의 Coil/Glide 정책을 유지한다. **Tech Stack:** Android XML Views, Kotlin custom View, ViewBinding/resource merge, JUnit4 local unit test. --- ## 작업 목표 - `Simple` variant는 Figma `24:4999` 기준 세로형 라이브 프로필 썸네일로 구현한다. - `Detail` variant는 Figma `24:5017` 기준 가로형 라이브 썸네일로 구현한다. - Figma placeholder 이미지 영역에는 실제 `imageUrl`을 로드할 수 있는 `ImageView`를 제공한다. - 모든 텍스트는 1줄 제한과 끝 말줄임 처리를 적용한다. - 터치 동작은 호출부 callback으로 위임한다. ## 파일 구조 - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailVariant.kt` - `Simple`, `Detail` variant를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItem.kt` - 라이브 썸네일 UI에 필요한 최소 데이터 계약을 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSize.kt` - Figma 기준 variant별 기본 크기와 텍스트 영역 width를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSimpleView.kt` - 세로형 variant의 텍스트 바인딩, 이미지 view 노출, 터치 callback을 처리한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailDetailView.kt` - 가로형 variant의 텍스트 바인딩, 이미지 view 노출, 터치 callback을 처리한다. - Create: `app/src/main/res/layout/view_live_thumbnail_simple.xml` - Figma `24:4999` 기준 세로형 layout을 정의한다. - Create: `app/src/main/res/layout/view_live_thumbnail_detail.xml` - Figma `24:5017` 기준 가로형 layout을 정의한다. - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_ring.xml` - `Simple` 외곽 ocean-blue ring drawable을 정의한다. - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_badge.xml` - LIVE badge black background + `#62CFFF` stroke + pill radius를 정의한다. - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_badge_capsule.xml` - `Detail` LIVE badge용 black background + pill radius, stroke 없는 capsule을 정의한다. - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_dot.xml` - LIVE badge 내부 red dot을 정의한다. - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_detail.xml` - `Detail` root `gray_900` background + `#62CFFF` stroke + pill radius를 정의한다. - Read: `app/src/main/res/values/colors.xml` - `color_62cfff` resource가 있는지 확인하고, 없으면 기존 `colors.xml`에 추가해 live thumbnail drawable에서 참조한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItemTest.kt` - 데이터 trimming 없이 원문을 보존하고 display 문자열 fallback을 검증한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSizeTest.kt` - variant별 Figma 기준 크기 계약을 검증한다. - Modify: `docs/plan-task/20260520_라이브썸네일컴포넌트.md` - 구현 중 체크박스와 검증 기록을 누적한다. ## 구현 계획 ### Task 1: 기존 패턴 및 Figma 기준 확인 **Files:** - Read: `docs/prd/20260520_라이브썸네일컴포넌트_prd.md` - Read: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt` - Read: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/creatorranking/CreatorRankingLargeCardView.kt` - Read: `app/src/main/res/layout/item_home_live.xml` - Read: `app/src/main/res/layout/item_live_now.xml` - Read: `app/src/main/res/values/colors.xml` - Read: `app/src/main/res/values/typography.xml` - [x] **Step 1: 관련 기존 코드 확인** Run: `rg -n "AudioContentCardView|CreatorRankingLargeCardView|iv_profile|img_live|ellipsize=\"end\"|maxLines=\"1\"" app/src/main/java app/src/main/res/layout app/src/main/res/values` Expected: 기존 v2 custom view의 `imageView()` 노출 패턴, 기존 라이브 profile/LIVE badge 사용처, 1줄 ellipsis 적용 예시를 확인한다. - [x] **Step 2: Figma 세부 컨텍스트 재확인** Run tools: - `Figma_get_design_context(24:4999)` - `Figma_get_screenshot(24:4999)` - `Figma_get_design_context(24:5017)` - `Figma_get_screenshot(24:5017)` Expected: `Simple`과 `Detail` variant의 size, typography, color, radius, spacing, image placeholder 위치를 확인한다. - [x] **Step 3: 구현 기준 token 정리** Expected token contract: - `Simple` root width: `70dp` - `Simple` profile area: `70dp x 76dp` - `Simple` image: `58dp x 58dp`, circle crop, start/top `6dp` - `Simple` ring: `70dp x 70dp`, ocean-blue stroke/gradient - Simple LIVE badge: black background, `#62CFFF` stroke `2dp`, height `18dp`, radius pill - Detail LIVE badge: black background, stroke 없음, height `18dp`, radius pill - LIVE badge dot: `8dp x 8dp` - `Simple` creator name: `@style/Typography.Body5`, white, center, 1 line ellipsis - `Detail` root: `266dp x 99dp`, `gray_900`, `#62CFFF` stroke `2dp`, radius `90dp` - `Detail` image: `75dp x 75dp`, circle crop, start `10dp`, vertical center - `Detail` text column: start `93dp`, width `149dp`, vertical center, gap `4dp` - `Detail` title: `@style/Typography.Heading4`, white, 1 line ellipsis - `Detail` creator/time: `@style/Typography.Body6`, `gray_500`, 1 line ellipsis ### Task 2: Live thumbnail data contract TDD **Files:** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItemTest.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItem.kt` - [x] **Step 1: RED - item display contract 테스트 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.livethumbnail import org.junit.Assert.assertEquals import org.junit.Test class LiveThumbnailItemTest { @Test fun `item keeps title creator and live start time text`() { val item = LiveThumbnailItem( liveId = 10L, creatorId = 20L, imageUrl = "https://example.com/profile.png", title = "라이브 제목", creatorName = "크리에이터이름", liveStartTimeText = "00:30" ) assertEquals("라이브 제목", item.title) assertEquals("크리에이터이름", item.creatorName) assertEquals("00:30", item.liveStartTimeText) } @Test fun `blank live start time remains blank`() { val item = LiveThumbnailItem( liveId = 10L, creatorId = 20L, imageUrl = "https://example.com/profile.png", title = "라이브 제목", creatorName = "크리에이터이름", liveStartTimeText = "" ) assertEquals("", item.liveStartTimeText) } } ``` - [x] **Step 2: RED 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailItemTest"` Expected: `Unresolved reference 'LiveThumbnailItem'`로 실패한다. - [x] **Step 3: GREEN - 최소 data contract 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.livethumbnail data class LiveThumbnailItem( val liveId: Long, val creatorId: Long, val imageUrl: String, val title: String, val creatorName: String, val liveStartTimeText: String ) ``` - [x] **Step 4: GREEN 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailItemTest"` Expected: `BUILD SUCCESSFUL` ### Task 3: Variant size contract TDD **Files:** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSizeTest.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailVariant.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSize.kt` - [x] **Step 1: RED - variant별 Figma 크기 테스트 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.livethumbnail import org.junit.Assert.assertEquals import org.junit.Test class LiveThumbnailSizeTest { @Test fun `simple variant uses figma base size`() { val size = LiveThumbnailSize.from(LiveThumbnailVariant.Simple) assertEquals(70, size.rootWidthDp) assertEquals(76, size.profileAreaHeightDp) assertEquals(58, size.imageSizeDp) assertEquals(70, size.textWidthDp) } @Test fun `detail variant uses figma base size`() { val size = LiveThumbnailSize.from(LiveThumbnailVariant.Detail) assertEquals(266, size.rootWidthDp) assertEquals(99, size.rootHeightDp) assertEquals(75, size.imageSizeDp) assertEquals(149, size.textWidthDp) } } ``` - [x] **Step 2: RED 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSizeTest"` Expected: `Unresolved reference 'LiveThumbnailSize'` 또는 `Unresolved reference 'LiveThumbnailVariant'`로 실패한다. - [x] **Step 3: GREEN - variant와 size contract 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.livethumbnail enum class LiveThumbnailVariant { Simple, Detail } ``` ```kotlin package kr.co.vividnext.sodalive.v2.widget.livethumbnail data class LiveThumbnailSize( val rootWidthDp: Int, val rootHeightDp: Int?, val profileAreaHeightDp: Int?, val imageSizeDp: Int, val textWidthDp: Int ) { companion object { fun from(variant: LiveThumbnailVariant): LiveThumbnailSize = when (variant) { LiveThumbnailVariant.Simple -> LiveThumbnailSize( rootWidthDp = 70, rootHeightDp = null, profileAreaHeightDp = 76, imageSizeDp = 58, textWidthDp = 70 ) LiveThumbnailVariant.Detail -> LiveThumbnailSize( rootWidthDp = 266, rootHeightDp = 99, profileAreaHeightDp = null, imageSizeDp = 75, textWidthDp = 149 ) } } } ``` - [x] **Step 4: GREEN 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSizeTest"` Expected: `BUILD SUCCESSFUL` ### Task 4: Drawable resources 추가 **Files:** - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_badge.xml` - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_badge_capsule.xml` - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_dot.xml` - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_detail.xml` - Add if missing: `app/src/main/res/drawable/bg_live_thumbnail_ring.xml` - Read: `app/src/main/res/values/colors.xml` - [x] **Step 1: 기존 동일 drawable 존재 여부 확인** Run: `rg -n "62CFFF|color_62cfff|live_thumbnail|img_live|gray_900| ``` - [x] **Step 3: Detail LIVE badge capsule drawable 추가** ```xml ``` - [x] **Step 4: LIVE dot drawable 추가** ```xml ``` - [x] **Step 5: Detail root drawable 추가** ```xml ``` - [x] **Step 6: Simple ring drawable 추가** ```xml ``` - [x] **Step 7: Resource merge 확인** Run: `./gradlew :app:mergeDebugResources` Expected: `BUILD SUCCESSFUL` ### Task 5: Simple layout 및 view 구현 **Files:** - Create: `app/src/main/res/layout/view_live_thumbnail_simple.xml` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSimpleView.kt` - [x] **Step 1: Simple XML layout 추가** ```xml ``` - [x] **Step 2: Simple custom view 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.livethumbnail import android.content.Context import android.graphics.Outline import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import kr.co.vividnext.sodalive.R class LiveThumbnailSimpleView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { private var image: ImageView? = null private var creatorText: TextView? = null private var currentItem: LiveThumbnailItem? = null private var clickListener: ((LiveThumbnailItem) -> Unit)? = null override fun onFinishInflate() { super.onFinishInflate() image = findViewById(R.id.iv_live_thumbnail_image) creatorText = findViewById(R.id.tv_live_thumbnail_creator) imageView().clipToOutline = true imageView().outlineProvider = circleOutlineProvider() } fun bind(item: LiveThumbnailItem) { currentItem = item requireNotNull(creatorText).text = item.creatorName isClickable = clickListener != null setOnClickListener(if (isClickable) View.OnClickListener { clickListener?.invoke(item) } else null) } fun imageView(): ImageView = requireNotNull(image) fun setOnLiveThumbnailClick(listener: ((LiveThumbnailItem) -> Unit)?) { clickListener = listener currentItem?.let(::bind) } private fun circleOutlineProvider() = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { outline.setOval(0, 0, view.width, view.height) } } } ``` - [x] **Step 3: Resource merge 확인** Run: `./gradlew :app:mergeDebugResources` Expected: `BUILD SUCCESSFUL` ### Task 6: Detail layout 및 view 구현 **Files:** - Create: `app/src/main/res/layout/view_live_thumbnail_detail.xml` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailDetailView.kt` - [x] **Step 1: Detail XML layout 추가** ```xml ``` - [x] **Step 2: Detail custom view 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.livethumbnail import android.content.Context import android.graphics.Outline import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import kr.co.vividnext.sodalive.R class LiveThumbnailDetailView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr) { private var image: ImageView? = null private var liveStartTimeText: TextView? = null private var titleText: TextView? = null private var creatorText: TextView? = null private var currentItem: LiveThumbnailItem? = null private var clickListener: ((LiveThumbnailItem) -> Unit)? = null override fun onFinishInflate() { super.onFinishInflate() image = findViewById(R.id.iv_live_thumbnail_image) liveStartTimeText = findViewById(R.id.tv_live_thumbnail_start_time) titleText = findViewById(R.id.tv_live_thumbnail_title) creatorText = findViewById(R.id.tv_live_thumbnail_creator) imageView().clipToOutline = true imageView().outlineProvider = circleOutlineProvider() } fun bind(item: LiveThumbnailItem) { currentItem = item requireNotNull(liveStartTimeText).text = item.liveStartTimeText requireNotNull(titleText).text = item.title requireNotNull(creatorText).text = item.creatorName isClickable = clickListener != null setOnClickListener(if (isClickable) View.OnClickListener { clickListener?.invoke(item) } else null) } fun imageView(): ImageView = requireNotNull(image) fun setOnLiveThumbnailClick(listener: ((LiveThumbnailItem) -> Unit)?) { clickListener = listener currentItem?.let(::bind) } private fun circleOutlineProvider() = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { outline.setOval(0, 0, view.width, view.height) } } } ``` - [x] **Step 3: Resource merge 확인** Run: `./gradlew :app:mergeDebugResources` Expected: `BUILD SUCCESSFUL` ### Task 7: 실제 이미지 로딩 호출 예시 정리 **Files:** - No immediate file changes in this task. - Use this task as the binding contract for the caller screen selected in a later implementation request. - [x] **Step 1: 호출부 이미지 로딩 방식 확인** Run: `rg -n "\.load\(|Glide\.with|iv_.*\.load|placeholder\(" app/src/main/java/kr/co/vividnext/sodalive` Expected: 대상 화면이 Coil 또는 Glide 중 어떤 방식을 사용하는지 확인한다. - [x] **Step 1-1: 컴포넌트 내부 이미지 로더 비고정 확인** Run: `rg -n "Glide\.with|coil|\.load\(" app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail` Expected: 검색 결과가 없어야 한다. `imageView()`만 노출하고 실제 이미지 로딩은 호출부가 수행한다. - [x] **Step 2: Simple 호출부 바인딩 예시 문서화** ```kotlin binding.liveThumbnailSimple.bind(item) binding.liveThumbnailSimple.imageView().load(item.imageUrl) { crossfade(true) placeholder(R.drawable.ic_place_holder) } binding.liveThumbnailSimple.setOnLiveThumbnailClick { liveThumbnailItem -> // 호출 화면의 라이브 상세 이동 로직을 연결한다. } ``` - [x] **Step 3: Detail 호출부 바인딩 예시 문서화** ```kotlin binding.liveThumbnailDetail.bind(item) binding.liveThumbnailDetail.imageView().load(item.imageUrl) { crossfade(true) placeholder(R.drawable.ic_place_holder) } binding.liveThumbnailDetail.setOnLiveThumbnailClick { liveThumbnailItem -> // 호출 화면의 라이브 상세 이동 로직을 연결한다. } ``` Expected: 실제 구현 시 이미지 영역은 Figma 빈 이미지가 아니라 `item.imageUrl`에서 로드된 이미지로 표시된다. ### Task 8: 최종 검증 **Files:** - Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItem.kt` - Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailVariant.kt` - Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSize.kt` - Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSimpleView.kt` - Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailDetailView.kt` - Check: `app/src/main/res/layout/view_live_thumbnail_simple.xml` - Check: `app/src/main/res/layout/view_live_thumbnail_detail.xml` - [x] **Step 1: changed Kotlin 파일 LSP diagnostics 확인** Run tool: `lsp_diagnostics` on each changed Kotlin file. Expected: 새로 추가한 Kotlin 파일에 error가 없다. - [x] **Step 2: 단위 테스트 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.*"` Expected: `BUILD SUCCESSFUL` - [x] **Step 3: Android resource merge 실행** Run: `./gradlew :app:mergeDebugResources` Expected: `BUILD SUCCESSFUL` - [x] **Step 4: 전체 app test 실행** Run: `./gradlew :app:testDebugUnitTest` Expected: `BUILD SUCCESSFUL` 또는 변경과 무관한 기존 실패만 별도 기록한다. - [x] **Step 4-1: UI contract 속성 확인** Run: `rg -n "bg_live_thumbnail_badge_capsule|clipToOutline=\"true\"|outlineProvider|scaleType=\"centerCrop\"|maxLines=\"1\"|ellipsize=\"end\"|color_62cfff" app/src/main/res/layout app/src/main/res/drawable app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail` Expected: `Detail` LIVE badge는 `bg_live_thumbnail_badge_capsule`, 이미지 영역은 `centerCrop`과 circle outline clipping, 사용자 텍스트는 1줄 말줄임, cyan 색상은 `color_62cfff` resource 참조로 확인된다. - [x] **Step 4-2: Figma 시각 정합성 QA 기준 확인** Expected manual/screenshot QA: - `Simple`은 `70dp` 폭, `70dp` ring, `58dp` 원형 이미지, bottom centered LIVE badge, 1줄 creator name을 유지한다. - `Detail`은 `266dp x 99dp`, `75dp` 원형 이미지, stroke 없는 LIVE capsule badge, start time/title/creator 1줄 말줄임을 유지한다. - [x] **Step 5: 계획 문서 검증 기록 누적** Append to this file: ```markdown ## 검증 기록 ### YYYY-MM-DD HH:mm KST - 무엇: 라이브 썸네일 컴포넌트 구현 검증 - 왜: Figma `24:4999`, `24:5017` 기준 UI와 실제 이미지 바인딩 계약이 동작하는지 확인 - 어떻게: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.*"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:testDebugUnitTest` - 결과: 실행 결과를 그대로 기록한다. ``` ## 체크리스트 - [x] AC1: `Simple` variant는 `70dp` root width, `70dp x 76dp` profile area, `58dp` 원형 이미지, LIVE ring/badge를 사용한다. - [x] AC2: `Detail` variant는 `266dp x 99dp` root, `75dp` 원형 이미지, `149dp` text column, stroke 없는 LIVE capsule badge를 사용한다. - [x] AC3: `Detail` root와 `Simple` ring/badge stroke의 cyan 색상은 `color_62cfff` resource를 통해 참조한다. - [x] AC4: 모든 사용자 표시 텍스트는 `maxLines=1`, `ellipsize=end`를 적용한다. - [x] AC5: 컴포넌트 내부는 이미지 로딩 라이브러리를 고정하지 않고 `imageView()`를 노출해 호출부가 Coil/Glide 등 기존 정책으로 로드한다. - [x] AC6: 기존 레거시 live layout과 호출 화면은 이번 widget 계약에서 직접 변경하지 않는다. - [x] AC7: livethumbnail 단위 테스트, debug resource merge, 전체 debug unit test 또는 변경과 무관한 기존 실패 기록으로 검증한다. ## 검증 기록 ### 2026-05-20 KST - 무엇: 코드 리뷰 반영 문서/리소스 정합성 검증 - 왜: Detail LIVE badge capsule 문서 누락, 이미지 로더 비고정 검증, AC/QA 기준, cyan color resource 분리를 반영했는지 확인 - 어떻게: - `rg -n "Glide\.with|coil|\.load\(" app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail` - `./gradlew :app:mergeDebugResources` - XML LSP diagnostics 시도 - 결과: - livethumbnail 내부 이미지 로더 검색: 결과 없음 - debug resource merge: `BUILD SUCCESSFUL` - XML LSP diagnostics: 이 환경에 `.xml` LSP 서버가 없어 실행 불가, resource merge로 대체 확인 ### 2026-05-20 KST - 무엇: 라이브 썸네일 컴포넌트 구현 검증 - 왜: Figma `24:4999`, `24:5017` 기준 UI와 실제 이미지 바인딩 계약이 동작하는지 확인 - 어떻게: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.*"` - `./gradlew :app:mergeDebugResources` - `./gradlew :app:testDebugUnitTest` - `lsp_diagnostics` on changed Kotlin files - 결과: - livethumbnail 단위 테스트: `BUILD SUCCESSFUL` - debug resource merge: `BUILD SUCCESSFUL` - 전체 debug unit test: `BUILD SUCCESSFUL` - Kotlin LSP diagnostics: 이 환경에 `.kt` LSP 서버가 없어 실행 불가, Gradle Kotlin compile/test로 대체 확인 ### 2026-05-20 KST - 무엇: 코드 리뷰 후 비차단 개선 검증 - 왜: review-work에서 제안된 XML typography 정합성과 size contract 테스트 보강을 반영했는지 확인 - 어떻게: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.*"` - `./gradlew :app:mergeDebugResources` - 결과: - LIVE badge 및 사용자 텍스트 TextView에 `android:includeFontPadding="false"` 적용 - `LiveThumbnailSizeTest`에 nullable contract assertion 추가 - livethumbnail 단위 테스트: `BUILD SUCCESSFUL` - debug resource merge: `BUILD SUCCESSFUL` ### 2026-05-20 KST - 무엇: 기존 디자인 토큰 적용 검증 - 왜: 신규 디자인 토큰 생성 없이 현재 미커밋 UI 파일에서 기존 Typography/spacing/color token을 적용할 수 있는 하드코딩을 줄이기 위함 - 어떻게: - `app/src/main/res/values/typography.xml`, `dimens.xml`, `colors.xml` 확인 - `view_live_thumbnail_simple.xml`, `view_live_thumbnail_detail.xml`, `bg_live_thumbnail_*.xml` 확인 - visual-engineering 검토로 추가 안전 치환 가능 여부 확인 - 결과: - LIVE label: `@style/Typography.Caption3` 적용 - Simple creator: `@style/Typography.Body5` 적용 - Detail start time/creator: `@style/Typography.Body6` 적용 - Detail title: `@style/Typography.Heading4` 적용 - `4dp`, `6dp`, `8dp` spacing은 기존 `@dimen/spacing_4`, `@dimen/spacing_6`, `@dimen/spacing_8`로 치환 - `#62CFFF`는 이후 `color_62cfff` resource로 분리하고 drawable에서 참조하도록 보강 - `90dp`, `100dp`, 컴포넌트 고유 width/height는 정확히 대응되는 기존 token이 없어 유지 ### 2026-05-20 KST - 무엇: 문서 작성 범위 검증 - 왜: 사용자가 구현이 아닌 문서 작성만 요청했으므로 코드 변경 없이 PRD와 구현 계획/TASK 문서만 준비했는지 확인 - 어떻게: Figma `24:4999`, `24:5017` design context/screenshot 확인, 기존 v2 widget 및 live layout 패턴 확인 - 결과: `docs/prd/20260520_라이브썸네일컴포넌트_prd.md`, `docs/plan-task/20260520_라이브썸네일컴포넌트.md` 문서만 추가 대상으로 작성함