feat(creator): 채널 홈 탭 전환을 연결한다

This commit is contained in:
2026-06-16 14:35:39 +09:00
parent 984aa13edf
commit f3c19ed8ba
5 changed files with 245 additions and 171 deletions

View File

@@ -35,17 +35,12 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("fun newIntent(context: Context, creatorId: Long): Intent"))
assertTrue(source.contains("Intent(context, CreatorChannelActivity::class.java)"))
assertTrue(source.contains("putExtra(EXTRA_CREATOR_ID, creatorId)"))
assertTrue(source.contains("private val viewModel: CreatorChannelHomeViewModel by viewModel()"))
assertTrue(source.contains("if (creatorId <= 0L)"))
assertTrue(source.contains("finish()"))
assertTrue(source.contains("viewModel.loadHome(creatorId)"))
assertTrue(source.contains("viewModel.homeStateLiveData.observe(this)"))
assertTrue(source.contains("viewModel.chatRoomIdLiveData.observe(this)"))
assertFalse(source.contains("is CreatorChannelHomeUiState.Error -> showToast"))
assertTrue(source.contains("event.consume()?.let"))
assertTrue(source.contains("ChatRoomActivity.newIntent(this, chatRoomId)"))
assertTrue(source.contains("DmChatRoomActivity.newIntentByCreatorId(this, creatorId)"))
assertTrue(source.contains("viewModel.createChatRoom(characterId)"))
assertTrue(source.contains("homeActionDelegate?.createChatRoom(characterId)"))
assertTrue(source.contains("updateActionButtonLayout"))
assertTrue(source.contains("marginStart = if (isChatVisible && isDmVisible)"))
}
@@ -61,32 +56,90 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("binding.ivBell.setOnClickListener"))
assertTrue(source.contains("private fun onFollowActionClicked"))
assertTrue(source.contains("if (!header.isFollow)"))
assertTrue(source.contains("viewModel.follow(follow = true, notify = true)"))
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = true)"))
assertTrue(source.contains("showFollowNotifyFragment"))
assertTrue(source.contains("onClickNotifyAll = { viewModel.follow(follow = true, notify = true) }"))
assertTrue(source.contains("onClickNotifyNone = { viewModel.follow(follow = true, notify = false) }"))
assertTrue(source.contains("onClickUnFollow = { viewModel.follow(follow = false, notify = false) }"))
assertTrue(source.contains("viewModel.isFollowInProgressLiveData.observe(this)"))
assertTrue(source.contains("onClickNotifyAll = { homeActionDelegate?.follow(follow = true, notify = true) }"))
assertTrue(source.contains("onClickNotifyNone = { homeActionDelegate?.follow(follow = true, notify = false) }"))
assertTrue(source.contains("onClickUnFollow = { homeActionDelegate?.follow(follow = false, notify = false) }"))
assertTrue(source.contains("onCreatorChannelFollowProgressChanged"))
assertTrue(source.contains("binding.layoutFollowCapsule.isEnabled = titleBarState.isActionEnabled"))
assertTrue(source.contains("binding.ivBell.isEnabled = titleBarState.isActionEnabled"))
}
@Test
fun `layout source는 HorizontalScrollView 기반 7개 탭 컨테이너와 RecyclerView를 가진다`() {
fun `Phase 10 컨테이너 layout은 TabLayout과 ViewPager2를 가진다`() {
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
assertTrue(layout.contains("<com.google.android.material.tabs.TabLayout"))
assertTrue(layout.contains("@+id/tab_layout"))
assertTrue(layout.contains("<androidx.viewpager2.widget.ViewPager2"))
assertTrue(layout.contains("@+id/view_pager"))
assertFalse(layout.contains("<HorizontalScrollView"))
assertFalse(layout.contains("@+id/horizontal_tab_scroll_view"))
assertFalse(layout.contains("@+id/tab_container"))
assertFalse(layout.contains("@+id/rv_home_sections"))
}
@Test
fun `Phase 10 Activity는 pager adapter를 사용하고 홈 탭 구현을 Fragment로 위임한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
assertTrue(layout.contains("<HorizontalScrollView"))
assertTrue(layout.contains("@+id/horizontal_tab_scroll_view"))
assertTrue(layout.contains("android:elevation=\"1dp\""))
assertTrue(layout.contains("@+id/tab_container"))
assertTrue(source.contains("CreatorChannelPagerAdapter"))
assertTrue(source.contains("binding.viewPager.adapter"))
assertTrue(source.contains("CreatorChannelHomeFragment"))
assertFalse(source.contains("private val viewModel: CreatorChannelHomeViewModel by viewModel()"))
assertFalse(source.contains("CreatorChannelHomeSectionAdapter"))
assertFalse(source.contains("sectionAdapter"))
assertFalse(source.contains("viewModel.loadHome(creatorId)"))
assertFalse(source.contains("bindTabs(content.tabs)"))
assertFalse(source.contains("private fun createTabView"))
assertFalse(source.contains("private fun onTabClicked"))
}
@Test
fun `home Fragment source는 creatorId argument와 home ViewModel RecyclerView를 소유한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
).readText()
val layout = projectFile("app/src/main/res/layout/fragment_creator_channel_home.xml").readText()
assertTrue(source.contains("BaseFragment<FragmentCreatorChannelHomeBinding>"))
assertTrue(source.contains("private const val ARG_CREATOR_ID"))
assertTrue(source.contains("fun newInstance(creatorId: Long): CreatorChannelHomeFragment"))
assertTrue(source.contains("arguments = Bundle().apply"))
assertTrue(source.contains("putLong(ARG_CREATOR_ID, creatorId)"))
assertTrue(source.contains("private val viewModel: CreatorChannelHomeViewModel by viewModel()"))
assertTrue(source.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked, ::onAudioContentClicked)"))
assertTrue(source.contains("binding.rvHomeSections.layoutManager = LinearLayoutManager(requireContext())"))
assertTrue(source.contains("binding.rvHomeSections.adapter = sectionAdapter"))
assertTrue(source.contains("viewModel.homeStateLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("viewModel.chatRoomIdLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("viewModel.toastLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("viewModel.isFollowInProgressLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("host.onCreatorChannelHomeContentChanged()"))
assertTrue(source.contains("binding.rvHomeSections.adapter = null"))
assertTrue(source.contains("if (creatorId > 0L)"))
assertTrue(source.contains("viewModel.loadHome(creatorId)"))
assertTrue(layout.contains("@+id/rv_home_sections"))
}
@Test
fun `layout source는 Activity 컨테이너와 홈 Fragment RecyclerView를 분리한다`() {
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
val fragmentLayout = projectFile("app/src/main/res/layout/fragment_creator_channel_home.xml").readText()
assertTrue(layout.contains("@+id/tab_layout"))
assertTrue(layout.contains("@+id/view_pager"))
assertTrue(layout.contains("android:layout_height=\"1dp\""))
assertFalse(layout.contains("@+id/horizontal_tab_scroll_view"))
assertFalse(layout.contains("@+id/tab_container"))
assertFalse(layout.contains("@+id/rv_home_sections"))
assertTrue(fragmentLayout.contains("@+id/rv_home_sections"))
assertTrue(fragmentLayout.contains("tools:listitem=\"@layout/item_creator_channel_home_audio\""))
assertTrue(layout.contains("android:drawableStart=\"@drawable/ic_new_talk\""))
assertTrue(layout.contains("android:drawableStart=\"@drawable/ic_new_dm\""))
assertFalse(layout.contains("TextTabBarView"))
assertTrue(source.contains("getString(tab.labelResId)"))
}
@Test
@@ -121,7 +174,7 @@ class CreatorChannelActivitySourceTest {
}
@Test
fun `scroll source는 tab sticky와 title bar black 전환을 연결한다`() {
fun `scroll source는 tab sticky와 title bar black 전환을 유지한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
@@ -129,7 +182,8 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("setupScrollListener"))
assertTrue(source.contains("binding.nestedScrollView.setOnScrollChangeListener"))
assertTrue(source.contains("CreatorChannelScrollState.calculateStickyTop"))
assertTrue(source.contains("binding.horizontalTabScrollView.translationY"))
assertTrue(source.contains("binding.tabLayout.translationY = tabTranslationY.toFloat()"))
assertTrue(source.contains("val tabBarTop = headerHeight - scrollY + tabTranslationY"))
assertTrue(source.contains("CreatorChannelScrollState.shouldUseBlackTitleBar"))
assertTrue(source.contains("binding.titleBarContainer.setBackgroundColor"))
assertTrue(source.contains("Color.BLACK"))
@@ -147,29 +201,67 @@ class CreatorChannelActivitySourceTest {
}
@Test
fun `tab source는 Figma 기준 selected indicator와 16sp 고정 폭 탭을 사용한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
fun `pager adapter source는 7개 탭 순서와 홈 placeholder Fragment를 연결한다`() {
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt"
).readText()
val placeholder = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPlaceholderFragment.kt"
).readText()
val placeholderLayout = projectFile(
"app/src/main/res/layout/fragment_creator_channel_placeholder.xml"
).readText()
assertTrue(source.contains("createTabView(tab, isSelected = tab == selectedTab)"))
assertTrue(source.contains("tabText.textSize = 16f"))
assertTrue(source.contains("width = 110.dpToPx().toInt()"))
assertTrue(source.contains("indicator.setBackgroundColor(getColor(R.color.soda_400))"))
assertTrue(source.contains("indicator.isVisible = isSelected"))
assertTrue(adapter.contains("class CreatorChannelPagerAdapter"))
assertTrue(adapter.contains("FragmentStateAdapter"))
assertTrue(adapter.contains("private val tabs: List<CreatorChannelTab> = CreatorChannelTab.entries"))
assertTrue(adapter.contains("override fun getItemCount(): Int = tabs.size"))
assertTrue(adapter.contains("CreatorChannelTab.Home -> CreatorChannelHomeFragment.newInstance(creatorId)"))
assertTrue(adapter.contains("else -> CreatorChannelPlaceholderFragment.newInstance(tab)"))
assertTrue(placeholder.contains("private const val ARG_TAB_NAME"))
assertTrue(placeholder.contains("fun newInstance(tab: CreatorChannelTab): CreatorChannelPlaceholderFragment"))
assertTrue(placeholderLayout.contains("@+id/tv_placeholder"))
assertTrue(placeholderLayout.contains("android:layout_height=\"160dp\""))
assertFalse(placeholderLayout.contains("android:visibility=\"gone\""))
}
@Test
fun `tab source는 홈 기본 선택과 홈 외 탭 no op 정책을 명시한다`() {
fun `tab source는 TabLayoutMediator와 ViewPager2를 연결한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
assertTrue(source.contains("private var selectedTab: CreatorChannelTab = CreatorChannelTab.Home"))
assertTrue(source.contains("createTabView(tab, isSelected = tab == selectedTab)"))
assertTrue(source.contains("setOnClickListener { onTabClicked(tab) }"))
assertTrue(source.contains("private fun onTabClicked(tab: CreatorChannelTab)"))
assertTrue(source.contains("if (tab != CreatorChannelTab.Home) return"))
assertTrue(source.contains("TabLayoutMediator"))
assertTrue(source.contains("private var tabLayoutMediator: TabLayoutMediator? = null"))
assertTrue(source.contains("private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null"))
assertTrue(source.contains("binding.viewPager.adapter = CreatorChannelPagerAdapter(this, creatorId)"))
assertTrue(source.contains("tabLayoutMediator = TabLayoutMediator(binding.tabLayout, binding.viewPager)"))
assertTrue(source.contains("tab.text = getString(CreatorChannelTab.entries[position].labelResId)"))
assertTrue(source.contains(".attach()"))
assertTrue(source.contains("binding.viewPager.isUserInputEnabled = true"))
assertTrue(source.contains("binding.viewPager.offscreenPageLimit = CreatorChannelTab.entries.size - 1"))
assertTrue(source.contains("binding.viewPager.registerOnPageChangeCallback(callback)"))
assertTrue(source.contains("override fun onDestroy()"))
assertTrue(source.contains("tabLayoutMediator?.detach()"))
assertTrue(source.contains("binding.viewPager.unregisterOnPageChangeCallback(callback)"))
assertTrue(source.contains("private fun updateViewPagerHeight()"))
assertTrue(source.contains("findViewByPosition(binding.viewPager.currentItem)"))
assertTrue(source.contains("currentPage.measure(widthSpec, heightSpec)"))
assertTrue(source.contains("binding.viewPager.updateLayoutParams"))
assertFalse(source.contains("binding.tabLayout.addTab"))
assertFalse(source.contains("private fun createTabView"))
}
@Test
fun `tab source는 기존 custom tab no op 정책을 제거한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
assertFalse(source.contains("private var selectedTab: CreatorChannelTab = CreatorChannelTab.Home"))
assertFalse(source.contains("setOnClickListener { onTabClicked(tab) }"))
assertFalse(source.contains("private fun onTabClicked(tab: CreatorChannelTab)"))
assertFalse(source.contains("if (tab != CreatorChannelTab.Home) return"))
assertFalse(source.contains("CreatorChannelTab.Live ->"))
assertFalse(source.contains("CreatorChannelTab.Audio ->"))
assertFalse(source.contains("CreatorChannelTab.Series ->"))
@@ -589,8 +681,13 @@ class CreatorChannelActivitySourceTest {
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/CreatorChannelHomeFragment.kt"
).readText()
assertTrue(source.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked"))
assertTrue(fragment.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked"))
assertTrue(fragment.contains("private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse)"))
assertTrue(fragment.contains("host.onCreatorChannelScheduleClicked(schedule)"))
assertTrue(source.contains("private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse)"))
assertTrue(source.contains("CreatorActivityType.Audio"))
assertTrue(source.contains("CreatorActivityType.LiveReplay"))
@@ -663,8 +760,13 @@ class CreatorChannelActivitySourceTest {
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/CreatorChannelHomeFragment.kt"
).readText()
assertTrue(source.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked, ::onAudioContentClicked)"))
assertTrue(fragment.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked, ::onAudioContentClicked)"))
assertTrue(fragment.contains("private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)"))
assertTrue(fragment.contains("host.onCreatorChannelAudioContentClicked(audioContent)"))
assertTrue(source.contains("private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)"))
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContent.audioContentId)"))