feat(content): 전체 탭 화면 연결을 추가한다

This commit is contained in:
2026-06-25 11:54:43 +09:00
parent 4df724ee56
commit f4a32086b0
2 changed files with 396 additions and 2 deletions

View File

@@ -71,6 +71,154 @@ class ContentMainFragmentSourceTest {
assertTrue(source.contains("screen_content_ranking_type_like_count"))
}
@Test
fun `content 전체 layout과 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()
val dayFilterBackground = projectFile("app/src/main/res/drawable/bg_content_all_day_filter.xml").readText()
val selectedDayBackground = projectFile("app/src/main/res/drawable/bg_content_all_day_selected.xml").readText()
assertTrue(fragmentLayout.contains("@+id/layout_content_all_surface"))
assertTrue(fragmentLayout.contains("@+id/view_content_all_type_tabs"))
assertTrue(fragmentLayout.contains("@+id/layout_content_all_day_filter"))
assertTrue(fragmentLayout.contains("android:layout_width=\"wrap_content\""))
assertTrue(fragmentLayout.contains("android:layout_height=\"36dp\""))
assertTrue(fragmentLayout.contains("android:layout_marginTop=\"@dimen/spacing_8\""))
assertTrue(fragmentLayout.contains("android:background=\"@drawable/bg_content_all_day_filter\""))
assertTrue(fragmentLayout.contains("android:padding=\"@dimen/spacing_4\""))
assertTrue(fragmentLayout.contains("app:layout_constraintEnd_toEndOf=\"parent\""))
assertTrue(fragmentLayout.contains("app:layout_constraintHorizontal_bias=\"0.5\""))
assertTrue(dayFilterBackground.contains("@color/gray_900"))
assertTrue(dayFilterBackground.contains("@dimen/radius_8"))
assertTrue(selectedDayBackground.contains("@color/white"))
assertTrue(selectedDayBackground.contains("@dimen/radius_8"))
assertTrue(fragmentLayout.contains("@+id/layout_content_all_sort_bar"))
assertTrue(fragmentLayout.contains("android:layout_height=\"52dp\""))
assertTrue(fragmentLayout.contains("@+id/tv_content_all_total_label"))
assertTrue(fragmentLayout.contains("android:text=\"@string/screen_content_tab_all\""))
assertTrue(fragmentLayout.contains("android:textColor=\"@color/white\""))
assertTrue(fragmentLayout.contains("@+id/tv_content_all_total_count"))
assertTrue(fragmentLayout.contains("style=\"@style/Typography.Body2\""))
assertTrue(fragmentLayout.contains("android:textColor=\"@color/gray_500\""))
assertTrue(fragmentLayout.contains("@+id/layout_content_all_sort_button"))
assertTrue(fragmentLayout.contains("android:layout_height=\"match_parent\""))
assertTrue(fragmentLayout.contains("@+id/tv_content_all_sort_label"))
assertTrue(fragmentLayout.contains("style=\"@style/Typography.Body3\""))
assertTrue(fragmentLayout.contains("@+id/iv_content_all_sort"))
assertTrue(fragmentLayout.contains("android:src=\"@drawable/ic_new_sort\""))
assertTrue(fragmentLayout.contains("@+id/rv_content_all_items"))
assertTrue(fragmentLayout.contains("@+id/layout_content_all_empty_error"))
assertTrue(fragmentLayout.contains("@+id/tv_content_all_empty_error"))
assertTrue(source.contains("private val contentAllTabViewModel: ContentAllTabViewModel by viewModel()"))
assertTrue(source.contains("ContentAllAudioCardAdapter"))
assertTrue(source.contains("ContentAllSeriesCardAdapter"))
assertTrue(source.contains("CONTENT_ALL_GRID_SPAN_COUNT"))
assertTrue(source.contains("GridLayoutManager(requireContext(), CONTENT_ALL_GRID_SPAN_COUNT)"))
assertTrue(source.contains("addContentGridItemSpacing(CONTENT_ALL_GRID_SPAN_COUNT)"))
assertTrue(source.contains("MainContentAllType.AUDIO"))
assertTrue(source.contains("MainContentAllType.SERIES"))
assertTrue(source.contains("MainContentAllType.ORIGINAL"))
assertTrue(source.contains("MainContentAllType.FREE"))
assertTrue(source.contains("MainContentAllType.POINT"))
assertFalse(source.contains("screen_content_all_type_all"))
assertFalse(source.contains("screen_content_all_type_serialized"))
assertTrue(source.contains("CreatorChannelSortPopup"))
assertTrue(source.contains("ContentSort.entries"))
assertTrue(source.contains("toLabelResId()"))
assertTrue(source.contains("contentAllDayOfWeekOptions"))
assertTrue(source.contains("toContentAllDayLabelResId()"))
assertTrue(source.contains("R.drawable.bg_content_all_day_selected"))
assertTrue(source.contains("if (isSelected) R.color.black else R.color.white"))
assertTrue(source.contains("loadMore()"))
assertTrue(source.contains("consumePaginationErrorMessage()"))
assertTrue(source.contains("openAudioContentDetail(audioContentId)"))
assertTrue(source.contains("openSeriesDetail(seriesId)"))
assertTrue(source.contains("override fun onDestroyView()"))
assertTrue(source.contains("sortPopup?.dismiss()"))
assertTrue(source.contains("sortPopup = null"))
assertTrue(source.contains("bannerBinder = null"))
assertTrue(source.contains("layoutContentAllEmptyError"))
assertTrue(source.contains("tvContentAllEmptyError.setText"))
}
@Test
fun `content 전체 탭 source는 최신 전체 상태를 보관한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
assertSourceContains(
source,
"private var currentAllTabState: MainContentAllTabUiState? = null",
"전체 탭은 Content만 보관하지 않고 Empty Error Loading까지 포함하는 최신 상태를 캐시해야 한다."
)
assertFalse(
"전체 탭 캐시는 MainContentAllTabUiState.Content 전용 currentAllTabContent에 머물면 안 된다.",
source.contains("currentAllTabContent: MainContentAllTabUiState.Content?")
)
}
@Test
fun `content 전체 탭 observer는 guard 전에 최신 상태를 저장한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
assertSourceContains(source, "contentAllTabViewModel.allTabStateLiveData.observe(viewLifecycleOwner) { state ->")
assertSourceContains(source, "currentAllTabState = state")
assertSourceContains(source, "renderAllTabState(state)")
assertTrue(
"allTabStateLiveData observer는 render/guard 전에 currentAllTabState = state를 먼저 실행해야 한다.",
source.indexOf("currentAllTabState = state") < source.indexOf("renderAllTabState(state)")
)
assertFalse(
"inactive ALL 탭 emission을 버리는 observer/render guard는 제거되어야 한다.",
source.contains("if (currentContentTab != CONTENT_TAB_ALL) return")
)
}
@Test
fun `content 전체 탭 진입은 캐시 상태를 우선 렌더링하고 필요할 때만 초기 로드한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
assertSourceContains(source, "private fun showAllContent()")
assertSourceContains(source, "currentAllTabState?.let(::renderAllTabState)")
assertSourceContains(source, "if (currentAllTabState == null && !hasSelectedAllTab)")
assertSourceContains(source, "contentAllTabViewModel.loadInitial()")
}
@Test
fun `content 전체 탭 source는 상태별 dispatcher를 둔다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
assertSourceContains(source, "private fun renderAllTabState(state: MainContentAllTabUiState)")
assertSourceContains(source, "is MainContentAllTabUiState.Content ->")
assertSourceContains(source, "is MainContentAllTabUiState.Empty ->")
assertSourceContains(source, "is MainContentAllTabUiState.Error ->")
assertSourceContains(source, "is MainContentAllTabUiState.Loading ->")
}
@Test
fun `content 전체 탭 source는 비 content 상태에도 공통 control을 바인딩한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
assertSourceContains(source, "private fun bindAllTabControls(state: MainContentAllTabUiState)")
assertSourceContains(source, "bindAllTabControls(state)")
assertSourceContains(source, "binding.viewContentAllTypeTabs.root.setMenus")
assertSourceContains(source, "binding.layoutContentAllDayFilter.visibility")
assertSourceContains(source, "binding.tvContentAllSortLabel.setText")
assertSourceContains(source, "binding.tvContentAllTotalCount.text")
}
@Test
fun `content 추천 source는 오디오와 시리즈 routing extra를 사용한다`() {
val source = projectFile(
@@ -207,6 +355,10 @@ class ContentMainFragmentSourceTest {
link = link
)
private fun assertSourceContains(source: String, expected: String, message: String? = null) {
assertTrue(message ?: "Expected source to contain: $expected", source.contains(expected))
}
private fun projectFile(relativePath: String): File {
val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() }