Skip to content

Commit 165c773

Browse files
committed
[BOOK-274] feat: 스플래시 화면내 강제 업데이트 기능 구현
1 parent c5552a2 commit 165c773

File tree

14 files changed

+196
-25
lines changed

14 files changed

+196
-25
lines changed

build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import com.ninecraft.booket.convention.ApplicationConstants
33
import com.ninecraft.booket.convention.Plugins
44
import com.ninecraft.booket.convention.applyPlugins
55
import com.ninecraft.booket.convention.configureAndroid
6+
import com.ninecraft.booket.convention.libs
67
import org.gradle.api.Plugin
78
import org.gradle.api.Project
89
import org.gradle.kotlin.dsl.configure
@@ -19,9 +20,9 @@ internal class AndroidApplicationConventionPlugin : Plugin<Project> {
1920
configureAndroid(this)
2021

2122
defaultConfig {
22-
targetSdk = ApplicationConstants.TARGET_SDK
23-
versionName = ApplicationConstants.VERSION_NAME
24-
versionCode = ApplicationConstants.VERSION_CODE
23+
targetSdk = libs.versions.targetSdk.get().toInt()
24+
versionName = libs.versions.versionName.get()
25+
versionCode = libs.versions.versionCode.get().toInt()
2526
}
2627
}
2728
}

build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import com.android.build.gradle.LibraryExtension
22
import com.ninecraft.booket.convention.Plugins
33
import com.ninecraft.booket.convention.applyPlugins
44
import com.ninecraft.booket.convention.configureAndroid
5+
import com.ninecraft.booket.convention.libs
56
import org.gradle.api.Plugin
67
import org.gradle.api.Project
78
import org.gradle.kotlin.dsl.configure
8-
import com.ninecraft.booket.convention.ApplicationConstants
99

1010
internal class AndroidLibraryConventionPlugin : Plugin<Project> {
1111
override fun apply(target: Project) {
@@ -19,7 +19,7 @@ internal class AndroidLibraryConventionPlugin : Plugin<Project> {
1919
configureAndroid(this)
2020

2121
defaultConfig.apply {
22-
targetSdk = ApplicationConstants.TARGET_SDK
22+
targetSdk = libs.versions.targetSdk.get().toInt()
2323
}
2424
}
2525
}

build-logic/src/main/kotlin/com/ninecraft/booket/convention/Android.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
88

99
internal fun Project.configureAndroid(extension: CommonExtension<*, *, *, *, *, *>) {
1010
extension.apply {
11-
compileSdk = ApplicationConstants.COMPILE_SDK
11+
compileSdk = libs.versions.compileSdk.get().toInt()
1212

1313
defaultConfig {
14-
minSdk = ApplicationConstants.MIN_SDK
14+
minSdk = libs.versions.minSdk.get().toInt()
1515
}
1616

1717
compileOptions {

build-logic/src/main/kotlin/com/ninecraft/booket/convention/ApplicationConstants.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ package com.ninecraft.booket.convention
33
import org.gradle.api.JavaVersion
44

55
internal object ApplicationConstants {
6-
const val MIN_SDK = 28
7-
const val TARGET_SDK = 35
8-
const val COMPILE_SDK = 35
9-
const val VERSION_CODE = 3
10-
const val VERSION_NAME = "1.0.0"
116
const val JAVA_VERSION_INT = 17
127
val javaVersion = JavaVersion.VERSION_17
138
}

core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/RemoteConfigRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ package com.ninecraft.booket.core.data.api.repository
22

33
interface RemoteConfigRepository {
44
suspend fun getLatestVersion(): Result<String>
5+
suspend fun shouldUpdate(): Result<Boolean>
56
}

core/data/impl/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ android {
1212
buildFeatures {
1313
buildConfig = true
1414
}
15+
16+
defaultConfig {
17+
buildConfigField("String", "APP_VERSION", "\"${libs.versions.versionName.get()}\"")
18+
}
1519
}
1620

1721
dependencies {

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRemoteConfigRepository.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.ninecraft.booket.core.data.impl.repository
33
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
44
import com.google.firebase.remoteconfig.get
55
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
6+
import com.ninecraft.booket.core.data.impl.BuildConfig
67
import com.orhanobut.logger.Logger
78
import kotlinx.coroutines.suspendCancellableCoroutine
89
import javax.inject.Inject
@@ -24,7 +25,49 @@ class DefaultRemoteConfigRepository @Inject constructor(
2425
}
2526
}
2627

28+
override suspend fun shouldUpdate(): Result<Boolean> = suspendCancellableCoroutine { continuation ->
29+
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
30+
if (task.isSuccessful) {
31+
val minVersion = remoteConfig[KEY_MIN_VERSION].asString()
32+
val currentVersion = BuildConfig.APP_VERSION
33+
continuation.resume(Result.success(checkMinVersion(currentVersion, minVersion)))
34+
} else {
35+
Logger.e(task.exception, "shouldUpdate: getMinVersion failed")
36+
continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
37+
}
38+
}
39+
}
40+
41+
/**
42+
* 현재 앱 버전이 최소 요구 버전보다 낮은지 확인하는 함수
43+
*
44+
* @param currentVersion 현재 앱의 버전 (예: "1.0.0")
45+
* @param minVersion 최소 요구 버전 (Firebase Remote Config에서 가져온 값)
46+
* @return true면 강제 업데이트 필요 (현재 버전 < 최소 요구 버전), false면 업데이트 불필요
47+
*
48+
* 버전 형식: "메이저.마이너.패치" (예: 1.2.3)
49+
* 비교 순서: 메이저 → 마이너 → 패치 버전 순으로 비교
50+
*/
51+
private fun checkMinVersion(currentVersion: String, minVersion: String): Boolean {
52+
Logger.d("checkMinVersion: current: $currentVersion, min: $minVersion")
53+
if (!Regex("""^\d+\.\d+\.\d+$""").matches(currentVersion)) return false
54+
if (!Regex("""^\d+\.\d+\.\d+$""").matches(minVersion)) return false
55+
56+
val current = currentVersion.split('.').map { it.toInt() }
57+
val min = minVersion.split('.').map { it.toInt() }
58+
59+
// 메이저 버전 비교
60+
if (current[0] != min[0]) return current[0] < min[0]
61+
62+
// 마이너 버전 비교
63+
if (current[1] != min[1]) return current[1] < min[1]
64+
65+
// 패치 버전 비교
66+
return current[2] < min[2]
67+
}
68+
2769
companion object {
2870
private const val KEY_LATEST_VERSION = "LatestVersion"
71+
private const val KEY_MIN_VERSION = "MinVersion"
2972
}
3073
}

feature/splash/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ plugins {
66

77
android {
88
namespace = "com.ninecraft.booket.feature.splash"
9+
10+
buildFeatures {
11+
buildConfig = true
12+
}
13+
14+
defaultConfig {
15+
buildConfigField("String", "PACKAGE_NAME", "\"${libs.versions.packageName.get()}\"")
16+
}
917
}
1018

1119
ksp {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.ninecraft.booket.splash
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.ui.platform.LocalContext
7+
import androidx.core.net.toUri
8+
import com.ninecraft.booket.feature.splash.BuildConfig
9+
import com.skydoves.compose.effects.RememberedEffect
10+
11+
@Composable
12+
internal fun HandleSplashSideEffects(
13+
state: SplashUiState,
14+
eventSink: (SplashUiEvent) -> Unit,
15+
) {
16+
val context = LocalContext.current
17+
18+
RememberedEffect(state.sideEffect) {
19+
when (state.sideEffect) {
20+
is SplashSideEffect.NavigateToPlayStore -> {
21+
openPlayStore(context)
22+
}
23+
null -> {}
24+
}
25+
26+
if (state.sideEffect != null) {
27+
eventSink(SplashUiEvent.InitSideEffect)
28+
}
29+
}
30+
}
31+
32+
private fun openPlayStore(context: Context) {
33+
// https://play.google.com/store/apps/details?id=com.ninecraft.booket
34+
val intent = Intent(Intent.ACTION_VIEW, "market://details?id=${BuildConfig.PACKAGE_NAME}".toUri())
35+
context.startActivity(intent)
36+
}

feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.runtime.setValue
99
import com.ninecraft.booket.core.common.constants.ErrorScope
1010
import com.ninecraft.booket.core.common.utils.postErrorDialog
1111
import com.ninecraft.booket.core.data.api.repository.AuthRepository
12+
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
1213
import com.ninecraft.booket.core.data.api.repository.UserRepository
1314
import com.ninecraft.booket.core.model.AutoLoginState
1415
import com.ninecraft.booket.core.model.OnboardingState
@@ -17,7 +18,7 @@ import com.ninecraft.booket.feature.screens.HomeScreen
1718
import com.ninecraft.booket.feature.screens.LoginScreen
1819
import com.ninecraft.booket.feature.screens.OnboardingScreen
1920
import com.ninecraft.booket.feature.screens.SplashScreen
20-
import com.skydoves.compose.effects.RememberedEffect
21+
import com.orhanobut.logger.Logger
2122
import com.slack.circuit.codegen.annotations.CircuitInject
2223
import com.slack.circuit.retained.collectAsRetainedState
2324
import com.slack.circuit.retained.rememberRetained
@@ -34,14 +35,16 @@ class SplashPresenter @AssistedInject constructor(
3435
@Assisted private val navigator: Navigator,
3536
private val userRepository: UserRepository,
3637
private val authRepository: AuthRepository,
38+
private val remoteConfigRepository: RemoteConfigRepository,
3739
) : Presenter<SplashUiState> {
3840

3941
@Composable
4042
override fun present(): SplashUiState {
4143
val scope = rememberCoroutineScope()
4244
val onboardingState by userRepository.onboardingState.collectAsRetainedState(initial = OnboardingState.IDLE)
4345
val autoLoginState by authRepository.autoLoginState.collectAsRetainedState(initial = AutoLoginState.IDLE)
44-
var isSplashTimeCompleted by rememberRetained { mutableStateOf(false) }
46+
var isForceUpdateDialogVisible by rememberRetained { mutableStateOf(false) }
47+
var sideEffect by rememberRetained { mutableStateOf<SplashSideEffect?>(null) }
4548

4649
fun checkTermsAgreement() {
4750
scope.launch {
@@ -64,14 +67,7 @@ class SplashPresenter @AssistedInject constructor(
6467
}
6568
}
6669

67-
LaunchedEffect(Unit) {
68-
delay(1000L)
69-
isSplashTimeCompleted = true
70-
}
71-
72-
RememberedEffect(onboardingState, autoLoginState, isSplashTimeCompleted) {
73-
if (!isSplashTimeCompleted) return@RememberedEffect
74-
70+
fun proceedToNextScreen() {
7571
when (onboardingState) {
7672
OnboardingState.NOT_COMPLETED -> {
7773
navigator.resetRoot(OnboardingScreen)
@@ -99,7 +95,44 @@ class SplashPresenter @AssistedInject constructor(
9995
}
10096
}
10197

102-
return SplashUiState
98+
fun handleEvent(event: SplashUiEvent) {
99+
when (event) {
100+
SplashUiEvent.OnUpdateButtonClick -> {
101+
sideEffect = SplashSideEffect.NavigateToPlayStore
102+
}
103+
104+
SplashUiEvent.InitSideEffect -> {
105+
sideEffect = null
106+
}
107+
}
108+
}
109+
110+
LaunchedEffect(onboardingState, autoLoginState) {
111+
delay(1000L)
112+
113+
if (onboardingState == OnboardingState.IDLE || autoLoginState == AutoLoginState.IDLE) {
114+
return@LaunchedEffect
115+
}
116+
117+
remoteConfigRepository.shouldUpdate()
118+
.onSuccess { shouldUpdate ->
119+
if (shouldUpdate) {
120+
isForceUpdateDialogVisible = true
121+
} else {
122+
proceedToNextScreen()
123+
}
124+
}
125+
.onFailure { exception ->
126+
Logger.e("${exception.message}")
127+
proceedToNextScreen()
128+
}
129+
}
130+
131+
return SplashUiState(
132+
isForceUpdateDialogVisible = isForceUpdateDialogVisible,
133+
sideEffect = sideEffect,
134+
eventSink = ::handleEvent,
135+
)
103136
}
104137

105138
@CircuitInject(SplashScreen::class, ActivityRetainedComponent::class)

0 commit comments

Comments
 (0)