From f3a4578a6633254a539c988a18779fb733853c48 Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 9 Jul 2026 08:19:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator-channel):=20=ED=99=88=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EC=9D=B4=EB=8F=99=EC=9D=84=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=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 | 13 ++++ .../channel/CreatorChannelHomeFragment.kt | 15 ++++- .../ui/CreatorChannelHomeSectionAdapter.kt | 38 +++++++++++- .../CreatorChannelActivitySourceTest.kt | 62 +++++++++++++++++++ 4 files changed, 124 insertions(+), 4 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 e17d7843..f24dfaf4 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 @@ -546,6 +546,11 @@ class CreatorChannelActivity : updateViewPagerHeight() } + override fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab) { + selectCreatorChannelTab(tab) + scrollCreatorChannelToTop() + } + override fun onCreatorChannelLiveContentChanged() { updateCreatorChannelTabViewportHeight() updateViewPagerHeight { @@ -1184,6 +1189,14 @@ class CreatorChannelActivity : ) } + private fun selectCreatorChannelTab(tab: CreatorChannelTab) { + binding.viewPager.currentItem = tab.ordinal + } + + private fun scrollCreatorChannelToTop() { + binding.nestedScrollView.scrollTo(0, 0) + } + override fun onResume() { super.onResume() binding.btnCreatorChannelOwnerCta.isEnabled = true diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt index 03ec3f2b..50750979 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt @@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleRe import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState +import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelHomeSectionAdapter import org.koin.androidx.viewmodel.ext.android.viewModel @@ -25,7 +26,9 @@ class CreatorChannelHomeFragment : BaseFragment Unit = {}, private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit = {}, private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {}, - private val onDonationClick: () -> Unit = {} + private val onDonationClick: () -> Unit = {}, + private val onSectionChevronClick: (CreatorChannelTab) -> Unit = {}, + private val onCommunityClick: (Long) -> Unit = {} ) : RecyclerView.Adapter() { private var items: List = emptyList() @@ -53,7 +56,16 @@ class CreatorChannelHomeSectionAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder { val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - return SectionViewHolder(view, onLiveClick, onScheduleClick, onAudioContentClick, onSeriesClick, onDonationClick) + return SectionViewHolder( + view, + onLiveClick, + onScheduleClick, + onAudioContentClick, + onSeriesClick, + onDonationClick, + onSectionChevronClick, + onCommunityClick + ) } override fun onBindViewHolder(holder: SectionViewHolder, position: Int) { @@ -68,9 +80,12 @@ class CreatorChannelHomeSectionAdapter( private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit, private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit, private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit, - private val onDonationClick: () -> Unit + private val onDonationClick: () -> Unit, + private val onSectionChevronClick: (CreatorChannelTab) -> Unit, + private val onCommunityClick: (Long) -> Unit ) : RecyclerView.ViewHolder(view) { private val title: TextView? = view.findViewById(R.id.tv_section_title) + private val sectionTitleChevron: ImageView? = view.findViewById(R.id.iv_section_title_chevron) private val currentLiveTitle: TextView? = view.findViewById(R.id.tv_current_live_title) private val currentLiveStartTime: TextView? = view.findViewById(R.id.tv_current_live_start_time) private val currentLivePrice: TextView? = view.findViewById(R.id.tv_current_live_price) @@ -93,6 +108,7 @@ class CreatorChannelHomeSectionAdapter( private val audioContentsRecyclerView: RecyclerView? = view.findViewById(R.id.rv_audio_contents) private val seriesItems: LinearLayout? = view.findViewById(R.id.ll_series_items) private val communityItems: LinearLayout? = view.findViewById(R.id.ll_community_items) + private val communityMoreButton: View? = view.findViewById(R.id.layout_community_more_button) private val fanTalkCard: View? = view.findViewById(R.id.layout_fantalk_card) private val fanTalkTotalRow: View? = view.findViewById(R.id.layout_fantalk_total_row) private val fanTalkLatestRow: View? = view.findViewById(R.id.layout_fantalk_latest_row) @@ -120,6 +136,11 @@ class CreatorChannelHomeSectionAdapter( fun bind(item: CreatorChannelHomeSection) { itemView.setOnClickListener(null) title?.setText(item.titleResId) + sectionTitleChevron?.isVisible = item.chevronTab != null + sectionTitleChevron?.setOnClickListener { + item.chevronTab?.let(onSectionChevronClick) + } + communityMoreButton?.setOnClickListener(null) donationItems?.removeAllViews() noticeItems?.removeAllViews() scheduleTimeline?.removeAllViews() @@ -351,6 +372,7 @@ class CreatorChannelHomeSectionAdapter( } private fun bindCommunities(item: CreatorChannelHomeSection.Communities) { + communityMoreButton?.setOnClickListener { onSectionChevronClick(CreatorChannelTab.Community) } val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT) visibleCommunities.forEachIndexed { index, community -> val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp( @@ -377,6 +399,7 @@ class CreatorChannelHomeSectionAdapter( ).apply { bottomMargin = if (index == visibleCommunities.lastIndex) 0 else 8.dp() } + row.setOnClickListener { onCommunityClick(community.postId) } communityItems?.addView(row) } } @@ -590,6 +613,15 @@ class CreatorChannelHomeSectionAdapter( is CreatorChannelHomeSection.Sns -> R.string.creator_channel_section_sns } + val CreatorChannelHomeSection.chevronTab: CreatorChannelTab? + get() = when (this) { + is CreatorChannelHomeSection.Donations -> CreatorChannelTab.Donation + is CreatorChannelHomeSection.AudioContents -> CreatorChannelTab.Audio + is CreatorChannelHomeSection.Series -> CreatorChannelTab.Series + is CreatorChannelHomeSection.FanTalk -> CreatorChannelTab.FanTalk + else -> null + } + private const val MAX_DONATION_ITEM_COUNT = 8 private const val MAX_NOTICE_ITEM_COUNT = 3 private const val MAX_SCHEDULE_ITEM_COUNT = 3 diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt index 27c0e3e3..bff96a56 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt @@ -980,6 +980,68 @@ class CreatorChannelActivitySourceTest { assertTrue(activity.contains("dialog.show(screenWidth - 26.7f.dpToPx().toInt())")) } + @Test + fun `홈 섹션 chevron과 커뮤니티 전체보기는 대응 탭으로 이동한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + val fragment = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt" + ).readText() + val activity = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" + ).readText() + + assertTrue(adapter.contains("private val sectionTitleChevron: ImageView?")) + assertTrue(adapter.contains("private val communityMoreButton: View?")) + assertTrue(adapter.contains("sectionTitleChevron?.isVisible = item.chevronTab != null")) + assertTrue(adapter.contains("sectionTitleChevron?.setOnClickListener")) + assertTrue(adapter.contains("item.chevronTab?.let(onSectionChevronClick)")) + assertTrue( + adapter.contains( + "communityMoreButton?.setOnClickListener { onSectionChevronClick(CreatorChannelTab.Community) }" + ) + ) + assertTrue(adapter.contains("is CreatorChannelHomeSection.Donations -> CreatorChannelTab.Donation")) + assertTrue(adapter.contains("is CreatorChannelHomeSection.AudioContents -> CreatorChannelTab.Audio")) + assertTrue(adapter.contains("is CreatorChannelHomeSection.Series -> CreatorChannelTab.Series")) + assertTrue(adapter.contains("is CreatorChannelHomeSection.FanTalk -> CreatorChannelTab.FanTalk")) + assertTrue(fragment.contains("onSectionChevronClick = ::onSectionChevronClicked")) + assertTrue(fragment.contains("host.onCreatorChannelHomeTabRequested(tab)")) + assertTrue(activity.contains("override fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab)")) + assertTrue(activity.contains("private fun selectCreatorChannelTab(tab: CreatorChannelTab)")) + assertTrue(activity.contains("binding.viewPager.currentItem = tab.ordinal")) + assertTrue(activity.contains("private fun scrollCreatorChannelToTop()")) + assertTrue(activity.contains("binding.nestedScrollView.scrollTo(0, 0)")) + assertTrue( + activity.contains( + "override fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab) {\n" + + " selectCreatorChannelTab(tab)\n" + + " scrollCreatorChannelToTop()" + ) + ) + } + + @Test + fun `홈 커뮤니티 아이템 클릭은 커뮤니티 상세 이동 host 계약을 사용한다`() { + val adapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt" + ).readText() + val fragment = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt" + ).readText() + val activity = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" + ).readText() + + assertTrue(adapter.contains("private val onCommunityClick: (Long) -> Unit = {}")) + assertTrue(adapter.contains("row.setOnClickListener { onCommunityClick(community.postId) }")) + assertTrue(fragment.contains("onCommunityClick = ::onCommunityClicked")) + assertTrue(fragment.contains("host.onCreatorChannelCommunityPostClicked(postId)")) + assertTrue(activity.contains("override fun onCreatorChannelCommunityPostClicked(postId: Long)")) + assertTrue(activity.contains("CreatorChannelCommunityDetailActivity.newIntent(this, postId)")) + } + @Test fun `후원 can 수량은 요청된 배경색 resource 경계로 매핑한다`() { assertEquals(R.color.gray_200, calculateCreatorChannelDonationHeaderColorRes(1))