# 캐릭터 채팅 썸네일 컴포넌트 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:5032` 기준으로 캐릭터 이미지, 총 채팅 개수, 이름, 소개, 원작 작품명을 표시하는 재사용 가능한 캐릭터 채팅 썸네일 컴포넌트를 추가한다. **Architecture:** 캐릭터 카드 표시 데이터와 채팅 수 포맷을 순수 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. --- ## 작업 목표 - Figma `24:5032` 기준 `chat-thumbnail` 카드 UI를 구현한다. - 이미지 영역은 `185dp x 185dp`로 고정하고 centerCrop + clipping을 적용한다. - 왼쪽 상단 badge에 `ic_chat_message_count`와 캐릭터 총 채팅 개수를 표시한다. - 캐릭터 총 채팅 개수가 `100` 미만이면 왼쪽 상단 채팅 수 badge를 표시하지 않는다. - 캐릭터 이름은 1줄, 캐릭터 소개는 2줄, 원작 작품명은 1줄 말줄임으로 표시한다. - `hasOriginal == false`일 때 원작 작품명 영역은 `View.INVISIBLE`로 처리해 카드 높이를 유지한다. - 기존 캐릭터 목록 화면 연결은 별도 승인 전까지 하지 않는다. ## 파일 구조 - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailItem.kt` - 캐릭터 채팅 썸네일 UI에 필요한 최소 데이터 계약을 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatCountFormatter.kt` - 총 채팅 개수를 Figma 예시와 같은 표시 문자열로 변환한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailView.kt` - 텍스트 바인딩, 원작 표시 상태, 이미지 view 노출, 터치 callback을 처리한다. - Create: `app/src/main/res/layout/view_character_chat_thumbnail.xml` - Figma `24:5032` 기준 XML layout을 정의한다. - Add if missing: `app/src/main/res/drawable/bg_character_chat_thumbnail.xml` - root `gray_900` background + `14dp` radius를 정의한다. - Add if missing: `app/src/main/res/drawable/bg_character_chat_count_badge.xml` - 채팅 수 badge black `60%` background + `4dp` radius를 정의한다. - Add if missing: `app/src/main/res/drawable/bg_character_chat_thumbnail_dim.xml` - 이미지 하단 dim gradient를 정의한다. - Add if missing: `app/src/main/res/drawable/ic_chat_message_count.xml` - Figma 채팅 수 아이콘을 Android drawable로 추가한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailItemTest.kt` - 원작 표시 정책과 데이터 보존을 검증한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatCountFormatterTest.kt` - 채팅 수 표시 문자열을 검증한다. - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailViewTest.kt` - `bind`, `imageView`, `setOnCharacterClick` 동작을 Robolectric 기반 로컬 단위 테스트로 검증한다. - 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/livethumbnail/LiveThumbnailSimpleView.kt` - Read: `app/src/main/res/layout/item_character.xml` - Read: `app/src/main/res/layout/item_new_character_all.xml` - Read: `app/src/main/res/layout/item_other_character.xml` - Read: `app/src/main/res/values/colors.xml` - Read: `app/src/main/res/values/dimens.xml` - Read: `app/src/main/res/values/typography.xml` - [x] **Step 1: 관련 기존 코드 확인** Run: `rg -n "AudioContentCardView|LiveThumbnailSimpleView|item_character|item_new_character_all|ic_chat_message_count|ellipsize=\"end\"|maxLines=\"2\"" app/src/main/java app/src/main/res/layout app/src/main/res/drawable app/src/main/res/values` Expected: 기존 v2 custom view의 `imageView()` 노출 패턴, 캐릭터 카드 layout, 1줄/2줄 ellipsis 적용 예시, `ic_chat_message_count` 존재 여부를 확인한다. - [x] **Step 2: Figma 세부 컨텍스트 재확인** Run tools: - `Figma_get_design_context(24:5032)` - `Figma_get_screenshot(24:5032)` Expected: root size, image size, badge 위치, typography, color, radius, spacing을 확인한다. - [x] **Step 3: 구현 기준 token 정리** Expected token contract: - root width: `185dp` - image area: `185dp x 185dp` - root background: `gray_900`, radius `14dp` - chat count badge: start/top `8dp`, black `60%`, radius `4dp`, padding horizontal `4dp`, padding vertical `2dp` - chat count icon: `ic_chat_message_count`, `18dp x 18dp` - chat count text: Pretendard Regular `14sp`, `gray_100`, line-height `1.45` - text column: start/end `12dp`, top `176dp`, gap `6dp` - character name: Pretendard Bold `20sp`, white, maxLines `1` - character description: Pretendard Medium `14sp`, white, line-height `1.45`, maxLines `2` - original title: Pretendard Medium `12sp`, `soda_300`, maxLines `1` ### Task 2: Character thumbnail data contract TDD **Files:** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailItemTest.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailItem.kt` - [x] **Step 1: RED - item display contract 테스트 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test class CharacterChatThumbnailItemTest { @Test fun `item keeps character display fields`() { val item = sampleItem() assertEquals(10L, item.characterId) assertEquals("https://example.com/character.png", item.imageUrl) assertEquals("캐릭터 이름", item.characterName) assertEquals("캐릭터 소개", item.characterDescription) assertEquals(14000L, item.chatMessageCount) } @Test fun `original title is visible when item has original`() { val item = sampleItem(hasOriginal = true, originalTitle = "작품 이름") assertTrue(item.shouldShowOriginalTitle) assertEquals("작품 이름", item.originalTitle) } @Test fun `original title keeps space when item has no original`() { val item = sampleItem(hasOriginal = false, originalTitle = "") assertFalse(item.shouldShowOriginalTitle) assertEquals("", item.originalTitle) } @Test fun `chat count badge is hidden when count is less than one hundred`() { val item = sampleItem(chatMessageCount = 99L) assertFalse(item.shouldShowChatCountBadge) } @Test fun `chat count badge is visible when count is one hundred or more`() { val item = sampleItem(chatMessageCount = 100L) assertTrue(item.shouldShowChatCountBadge) } private fun sampleItem( hasOriginal: Boolean = true, originalTitle: String = "작품 이름", chatMessageCount: Long = 14000L ) = CharacterChatThumbnailItem( characterId = 10L, imageUrl = "https://example.com/character.png", characterName = "캐릭터 이름", characterDescription = "캐릭터 소개", chatMessageCount = chatMessageCount, hasOriginal = hasOriginal, originalTitle = originalTitle ) } ``` - [x] **Step 2: RED 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailItemTest"` Expected: `Unresolved reference 'CharacterChatThumbnailItem'`로 실패한다. - [x] **Step 3: GREEN - 최소 data contract 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail data class CharacterChatThumbnailItem( val characterId: Long, val imageUrl: String, val characterName: String, val characterDescription: String, val chatMessageCount: Long, val hasOriginal: Boolean, val originalTitle: String ) { val shouldShowOriginalTitle: Boolean = hasOriginal val shouldShowChatCountBadge: Boolean = chatMessageCount >= 100 } ``` - [x] **Step 4: GREEN 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailItemTest"` Expected: `BUILD SUCCESSFUL` ### Task 3: Chat count formatter TDD **Files:** - Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatCountFormatterTest.kt` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatCountFormatter.kt` - [x] **Step 1: RED - Figma 예시 기반 count format 테스트 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail import org.junit.Assert.assertEquals import org.junit.Test class CharacterChatCountFormatterTest { @Test fun `zero count displays zero`() { assertEquals("0", CharacterChatCountFormatter.format(0)) } @Test fun `under ten thousand displays plain number`() { assertEquals("9999", CharacterChatCountFormatter.format(9999)) } @Test fun `ten thousand or more displays korean ten thousand unit`() { assertEquals("1만", CharacterChatCountFormatter.format(10000)) assertEquals("1.4만", CharacterChatCountFormatter.format(14000)) assertEquals("1.9만", CharacterChatCountFormatter.format(19999)) assertEquals("2.9만", CharacterChatCountFormatter.format(29999)) assertEquals("9.9만", CharacterChatCountFormatter.format(99999)) } @Test fun `one hundred thousand or more displays integer korean ten thousand unit`() { assertEquals("10만", CharacterChatCountFormatter.format(100000)) assertEquals("10만", CharacterChatCountFormatter.format(109999)) assertEquals("99만", CharacterChatCountFormatter.format(999999)) } @Test fun `negative count displays zero`() { assertEquals("0", CharacterChatCountFormatter.format(-1)) } } ``` - [x] **Step 2: RED 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatCountFormatterTest"` Expected: `Unresolved reference 'CharacterChatCountFormatter'`로 실패한다. - [x] **Step 3: GREEN - formatter 추가** ```kotlin package kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail import java.util.Locale object CharacterChatCountFormatter { fun format(count: Long): String { val safeCount = count.coerceAtLeast(0) if (safeCount < 10_000) return safeCount.toString() val value = safeCount / 10_000.0 return if (safeCount % 10_000L == 0L) { "${safeCount / 10_000}만" } else { String.format(Locale.KOREAN, "%.1f만", floor(value * 10) / 10) } } } ``` - [x] **Step 4: GREEN 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatCountFormatterTest"` Expected: `BUILD SUCCESSFUL` ### Task 4: Drawable resources 추가 **Files:** - Add if missing: `app/src/main/res/drawable/bg_character_chat_thumbnail.xml` - Add if missing: `app/src/main/res/drawable/bg_character_chat_count_badge.xml` - Add if missing: `app/src/main/res/drawable/bg_character_chat_thumbnail_dim.xml` - Add if missing: `app/src/main/res/drawable/ic_chat_message_count.xml` - [x] **Step 1: 기존 동일 리소스 존재 여부 확인** Run: `rg -n "ic_chat_message_count|character_chat_thumbnail|4fd2f9|gray_100|gray_900" app/src/main/res/drawable app/src/main/res/values` Expected: 재사용 가능한 drawable/color가 있으면 새 파일을 만들지 않고 계획 문서에 재사용 파일명을 기록한다. `ic_chat_message_count`가 없으면 Figma 에셋을 vector/drawable로 추가한다. - [x] **Step 2: root background drawable 추가** ```xml ``` - [x] **Step 3: chat count badge drawable 추가** ```xml ``` - [x] **Step 4: dim gradient drawable 추가** ```xml ``` - [x] **Step 5: Resource merge 확인** Run: `./gradlew :app:mergeDebugResources` Expected: `BUILD SUCCESSFUL` ### Task 5: XML layout 및 custom view 구현 **Files:** - Create: `app/src/main/res/layout/view_character_chat_thumbnail.xml` - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailView.kt` - [x] **Step 1: XML layout 추가** Required layout properties: - Root custom view width `185dp`, height `wrap_content`, background `bg_character_chat_thumbnail`, clipped rounded card. - ImageView width/height `185dp`, scaleType `centerCrop`, `contentDescription=@null`. - Dim gradient View overlays the image area. - Chat count badge at start/top `8dp`, background `bg_character_chat_count_badge`, padding horizontal `4dp`, padding vertical `2dp`. - Badge icon uses `@drawable/ic_chat_message_count`, width/height `18dp`. - Badge text uses `@style/Typography.Body6`, `@color/gray_100`, single line. - Text container start/end margin `12dp`, top `176dp`, vertical gap `6dp`. - Character name TextView: `@style/Typography.Heading3`, `maxLines=1`, `ellipsize=end`. - Character description TextView: `@style/Typography.Body5`, `maxLines=2`, `ellipsize=end`. - Original title TextView: `@style/Typography.Caption2`, `@color/soda_300`, `maxLines=1`, `ellipsize=end`. - [x] **Step 2: custom view 추가** Required API: ```kotlin fun bind(item: CharacterChatThumbnailItem) fun imageView(): ImageView fun setOnCharacterClick(listener: ((CharacterChatThumbnailItem) -> Unit)?) ``` Required behavior: - `bind()`는 이름, 소개, 채팅 수, 원작 작품명을 바인딩한다. - 채팅 수는 `CharacterChatCountFormatter.format(item.chatMessageCount)` 결과를 표시한다. - `item.shouldShowChatCountBadge == true`이면 chat count badge container는 `View.VISIBLE`이다. - `item.shouldShowChatCountBadge == false`이면 chat count badge container는 `View.GONE`이다. - `item.shouldShowOriginalTitle == true`이면 original title TextView는 `View.VISIBLE`이다. - `item.shouldShowOriginalTitle == false`이면 original title TextView는 `View.INVISIBLE`이다. - `imageView()`는 실제 이미지 로딩을 호출부가 수행할 수 있도록 ImageView를 반환한다. - click listener가 없으면 root click listener를 제거한다. - [x] **Step 3: Resource merge 확인** Run: `./gradlew :app:mergeDebugResources` Expected: `BUILD SUCCESSFUL` ### Task 6: 실제 이미지 로딩 호출 예시 정리 **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 2: 컴포넌트 내부 이미지 로더 비고정 확인** Run: `rg -n "Glide\.with|coil|\.load\(" app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail` Expected: 검색 결과가 없어야 한다. `imageView()`만 노출하고 실제 이미지 로딩은 호출부가 수행한다. - [x] **Step 3: 호출부 바인딩 예시 문서화** ```kotlin binding.characterChatThumbnail.bind(item) binding.characterChatThumbnail.imageView().load(item.imageUrl) { crossfade(true) placeholder(R.drawable.ic_place_holder) } binding.characterChatThumbnail.setOnCharacterClick { characterItem -> // 호출 화면의 캐릭터 상세 또는 채팅 진입 로직을 연결한다. } ``` Expected: 실제 구현 시 이미지 영역은 Figma placeholder가 아니라 `item.imageUrl`에서 로드된 이미지로 표시된다. ### Task 7: 최종 검증 **Files:** - Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailItem.kt` - Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatCountFormatter.kt` - Check: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailView.kt` - Check: `app/src/main/res/layout/view_character_chat_thumbnail.xml` - Check: `app/src/main/res/drawable/bg_character_chat_thumbnail.xml` - Check: `app/src/main/res/drawable/bg_character_chat_count_badge.xml` - Check: `app/src/main/res/drawable/bg_character_chat_thumbnail_dim.xml` - Check: `app/src/main/res/drawable/ic_chat_message_count.xml` - Modify: `docs/plan-task/20260520_캐릭터채팅썸네일컴포넌트.md` - [x] **Step 1: changed Kotlin 파일 LSP diagnostics 확인** Run tool: `lsp_diagnostics` on each changed Kotlin file. Expected: 새로 추가한 Kotlin 파일에 error가 없다. Kotlin LSP가 환경에 없으면 Gradle compile/test 결과로 대체하고 검증 기록에 남긴다. - [x] **Step 2: 단위 테스트 실행** Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.*"` Expected: `BUILD SUCCESSFUL` - [x] **Step 3: Android resource merge 실행** Run: `./gradlew :app:mergeDebugResources` Expected: `BUILD SUCCESSFUL` - [x] **Step 4: UI contract 속성 확인** Run: `rg -n "ic_chat_message_count|shouldShowChatCountBadge|View.GONE|GONE|View.INVISIBLE|INVISIBLE|maxLines=\"1\"|maxLines=\"2\"|ellipsize=\"end\"|centerCrop|CharacterChatCountFormatter" app/src/main/res/layout app/src/main/res/drawable app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail` Expected: 채팅 수 아이콘, `100` 미만 채팅 수 badge `GONE` 처리, 원작 invisible 처리, 이름 1줄, 소개 2줄, 원작 1줄, 이미지 centerCrop, 채팅 수 formatter 적용이 확인된다. - [x] **Step 5: 계획 문서 검증 기록 누적** Append to this file: ```markdown ## 검증 기록 ### YYYY-MM-DD HH:mm KST - 무엇: 캐릭터 채팅 썸네일 컴포넌트 구현 검증 - 왜: Figma `24:5032` 기준 UI와 총 채팅 수/원작 표시 계약이 동작하는지 확인 - 어떻게: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.*"` - `./gradlew :app:mergeDebugResources` - `rg -n "ic_chat_message_count|shouldShowChatCountBadge|View.GONE|GONE|View.INVISIBLE|INVISIBLE|maxLines=\"1\"|maxLines=\"2\"|ellipsize=\"end\"|centerCrop|CharacterChatCountFormatter" app/src/main/res/layout app/src/main/res/drawable app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail` - 결과: 실행 결과를 그대로 기록한다. ``` ## 체크리스트 - [x] AC1: root card는 Figma `24:5032` 기준 `185dp` 폭, `gray_900` 배경, `14dp` radius를 사용한다. - [x] AC2: 이미지 영역은 `185dp x 185dp`로 고정하고 `centerCrop`으로 표시한다. - [x] AC3: `chatMessageCount >= 100`이면 왼쪽 상단 badge는 `ic_chat_message_count`와 총 채팅 개수 텍스트를 표시한다. - [x] AC4: `chatMessageCount < 100`이면 왼쪽 상단 채팅 수 badge를 표시하지 않는다. - [x] AC5: 채팅 수 `14000`은 Figma 예시와 같이 `1.4만`으로 표시되고, `19999`는 반올림하지 않고 `1.9만`으로 절삭 표시된다. `10만` 이상은 소수점 없이 정수 만 단위로 표시된다. - [x] AC6: 캐릭터 이름은 `maxLines=1`, `ellipsize=end`를 적용한다. - [x] AC7: 캐릭터 소개는 `maxLines=2`, `ellipsize=end`를 적용한다. - [x] AC8: 원작 작품명은 `maxLines=1`, `ellipsize=end`를 적용한다. - [x] AC9: `hasOriginal == true`이면 원작 작품명 TextView가 `View.VISIBLE`로 표시된다. - [x] AC10: `hasOriginal == false`이면 원작 작품명 TextView가 `View.INVISIBLE`로 숨겨지고 카드 높이를 유지한다. - [x] AC11: 컴포넌트 내부는 이미지 로딩 라이브러리를 고정하지 않고 `imageView()`를 노출해 호출부가 Coil/Glide 등 기존 정책으로 로드한다. - [x] AC12: 기존 캐릭터 목록 화면은 사용자 추가 승인 없이 교체하지 않는다. ## 검증 기록 ### 2026-05-20 KST - 무엇: 문서 작성 범위 검증 - 왜: 사용자가 구현이 아닌 문서 작성만 요청했으므로 코드 변경 없이 PRD와 구현 계획/TASK 문서만 준비했는지 확인 - 어떻게: - `Figma_get_design_context(24:5032)` - `Figma_get_screenshot(24:5032)` - `read(docs/agent-guides/workflow-docs-commits.md)` - `read(docs/prd/sample-prd.md)` - `read(docs/prd/20260520_라이브썸네일컴포넌트_prd.md)` - `read(docs/plan-task/20260520_라이브썸네일컴포넌트.md)` - `rg -n "format.*Count|count.*format|만|K|chat.*count|message.*count|DecimalFormat|NumberFormat|abbrev|abbreviat" app/src/main/java app/src/main/res docs/prd docs/plan-task` - `rg -n "AudioContentCardView|CreatorRanking.*CardView|LiveThumbnail.*View|ellipsize=\"end\"|maxLines=\"2\"" app/src/main/java/kr/co/vividnext/sodalive/v2 app/src/main/res/layout app/src/main/res/values` - 결과: - PRD 문서는 `docs/prd/20260520_캐릭터채팅썸네일컴포넌트_prd.md`에 작성했다. - 계획/TASK 문서는 `docs/plan-task/20260520_캐릭터채팅썸네일컴포넌트.md`에 작성했다. - Figma `24:5032`에서 `chat-thumbnail`, `185dp` 이미지 영역, 좌상단 채팅 수 badge, 이름/소개/원작명 텍스트 구조를 확인했다. - 사용자 요청에 따라 코드, 리소스, 레이아웃 구현 파일은 변경하지 않았다. - 실제 구현과 빌드 검증은 사용자 승인 후 계획 문서 체크리스트에 따라 진행한다. ### 2026-05-20 KST - 무엇: 채팅 수 100 미만 badge 미표시 요구사항 문서 반영 - 왜: 사용자가 총 채팅 개수가 100 미만이면 채팅 수 UI를 표시하지 않도록 문서 수정을 요청했다. - 어떻게: - `docs/prd/20260520_캐릭터채팅썸네일컴포넌트_prd.md`의 Goals, Display Requirements, Edge Cases, Metrics에 `chatMessageCount < 100` 미표시 조건을 추가했다. - `docs/plan-task/20260520_캐릭터채팅썸네일컴포넌트.md`의 item contract 테스트, `CharacterChatThumbnailItem` 예시, custom view 바인딩 요구사항, 최종 검증 rg, AC에 동일 조건을 추가했다. - 결과: 코드/리소스/레이아웃 구현 없이 문서에만 반영했다. ### 2026-05-20 KST - 무엇: 캐릭터 채팅 썸네일 컴포넌트 구현 검증 - 왜: Figma `24:5032` 기준 UI와 총 채팅 수/원작 표시 계약이 동작하는지 확인했다. - 어떻게: - `lsp_diagnostics` on changed Kotlin files - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.*"` - `./gradlew :app:mergeDebugResources` - `rg -n "ic_chat_message_count|shouldShowChatCountBadge|View.GONE|GONE|View.INVISIBLE|INVISIBLE|maxLines=\"1\"|maxLines=\"2\"|ellipsize=\"end\"|centerCrop|CharacterChatCountFormatter" app/src/main/res/layout app/src/main/res/drawable app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail` - `rg -n "Glide\.with|coil|\.load\(" app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail` - 결과: - Kotlin LSP diagnostics: 이 환경에 `.kt` LSP 서버가 없어 실행 불가, Gradle compile/test로 대체 확인했다. - characterchatthumbnail 단위 테스트: `BUILD SUCCESSFUL` - debug resource merge: `BUILD SUCCESSFUL` - UI contract 검색: `ic_chat_message_count`, `shouldShowChatCountBadge`, `View.GONE`, `View.INVISIBLE`, `centerCrop`, `maxLines`, `ellipsize`, `CharacterChatCountFormatter` 확인 - 이미지 로더 검색: 결과 없음 - 기존 캐릭터 목록 화면과 API/DTO는 변경하지 않았다. ### 2026-05-20 KST - 무엇: 코드 리뷰 보완 반영 - 왜: `CharacterChatThumbnailSize.Figma`와 XML의 크기 source of truth 분리를 제거하고, `CharacterChatThumbnailView`의 public 동작을 직접 검증하기 위해 보완했다. - 어떻게: - 크기 source of truth는 `view_character_chat_thumbnail.xml`로 단일화하고 `CharacterChatThumbnailSize.kt`, `CharacterChatThumbnailSizeTest.kt`를 제거했다. - Robolectric 기반 `CharacterChatThumbnailViewTest`를 추가해 `bind`, `imageView`, `setOnCharacterClick` 동작을 검증한다. - 로컬 View 테스트 실행을 위해 `testOptions.unitTests.includeAndroidResources = true`, `androidx.test:core-ktx`, `robolectric`, `app/src/test/AndroidManifest.xml`을 추가했다. - 결과: - `CharacterChatThumbnailViewTest` 단독 실행: `BUILD SUCCESSFUL` - `characterchatthumbnail` 패키지 단위 테스트: `BUILD SUCCESSFUL` - debug resource merge: `BUILD SUCCESSFUL` - debug Kotlin compile: `BUILD SUCCESSFUL`