Skip to content

Commit 69f2ae6

Browse files
committed
[BOOK-274] feat: 설정화면 내 선택 업데이트 팝업 연동 및 스토어 이동 플로우 구현
1 parent c8725a8 commit 69f2ae6

File tree

11 files changed

+152
-101
lines changed

11 files changed

+152
-101
lines changed

core/common/build.gradle.kts

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

1010
android {
1111
namespace = "com.ninecraft.booket.core.common"
12+
13+
buildFeatures {
14+
buildConfig = true
15+
}
16+
17+
defaultConfig {
18+
buildConfigField("String", "PACKAGE_NAME", "\"${libs.versions.packageName.get()}\"")
19+
}
1220
}
1321

1422
dependencies {

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

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.ninecraft.booket.core.data.impl.repository
22

33
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
44
import com.google.firebase.remoteconfig.get
5+
import com.ninecraft.booket.core.common.util.isUpdateRequired
56
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
67
import com.ninecraft.booket.core.data.impl.BuildConfig
78
import com.orhanobut.logger.Logger
@@ -30,42 +31,14 @@ class DefaultRemoteConfigRepository @Inject constructor(
3031
if (task.isSuccessful) {
3132
val minVersion = remoteConfig[KEY_MIN_VERSION].asString()
3233
val currentVersion = BuildConfig.APP_VERSION
33-
continuation.resume(Result.success(checkMinVersion(currentVersion, minVersion)))
34+
continuation.resume(Result.success(isUpdateRequired(currentVersion, minVersion)))
3435
} else {
3536
Logger.e(task.exception, "shouldUpdate: getMinVersion failed")
3637
continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
3738
}
3839
}
3940
}
4041

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-
6942
companion object {
7043
private const val KEY_LATEST_VERSION = "LatestVersion"
7144
private const val KEY_MIN_VERSION = "MinVersion"

feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/HandlingSettingsSideEffect.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.ninecraft.booket.feature.settings
33
import android.widget.Toast
44
import androidx.compose.runtime.Composable
55
import androidx.compose.ui.platform.LocalContext
6+
import com.ninecraft.booket.core.common.extensions.openPlayStore
67
import com.skydoves.compose.effects.RememberedEffect
78

89
@Composable
@@ -18,7 +19,11 @@ internal fun HandleSettingsSideEffects(
1819
Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show()
1920
}
2021

21-
null -> {}
22+
is SettingsSideEffect.NavigateToPlayStore -> {
23+
context.openPlayStore()
24+
}
25+
26+
else -> {}
2227
}
2328

2429
if (state.sideEffect != null) {

feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class SettingsPresenter @AssistedInject constructor(
3939
var isWithdrawBottomSheetVisible by rememberRetained { mutableStateOf(false) }
4040
var isWithdrawConfirmed by rememberRetained { mutableStateOf(false) }
4141
var latestVersion by rememberRetained { mutableStateOf("") }
42+
var isOptionalUpdateDialogVisible by rememberRetained { mutableStateOf(false) }
4243
var sideEffect by rememberRetained { mutableStateOf<SettingsSideEffect?>(null) }
4344

4445
fun logout() {
@@ -173,6 +174,18 @@ class SettingsPresenter @AssistedInject constructor(
173174
is SettingsUiEvent.Withdraw -> {
174175
withdraw()
175176
}
177+
178+
is SettingsUiEvent.OnVersionClick -> {
179+
isOptionalUpdateDialogVisible = true
180+
}
181+
182+
is SettingsUiEvent.OnOptionalUpdateDialogDismiss -> {
183+
isOptionalUpdateDialogVisible = false
184+
}
185+
186+
is SettingsUiEvent.OnUpdateButtonClick -> {
187+
sideEffect = SettingsSideEffect.NavigateToPlayStore
188+
}
176189
}
177190
}
178191

@@ -186,6 +199,7 @@ class SettingsPresenter @AssistedInject constructor(
186199
isWithdrawBottomSheetVisible = isWithdrawBottomSheetVisible,
187200
isWithdrawConfirmed = isWithdrawConfirmed,
188201
latestVersion = latestVersion,
202+
isOptionalUpdateDialogVisible = isOptionalUpdateDialogVisible,
189203
sideEffect = sideEffect,
190204
eventSink = ::handleEvent,
191205
)

feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import androidx.compose.foundation.layout.Column
66
import androidx.compose.foundation.layout.Row
77
import androidx.compose.foundation.layout.Spacer
88
import androidx.compose.foundation.layout.fillMaxSize
9-
import androidx.compose.foundation.layout.fillMaxWidth
109
import androidx.compose.foundation.layout.height
1110
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.foundation.layout.width
1212
import androidx.compose.foundation.rememberScrollState
1313
import androidx.compose.foundation.verticalScroll
1414
import androidx.compose.material3.CircularProgressIndicator
@@ -26,7 +26,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
2626
import androidx.compose.ui.platform.LocalContext
2727
import androidx.compose.ui.res.stringResource
2828
import androidx.compose.ui.res.vectorResource
29-
import com.ninecraft.booket.core.common.extensions.clickableSingle
29+
import com.ninecraft.booket.core.common.util.compareVersions
3030
import com.ninecraft.booket.core.designsystem.DevicePreview
3131
import com.ninecraft.booket.core.designsystem.component.ReedDivider
3232
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
@@ -35,10 +35,12 @@ import com.ninecraft.booket.core.ui.ReedScaffold
3535
import com.ninecraft.booket.core.ui.component.ReedBackTopAppBar
3636
import com.ninecraft.booket.core.ui.component.ReedDialog
3737
import com.ninecraft.booket.feature.screens.SettingsScreen
38+
import com.ninecraft.booket.feature.settings.component.SettingItem
3839
import com.ninecraft.booket.feature.settings.component.WithdrawConfirmationBottomSheet
3940
import com.slack.circuit.codegen.annotations.CircuitInject
4041
import dagger.hilt.android.components.ActivityRetainedComponent
4142
import kotlinx.coroutines.launch
43+
import com.ninecraft.booket.core.designsystem.R as designR
4244

4345
@OptIn(ExperimentalMaterial3Api::class)
4446
@CircuitInject(SettingsScreen::class, ActivityRetainedComponent::class)
@@ -62,6 +64,10 @@ internal fun SettingsUi(
6264
}.getOrNull() ?: "Unknown"
6365
}
6466

67+
val isUpdateAvailable = remember(appVersion, state.latestVersion) {
68+
compareVersions(state.latestVersion, appVersion) > 0
69+
}
70+
6571
ReedScaffold(
6672
modifier = modifier
6773
.fillMaxSize()
@@ -88,7 +94,7 @@ internal fun SettingsUi(
8894
},
8995
action = {
9096
Icon(
91-
imageVector = ImageVector.vectorResource(id = com.ninecraft.booket.core.designsystem.R.drawable.ic_chevron_right),
97+
imageVector = ImageVector.vectorResource(id = designR.drawable.ic_chevron_right),
9298
contentDescription = "Right Chevron Icon",
9399
tint = Color.Unspecified,
94100
)
@@ -101,7 +107,7 @@ internal fun SettingsUi(
101107
},
102108
action = {
103109
Icon(
104-
imageVector = ImageVector.vectorResource(id = com.ninecraft.booket.core.designsystem.R.drawable.ic_chevron_right),
110+
imageVector = ImageVector.vectorResource(id = designR.drawable.ic_chevron_right),
105111
contentDescription = "Right Chevron Icon",
106112
tint = Color.Unspecified,
107113
)
@@ -114,21 +120,36 @@ internal fun SettingsUi(
114120
},
115121
action = {
116122
Icon(
117-
imageVector = ImageVector.vectorResource(id = com.ninecraft.booket.core.designsystem.R.drawable.ic_chevron_right),
123+
imageVector = ImageVector.vectorResource(id = designR.drawable.ic_chevron_right),
118124
contentDescription = "Right Chevron Icon",
119125
tint = Color.Unspecified,
120126
)
121127
},
122128
)
123129
SettingItem(
124130
title = stringResource(R.string.settings_app_version),
125-
isClickable = false,
131+
isClickable = isUpdateAvailable,
132+
onItemClick = {
133+
state.eventSink(SettingsUiEvent.OnVersionClick)
134+
},
126135
action = {
127-
Text(
128-
text = appVersion,
129-
style = ReedTheme.typography.body1Medium,
130-
color = ReedTheme.colors.contentBrand,
131-
)
136+
Row(
137+
verticalAlignment = Alignment.CenterVertically,
138+
) {
139+
Text(
140+
text = appVersion,
141+
style = ReedTheme.typography.body1Medium,
142+
color = ReedTheme.colors.contentBrand,
143+
)
144+
if (isUpdateAvailable) {
145+
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2))
146+
Icon(
147+
imageVector = ImageVector.vectorResource(id = designR.drawable.ic_chevron_right),
148+
contentDescription = "Right Chevron Icon",
149+
tint = Color.Unspecified,
150+
)
151+
}
152+
}
132153
},
133154
description = {
134155
Text(
@@ -199,46 +220,19 @@ internal fun SettingsUi(
199220
},
200221
)
201222
}
202-
}
203-
}
204-
205-
@Composable
206-
private fun SettingItem(
207-
title: String,
208-
modifier: Modifier = Modifier,
209-
isClickable: Boolean = true,
210-
onItemClick: () -> Unit = {},
211-
action: @Composable () -> Unit = {},
212-
description: @Composable () -> Unit = {},
213-
) {
214-
val combinedModifier = if (isClickable) {
215-
modifier
216-
.fillMaxWidth()
217-
.clickableSingle { onItemClick() }
218-
} else {
219-
modifier.fillMaxWidth()
220-
}
221223

222-
Column {
223-
Row(
224-
modifier = combinedModifier
225-
.padding(
226-
horizontal = ReedTheme.spacing.spacing5,
227-
vertical = ReedTheme.spacing.spacing4,
228-
),
229-
verticalAlignment = Alignment.CenterVertically,
230-
) {
231-
Column(
232-
modifier = Modifier.weight(1f),
233-
) {
234-
Text(
235-
text = title,
236-
style = ReedTheme.typography.body1Medium,
237-
color = ReedTheme.colors.contentPrimary,
238-
)
239-
description()
240-
}
241-
action()
224+
if (state.isOptionalUpdateDialogVisible) {
225+
ReedDialog(
226+
onDismissRequest = {
227+
state.eventSink(SettingsUiEvent.OnOptionalUpdateDialogDismiss)
228+
},
229+
title = stringResource(R.string.settings_optional_update_title),
230+
description = stringResource(R.string.settings_optional_update_message),
231+
confirmButtonText = stringResource(R.string.settings_optional_update_button_text),
232+
onConfirmRequest = {
233+
state.eventSink(SettingsUiEvent.OnUpdateButtonClick)
234+
},
235+
)
242236
}
243237
}
244238
}

feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ data class SettingsUiState(
1111
val isWithdrawBottomSheetVisible: Boolean = false,
1212
val isWithdrawConfirmed: Boolean = false,
1313
val latestVersion: String = "",
14+
val isUpdateAvailable: Boolean = false,
15+
val isOptionalUpdateDialogVisible: Boolean = false,
1416
val sideEffect: SettingsSideEffect? = null,
1517
val eventSink: (SettingsUiEvent) -> Unit,
1618
) : CircuitUiState
@@ -21,6 +23,8 @@ sealed interface SettingsSideEffect {
2123
val message: String,
2224
private val key: String = UUID.randomUUID().toString(),
2325
) : SettingsSideEffect
26+
27+
data object NavigateToPlayStore : SettingsSideEffect
2428
}
2529

2630
sealed interface SettingsUiEvent : CircuitUiEvent {
@@ -35,4 +39,7 @@ sealed interface SettingsUiEvent : CircuitUiEvent {
3539
data object OnWithdrawConfirmationToggled : SettingsUiEvent
3640
data object Logout : SettingsUiEvent
3741
data object Withdraw : SettingsUiEvent
42+
data object OnVersionClick : SettingsUiEvent
43+
data object OnOptionalUpdateDialogDismiss : SettingsUiEvent
44+
data object OnUpdateButtonClick : SettingsUiEvent
3845
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.ninecraft.booket.feature.settings.component
2+
3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.material3.Text
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.ui.Alignment
10+
import androidx.compose.ui.Modifier
11+
import com.ninecraft.booket.core.common.extensions.clickableSingle
12+
import com.ninecraft.booket.core.designsystem.ComponentPreview
13+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
14+
15+
@Composable
16+
internal fun SettingItem(
17+
title: String,
18+
modifier: Modifier = Modifier,
19+
isClickable: Boolean = true,
20+
onItemClick: () -> Unit = {},
21+
action: @Composable () -> Unit = {},
22+
description: @Composable () -> Unit = {},
23+
) {
24+
val combinedModifier = if (isClickable) {
25+
modifier
26+
.fillMaxWidth()
27+
.clickableSingle { onItemClick() }
28+
} else {
29+
modifier.fillMaxWidth()
30+
}
31+
32+
Column {
33+
Row(
34+
modifier = combinedModifier
35+
.padding(
36+
horizontal = ReedTheme.spacing.spacing5,
37+
vertical = ReedTheme.spacing.spacing4,
38+
),
39+
verticalAlignment = Alignment.CenterVertically,
40+
) {
41+
Column(
42+
modifier = Modifier.weight(1f),
43+
) {
44+
Text(
45+
text = title,
46+
style = ReedTheme.typography.body1Medium,
47+
color = ReedTheme.colors.contentPrimary,
48+
)
49+
description()
50+
}
51+
action()
52+
}
53+
}
54+
}
55+
56+
@ComponentPreview
57+
@Composable
58+
private fun SettingItemPreview() {
59+
ReedTheme {
60+
SettingItem(
61+
title = "로그아웃",
62+
)
63+
}
64+
}

feature/settings/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@
1515
<string name="settings_withdraw_action">탈퇴하기</string>
1616
<string name="oss_licenses_title">오픈소스 라이선스</string>
1717
<string name="latest_version">최신 버전 %1$s</string>
18+
<string name="settings_optional_update_title">최신 버전이 출시되었습니다.</string>
19+
<string name="settings_optional_update_message">최적의 사용 환경을 위해 업데이트해주세요.</string>
20+
<string name="settings_optional_update_button_text">업데이트하기</string>
1821
</resources>

0 commit comments

Comments
 (0)