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.common.ToastMessage
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
||||||
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
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.AudioRecommendationsUiState
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardSection
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardUiModel
|
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.addContentGridItemSpacing
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.ui.addContentHorizontalItemSpacing
|
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.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
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||||
FragmentV2MainContentBinding::inflate
|
FragmentV2MainContentBinding::inflate
|
||||||
) {
|
) {
|
||||||
private val contentMainViewModel: ContentMainViewModel by viewModel()
|
private val contentMainViewModel: ContentMainViewModel by viewModel()
|
||||||
|
private val contentRankingViewModel: ContentRankingViewModel by viewModel()
|
||||||
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
|
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
|
||||||
private val originalSeriesAdapter = ContentOriginalSeriesAdapter { openSeriesDetail(it) }
|
private val originalSeriesAdapter = ContentOriginalSeriesAdapter { openSeriesDetail(it) }
|
||||||
private val latestAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(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 pointAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(it) }
|
||||||
private val commentedAudioAdapter = ContentCommentedAudioAdapter { openAudioContentDetail(it) }
|
private val commentedAudioAdapter = ContentCommentedAudioAdapter { openAudioContentDetail(it) }
|
||||||
private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) }
|
private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) }
|
||||||
|
private val contentRankingAdapter = ContentRankingAdapter { openRankingAudioContentDetail(it) }
|
||||||
private var bannerBinder: ContentBannerBinder? = null
|
private var bannerBinder: ContentBannerBinder? = null
|
||||||
|
private var isRecommendationLoading = false
|
||||||
|
private var isRankingLoading = false
|
||||||
|
private var hasSelectedRankingTab = false
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding.textTabBarContent.root.setMenus(
|
setUpTextTabs()
|
||||||
listOf(getString(R.string.screen_content_tab_recommendation)),
|
setUpRankingTypeTabs()
|
||||||
selectedIndex = 0
|
showContentTab(CONTENT_TAB_RECOMMENDATION)
|
||||||
)
|
|
||||||
setUpSectionTitles()
|
setUpSectionTitles()
|
||||||
setUpAdapters()
|
setUpAdapters()
|
||||||
bindObservers()
|
bindObservers()
|
||||||
contentMainViewModel.loadRecommendations()
|
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() {
|
private fun setUpAdapters() {
|
||||||
bannerBinder = ContentBannerBinder(binding.rvContentBanners).apply {
|
bannerBinder = ContentBannerBinder(binding.rvContentBanners).apply {
|
||||||
setOnBannerClick { onBannerClick(it) }
|
setOnBannerClick { onBannerClick(it) }
|
||||||
@@ -101,6 +163,10 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
adapter = recommendedAudioAdapter
|
adapter = recommendedAudioAdapter
|
||||||
addContentGridItemSpacing()
|
addContentGridItemSpacing()
|
||||||
}
|
}
|
||||||
|
binding.rvContentRankings.apply {
|
||||||
|
layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())
|
||||||
|
adapter = contentRankingAdapter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindObservers() {
|
private fun bindObservers() {
|
||||||
@@ -114,15 +180,28 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentMainViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
contentMainViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||||
if (isLoading) {
|
isRecommendationLoading = isLoading
|
||||||
loadingDialog.show(screenWidth)
|
updateLoadingDialog()
|
||||||
} else {
|
|
||||||
loadingDialog.dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
contentMainViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
contentMainViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||||
toastMessage?.let(::showToast)
|
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) {
|
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) {
|
private fun openSeriesDetail(item: ContentOriginalSeriesUiModel) {
|
||||||
val seriesId = item.seriesId.takeIf { it > 0L } ?: return
|
val seriesId = item.seriesId.takeIf { it > 0L } ?: return
|
||||||
startActivity(
|
startActivity(
|
||||||
@@ -227,6 +311,14 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
?: toastMessage.resId?.let { resId -> showToast(getString(resId)) }
|
?: toastMessage.resId?.let { resId -> showToast(getString(resId)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateLoadingDialog() {
|
||||||
|
if (isRecommendationLoading || isRankingLoading) {
|
||||||
|
loadingDialog.show(screenWidth)
|
||||||
|
} else {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun emptyContent(): AudioRecommendationsUiState.Content {
|
private fun emptyContent(): AudioRecommendationsUiState.Content {
|
||||||
return AudioRecommendationsUiState.Content(
|
return AudioRecommendationsUiState.Content(
|
||||||
banners = ContentBannerSection(emptyList()),
|
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 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()
|
).readText()
|
||||||
|
|
||||||
assertTrue(source.contains("private val contentMainViewModel: ContentMainViewModel by viewModel()"))
|
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("ContentBannerBinder(binding.rvContentBanners)"))
|
||||||
assertTrue(source.contains("ContentOriginalSeriesAdapter"))
|
assertTrue(source.contains("ContentOriginalSeriesAdapter"))
|
||||||
assertTrue(source.contains("ContentAudioCardAdapter"))
|
assertTrue(source.contains("ContentAudioCardAdapter"))
|
||||||
assertTrue(source.contains("ContentNewAndHotAdapter"))
|
assertTrue(source.contains("ContentNewAndHotAdapter"))
|
||||||
assertTrue(source.contains("ContentCommentedAudioAdapter"))
|
assertTrue(source.contains("ContentCommentedAudioAdapter"))
|
||||||
|
assertTrue(source.contains("ContentRankingAdapter"))
|
||||||
|
assertTrue(source.contains("ContentRankingAdapter.createGridLayoutManager(requireContext())"))
|
||||||
assertTrue(source.contains("recommendationsStateLiveData.observe(viewLifecycleOwner)"))
|
assertTrue(source.contains("recommendationsStateLiveData.observe(viewLifecycleOwner)"))
|
||||||
|
assertTrue(source.contains("rankingStateLiveData.observe(viewLifecycleOwner)"))
|
||||||
assertTrue(source.contains("contentMainViewModel.loadRecommendations()"))
|
assertTrue(source.contains("contentMainViewModel.loadRecommendations()"))
|
||||||
|
assertTrue(source.contains("contentRankingViewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR)"))
|
||||||
assertTrue(source.contains("LoadingDialog(requireActivity(), layoutInflater)"))
|
assertTrue(source.contains("LoadingDialog(requireActivity(), layoutInflater)"))
|
||||||
assertTrue(source.contains("loadingDialog.show(screenWidth)"))
|
assertTrue(source.contains("loadingDialog.show(screenWidth)"))
|
||||||
assertTrue(source.contains("toastMessage?.let(::showToast)"))
|
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
|
@Test
|
||||||
fun `content 추천 source는 오디오와 시리즈 routing extra를 사용한다`() {
|
fun `content 추천 source는 오디오와 시리즈 routing extra를 사용한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
|
|||||||
Reference in New Issue
Block a user