Skip to content

[#242] 업데이트 다이얼로그 로직 구현#243

Merged
hyuns66 merged 2 commits intodevelopfrom
feat/update-dialog
Oct 23, 2025
Merged

[#242] 업데이트 다이얼로그 로직 구현#243
hyuns66 merged 2 commits intodevelopfrom
feat/update-dialog

Conversation

@t1nm1ksun
Copy link
Member

@t1nm1ksun t1nm1ksun commented Oct 21, 2025

#️⃣연관된 이슈

📝작업 내용

  • 설치되어있는 앱 버전과 최신버전을 비교해 업데이트 다이얼로그를 띄웁니다
  • 필수, 권장 분기처리

PR 발행 전 체크 리스트

  • 발행자 확인
  • 프로젝트 설정 확인
  • 라벨 확인
  • 코드 린트 확인

스크린샷 (선택)

필수

Screen_recording_20251021_190210.mp4

권장

Screen_recording_20251021_190304.mp4

💬리뷰 요구사항(선택)

  • 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

Summary by CodeRabbit

릴리스 노트

  • New Features
    • 앱 버전 확인 기능 추가
    • 필수 업데이트 및 선택적 업데이트 알림 기능 구현
    • 플레이스토어로 직접 연결되는 업데이트 안내 제공

@t1nm1ksun t1nm1ksun self-assigned this Oct 21, 2025
@t1nm1ksun t1nm1ksun added the feat Feature label Oct 21, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 21, 2025

Walkthrough

앱 버전 확인 기능을 구현하는 완전한 계층을 추가합니다. 원격 버전 서비스에서 최신 버전을 조회하고, MainViewModel에서 설치된 버전과 비교하여 필수/선택적 업데이트 다이얼로그를 표시합니다.

Changes

Cohort / File(s) Summary
Remote Data & Service Layer
app/src/main/java/com/depromeet/team6/data/dataremote/datasource/VersionRemoteDataSource.kt, app/src/main/java/com/depromeet/team6/data/dataremote/service/VersionService.kt
새로운 Retrofit 서비스 인터페이스 VersionService와 VersionRemoteDataSource 클래스를 추가하여 원격 버전 엔드포인트에 접근
Repository Layer
app/src/main/java/com/depromeet/team6/domain/repository/VersionRepository.kt, app/src/main/java/com/depromeet/team6/data/repositoryimpl/VersionRepositoryImpl.kt
VersionRepository 인터페이스와 이를 구현하는 VersionRepositoryImpl을 정의하여 원격 데이터 소스를 추상화
Dependency Injection
app/src/main/java/com/depromeet/team6/di/RepositoryModule.kt, app/src/main/java/com/depromeet/team6/di/ServiceModule.kt
VersionRepository와 VersionService를 DI 컨테이너에 바인딩
Domain Layer
app/src/main/java/com/depromeet/team6/domain/usecase/GetAppVersionUseCase.kt, app/src/main/java/com/depromeet/team6/domain/ErrorConstraints.kt
GetAppVersionUseCase 유스케이스를 추가하고 API 오류 코드 APP_001을 처리하기 위한 에러 상수 추가
UI & ViewModel
app/src/main/java/com/depromeet/team6/presentation/ui/main/MainViewModel.kt, app/src/main/java/com/depromeet/team6/presentation/ui/main/MainActivity.kt, app/src/main/java/com/depromeet/team6/presentation/ui/main/MainContract.kt
버전 비교 로직, 업데이트 필요 여부 판단, 사이드이펙트 발생 및 UI 다이얼로그 표시 기능 추가
Constants
app/src/main/java/com/depromeet/team6/presentation/util/Constraints.kt
Play Store 패키지명과 URL 상수를 AppConstants에 추가

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

근거:

  • 긍정적 요소: DI 바인딩, 저장소 계층, 서비스 정의가 명확한 패턴을 따르며 반복적이어서 검토가 빠름
  • 복잡성 요소: MainViewModel의 compareVersions() 버전 비교 로직과 checkAppVersion() 흐름은 세밀한 검토 필요 (문자열 정규화, 비교 알고리즘 정확성)
  • scope: 8개 파일에 걸쳐 있으나 계층 간 책임이 명확하게 분리됨

Possibly related issues

Possibly related PRs

  • [#227] OneButtonDialog 구현 #229: 이 PR의 새로운 업데이트 관련 사이드이펙트(ShowUpdateRequiredDialog, ShowUpdateOptionalDialog)와 MainActivity의 다이얼로그 사용은 해당 PR에서 도입된 다이얼로그 API(OneButton/TwoButton 컴포넌트, DialogController)에 직접 의존합니다.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "[#242] 업데이트 다이얼로그 로직 구현"은 이 PR의 핵심 기능을 명확하고 구체적으로 설명합니다. 변경사항 분석에서 확인할 수 있듯이, 이 PR은 앱의 설치 버전과 최신 버전을 비교하여 필수/권장 업데이트 구분 처리와 업데이트 다이얼로그 표시 로직을 구현합니다. 제목은 이러한 주요 변경사항을 정확히 반영하며, 간결하고 명확합니다. 이슈 번호 #242를 포함하여 추적성도 우수합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/update-dialog

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]
coderabbitai bot previously requested changes Oct 21, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2c181 and 55c3915.

📒 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() 확장 함수를 통해 BaseResponseResult로 변환하는 구조가 적절합니다.

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.

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 모두 일관되게 처리해야 합니다:

권장 개선:

  1. 두 URL을 모두 런타임으로 동적 구성하거나
  2. 두 URL을 모두 AppConstants에서 유지하되, packageName을 상수로 분리하여 관리하거나
  3. PlayStoreUrl 상수를 유지하되 packageName 부분만 런타임 변수로 구성

마켓 URI만 수정하고 폴백 URL을 하드코딩하면 원래 의도한 설정 일관성을 부분적으로만 달성합니다.

Comment on lines +11 to +13
override suspend fun getVersion(): Result<String> = runCatching {
versionRemoteDataSource.getVersion().toString()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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).

Comment on lines +140 to +162
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 = "닫기"
)
}
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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' -C3

Length 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 -100

Length 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.kt

Length 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.

Comment on lines 54 to 58
init {
loadInitialData()
fetchFcmToken()
checkAppVersion()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +148 to +176
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, "앱 버전 확인 실패")
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 kotlin

Length 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.kt

Length of output: 492


🏁 Script executed:

#!/bin/bash
# Search for Repository version fetching implementation
rg -nP 'class.*VersionRepository|getAppVersion' -A 8 --type kotlin | head -60

Length 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 -80

Length of output: 55


저장소 계층의 .toString() 호출이 원인: 즉시 수정 필요

검증 결과, 세 가지 중대 결함을 확인했습니다:

  1. 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(") 같은 우회가 필요한 이유입니다.

  2. 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)

  3. init()에서 네트워크 호출 시 IO 디스패처 미보장 (MainViewModel.kt:54-57)

    • init 블록에서 checkAppVersion()을 호출하면 Main 스레드에서 네트워크 작업 시작
    • viewModelScope.launch { getAppVersionUseCase()... } 내에 withContext(Dispatchers.IO) 없음

조치:

  • 우선: VersionRepositoryImpl.getVersion() 수정 — 응답을 직접 반환하도록 변경 (.toString() 제거)
  • 필수: compareVersions() 비교 로직 수정 — 세 자리 모두 올바른 순서 검증
  • 권장: checkAppVersion()getAppVersionUseCase() 호출을 withContext(Dispatchers.IO) { ... } 로 감싸기

Comment on lines +181 to +193
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
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

버전 비교 로직 오류(세그먼트 음수 패딩/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.

@hyuns66 hyuns66 merged commit b2b56ed into develop Oct 23, 2025
2 checks passed
@t1nm1ksun t1nm1ksun deleted the feat/update-dialog branch October 26, 2025 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat Feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 버전 업데이트 필요시 다이얼로그

2 participants