feat(creator): empty state 문구와 버튼 정책을 정리한다

This commit is contained in:
2026-08-14 18:30:03 +09:00
parent 5d43f367e3
commit 60df86f899
17 changed files with 305 additions and 94 deletions
@@ -102,12 +102,11 @@ class CreatorChannelDonationAdapter(
private val onEmptyDonationClick: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {
init {
binding.btnCreatorChannelDonationEmptyWrite.setOnClickListener { onEmptyDonationClick() }
}
fun bind(isOwner: Boolean) = with(binding) {
btnCreatorChannelDonationEmptyWrite.isVisible = !isOwner
btnCreatorChannelDonationEmptyWrite.setOnClickListener(
if (!isOwner) View.OnClickListener { onEmptyDonationClick() } else null
)
}
}
@@ -80,7 +80,6 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
viewModel.retryFanTalks()
}
btnCreatorChannelFantalkWrite.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }
layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }
}
private fun observeViewModel() {
@@ -113,6 +112,10 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
tvCreatorChannelFantalkErrorMessage.isVisible = false
btnCreatorChannelFantalkRetry.isVisible = false
btnCreatorChannelFantalkWrite.isVisible = false
layoutCreatorChannelFantalkEmptyWriteButton.isVisible = !host.isCreatorChannelOwner()
layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener(
if (!host.isCreatorChannelOwner()) View.OnClickListener { host.onCreatorChannelFanTalkWriteClicked() } else null
)
host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)
host.onCreatorChannelFanTalkContentChanged()
}
@@ -20,7 +20,7 @@ fun CreatorChannelHomeResponse.toUiContent(currentMemberId: Long): CreatorChanne
series.takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Series(it)) }
communities.takeIf { it.isNotEmpty() }
?.let { add(CreatorChannelHomeSection.Communities(it, currentMemberId)) }
add(CreatorChannelHomeSection.FanTalk(fanTalk))
add(CreatorChannelHomeSection.FanTalk(fanTalk, isOwner))
introduce.takeIf { it.isNotBlank() }?.let { add(CreatorChannelHomeSection.Introduce(it)) }
add(CreatorChannelHomeSection.Activity(activity))
sns.toUiItems().takeIf { it.isNotEmpty() }?.let { add(CreatorChannelHomeSection.Sns(it)) }
@@ -61,7 +61,10 @@ sealed interface CreatorChannelHomeSection {
val communities: List<CreatorChannelCommunityPostResponse>,
val currentMemberId: Long
) : CreatorChannelHomeSection
data class FanTalk(val fanTalk: CreatorChannelFanTalkSummaryResponse) : CreatorChannelHomeSection
data class FanTalk(
val fanTalk: CreatorChannelFanTalkSummaryResponse,
val isOwner: Boolean
) : CreatorChannelHomeSection
data class Introduce(val introduce: String) : CreatorChannelHomeSection
data class Activity(val activity: CreatorChannelActivityResponse) : CreatorChannelHomeSection
data class Sns(val items: List<CreatorChannelSnsUiItem>) : CreatorChannelHomeSection
@@ -113,6 +113,7 @@ class CreatorChannelHomeSectionAdapter(
private val fanTalkTotalRow: View? = view.findViewById(R.id.layout_fantalk_total_row)
private val fanTalkLatestRow: View? = view.findViewById(R.id.layout_fantalk_latest_row)
private val fanTalkEmpty: View? = view.findViewById(R.id.layout_fantalk_empty)
private val fanTalkSupportButton: View? = view.findViewById(R.id.layout_fantalk_support_button)
private val fanTalkTotalCount: TextView? = view.findViewById(R.id.tv_fantalk_total_count)
private val fanTalkProfile: ImageView? = view.findViewById(R.id.iv_fantalk_profile)
private val fanTalkContent: TextView? = view.findViewById(R.id.tv_fantalk_content)
@@ -184,13 +185,7 @@ class CreatorChannelHomeSectionAdapter(
val isDonationEmptyButtonVisible = !item.isOwner
donationButton?.isVisible = isDonationButtonVisible
donationEmptyButton?.isVisible = isDonationEmptyButtonVisible
donationEmptyTitle?.setText(
if (item.isOwner) {
R.string.creator_channel_donation_empty_owner_title
} else {
R.string.creator_channel_donation_empty_title
}
)
donationEmptyTitle?.setText(R.string.creator_channel_home_donation_empty_title)
donationButton?.setOnClickListener(
if (isDonationButtonVisible) View.OnClickListener { onDonationClick() } else null
)
@@ -417,6 +412,11 @@ class CreatorChannelHomeSectionAdapter(
fanTalkTotalRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
fanTalkLatestRow?.isVisible = fanTalk != null && item.fanTalk.totalCount > 0
fanTalkEmpty?.isVisible = fanTalk == null || item.fanTalk.totalCount <= 0
val showSupportButton = (fanTalk == null || item.fanTalk.totalCount <= 0) && !item.isOwner
fanTalkSupportButton?.isVisible = showSupportButton
fanTalkSupportButton?.setOnClickListener(
if (showSupportButton) View.OnClickListener { onSectionChevronClick(CreatorChannelTab.FanTalk) } else null
)
if (fanTalk != null) {
fanTalkContent?.text = fanTalk.content
fanTalkProfile?.loadUrl(fanTalk.profileImageUrl) {
@@ -49,7 +49,7 @@
android:layout_height="0dp"
android:layout_weight="1"
android:includeFontPadding="true"
android:text="@string/creator_channel_donation_empty_title"
android:text="@string/creator_channel_home_donation_empty_title"
android:textColor="@color/white" />
<LinearLayout
@@ -57,7 +57,7 @@
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_marginTop="42dp"
android:background="@drawable/bg_creator_channel_donation_empty_button"
android:background="@drawable/bg_round_corner_999_white"
android:gravity="center"
android:orientation="horizontal">
@@ -77,10 +77,11 @@
tools:text="팬이 쓴 응원이 보이는 부분입니다. 두 줄까지만 보여주고 초과 시 말줄임 처리." />
</LinearLayout>
<FrameLayout
<LinearLayout
android:id="@+id/layout_fantalk_empty"
android:layout_width="match_parent"
android:layout_height="169dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
@@ -89,7 +90,6 @@
style="@style/Typography.Heading1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_marginStart="@dimen/spacing_6"
android:layout_marginTop="@dimen/spacing_6"
android:includeFontPadding="false"
@@ -100,8 +100,7 @@
android:id="@+id/layout_fantalk_support_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="@dimen/spacing_6"
android:layout_marginTop="@dimen/spacing_14"
android:background="@drawable/bg_round_corner_999_white"
android:gravity="center"
android:orientation="horizontal"
@@ -122,6 +121,6 @@
android:text="@string/creator_channel_fantalk_support_action"
android:textColor="@color/black" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelFanTalkCardView>
</LinearLayout>
+5 -4
View File
@@ -375,9 +375,9 @@
<string name="creator_channel_community_invalid_post">Could not load the post.</string>
<string name="creator_channel_community_purchased_badge">Purchased</string>
<string name="creator_channel_fantalk_summary">%1$s · Total %2$d</string>
<string name="creator_channel_fantalk_empty_title">Be the first\nto cheer them on!</string>
<string name="creator_channel_fantalk_support_action">Leave support</string>
<string name="creator_channel_fantalk_empty_message">No cheers yet.\nBe the first to cheer for the creator!</string>
<string name="creator_channel_fantalk_empty_title">We\'re waiting for your\nfirst fan letter!</string>
<string name="creator_channel_fantalk_support_action">Send fan letter</string>
<string name="creator_channel_fantalk_empty_message">No fan letters yet.\nSend the first fan letter!</string>
<string name="creator_channel_fantalk_error_message">Could not load Fan Letter.</string>
<string name="creator_channel_fantalk_retry">Retry</string>
<string name="creator_channel_fantalk_all_label">All</string>
@@ -404,7 +404,8 @@
<string name="creator_channel_donation_action">Support</string>
<string name="creator_channel_donation_error_message">Could not load donation history.</string>
<string name="creator_channel_donation_retry">Retry</string>
<string name="creator_channel_donation_empty_title">Be the first to\nsupport this creator!</string>
<string name="creator_channel_home_donation_empty_title">Be the first to\nsupport this creator!</string>
<string name="creator_channel_donation_empty_title">No support yet.\nBe the first to support this creator!</string>
<string name="creator_channel_donation_empty_owner_title">No donation history yet.</string>
<string name="creator_channel_donation_can_format">%1$s cans</string>
<string name="creator_channel_donation_fallback_message">Supported with %1$d cans.</string>
+2 -1
View File
@@ -375,7 +375,7 @@
<string name="creator_channel_community_invalid_post">投稿を読み込めません。</string>
<string name="creator_channel_community_purchased_badge">購入済み</string>
<string name="creator_channel_fantalk_summary">%1$s · 全体 %2$d</string>
<string name="creator_channel_fantalk_empty_title">最初の応援を\n待っています!</string>
<string name="creator_channel_fantalk_empty_title">あなたの最初のファンレターを待っています!</string>
<string name="creator_channel_fantalk_support_action">ファンレターを送る</string>
<string name="creator_channel_fantalk_empty_message">まだファンレターがありません。\n最初のファンレターを送ってみましょう!</string>
<string name="creator_channel_fantalk_error_message">ファンレターを読み込めませんでした。</string>
@@ -404,6 +404,7 @@
<string name="creator_channel_donation_action">サポートする</string>
<string name="creator_channel_donation_error_message">応援履歴を読み込めませんでした。</string>
<string name="creator_channel_donation_retry">再試行</string>
<string name="creator_channel_home_donation_empty_title">最初のサポートをしてみましょう!</string>
<string name="creator_channel_donation_empty_title">まだサポートがありません。\n最初のサポートをしてみましょう!</string>
<string name="creator_channel_donation_empty_owner_title">応援履歴がありません</string>
<string name="creator_channel_donation_can_format">%1$sCAN</string>
+2 -1
View File
@@ -375,7 +375,7 @@
<string name="creator_channel_community_purchased_badge">구매 완료</string>
<string name="creator_channel_fantalk_summary">%1$s · 전체 %2$d</string>
<string name="creator_channel_fantalk_empty_title">당신의 첫 응원을\n기다리고 있어요!</string>
<string name="creator_channel_fantalk_support_action">응원 남기기</string>
<string name="creator_channel_fantalk_support_action">응원남기기</string>
<string name="creator_channel_fantalk_empty_message">아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!</string>
<string name="creator_channel_fantalk_error_message">팬레터를 불러오지 못했습니다.</string>
<string name="creator_channel_fantalk_retry">다시 시도</string>
@@ -403,6 +403,7 @@
<string name="creator_channel_donation_action">후원하기</string>
<string name="creator_channel_donation_error_message">후원 내역을 불러오지 못했습니다.</string>
<string name="creator_channel_donation_retry">다시 시도</string>
<string name="creator_channel_home_donation_empty_title">처음으로 크리에이터를\n후원해 보세요!</string>
<string name="creator_channel_donation_empty_title">아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!</string>
<string name="creator_channel_donation_empty_owner_title">후원 내역이 없습니다</string>
<string name="creator_channel_donation_can_format">%1$s캔</string>
@@ -1113,7 +1113,12 @@ class CreatorChannelActivitySourceTest {
assertTrue(donationLayout.contains("@+id/ll_donation_items"))
assertTrue(donationLayout.contains("@+id/layout_donation_empty"))
assertTrue(donationLayout.contains("@+id/tv_donation_empty_title"))
assertTrue(donationLayout.contains("@string/creator_channel_donation_empty_title"))
assertTrue(donationLayout.contains("@string/creator_channel_home_donation_empty_title"))
val donationEmptyButtonBlock = donationLayout
.substringAfter("android:id=\"@+id/layout_donation_empty_button\"")
.substringBefore("</LinearLayout>")
assertTrue(donationEmptyButtonBlock.contains("android:background=\"@drawable/bg_round_corner_999_white\""))
assertTrue(donationEmptyButtonBlock.contains("android:textColor=\"@color/black\""))
assertTrue(donationLayout.contains("android:layout_width=\"match_parent\""))
assertTrue(donationLayout.contains("android:layout_height=\"wrap_content\""))
assertTrue(donationLayout.contains("@+id/layout_donation_button"))
@@ -1308,13 +1313,15 @@ class CreatorChannelActivitySourceTest {
val en = projectFile("app/src/main/res/values-en/strings.xml").readText()
val ja = projectFile("app/src/main/res/values-ja/strings.xml").readText()
assertTrue(
ko.contains(
"name=\"creator_channel_donation_empty_title\">아직 후원이 없습니다.\\n처음으로 크리에이터를 후원해 보세요!"
)
)
assertTrue(en.contains("creator_channel_donation_empty_title"))
assertTrue(ja.contains("creator_channel_donation_empty_title"))
assertTrue(ko.contains("name=\"creator_channel_home_donation_empty_title\">처음으로 크리에이터를\\n후원해 보세요!"))
assertTrue(en.contains("name=\"creator_channel_home_donation_empty_title\">Be the first to\\nsupport this creator!"))
assertTrue(ja.contains("name=\"creator_channel_home_donation_empty_title\">最初のサポートをしてみましょう!"))
assertTrue(ko.contains("name=\"creator_channel_donation_empty_title\">아직 후원이 없습니다.\\n처음으로 크리에이터를 후원해 보세요!"))
assertTrue(en.contains("name=\"creator_channel_donation_empty_title\">No support yet.\\nBe the first to support this creator!"))
assertTrue(ja.contains("name=\"creator_channel_donation_empty_title\">まだサポートがありません。\\n最初のサポートをしてみましょう!"))
assertTrue(ko.contains("name=\"creator_channel_donation_action\">후원하기"))
assertTrue(en.contains("name=\"creator_channel_donation_action\">Support"))
assertTrue(ja.contains("name=\"creator_channel_donation_action\">サポートする"))
}
@Test
@@ -2136,10 +2143,17 @@ class CreatorChannelActivitySourceTest {
assertTrue(fanTalkLayout.contains("@+id/layout_fantalk_support_button"))
assertTrue(fanTalkLayout.contains("@string/creator_channel_fantalk_empty_title"))
assertTrue(fanTalkLayout.contains("@string/creator_channel_fantalk_support_action"))
val fanTalkSupportButtonBlock = fanTalkLayout
.substringAfter("android:id=\"@+id/layout_fantalk_support_button\"")
.substringBefore("</LinearLayout>")
assertTrue(fanTalkSupportButtonBlock.contains("android:background=\"@drawable/bg_round_corner_999_white\""))
assertTrue(fanTalkSupportButtonBlock.contains("android:textColor=\"@color/black\""))
assertTrue(fanTalkLayout.contains("@drawable/ic_new_fantalk_plus"))
assertTrue(fanTalkLayout.contains("android:layout_width=\"match_parent\""))
assertTrue(fanTalkLayout.contains("android:layout_height=\"169dp\""))
assertTrue(fanTalkLayout.contains("android:layout_marginBottom=\"@dimen/spacing_6\""))
assertTrue(fanTalkLayout.contains("android:layout_height=\"wrap_content\""))
assertTrue(fanTalkLayout.contains("android:layout_marginTop=\"@dimen/spacing_14\""))
assertFalse(fanTalkLayout.contains("android:layout_height=\"169dp\""))
assertFalse(fanTalkLayout.contains("android:layout_marginBottom=\"@dimen/spacing_6\""))
assertFalse(fanTalkLayout.contains("@+id/ll_section_items"))
assertFalse(fanTalkLayout.contains("android:clipToOutline"))
assertTrue(fanTalkCardView.contains("clipToOutline = true"))
@@ -2153,10 +2167,14 @@ class CreatorChannelActivitySourceTest {
assertTrue(adapter.contains("private val fanTalkTotalRow: View?"))
assertTrue(adapter.contains("private val fanTalkLatestRow: View?"))
assertTrue(adapter.contains("private val fanTalkEmpty: View?"))
assertTrue(adapter.contains("private val fanTalkSupportButton: View?"))
assertTrue(adapter.contains("fanTalkTotalCount?.text = item.fanTalk.totalCount.toString()"))
assertTrue(adapter.contains("fanTalkTotalRow?.isVisible = fanTalk != null"))
assertTrue(adapter.contains("fanTalkLatestRow?.isVisible = fanTalk != null"))
assertTrue(adapter.contains("fanTalkEmpty?.isVisible = fanTalk == null"))
assertTrue(adapter.contains("val showSupportButton = (fanTalk == null || item.fanTalk.totalCount <= 0) && !item.isOwner"))
assertTrue(adapter.contains("fanTalkSupportButton?.isVisible = showSupportButton"))
assertTrue(adapter.contains("fanTalkSupportButton?.setOnClickListener"))
assertTrue(adapter.contains("fanTalkContent?.text = fanTalk.content"))
assertTrue(adapter.contains("fanTalkProfile?.loadUrl(fanTalk.profileImageUrl)"))
assertTrue(adapter.contains("fanTalkContent?.text = \"\""))
@@ -53,12 +53,14 @@ class CreatorChannelHomeMapperTest {
}
@Test
fun `본인 채널 여부를 후원 section에도 매핑한다`() {
fun `본인 채널 여부를 후원과 팬Talk section에도 매핑한다`() {
val ownerContent = response().toUiContent(currentMemberId = 100L)
val visitorContent = response().toUiContent(currentMemberId = 1L)
assertTrue(ownerContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.Donations>().single().isOwner)
assertTrue(ownerContent.sections.filterIsInstance<CreatorChannelHomeSection.FanTalk>().single().isOwner)
assertFalse(visitorContent.sections.filterIsInstance<CreatorChannelHomeSection.FanTalk>().single().isOwner)
}
@Test
@@ -91,7 +91,7 @@ class CreatorChannelDonationActionTest {
assertTrue(adapter.contains("onEmptyDonationClick"))
assertTrue(adapter.contains("btnCreatorChannelDonationRankingAll.setOnClickListener"))
assertTrue(adapter.contains("btnCreatorChannelDonationEmptyWrite.isVisible = !isOwner"))
assertTrue(adapter.contains("btnCreatorChannelDonationEmptyWrite.setOnClickListener"))
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"))
@@ -57,6 +57,7 @@ class CreatorChannelDonationFragmentLayoutTest {
assertTrue(layout.contains("android:text=\"@string/creator_channel_donation_empty_title\""))
assertTrue(layout.contains("android:text=\"@string/creator_channel_donation_action\""))
assertTrue(layout.contains("android:background=\"@drawable/bg_creator_channel_donation_empty_button\""))
assertTrue(layout.contains("android:textColor=\"@color/white\""))
assertTrue(layout.contains("android:layout_marginTop=\"@dimen/spacing_14\""))
assertTrue(layout.contains("android:src=\"@drawable/ic_new_donation\""))
assertTrue(layout.contains("android:paddingTop=\"@dimen/spacing_48\""))
@@ -81,9 +81,15 @@ class CreatorChannelFanTalkFragmentLayoutTest {
.readText()
.contains("@color/gray_800")
)
assertLocaleString("values/strings.xml", "아직 응원이 없습니다.\\n처음으로 크리에이터를 응원해 보세요!")
assertLocaleString("values-en/strings.xml", "No cheers yet.\\nBe the first to cheer for the creator!")
assertLocaleString("values-ja/strings.xml", "まだファンレターがありません。\\n最初のファンレターをってみましょう")
assertLocaleString("values/strings.xml", "creator_channel_fantalk_empty_title", "당신의 첫 응원을\\n기다리고 있어요!")
assertLocaleString("values-en/strings.xml", "creator_channel_fantalk_empty_title", "We\\'re waiting for your\\nfirst fan letter!")
assertLocaleString("values-ja/strings.xml", "creator_channel_fantalk_empty_title", "あなたの最初のファンレターをっています")
assertLocaleString("values/strings.xml", "creator_channel_fantalk_empty_message", "아직 응원이 없습니다.\\n처음으로 크리에이터를 응원해 보세요!")
assertLocaleString("values-en/strings.xml", "creator_channel_fantalk_empty_message", "No fan letters yet.\\nSend the first fan letter!")
assertLocaleString("values-ja/strings.xml", "creator_channel_fantalk_empty_message", "まだファンレターがありません。\\n最初のファンレターを送ってみましょう!")
assertLocaleString("values/strings.xml", "creator_channel_fantalk_support_action", "응원남기기")
assertLocaleString("values-en/strings.xml", "creator_channel_fantalk_support_action", "Send fan letter")
assertLocaleString("values-ja/strings.xml", "creator_channel_fantalk_support_action", "ファンレターを送る")
}
@Test
@@ -209,10 +215,12 @@ class CreatorChannelFanTalkFragmentLayoutTest {
assertTrue(fragment.contains("fun onCreatorChannelFanTalkWriteClicked()"))
assertTrue(fragment.contains("fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged(isVisible: Boolean)"))
assertTrue(fragment.contains("layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener"))
assertTrue(fragment.contains("layoutCreatorChannelFantalkEmptyWriteButton.isVisible = !host.isCreatorChannelOwner()"))
assertTrue(fragment.contains("host.onCreatorChannelFanTalkWriteClicked()"))
assertTrue(fragment.contains("host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(true)"))
assertTrue(fragment.contains("host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)"))
assertFalse(fragment.contains("btnCreatorChannelFantalkWrite.setOnClickListener { }"))
assertFalse(fragment.contains("layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }"))
assertFalse(fragment.contains("btnCreatorChannelFantalkWrite.isVisible = true"))
assertTrue(layout.contains("@+id/layout_creator_channel_fantalk_empty_write_button"))
assertTrue(layout.contains("android:id=\"@+id/btn_creator_channel_fantalk_write\""))
@@ -227,9 +235,9 @@ class CreatorChannelFanTalkFragmentLayoutTest {
private fun projectFile(path: String): File = File(projectRoot(), path)
private fun assertLocaleString(path: String, expectedValue: String) {
private fun assertLocaleString(path: String, name: String, expectedValue: String) {
val strings = projectFile("app/src/main/res/$path").readText()
assertTrue(strings.contains("<string name=\"creator_channel_fantalk_empty_message\">$expectedValue</string>"))
assertTrue(strings.contains("<string name=\"$name\">$expectedValue</string>"))
}
private fun projectRoot(): File {