feat(home): 랭킹 탭 목록을 연결한다
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user