fix(creator): 후원 item locale 표시를 적용한다

This commit is contained in:
2026-08-26 13:27:20 +09:00
parent 3560d7bf89
commit 4f4ae62026
8 changed files with 260 additions and 14 deletions
@@ -30,8 +30,8 @@ private fun CreatorChannelDonationResponse.toDonationUiModel(
nickname = nickname,
profileImageUrl = profileImageUrl,
can = can,
message = message.takeUnless { it.isBlank() } ?: context.getString(R.string.creator_channel_donation_fallback_message, can),
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
message = message,
createdAtText = createdAtUtc,
headerColorResId = calculateDonationHeaderColorRes(can)
)
@@ -8,6 +8,8 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationBinding
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationEmptyBinding
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationRankingBinding
@@ -115,6 +117,7 @@ class CreatorChannelDonationAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: CreatorChannelDonationUiModel) = with(binding) {
val context = root.context
bindCreatorChannelDonationCard(
headerView = layoutCreatorChannelDonationHeader,
profileView = ivCreatorChannelDonationProfile,
@@ -125,9 +128,10 @@ class CreatorChannelDonationAdapter(
headerColorResId = item.headerColorResId,
profileImageUrl = item.profileImageUrl,
nickname = item.nickname,
createdAtText = item.createdAtText,
createdAtText = formatUtcRelativeTimeText(context, item.createdAtText),
can = item.can,
message = item.message
message = item.message.takeUnless { it.isBlank() }
?: context.getString(R.string.creator_channel_donation_fallback_message, item.can)
)
}
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.common
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import org.junit.Assert.assertEquals
@@ -57,6 +58,21 @@ class RelativeTimeFormatterTest {
assertEquals(context.getString(R.string.character_comment_time_just_now), text)
}
@Test
fun `epoch millis는 한국어 영어 일본어 context에서 locale별 경과 분 resource를 반환한다`() {
listOf(Locale.KOREAN, Locale.ENGLISH, Locale.JAPANESE).forEach { locale ->
val localizedContext = context.createConfigurationContext(
Configuration(context.resources.configuration).apply { setLocale(locale) }
)
val twoMinutesAgoMillis = System.currentTimeMillis() - 2 * MINUTE
assertEquals(
localizedContext.getString(R.string.character_comment_time_minutes, 2),
formatUtcRelativeTimeText(localizedContext, twoMinutesAgoMillis.toString())
)
}
}
private fun utcIso(millis: Long): String {
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
@@ -94,8 +94,8 @@ class CreatorChannelDonationActionTest {
assertTrue(adapter.contains("if (!isOwner) View.OnClickListener { onEmptyDonationClick() } else null"))
assertTrue(adapter.contains("bindCreatorChannelDonationCard("))
assertTrue(adapter.contains("headerColorResId = item.headerColorResId"))
assertTrue(adapter.contains("createdAtText = item.createdAtText"))
assertTrue(adapter.contains("message = item.message"))
assertTrue(adapter.contains("createdAtText = formatUtcRelativeTimeText(context, item.createdAtText)"))
assertTrue(adapter.contains("R.string.creator_channel_donation_fallback_message"))
assertTrue(adapter.contains("GridLayoutManager(itemView.context, 4)"))
assertTrue(rankingAdapter.contains("item.profileImageUrl"))
assertTrue(rankingAdapter.contains("item.rank.toString()"))
@@ -1,14 +1,21 @@
package kr.co.vividnext.sodalive.v2.creator.channel.donation
import android.app.Application
import android.content.res.Configuration
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.CreatorChannelDonationUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.donation.ui.CreatorChannelDonationAdapter
import kr.co.vividnext.sodalive.v2.creator.channel.ui.bindCreatorChannelDonationCard
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
@@ -17,6 +24,7 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.io.File
import java.util.Locale
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
@@ -167,6 +175,63 @@ class CreatorChannelDonationFragmentLayoutTest {
assertTrue(donationCardBinder.contains("createdAtView.setTextColor(context.getColor(R.color.gray_700))"))
}
@Test
fun `후원 can binder는 한국어 영어 일본어에서 1,000 단위 표시를 사용한다`() {
listOf(Locale.KOREAN to "1,000캔", Locale.ENGLISH to "1,000 cans", Locale.JAPANESE to "1,000CAN")
.forEach { (locale, expected) ->
val application = ApplicationProvider.getApplicationContext<Application>()
if (!ImageLoaderProvider.isInitialized) ImageLoaderProvider.init(application)
val context = application.createConfigurationContext(
Configuration(application.resources.configuration).apply { setLocale(locale) }
)
val canView = TextView(context)
bindCreatorChannelDonationCard(
View(context), ImageView(context), TextView(context), TextView(context), canView,
TextView(context), R.color.gray_200, "", "member", "time", 1000, "message"
)
assertEquals(expected, canView.text.toString())
}
}
@Test
fun `후원 adapter는 item view locale로 기본 문구와 상대 시간을 표시한다`() {
val application = ApplicationProvider.getApplicationContext<Application>()
if (!ImageLoaderProvider.isInitialized) ImageLoaderProvider.init(application)
val twoMinutesAgoMillis = System.currentTimeMillis() - 2 * MINUTE
listOf(Locale.KOREAN, Locale.ENGLISH, Locale.JAPANESE).forEach { locale ->
val context = application.createConfigurationContext(
Configuration(application.resources.configuration).apply { setLocale(locale) }
)
val adapter = CreatorChannelDonationAdapter()
adapter.submitItems(
rankings = emptyList(),
donations = listOf(
CreatorChannelDonationUiModel(
nickname = "member",
profileImageUrl = "",
can = 50,
message = "",
createdAtText = twoMinutesAgoMillis.toString(),
headerColorResId = R.color.gray_200
)
)
)
val viewHolder = adapter.onCreateViewHolder(FrameLayout(context), adapter.getItemViewType(0))
adapter.onBindViewHolder(viewHolder, 0)
assertEquals(
context.getString(R.string.creator_channel_donation_fallback_message, 50),
viewHolder.itemView.findViewById<TextView>(R.id.tv_creator_channel_donation_message).text.toString()
)
assertEquals(
context.getString(R.string.character_comment_time_minutes, 2),
viewHolder.itemView.findViewById<TextView>(R.id.tv_creator_channel_donation_created_at).text.toString()
)
}
}
@Test
fun `후원 문자열은 한국어 영어 일본어에 추가된다`() {
val ko = projectFile("app/src/main/res/values/strings.xml").readText()
@@ -194,4 +259,8 @@ class CreatorChannelDonationFragmentLayoutTest {
return candidates.firstOrNull { it.exists() }
?: error("Project file not found: $relativePath")
}
private companion object {
const val MINUTE = 60_000L
}
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.donation
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter
@@ -14,6 +15,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.util.Locale
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
@@ -35,21 +37,38 @@ class CreatorChannelDonationMapperTest {
}
@Test
fun `후원 message가 blank이면 can을 포함한 fallback 문구를 사용한다`() {
val item = listOf(donation(can = 50, message = " "))
fun `후원 mapper는 message 원문을 보존한다`() {
val item = listOf(donation(can = 50, message = ""))
.toDonationUiModels(context, relativeTimeTextFormatter)
.single()
assertEquals(context.getString(R.string.creator_channel_donation_fallback_message, 50), item.message)
assertEquals("", item.message)
}
@Test
fun `후원 createdAtUtc는 상대 시간으로 매핑하고 can 기준 header color를 계산한다`() {
val item = listOf(donation(can = 501, createdAtUtc = System.currentTimeMillis().toString()))
fun `message는 한국어 영어 일본어 context와 관계없이 API 원문을 유지한다`() {
listOf(Locale.KOREAN, Locale.ENGLISH, Locale.JAPANESE).forEach { locale ->
val localizedContext = context.createConfigurationContext(
Configuration(context.resources.configuration).apply { setLocale(locale) }
)
val items = listOf(
donation(can = 50, message = ""),
donation(can = 50, message = "응원합니다")
).toDonationUiModels(localizedContext) { "time" }
assertEquals("", items[0].message)
assertEquals("응원합니다", items[1].message)
}
}
@Test
fun `후원 mapper는 createdAtUtc 원문을 보존하고 can 기준 header color를 계산한다`() {
val createdAtUtc = System.currentTimeMillis().toString()
val item = listOf(donation(can = 501, createdAtUtc = createdAtUtc))
.toDonationUiModels(context, relativeTimeTextFormatter)
.single()
assertEquals(context.getString(R.string.character_comment_time_just_now), item.createdAtText)
assertEquals(createdAtUtc, item.createdAtText)
assertEquals(R.color.red_400, item.headerColorResId)
}