Conversation
Walkthrough앱 버전 확인 기능을 구현하는 완전한 계층을 추가합니다. 원격 버전 서비스에서 최신 버전을 조회하고, MainViewModel에서 설치된 버전과 비교하여 필수/선택적 업데이트 다이얼로그를 표시합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant MainActivity
participant MainViewModel
participant GetAppVersionUseCase
participant VersionRepository
participant VersionRemoteDataSource
participant VersionService
MainActivity->>MainViewModel: initialization
activate MainViewModel
MainViewModel->>MainViewModel: checkAppVersion()
MainViewModel->>GetAppVersionUseCase: invoke()
activate GetAppVersionUseCase
GetAppVersionUseCase->>VersionRepository: getVersion()
activate VersionRepository
VersionRepository->>VersionRemoteDataSource: getVersion()
activate VersionRemoteDataSource
VersionRemoteDataSource->>VersionService: getVersion()
activate VersionService
VersionService-->>VersionRemoteDataSource: BaseResponse<String>
deactivate VersionService
VersionRemoteDataSource-->>VersionRepository: Result<String>
deactivate VersionRemoteDataSource
VersionRepository-->>GetAppVersionUseCase: Result<String>
deactivate VersionRepository
deactivate GetAppVersionUseCase
alt Compare versions
MainViewModel->>MainViewModel: compareVersions(installed, latest)
alt Required update
MainViewModel->>MainViewModel: emit ShowUpdateRequiredDialog
else Optional update
MainViewModel->>MainViewModel: emit ShowUpdateOptionalDialog
else No update needed
MainViewModel->>MainViewModel: log success
end
else Fetch/Parse failed
MainViewModel->>MainViewModel: log error
end
deactivate MainViewModel
MainActivity->>MainActivity: collect sideEffect
activate MainActivity
alt Show update dialog
MainActivity->>MainActivity: openPlayStoreForUpdate()
end
deactivate MainActivity
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 근거:
Possibly related issues
Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (12)
app/src/main/java/com/depromeet/team6/data/dataremote/datasource/VersionRemoteDataSource.kt(1 hunks)app/src/main/java/com/depromeet/team6/data/dataremote/service/VersionService.kt(1 hunks)app/src/main/java/com/depromeet/team6/data/repositoryimpl/VersionRepositoryImpl.kt(1 hunks)app/src/main/java/com/depromeet/team6/di/RepositoryModule.kt(3 hunks)app/src/main/java/com/depromeet/team6/di/ServiceModule.kt(2 hunks)app/src/main/java/com/depromeet/team6/domain/ErrorConstraints.kt(1 hunks)app/src/main/java/com/depromeet/team6/domain/repository/VersionRepository.kt(1 hunks)app/src/main/java/com/depromeet/team6/domain/usecase/GetAppVersionUseCase.kt(1 hunks)app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt(5 hunks)app/src/main/java/com/depromeet/team6/presentation/ui/main/MainContract.kt(1 hunks)app/src/main/java/com/depromeet/team6/presentation/ui/main/MainViewModel.kt(4 hunks)app/src/main/java/com/depromeet/team6/presentation/util/Constraints.kt(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/depromeet/team6/presentation/ui/main/MainViewModel.kt (1)
app/src/main/java/com/depromeet/team6/presentation/util/base/BaseViewModel.kt (1)
setSideEffect(103-105)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: CI
🔇 Additional comments (9)
app/src/main/java/com/depromeet/team6/domain/ErrorConstraints.kt (1)
65-66: 에러 메시지 상수 추가 확인플랫폼 불일치 에러 메시지 상수가 정상적으로 추가되었습니다. 이 상수는 GetAppVersionUseCase에서 APP_001 에러 코드 매핑에 사용됩니다.
app/src/main/java/com/depromeet/team6/presentation/ui/main/MainContract.kt (1)
14-17: 업데이트 다이얼로그 사이드 이펙트 추가 확인필수/권장 업데이트 다이얼로그를 위한 사이드 이펙트가 적절하게 정의되었습니다. Sealed interface 구조를 통해 타입 안전성이 보장됩니다.
app/src/main/java/com/depromeet/team6/di/ServiceModule.kt (1)
73-76: VersionService DI 바인딩 확인VersionService의 의존성 주입 설정이 기존 패턴과 일관성 있게 구현되었습니다.
app/src/main/java/com/depromeet/team6/domain/repository/VersionRepository.kt (1)
1-5: VersionRepository 인터페이스 확인깔끔한 repository 인터페이스 정의입니다.
Result<String>을 사용하여 에러 핸들링을 적절하게 처리하고 있습니다.app/src/main/java/com/depromeet/team6/di/RepositoryModule.kt (1)
68-70: VersionRepository DI 바인딩 확인VersionRepository의 의존성 주입 설정이 기존 패턴과 일관성 있게 구현되었습니다.
app/src/main/java/com/depromeet/team6/data/dataremote/datasource/VersionRemoteDataSource.kt (1)
1-11: VersionRemoteDataSource 구현 확인데이터 소스 구현이 단순하고 명확합니다.
parse()확장 함수를 통해BaseResponse를Result로 변환하는 구조가 적절합니다.app/src/main/java/com/depromeet/team6/data/dataremote/service/VersionService.kt (1)
1-10: VersionService API 정의 확인Retrofit 서비스 인터페이스가 표준 패턴으로 잘 정의되었습니다. 버전 문자열의 형식 검증 및 비교 로직은 도메인 계층(GetAppVersionUseCase)에서 처리되어야 하므로, 이 계층에서는 단순히 API 응답을 반환하는 것이 적절합니다.
버전 비교 로직이 구현된 GetAppVersionUseCase와 MainViewModel 파일이 리뷰에 포함되지 않았습니다. 다음 사항들이 해당 파일들에서 올바르게 처리되는지 확인이 필요합니다:
- 버전 문자열 파싱 (예: "1.2.3" 형식)
- 의미론적 버전 비교 (semantic versioning)
- 필수/권장 업데이트 판단 로직
- 에러 케이스 처리
app/src/main/java/com/depromeet/team6/presentation/util/Constraints.kt (1)
27-31: 빌드 설정과 일치하므로 조치 불필요검증 결과,
app/build.gradle.kts(39줄)의applicationId = "com.depromeet.team6"과 하드코딩된 값이 정확히 일치합니다. 또한 빌드 variant나 flavor가 정의되어 있지 않으므로 패키지명이 변경될 가능성이 없습니다. 현재 코드는 빌드 설정과 동기화되어 있으며 기능상 문제가 없습니다.app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt (1)
61-82: 웹 및 코드베이스 검색을 통해 이 제안을 검증하겠습니다.<!-- [scratchpad_update]
[task review_main_1/1 verify_intent_package_2/2][observations_update]
- Context.getPackageName() returns the application ID at runtime (Android docs official)
- BuildConfig.APPLICATION_ID is compile-time constant derived from gradle applicationId
- In normal cases, they are identical; the app's applicationId is what matters for Play Store
- packageName reference in Activity context refers to this applicationId
- Hardcoding constants that should match applicationId is a legitimate config drift risk
[analysis_update]
- The review suggestion is fundamentally correct: using context packageName vs hardcoded constant reduces config risk
- However, proposed diff has inconsistency: fallback URL is hardcoded string instead of constant
- Need to verify codebase for AppConstants content and whether packageName is in scope
[pending]
- Check if AppConstants constants exist and their values
- Verify if packageName is accessible in MainActivity.openPlayStoreForUpdate context
- Determine if fallback URL should remain constant
[scratchpad_end] -->
Now let me search the codebase for AppConstants and the actual context:<!-- [scratchpad_final]
[task review_main_1/1 verification_complete][final_observations]
- Context.getPackageName() is available in Activity and returns the applicationId at runtime
- It's a standard Android API call, fully accessible from MainActivity methods
- Using runtime packageName instead of hardcoded constant is a valid best practice
- Addresses real config mismatch risk when constants drift from actual app ID
[final_analysis]
ISSUE FOUND: The proposed diff is correct in concept but INCONSISTENT.
- Primary change (market URI): good - uses runtime packageName
- Fallback URL: problematic - hardcodes "https://play.google.com/store/apps/details?id=$packageName" as a string literal instead of using a constant
This inconsistency violates configuration management principles. If we need dynamic packageName, we should also avoid hardcoding the Play Store URL template.
VERDICT:
- The concern is VALID and significant (prevents configuration mismatches)
- But the proposed solution is INCOMPLETE/INCONSISTENT (mixes hardcoded URL with dynamic ID)
- Classification: The original review suggests a valid refactor but needs refinement
- The fallback URL should either be a constant or constructed dynamically
[scratchpad_end] -->
재검토 완료. 원본 리뷰 의견에는 타당한 기초가 있지만 제안된 수정안이 일관성이 없습니다.
<anit:cite index="3-1,7-14,16-7">Context.getPackageName()은 런타임에 애플리케이션 ID를 반환합니다</cit:cite>. 하드코딩된 상수 대신 런타임 packageName을 사용하는 것은 배포 식별자 변경 시 설정 불일치를 방지하는 정당한 아키텍처 개선입니다.
그러나 제안된 diff에는 일관성 문제가 있습니다:
- ✅ 마켓 URI:
AppConstants.PLAY_STORE_PACKAGE_NAME→$packageName(개선)- ❌ 폴백 URL:
AppConstants.PLAY_STORE_URL→ 하드코딩된 문자열"https://play.google.com/store/apps/details?id=$packageName"(문제)폴백 URL을 문자열로 하드코딩하는 것은 원래 상수 사용으로 인한 설정 관리 문제를 완전히 해결하지 못합니다. 두 URI 모두 일관되게 처리해야 합니다:
권장 개선:
- 두 URL을 모두 런타임으로 동적 구성하거나
- 두 URL을 모두 AppConstants에서 유지하되, packageName을 상수로 분리하여 관리하거나
- PlayStoreUrl 상수를 유지하되 packageName 부분만 런타임 변수로 구성
마켓 URI만 수정하고 폴백 URL을 하드코딩하면 원래 의도한 설정 일관성을 부분적으로만 달성합니다.
| override suspend fun getVersion(): Result<String> = runCatching { | ||
| versionRemoteDataSource.getVersion().toString() | ||
| } |
There was a problem hiding this comment.
Result를 문자열화하는 치명적 버그
versionRemoteDataSource.getVersion()이 이미 Result<String>을 반환하는데, 여기에 toString()을 호출해 "Success(...)"/"Failure(...)" 문자열로 만들어 버립니다. 상위(ViewModel)에서 문자열 트리밍 우회가 필요해지고, 실패/예외가 정상 흐름으로 오인됩니다.
다음과 같이 그대로 위임하세요:
- override suspend fun getVersion(): Result<String> = runCatching {
- versionRemoteDataSource.getVersion().toString()
- }
+ override suspend fun getVersion(): Result<String> =
+ versionRemoteDataSource.getVersion()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| override suspend fun getVersion(): Result<String> = runCatching { | |
| versionRemoteDataSource.getVersion().toString() | |
| } | |
| override suspend fun getVersion(): Result<String> = | |
| versionRemoteDataSource.getVersion() |
🤖 Prompt for AI Agents
In
app/src/main/java/com/depromeet/team6/data/repositoryimpl/VersionRepositoryImpl.kt
around lines 11 to 13, the code calls toString() on
versionRemoteDataSource.getVersion() which already returns Result<String>,
converting it into "Success(...)" / "Failure(...)" text; remove the toString()
and simply return/delegate the Result from versionRemoteDataSource.getVersion()
(i.e., replace the current runCatching/toString logic by directly returning the
Result so success/failure semantics are preserved).
| LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { | ||
| viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) | ||
| .collect { onboardingSideEffect -> | ||
| when (onboardingSideEffect) { | ||
| is MainContract.MainSideEffect.ShowUpdateRequiredDialog -> { | ||
| dialogController.showAtchaOneButtonAlert( | ||
| message = "더 좋아진 앗차를 사용하기 위해\n업데이트가 필요해요", | ||
| onConfirm = { openPlayStoreForUpdate() }, | ||
| confirmButtonText = "업데이트 하기" | ||
| ) | ||
| } | ||
|
|
||
| is MainContract.MainSideEffect.ShowUpdateOptionalDialog -> { | ||
| dialogController.showAtchaTwoButtonAlert( | ||
| message = "더 좋아진 앗차를 사용하기 위해\n업데이트가 필요해요", | ||
| onConfirm = { openPlayStoreForUpdate() }, | ||
| confirmButtonText = "업데이트 하기", | ||
| closeButtonText = "닫기" | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
초기 수집 전 SideEffect 유실 위험
ViewModel에서 init 시점에 emit하면(라인 54-58) 첫 Compose 수집이 붙기 전에 이벤트가 흘러 유실될 수 있습니다. 필수/권장 업데이트 다이얼로그가 간헐적으로 뜨지 않는 증상으로 이어집니다.
- BaseViewModel의 sideEffect가
MutableSharedFlow(replay>0 또는 충분한 extraBufferCapacity)인지 확인 필요. - 아니라면 replay=1(+DROP_OLDEST)로 보강하거나, 수집 시작 이후 트리거하도록 변경하세요.
검증 스크립트:
🏁 Script executed:
#!/bin/bash
# sideEffect 공유플로우 설정 확인
rg -nP 'MutableSharedFlow<.+>\s*\(' -C2
rg -nP '_sideEffect' -C3Length of output: 5920
BaseViewModel의 sideEffect MutableSharedFlow에 replay 메커니즘 필요
기본 MutableSharedFlow()는 replay 캐시와 버퍼가 없어서, 구독자가 없을 때 emit은 즉시 반환되고 이벤트가 손실됩니다. MainActivity의 LaunchedEffect(lines 140-162)가 수집을 시작하기 전에 ViewModel이 sideEffect를 발행하면 메시지가 도착하지 않습니다.
수정 사항:
BaseViewModel.kt 라인 35의 sideEffect 정의를 다음과 같이 변경하세요:
private val _sideEffect: MutableSharedFlow<UiSideEffect> =
MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)이렇게 구성하면 replay=1로 최신 값을 새로운 구독자에게 재발행하고, 버퍼 오버플로우 시 DROP_OLDEST로 관리되어 필수/선택 업데이트 다이얼로그가 안정적으로 표시됩니다.
🏁 Script executed:
#!/bin/bash
# MainViewModel 초기화 시점의 sideEffect 발행 확인
rg -nP 'class MainViewModel|init\s*\{' -A20 app/src/main/java/com/depromeet/team6/presentation/ui/main/MainViewModel.kt
# MainViewModel의 전체 init 블록 확인
ast-grep --pattern $'class MainViewModel {
$$$
init {
$$$
}
$$$
}'Length of output: 1728
🏁 Script executed:
#!/bin/bash
# MainViewModel의 init 블록에서 sideEffect 발행 확인
fd MainViewModel.kt -x cat {} | head -100Length of output: 3731
🏁 Script executed:
#!/bin/bash
# checkAppVersion() 메서드 구현 확인 - UPDATE 다이얼로그 관련 sideEffect 발행 확인
fd MainViewModel.kt -x cat {} | grep -A 30 "fun checkAppVersion"
# sideEffect.emit 또는 setSideEffect 호출 확인
fd MainViewModel.kt -x cat {} | grep -n "setSideEffect\|sideEffect.emit"Length of output: 1382
🏁 Script executed:
#!/bin/bash
# checkAppVersion() 메서드 전체 구현과 sideEffect 발행 패턴
rg -nP 'checkAppVersion|ShowUpdateRequired|ShowUpdateOptional' -A 15 app/src/main/java/com/depromeet/team6/presentation/ui/main/
# MainViewModel 전체 파일에서 sideEffect 발행 라인
rg -n 'setSideEffect|\.emit' app/src/main/java/com/depromeet/team6/presentation/ui/main/MainViewModel.ktLength of output: 9561
BaseViewModel의 sideEffect MutableSharedFlow에 replay 메커니즘 필수
검증 완료: race condition이 실제로 존재합니다.
MainViewModel의 checkAppVersion() (라인 148-176)은 viewModelScope.launch로 비동기 실행되며, 내부에서 ShowUpdateRequiredDialog/ShowUpdateOptionalDialog sideEffect를 발행합니다. 이 발행이 MainActivity의 LaunchedEffect (라인 140-162) 수집 시작보다 먼저 완료되면, replay=0인 MutableSharedFlow는 이벤트를 버립니다.
결과: 앱 버전 확인 후 필수/선택 업데이트 다이얼로그가 간헐적으로 나타나지 않습니다 (타이밍 경쟁에 따라).
수정 필수 사항:
BaseViewModel.kt 라인 35를 다음과 같이 변경하세요:
private val _sideEffect: MutableSharedFlow<UiSideEffect> =
MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)이렇게 구성하면 새로운 구독자가 attach될 때 최신 sideEffect 하나를 재발행하여 어떤 타이밍에서도 업데이트 다이얼로그가 안정적으로 표시됩니다.
🤖 Prompt for AI Agents
In app/src/main/java/com/depromeet/team6/presentation/base/BaseViewModel.kt
around line 35, the MutableSharedFlow for sideEffect must be changed to replay
the latest event so late subscribers (like MainActivity's LaunchedEffect) don’t
miss it; modify the _sideEffect declaration to use MutableSharedFlow with replay
= 1 and onBufferOverflow = BufferOverflow.DROP_OLDEST, and add the necessary
import for BufferOverflow if not present.
| init { | ||
| loadInitialData() | ||
| fetchFcmToken() | ||
| checkAppVersion() | ||
| } |
There was a problem hiding this comment.
init 시점 emit로 인한 다이얼로그 미노출 가능성
checkAppVersion()이 init에서 즉시 실행되어 SideEffect가 UI 수집 이전에 방출될 수 있습니다. 수집 측(액티비티)과의 경합으로 업데이트 다이얼로그가 누락될 수 있습니다.
대안:
- BaseViewModel의 sideEffect를
MutableSharedFlow(replay=1, extraBufferCapacity>=1)로 구성. - 또는 UI 수집이 시작된 후 트리거하도록 이벤트로 노출하고 UI에서 요청.
🤖 Prompt for AI Agents
In app/src/main/java/com/depromeet/team6/presentation/ui/main/MainViewModel.kt
around lines 54 to 58, checkAppVersion() is invoked in init which can emit a
SideEffect before the Activity/Fragment starts collecting, causing the update
dialog to be missed; either (A) change the BaseViewModel sideEffect to a
MutableSharedFlow with replay=1 and extraBufferCapacity>=1 so late collectors
still receive the event, or (B) remove the immediate call from init and expose a
public triggerCheckAppVersion() (or event) that the UI invokes after it starts
collecting; implement one of these fixes and update usages/tests accordingly.
| private fun checkAppVersion() { | ||
| val installedVersion = getCurrentVersionName()?.cleanVersion() ?: "0.0.0" | ||
| viewModelScope.launch { | ||
| getAppVersionUseCase().onSuccess { appVersion -> | ||
| val latestVersion = appVersion.removePrefix("Success(") | ||
| .removePrefix("v") | ||
| .removeSuffix(")") | ||
| .cleanVersion() | ||
|
|
||
| Timber.d("latestVersion: $latestVersion, installedVersion: $installedVersion") | ||
|
|
||
| when (compareVersions(installedVersion, latestVersion)) { | ||
| VersionResult.UPDATE_REQUIRED -> { | ||
| setSideEffect(MainContract.MainSideEffect.ShowUpdateRequiredDialog) | ||
| } | ||
|
|
||
| VersionResult.UPDATE_OPTIONAL -> { | ||
| setSideEffect(MainContract.MainSideEffect.ShowUpdateOptionalDialog) | ||
| } | ||
|
|
||
| VersionResult.UP_TO_DATE -> { | ||
| Timber.d("최신 버전입니다") | ||
| } | ||
| } | ||
| }.onFailure { | ||
| Timber.e(it, "앱 버전 확인 실패") | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
Result를 문자열로 파싱하는 임시 처리 제거 필요
removePrefix("Success(")...removeSuffix(")")는 Result.toString()에 의존한 취약한 우회입니다. 이는 저장소 계층이 결과를 문자열화하고 있음을 시사하며(아래 Repository 코멘트 참조), 오류 처리/형식 안정성 모두 깨집니다. 저장소를 바로잡은 뒤 여기서는 순수 버전 문자열만 정규화하세요.
다음 패치(Repository 수정이 선행되어 onSuccess { appVersion: String }가 보장된다는 가정):
- getAppVersionUseCase().onSuccess { appVersion ->
- val latestVersion = appVersion.removePrefix("Success(")
- .removePrefix("v")
- .removeSuffix(")")
- .cleanVersion()
+ getAppVersionUseCase().onSuccess { appVersion ->
+ val latestVersion = appVersion
+ .removePrefix("v")
+ .cleanVersion()
Timber.d("latestVersion: $latestVersion, installedVersion: $installedVersion")
when (compareVersions(installedVersion, latestVersion)) {네트워크 호출이 Main 스레드를 점유하지 않는지 확인해 주세요(UseCase/Repository 내부에서 IO 디스패처 사용). 빠른 확인 스크립트:
🏁 Script executed:
#!/bin/bash
# NetworkRequestUseCase/Repository에서 IO 디스패처 사용 여부 확인
rg -nP 'class\s+NetworkRequestUseCase\b' -C3
rg -nP 'withContext\s*\(\s*Dispatchers\.IO' -C2 --glob '!**/test/**'Length of output: 2676
---
🏁 Script executed:
```shell
#!/bin/bash
# Find getAppVersionUseCase implementation
rg -nP 'getAppVersionUseCase|class.*AppVersionUseCase' -A 10 --type kotlin
Length of output: 4028
🏁 Script executed:
#!/bin/bash
# Find compareVersions function implementation
rg -nP 'fun compareVersions|compareVersions\s*\(' -B 2 -A 10 --type kotlinLength of output: 3312
🏁 Script executed:
#!/bin/bash
# Find init block in MainViewModel
rg -nP 'init\s*\{' -A 15 app/src/main/java/com/depromeet/team6/presentation/ui/main/MainViewModel.ktLength of output: 492
🏁 Script executed:
#!/bin/bash
# Search for Repository version fetching implementation
rg -nP 'class.*VersionRepository|getAppVersion' -A 8 --type kotlin | head -60Length of output: 3192
🏁 Script executed:
#!/bin/bash
# Check if Repository is returning Result as string
rg -nP 'Result<.*String>|toString|Success\(' -B 3 -A 3 data/dataremote --type kotlin | head -80Length of output: 55
저장소 계층의 .toString() 호출이 원인: 즉시 수정 필요
검증 결과, 세 가지 중대 결함을 확인했습니다:
-
VersionRepositoryImpl.getVersion()에서 원본 데이터 손상 (app/src/main/java/com/depromeet/team6/data/repositoryimpl/VersionRepositoryImpl.kt:12)override suspend fun getVersion(): Result<String> = runCatching { versionRemoteDataSource.getVersion().toString() // ← 문제: 객체를 문자열화함 }
이 줄이
versionRemoteDataSource응답을.toString()으로 변환하고 있으므로, MainViewModel에서removePrefix("Success(")같은 우회가 필요한 이유입니다. -
compareVersions()버전 비교 로직 오류 (MainViewModel.kt:189)return when { a1 < a2 || b1 < b2 -> VersionResult.UPDATE_REQUIRED // 오류!
예: installed=2.0.0, latest=1.5.0일 때
b1(0) < b2(5)→ UPDATE_REQUIRED 반환 (잘못됨)올바른 로직:
a1 < a2 || (a1 == a2 && b1 < b2) || (a1 == a2 && b1 == b2 && c1 < c2) -
init()에서 네트워크 호출 시 IO 디스패처 미보장 (MainViewModel.kt:54-57)
init블록에서checkAppVersion()을 호출하면 Main 스레드에서 네트워크 작업 시작viewModelScope.launch { getAppVersionUseCase()... }내에withContext(Dispatchers.IO)없음
조치:
- 우선:
VersionRepositoryImpl.getVersion()수정 — 응답을 직접 반환하도록 변경 (.toString()제거) - 필수:
compareVersions()비교 로직 수정 — 세 자리 모두 올바른 순서 검증 - 권장:
checkAppVersion()내getAppVersionUseCase()호출을withContext(Dispatchers.IO) { ... }로 감싸기
| private fun compareVersions(installed: String, latest: String): VersionResult { | ||
| val installedParts = installed.split(".").map { it.toIntOrNull() ?: 0 } | ||
| val latestParts = latest.split(".").map { it.toIntOrNull() ?: 0 } | ||
|
|
||
| val (a1, b1, c1) = installedParts + List(3 - installedParts.size) { 0 } | ||
| val (a2, b2, c2) = latestParts + List(3 - latestParts.size) { 0 } | ||
|
|
||
| return when { | ||
| a1 < a2 || b1 < b2 -> VersionResult.UPDATE_REQUIRED | ||
| c1 < c2 -> VersionResult.UPDATE_OPTIONAL | ||
| else -> VersionResult.UP_TO_DATE | ||
| } | ||
| } |
There was a problem hiding this comment.
버전 비교 로직 오류(세그먼트 음수 패딩/OR 비교)
- 세그먼트가 3개 초과 시
List(3 - size)로 예외 가능. a1 < a2 || b1 < b2는 사전식 비교 규칙을 위반(메이저가 더 큰데 마이너가 더 작으면 잘못된 REQUIRED 판정).
다세그먼트 안전 + 메이저/마이너(필수), 패치+(권장) 기준의 사전식 비교로 교체:
- private fun compareVersions(installed: String, latest: String): VersionResult {
- val installedParts = installed.split(".").map { it.toIntOrNull() ?: 0 }
- val latestParts = latest.split(".").map { it.toIntOrNull() ?: 0 }
-
- val (a1, b1, c1) = installedParts + List(3 - installedParts.size) { 0 }
- val (a2, b2, c2) = latestParts + List(3 - latestParts.size) { 0 }
-
- return when {
- a1 < a2 || b1 < b2 -> VersionResult.UPDATE_REQUIRED
- c1 < c2 -> VersionResult.UPDATE_OPTIONAL
- else -> VersionResult.UP_TO_DATE
- }
- }
+ private fun compareVersions(installed: String, latest: String): VersionResult {
+ val inst = installed.split(".").map { it.toIntOrNull() ?: 0 }
+ val lat = latest.split(".").map { it.toIntOrNull() ?: 0 }
+ val max = maxOf(inst.size, lat.size)
+ for (i in 0 until max) {
+ val a = inst.getOrElse(i) { 0 }
+ val b = lat.getOrElse(i) { 0 }
+ if (a == b) continue
+ return if (a < b) {
+ if (i <= 1) VersionResult.UPDATE_REQUIRED else VersionResult.UPDATE_OPTIONAL
+ } else {
+ VersionResult.UP_TO_DATE
+ }
+ }
+ return VersionResult.UP_TO_DATE
+ }🤖 Prompt for AI Agents
In app/src/main/java/com/depromeet/team6/presentation/ui/main/MainViewModel.kt
around lines 181-193, the version comparison mishandles inputs with more than 3
segments (causing negative padding) and uses an incorrect OR comparison that
breaks lexicographic rules; fix by normalizing both version lists to exactly
three numeric segments by converting to ints, truncating extras and padding
missing segments with zeros, then perform proper lexicographic comparison: first
compare major (if unequal return REQUIRED/UP_TO_DATE), then minor (if unequal
return REQUIRED/UP_TO_DATE), then compare patch to decide OPTIONAL vs
UP_TO_DATE; ensure toIntOrNull fallback to 0 is preserved for non-numeric
segments.
#️⃣연관된 이슈
📝작업 내용
PR 발행 전 체크 리스트
스크린샷 (선택)
필수
Screen_recording_20251021_190210.mp4
권장
Screen_recording_20251021_190304.mp4
💬리뷰 요구사항(선택)
리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요
Summary by CodeRabbit
릴리스 노트