diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt index e5f393f9..562aa6ca 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt @@ -26,6 +26,7 @@ import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek import kr.co.vividnext.sodalive.v2.common.data.ContentSort import kr.co.vividnext.sodalive.v2.creator.channel.model.toLabelResId import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelSortPopup +import kr.co.vividnext.sodalive.v2.main.ensureMainV2NavigationAllowed import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingType import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType import kr.co.vividnext.sodalive.v2.main.content.model.AudioRankingsUiState @@ -86,9 +87,11 @@ class ContentMainFragment : BaseFragment( private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) } private val contentRankingAdapter = ContentRankingAdapter { openRankingAudioContentDetail(it) } private val contentAllAudioCardAdapter = ContentAllAudioCardAdapter { audioContentId -> - openAudioContentDetail(audioContentId) + openAudioContentDetail(audioContentId, findAllTabAudioAdultAccess(audioContentId)) + } + private val contentAllSeriesCardAdapter = ContentAllSeriesCardAdapter { seriesId -> + openSeriesDetail(seriesId, findAllTabSeriesAdultAccess(seriesId)) } - private val contentAllSeriesCardAdapter = ContentAllSeriesCardAdapter { seriesId -> openSeriesDetail(seriesId) } private var bannerBinder: ContentBannerBinder? = null private var sortPopup: CreatorChannelSortPopup? = null private var isRecommendationLoading = false @@ -520,24 +523,31 @@ class ContentMainFragment : BaseFragment( private fun onBannerClick(item: ContentBannerUiModel) { val route = item.toContentBannerRoute() ?: return - startActivity(route.toContentBannerIntent(requireContext())) + ensureMainV2NavigationAllowed { + startActivity(route.toContentBannerIntent(requireContext())) + } } private fun openAudioContentDetail(item: ContentAudioCardUiModel) { - openAudioContentDetail(item.audioContentId) + openAudioContentDetail(item.audioContentId, item.showAdultBadge) } private fun openAudioContentDetail(item: ContentCommentedAudioUiModel) { openAudioContentDetail(item.audioContentId) } - private fun openAudioContentDetail(audioContentId: Long) { + private fun openAudioContentDetail( + audioContentId: Long, + requiresAdultContentAccess: Boolean = false + ) { if (audioContentId <= 0L) return - startActivity( - Intent(requireContext(), AudioContentDetailActivity::class.java).apply { - putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId) - } - ) + ensureMainV2NavigationAllowed(requiresAdultContentAccess = requiresAdultContentAccess) { + startActivity( + Intent(requireContext(), AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId) + } + ) + } } private fun openRankingAudioContentDetail(item: ContentRankingItem) { @@ -550,13 +560,28 @@ class ContentMainFragment : BaseFragment( openSeriesDetail(seriesId) } - private fun openSeriesDetail(seriesId: Long) { + private fun openSeriesDetail( + seriesId: Long, + requiresAdultContentAccess: Boolean = false + ) { if (seriesId <= 0L) return - startActivity( - Intent(requireContext(), SeriesDetailActivity::class.java).apply { - putExtra(Constants.EXTRA_SERIES_ID, seriesId) - } - ) + ensureMainV2NavigationAllowed(requiresAdultContentAccess = requiresAdultContentAccess) { + startActivity( + Intent(requireContext(), SeriesDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_SERIES_ID, seriesId) + } + ) + } + } + + private fun findAllTabAudioAdultAccess(audioContentId: Long): Boolean { + val state = currentAllTabState as? MainContentAllTabUiState.Content ?: return false + return state.audioItems.firstOrNull { it.audioContentId == audioContentId }?.showAdultBadge == true + } + + private fun findAllTabSeriesAdultAccess(seriesId: Long): Boolean { + val state = currentAllTabState as? MainContentAllTabUiState.Content ?: return false + return state.seriesItems.firstOrNull { it.seriesId == seriesId }?.showAdultBadge == true } private fun showToast(toastMessage: ToastMessage) { diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentLoginGuardSourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentLoginGuardSourceTest.kt new file mode 100644 index 00000000..71bcde7c --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentLoginGuardSourceTest.kt @@ -0,0 +1,76 @@ +package kr.co.vividnext.sodalive.v2.main.content + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File + +class ContentMainFragmentLoginGuardSourceTest { + + @Test + fun `ContentMainFragment 실제 이동은 MainV2 로그인 가드를 통과한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt" + ).readText() + + assertTrue(source.contains("import kr.co.vividnext.sodalive.v2.main.ensureMainV2NavigationAllowed")) + assertGuardedStartActivity(source, "private fun onBannerClick(item: ContentBannerUiModel)") + assertGuardedStartActivity(source, "audioContentId: Long,\n requiresAdultContentAccess: Boolean = false") + assertGuardedStartActivity(source, "seriesId: Long,\n requiresAdultContentAccess: Boolean = false") + assertFalse(source.contains("kr.co.vividnext.sodalive.main.MainActivity")) + } + + @Test + fun `ContentMainFragment invalid id return은 로그인 가드보다 먼저 유지된다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt" + ).readText() + + assertBeforeGuard(source, "val route = item.toContentBannerRoute() ?: return") + assertBeforeGuard(source, "if (audioContentId <= 0L) return") + assertBeforeGuard(source, "if (seriesId <= 0L) return") + } + + @Test + fun `ContentMainFragment는 판단 가능한 성인 콘텐츠 여부를 guard에 전달한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt" + ).readText() + + assertTrue(source.contains("openAudioContentDetail(item.audioContentId, item.showAdultBadge)")) + assertTrue(source.contains("openAudioContentDetail(audioContentId, findAllTabAudioAdultAccess(audioContentId))")) + assertTrue(source.contains("openSeriesDetail(seriesId, findAllTabSeriesAdultAccess(seriesId))")) + assertTrue(source.contains("requiresAdultContentAccess = requiresAdultContentAccess")) + assertFalse(source.contains("ContentCommentedAudioUiModel.showAdultBadge")) + assertFalse(source.contains("ContentOriginalSeriesUiModel.showAdultBadge")) + } + + private fun assertGuardedStartActivity(source: String, functionSignature: String) { + val functionSource = source.substringFrom(functionSignature) + assertTrue( + "$functionSignature must call ensureMainV2NavigationAllowed before startActivity.", + functionSource.indexOf("ensureMainV2NavigationAllowed") in 0 until functionSource.indexOf("startActivity") + ) + } + + private fun assertBeforeGuard(source: String, expectedReturn: String) { + val returnIndex = source.indexOf(expectedReturn) + val guardIndex = source.indexOf("ensureMainV2NavigationAllowed", returnIndex) + + assertTrue("Missing source: $expectedReturn", returnIndex >= 0) + assertTrue("$expectedReturn must stay before guard.", guardIndex > returnIndex) + } + + private fun String.substringFrom(marker: String): String { + val startIndex = indexOf(marker) + assertTrue("Missing function: $marker", startIndex >= 0) + val nextFunctionIndex = indexOf("\n private fun ", startIndex + marker.length).takeIf { it >= 0 } ?: length + return substring(startIndex, nextFunctionIndex) + } + + private fun projectFile(relativePath: String): File { + val candidates = listOf(File(relativePath), File("../$relativePath")) + return candidates.firstOrNull { it.exists() } + ?: error("Project file not found: $relativePath") + } +}