-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 설정 화면 내 최신 버전 정보 조회 기능 구현 #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
13e95da
23cf844
248a00a
501a537
c5552a2
165c773
c8725a8
69f2ae6
6451482
4b2fd58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.ninecraft.booket.core.common.util | ||
|
|
||
| import com.orhanobut.logger.Logger | ||
|
|
||
| /** | ||
| * 두 버전을 비교하는 함수 | ||
| * | ||
| * @param version1 첫 번째 버전 (예: "1.2.3") | ||
| * @param version2 두 번째 버전 (예: "1.1.0") | ||
| * @return 양수면 version1 > version2, 음수면 version1 < version2, 0이면 같음 | ||
| * | ||
| * 버전 형식: "메이저.마이너.패치" (예: 1.2.3) | ||
| * 비교 순서: 메이저 → 마이너 → 패치 버전 순으로 비교 | ||
| */ | ||
| fun compareVersions(version1: String, version2: String): Int { | ||
| Logger.d("compareVersions: version1: $version1, version2: $version2") | ||
|
|
||
| if (!Regex("""^\d+\.\d+\.\d+$""").matches(version1)) return 0 | ||
| if (!Regex("""^\d+\.\d+\.\d+$""").matches(version2)) return 0 | ||
|
|
||
| val v1 = version1.split('.').map { it.toInt() } | ||
| val v2 = version2.split('.').map { it.toInt() } | ||
|
|
||
| // 메이저 버전 비교 | ||
| if (v1[0] != v2[0]) return v1[0] - v2[0] | ||
|
|
||
| // 마이너 버전 비교 | ||
| if (v1[1] != v2[1]) return v1[1] - v2[1] | ||
|
|
||
| // 패치 버전 비교 | ||
| return v1[2] - v2[2] | ||
| } | ||
|
|
||
| /** | ||
| * 현재 앱 버전이 최소 요구 버전보다 낮은지 확인하는 함수 | ||
| * | ||
| * @param currentVersion 현재 앱의 버전 (예: "1.0.0") | ||
| * @param minVersion 최소 요구 버전 (Firebase Remote Config에서 가져온 값) | ||
| * @return true면 강제 업데이트 필요 (현재 버전 < 최소 요구 버전), false면 업데이트 불필요 | ||
| */ | ||
| fun isUpdateRequired(currentVersion: String, minVersion: String): Boolean { | ||
| return compareVersions(currentVersion, minVersion) < 0 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.ninecraft.booket.core.data.api.repository | ||
|
|
||
| interface RemoteConfigRepository { | ||
| suspend fun getLatestVersion(): Result<String> | ||
| suspend fun shouldUpdate(): Result<Boolean> | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||
| package com.ninecraft.booket.core.data.impl.di | ||||||
|
|
||||||
| import com.google.firebase.Firebase | ||||||
| import com.google.firebase.remoteconfig.FirebaseRemoteConfig | ||||||
| import com.google.firebase.remoteconfig.remoteConfig | ||||||
| import com.google.firebase.remoteconfig.remoteConfigSettings | ||||||
| import com.ninecraft.booket.core.data.impl.BuildConfig | ||||||
| import dagger.Module | ||||||
| import dagger.Provides | ||||||
| import dagger.hilt.InstallIn | ||||||
| import dagger.hilt.components.SingletonComponent | ||||||
| import javax.inject.Singleton | ||||||
|
|
||||||
| @InstallIn(SingletonComponent::class) | ||||||
| @Module | ||||||
| internal object FirebaseModule { | ||||||
| @Singleton | ||||||
| @Provides | ||||||
| fun provideRemoteConfig(): FirebaseRemoteConfig { | ||||||
| return Firebase.remoteConfig.apply { | ||||||
| val configSettings by lazy { | ||||||
| remoteConfigSettings { | ||||||
| minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 60 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minimumFetchIntervalInSeconds 타입(Int→Long) 불일치 가능성 — L 접미사 필요
- minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 60
+ minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0L else 60L📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| } | ||||||
| } | ||||||
| setConfigSettingsAsync(configSettings) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package com.ninecraft.booket.core.data.impl.repository | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.google.firebase.remoteconfig.FirebaseRemoteConfig | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.google.firebase.remoteconfig.get | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.common.util.isUpdateRequired | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.ninecraft.booket.core.data.impl.BuildConfig | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.orhanobut.logger.Logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.coroutines.suspendCancellableCoroutine | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import javax.inject.Inject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlin.coroutines.resume | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class DefaultRemoteConfigRepository @Inject constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val remoteConfig: FirebaseRemoteConfig, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : RemoteConfigRepository { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| override suspend fun getLatestVersion(): Result<String> = suspendCancellableCoroutine { continuation -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remoteConfig.fetchAndActivate().addOnCompleteListener { task -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (task.isSuccessful) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val latestVersion = remoteConfig[KEY_LATEST_VERSION].asString() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Logger.d("LatestVersion: $latestVersion") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continuation.resume(Result.success(latestVersion)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Logger.e(task.exception, "getLatestVersion failed") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continuation.resume(Result.failure(task.exception ?: Exception("Unknown error"))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코루틴 취소 시 resume 중복 호출 가능성 — 안전 가드와 캐시 폴백 추가 필요 suspendCancellableCoroutine + addOnCompleteListener 조합에서는 코루틴이 취소된 뒤에도 리스너가 호출될 수 있어, continuation.resume을 호출하면 IllegalStateException이 발생할 수 있습니다. 또한 fetch 실패 시 활성화된 기존 값으로의 폴백이 있으면 UX가 좋아집니다. 다음과 같이 isActive 가드와 캐시 폴백을 추가해 주세요: - override suspend fun getLatestVersion(): Result<String> = suspendCancellableCoroutine { continuation ->
- remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
- if (task.isSuccessful) {
- val latestVersion = remoteConfig[KEY_LATEST_VERSION].asString()
- Logger.d("LatestVersion: $latestVersion")
- continuation.resume(Result.success(latestVersion))
- } else {
- Logger.e(task.exception, "getLatestVersion failed")
- continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
- }
- }
- }
+ override suspend fun getLatestVersion(): Result<String> = suspendCancellableCoroutine { continuation ->
+ remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
+ if (!continuation.isActive) return@addOnCompleteListener
+ if (task.isSuccessful) {
+ val latestVersion = remoteConfig[KEY_LATEST_VERSION]
+ .asString()
+ .takeIf { it.isNotBlank() } ?: "Unknown"
+ Logger.d("LatestVersion: $latestVersion")
+ continuation.resume(Result.success(latestVersion))
+ } else {
+ Logger.e(task.exception, "getLatestVersion failed")
+ // fetch 실패 시에도 마지막 활성화된 값을 시도
+ val cached = remoteConfig[KEY_LATEST_VERSION].asString()
+ if (cached.isNotBlank()) {
+ continuation.resume(Result.success(cached))
+ } else {
+ continuation.resume(Result.failure(task.exception ?: Exception("Unknown error")))
+ }
+ }
+ }
+ }추가로, 필요하다면 continuation.invokeOnCancellation { /* no-op */ }를 등록해 의도를 더 분명히 할 수 있습니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| override suspend fun shouldUpdate(): Result<Boolean> = suspendCancellableCoroutine { continuation -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remoteConfig.fetchAndActivate().addOnCompleteListener { task -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (task.isSuccessful) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val minVersion = remoteConfig[KEY_MIN_VERSION].asString() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val currentVersion = BuildConfig.APP_VERSION | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continuation.resume(Result.success(isUpdateRequired(currentVersion, minVersion))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Logger.e(task.exception, "shouldUpdate: getMinVersion failed") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continuation.resume(Result.failure(task.exception ?: Exception("Unknown error"))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| companion object { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private const val KEY_LATEST_VERSION = "LatestVersion" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private const val KEY_MIN_VERSION = "MinVersion" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <string name="no_more_result">더 이상 결과가 없습니다</string> | ||
| <string name="retry">다시 시도</string> | ||
| <string name="retry">다시 시도하기</string> | ||
| <string name="network_error_message">네트워크 연결이 불안정합니다.\n인터넷 연결을 확인해주세요</string> | ||
| <string name="server_error_message">알 수 없는 문제가 발생했어요.\n다시 시도해주세요</string> | ||
| </resources> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Play 스토어 미탑재 기기/에뮬레이터에서 ActivityNotFoundException로 크래시 가능 — 핸들러 확인, 웹 URL 폴백, NEW_TASK 플래그 추가 필요
market 스킴을 처리할 앱이 없는 환경(일부 AOSP 기기/중국 ROM/에뮬레이터 등)에서 startActivity가 예외를 던질 수 있습니다. 또한 비-Activity Context에서 호출될 수 있는 확장 함수 특성상 NEW_TASK 플래그가 필요합니다. 아래와 같이 안전하게 처리해 주세요.
위 변경에 따라 필요한 import(파일 상단)에 아래를 추가해 주세요.
📝 Committable suggestion
🤖 Prompt for AI Agents