feat(widget): 캐릭터 채팅 썸네일 컴포넌트를 추가한다

This commit is contained in:
2026-05-21 11:22:23 +09:00
parent c58f03be08
commit c32f9cdd9f
15 changed files with 1164 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="false"
tools:replace="android:allowBackup" />
</manifest>

View File

@@ -0,0 +1,42 @@
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))
}
@Test
fun `ten thousand unit decimal is truncated`() {
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))
}
}

View File

@@ -0,0 +1,64 @@
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
)
}

View File

@@ -0,0 +1,121 @@
package kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail
import android.app.Application
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class CharacterChatThumbnailViewTest {
@Test
fun `bind displays character fields and visible states`() {
val view = inflateView()
view.bind(sampleItem())
assertEquals("1.4만", view.chatCountText().text.toString())
assertEquals(View.VISIBLE, view.chatCountBadge().visibility)
assertEquals("캐릭터 이름", view.nameText().text.toString())
assertEquals("캐릭터 소개", view.descriptionText().text.toString())
assertEquals("작품 이름", view.originalTitleText().text.toString())
assertEquals(View.VISIBLE, view.originalTitleText().visibility)
}
@Test
fun `bind hides chat badge and keeps original title space when item requires it`() {
val view = inflateView()
view.bind(sampleItem(chatMessageCount = 99L, hasOriginal = false, originalTitle = ""))
assertEquals(View.GONE, view.chatCountBadge().visibility)
assertEquals(View.INVISIBLE, view.originalTitleText().visibility)
}
@Test
fun `imageView returns thumbnail image view`() {
val view = inflateView()
assertSame(view.findViewById<ImageView>(R.id.iv_character_chat_thumbnail_image), view.imageView())
}
@Test
fun `setOnCharacterClick invokes listener with bound item`() {
val view = inflateView()
val item = sampleItem(characterId = 20L)
var clickedItem: CharacterChatThumbnailItem? = null
view.bind(item)
view.setOnCharacterClick { clickedItem = it }
view.performClick()
assertSame(item, clickedItem)
}
@Test
fun `setOnCharacterClick removes root click listener when listener is null`() {
val view = inflateView()
var clickCount = 0
view.bind(sampleItem())
view.setOnCharacterClick { clickCount += 1 }
view.setOnCharacterClick(null)
assertFalse(view.isClickable)
assertFalse(view.hasOnClickListeners())
view.performClick()
assertEquals(0, clickCount)
}
private fun inflateView(): CharacterChatThumbnailView {
val context = ApplicationProvider.getApplicationContext<Context>()
return LayoutInflater.from(context).inflate(
R.layout.view_character_chat_thumbnail,
null,
false
) as CharacterChatThumbnailView
}
private fun CharacterChatThumbnailView.chatCountBadge(): View =
findViewById(R.id.ll_character_chat_count_badge)
private fun CharacterChatThumbnailView.chatCountText(): TextView =
findViewById(R.id.tv_character_chat_count)
private fun CharacterChatThumbnailView.nameText(): TextView =
findViewById(R.id.tv_character_chat_name)
private fun CharacterChatThumbnailView.descriptionText(): TextView =
findViewById(R.id.tv_character_chat_description)
private fun CharacterChatThumbnailView.originalTitleText(): TextView =
findViewById(R.id.tv_character_chat_original_title)
private fun sampleItem(
characterId: Long = 10L,
chatMessageCount: Long = 14000L,
hasOriginal: Boolean = true,
originalTitle: String = "작품 이름"
) = CharacterChatThumbnailItem(
characterId = characterId,
imageUrl = "https://example.com/character.png",
characterName = "캐릭터 이름",
characterDescription = "캐릭터 소개",
chatMessageCount = chatMessageCount,
hasOriginal = hasOriginal,
originalTitle = originalTitle
)
}