feat(home): 랭킹 탭 목록을 연결한다

This commit is contained in:
2026-06-08 18:07:00 +09:00
parent bb60f8bb9f
commit 5d66014044
3 changed files with 178 additions and 1 deletions

View File

@@ -16,6 +16,7 @@ import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
import kr.co.vividnext.sodalive.v2.main.home.HomeCreatorRankingViewModel
import kr.co.vividnext.sodalive.v2.main.home.HomeRecommendationViewModel import kr.co.vividnext.sodalive.v2.main.home.HomeRecommendationViewModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationAiCharacterSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerSection
@@ -32,6 +33,7 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularComm
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentDebutCreatorSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentDebutCreatorSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeCreatorRankingUiState
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationUiState import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationUiState
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerIntent import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerIntent
import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute
@@ -48,6 +50,8 @@ import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomePopularCommunityAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomePopularCommunityAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentActivityCreatorAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentActivityCreatorAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentDebutCreatorAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentDebutCreatorAdapter
import kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingAdapter
import kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingItem
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -55,6 +59,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
FragmentV2MainHomeBinding::inflate FragmentV2MainHomeBinding::inflate
) { ) {
private val homeRecommendationViewModel: HomeRecommendationViewModel by viewModel() private val homeRecommendationViewModel: HomeRecommendationViewModel by viewModel()
private val homeCreatorRankingViewModel: HomeCreatorRankingViewModel by viewModel()
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) } private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
private val liveAdapter = HomeLiveAdapter() private val liveAdapter = HomeLiveAdapter()
private val recentActivityCreatorAdapter = HomeRecentActivityCreatorAdapter { onRecentActivityClick(it) } private val recentActivityCreatorAdapter = HomeRecentActivityCreatorAdapter { onRecentActivityClick(it) }
@@ -70,9 +75,11 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
onCreatorClick = { creator -> openCreatorProfile(creator.creatorId) } onCreatorClick = { creator -> openCreatorProfile(creator.creatorId) }
) )
private val popularCommunityAdapter = HomePopularCommunityAdapter { openPopularCommunityPost(it) } private val popularCommunityAdapter = HomePopularCommunityAdapter { openPopularCommunityPost(it) }
private val creatorRankingAdapter = CreatorRankingAdapter { openCreatorRankingProfile(it) }
private var bannerBinder: HomeBannerBinder? = null private var bannerBinder: HomeBannerBinder? = null
private var onGenreFollowAllClick: (List<Long>) -> Unit = {} private var onGenreFollowAllClick: (List<Long>) -> Unit = {}
private var onCheerFollowAllClick: (List<Long>) -> Unit = {} private var onCheerFollowAllClick: (List<Long>) -> Unit = {}
private var hasLoadedCreatorRankings = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -84,11 +91,15 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
), ),
selectedIndex = 0 selectedIndex = 0
) )
binding.textTabBarHome.root.setOnTabSelectedListener { } binding.textTabBarHome.root.setOnTabSelectedListener { index ->
showHomeTab(index)
}
setUpSectionTitles() setUpSectionTitles()
setUpRecommendationAdapters() setUpRecommendationAdapters()
setUpCreatorRankingAdapter()
setUpBusinessInfo() setUpBusinessInfo()
bindHomeRecommendationObservers() bindHomeRecommendationObservers()
bindHomeCreatorRankingObservers()
homeRecommendationViewModel.loadRecommendations() homeRecommendationViewModel.loadRecommendations()
} }
@@ -141,6 +152,52 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
} }
} }
private fun setUpCreatorRankingAdapter() {
binding.rvHomeCreatorRankings.apply {
layoutManager = CreatorRankingAdapter.createGridLayoutManager(requireContext())
adapter = creatorRankingAdapter
}
}
private fun bindHomeCreatorRankingObservers() {
homeCreatorRankingViewModel.rankingStateLiveData.observe(viewLifecycleOwner) { state ->
when (state) {
is HomeCreatorRankingUiState.Content -> creatorRankingAdapter.submitItems(state.items)
HomeCreatorRankingUiState.Empty,
is HomeCreatorRankingUiState.Error -> creatorRankingAdapter.submitItems(emptyList())
HomeCreatorRankingUiState.Loading -> Unit
}
}
homeCreatorRankingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
if (isLoading) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
homeCreatorRankingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
toastMessage?.let(::showToast)
}
}
private fun showHomeTab(index: Int) {
when (index) {
HOME_TAB_RECOMMENDATION -> {
binding.nsvHomeRecommendationContent.visibility = View.VISIBLE
binding.rvHomeCreatorRankings.visibility = View.GONE
}
HOME_TAB_RANKING -> {
binding.nsvHomeRecommendationContent.visibility = View.GONE
binding.rvHomeCreatorRankings.visibility = View.VISIBLE
if (!hasLoadedCreatorRankings) {
hasLoadedCreatorRankings = true
homeCreatorRankingViewModel.loadCreatorRankings()
}
}
HOME_TAB_FOLLOWING -> Unit
}
}
private fun bindHomeRecommendationObservers() { private fun bindHomeRecommendationObservers() {
homeRecommendationViewModel.recommendationStateLiveData.observe(viewLifecycleOwner) { state -> homeRecommendationViewModel.recommendationStateLiveData.observe(viewLifecycleOwner) { state ->
when (state) { when (state) {
@@ -280,6 +337,11 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
startActivity(route.toHomeRecommendationRecentlyActiveCreatorIntent(requireContext())) startActivity(route.toHomeRecommendationRecentlyActiveCreatorIntent(requireContext()))
} }
private fun openCreatorRankingProfile(item: CreatorRankingItem) {
if (item.creatorId <= 0L) return
openCreatorProfile(item.creatorId)
}
private fun openCreatorProfile(creatorId: Long) { private fun openCreatorProfile(creatorId: Long) {
startActivity( startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply { Intent(requireContext(), UserProfileActivity::class.java).apply {
@@ -351,6 +413,9 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
private fun List<*>.toSectionVisibility(): Int = if (isEmpty()) View.GONE else View.VISIBLE private fun List<*>.toSectionVisibility(): Int = if (isEmpty()) View.GONE else View.VISIBLE
private companion object { private companion object {
const val HOME_TAB_RECOMMENDATION = 0
const val HOME_TAB_RANKING = 1
const val HOME_TAB_FOLLOWING = 2
const val SECTION_KEY_CHEER_CREATORS = "cheerCreators" const val SECTION_KEY_CHEER_CREATORS = "cheerCreators"
const val SECTION_KEY_GENRE_CREATORS = "genreCreators" const val SECTION_KEY_GENRE_CREATORS = "genreCreators"
} }

View File

@@ -238,4 +238,19 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_creator_rankings"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/spacing_14"
android:paddingTop="@dimen/spacing_14"
android:paddingBottom="@dimen/spacing_28"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_tab_bar_home"
tools:listitem="@layout/view_creator_ranking_horizontal_card" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -697,6 +697,38 @@ class HomeMainFragmentLayoutTest {
assertEquals(expectedFreeTag.paddingStart, freeTag.paddingStart) assertEquals(expectedFreeTag.paddingStart, freeTag.paddingStart)
} }
@Test
fun `home ranking layout contains ranking list below text tab bar`() {
val root = inflateView(R.layout.fragment_v2_main_home)
val rankingList = root.findViewById<RecyclerView>(R.id.rv_home_creator_rankings)
val layoutParams = rankingList.layoutParams as ConstraintLayout.LayoutParams
assertNotNull(rankingList)
assertSame(root, rankingList.parent)
assertEquals(0, layoutParams.width)
assertEquals(0, layoutParams.height)
assertEquals(R.id.text_tab_bar_home, layoutParams.topToBottom)
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, layoutParams.startToStart)
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, layoutParams.endToEnd)
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, layoutParams.bottomToBottom)
assertEquals(View.GONE, rankingList.visibility)
assertEquals(false, rankingList.clipToPadding)
assertEquals(14.dpToPx(), rankingList.paddingStart)
assertEquals(14.dpToPx(), rankingList.paddingTop)
assertEquals(28.dpToPx(), rankingList.paddingBottom)
}
@Test
fun `home ranking layout does not add capsule tab bar`() {
val root = inflateView(R.layout.fragment_v2_main_home)
val layoutSource = homeMainLayoutSource()
assertFalse(root.containsClassName("kr.co.vividnext.sodalive.v2.widget.CapsuleTabBarView"))
assertFalse(layoutSource.contains("view_capsule_tab_bar"))
assertFalse(layoutSource.contains("hsv_capsule_tab_bar"))
assertFalse(layoutSource.contains("ll_capsule_tab_container"))
}
@Test @Test
fun `popular community section is hidden until phase7 binding is implemented`() { fun `popular community section is hidden until phase7 binding is implemented`() {
val root = inflateView(R.layout.fragment_v2_main_home) val root = inflateView(R.layout.fragment_v2_main_home)
@@ -1264,6 +1296,63 @@ class HomeMainFragmentLayoutTest {
} }
} }
@Test
fun `home ranking fragment wires adapter and grid layout manager`() {
val source = homeMainFragmentSource()
assertTrue(source.contains("HomeCreatorRankingViewModel"))
assertTrue(source.contains("CreatorRankingAdapter { openCreatorRankingProfile(it) }"))
assertTrue(source.contains("binding.rvHomeCreatorRankings.apply"))
assertTrue(source.contains("layoutManager = CreatorRankingAdapter.createGridLayoutManager(requireContext())"))
assertTrue(source.contains("adapter = creatorRankingAdapter"))
}
@Test
fun `home ranking fragment switches recommendation and ranking content by tab`() {
val source = homeMainFragmentSource()
assertTrue(source.contains("HOME_TAB_RECOMMENDATION = 0"))
assertTrue(source.contains("HOME_TAB_RANKING = 1"))
assertTrue(source.contains("HOME_TAB_FOLLOWING = 2"))
assertTrue(source.contains("binding.textTabBarHome.root.setOnTabSelectedListener { index ->"))
assertTrue(source.contains("showHomeTab(index)"))
assertTrue(source.contains("binding.nsvHomeRecommendationContent.visibility = View.GONE"))
assertTrue(source.contains("binding.rvHomeCreatorRankings.visibility = View.VISIBLE"))
assertTrue(source.contains("binding.nsvHomeRecommendationContent.visibility = View.VISIBLE"))
assertTrue(source.contains("binding.rvHomeCreatorRankings.visibility = View.GONE"))
}
@Test
fun `home ranking fragment loads rankings once on first ranking selection`() {
val source = homeMainFragmentSource()
assertTrue(source.contains("private var hasLoadedCreatorRankings = false"))
assertTrue(source.contains("if (!hasLoadedCreatorRankings)"))
assertTrue(source.contains("hasLoadedCreatorRankings = true"))
assertTrue(source.contains("homeCreatorRankingViewModel.loadCreatorRankings()"))
}
@Test
fun `home ranking fragment observes ranking state and submits items`() {
val source = homeMainFragmentSource()
assertTrue(source.contains("rankingStateLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("is HomeCreatorRankingUiState.Content -> creatorRankingAdapter.submitItems(state.items)"))
assertTrue(source.contains("HomeCreatorRankingUiState.Empty,"))
assertTrue(source.contains("is HomeCreatorRankingUiState.Error -> creatorRankingAdapter.submitItems(emptyList())"))
assertTrue(source.contains("HomeCreatorRankingUiState.Loading -> Unit"))
}
@Test
fun `home ranking fragment opens profile only for touchable creator`() {
val source = homeMainFragmentSource()
assertTrue(source.contains("private fun openCreatorRankingProfile(item: CreatorRankingItem)"))
assertTrue(source.contains("if (item.creatorId <= 0L) return"))
assertTrue(source.contains("openCreatorProfile(item.creatorId)"))
assertFalse(source.contains("EXTRA_USER_ID, 0L"))
}
@Test @Test
fun `home main fragment phase9 replaces sample content with viewmodel state binding`() { fun `home main fragment phase9 replaces sample content with viewmodel state binding`() {
val source = homeMainFragmentSource() val source = homeMainFragmentSource()
@@ -1321,6 +1410,14 @@ class HomeMainFragmentLayoutTest {
).readText() ).readText()
} }
private fun homeMainLayoutSource(): String {
val projectRoot = java.io.File("..").canonicalFile
return java.io.File(
projectRoot,
"app/src/main/res/layout/fragment_v2_main_home.xml"
).readText()
}
private fun homeRecommendationViewModelSource(): String { private fun homeRecommendationViewModelSource(): String {
val projectRoot = java.io.File("..").canonicalFile val projectRoot = java.io.File("..").canonicalFile
return java.io.File( return java.io.File(