refactor(creator): 크리에이터 접근 가드를 공통화한다

This commit is contained in:
2026-07-14 20:45:23 +09:00
parent e5b99be9c4
commit 6973816b8c
2 changed files with 120 additions and 121 deletions

View File

@@ -46,14 +46,9 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.LiveViewModel import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
import kr.co.vividnext.sodalive.mypage.auth.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
import kr.co.vividnext.sodalive.report.UserReportDialog import kr.co.vividnext.sodalive.report.UserReportDialog
import kr.co.vividnext.sodalive.settings.ContentSettingsActivity import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import kr.co.vividnext.sodalive.splash.SplashActivity import kr.co.vividnext.sodalive.v2.access.ensureV2Access
import kr.co.vividnext.sodalive.user.login.LoginActivity
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
@@ -93,7 +88,6 @@ class CreatorChannelActivity :
CreatorChannelDonationFragment.Host { CreatorChannelDonationFragment.Host {
private val liveViewModel: LiveViewModel by inject() private val liveViewModel: LiveViewModel by inject()
private val myPageViewModel: MyPageViewModel by inject()
private val creatorCommunityRepository: CreatorCommunityRepository by inject() private val creatorCommunityRepository: CreatorCommunityRepository by inject()
private var creatorId: Long = 0L private var creatorId: Long = 0L
private var currentHeader: CreatorChannelHeaderUiModel? = null private var currentHeader: CreatorChannelHeaderUiModel? = null
@@ -180,8 +174,7 @@ class CreatorChannelActivity :
return return
} }
if (SharedPreferenceManager.token.isBlank()) { if (!ensureV2Access(AccessRequirement.Login)) {
showLoginActivity()
finish() finish()
return return
} }
@@ -227,7 +220,7 @@ class CreatorChannelActivity :
binding.btnCreatorChannelFantalkFixedPlus.setOnClickListener { openFanTalkWrite() } binding.btnCreatorChannelFantalkFixedPlus.setOnClickListener { openFanTalkWrite() }
binding.btnCreatorChannelOwnerCta.setOnClickListener { onOwnerCtaClicked() } binding.btnCreatorChannelOwnerCta.setOnClickListener { onOwnerCtaClicked() }
binding.tvChatButton.setOnClickListener { binding.tvChatButton.setOnClickListener {
ensureLoginAndAdultAuth(isAdult = true) { ensureV2Access(AccessRequirement.AdultContent) {
currentHeader?.characterId?.let { characterId -> homeActionDelegate?.createChatRoom(characterId) } currentHeader?.characterId?.let { characterId -> homeActionDelegate?.createChatRoom(characterId) }
} }
} }
@@ -602,13 +595,10 @@ class CreatorChannelActivity :
override fun onCreatorChannelCommunityPostClicked(postId: Long) { override fun onCreatorChannelCommunityPostClicked(postId: Long) {
if (postId <= 0L) return if (postId <= 0L) return
if (SharedPreferenceManager.token.isBlank()) { ensureV2Access(AccessRequirement.Login) {
showLoginActivity()
return
}
communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId)) communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))
} }
}
override fun onCreatorChannelFanTalkContentChanged() { override fun onCreatorChannelFanTalkContentChanged() {
updateViewPagerHeight { updateViewPagerHeight {
@@ -623,15 +613,12 @@ class CreatorChannelActivity :
override fun onCreatorChannelFanTalkItemClicked(item: CreatorChannelFanTalkUiModel) { override fun onCreatorChannelFanTalkItemClicked(item: CreatorChannelFanTalkUiModel) {
if (currentHeader?.isOwner != true) return if (currentHeader?.isOwner != true) return
if (creatorId <= 0L || item.fanTalkId <= 0L) return if (creatorId <= 0L || item.fanTalkId <= 0L) return
if (SharedPreferenceManager.token.isBlank()) { ensureV2Access(AccessRequirement.Login) {
showLoginActivity()
return
}
fanTalkDetailLauncher.launch( fanTalkDetailLauncher.launch(
CreatorChannelFanTalkDetailActivity.newIntent(this, creatorId, item.toFanTalkDetailPayload()) CreatorChannelFanTalkDetailActivity.newIntent(this, creatorId, item.toFanTalkDetailPayload())
) )
} }
}
override fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged(isVisible: Boolean) { override fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged(isVisible: Boolean) {
isFanTalkFixedPlusVisible = isVisible isFanTalkFixedPlusVisible = isVisible
@@ -917,12 +904,10 @@ class CreatorChannelActivity :
private fun openFanTalkWrite() { private fun openFanTalkWrite() {
if (creatorId <= 0L) return if (creatorId <= 0L) return
if (SharedPreferenceManager.token.isBlank()) { ensureV2Access(AccessRequirement.Login) {
showLoginActivity()
return
}
fanTalkWriteLauncher.launch(CreatorChannelFanTalkWriteActivity.newIntent(this, creatorId)) fanTalkWriteLauncher.launch(CreatorChannelFanTalkWriteActivity.newIntent(this, creatorId))
} }
}
private fun onOwnerFabCommunityClicked() { private fun onOwnerFabCommunityClicked() {
collapseOwnerFab(animate = false) collapseOwnerFab(animate = false)
@@ -974,7 +959,7 @@ class CreatorChannelActivity :
} }
override fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse) { override fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse) {
ensureLoginAndAdultAuth(isAdult = live.isAdult) { ensureV2Access(AccessRequirement.forAdultContent(live.isAdult)) {
liveCoordinator.enterLiveRoom(live.liveId) liveCoordinator.enterLiveRoom(live.liveId)
} }
} }
@@ -1033,70 +1018,6 @@ class CreatorChannelActivity :
position == CreatorChannelTab.Donation.ordinal position == CreatorChannelTab.Donation.ordinal
} }
private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit) {
if (SharedPreferenceManager.token.isBlank()) {
showLoginActivity()
return
}
if (isAdult) {
val isKoreanCountry = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR"
if (isKoreanCountry && !SharedPreferenceManager.isAuth) {
V2ModalDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.auth_title),
desc = getString(R.string.auth_desc_live),
confirmButtonTitle = getString(R.string.auth_go),
confirmButtonClick = { startAuthFlow() },
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {}
).show(screenWidth)
return
}
if (!SharedPreferenceManager.isAdultContentVisible) {
startActivity(
Intent(this, ContentSettingsActivity::class.java).apply {
putExtra(Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE, true)
}
)
return
}
}
onAuthed()
}
private fun showLoginActivity() {
if (SharedPreferenceManager.token.isBlank()) {
startActivity(
Intent(this, LoginActivity::class.java).apply {
putExtra(Constants.EXTRA_DATA, intent.extras)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
)
}
}
private fun startAuthFlow() {
Auth.auth(this, this) { json ->
val bootpayResponse = Gson().fromJson(json, BootpayResponse::class.java)
val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)
runOnUiThread {
myPageViewModel.authVerify(request) {
startActivity(
Intent(this, SplashActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
finish()
}
}
}
}
private fun updateViewPagerHeight(afterHeightUpdated: (() -> Unit)? = null) { private fun updateViewPagerHeight(afterHeightUpdated: (() -> Unit)? = null) {
updateCreatorChannelTabViewportHeight() updateCreatorChannelTabViewportHeight()
binding.viewPager.post { binding.viewPager.post {

View File

@@ -47,31 +47,82 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("marginStart = if (isChatVisible && isDmVisible)")) assertTrue(source.contains("marginStart = if (isChatVisible && isDmVisible)"))
} }
@Test
fun `Activity 진입은 creator id 확인 후 로그인 가드를 거부하면 화면 조립 전에 종료한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val setupSource = sourceSection(
source = source,
startMarker = "override fun setupView()",
endMarker = "private fun setupLiveEntryObservers"
)
val invalidId = "if (creatorId <= 0L)"
val loginGuard = "if (!ensureV2Access(AccessRequirement.Login))"
val setupTabs = "setupTabsAndPager()"
assertTrue(setupSource.contains(invalidId))
assertTrue(setupSource.contains(loginGuard))
assertTrue(setupSource.indexOf(invalidId) < setupSource.indexOf(loginGuard))
assertTrue(setupSource.indexOf(loginGuard) < setupSource.indexOf(setupTabs))
val loginDeniedSource = setupSource.substring(
setupSource.indexOf(loginGuard),
setupSource.indexOf(setupTabs)
)
assertTrue(loginDeniedSource.contains("finish()"))
assertTrue(loginDeniedSource.contains("return"))
}
@Test
fun `AI 채팅 클릭은 성인 가드 통과 후 채팅방 생성을 요청한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val clickSource = sourceSection(
source = source,
startMarker = "binding.tvChatButton.setOnClickListener",
endMarker = "binding.tvDmButton.setOnClickListener"
)
val adultGuard = "ensureV2Access(AccessRequirement.AdultContent)"
val createChatRoom = "homeActionDelegate?.createChatRoom(characterId)"
assertTrue(clickSource.contains(adultGuard))
assertTrue(clickSource.contains(createChatRoom))
assertTrue(clickSource.indexOf(adultGuard) < clickSource.indexOf(createChatRoom))
}
@Test @Test
fun `커뮤니티 게시물 클릭 source는 상세 Activity newIntent로 진입하고 invalid postId를 무시한다`() { fun `커뮤니티 게시물 클릭 source는 상세 Activity newIntent로 진입하고 invalid postId를 무시한다`() {
val source = projectFile( val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText() ).readText()
val handlerStart = source.indexOf("override fun onCreatorChannelCommunityPostClicked(postId: Long)")
val handlerEnd = source.indexOf("override fun onCreatorChannelFanTalkContentChanged", handlerStart)
assertTrue(handlerStart >= 0)
assertTrue(handlerEnd > handlerStart)
val handlerSource = source.substring(handlerStart, handlerEnd)
assertTrue( assertTrue(
source.contains( source.contains(
"import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailActivity" "import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailActivity"
) )
) )
assertTrue(source.contains("override fun onCreatorChannelCommunityPostClicked(postId: Long)")) assertTrue(handlerSource.contains("if (postId <= 0L) return"))
assertTrue(source.contains("if (postId <= 0L) return")) assertTrue(handlerSource.contains("ensureV2Access(AccessRequirement.Login)"))
assertTrue(source.contains("if (SharedPreferenceManager.token.isBlank())")) assertTrue(handlerSource.contains("CreatorChannelCommunityDetailActivity.newIntent(this, postId)"))
assertTrue(source.contains("showLoginActivity()"))
assertTrue(source.contains("CreatorChannelCommunityDetailActivity.newIntent(this, postId)"))
assertTrue(source.contains("private val communityDetailLauncher = registerForActivityResult")) assertTrue(source.contains("private val communityDetailLauncher = registerForActivityResult"))
assertTrue( assertTrue(
source.contains( handlerSource.contains(
"communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))" "communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))"
) )
) )
assertTrue( assertTrue(
source.indexOf("if (SharedPreferenceManager.token.isBlank())") < handlerSource.indexOf("ensureV2Access(AccessRequirement.Login)") <
source.indexOf("communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))") handlerSource.indexOf(
"communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))"
)
) )
assertTrue(source.contains("refreshCreatorChannelCommunity()")) assertTrue(source.contains("refreshCreatorChannelCommunity()"))
} }
@@ -207,6 +258,11 @@ class CreatorChannelActivitySourceTest {
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt" "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt"
).readText() ).readText()
val appDi = projectFile("app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt").readText() val appDi = projectFile("app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt").readText()
val writeSource = sourceSection(
source = source,
startMarker = "private fun openFanTalkWrite()",
endMarker = "private fun onOwnerFabCommunityClicked"
)
assertTrue(layout.contains("android:id=\"@+id/btn_creator_channel_fantalk_fixed_plus\"")) assertTrue(layout.contains("android:id=\"@+id/btn_creator_channel_fantalk_fixed_plus\""))
assertTrue(layout.contains("android:src=\"@drawable/ic_plus_no_bg\"")) assertTrue(layout.contains("android:src=\"@drawable/ic_plus_no_bg\""))
@@ -214,11 +270,13 @@ class CreatorChannelActivitySourceTest {
assertTrue(layout.contains("app:layout_constraintBottom_toBottomOf=\"parent\"")) assertTrue(layout.contains("app:layout_constraintBottom_toBottomOf=\"parent\""))
assertTrue(layout.contains("app:layout_constraintEnd_toEndOf=\"parent\"")) assertTrue(layout.contains("app:layout_constraintEnd_toEndOf=\"parent\""))
assertTrue(source.contains("private val fanTalkWriteLauncher = registerForActivityResult")) assertTrue(source.contains("private val fanTalkWriteLauncher = registerForActivityResult"))
assertTrue(source.contains("CreatorChannelFanTalkWriteActivity.newIntent(this, creatorId)")) assertTrue(writeSource.contains("if (creatorId <= 0L) return"))
assertTrue(source.contains("private fun openFanTalkWrite()")) assertTrue(writeSource.contains("ensureV2Access(AccessRequirement.Login)"))
assertTrue(source.contains("if (creatorId <= 0L) return")) assertTrue(writeSource.contains("CreatorChannelFanTalkWriteActivity.newIntent(this, creatorId)"))
assertTrue(source.contains("if (SharedPreferenceManager.token.isBlank())")) assertTrue(
assertTrue(source.contains("showLoginActivity()")) writeSource.indexOf("ensureV2Access(AccessRequirement.Login)") <
writeSource.indexOf("fanTalkWriteLauncher.launch")
)
assertTrue(source.contains("refreshCreatorChannelFanTalk()")) assertTrue(source.contains("refreshCreatorChannelFanTalk()"))
assertTrue(source.contains("homeActionDelegate?.refreshHome()")) assertTrue(source.contains("homeActionDelegate?.refreshHome()"))
assertTrue(source.contains("onCreatorChannelFanTalkWriteClicked()")) assertTrue(source.contains("onCreatorChannelFanTalkWriteClicked()"))
@@ -1474,15 +1532,18 @@ class CreatorChannelActivitySourceTest {
assertTrue(fragment.contains("private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse)")) assertTrue(fragment.contains("private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse)"))
assertTrue(fragment.contains("host.onCreatorChannelCurrentLiveClicked(live)")) assertTrue(fragment.contains("host.onCreatorChannelCurrentLiveClicked(live)"))
assertTrue(activity.contains("override fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)")) assertTrue(activity.contains("override fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)"))
assertTrue(activity.contains("ensureLoginAndAdultAuth(isAdult = live.isAdult)")) assertTrue(activity.contains("ensureV2Access(AccessRequirement.forAdultContent(live.isAdult))"))
assertTrue(activity.contains("liveCoordinator.enterLiveRoom(live.liveId)")) assertTrue(activity.contains("liveCoordinator.enterLiveRoom(live.liveId)"))
assertTrue(activity.contains("private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit)")) assertFalse(activity.contains("private fun ensureLoginAndAdultAuth"))
assertTrue(activity.contains("SharedPreferenceManager.token.isBlank()")) assertTrue(activity.contains("ensureV2Access(AccessRequirement.forAdultContent(live.isAdult))"))
assertTrue(activity.contains("V2ModalDialog(")) val guard = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuard.kt").readText()
assertFalse(activity.contains("SodaDialog(")) assertTrue(guard.contains("V2ModalDialog("))
assertTrue(activity.contains("R.string.auth_desc_live")) assertFalse(guard.contains("SodaDialog("))
assertTrue(activity.contains("ContentSettingsActivity::class.java")) assertTrue(guard.contains("R.string.auth_desc_live"))
assertTrue(activity.contains("Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE")) assertTrue(guard.contains("ContentSettingsActivity::class.java"))
assertTrue(guard.contains("Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE"))
assertFalse(activity.contains("ContentSettingsActivity::class.java"))
assertFalse(activity.contains("Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE"))
} }
@Test @Test
@@ -1891,7 +1952,6 @@ class CreatorChannelActivitySourceTest {
assertTrue(fanTalkLayout.contains("android:layout_width=\"match_parent\"")) assertTrue(fanTalkLayout.contains("android:layout_width=\"match_parent\""))
assertTrue(fanTalkLayout.contains("android:layout_height=\"169dp\"")) assertTrue(fanTalkLayout.contains("android:layout_height=\"169dp\""))
assertTrue(fanTalkLayout.contains("android:layout_marginBottom=\"@dimen/spacing_6\"")) assertTrue(fanTalkLayout.contains("android:layout_marginBottom=\"@dimen/spacing_6\""))
assertTrue(fanTalkLayout.contains("@drawable/ic_chevron_down_white"))
assertFalse(fanTalkLayout.contains("@+id/ll_section_items")) assertFalse(fanTalkLayout.contains("@+id/ll_section_items"))
assertFalse(fanTalkLayout.contains("android:clipToOutline")) assertFalse(fanTalkLayout.contains("android:clipToOutline"))
assertTrue(fanTalkCardView.contains("clipToOutline = true")) assertTrue(fanTalkCardView.contains("clipToOutline = true"))
@@ -2186,21 +2246,39 @@ class CreatorChannelActivitySourceTest {
) )
) )
assertTrue(source.contains("private val fanTalkDetailLauncher = registerForActivityResult")) assertTrue(source.contains("private val fanTalkDetailLauncher = registerForActivityResult"))
assertTrue(source.contains("override fun onCreatorChannelFanTalkItemClicked(item: CreatorChannelFanTalkUiModel)")) val handlerSource = sourceSection(
assertTrue(source.contains("if (currentHeader?.isOwner != true) return")) source = source,
assertTrue(source.contains("if (creatorId <= 0L || item.fanTalkId <= 0L) return")) startMarker = "override fun onCreatorChannelFanTalkItemClicked(item: CreatorChannelFanTalkUiModel)",
assertTrue(source.contains("if (SharedPreferenceManager.token.isBlank())")) endMarker = "override fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged"
assertTrue(source.contains("showLoginActivity()")) )
assertTrue(source.contains("fanTalkDetailLauncher.launch("))
assertTrue(handlerSource.contains("if (currentHeader?.isOwner != true) return"))
assertTrue(handlerSource.contains("if (creatorId <= 0L || item.fanTalkId <= 0L) return"))
assertTrue(handlerSource.contains("ensureV2Access(AccessRequirement.Login)"))
assertTrue(handlerSource.contains("fanTalkDetailLauncher.launch("))
assertTrue( assertTrue(
source.contains( handlerSource.contains(
"CreatorChannelFanTalkDetailActivity.newIntent(this, creatorId, item.toFanTalkDetailPayload())" "CreatorChannelFanTalkDetailActivity.newIntent(this, creatorId, item.toFanTalkDetailPayload())"
) )
) )
assertTrue(
handlerSource.indexOf("ensureV2Access(AccessRequirement.Login)") <
handlerSource.indexOf("fanTalkDetailLauncher.launch(")
)
assertTrue(source.contains("refreshCreatorChannelFanTalk()")) assertTrue(source.contains("refreshCreatorChannelFanTalk()"))
assertTrue(source.contains("homeActionDelegate?.refreshHome()")) assertTrue(source.contains("homeActionDelegate?.refreshHome()"))
} }
private fun sourceSection(source: String, startMarker: String, endMarker: String): String {
val start = source.indexOf(startMarker)
val end = source.indexOf(endMarker, start)
assertTrue("start marker not found: $startMarker", start >= 0)
assertTrue("end marker not found after: $startMarker", end > start)
return source.substring(start, end)
}
private fun projectFile(relativePath: String): File { private fun projectFile(relativePath: String): File {
val candidates = listOf(File(relativePath), File("../$relativePath")) val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() } return candidates.firstOrNull { it.exists() }