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.room.create.LiveRoomCreateActivity
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.settings.ContentSettingsActivity
import kr.co.vividnext.sodalive.splash.SplashActivity
import kr.co.vividnext.sodalive.user.login.LoginActivity
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
@@ -93,7 +88,6 @@ class CreatorChannelActivity :
CreatorChannelDonationFragment.Host {
private val liveViewModel: LiveViewModel by inject()
private val myPageViewModel: MyPageViewModel by inject()
private val creatorCommunityRepository: CreatorCommunityRepository by inject()
private var creatorId: Long = 0L
private var currentHeader: CreatorChannelHeaderUiModel? = null
@@ -180,8 +174,7 @@ class CreatorChannelActivity :
return
}
if (SharedPreferenceManager.token.isBlank()) {
showLoginActivity()
if (!ensureV2Access(AccessRequirement.Login)) {
finish()
return
}
@@ -227,7 +220,7 @@ class CreatorChannelActivity :
binding.btnCreatorChannelFantalkFixedPlus.setOnClickListener { openFanTalkWrite() }
binding.btnCreatorChannelOwnerCta.setOnClickListener { onOwnerCtaClicked() }
binding.tvChatButton.setOnClickListener {
ensureLoginAndAdultAuth(isAdult = true) {
ensureV2Access(AccessRequirement.AdultContent) {
currentHeader?.characterId?.let { characterId -> homeActionDelegate?.createChatRoom(characterId) }
}
}
@@ -602,13 +595,10 @@ class CreatorChannelActivity :
override fun onCreatorChannelCommunityPostClicked(postId: Long) {
if (postId <= 0L) return
if (SharedPreferenceManager.token.isBlank()) {
showLoginActivity()
return
}
ensureV2Access(AccessRequirement.Login) {
communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))
}
}
override fun onCreatorChannelFanTalkContentChanged() {
updateViewPagerHeight {
@@ -623,15 +613,12 @@ class CreatorChannelActivity :
override fun onCreatorChannelFanTalkItemClicked(item: CreatorChannelFanTalkUiModel) {
if (currentHeader?.isOwner != true) return
if (creatorId <= 0L || item.fanTalkId <= 0L) return
if (SharedPreferenceManager.token.isBlank()) {
showLoginActivity()
return
}
ensureV2Access(AccessRequirement.Login) {
fanTalkDetailLauncher.launch(
CreatorChannelFanTalkDetailActivity.newIntent(this, creatorId, item.toFanTalkDetailPayload())
)
}
}
override fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged(isVisible: Boolean) {
isFanTalkFixedPlusVisible = isVisible
@@ -917,12 +904,10 @@ class CreatorChannelActivity :
private fun openFanTalkWrite() {
if (creatorId <= 0L) return
if (SharedPreferenceManager.token.isBlank()) {
showLoginActivity()
return
}
ensureV2Access(AccessRequirement.Login) {
fanTalkWriteLauncher.launch(CreatorChannelFanTalkWriteActivity.newIntent(this, creatorId))
}
}
private fun onOwnerFabCommunityClicked() {
collapseOwnerFab(animate = false)
@@ -974,7 +959,7 @@ class CreatorChannelActivity :
}
override fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse) {
ensureLoginAndAdultAuth(isAdult = live.isAdult) {
ensureV2Access(AccessRequirement.forAdultContent(live.isAdult)) {
liveCoordinator.enterLiveRoom(live.liveId)
}
}
@@ -1033,70 +1018,6 @@ class CreatorChannelActivity :
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) {
updateCreatorChannelTabViewportHeight()
binding.viewPager.post {

View File

@@ -47,31 +47,82 @@ class CreatorChannelActivitySourceTest {
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
fun `커뮤니티 게시물 클릭 source는 상세 Activity newIntent로 진입하고 invalid postId를 무시한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).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(
source.contains(
"import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailActivity"
)
)
assertTrue(source.contains("override fun onCreatorChannelCommunityPostClicked(postId: Long)"))
assertTrue(source.contains("if (postId <= 0L) return"))
assertTrue(source.contains("if (SharedPreferenceManager.token.isBlank())"))
assertTrue(source.contains("showLoginActivity()"))
assertTrue(source.contains("CreatorChannelCommunityDetailActivity.newIntent(this, postId)"))
assertTrue(handlerSource.contains("if (postId <= 0L) return"))
assertTrue(handlerSource.contains("ensureV2Access(AccessRequirement.Login)"))
assertTrue(handlerSource.contains("CreatorChannelCommunityDetailActivity.newIntent(this, postId)"))
assertTrue(source.contains("private val communityDetailLauncher = registerForActivityResult"))
assertTrue(
source.contains(
handlerSource.contains(
"communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))"
)
)
assertTrue(
source.indexOf("if (SharedPreferenceManager.token.isBlank())") <
source.indexOf("communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))")
handlerSource.indexOf("ensureV2Access(AccessRequirement.Login)") <
handlerSource.indexOf(
"communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))"
)
)
assertTrue(source.contains("refreshCreatorChannelCommunity()"))
}
@@ -207,6 +258,11 @@ class CreatorChannelActivitySourceTest {
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.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: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_constraintEnd_toEndOf=\"parent\""))
assertTrue(source.contains("private val fanTalkWriteLauncher = registerForActivityResult"))
assertTrue(source.contains("CreatorChannelFanTalkWriteActivity.newIntent(this, creatorId)"))
assertTrue(source.contains("private fun openFanTalkWrite()"))
assertTrue(source.contains("if (creatorId <= 0L) return"))
assertTrue(source.contains("if (SharedPreferenceManager.token.isBlank())"))
assertTrue(source.contains("showLoginActivity()"))
assertTrue(writeSource.contains("if (creatorId <= 0L) return"))
assertTrue(writeSource.contains("ensureV2Access(AccessRequirement.Login)"))
assertTrue(writeSource.contains("CreatorChannelFanTalkWriteActivity.newIntent(this, creatorId)"))
assertTrue(
writeSource.indexOf("ensureV2Access(AccessRequirement.Login)") <
writeSource.indexOf("fanTalkWriteLauncher.launch")
)
assertTrue(source.contains("refreshCreatorChannelFanTalk()"))
assertTrue(source.contains("homeActionDelegate?.refreshHome()"))
assertTrue(source.contains("onCreatorChannelFanTalkWriteClicked()"))
@@ -1474,15 +1532,18 @@ class CreatorChannelActivitySourceTest {
assertTrue(fragment.contains("private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse)"))
assertTrue(fragment.contains("host.onCreatorChannelCurrentLiveClicked(live)"))
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("private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit)"))
assertTrue(activity.contains("SharedPreferenceManager.token.isBlank()"))
assertTrue(activity.contains("V2ModalDialog("))
assertFalse(activity.contains("SodaDialog("))
assertTrue(activity.contains("R.string.auth_desc_live"))
assertTrue(activity.contains("ContentSettingsActivity::class.java"))
assertTrue(activity.contains("Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE"))
assertFalse(activity.contains("private fun ensureLoginAndAdultAuth"))
assertTrue(activity.contains("ensureV2Access(AccessRequirement.forAdultContent(live.isAdult))"))
val guard = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuard.kt").readText()
assertTrue(guard.contains("V2ModalDialog("))
assertFalse(guard.contains("SodaDialog("))
assertTrue(guard.contains("R.string.auth_desc_live"))
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
@@ -1891,7 +1952,6 @@ class CreatorChannelActivitySourceTest {
assertTrue(fanTalkLayout.contains("android:layout_width=\"match_parent\""))
assertTrue(fanTalkLayout.contains("android:layout_height=\"169dp\""))
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("android:clipToOutline"))
assertTrue(fanTalkCardView.contains("clipToOutline = true"))
@@ -2186,21 +2246,39 @@ class CreatorChannelActivitySourceTest {
)
)
assertTrue(source.contains("private val fanTalkDetailLauncher = registerForActivityResult"))
assertTrue(source.contains("override fun onCreatorChannelFanTalkItemClicked(item: CreatorChannelFanTalkUiModel)"))
assertTrue(source.contains("if (currentHeader?.isOwner != true) return"))
assertTrue(source.contains("if (creatorId <= 0L || item.fanTalkId <= 0L) return"))
assertTrue(source.contains("if (SharedPreferenceManager.token.isBlank())"))
assertTrue(source.contains("showLoginActivity()"))
assertTrue(source.contains("fanTalkDetailLauncher.launch("))
val handlerSource = sourceSection(
source = source,
startMarker = "override fun onCreatorChannelFanTalkItemClicked(item: CreatorChannelFanTalkUiModel)",
endMarker = "override fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged"
)
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(
source.contains(
handlerSource.contains(
"CreatorChannelFanTalkDetailActivity.newIntent(this, creatorId, item.toFanTalkDetailPayload())"
)
)
assertTrue(
handlerSource.indexOf("ensureV2Access(AccessRequirement.Login)") <
handlerSource.indexOf("fanTalkDetailLauncher.launch(")
)
assertTrue(source.contains("refreshCreatorChannelFanTalk()"))
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 {
val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() }