feat(creator): 라이브 탭 본인 CTA를 연결한다

This commit is contained in:
2026-06-18 11:12:55 +09:00
parent 0d11839a96
commit a49951c51f
4 changed files with 110 additions and 0 deletions

View File

@@ -387,6 +387,7 @@ class CreatorChannelActivity :
bindHeader(header) bindHeader(header)
bindTitleBar(header) bindTitleBar(header)
updateOwnerFabVisibility() updateOwnerFabVisibility()
findLiveFragment()?.onCreatorChannelOwnerChanged(header.isOwner)
} }
override fun onCreatorChannelFollowProgressChanged(inProgress: Boolean) { override fun onCreatorChannelFollowProgressChanged(inProgress: Boolean) {
@@ -425,6 +426,10 @@ class CreatorChannelActivity :
postCheckCreatorChannelLiveNeedsMore() postCheckCreatorChannelLiveNeedsMore()
} }
override fun isCreatorChannelOwner(): Boolean {
return currentHeader?.isOwner == true
}
private fun setupOwnerFabInsets() { private fun setupOwnerFabInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.ownerFabButton) { _, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.ownerFabButton) { _, insets ->
val navigationBottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom val navigationBottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
@@ -556,6 +561,10 @@ class CreatorChannelActivity :
startAudioContentDetail(audioContentId) startAudioContentDetail(audioContentId)
} }
override fun onCreatorChannelLiveStartClicked() {
onOwnerFabLiveClicked()
}
private fun findLiveFragment(): CreatorChannelLiveFragment? { private fun findLiveFragment(): CreatorChannelLiveFragment? {
val fragmentTag = "f${CreatorChannelTab.Live.ordinal}" val fragmentTag = "f${CreatorChannelTab.Live.ordinal}"
return supportFragmentManager.findFragmentByTag(fragmentTag) as? CreatorChannelLiveFragment return supportFragmentManager.findFragmentByTag(fragmentTag) as? CreatorChannelLiveFragment

View File

@@ -3,11 +3,17 @@ package kr.co.vividnext.sodalive.v2.creator.channel.live
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast 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.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelLiveBinding import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelLiveBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
import kr.co.vividnext.sodalive.v2.creator.channel.live.model.toLabelResId import kr.co.vividnext.sodalive.v2.creator.channel.live.model.toLabelResId
@@ -28,6 +34,8 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
private var lastContentLayoutKey: CreatorChannelLiveContentLayoutKey? = null private var lastContentLayoutKey: CreatorChannelLiveContentLayoutKey? = null
private var sortPopup: CreatorChannelLiveSortPopup? = null private var sortPopup: CreatorChannelLiveSortPopup? = null
private var currentContentState: CreatorChannelLiveUiState.Content? = null private var currentContentState: CreatorChannelLiveUiState.Content? = null
private var isOwner: Boolean = false
private var ownerCtaBottomInset: Int = 0
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L } private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
private val host: Host private val host: Host
get() = requireActivity() as Host get() = requireActivity() as Host
@@ -36,8 +44,15 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
bindLoading() bindLoading()
setupReplayList() setupReplayList()
setupOwnerCtaInsets()
setupClickListeners() setupClickListeners()
observeViewModel() observeViewModel()
bindOwnerCta(host.isCreatorChannelOwner())
}
override fun onResume() {
super.onResume()
binding.layoutCreatorChannelLiveOwnerCta.isEnabled = true
} }
fun onCreatorChannelLiveTabSelected() { fun onCreatorChannelLiveTabSelected() {
@@ -63,6 +78,19 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
viewModel.loadMore() viewModel.loadMore()
} }
fun onCreatorChannelOwnerChanged(isOwner: Boolean) {
bindOwnerCta(isOwner)
}
private fun setupOwnerCtaInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.layoutCreatorChannelLiveOwnerCta) { _, insets ->
ownerCtaBottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
updateOwnerCtaInsets()
insets
}
ViewCompat.requestApplyInsets(binding.layoutCreatorChannelLiveOwnerCta)
}
private fun setupClickListeners() { private fun setupClickListeners() {
binding.ivCreatorChannelLiveSort.setImageResource(R.drawable.ic_new_sort) binding.ivCreatorChannelLiveSort.setImageResource(R.drawable.ic_new_sort)
binding.layoutCreatorChannelLiveSortButton.setOnClickListener { binding.layoutCreatorChannelLiveSortButton.setOnClickListener {
@@ -71,6 +99,29 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
binding.btnCreatorChannelLiveRetry.setOnClickListener { binding.btnCreatorChannelLiveRetry.setOnClickListener {
viewModel.retryLive() 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() { private fun observeViewModel() {
@@ -174,13 +225,18 @@ class CreatorChannelLiveFragment : BaseFragment<FragmentCreatorChannelLiveBindin
} }
interface Host { interface Host {
fun isCreatorChannelOwner(): Boolean
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse) fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
fun onCreatorChannelLiveReplayClicked(audioContentId: Long) fun onCreatorChannelLiveReplayClicked(audioContentId: Long)
fun onCreatorChannelLiveStartClicked()
fun onCreatorChannelLiveContentChanged() fun onCreatorChannelLiveContentChanged()
} }
companion object { companion object {
private const val ARG_CREATOR_ID: String = "arg_creator_id" 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
fun newInstance(creatorId: Long): CreatorChannelLiveFragment { fun newInstance(creatorId: Long): CreatorChannelLiveFragment {
return CreatorChannelLiveFragment().apply { return CreatorChannelLiveFragment().apply {

View File

@@ -994,6 +994,25 @@ class CreatorChannelActivitySourceTest {
assertFalse(source.contains("startActivity(Intent(this, LiveRoomCreateActivity::class.java))")) assertFalse(source.contains("startActivity(Intent(this, LiveRoomCreateActivity::class.java))"))
} }
@Test
fun `라이브 탭 owner CTA는 header 본인 여부와 기존 라이브 생성 플로우를 재사용한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt"
).readText()
assertTrue(source.contains("findLiveFragment()?.onCreatorChannelOwnerChanged(header.isOwner)"))
assertTrue(source.contains("override fun isCreatorChannelOwner(): Boolean"))
assertTrue(source.contains("return currentHeader?.isOwner == true"))
assertTrue(source.contains("override fun onCreatorChannelLiveStartClicked()"))
assertTrue(source.contains("onOwnerFabLiveClicked()"))
assertTrue(source.contains("liveRoomCreateLauncher.launch(Intent(this, LiveRoomCreateActivity::class.java))"))
assertTrue(fragment.contains("fun onCreatorChannelOwnerChanged(isOwner: Boolean)"))
assertTrue(fragment.contains("host.onCreatorChannelLiveStartClicked()"))
}
@Test @Test
fun `크리에이터 채널 라이브 로직은 coordinator로 분리한다`() { fun `크리에이터 채널 라이브 로직은 coordinator로 분리한다`() {
val activity = projectFile( val activity = projectFile(

View File

@@ -196,6 +196,32 @@ class CreatorChannelLiveFragmentLayoutTest {
assertTrue(adapter.contains("layoutCreatorChannelLiveReplayActionText.isVisible = true")) assertTrue(adapter.contains("layoutCreatorChannelLiveReplayActionText.isVisible = true"))
} }
@Test
fun `라이브 owner CTA source는 본인 여부 노출 inset padding click 연결을 포함한다`() {
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()
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()"))
}
@Test @Test
fun `라이브 sort popup source는 outside dismiss와 화면 밖 보정 및 같은 정렬 dismiss를 포함한다`() { fun `라이브 sort popup source는 outside dismiss와 화면 밖 보정 및 같은 정렬 dismiss를 포함한다`() {
val popup = projectFile( val popup = projectFile(