feat(content): 콘텐츠 이동 가드를 보강한다
This commit is contained in:
@@ -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.common.data.ContentSort
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.toLabelResId
|
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.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.AudioRankingType
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType
|
import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRankingsUiState
|
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRankingsUiState
|
||||||
@@ -86,9 +87,11 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) }
|
private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) }
|
||||||
private val contentRankingAdapter = ContentRankingAdapter { openRankingAudioContentDetail(it) }
|
private val contentRankingAdapter = ContentRankingAdapter { openRankingAudioContentDetail(it) }
|
||||||
private val contentAllAudioCardAdapter = ContentAllAudioCardAdapter { audioContentId ->
|
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 bannerBinder: ContentBannerBinder? = null
|
||||||
private var sortPopup: CreatorChannelSortPopup? = null
|
private var sortPopup: CreatorChannelSortPopup? = null
|
||||||
private var isRecommendationLoading = false
|
private var isRecommendationLoading = false
|
||||||
@@ -520,25 +523,32 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
|
|
||||||
private fun onBannerClick(item: ContentBannerUiModel) {
|
private fun onBannerClick(item: ContentBannerUiModel) {
|
||||||
val route = item.toContentBannerRoute() ?: return
|
val route = item.toContentBannerRoute() ?: return
|
||||||
|
ensureMainV2NavigationAllowed {
|
||||||
startActivity(route.toContentBannerIntent(requireContext()))
|
startActivity(route.toContentBannerIntent(requireContext()))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun openAudioContentDetail(item: ContentAudioCardUiModel) {
|
private fun openAudioContentDetail(item: ContentAudioCardUiModel) {
|
||||||
openAudioContentDetail(item.audioContentId)
|
openAudioContentDetail(item.audioContentId, item.showAdultBadge)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAudioContentDetail(item: ContentCommentedAudioUiModel) {
|
private fun openAudioContentDetail(item: ContentCommentedAudioUiModel) {
|
||||||
openAudioContentDetail(item.audioContentId)
|
openAudioContentDetail(item.audioContentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAudioContentDetail(audioContentId: Long) {
|
private fun openAudioContentDetail(
|
||||||
|
audioContentId: Long,
|
||||||
|
requiresAdultContentAccess: Boolean = false
|
||||||
|
) {
|
||||||
if (audioContentId <= 0L) return
|
if (audioContentId <= 0L) return
|
||||||
|
ensureMainV2NavigationAllowed(requiresAdultContentAccess = requiresAdultContentAccess) {
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
|
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun openRankingAudioContentDetail(item: ContentRankingItem) {
|
private fun openRankingAudioContentDetail(item: ContentRankingItem) {
|
||||||
val audioContentId = item.contentId.toLongOrNull()?.takeIf { it > 0L } ?: return
|
val audioContentId = item.contentId.toLongOrNull()?.takeIf { it > 0L } ?: return
|
||||||
@@ -550,14 +560,29 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
openSeriesDetail(seriesId)
|
openSeriesDetail(seriesId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openSeriesDetail(seriesId: Long) {
|
private fun openSeriesDetail(
|
||||||
|
seriesId: Long,
|
||||||
|
requiresAdultContentAccess: Boolean = false
|
||||||
|
) {
|
||||||
if (seriesId <= 0L) return
|
if (seriesId <= 0L) return
|
||||||
|
ensureMainV2NavigationAllowed(requiresAdultContentAccess = requiresAdultContentAccess) {
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
|
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
|
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) {
|
private fun showToast(toastMessage: ToastMessage) {
|
||||||
toastMessage.message?.let { message -> showToast(message) }
|
toastMessage.message?.let { message -> showToast(message) }
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user