feat(access): 공통 접근 실행기를 추가한다

This commit is contained in:
2026-07-14 20:44:20 +09:00
parent 4d51806cee
commit a6601dcea1
3 changed files with 227 additions and 12 deletions

View File

@@ -0,0 +1,160 @@
package kr.co.vividnext.sodalive.v2.access
import android.app.Activity
import android.content.Intent
import androidx.fragment.app.Fragment
import com.google.gson.Gson
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.Constants
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.settings.ContentSettingsActivity
import kr.co.vividnext.sodalive.splash.SplashActivity
import kr.co.vividnext.sodalive.user.login.LoginActivity
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import org.koin.android.ext.android.get
class V2AccessGuard(
private val policy: AccessPolicy = AccessPolicy(),
private val stateProvider: AccessStateProvider = SharedPreferenceAccessStateProvider()
) {
fun ensureAccess(
activity: Activity,
requirement: AccessRequirement,
onAllowed: () -> Unit = {}
): Boolean {
return handleDecision(
activity = activity,
requirement = requirement,
startLogin = { startLoginActivity(activity) },
onAllowed = onAllowed
)
}
fun ensureAccess(
fragment: Fragment,
requirement: AccessRequirement,
onAllowed: () -> Unit = {}
): Boolean {
val activity = fragment.requireActivity()
return handleDecision(
activity = activity,
requirement = requirement,
startLogin = { startLoginActivity(activity) },
onAllowed = onAllowed
)
}
fun isAllowed(requirement: AccessRequirement): Boolean {
return policy.decide(requirement, stateProvider.current()) == AccessDecision.Allowed
}
private fun handleDecision(
activity: Activity,
requirement: AccessRequirement,
startLogin: () -> Unit,
onAllowed: () -> Unit
): Boolean {
return executeAccessDecision(
decision = policy.decide(requirement, stateProvider.current()),
onAllowed = onAllowed,
onLoginRequired = startLogin,
onAdultVerificationRequired = { showAuthDialog(activity) },
onAdultContentSettingRequired = { startContentSettings(activity) }
)
}
private fun startLoginActivity(activity: Activity) {
activity.startActivity(
Intent(activity, LoginActivity::class.java).apply {
putExtra(Constants.EXTRA_DATA, activity.intent.extras)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
)
}
private fun showAuthDialog(activity: Activity) {
V2ModalDialog(
activity = activity,
layoutInflater = activity.layoutInflater,
title = activity.getString(R.string.auth_title),
desc = activity.getString(R.string.auth_desc_live),
confirmButtonTitle = activity.getString(R.string.auth_go),
confirmButtonClick = { startAuthFlow(activity) },
cancelButtonTitle = activity.getString(R.string.cancel),
cancelButtonClick = {}
).show(activity.resources.displayMetrics.widthPixels)
}
private fun startAuthFlow(activity: Activity) {
val myPageViewModel: MyPageViewModel = activity.get()
Auth.auth(activity, activity) { json ->
val bootpayResponse = Gson().fromJson(json, BootpayResponse::class.java)
val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)
activity.runOnUiThread {
if (activity.isFinishing || activity.isDestroyed) return@runOnUiThread
myPageViewModel.authVerify(request) {
if (activity.isFinishing || activity.isDestroyed) return@authVerify
activity.startActivity(
Intent(activity, SplashActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
activity.finish()
}
}
}
}
private fun startContentSettings(activity: Activity) {
activity.startActivity(
Intent(activity, ContentSettingsActivity::class.java).apply {
putExtra(Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE, true)
}
)
}
}
internal fun executeAccessDecision(
decision: AccessDecision,
onAllowed: () -> Unit,
onLoginRequired: () -> Unit,
onAdultVerificationRequired: () -> Unit,
onAdultContentSettingRequired: () -> Unit
): Boolean {
return when (decision) {
AccessDecision.Allowed -> {
onAllowed()
true
}
AccessDecision.LoginRequired -> {
onLoginRequired()
false
}
AccessDecision.AdultVerificationRequired -> {
onAdultVerificationRequired()
false
}
AccessDecision.AdultContentSettingRequired -> {
onAdultContentSettingRequired()
false
}
}
}
fun Activity.ensureV2Access(
requirement: AccessRequirement,
onAllowed: () -> Unit = {}
): Boolean = V2AccessGuard().ensureAccess(this, requirement, onAllowed)
fun Fragment.ensureV2Access(
requirement: AccessRequirement,
onAllowed: () -> Unit = {}
): Boolean = V2AccessGuard().ensureAccess(this, requirement, onAllowed)
fun isV2AccessAllowed(requirement: AccessRequirement): Boolean {
return V2AccessGuard().isAllowed(requirement)
}

View File

@@ -0,0 +1,32 @@
package kr.co.vividnext.sodalive.v2.access
import org.junit.Assert.assertEquals
import org.junit.Test
class V2AccessGuardTest {
@Test
fun `각 Access 결과는 대응하는 action만 정확히 한 번 실행한다`() {
val cases = listOf(
AccessDecision.Allowed to "allowed",
AccessDecision.LoginRequired to "login",
AccessDecision.AdultVerificationRequired to "adultVerification",
AccessDecision.AdultContentSettingRequired to "adultContentSetting"
)
cases.forEach { (decision, expectedAction) ->
val executedActions = mutableListOf<String>()
val isAllowed = executeAccessDecision(
decision = decision,
onAllowed = { executedActions += "allowed" },
onLoginRequired = { executedActions += "login" },
onAdultVerificationRequired = { executedActions += "adultVerification" },
onAdultContentSettingRequired = { executedActions += "adultContentSetting" }
)
assertEquals(decision == AccessDecision.Allowed, isAllowed)
assertEquals(listOf(expectedAction), executedActions)
}
}
}

View File

@@ -8,27 +8,50 @@ import java.io.File
class MainV2LoginGuardSourceTest { class MainV2LoginGuardSourceTest {
@Test @Test
fun `MainV2 로그인 가드는 v2 Activity 로그인 진입점과 성인 콘텐츠 설정을 사용한다`() { fun `V2AccessGuard는 기존 로그인 본인인증 성인 콘텐츠 설정 이동 계약을 유지한다`() {
val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2LoginGuard.kt").readText() val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuard.kt").readText()
val policy = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessPolicy.kt").readText()
val state = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessState.kt").readText()
val mainV2Activity = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt"
).readText()
val handleDecisionSource = source.substring(
source.indexOf("private fun handleDecision("),
source.indexOf("private fun startLoginActivity")
)
assertTrue(source.contains("fun Fragment.ensureMainV2NavigationAllowed(")) assertTrue(source.contains("fun Activity.ensureV2Access("))
assertTrue(source.contains("requiresAdultContentAccess: Boolean = false")) assertTrue(source.contains("fun Fragment.ensureV2Access("))
assertTrue(source.contains("SharedPreferenceManager.token.isBlank()")) assertTrue(source.contains("fun isV2AccessAllowed(requirement: AccessRequirement): Boolean"))
assertTrue(source.contains("(activity as? MainV2Activity)?.showLoginActivity()")) assertTrue(source.contains("fun isAllowed(requirement: AccessRequirement): Boolean"))
assertTrue(source.contains("SharedPreferenceManager.countryCode.ifBlank { \"KR\" }")) assertTrue(source.contains("AccessDecision.Allowed"))
assertTrue(source.contains("SharedPreferenceManager.isAuth")) assertTrue(source.contains("onAllowed()"))
assertTrue(handleDecisionSource.contains("onLoginRequired = startLogin"))
assertTrue(handleDecisionSource.contains("onAdultVerificationRequired = { showAuthDialog(activity) }"))
assertTrue(handleDecisionSource.contains("onAdultContentSettingRequired = { startContentSettings(activity) }"))
assertTrue(source.contains("Intent(activity, LoginActivity::class.java)"))
assertTrue(source.contains("putExtra(Constants.EXTRA_DATA, activity.intent.extras)"))
assertTrue(source.contains("Intent.FLAG_ACTIVITY_CLEAR_TOP"))
assertTrue(source.contains("Intent.FLAG_ACTIVITY_SINGLE_TOP"))
assertTrue(source.contains("V2ModalDialog(")) assertTrue(source.contains("V2ModalDialog("))
assertFalse(source.contains("SodaDialog(")) assertFalse(source.contains("SodaDialog("))
assertTrue(source.contains("Auth.auth(requireActivity(), requireContext())")) assertTrue(source.contains("Auth.auth(activity, activity)"))
assertTrue(source.contains("Gson().fromJson(json, BootpayResponse::class.java)")) assertTrue(source.contains("Gson().fromJson(json, BootpayResponse::class.java)"))
assertTrue(source.contains("AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)")) assertTrue(source.contains("AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)"))
assertTrue(source.contains("myPageViewModel.authVerify(request)")) assertTrue(source.contains("myPageViewModel.authVerify(request)"))
assertTrue(source.contains("if (activity.isFinishing || activity.isDestroyed)"))
assertTrue(source.contains("SplashActivity::class.java")) assertTrue(source.contains("SplashActivity::class.java"))
assertTrue(source.contains("SharedPreferenceManager.isAdultContentVisible")) assertTrue(source.contains("Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK"))
assertTrue(source.contains("ContentSettingsActivity::class.java")) assertTrue(source.contains("ContentSettingsActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE, true)")) assertTrue(source.contains("putExtra(Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE, true)"))
assertFalse(source.contains("Auth.auth(requireActivity(), requireContext()) {}")) assertFalse(source.contains("Auth.auth(activity, activity) {}"))
assertFalse(source.contains("kr.co.vividnext.sodalive.main.MainActivity")) assertTrue(policy.contains("state.token.isNotBlank() && state.token.length > LOGIN_TOKEN_MIN_LENGTH"))
assertTrue(policy.contains("state.countryCode.ifBlank { \"KR\" } == \"KR\""))
assertTrue(state.contains("SharedPreferenceManager.token"))
assertTrue(state.contains("SharedPreferenceManager.isAuth"))
assertTrue(state.contains("SharedPreferenceManager.isAdultContentVisible"))
assertTrue(mainV2Activity.contains("fun showLoginActivity()"))
assertTrue(mainV2Activity.contains("ensureV2Access(AccessRequirement.Login)"))
} }
private fun projectFile(relativePath: String): File { private fun projectFile(relativePath: String): File {