Files
sodalive-android/docs/plan-task/20260520_캐릭터채팅썸네일컴포넌트.md

26 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: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

  • 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 존재 여부를 확인한다.

  • 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을 확인한다.

  • 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

  • Step 1: RED - item display contract 테스트 추가

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
    )
}
  • Step 2: RED 실행

Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailItemTest"

Expected: Unresolved reference 'CharacterChatThumbnailItem'로 실패한다.

  • Step 3: GREEN - 최소 data contract 추가
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
}
  • 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

  • Step 1: RED - Figma 예시 기반 count format 테스트 추가

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))
    }
}
  • Step 2: RED 실행

Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatCountFormatterTest"

Expected: Unresolved reference 'CharacterChatCountFormatter'로 실패한다.

  • Step 3: GREEN - formatter 추가
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)
        }
    }
}
  • 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

  • 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로 추가한다.

  • Step 2: root background 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" />
    <corners android:radius="14dp" />
</shape>
  • Step 3: chat count 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="#99000000" />
    <corners android:radius="4dp" />
</shape>
  • Step 4: dim gradient drawable 추가
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
        android:angle="270"
        android:endColor="#1F1F1F"
        android:startColor="#001F1F1F" />
</shape>
  • 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

  • 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.

  • Step 2: custom view 추가

Required API:

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를 제거한다.

  • 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.

  • Step 1: 호출부 이미지 로딩 방식 확인

Run: rg -n "\.load\(|Glide\.with|iv_.*\.load|placeholder\(" app/src/main/java/kr/co/vividnext/sodalive

Expected: 대상 화면이 Coil 또는 Glide 중 어떤 방식을 사용하는지 확인한다.

  • Step 2: 컴포넌트 내부 이미지 로더 비고정 확인

Run: rg -n "Glide\.with|coil|\.load\(" app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail

Expected: 검색 결과가 없어야 한다. imageView()만 노출하고 실제 이미지 로딩은 호출부가 수행한다.

  • Step 3: 호출부 바인딩 예시 문서화
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

  • Step 1: changed Kotlin 파일 LSP diagnostics 확인

Run tool: lsp_diagnostics on each changed Kotlin file.

Expected: 새로 추가한 Kotlin 파일에 error가 없다. Kotlin LSP가 환경에 없으면 Gradle compile/test 결과로 대체하고 검증 기록에 남긴다.

  • Step 2: 단위 테스트 실행

Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.*"

Expected: BUILD SUCCESSFUL

  • Step 3: Android resource merge 실행

Run: ./gradlew :app:mergeDebugResources

Expected: BUILD SUCCESSFUL

  • 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 적용이 확인된다.

  • Step 5: 계획 문서 검증 기록 누적

Append to this file:

## 검증 기록

### 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`
- 결과: 실행 결과를 그대로 기록한다.

체크리스트

  • AC1: root card는 Figma 24:5032 기준 185dp 폭, gray_900 배경, 14dp radius를 사용한다.
  • AC2: 이미지 영역은 185dp x 185dp로 고정하고 centerCrop으로 표시한다.
  • AC3: chatMessageCount >= 100이면 왼쪽 상단 badge는 ic_chat_message_count와 총 채팅 개수 텍스트를 표시한다.
  • AC4: chatMessageCount < 100이면 왼쪽 상단 채팅 수 badge를 표시하지 않는다.
  • AC5: 채팅 수 14000은 Figma 예시와 같이 1.4만으로 표시되고, 19999는 반올림하지 않고 1.9만으로 절삭 표시된다. 10만 이상은 소수점 없이 정수 만 단위로 표시된다.
  • AC6: 캐릭터 이름은 maxLines=1, ellipsize=end를 적용한다.
  • AC7: 캐릭터 소개는 maxLines=2, ellipsize=end를 적용한다.
  • AC8: 원작 작품명은 maxLines=1, ellipsize=end를 적용한다.
  • AC9: hasOriginal == true이면 원작 작품명 TextView가 View.VISIBLE로 표시된다.
  • AC10: hasOriginal == false이면 원작 작품명 TextView가 View.INVISIBLE로 숨겨지고 카드 높이를 유지한다.
  • AC11: 컴포넌트 내부는 이미지 로딩 라이브러리를 고정하지 않고 imageView()를 노출해 호출부가 Coil/Glide 등 기존 정책으로 로드한다.
  • 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