From 4d51806cee9815ea7107b8a9ded11090465be89f Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 14 Jul 2026 20:44:13 +0900 Subject: [PATCH] =?UTF-8?q?feat(access):=20=EC=A0=91=EA=B7=BC=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20=EB=AA=A8=EB=8D=B8=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/v2/access/AccessDecision.kt | 8 + .../sodalive/v2/access/AccessPolicy.kt | 38 +++++ .../sodalive/v2/access/AccessRequirement.kt | 12 ++ .../sodalive/v2/access/AccessState.kt | 23 +++ .../sodalive/v2/access/AccessPolicyTest.kt | 160 ++++++++++++++++++ 5 files changed, 241 insertions(+) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessDecision.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessPolicy.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessRequirement.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessState.kt create mode 100644 app/src/test/java/kr/co/vividnext/sodalive/v2/access/AccessPolicyTest.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessDecision.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessDecision.kt new file mode 100644 index 00000000..f6e7344d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessDecision.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.v2.access + +sealed interface AccessDecision { + data object Allowed : AccessDecision + data object LoginRequired : AccessDecision + data object AdultVerificationRequired : AccessDecision + data object AdultContentSettingRequired : AccessDecision +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessPolicy.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessPolicy.kt new file mode 100644 index 00000000..a05b5ce8 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessPolicy.kt @@ -0,0 +1,38 @@ +package kr.co.vividnext.sodalive.v2.access + +class AccessPolicy { + fun decide(requirement: AccessRequirement, state: AccessState): AccessDecision { + return when (requirement) { + AccessRequirement.Login -> decideLogin(state) + AccessRequirement.AdultContent -> decideAdultContent(state) + } + } + + private fun decideLogin(state: AccessState): AccessDecision { + return if (state.token.isNotBlank() && state.token.length > LOGIN_TOKEN_MIN_LENGTH) { + AccessDecision.Allowed + } else { + AccessDecision.LoginRequired + } + } + + private companion object { + const val LOGIN_TOKEN_MIN_LENGTH = 10 + } + + private fun decideAdultContent(state: AccessState): AccessDecision { + val loginDecision = decideLogin(state) + if (loginDecision != AccessDecision.Allowed) return loginDecision + + val isKoreanCountry = state.countryCode.ifBlank { "KR" } == "KR" + if (isKoreanCountry && !state.isAuth) { + return AccessDecision.AdultVerificationRequired + } + + if (!state.isAdultContentVisible) { + return AccessDecision.AdultContentSettingRequired + } + + return AccessDecision.Allowed + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessRequirement.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessRequirement.kt new file mode 100644 index 00000000..cdc7867d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessRequirement.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.v2.access + +sealed interface AccessRequirement { + data object Login : AccessRequirement + data object AdultContent : AccessRequirement + + companion object { + fun forAdultContent(requiresAdultContentAccess: Boolean): AccessRequirement { + return if (requiresAdultContentAccess) AdultContent else Login + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessState.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessState.kt new file mode 100644 index 00000000..30b7fb33 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessState.kt @@ -0,0 +1,23 @@ +package kr.co.vividnext.sodalive.v2.access + +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +data class AccessState( + val token: String, + val countryCode: String, + val isAuth: Boolean, + val isAdultContentVisible: Boolean +) + +fun interface AccessStateProvider { + fun current(): AccessState +} + +class SharedPreferenceAccessStateProvider : AccessStateProvider { + override fun current(): AccessState = AccessState( + token = SharedPreferenceManager.token, + countryCode = SharedPreferenceManager.countryCode, + isAuth = SharedPreferenceManager.isAuth, + isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible + ) +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/access/AccessPolicyTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/access/AccessPolicyTest.kt new file mode 100644 index 00000000..6c6c7ba2 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/access/AccessPolicyTest.kt @@ -0,0 +1,160 @@ +package kr.co.vividnext.sodalive.v2.access + +import org.junit.Assert.assertEquals +import org.junit.Test + +class AccessPolicyTest { + + private val policy = AccessPolicy() + + @Test + fun `로그인 요구사항은 토큰이 비어 있으면 로그인이 필요하다`() { + val decision = policy.decide( + requirement = AccessRequirement.Login, + state = accessState(token = "") + ) + + assertEquals(AccessDecision.LoginRequired, decision) + } + + @Test + fun `로그인 요구사항은 토큰이 10자 이하면 로그인이 필요하다`() { + val decision = policy.decide( + requirement = AccessRequirement.Login, + state = accessState(token = "1234567890") + ) + + assertEquals(AccessDecision.LoginRequired, decision) + } + + @Test + fun `로그인 요구사항은 토큰이 11자 이상 공백이면 로그인이 필요하다`() { + val decision = policy.decide( + requirement = AccessRequirement.Login, + state = accessState(token = " ") + ) + + assertEquals(AccessDecision.LoginRequired, decision) + } + + @Test + fun `로그인 요구사항은 토큰이 11자 이상이면 성인 조건을 확인하지 않고 허용한다`() { + val decision = policy.decide( + requirement = AccessRequirement.Login, + state = accessState( + token = "12345678901", + countryCode = "KR", + isAuth = false, + isAdultContentVisible = false + ) + ) + + assertEquals(AccessDecision.Allowed, decision) + } + + @Test + fun `성인 콘텐츠 요구사항은 토큰이 10자 이하면 로그인이 필요하다`() { + val decision = policy.decide( + requirement = AccessRequirement.AdultContent, + state = accessState(token = "1234567890") + ) + + assertEquals(AccessDecision.LoginRequired, decision) + } + + @Test + fun `성인 콘텐츠 요구사항은 빈 국가 코드를 KR로 처리한다`() { + val decision = policy.decide( + requirement = AccessRequirement.AdultContent, + state = accessState( + countryCode = "", + isAuth = false, + isAdultContentVisible = true + ) + ) + + assertEquals(AccessDecision.AdultVerificationRequired, decision) + } + + @Test + fun `성인 콘텐츠 요구사항은 국내 미인증 사용자의 본인인증이 필요하다`() { + val decision = policy.decide( + requirement = AccessRequirement.AdultContent, + state = accessState( + countryCode = "KR", + isAuth = false, + isAdultContentVisible = true + ) + ) + + assertEquals(AccessDecision.AdultVerificationRequired, decision) + } + + @Test + fun `성인 콘텐츠 요구사항은 본인인증 필요 결과를 성인 콘텐츠 설정보다 먼저 반환한다`() { + val decision = policy.decide( + requirement = AccessRequirement.AdultContent, + state = accessState( + countryCode = "KR", + isAuth = false, + isAdultContentVisible = false + ) + ) + + assertEquals(AccessDecision.AdultVerificationRequired, decision) + } + + @Test + fun `성인 콘텐츠 요구사항은 국외 사용자의 국내 본인인증을 건너뛴다`() { + val decision = policy.decide( + requirement = AccessRequirement.AdultContent, + state = accessState( + countryCode = "US", + isAuth = false, + isAdultContentVisible = true + ) + ) + + assertEquals(AccessDecision.Allowed, decision) + } + + @Test + fun `성인 콘텐츠 요구사항은 성인 콘텐츠 표시 설정이 꺼져 있으면 설정이 필요하다`() { + val decision = policy.decide( + requirement = AccessRequirement.AdultContent, + state = accessState( + countryCode = "US", + isAuth = false, + isAdultContentVisible = false + ) + ) + + assertEquals(AccessDecision.AdultContentSettingRequired, decision) + } + + @Test + fun `성인 콘텐츠 요구사항은 모든 조건을 충족하면 허용한다`() { + val decision = policy.decide( + requirement = AccessRequirement.AdultContent, + state = accessState( + countryCode = "KR", + isAuth = true, + isAdultContentVisible = true + ) + ) + + assertEquals(AccessDecision.Allowed, decision) + } + + private fun accessState( + token: String = "12345678901", + countryCode: String = "KR", + isAuth: Boolean = true, + isAdultContentVisible: Boolean = true + ): AccessState = AccessState( + token = token, + countryCode = countryCode, + isAuth = isAuth, + isAdultContentVisible = isAdultContentVisible + ) +}