From 757f24228560936ee706d5bbfdf7d438d1a336ec Mon Sep 17 00:00:00 2001 From: klaus Date: Fri, 19 Jun 2026 21:04:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=EC=98=A4=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=ED=83=AD=20activity=20=EC=97=B0=EB=8F=99=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../creator/channel/CreatorChannelActivity.kt | 150 +++++++++++++----- .../res/layout/activity_creator_channel.xml | 6 +- .../CreatorChannelActivitySourceTest.kt | 60 ++++--- 3 files changed, 148 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt index b8dc2482..ae197419 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt @@ -54,6 +54,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioConte import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse +import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragment import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelScrollState @@ -68,7 +69,8 @@ import org.koin.android.ext.android.inject class CreatorChannelActivity : BaseActivity(ActivityCreatorChannelBinding::inflate), CreatorChannelHomeFragment.Host, - CreatorChannelLiveFragment.Host { + CreatorChannelLiveFragment.Host, + CreatorChannelAudioFragment.Host { private val liveViewModel: LiveViewModel by inject() private val myPageViewModel: MyPageViewModel by inject() @@ -129,7 +131,7 @@ class CreatorChannelActivity : setStatusBarIconAppearance() setTitleBarTopInset() setupOwnerFabInsets() - setupLiveOwnerCtaInsets() + setupOwnerCtaInsets() setupScrollListener() setupClickListeners() setupLiveEntryObservers() @@ -160,7 +162,7 @@ class CreatorChannelActivity : binding.ownerFabCommunityButton.setOnClickListener { onOwnerFabCommunityClicked() } binding.ownerFabAudioButton.setOnClickListener { onOwnerFabAudioClicked() } binding.ownerFabLiveButton.setOnClickListener { onOwnerFabLiveClicked() } - binding.btnCreatorChannelLiveOwnerCta.setOnClickListener { onLiveOwnerCtaClicked() } + binding.btnCreatorChannelOwnerCta.setOnClickListener { onOwnerCtaClicked() } binding.tvChatButton.setOnClickListener { currentHeader?.characterId?.let { characterId -> homeActionDelegate?.createChatRoom(characterId) } } @@ -273,18 +275,18 @@ class CreatorChannelActivity : } private fun onCreatorChannelNestedScrollChanged(scrollY: Int, oldScrollY: Int) { - if (binding.viewPager.currentItem != CreatorChannelTab.Live.ordinal) return + if (!isCreatorChannelLoadMoreTab(binding.viewPager.currentItem)) return if (scrollY <= oldScrollY) return val contentHeight = binding.nestedScrollView.getChildAt(0)?.height ?: return - val threshold = CREATOR_CHANNEL_LIVE_LOAD_MORE_THRESHOLD_DP.dpToPx().toInt() - val remainingScroll = calculateCreatorChannelLiveRemainingScroll( + val threshold = CREATOR_CHANNEL_LOAD_MORE_THRESHOLD_DP.dpToPx().toInt() + val remainingScroll = calculateCreatorChannelRemainingScroll( contentHeight = contentHeight, viewportHeight = binding.nestedScrollView.height, scrollY = scrollY ) if (remainingScroll <= threshold) { - findLiveFragment()?.onCreatorChannelLiveScrolledToBottom() + notifyCurrentCreatorChannelTabScrolledToBottom() } } @@ -390,13 +392,16 @@ class CreatorChannelActivity : collapseOwnerFab(animate = false) } updateOwnerFabVisibility() - updateLiveOwnerCtaVisibility() - updateCreatorChannelLiveViewportHeight() + updateOwnerCtaVisibility() + updateCreatorChannelTabViewportHeight() updateViewPagerHeight() - if (position == CreatorChannelTab.Live.ordinal) { - binding.viewPager.post { + when (position) { + CreatorChannelTab.Live.ordinal -> binding.viewPager.post { findLiveFragment()?.onCreatorChannelLiveTabSelected() } + CreatorChannelTab.Audio.ordinal -> binding.viewPager.post { + findAudioFragment()?.onCreatorChannelAudioTabSelected() + } } } } @@ -409,7 +414,12 @@ class CreatorChannelActivity : bindHeader(header) bindTitleBar(header) updateOwnerFabVisibility() - updateLiveOwnerCtaVisibility() + updateOwnerCtaVisibility() + if (binding.viewPager.currentItem == CreatorChannelTab.Audio.ordinal) { + binding.viewPager.post { + findAudioFragment()?.onCreatorChannelAudioTabSelected() + } + } } override fun onCreatorChannelFollowProgressChanged(inProgress: Boolean) { @@ -444,28 +454,63 @@ class CreatorChannelActivity : } override fun onCreatorChannelLiveContentChanged() { - updateCreatorChannelLiveViewportHeight() + updateCreatorChannelTabViewportHeight() updateViewPagerHeight() - postCheckCreatorChannelLiveNeedsMore() + postCheckCreatorChannelCurrentTabNeedsMore() + } + + override fun isCreatorChannelOwner(): Boolean = currentHeader?.isOwner == true + + override fun onCreatorChannelAudioContentClicked(audioContentId: Long) { + startAudioContentDetail(audioContentId) + } + + override fun onCreatorChannelAudioContentChanged() { + updateCreatorChannelTabViewportHeight() + updateViewPagerHeight() + postCheckCreatorChannelCurrentTabNeedsMore() } private fun setupOwnerFabInsets() { binding.viewPager.updatePadding(bottom = OWNER_FAB_CONTENT_BOTTOM_PADDING_DP.dpToPx().toInt()) } - private fun setupLiveOwnerCtaInsets() { - updateLiveOwnerCtaVisibility() + private fun setupOwnerCtaInsets() { + updateOwnerCtaVisibility() } - private fun updateLiveOwnerCtaVisibility() { - val shouldShowLiveOwnerCta = shouldShowLiveOwnerCta() - binding.layoutCreatorChannelLiveOwnerCta.isVisible = shouldShowLiveOwnerCta - binding.btnCreatorChannelLiveOwnerCta.isEnabled = true - findLiveFragment()?.onCreatorChannelLiveOwnerCtaVisibilityChanged(shouldShowLiveOwnerCta) + private fun updateOwnerCtaVisibility() { + val ownerCtaTab = currentOwnerCtaTab() + val shouldShowOwnerCta = ownerCtaTab != null + binding.layoutCreatorChannelOwnerCta.isVisible = shouldShowOwnerCta + binding.btnCreatorChannelOwnerCta.isEnabled = true + when (ownerCtaTab) { + CreatorChannelTab.Live -> bindOwnerCta( + iconResId = R.drawable.ic_new_create_live, + textResId = R.string.creator_channel_live_start_button + ) + CreatorChannelTab.Audio -> bindOwnerCta( + iconResId = R.drawable.ic_new_upload_audio, + textResId = R.string.creator_channel_audio_upload_button + ) + else -> Unit + } + findLiveFragment()?.onCreatorChannelLiveOwnerCtaVisibilityChanged(ownerCtaTab == CreatorChannelTab.Live) + findAudioFragment()?.onCreatorChannelAudioOwnerCtaVisibilityChanged(ownerCtaTab == CreatorChannelTab.Audio) } - private fun shouldShowLiveOwnerCta(): Boolean { - return currentHeader?.isOwner == true && binding.viewPager.currentItem == CreatorChannelTab.Live.ordinal + private fun bindOwnerCta(iconResId: Int, textResId: Int) { + binding.ivCreatorChannelOwnerCta.setImageResource(iconResId) + binding.tvCreatorChannelOwnerCta.setText(textResId) + } + + private fun currentOwnerCtaTab(): CreatorChannelTab? { + if (currentHeader?.isOwner != true) return null + return when (binding.viewPager.currentItem) { + CreatorChannelTab.Live.ordinal -> CreatorChannelTab.Live + CreatorChannelTab.Audio.ordinal -> CreatorChannelTab.Audio + else -> null + } } private fun expandOwnerFab() { @@ -556,9 +601,12 @@ class CreatorChannelActivity : liveRoomCreateLauncher.launch(Intent(this, LiveRoomCreateActivity::class.java)) } - private fun onLiveOwnerCtaClicked() { - binding.btnCreatorChannelLiveOwnerCta.isEnabled = false - onOwnerFabLiveClicked() + private fun onOwnerCtaClicked() { + binding.btnCreatorChannelOwnerCta.isEnabled = false + when (binding.viewPager.currentItem) { + CreatorChannelTab.Live.ordinal -> onOwnerFabLiveClicked() + CreatorChannelTab.Audio.ordinal -> onOwnerFabAudioClicked() + } } override fun onCreatorChannelDonationClicked() { @@ -593,6 +641,22 @@ class CreatorChannelActivity : return supportFragmentManager.findFragmentByTag(fragmentTag) as? CreatorChannelLiveFragment } + private fun findAudioFragment(): CreatorChannelAudioFragment? { + val fragmentTag = "f${CreatorChannelTab.Audio.ordinal}" + return supportFragmentManager.findFragmentByTag(fragmentTag) as? CreatorChannelAudioFragment + } + + private fun notifyCurrentCreatorChannelTabScrolledToBottom() { + when (binding.viewPager.currentItem) { + CreatorChannelTab.Live.ordinal -> findLiveFragment()?.onCreatorChannelLiveScrolledToBottom() + CreatorChannelTab.Audio.ordinal -> findAudioFragment()?.onCreatorChannelAudioScrolledToBottom() + } + } + + private fun isCreatorChannelLoadMoreTab(position: Int): Boolean { + return position == CreatorChannelTab.Live.ordinal || position == CreatorChannelTab.Audio.ordinal + } + private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit) { if (SharedPreferenceManager.token.isBlank()) { showLoginActivity() @@ -659,7 +723,7 @@ class CreatorChannelActivity : } private fun updateViewPagerHeight() { - updateCreatorChannelLiveViewportHeight() + updateCreatorChannelTabViewportHeight() binding.viewPager.post { val recyclerView = binding.viewPager.getChildAt(0) as? RecyclerView ?: return@post val currentPage = recyclerView.layoutManager?.findViewByPosition(binding.viewPager.currentItem) ?: return@post @@ -675,15 +739,15 @@ class CreatorChannelActivity : } } - private fun updateCreatorChannelLiveViewportHeight() { - if (binding.viewPager.currentItem != CreatorChannelTab.Live.ordinal) return - - findLiveFragment()?.onCreatorChannelLiveViewportHeightChanged( - calculateCreatorChannelLiveEmptyMinHeight() - ) + private fun updateCreatorChannelTabViewportHeight() { + val minHeight = calculateCreatorChannelTabEmptyMinHeight() + when (binding.viewPager.currentItem) { + CreatorChannelTab.Live.ordinal -> findLiveFragment()?.onCreatorChannelLiveViewportHeightChanged(minHeight) + CreatorChannelTab.Audio.ordinal -> findAudioFragment()?.onCreatorChannelAudioViewportHeightChanged(minHeight) + } } - private fun calculateCreatorChannelLiveEmptyMinHeight(): Int { + private fun calculateCreatorChannelTabEmptyMinHeight(): Int { val stickyScrollY = calculateCreatorChannelStickyScrollY() val visibleTabViewportHeight = binding.nestedScrollView.height - binding.tabLayout.height val scrollRangeRequiredHeight = binding.nestedScrollView.height + @@ -692,23 +756,23 @@ class CreatorChannelActivity : return maxOf(visibleTabViewportHeight, scrollRangeRequiredHeight, 0) } - private fun postCheckCreatorChannelLiveNeedsMore() { + private fun postCheckCreatorChannelCurrentTabNeedsMore() { binding.nestedScrollView.post { - checkCreatorChannelLiveNeedsMore() + checkCreatorChannelCurrentTabNeedsMore() } } - private fun checkCreatorChannelLiveNeedsMore() { - if (binding.viewPager.currentItem != CreatorChannelTab.Live.ordinal) return + private fun checkCreatorChannelCurrentTabNeedsMore() { + if (!isCreatorChannelLoadMoreTab(binding.viewPager.currentItem)) return val contentHeight = binding.nestedScrollView.getChildAt(0)?.height ?: return - val remainingScroll = calculateCreatorChannelLiveRemainingScroll( + val remainingScroll = calculateCreatorChannelRemainingScroll( contentHeight = contentHeight, viewportHeight = binding.nestedScrollView.height, scrollY = binding.nestedScrollView.scrollY ) if (remainingScroll <= 0) { - findLiveFragment()?.onCreatorChannelLiveScrolledToBottom() + notifyCurrentCreatorChannelTabScrolledToBottom() } } @@ -749,7 +813,7 @@ class CreatorChannelActivity : override fun onResume() { super.onResume() - binding.btnCreatorChannelLiveOwnerCta.isEnabled = true + binding.btnCreatorChannelOwnerCta.isEnabled = true } override fun onDestroy() { @@ -787,7 +851,7 @@ class CreatorChannelActivity : private const val OWNER_FAB_SPRING_MASS = 1f private const val OWNER_FAB_SPRING_STIFFNESS = 256f private const val OWNER_FAB_SPRING_DAMPING = 24f - private const val CREATOR_CHANNEL_LIVE_LOAD_MORE_THRESHOLD_DP = 200 + private const val CREATOR_CHANNEL_LOAD_MORE_THRESHOLD_DP = 200 fun newIntent(context: Context, creatorId: Long): Intent { return Intent(context, CreatorChannelActivity::class.java).apply { @@ -797,7 +861,7 @@ class CreatorChannelActivity : } } -internal fun calculateCreatorChannelLiveRemainingScroll( +internal fun calculateCreatorChannelRemainingScroll( contentHeight: Int, viewportHeight: Int, scrollY: Int diff --git a/app/src/main/res/layout/activity_creator_channel.xml b/app/src/main/res/layout/activity_creator_channel.xml index 9896596b..db3e09ad 100644 --- a/app/src/main/res/layout/activity_creator_channel.xml +++ b/app/src/main/res/layout/activity_creator_channel.xml @@ -239,7 +239,7 @@ ")) + assertTrue(pagerAdapter.contains("CreatorChannelTab.Audio -> CreatorChannelAudioFragment.newInstance(creatorId)")) assertFalse(source.contains("CreatorChannelTab.Series ->")) assertFalse(source.contains("CreatorChannelTab.Community ->")) assertFalse(source.contains("CreatorChannelTab.FanTalk ->")) @@ -925,16 +929,19 @@ class CreatorChannelActivitySourceTest { assertTrue(source.contains("private fun onCreatorChannelNestedScrollChanged(scrollY: Int, oldScrollY: Int)")) assertTrue(source.contains("binding.nestedScrollView.getChildAt(0)?.height")) assertTrue(source.contains("binding.nestedScrollView.height")) - assertTrue(source.contains("CREATOR_CHANNEL_LIVE_LOAD_MORE_THRESHOLD_DP.dpToPx().toInt()")) + assertTrue(source.contains("CREATOR_CHANNEL_LOAD_MORE_THRESHOLD_DP.dpToPx().toInt()")) assertTrue(source.contains("findLiveFragment()?.onCreatorChannelLiveScrolledToBottom()")) + assertTrue(source.contains("findAudioFragment()?.onCreatorChannelAudioScrolledToBottom()")) assertTrue(source.contains("private fun findLiveFragment(): CreatorChannelLiveFragment?")) assertTrue(source.contains("supportFragmentManager.findFragmentByTag")) assertTrue(source.contains("override fun onCreatorChannelLiveContentChanged()")) - assertTrue(source.contains("postCheckCreatorChannelLiveNeedsMore()")) + assertTrue(source.contains("postCheckCreatorChannelCurrentTabNeedsMore()")) assertTrue(source.contains("updateViewPagerHeight()")) - assertTrue(source.contains("updateCreatorChannelLiveViewportHeight()")) - assertTrue(source.contains("if (position == CreatorChannelTab.Live.ordinal)")) + assertTrue(source.contains("updateCreatorChannelTabViewportHeight()")) + assertTrue(source.contains("CreatorChannelTab.Live.ordinal -> binding.viewPager.post")) assertTrue(source.contains("findLiveFragment()?.onCreatorChannelLiveTabSelected()")) + assertTrue(source.contains("CreatorChannelTab.Audio.ordinal -> binding.viewPager.post")) + assertTrue(source.contains("findAudioFragment()?.onCreatorChannelAudioTabSelected()")) assertTrue(source.contains("binding.viewPager.offscreenPageLimit = CreatorChannelTab.entries.size - 1")) assertFalse(source.contains("currentPage.minimumHeight = calculateCreatorChannelTabViewportHeight()")) assertFalse(source.contains("private fun calculateCreatorChannelTabViewportHeight(): Int")) @@ -950,10 +957,11 @@ class CreatorChannelActivitySourceTest { "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" ).readText() - assertTrue(source.contains("private fun updateCreatorChannelLiveViewportHeight()")) - assertTrue(source.contains("findLiveFragment()?.onCreatorChannelLiveViewportHeightChanged")) - assertTrue(source.contains("calculateCreatorChannelLiveEmptyMinHeight()")) - assertTrue(source.contains("private fun calculateCreatorChannelLiveEmptyMinHeight(): Int")) + assertTrue(source.contains("private fun updateCreatorChannelTabViewportHeight()")) + assertTrue(source.contains("findLiveFragment()?.onCreatorChannelLiveViewportHeightChanged(minHeight)")) + assertTrue(source.contains("findAudioFragment()?.onCreatorChannelAudioViewportHeightChanged(minHeight)")) + assertTrue(source.contains("calculateCreatorChannelTabEmptyMinHeight()")) + assertTrue(source.contains("private fun calculateCreatorChannelTabEmptyMinHeight(): Int")) assertTrue(source.contains("val stickyScrollY = calculateCreatorChannelStickyScrollY()")) assertTrue(source.contains("binding.nestedScrollView.height - binding.tabLayout.height")) assertTrue(source.contains("val scrollRangeRequiredHeight = binding.nestedScrollView.height +")) @@ -989,18 +997,18 @@ class CreatorChannelActivitySourceTest { "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" ).readText() - assertTrue(source.contains("private fun postCheckCreatorChannelLiveNeedsMore()")) + assertTrue(source.contains("private fun postCheckCreatorChannelCurrentTabNeedsMore()")) assertTrue(source.contains("binding.nestedScrollView.post")) - assertTrue(source.contains("checkCreatorChannelLiveNeedsMore()")) - assertTrue(source.contains("if (binding.viewPager.currentItem != CreatorChannelTab.Live.ordinal) return")) + assertTrue(source.contains("checkCreatorChannelCurrentTabNeedsMore()")) + assertTrue(source.contains("if (!isCreatorChannelLoadMoreTab(binding.viewPager.currentItem)) return")) assertTrue(source.contains("findLiveFragment()?.onCreatorChannelLiveScrolledToBottom()")) } @Test fun `라이브 pagination remaining scroll helper는 content viewport scroll 차이를 계산한다`() { - assertEquals(100, calculateCreatorChannelLiveRemainingScroll(contentHeight = 1000, viewportHeight = 700, scrollY = 200)) - assertEquals(0, calculateCreatorChannelLiveRemainingScroll(contentHeight = 1000, viewportHeight = 700, scrollY = 300)) - assertEquals(-20, calculateCreatorChannelLiveRemainingScroll(contentHeight = 1000, viewportHeight = 700, scrollY = 320)) + assertEquals(100, calculateCreatorChannelRemainingScroll(contentHeight = 1000, viewportHeight = 700, scrollY = 200)) + assertEquals(0, calculateCreatorChannelRemainingScroll(contentHeight = 1000, viewportHeight = 700, scrollY = 300)) + assertEquals(-20, calculateCreatorChannelRemainingScroll(contentHeight = 1000, viewportHeight = 700, scrollY = 320)) } @Test @@ -1042,17 +1050,23 @@ class CreatorChannelActivitySourceTest { ).readText() val activityLayout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText() - assertTrue(activityLayout.contains("android:id=\"@+id/layout_creator_channel_live_owner_cta\"")) - assertTrue(source.contains("updateLiveOwnerCtaVisibility()")) + assertTrue(activityLayout.contains("android:id=\"@+id/layout_creator_channel_owner_cta\"")) + assertTrue(source.contains("updateOwnerCtaVisibility()")) + assertTrue(source.contains("if (binding.viewPager.currentItem == CreatorChannelTab.Audio.ordinal)")) + assertTrue(source.contains("findAudioFragment()?.onCreatorChannelAudioTabSelected()")) assertTrue( source.contains( - "currentHeader?.isOwner == true && binding.viewPager.currentItem == CreatorChannelTab.Live.ordinal" + "CreatorChannelTab.Live.ordinal -> CreatorChannelTab.Live" ) ) - assertTrue(source.contains("binding.layoutCreatorChannelLiveOwnerCta.isVisible = shouldShowLiveOwnerCta")) - assertTrue(source.contains("findLiveFragment()?.onCreatorChannelLiveOwnerCtaVisibilityChanged(shouldShowLiveOwnerCta)")) - assertTrue(source.contains("return currentHeader?.isOwner == true")) - assertTrue(source.contains("binding.btnCreatorChannelLiveOwnerCta.setOnClickListener { onLiveOwnerCtaClicked() }")) + assertTrue(source.contains("binding.layoutCreatorChannelOwnerCta.isVisible = shouldShowOwnerCta")) + assertTrue( + source.contains( + "findLiveFragment()?.onCreatorChannelLiveOwnerCtaVisibilityChanged(ownerCtaTab == CreatorChannelTab.Live)" + ) + ) + assertTrue(source.contains("if (currentHeader?.isOwner != true) return null")) + assertTrue(source.contains("binding.btnCreatorChannelOwnerCta.setOnClickListener { onOwnerCtaClicked() }")) assertTrue(source.contains("onOwnerFabLiveClicked()")) assertTrue(source.contains("liveRoomCreateLauncher.launch(Intent(this, LiveRoomCreateActivity::class.java))")) } @@ -1065,7 +1079,7 @@ class CreatorChannelActivitySourceTest { assertFalse(source.contains("binding.ownerFabButton.updateLayoutParams")) assertFalse(source.contains("binding.ownerFabExpandedContainer.updateLayoutParams")) - assertFalse(source.contains("binding.layoutCreatorChannelLiveOwnerCta.updateLayoutParams")) + assertFalse(source.contains("binding.layoutCreatorChannelOwnerCta.updateLayoutParams")) assertFalse(source.contains("WindowInsetsCompat.Type.navigationBars()).bottom")) assertTrue( source.contains(