feat(content): 랭킹 탭 화면 연결을 추가한다
This commit is contained in:
@@ -14,6 +14,8 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingType
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRankingsUiState
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRecommendationsUiState
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardSection
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardUiModel
|
||||
@@ -34,12 +36,15 @@ import kr.co.vividnext.sodalive.v2.main.content.ui.ContentOriginalSeriesAdapter
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.addContentGridItemSpacing
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.addContentHorizontalItemSpacing
|
||||
import kr.co.vividnext.sodalive.v2.widget.AudioContentCardSize
|
||||
import kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingAdapter
|
||||
import kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItem
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
FragmentV2MainContentBinding::inflate
|
||||
) {
|
||||
private val contentMainViewModel: ContentMainViewModel by viewModel()
|
||||
private val contentRankingViewModel: ContentRankingViewModel by viewModel()
|
||||
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
|
||||
private val originalSeriesAdapter = ContentOriginalSeriesAdapter { openSeriesDetail(it) }
|
||||
private val latestAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(it) }
|
||||
@@ -48,20 +53,77 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
private val pointAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(it) }
|
||||
private val commentedAudioAdapter = ContentCommentedAudioAdapter { openAudioContentDetail(it) }
|
||||
private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) }
|
||||
private val contentRankingAdapter = ContentRankingAdapter { openRankingAudioContentDetail(it) }
|
||||
private var bannerBinder: ContentBannerBinder? = null
|
||||
private var isRecommendationLoading = false
|
||||
private var isRankingLoading = false
|
||||
private var hasSelectedRankingTab = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.textTabBarContent.root.setMenus(
|
||||
listOf(getString(R.string.screen_content_tab_recommendation)),
|
||||
selectedIndex = 0
|
||||
)
|
||||
setUpTextTabs()
|
||||
setUpRankingTypeTabs()
|
||||
showContentTab(CONTENT_TAB_RECOMMENDATION)
|
||||
setUpSectionTitles()
|
||||
setUpAdapters()
|
||||
bindObservers()
|
||||
contentMainViewModel.loadRecommendations()
|
||||
}
|
||||
|
||||
private fun setUpTextTabs() {
|
||||
binding.textTabBarContent.root.setMenus(
|
||||
listOf(
|
||||
getString(R.string.screen_content_tab_recommendation),
|
||||
getString(R.string.screen_content_tab_ranking),
|
||||
getString(R.string.screen_content_tab_all)
|
||||
),
|
||||
selectedIndex = 0
|
||||
)
|
||||
binding.textTabBarContent.root.setOnTabSelectedListener { index ->
|
||||
showContentTab(index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpRankingTypeTabs() {
|
||||
binding.viewContentRankingTypeTabs.root.setMenus(
|
||||
AudioRankingType.entries.map { type -> getString(type.labelResId()) },
|
||||
selectedIndex = AudioRankingType.WEEKLY_POPULAR.ordinal
|
||||
)
|
||||
binding.viewContentRankingTypeTabs.root.setOnTabSelectedListener { index ->
|
||||
contentRankingViewModel.loadRankings(AudioRankingType.entries[index])
|
||||
}
|
||||
}
|
||||
|
||||
private fun showContentTab(index: Int) {
|
||||
when (index) {
|
||||
CONTENT_TAB_RECOMMENDATION -> showRecommendationContent()
|
||||
CONTENT_TAB_RANKING -> showRankingContent()
|
||||
CONTENT_TAB_ALL -> hideContentSurfaces()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRecommendationContent() {
|
||||
binding.nsvContentRecommendationContent.visibility = View.VISIBLE
|
||||
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
||||
binding.rvContentRankings.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showRankingContent() {
|
||||
binding.nsvContentRecommendationContent.visibility = View.GONE
|
||||
binding.viewContentRankingTypeTabs.root.visibility = View.VISIBLE
|
||||
binding.rvContentRankings.visibility = View.VISIBLE
|
||||
if (!hasSelectedRankingTab) {
|
||||
hasSelectedRankingTab = true
|
||||
contentRankingViewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideContentSurfaces() {
|
||||
binding.nsvContentRecommendationContent.visibility = View.GONE
|
||||
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
||||
binding.rvContentRankings.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun setUpAdapters() {
|
||||
bannerBinder = ContentBannerBinder(binding.rvContentBanners).apply {
|
||||
setOnBannerClick { onBannerClick(it) }
|
||||
@@ -101,6 +163,10 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
adapter = recommendedAudioAdapter
|
||||
addContentGridItemSpacing()
|
||||
}
|
||||
binding.rvContentRankings.apply {
|
||||
layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())
|
||||
adapter = contentRankingAdapter
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindObservers() {
|
||||
@@ -114,15 +180,28 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
}
|
||||
}
|
||||
contentMainViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
if (isLoading) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
isRecommendationLoading = isLoading
|
||||
updateLoadingDialog()
|
||||
}
|
||||
contentMainViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||
toastMessage?.let(::showToast)
|
||||
}
|
||||
contentRankingViewModel.rankingStateLiveData.observe(viewLifecycleOwner) { state ->
|
||||
when (state) {
|
||||
is AudioRankingsUiState.Content -> contentRankingAdapter.submitItems(state.items)
|
||||
is AudioRankingsUiState.Empty,
|
||||
is AudioRankingsUiState.Error -> contentRankingAdapter.submitItems(emptyList())
|
||||
|
||||
AudioRankingsUiState.Loading -> Unit
|
||||
}
|
||||
}
|
||||
contentRankingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
isRankingLoading = isLoading
|
||||
updateLoadingDialog()
|
||||
}
|
||||
contentRankingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||
toastMessage?.let(::showToast)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindContent(content: AudioRecommendationsUiState.Content) {
|
||||
@@ -213,6 +292,11 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
)
|
||||
}
|
||||
|
||||
private fun openRankingAudioContentDetail(item: ContentRankingItem) {
|
||||
val audioContentId = item.contentId.toLongOrNull() ?: return
|
||||
openAudioContentDetail(audioContentId)
|
||||
}
|
||||
|
||||
private fun openSeriesDetail(item: ContentOriginalSeriesUiModel) {
|
||||
val seriesId = item.seriesId.takeIf { it > 0L } ?: return
|
||||
startActivity(
|
||||
@@ -227,6 +311,14 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
?: toastMessage.resId?.let { resId -> showToast(getString(resId)) }
|
||||
}
|
||||
|
||||
private fun updateLoadingDialog() {
|
||||
if (isRecommendationLoading || isRankingLoading) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun emptyContent(): AudioRecommendationsUiState.Content {
|
||||
return AudioRecommendationsUiState.Content(
|
||||
banners = ContentBannerSection(emptyList()),
|
||||
@@ -241,4 +333,19 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
}
|
||||
|
||||
private fun List<*>.toSectionVisibility(): Int = if (isEmpty()) View.GONE else View.VISIBLE
|
||||
|
||||
private fun AudioRankingType.labelResId(): Int = when (this) {
|
||||
AudioRankingType.WEEKLY_POPULAR -> R.string.screen_content_ranking_type_weekly_popular
|
||||
AudioRankingType.RISING -> R.string.screen_content_ranking_type_rising
|
||||
AudioRankingType.REVENUE -> R.string.screen_content_ranking_type_revenue
|
||||
AudioRankingType.SALES_COUNT -> R.string.screen_content_ranking_type_sales_count
|
||||
AudioRankingType.COMMENT_COUNT -> R.string.screen_content_ranking_type_comment_count
|
||||
AudioRankingType.LIKE_COUNT -> R.string.screen_content_ranking_type_like_count
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_TAB_RECOMMENDATION = 0
|
||||
private const val CONTENT_TAB_RANKING = 1
|
||||
private const val CONTENT_TAB_ALL = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,18 +33,44 @@ class ContentMainFragmentSourceTest {
|
||||
).readText()
|
||||
|
||||
assertTrue(source.contains("private val contentMainViewModel: ContentMainViewModel by viewModel()"))
|
||||
assertTrue(source.contains("private val contentRankingViewModel: ContentRankingViewModel by viewModel()"))
|
||||
assertTrue(source.contains("ContentBannerBinder(binding.rvContentBanners)"))
|
||||
assertTrue(source.contains("ContentOriginalSeriesAdapter"))
|
||||
assertTrue(source.contains("ContentAudioCardAdapter"))
|
||||
assertTrue(source.contains("ContentNewAndHotAdapter"))
|
||||
assertTrue(source.contains("ContentCommentedAudioAdapter"))
|
||||
assertTrue(source.contains("ContentRankingAdapter"))
|
||||
assertTrue(source.contains("ContentRankingAdapter.createGridLayoutManager(requireContext())"))
|
||||
assertTrue(source.contains("recommendationsStateLiveData.observe(viewLifecycleOwner)"))
|
||||
assertTrue(source.contains("rankingStateLiveData.observe(viewLifecycleOwner)"))
|
||||
assertTrue(source.contains("contentMainViewModel.loadRecommendations()"))
|
||||
assertTrue(source.contains("contentRankingViewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR)"))
|
||||
assertTrue(source.contains("LoadingDialog(requireActivity(), layoutInflater)"))
|
||||
assertTrue(source.contains("loadingDialog.show(screenWidth)"))
|
||||
assertTrue(source.contains("toastMessage?.let(::showToast)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content 랭킹 layout과 tab source는 Phase 5 요구를 포함한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||
).readText()
|
||||
val fragmentLayout = projectFile("app/src/main/res/layout/fragment_v2_main_content.xml").readText()
|
||||
|
||||
assertTrue(fragmentLayout.contains("@+id/view_content_ranking_type_tabs"))
|
||||
assertTrue(fragmentLayout.contains("layout=\"@layout/view_capsule_tab_bar\""))
|
||||
assertTrue(fragmentLayout.contains("@+id/rv_content_rankings"))
|
||||
assertTrue(source.contains("R.string.screen_content_tab_recommendation"))
|
||||
assertTrue(source.contains("R.string.screen_content_tab_ranking"))
|
||||
assertTrue(source.contains("R.string.screen_content_tab_all"))
|
||||
assertTrue(source.contains("binding.textTabBarContent.root.setOnTabSelectedListener"))
|
||||
assertTrue(source.contains("binding.viewContentRankingTypeTabs.root.setMenus"))
|
||||
assertTrue(source.contains("binding.viewContentRankingTypeTabs.root.setOnTabSelectedListener"))
|
||||
assertTrue(source.contains("AudioRankingType.entries[index]"))
|
||||
assertTrue(source.contains("screen_content_ranking_type_weekly_popular"))
|
||||
assertTrue(source.contains("screen_content_ranking_type_like_count"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content 추천 source는 오디오와 시리즈 routing extra를 사용한다`() {
|
||||
val source = projectFile(
|
||||
|
||||
Reference in New Issue
Block a user