diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailDetailView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailDetailView.kt
new file mode 100644
index 00000000..11a29e0d
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailDetailView.kt
@@ -0,0 +1,61 @@
+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
+ applyClickState(item)
+ }
+
+ fun imageView(): ImageView = requireNotNull(image)
+
+ fun setOnLiveThumbnailClick(listener: ((LiveThumbnailItem) -> Unit)?) {
+ clickListener = listener
+ currentItem?.let(::applyClickState)
+ }
+
+ private fun applyClickState(item: LiveThumbnailItem) {
+ isClickable = clickListener != null
+ setOnClickListener(if (isClickable) View.OnClickListener { clickListener?.invoke(item) } else null)
+ }
+
+ private fun circleOutlineProvider() = object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ outline.setOval(0, 0, view.width, view.height)
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItem.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItem.kt
new file mode 100644
index 00000000..72b8ba78
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItem.kt
@@ -0,0 +1,10 @@
+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
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSimpleView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSimpleView.kt
new file mode 100644
index 00000000..57286f1f
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSimpleView.kt
@@ -0,0 +1,55 @@
+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
+ applyClickState(item)
+ }
+
+ fun imageView(): ImageView = requireNotNull(image)
+
+ fun setOnLiveThumbnailClick(listener: ((LiveThumbnailItem) -> Unit)?) {
+ clickListener = listener
+ currentItem?.let(::applyClickState)
+ }
+
+ private fun applyClickState(item: LiveThumbnailItem) {
+ isClickable = clickListener != null
+ setOnClickListener(if (isClickable) View.OnClickListener { clickListener?.invoke(item) } else null)
+ }
+
+ private fun circleOutlineProvider() = object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ outline.setOval(0, 0, view.width, view.height)
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSize.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSize.kt
new file mode 100644
index 00000000..18ba5514
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSize.kt
@@ -0,0 +1,28 @@
+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
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailVariant.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailVariant.kt
new file mode 100644
index 00000000..10d063e4
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailVariant.kt
@@ -0,0 +1,6 @@
+package kr.co.vividnext.sodalive.v2.widget.livethumbnail
+
+enum class LiveThumbnailVariant {
+ Simple,
+ Detail
+}
diff --git a/app/src/main/res/drawable-mdpi/ic_chat_message_count.png b/app/src/main/res/drawable-mdpi/ic_chat_message_count.png
new file mode 100644
index 00000000..77bf9fe7
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_chat_message_count.png differ
diff --git a/app/src/main/res/drawable/bg_live_thumbnail_badge.xml b/app/src/main/res/drawable/bg_live_thumbnail_badge.xml
new file mode 100644
index 00000000..cd1c4c56
--- /dev/null
+++ b/app/src/main/res/drawable/bg_live_thumbnail_badge.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_live_thumbnail_badge_capsule.xml b/app/src/main/res/drawable/bg_live_thumbnail_badge_capsule.xml
new file mode 100644
index 00000000..7e565d07
--- /dev/null
+++ b/app/src/main/res/drawable/bg_live_thumbnail_badge_capsule.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_live_thumbnail_detail.xml b/app/src/main/res/drawable/bg_live_thumbnail_detail.xml
new file mode 100644
index 00000000..6bf7ef17
--- /dev/null
+++ b/app/src/main/res/drawable/bg_live_thumbnail_detail.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_live_thumbnail_dot.xml b/app/src/main/res/drawable/bg_live_thumbnail_dot.xml
new file mode 100644
index 00000000..38791d5c
--- /dev/null
+++ b/app/src/main/res/drawable/bg_live_thumbnail_dot.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_live_thumbnail_ring.xml b/app/src/main/res/drawable/bg_live_thumbnail_ring.xml
new file mode 100644
index 00000000..1233d336
--- /dev/null
+++ b/app/src/main/res/drawable/bg_live_thumbnail_ring.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/view_live_thumbnail_detail.xml b/app/src/main/res/layout/view_live_thumbnail_detail.xml
new file mode 100644
index 00000000..2fe7f357
--- /dev/null
+++ b/app/src/main/res/layout/view_live_thumbnail_detail.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/view_live_thumbnail_simple.xml b/app/src/main/res/layout/view_live_thumbnail_simple.xml
new file mode 100644
index 00000000..40663a9a
--- /dev/null
+++ b/app/src/main/res/layout/view_live_thumbnail_simple.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index b14f3562..7197565f 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -87,6 +87,7 @@
#59548F
#FFCB14
#4D6AA4
+ #62CFFF
#2D7390
#548F7D
#CC979797
diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItemTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItemTest.kt
new file mode 100644
index 00000000..7d4dd865
--- /dev/null
+++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailItemTest.kt
@@ -0,0 +1,37 @@
+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)
+ }
+}
diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSizeTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSizeTest.kt
new file mode 100644
index 00000000..65355467
--- /dev/null
+++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSizeTest.kt
@@ -0,0 +1,29 @@
+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(null, size.rootHeightDp)
+ 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(null, size.profileAreaHeightDp)
+ assertEquals(75, size.imageSizeDp)
+ assertEquals(149, size.textWidthDp)
+ }
+}
diff --git a/docs/plan-task/20260520_라이브썸네일컴포넌트.md b/docs/plan-task/20260520_라이브썸네일컴포넌트.md
new file mode 100644
index 00000000..22d9f34f
--- /dev/null
+++ b/docs/plan-task/20260520_라이브썸네일컴포넌트.md
@@ -0,0 +1,844 @@
+# 라이브 썸네일 컴포넌트 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` 문서만 추가 대상으로 작성함
diff --git a/docs/prd/20260520_라이브썸네일컴포넌트_prd.md b/docs/prd/20260520_라이브썸네일컴포넌트_prd.md
new file mode 100644
index 00000000..4f2a3480
--- /dev/null
+++ b/docs/prd/20260520_라이브썸네일컴포넌트_prd.md
@@ -0,0 +1,148 @@
+# PRD: 라이브 썸네일 컴포넌트
+
+## 1. Overview
+Figma `24:4999`, `24:5017` 디자인을 기준으로 현재 라이브 중인 상태를 표시하는 Android XML Views 기반 라이브 썸네일 컴포넌트를 문서화한다.
+
+---
+
+## 2. Problem
+- 현재 Figma의 프로필 이미지 영역은 빈 이미지로 표시되어 있으나, 실제 앱에서는 라이브 크리에이터의 프로필 또는 커버 이미지를 표시해야 한다.
+- 라이브 썸네일은 세로형 프로필 variant와 가로형 상세 variant가 함께 필요하다.
+- 라이브 제목과 크리에이터 이름이 지정된 영역을 넘으면 UI가 깨질 수 있으므로 모든 텍스트는 1줄 제한과 말줄임 처리가 필요하다.
+- 기존 라이브 관련 layout은 화면별로 흩어져 있어, 새 Figma variant를 재사용 가능한 v2 widget 계약으로 분리할 필요가 있다.
+
+---
+
+## 3. Goals
+- Figma `24:4999` 기준 세로형 `Simple` 라이브 썸네일을 제공한다.
+- Figma `24:5017` 기준 가로형 `Detail` 라이브 썸네일을 제공한다.
+- Figma에서 빈 이미지로 보이는 영역에는 실제 이미지 URL을 바인딩할 수 있는 `ImageView`를 제공한다.
+- 모든 텍스트는 `maxLines=1`, `ellipsize=end`로 표시한다.
+- LIVE 배지, 라이브 테두리, 배경, 간격, typography는 Figma 기준을 Android resource로 옮긴다.
+- 이미지 로딩 라이브러리는 컴포넌트 내부에 고정하지 않고, 기존 호출부가 Coil/Glide 등 현재 화면의 방식을 사용해 이미지를 로드할 수 있도록 한다.
+
+---
+
+## 4. Non-Goals
+- 이번 문서 범위에서는 서버 API 또는 DTO 필드명을 변경하지 않는다.
+- 라이브 목록 화면 전체 개편, 정렬, pagination, filtering은 포함하지 않는다.
+- Compose 컴포넌트 또는 Compose Theme를 추가하지 않는다.
+- Figma에 없는 skeleton loading, shimmer, pressed animation, 시청자 수, 가격, 잠금 배지는 추가하지 않는다.
+- 이미지 로딩 라이브러리 교체를 수행하지 않는다.
+- 라이브 상태 계산 로직 또는 라이브 입장 정책을 새로 만들지 않는다.
+
+---
+
+## 5. Target Users
+- 라이브 중인 크리에이터를 탐색하는 앱 사용자.
+- 메인/라이브/크리에이터 화면에서 라이브 썸네일 UI를 재사용해야 하는 Android 개발자.
+
+---
+
+## 6. User Stories
+- 사용자는 현재 라이브 중인 크리에이터를 LIVE 배지와 테두리로 빠르게 구분하고 싶다.
+- 사용자는 라이브 제목과 크리에이터 이름이 길어도 레이아웃이 깨지지 않는 썸네일을 보고 싶다.
+- 개발자는 같은 라이브 상태 UI를 세로형/가로형 variant로 일관되게 바인딩하고 싶다.
+- 개발자는 기존 이미지 로딩 방식을 유지하면서 실제 이미지 URL만 연결하고 싶다.
+
+---
+
+## 7. Core Features
+
+### Live Thumbnail Widget
+현재 라이브 중인 콘텐츠를 `Simple`, `Detail` 두 variant로 표시한다.
+
+#### Figma References
+- Simple live thumbnail: https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-4999&m=dev
+- Detail live thumbnail: https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-5017&m=dev
+
+#### Variant Requirements
+| Variant | Figma node | Size policy | Primary use |
+| --- | --- | --- | --- |
+| `Simple` | `24:4999` | 기본 폭 `70dp`, profile area `70dp x 76dp`, 이름 영역 `70dp` | 가로 스크롤 프로필 목록, 작은 추천 영역 |
+| `Detail` | `24:5017` | 기본 `266dp x 99dp`, pill radius `90dp` | 넓은 카드형 라이브 추천 영역 |
+
+#### Figma Token Requirements
+- `Simple` root는 세로 배치, gap `6dp`, width `70dp`를 기준으로 한다.
+- `Simple` 실제 이미지 영역은 `58dp x 58dp`, 원형 crop, top/start `6dp` 위치를 기준으로 한다.
+- `Simple` 라이브 ring은 `70dp x 70dp`이며 ocean-blue gradient 또는 동일한 기존 drawable을 사용한다.
+- `Simple` LIVE badge는 `50dp x 18dp`, black background, `#62CFFF` border `2dp`, radius `759dp`, icon `8dp`, text `12sp` regular white를 기준으로 한다.
+- `Simple` 크리에이터 이름은 `@style/Typography.Body5`, white, center 정렬이다.
+- `Detail` root는 `266dp x 99dp`, `gray_900` (`#202020`) background, `#62CFFF` border `2dp`, radius `90dp`, overflow clipped 형태를 기준으로 한다.
+- `Detail` 실제 이미지 영역은 `75dp x 75dp`, left `10dp`, vertical center, 원형 crop을 기준으로 한다.
+- `Detail` 텍스트 column은 left `93dp`, width `149dp`, vertical center, 내부 gap `4dp`를 기준으로 한다.
+- `Detail` LIVE badge는 `Tag(size=s,type=live)` 형태로 black background, radius `100dp`, height `18dp`, horizontal padding `4dp`, icon/text gap `4dp`를 기준으로 한다.
+- `Detail` LIVE badge는 `Simple`의 cyan stroke badge와 달리 stroke 없는 capsule background를 사용한다.
+- `Detail` 라이브 시작 시간은 `@style/Typography.Body6`, `gray_500` (`#959595`)를 기준으로 한다.
+- `Detail` 라이브 제목은 `@style/Typography.Heading4`, white를 기준으로 한다.
+- `Detail` 크리에이터 이름은 `@style/Typography.Body6`, `gray_500` (`#959595`)를 기준으로 한다.
+
+#### Display Requirements
+- 실제 이미지는 Figma placeholder가 아니라 `imageUrl` 데이터로 표시한다.
+- 이미지 로딩 실패 시 placeholder 정책은 호출 화면의 기존 이미지 로딩 정책을 따른다.
+- `Simple`은 크리에이터 이름만 표시한다.
+- `Detail`은 LIVE badge, 라이브 시작 시간, 라이브 제목, 크리에이터 이름을 표시한다.
+- `liveStartTimeText`가 비어 있으면 `00:00`을 강제하지 않고 빈 문자열 또는 호출부 정책을 따른다.
+- 모든 텍스트는 한 줄만 표시한다.
+- 모든 텍스트는 영역을 초과하면 끝 말줄임 처리한다.
+- LIVE 문구는 고정 문자열 `LIVE`로 표시한다.
+- 터치 동작은 컴포넌트 내부에서 목적지를 결정하지 않고 호출부 callback으로 위임한다.
+
+#### Data Contract Requirements
+- 최소 데이터 계약은 다음 정보를 포함해야 한다.
+ - `liveId`: 라이브 식별자.
+ - `creatorId`: 크리에이터 식별자.
+ - `imageUrl`: 실제로 표시할 프로필 또는 라이브 이미지 URL.
+ - `title`: 라이브 제목. `Detail` variant에서 사용한다.
+ - `creatorName`: 크리에이터 이름.
+ - `liveStartTimeText`: 라이브 시작 시간 표시 문자열. `Detail` variant에서 사용한다.
+- UI는 `imageUrl`의 의미가 프로필 이미지인지 커버 이미지인지 판단하지 않는다. 호출부가 variant 목적에 맞는 URL을 전달한다.
+
+#### Edge Cases
+- `imageUrl`이 비어 있으면 호출부 이미지 로딩 정책에 따라 placeholder 또는 빈 상태를 표시한다.
+- `creatorName`이 비어 있으면 해당 TextView는 빈 문자열로 유지하고 layout은 유지한다.
+- `title`이 비어 있으면 `Detail` title TextView는 빈 문자열로 유지하고 layout은 유지한다.
+- 긴 한글/영문/숫자 혼합 텍스트는 1줄 말줄임으로 처리한다.
+- `liveStartTimeText`가 긴 문자열이어도 1줄 말줄임으로 처리한다.
+
+---
+
+## 8. UX / UI Expectations
+- 전체 컴포넌트는 어두운 배경에서 사용하는 것을 전제로 한다.
+- 라이브 상태는 `#62CFFF` 계열 ring/border와 LIVE badge로 식별 가능해야 한다.
+- 실제 이미지가 원형 영역을 벗어나지 않도록 centerCrop + circle clipping을 적용한다.
+- 세로형과 가로형 모두 Figma의 둥근 형태와 clipping을 유지한다.
+- 텍스트가 길어도 컴포넌트의 지정 width를 밀어내지 않아야 한다.
+- Android 접근성 관점에서 장식용 LIVE dot/ring은 `contentDescription=@null`로 둔다.
+
+---
+
+## 9. Technical Constraints
+- 현재 프로젝트는 Android XML Views + Kotlin custom View + ViewBinding/resource 기반이므로 XML layout과 Kotlin custom view 패턴을 우선한다.
+- 신규 Kotlin 코드는 `app/src/main/java/kr/co/vividnext/sodalive/v2` 하위 패키지에 작성한다.
+- 재사용 가능한 위젯은 `kr.co.vividnext.sodalive.v2.widget.livethumbnail` 하위에 둔다.
+- 기존 `AudioContentCardView`, `CreatorRanking*CardView`처럼 custom view는 텍스트/상태를 바인딩하고, 실제 이미지 로딩은 `imageView()`를 통해 호출부가 처리할 수 있게 한다.
+- `gray_900`, `gray_500`, `white` 등 기존 color resource를 우선 재사용한다.
+- Pretendard font는 기존 `@font/regular`, `@font/medium`, `@font/bold`를 사용한다.
+- 기존 Typography/spacing/radius token과 정확히 대응되는 값은 신규 token을 만들지 않고 재사용한다.
+- `#62CFFF` 또는 ocean-blue gradient에 해당하는 기존 리소스가 없으면 구현 단계에서 drawable/color resource를 추가하고, drawable에서는 literal 색상보다 color resource를 참조한다.
+- 기존 레거시 layout(`item_home_live.xml`, `item_live_now.xml`, `item_live_now_all.xml`)은 참고 대상으로만 사용하고, 이번 widget 계약에서 직접 변경하지 않는다.
+
+---
+
+## 10. Metrics
+- `Simple` variant가 Figma `24:4999`의 주요 크기, ring, LIVE badge, 1줄 이름 표시와 일치한다.
+- `Detail` variant가 Figma `24:5017`의 주요 크기, 배경, border, 이미지, LIVE/time/title/name 배치와 일치한다.
+- 실제 이미지 URL을 호출부에서 `ImageView`에 로드할 수 있다.
+- 컴포넌트 내부에는 Coil/Glide 등 이미지 로딩 라이브러리 import나 `.load()` 호출이 없다.
+- 모든 텍스트 TextView에 `maxLines=1`과 `ellipsize=end`가 적용된다.
+- 원형 이미지 clipping과 Figma 주요 spacing/size/badge 형태는 XML/코드 속성 확인 또는 screenshot/manual QA로 확인한다.
+- 관련 local unit test, Android resource merge, build가 성공한다.
+
+---
+
+## 11. Open Questions
+- `Simple` variant의 이미지 URL은 기본적으로 크리에이터 프로필 이미지를 전달하는 것으로 가정한다.
+- `Detail` variant의 이미지 URL은 현재 Figma가 프로필형 원형 이미지를 사용하므로 프로필 이미지를 전달하는 것으로 가정한다. 호출 화면에서 라이브 커버를 써야 한다면 같은 `imageUrl` 계약에 다른 URL을 전달한다.
+- `liveStartTimeText` 포맷은 이 문서에서 계산하지 않고 호출부가 완성된 문자열로 전달한다.
+- Figma `get_design_context`와 screenshot 확인 결과, 빈 이미지 영역은 실제 이미지 바인딩 영역으로 문서화했다.