33 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 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.
작업 목표
Simplevariant는 Figma24:4999기준 세로형 라이브 프로필 썸네일로 구현한다.Detailvariant는 Figma24:5017기준 가로형 라이브 썸네일로 구현한다.- Figma placeholder 이미지 영역에는 실제
imageUrl을 로드할 수 있는ImageView를 제공한다. - 모든 텍스트는 1줄 제한과 끝 말줄임 처리를 적용한다.
- 터치 동작은 호출부 callback으로 위임한다.
파일 구조
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailVariant.ktSimple,Detailvariant를 정의한다.
- 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을 정의한다.
- Figma
- Create:
app/src/main/res/layout/view_live_thumbnail_detail.xml- Figma
24:5017기준 가로형 layout을 정의한다.
- Figma
- Add if missing:
app/src/main/res/drawable/bg_live_thumbnail_ring.xmlSimple외곽 ocean-blue ring drawable을 정의한다.
- Add if missing:
app/src/main/res/drawable/bg_live_thumbnail_badge.xml- LIVE badge black background +
#62CFFFstroke + pill radius를 정의한다.
- LIVE badge black background +
- Add if missing:
app/src/main/res/drawable/bg_live_thumbnail_badge_capsule.xmlDetailLIVE 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.xmlDetailrootgray_900background +#62CFFFstroke + pill radius를 정의한다.
- Read:
app/src/main/res/values/colors.xmlcolor_62cfffresource가 있는지 확인하고, 없으면 기존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 -
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 적용 예시를 확인한다.
- 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 위치를 확인한다.
- Step 3: 구현 기준 token 정리
Expected token contract:
Simpleroot width:70dpSimpleprofile area:70dp x 76dpSimpleimage:58dp x 58dp, circle crop, start/top6dpSimplering:70dp x 70dp, ocean-blue stroke/gradient- Simple LIVE badge: black background,
#62CFFFstroke2dp, height18dp, radius pill - Detail LIVE badge: black background, stroke 없음, height
18dp, radius pill - LIVE badge dot:
8dp x 8dp Simplecreator name:@style/Typography.Body5, white, center, 1 line ellipsisDetailroot:266dp x 99dp,gray_900,#62CFFFstroke2dp, radius90dpDetailimage:75dp x 75dp, circle crop, start10dp, vertical centerDetailtext column: start93dp, width149dp, vertical center, gap4dpDetailtitle:@style/Typography.Heading4, white, 1 line ellipsisDetailcreator/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 -
Step 1: RED - item display contract 테스트 추가
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)
}
}
- Step 2: RED 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailItemTest"
Expected: Unresolved reference 'LiveThumbnailItem'로 실패한다.
- Step 3: GREEN - 최소 data contract 추가
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
)
- 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 -
Step 1: RED - variant별 Figma 크기 테스트 추가
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)
}
}
- Step 2: RED 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSizeTest"
Expected: Unresolved reference 'LiveThumbnailSize' 또는 Unresolved reference 'LiveThumbnailVariant'로 실패한다.
- Step 3: GREEN - variant와 size contract 추가
package kr.co.vividnext.sodalive.v2.widget.livethumbnail
enum class LiveThumbnailVariant {
Simple,
Detail
}
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
)
}
}
}
- 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 -
Step 1: 기존 동일 drawable 존재 여부 확인
Run: rg -n "62CFFF|color_62cfff|live_thumbnail|img_live|gray_900|<stroke" app/src/main/res/drawable app/src/main/res/values
Expected: 재사용 가능한 drawable이 있으면 새 파일을 만들지 않고 계획 문서에 재사용 파일명을 기록한다. 없으면 다음 step의 drawable을 추가한다.
- Step 2: LIVE badge drawable 추가
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/black" />
<stroke
android:width="2dp"
android:color="@color/color_62cfff" />
<corners android:radius="100dp" />
</shape>
- Step 3: Detail LIVE badge capsule drawable 추가
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/black" />
<corners android:radius="100dp" />
</shape>
- Step 4: LIVE dot drawable 추가
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/color_ea3a25" />
</shape>
- Step 5: Detail root drawable 추가
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/gray_900" />
<stroke
android:width="2dp"
android:color="@color/color_62cfff" />
<corners android:radius="90dp" />
</shape>
- Step 6: Simple ring drawable 추가
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/transparent" />
<stroke
android:width="2dp"
android:color="@color/color_62cfff" />
</shape>
- 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 -
Step 1: Simple XML layout 추가
<?xml version="1.0" encoding="utf-8"?>
<kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSimpleView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout
android:layout_width="70dp"
android:layout_height="76dp">
<View
android:layout_width="70dp"
android:layout_height="70dp"
android:background="@drawable/bg_live_thumbnail_ring" />
<ImageView
android:id="@+id/iv_live_thumbnail_image"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_marginStart="@dimen/spacing_6"
android:layout_marginTop="@dimen/spacing_6"
android:contentDescription="@null"
android:scaleType="centerCrop"
tools:src="@drawable/ic_launcher_background" />
<LinearLayout
android:layout_width="50dp"
android:layout_height="18dp"
android:layout_gravity="bottom|center_horizontal"
android:background="@drawable/bg_live_thumbnail_badge"
android:gravity="center"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_8">
<View
android:layout_width="@dimen/spacing_8"
android:layout_height="@dimen/spacing_8"
android:background="@drawable/bg_live_thumbnail_dot" />
<TextView
style="@style/Typography.Caption3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_4"
android:text="LIVE"
android:textColor="@color/white"
tools:ignore="HardcodedText,SmallSp" />
</LinearLayout>
</FrameLayout>
<TextView
android:id="@+id/tv_live_thumbnail_creator"
style="@style/Typography.Body5"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_6"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textColor="@color/white"
tools:text="크리에이터이름" />
</kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSimpleView>
- Step 2: Simple custom view 추가
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)
}
}
}
- 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 -
Step 1: Detail XML layout 추가
<?xml version="1.0" encoding="utf-8"?>
<kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailDetailView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="266dp"
android:layout_height="99dp"
android:background="@drawable/bg_live_thumbnail_detail"
android:clipToOutline="true">
<ImageView
android:id="@+id/iv_live_thumbnail_image"
android:layout_width="75dp"
android:layout_height="75dp"
android:layout_marginStart="10dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_launcher_background" />
<LinearLayout
android:id="@+id/ll_live_thumbnail_text_container"
android:layout_width="149dp"
android:layout_height="wrap_content"
android:layout_marginStart="93dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="18dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="18dp"
android:background="@drawable/bg_live_thumbnail_badge_capsule"
android:gravity="center"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_4">
<View
android:layout_width="@dimen/spacing_8"
android:layout_height="@dimen/spacing_8"
android:background="@drawable/bg_live_thumbnail_dot" />
<TextView
style="@style/Typography.Caption3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_4"
android:text="LIVE"
android:textColor="@color/white"
tools:ignore="HardcodedText,SmallSp" />
</LinearLayout>
<TextView
android:id="@+id/tv_live_thumbnail_start_time"
style="@style/Typography.Body6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_8"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/gray_500"
tools:text="00:00" />
</LinearLayout>
<TextView
android:id="@+id/tv_live_thumbnail_title"
style="@style/Typography.Heading4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_4"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
tools:text="라이브 제목" />
<TextView
android:id="@+id/tv_live_thumbnail_creator"
style="@style/Typography.Body6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_4"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/gray_500"
tools:text="크리에이터이름" />
</LinearLayout>
</kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailDetailView>
- Step 2: Detail custom view 추가
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)
}
}
}
- 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.
-
Step 1: 호출부 이미지 로딩 방식 확인
Run: rg -n "\.load\(|Glide\.with|iv_.*\.load|placeholder\(" app/src/main/java/kr/co/vividnext/sodalive
Expected: 대상 화면이 Coil 또는 Glide 중 어떤 방식을 사용하는지 확인한다.
- Step 1-1: 컴포넌트 내부 이미지 로더 비고정 확인
Run: rg -n "Glide\.with|coil|\.load\(" app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail
Expected: 검색 결과가 없어야 한다. imageView()만 노출하고 실제 이미지 로딩은 호출부가 수행한다.
- Step 2: Simple 호출부 바인딩 예시 문서화
binding.liveThumbnailSimple.bind(item)
binding.liveThumbnailSimple.imageView().load(item.imageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
}
binding.liveThumbnailSimple.setOnLiveThumbnailClick { liveThumbnailItem ->
// 호출 화면의 라이브 상세 이동 로직을 연결한다.
}
- Step 3: Detail 호출부 바인딩 예시 문서화
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 -
Step 1: changed Kotlin 파일 LSP diagnostics 확인
Run tool: lsp_diagnostics on each changed Kotlin file.
Expected: 새로 추가한 Kotlin 파일에 error가 없다.
- Step 2: 단위 테스트 실행
Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.livethumbnail.*"
Expected: BUILD SUCCESSFUL
- Step 3: Android resource merge 실행
Run: ./gradlew :app:mergeDebugResources
Expected: BUILD SUCCESSFUL
- Step 4: 전체 app test 실행
Run: ./gradlew :app:testDebugUnitTest
Expected: BUILD SUCCESSFUL 또는 변경과 무관한 기존 실패만 별도 기록한다.
- 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 참조로 확인된다.
- Step 4-2: Figma 시각 정합성 QA 기준 확인
Expected manual/screenshot QA:
-
Simple은70dp폭,70dpring,58dp원형 이미지, bottom centered LIVE badge, 1줄 creator name을 유지한다. -
Detail은266dp x 99dp,75dp원형 이미지, stroke 없는 LIVE capsule badge, start time/title/creator 1줄 말줄임을 유지한다. -
Step 5: 계획 문서 검증 기록 누적
Append to this file:
## 검증 기록
### 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`
- 결과: 실행 결과를 그대로 기록한다.
체크리스트
- AC1:
Simplevariant는70dproot width,70dp x 76dpprofile area,58dp원형 이미지, LIVE ring/badge를 사용한다. - AC2:
Detailvariant는266dp x 99dproot,75dp원형 이미지,149dptext column, stroke 없는 LIVE capsule badge를 사용한다. - AC3:
Detailroot와Simplering/badge stroke의 cyan 색상은color_62cfffresource를 통해 참조한다. - AC4: 모든 사용자 표시 텍스트는
maxLines=1,ellipsize=end를 적용한다. - AC5: 컴포넌트 내부는 이미지 로딩 라이브러리를 고정하지 않고
imageView()를 노출해 호출부가 Coil/Glide 등 기존 정책으로 로드한다. - AC6: 기존 레거시 live layout과 호출 화면은 이번 widget 계약에서 직접 변경하지 않는다.
- 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: 이 환경에
.xmlLSP 서버가 없어 실행 불가, 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:testDebugUnitTestlsp_diagnosticson changed Kotlin files
- 결과:
- livethumbnail 단위 테스트:
BUILD SUCCESSFUL - debug resource merge:
BUILD SUCCESSFUL - 전체 debug unit test:
BUILD SUCCESSFUL - Kotlin LSP diagnostics: 이 환경에
.ktLSP 서버가 없어 실행 불가, Gradle Kotlin compile/test로 대체 확인
- livethumbnail 단위 테스트:
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
- LIVE badge 및 사용자 텍스트 TextView에
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,8dpspacing은 기존@dimen/spacing_4,@dimen/spacing_6,@dimen/spacing_8로 치환#62CFFF는 이후color_62cfffresource로 분리하고 drawable에서 참조하도록 보강90dp,100dp, 컴포넌트 고유 width/height는 정확히 대응되는 기존 token이 없어 유지
- LIVE label:
2026-05-20 KST
- 무엇: 문서 작성 범위 검증
- 왜: 사용자가 구현이 아닌 문서 작성만 요청했으므로 코드 변경 없이 PRD와 구현 계획/TASK 문서만 준비했는지 확인
- 어떻게: Figma
24:4999,24:5017design context/screenshot 확인, 기존 v2 widget 및 live layout 패턴 확인 - 결과:
docs/prd/20260520_라이브썸네일컴포넌트_prd.md,docs/plan-task/20260520_라이브썸네일컴포넌트.md문서만 추가 대상으로 작성함