fix(creator): 라이브 empty와 CTA 목록 여백을 보정한다

This commit is contained in:
2026-06-18 15:21:26 +09:00
parent f4af9868e6
commit efac753f83
3 changed files with 117 additions and 117 deletions

View File

@@ -3,11 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.live
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.R
@@ -34,8 +30,7 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
private var lastContentLayoutKey: CreatorChannelLiveContentLayoutKey? = null
private var sortPopup: CreatorChannelLiveSortPopup? = null
private var currentContentState: CreatorChannelLiveUiState.Content? = null
private var isOwner: Boolean = false
private var ownerCtaBottomInset: Int = 0
private var emptyMinHeight: Int = 0
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
private val host: Host
get() = requireActivity() as Host
@@ -44,15 +39,8 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
super.onViewCreated(view, savedInstanceState)
bindLoading()
setupReplayList()
setupOwnerCtaInsets()
setupClickListeners()
observeViewModel()
bindOwnerCta(host.isCreatorChannelOwner())
}
override fun onResume() {
super.onResume()
binding.layoutCreatorChannelLiveOwnerCta.isEnabled = true
}
fun onCreatorChannelLiveTabSelected() {
@@ -78,17 +66,18 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
viewModel.loadMore()
}
fun onCreatorChannelOwnerChanged(isOwner: Boolean) {
bindOwnerCta(isOwner)
fun onCreatorChannelLiveViewportHeightChanged(minHeight: Int) {
emptyMinHeight = minHeight.coerceAtLeast(0)
applyEmptyMinHeight()
}
private fun setupOwnerCtaInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.layoutCreatorChannelLiveOwnerCta) { _, insets ->
ownerCtaBottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
updateOwnerCtaInsets()
insets
fun onCreatorChannelLiveOwnerCtaVisibilityChanged(isVisible: Boolean) = with(binding) {
val bottomPadding = if (isVisible) {
LIVE_OWNER_CTA_LIST_BOTTOM_PADDING_DP.dpToPx().toInt()
} else {
DEFAULT_LIST_BOTTOM_PADDING_DP.dpToPx().toInt()
}
ViewCompat.requestApplyInsets(binding.layoutCreatorChannelLiveOwnerCta)
rvCreatorChannelLiveReplays.updatePadding(bottom = bottomPadding)
}
private fun setupClickListeners() {
@@ -99,29 +88,6 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
binding.btnCreatorChannelLiveRetry.setOnClickListener {
viewModel.retryLive()
}
binding.layoutCreatorChannelLiveOwnerCta.setOnClickListener {
binding.layoutCreatorChannelLiveOwnerCta.isEnabled = false
host.onCreatorChannelLiveStartClicked()
}
}
private fun bindOwnerCta(isOwner: Boolean) = with(binding) {
this@CreatorChannelLiveFragment.isOwner = isOwner
layoutCreatorChannelLiveOwnerCta.isVisible = isOwner
updateOwnerCtaInsets()
}
private fun updateOwnerCtaInsets() = with(binding) {
val baseBottomMargin = OWNER_CTA_BASE_MARGIN_DP.dpToPx().toInt()
val listBottomPadding = if (isOwner) {
OWNER_CTA_LIST_BOTTOM_PADDING_DP.dpToPx().toInt() + ownerCtaBottomInset
} else {
DEFAULT_LIST_BOTTOM_PADDING_DP.dpToPx().toInt()
}
layoutCreatorChannelLiveOwnerCta.updateLayoutParams<ConstraintLayout.LayoutParams> {
bottomMargin = baseBottomMargin + ownerCtaBottomInset
}
rvCreatorChannelLiveReplays.updatePadding(bottom = listBottomPadding)
}
private fun observeViewModel() {
@@ -141,7 +107,7 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
layoutCreatorChannelLiveSortBar.isVisible = false
layoutCreatorChannelLiveCurrentCard.isVisible = false
rvCreatorChannelLiveReplays.isVisible = false
tvCreatorChannelLiveEmptyMessage.isVisible = false
layoutCreatorChannelLiveEmpty.isVisible = false
tvCreatorChannelLiveErrorMessage.isVisible = false
btnCreatorChannelLiveRetry.isVisible = false
}
@@ -152,19 +118,24 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
layoutCreatorChannelLiveSortBar.isVisible = false
layoutCreatorChannelLiveCurrentCard.isVisible = false
rvCreatorChannelLiveReplays.isVisible = false
tvCreatorChannelLiveEmptyMessage.isVisible = true
layoutCreatorChannelLiveEmpty.isVisible = true
applyEmptyMinHeight()
tvCreatorChannelLiveErrorMessage.isVisible = false
btnCreatorChannelLiveRetry.isVisible = false
host.onCreatorChannelLiveContentChanged()
}
private fun applyEmptyMinHeight() = with(binding) {
layoutCreatorChannelLiveEmpty.minimumHeight = emptyMinHeight
}
private fun bindError(state: CreatorChannelLiveUiState.Error) = with(binding) {
currentContentState = null
lastContentLayoutKey = null
layoutCreatorChannelLiveSortBar.isVisible = false
layoutCreatorChannelLiveCurrentCard.isVisible = false
rvCreatorChannelLiveReplays.isVisible = false
tvCreatorChannelLiveEmptyMessage.isVisible = false
layoutCreatorChannelLiveEmpty.isVisible = false
tvCreatorChannelLiveErrorMessage.isVisible = true
tvCreatorChannelLiveErrorMessage.text = state.message ?: getString(R.string.creator_channel_live_error_message)
btnCreatorChannelLiveRetry.isVisible = true
@@ -173,7 +144,7 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
private fun bindContent(state: CreatorChannelLiveUiState.Content) = with(binding) {
currentContentState = state
tvCreatorChannelLiveEmptyMessage.isVisible = false
layoutCreatorChannelLiveEmpty.isVisible = false
tvCreatorChannelLiveErrorMessage.isVisible = false
btnCreatorChannelLiveRetry.isVisible = false
layoutCreatorChannelLiveSortBar.isVisible = true
@@ -225,18 +196,15 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
}
interface Host {
fun isCreatorChannelOwner(): Boolean
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
fun onCreatorChannelLiveReplayClicked(audioContentId: Long)
fun onCreatorChannelLiveStartClicked()
fun onCreatorChannelLiveContentChanged()
}
companion object {
private const val ARG_CREATOR_ID: String = "arg_creator_id"
private const val DEFAULT_LIST_BOTTOM_PADDING_DP = 32
private const val OWNER_CTA_BASE_MARGIN_DP = 14
private const val OWNER_CTA_LIST_BOTTOM_PADDING_DP = 102
private const val LIVE_OWNER_CTA_LIST_BOTTOM_PADDING_DP = 132
fun newInstance(creatorId: Long): CreatorChannelLiveFragment {
return CreatorChannelLiveFragment().apply {

View File

@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:background="@color/black">
<LinearLayout
@@ -195,21 +195,27 @@
tools:itemCount="3"
tools:listitem="@layout/item_creator_channel_live_replay" />
<TextView
android:id="@+id/tv_creator_channel_live_empty_message"
style="@style/Typography.Body3"
<FrameLayout
android:id="@+id/layout_creator_channel_live_empty"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/creator_channel_live_empty_message"
android:textColor="@color/gray_500"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.58"
tools:visibility="visible" />
tools:visibility="visible">
<TextView
android:id="@+id/tv_creator_channel_live_empty_message"
style="@style/Typography.Body3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="@string/creator_channel_live_empty_message"
android:textColor="@color/gray_500" />
</FrameLayout>
<TextView
android:id="@+id/tv_creator_channel_live_error_message"
@@ -220,11 +226,9 @@
android:text="@string/creator_channel_live_error_message"
android:textColor="@color/gray_500"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/btn_creator_channel_live_retry"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/btn_creator_channel_live_retry"
@@ -239,38 +243,9 @@
android:text="@string/creator_channel_live_retry_button"
android:textColor="@color/white"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_live_error_message" />
<LinearLayout
android:id="@+id/layout_creator_channel_live_owner_cta"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_marginHorizontal="@dimen/spacing_14"
android:layout_marginBottom="@dimen/spacing_14"
android:background="@drawable/bg_creator_channel_owner_fab"
android:gravity="center"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_new_create_live" />
<TextView
style="@style/Typography.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_8"
android:includeFontPadding="false"
android:text="@string/creator_channel_live_start_button"
android:textColor="@color/black" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -28,22 +28,53 @@ class CreatorChannelLiveFragmentLayoutTest {
@Test
fun `라이브 fragment layout은 sort current live list empty error owner CTA를 제공한다`() {
val root = inflateView(R.layout.fragment_creator_channel_live)
val layout = projectFile("app/src/main/res/layout/fragment_creator_channel_live.xml").readText()
val sortBar = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_live_sort_bar))
val currentLiveCard = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_live_current_card))
val replayList = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_creator_channel_live_replays))
val emptyMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_creator_channel_live_empty_message))
val emptyContainer = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_live_empty))
val errorMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_creator_channel_live_error_message))
val retryButton = requireNotNull(root.findViewById<TextView>(R.id.btn_creator_channel_live_retry))
val ownerCta = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_live_owner_cta))
assertSame(root, sortBar.parent)
assertSame(root, replayList.parent)
assertSame(root, emptyMessage.parent)
assertSame(root, emptyContainer.parent)
assertSame(emptyContainer, emptyMessage.parent)
assertSame(root, errorMessage.parent)
assertSame(root, retryButton.parent)
assertSame(root, ownerCta.parent)
assertNotNull(currentLiveCard.findViewById<TextView>(R.id.tv_creator_channel_live_current_title))
assertEquals(false, replayList.clipToPadding)
assertTrue(layout.contains("android:layout_height=\"wrap_content\""))
assertTrue(layout.contains("android:id=\"@+id/layout_creator_channel_live_empty\""))
assertFalse(layout.contains("android:minHeight=\"360dp\""))
assertTrue(layout.contains("android:gravity=\"center\""))
assertFalse(
layout.contains(
"android:id=\"@+id/tv_creator_channel_live_empty_message\"" +
"\n style=\"@style/Typography.Body3\"" +
"\n android:layout_width=\"0dp\"" +
"\n android:layout_height=\"wrap_content\"" +
"\n android:gravity=\"center\"" +
"\n android:text=\"@string/creator_channel_live_empty_message\"" +
"\n android:textColor=\"@color/gray_500\"" +
"\n android:visibility=\"gone\"" +
"\n app:layout_constraintBottom_toBottomOf=\"parent\""
)
)
assertFalse(layout.contains("app:layout_constraintBottom_toTopOf=\"@id/btn_creator_channel_live_retry\""))
}
@Test
fun `라이브 empty container 최소 높이는 Activity가 전달한 viewport 높이를 사용한다`() {
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt"
).readText()
assertTrue(fragment.contains("private var emptyMinHeight: Int = 0"))
assertTrue(fragment.contains("fun onCreatorChannelLiveViewportHeightChanged(minHeight: Int)"))
assertTrue(fragment.contains("emptyMinHeight = minHeight.coerceAtLeast(0)"))
assertTrue(fragment.contains("layoutCreatorChannelLiveEmpty.minimumHeight = emptyMinHeight"))
assertTrue(fragment.contains("applyEmptyMinHeight()"))
}
@Test
@@ -86,6 +117,11 @@ class CreatorChannelLiveFragmentLayoutTest {
@Test
fun `라이브 다시듣기 item layout은 썸네일 tag title duration action 영역을 제공한다`() {
val item = inflateView(R.layout.item_creator_channel_live_replay)
val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_live_replay.xml").readText()
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt"
).readText()
val thumbnail = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_live_replay_thumbnail))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_thumbnail))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_adult_badge))
@@ -100,6 +136,14 @@ class CreatorChannelLiveFragmentLayoutTest {
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_can))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_live_replay_action_text))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_live_replay_action_text))
assertEquals(dp(88), thumbnail.layoutParams.width)
assertEquals(dp(88), thumbnail.layoutParams.height)
assertTrue(itemLayout.contains("android:layout_marginBottom=\"@dimen/spacing_8\""))
assertTrue(itemLayout.contains("android:clipToOutline=\"true\""))
assertTrue(itemLayout.contains("android:outlineProvider=\"background\""))
assertTrue(itemLayout.contains("@drawable/bg_audio_content_card_thumbnail"))
assertTrue(adapter.contains("layoutCreatorChannelLiveReplayThumbnail.clipToOutline = true"))
assertTrue(adapter.contains("resources.getDimension(R.dimen.radius_14)"))
}
@Test
@@ -163,7 +207,6 @@ class CreatorChannelLiveFragmentLayoutTest {
if (creatorId > 0L)
""".trimIndent()
assertTrue(layout.contains("android:layout_height=\"match_parent\""))
assertTrue(fragment.contains("R.drawable.ic_new_sort"))
assertTrue(fragment.contains("CreatorChannelLiveSortPopup"))
assertTrue(fragment.contains("layoutCreatorChannelLiveSortButton.setOnClickListener"))
@@ -197,29 +240,43 @@ class CreatorChannelLiveFragmentLayoutTest {
}
@Test
fun `라이브 owner CTA source는 본인 여부 노출 inset padding click 연결을 포함한다`() {
fun `라이브 owner CTA는 Activity root overlay로 Figma 고정 영역을 제공한다`() {
val activity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val activityLayout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt"
).readText()
val layout = projectFile("app/src/main/res/layout/fragment_creator_channel_live.xml").readText()
val fragmentLayout = projectFile("app/src/main/res/layout/fragment_creator_channel_live.xml").readText()
assertTrue(layout.contains("android:id=\"@+id/layout_creator_channel_live_owner_cta\""))
assertTrue(layout.contains("@drawable/ic_new_create_live"))
assertTrue(layout.contains("@string/creator_channel_live_start_button"))
assertTrue(fragment.contains("setupOwnerCtaInsets()"))
assertTrue(fragment.contains("WindowInsetsCompat.Type.navigationBars()"))
assertTrue(fragment.contains("layoutCreatorChannelLiveOwnerCta.updateLayoutParams<ConstraintLayout.LayoutParams>"))
assertTrue(fragment.contains("rvCreatorChannelLiveReplays.updatePadding"))
assertTrue(fragment.contains("onCreatorChannelOwnerChanged(isOwner: Boolean)"))
assertTrue(fragment.contains("bindOwnerCta(host.isCreatorChannelOwner())"))
assertTrue(fragment.contains("layoutCreatorChannelLiveOwnerCta.isVisible = isOwner"))
assertTrue(fragment.contains("layoutCreatorChannelLiveOwnerCta.setOnClickListener"))
assertTrue(fragment.contains("host.onCreatorChannelLiveStartClicked()"))
assertTrue(fragment.contains("layoutCreatorChannelLiveOwnerCta.isEnabled = false"))
assertTrue(fragment.contains("onResume()"))
assertTrue(fragment.contains("interface Host"))
assertTrue(fragment.contains("fun isCreatorChannelOwner(): Boolean"))
assertTrue(fragment.contains("fun onCreatorChannelLiveStartClicked()"))
assertTrue(activityLayout.contains("android:id=\"@+id/layout_creator_channel_live_owner_cta\""))
assertTrue(activityLayout.contains("android:layout_height=\"100dp\""))
assertTrue(activityLayout.contains("android:background=\"@color/black\""))
assertTrue(activityLayout.contains("android:id=\"@+id/btn_creator_channel_live_owner_cta\""))
assertTrue(activityLayout.contains("android:layout_marginHorizontal=\"@dimen/spacing_14\""))
assertTrue(activityLayout.contains("android:layout_marginTop=\"@dimen/spacing_14\""))
assertTrue(activityLayout.contains("@drawable/bg_creator_channel_owner_fab"))
assertTrue(activityLayout.contains("@drawable/ic_new_create_live"))
assertTrue(activityLayout.contains("@string/creator_channel_live_start_button"))
assertTrue(activity.contains("setupLiveOwnerCtaInsets()"))
assertTrue(activity.contains("updateLiveOwnerCtaVisibility()"))
assertTrue(activity.contains("binding.viewPager.currentItem == CreatorChannelTab.Live.ordinal"))
assertTrue(activity.contains("binding.layoutCreatorChannelLiveOwnerCta.isVisible = shouldShowLiveOwnerCta"))
assertTrue(
activity.contains(
"findLiveFragment()?.onCreatorChannelLiveOwnerCtaVisibilityChanged(shouldShowLiveOwnerCta)"
)
)
assertTrue(fragment.contains("fun onCreatorChannelLiveOwnerCtaVisibilityChanged(isVisible: Boolean)"))
assertTrue(fragment.contains("val bottomPadding = if (isVisible)"))
assertTrue(fragment.contains("LIVE_OWNER_CTA_LIST_BOTTOM_PADDING_DP.dpToPx().toInt()"))
assertTrue(fragment.contains("DEFAULT_LIST_BOTTOM_PADDING_DP.dpToPx().toInt()"))
assertTrue(fragment.contains("rvCreatorChannelLiveReplays.updatePadding(bottom = bottomPadding)"))
assertTrue(activity.contains("binding.btnCreatorChannelLiveOwnerCta.setOnClickListener"))
assertTrue(activity.contains("onOwnerFabLiveClicked()"))
assertFalse(fragmentLayout.contains("android:id=\"@+id/layout_creator_channel_live_owner_cta\""))
assertFalse(fragment.contains("layoutCreatorChannelLiveOwnerCta"))
}
@Test