Skip to content

Conversation

@l5x5l
Copy link
Contributor

@l5x5l l5x5l commented Dec 3, 2025

[ PR Content ]

  • 설정 화면의 버전 표시 부분에 버전이 표시되지 않으며, 항상 "최신" 태그가 보여지고 있는 문제를 수정합니다.
  • 설정 화면의 ViewModel에서 orbit의 ContainerHost를 직접 사용하도록 변경합니다 (참고 [REFACTOR] MviViewModel 사용부분을 orbit으로 교체 #138)

Related issue

Screenshot 📸

  • 순서대로 업데이트 가능/최신 버전/업데이트 가능 여부를 불러오지 못한 경우

Work Description

  • 설정 화면에서 현재 앱의 버전이 표시되도록 수정
  • SettingViewModel에서 ContainerHost를 직접 사용하도록 수정
  • 앱의 업데이트 가능 여부에 따라 버전 표시 부분 오른쪽에 업데이트 또는 최신 태그가 표시되도록 수정

To Reviewers 📢

Summary by CodeRabbit

  • 새 기능

    • Google Play 스토어 업데이트 확인 및 스토어 열기 기능 추가
    • 앱 버전 표시 지원 추가(설정 화면)
  • 리팩터

    • 설정 화면 상태 관리를 새로운 컨테이너 기반 구조로 개선
    • Play 스토어 관련 유틸리티 재구성 및 통합
  • 기타

    • 빌드 설정 정리 및 Play 업데이트 라이브러리 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@l5x5l l5x5l self-assigned this Dec 3, 2025
@l5x5l l5x5l added 🔨 Refactor 기존 기능 개선 🐞 Fix 버그 수정 labels Dec 3, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Walkthrough

설정 화면 버전 표시 및 Play Store 업데이트 흐름을 추가하고, 버전 제공자 DI 도입과 빌드 관례로 애플리케이션 ID를 중앙화했습니다. MVI에서 Orbit으로 설정 화면 아키텍처를 전환했습니다.

Changes

코호트 / 파일(s) 변경 요약
애플리케이션 ID 중앙화
app/build.gradle.kts, build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/ApplicationId.kt, build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidApplicationPlugin.kt, build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidLibraryPlugin.kt
app 모듈에서 명시적 applicationId 제거하고, 빌드-로직 플러그인에 configureApplicationId() 추가해 애플리케이션 ID 설정을 중앙화함. 라이브러리에는 BuildConfig 필드로 APPLICATION_ID 주입 및 buildConfig 활성화.
버전 제공자 인터페이스 및 DI 바인딩
presentation/src/main/java/com/threegap/bitnagil/presentation/common/version/VersionNameProvider.kt, presentation/src/main/java/com/threegap/bitnagil/presentation/common/version/AndroidApplicationVersionNameProvider.kt, app/src/main/java/com/threegap/bitnagil/di/presentation/VersionNameProviderModule.kt
VersionNameProvider 인터페이스 추가, BuildConfig 기반 AndroidApplicationVersionNameProvider 구현 추가, Hilt 모듈로 바인딩(@Binds, @Singleton).
Play Store 업데이트 기능 추가
gradle/libs.versions.toml, presentation/build.gradle.kts, presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt, presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/UpdateAvailableState.kt
Play Core 라이브러리(앱 업데이트) 버전 및 번들 추가, Play Store 열기 유틸 및 업데이트 상태(enum) 추가, Compose용 업데이트 확인 훅 구현.
Setting 화면 아키텍처 및 UI 변경
presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt, presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt, presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingState.kt, presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingIntent.kt
ViewModel을 MVI에서 Orbit ContainerHost로 전환(컨테이너 초기화, intent/reduce 사용), VersionNameProvider 주입으로 초기 버전 설정, SettingIntent 파일 삭제, SettingState에서 latestVersion 제거 및 Parcelable 전환. UI는 UpdateAvailableState 기반 표시로 변경 및 Play Store 연동 추가.
Splash 모듈 Play Store 유틸 이동
presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt, presentation/src/main/java/com/threegap/bitnagil/presentation/splash/util/PlayStoreUtils.kt
Splash에서 사용하던 Play Store 유틸을 공통 패키지로 이관하고, 기존 splash 유틸 파일 삭제 및 import 경로 교체.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45-60 minutes

  • 집중 검토 항목:
    • SettingViewModel의 Orbit 컨테이너 초기화 및 intent/reduce 흐름
    • SettingState의 Parcelable 전환과 제거된 latestVersion 참조 흔적
    • Play Core(AppUpdateManager) 비동기 체크 로직과 Compose 연동 (updateAvailable())
    • VersionNameProvider DI 바인딩 및 BuildConfig 값 사용 위치
    • ApplicationId 설정 로직이 앱/라이브러리 빌드에 안전하게 적용되는지

Possibly related PRs

Suggested labels

🧤 대현

Poem

🐰 버전도 내 주머니에 쏙
플레이 스토어로 폴짝 폴짝
Orbit 춤추며 상태 바꿔
DI로 꾹, 빌드는 중앙에
업데이트 소식에 깡총! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 버전 표시 문제 수정이라는 주요 변경 사항을 명확하게 요약하고 있어 개발자 관점에서 핵심 변경 사항을 잘 나타내고 있습니다.
Description check ✅ Passed PR 설명에 관련 이슈, 스크린샷, 작업 내용, 그리고 리뷰어에게 하는 안내가 포함되어 있으며 필수 섹션이 대부분 채워져 있습니다.
Linked Issues check ✅ Passed 모든 코드 변경사항이 #129 이슈의 요구사항을 충족합니다: BuildConfig 인터페이스 구현, 버전 비교 로직, Play Store 오픈 기능이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 버전 표시 문제 수정 및 ViewModel 아키텍처 개선과 직접 관련되어 있으며 범위 내입니다.
✨ 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 fix/#129-settings_version_text

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.

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: 2

🧹 Nitpick comments (7)
build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/ApplicationId.kt (2)

8-15: applicationId 하드코딩 대신 설정/카탈로그로의 위임을 고려해 보세요.

현재 "com.threegap.bitnagil"이 빌드‑로직 안에 고정 문자열로 들어가 있어, 앱 ID 변경(스테이징/QA용 별도 applicationId 등)이 필요할 때마다 플러그인 코드를 수정해야 합니다.
gradle.properties, version catalog(libs.versions.toml), 혹은 별도 convention 설정 객체로부터 값을 주입받도록 바꾸면, 환경/플레이버 추가 시 더 유연하게 대응할 수 있습니다.


17-26: 모든 라이브러리 모듈에서 BuildConfig를 강제로 활성화하는 영향 확인 필요.

LibraryExtension.buildFeatures { buildConfig = true }를 공통으로 적용하면, 기존에 의도적으로 BuildConfig 생성을 끄고 있던 라이브러리도 전부 BuildConfig가 생성되도록 동작이 바뀝니다.
전 모듈에서 BuildConfig.APPLICATION_ID를 쓰겠다는 명확한 정책이라면 괜찮지만, 일부 모듈만 필요하다면:

  • opt‑out(예: 커스텀 플러그인 옵션)이나,
  • buildConfig가 이미 true인 경우에만 필드만 추가하는 형태

같은 방식을 한 번 검토해 보셔도 좋겠습니다.

build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidLibraryPlugin.kt (1)

5-24: LibraryExtension 블록 안에서의 이중 lookup을 줄일 수 있습니다.

extensions.configure<LibraryExtension> { ... configureApplicationId() }에서 configureApplicationId()가 다시 extensions.findByType<LibraryExtension>()를 호출해 같은 익스텐션을 재탐색합니다.
성능상 큰 문제는 없지만, 아래처럼 시그니처를 분리해 바로 this를 넘기면 의도가 더 명확해집니다.

// 예시
internal fun Project.configureApplicationId() {
    extensions.findByType<LibraryExtension>()?.let { it.configureApplicationId() }
}

internal fun LibraryExtension.configureApplicationId() {
    val applicationId = "com.threegap.bitnagil"
    defaultConfig {
        buildConfigField("String", "APPLICATION_ID", "\"$applicationId\"")
    }
    buildFeatures { buildConfig = true }
}
build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidApplicationPlugin.kt (1)

5-31: ApplicationExtension에 대해서도 configureApplicationId 재사용 방식을 정리할 여지가 있습니다.

여기서도 extensions.configure<ApplicationExtension> { ... configureApplicationId() } 호출 시, 내부에서 다시 extensions.findByType<ApplicationExtension>()를 수행하는 구조입니다.
동작에는 문제가 없지만, Library 쪽과 마찬가지로 ApplicationExtension용 오버로드를 분리해 this에 직접 적용하면, 확장 함수의 책임 범위가 더 명확해지고 추후 유지보수 시 추적이 쉬워질 것 같습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/UpdateAvailableState.kt (1)

3-4: Enum 상수 네이밍 일관성을 개선해주세요.

Enum 상수들의 네이밍 컨벤션이 일관되지 않습니다. Kotlin에서는 enum 상수에 PascalCase를 사용하는 것이 관례입니다.

  • Latest, NONE: PascalCase
  • NEED_UPDATE: SCREAMING_SNAKE_CASE

다음 diff를 적용하여 일관된 네이밍을 사용하세요:

 enum class UpdateAvailableState {
-    Latest, NEED_UPDATE, NONE
+    Latest, NeedUpdate, None
 }

또는 모두 PascalCase로:

 enum class UpdateAvailableState {
-    Latest, NEED_UPDATE, NONE
+    Latest, NeedsUpdate, None
 }
presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt (1)

43-59: 예외 로깅을 고려해보세요 (선택사항).

예외를 의도적으로 무시하여 폴백 로직을 구현한 것은 적절하지만, 디버깅을 위해 예외를 로깅하는 것을 고려해볼 수 있습니다.

 private fun tryOpenPlayStore(activity: ComponentActivity): Boolean =
     try {
         val intent = createPlayStoreIntent()
         activity.startActivity(intent)
         true
     } catch (e: ActivityNotFoundException) {
+        // Log.d("PlayStoreUtils", "Play Store app not found, will try browser")
         false
     }

 private fun tryOpenWebBrowser(activity: ComponentActivity): Boolean =
     try {
         val intent = createWebIntent()
         activity.startActivity(intent)
         true
     } catch (e: Exception) {
+        // Log.e("PlayStoreUtils", "Failed to open Play Store or browser", e)
         false
     }
presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt (1)

45-53: intent 블록 내에서는 state를 직접 사용할 수 있습니다.

container.stateFlow.value.loading 대신 state.loading을 사용하면 더 간결하고 Orbit 패턴에 일관됩니다.

     fun logout() = intent {
-        if (container.stateFlow.value.loading) return@intent
+        if (state.loading) return@intent

         reduce {
             state.copy(
                 logoutConfirmDialogVisible = false,
                 loading = true,
             )
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50043f9 and 814b10b.

📒 Files selected for processing (17)
  • app/build.gradle.kts (0 hunks)
  • app/src/main/java/com/threegap/bitnagil/di/presentation/VersionNameProviderModule.kt (1 hunks)
  • build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidApplicationPlugin.kt (2 hunks)
  • build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidLibraryPlugin.kt (2 hunks)
  • build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/ApplicationId.kt (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
  • presentation/build.gradle.kts (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/UpdateAvailableState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/version/AndroidApplicationVersionNameProvider.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/version/VersionNameProvider.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt (7 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/util/PlayStoreUtils.kt (0 hunks)
💤 Files with no reviewable changes (3)
  • app/build.gradle.kts
  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/util/PlayStoreUtils.kt
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-23T13:31:46.809Z
Learnt from: l5x5l
Repo: YAPP-Github/Bitnagil-Android PR: 41
File: presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt:6-14
Timestamp: 2025-07-23T13:31:46.809Z
Learning: In the Bitnagil Android project, MviState interface extends Parcelable, so any class implementing MviState automatically implements Parcelable. Therefore, Parcelize annotation works correctly without explicitly adding Parcelable implementation.

Applied to files:

  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingState.kt
📚 Learning: 2025-08-13T09:06:19.028Z
Learnt from: wjdrjs00
Repo: YAPP-Github/Bitnagil-Android PR: 101
File: presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt:61-67
Timestamp: 2025-08-13T09:06:19.028Z
Learning: In Android ViewModels, when logout success triggers navigation to a different screen (like NavigateToLogin), the current ViewModel's lifecycle ends, so loading states don't need to be explicitly reset in the success case since the ViewModel will be destroyed.

Applied to files:

  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt
🧬 Code graph analysis (3)
build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidApplicationPlugin.kt (1)
build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/ApplicationId.kt (1)
  • configureApplicationId (8-27)
build-logic/convention/src/main/java/com/threegap/bitnagil/convention/AndroidLibraryPlugin.kt (1)
build-logic/convention/src/main/java/com/threegap/bitnagil/convention/extension/ApplicationId.kt (1)
  • configureApplicationId (8-27)
presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt (2)
  • updateAvailable (86-96)
  • openAppInPlayStore (30-41)
🪛 detekt (1.23.8)
presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt

[warning] 48-48: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 57-57: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

⏰ 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: build
🔇 Additional comments (14)
presentation/build.gradle.kts (1)

24-24: LGTM!

app-update 라이브러리 의존성이 올바르게 추가되었습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt (1)

28-28: LGTM!

Play Store 유틸리티를 공통 모듈로 이동하여 재사용성을 개선했습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/common/version/VersionNameProvider.kt (1)

1-5: LGTM!

버전 이름 제공을 위한 깔끔한 추상화입니다. DI와 테스트 용이성을 개선합니다.

app/src/main/java/com/threegap/bitnagil/di/presentation/VersionNameProviderModule.kt (1)

11-17: LGTM!

VersionNameProvider를 위한 표준 Hilt 바인딩 모듈입니다. Singleton 스코프가 적절하게 적용되었습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt (3)

86-96: LGTM!

업데이트 가능 여부를 비동기로 확인하는 Composable 함수입니다. LaunchedEffect를 사용하여 초기 로딩 상태(NONE)에서 실제 상태로 전환하는 패턴이 적절합니다.


98-119: LGTM!

suspendCancellableCoroutine을 올바르게 사용하여 Play Core의 콜백 기반 API를 코루틴으로 변환했습니다.

  • continuation.isActive 체크로 취소된 코루틴 재개를 방지
  • 실패 시 NONE 반환으로 안전한 폴백 제공
  • 업데이트 가능 여부와 허용된 업데이트 타입을 모두 확인하는 로직이 적절함

26-26: No issue — BuildConfig.APPLICATION_ID is properly configured and available in the presentation library module.

The convention plugin (configureApplicationId()) explicitly enables buildConfig for all library modules and exposes the APPLICATION_ID constant via buildConfigField. This is the correct and standard approach used across the project.

Likely an incorrect or invalid review comment.

gradle/libs.versions.toml (1)

51-52: Verify Play Services version for CVE-2022-2390 compatibility.

Version 2.1.0 is the latest Play Core app-update release and contains no known unpatched vulnerabilities. However, the broader Play Services ecosystem has CVE-2022-2390 (mutable PendingIntent), which was fixed in Google Play Services v18.0.2. Ensure that the app's Play Services dependency is also updated to v18.0.2 or later and the app is rebuilt to mitigate this transitive vulnerability.

presentation/src/main/java/com/threegap/bitnagil/presentation/common/version/AndroidApplicationVersionNameProvider.kt (1)

6-14: LGTM!

VersionNameProvider 인터페이스 구현이 깔끔하고, DI를 위한 @Inject 생성자 사용이 적절합니다. BuildConfig에서 버전 정보를 조합하는 방식이 명확합니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt (2)

50-60: LGTM!

Orbit MVI 패턴으로의 마이그레이션이 적절합니다. collectAsState()collectSideEffect() 사용이 올바르며, 사이드 이펙트 처리 로직이 명확합니다.


143-168: 업데이트 상태별 UI 처리가 적절합니다.

UpdateAvailableState에 따른 분기 처리가 명확하고, 각 상태에 맞는 스타일링이 잘 적용되어 있습니다. NONE 상태에서 아무것도 표시하지 않는 것은 업데이트 가능 여부를 확인하지 못한 경우에 대한 적절한 처리입니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt (3)

69-89: 알람 토글 로직에 대한 확인이 필요합니다.

현재 toggleServiceAlarmtogglePushAlarm은 UI 상태만 변경하고, delay 후 실제 저장 로직이 없습니다. 이것이 의도된 것인지 확인 부탁드립니다.


28-31: LGTM!

Orbit 컨테이너 초기화가 적절합니다. VersionNameProvider를 통해 초기 버전 정보를 설정하고, savedStateHandle을 사용하여 상태 복원을 지원합니다.


91-93: LGTM!

navigateToWithdrawal 함수가 간결하게 구현되어 있습니다.

@l5x5l l5x5l requested a review from wjdrjs00 December 3, 2025 10:52
Copy link
Member

@wjdrjs00 wjdrjs00 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다~!
나중에 core:common 모듈을 생성해도 좋을거 같네욤~

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: 0

♻️ Duplicate comments (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt (1)

155-166: “업데이트” 배지의 터치 영역을 넓히려면 Modifier 순서를 바꾸는 것이 좋습니다. (기존 코멘트와 동일)

현재 UpdateAvailableState.NEED_UPDATE 분기에서 Modifier 순서는:

modifier = Modifier
    .background(...)
    .clickableWithoutRipple(onClick = onClickUpdate)
    .padding(horizontal = 10.dp, vertical = 5.dp)

이렇게 되면 패딩 영역은 클릭 영역에 포함되지 않아, 사용자가 텍스트/배지의 가장자리를 탭했을 때 터치가 먹지 않을 수 있습니다. 패딩까지 포함해서 넉넉한 터치 타겟을 주려면 .padding.clickableWithoutRipple 앞에 두는 편이 좋습니다:

-                    UpdateAvailableState.NEED_UPDATE -> Text(
+                    UpdateAvailableState.NEED_UPDATE -> Text(
                         text = "업데이트",
                         color = BitnagilTheme.colors.orange500,
                         style = BitnagilTheme.typography.button2,
                         modifier = Modifier
                             .background(
                                 color = BitnagilTheme.colors.orange50,
                                 shape = RoundedCornerShape(8.dp),
                             )
-                            .clickableWithoutRipple(onClick = onClickUpdate)
-                            .padding(horizontal = 10.dp, vertical = 5.dp),
+                            .padding(horizontal = 10.dp, vertical = 5.dp)
+                            .clickableWithoutRipple(onClick = onClickUpdate),
                     )

이 변경으로 시각적 결과는 유지하면서 실제 터치 영역은 더 넉넉해집니다.

🧹 Nitpick comments (4)
presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt (3)

26-28: 패키지 이름은 Context.packageName 기반으로 만들면 모듈/variant 변경에 더 안전합니다.

현재는 BuildConfig.APPLICATION_ID를 상수로 사용하고 있어 모듈 구조 변경이나 applicationId variant(예: flavor)에 따라 실제 런타임 패키지명과 어긋날 여지가 있습니다. createPlayStoreIntent / createWebIntent 쪽으로 packageName을 인자로 넘기고, activity.packageName 또는 context.packageName을 사용하면 이런 위험을 줄일 수 있습니다.

-private const val PACKAGE_NAME = BuildConfig.APPLICATION_ID
+// 필요시 제거하고, 호출부에서 activity.packageName/context.packageName 전달

-private fun createPlayStoreIntent(): Intent =
+private fun createPlayStoreIntent(packageName: String): Intent =
     Intent(
         Intent.ACTION_VIEW,
-        "market://details?id=$PACKAGE_NAME".toUri(),
+        "market://details?id=$packageName".toUri(),
     ).apply {
         addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
         setPackage(GOOGLE_PLAY_PACKAGE)
     }

-private fun createWebIntent(): Intent =
+private fun createWebIntent(packageName: String): Intent =
     Intent(
         Intent.ACTION_VIEW,
-        "https://play.google.com/store/apps/details?id=$PACKAGE_NAME".toUri(),
+        "https://play.google.com/store/apps/details?id=$packageName".toUri(),
     ).apply {
         addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
     }

-private fun tryOpenPlayStore(activity: ComponentActivity): Boolean =
+private fun tryOpenPlayStore(activity: ComponentActivity): Boolean =
     try {
-        val intent = createPlayStoreIntent()
+        val intent = createPlayStoreIntent(activity.packageName)
         activity.startActivity(intent)
         true
     } catch (e: ActivityNotFoundException) {
         false
     }

-private fun tryOpenWebBrowser(activity: ComponentActivity): Boolean =
+private fun tryOpenWebBrowser(activity: ComponentActivity): Boolean =
     try {
-        val intent = createWebIntent()
+        val intent = createWebIntent(activity.packageName)
         activity.startActivity(intent)
         true
     } catch (e: Exception) {
         false
     }

30-41: exitProcess(0) 기반 강제 종료는 기본값을 보수적으로 두는 편이 좋습니다.

finishAppWithDelay에서 finishAffinity()exitProcess(0)까지 호출하고 있어, 다른 호출부에서 기본값(shouldFinishApp = true)을 그대로 사용할 경우 사용자가 뒤로가기로 앱으로 돌아올 수 없는 “강제 종료” UX가 됩니다.

현재 Setting 화면에서는 shouldFinishApp = false로 안전하게 쓰이고 있지만, 실수 방지를 위해:

  • 기본값을 false로 바꾸거나,
  • 정말 강제 종료가 필요한 경우에만 사용하는 별도 함수(예: openAppInPlayStoreAndExit(...))를 두는 방식을 고려해볼 수 있습니다.
-fun openAppInPlayStore(
-    activity: ComponentActivity?,
-    shouldFinishApp: Boolean = true,
-) {
+fun openAppInPlayStore(
+    activity: ComponentActivity?,
+    shouldFinishApp: Boolean = false,
+) {
     activity?.let {
         val isSuccess = tryOpenPlayStore(it) || tryOpenWebBrowser(it)

         if (isSuccess && shouldFinishApp) {
             finishAppWithDelay(it)
         }
     }
}

강제 종료가 제품 기획상 꼭 필요한 시나리오라면, 해당 호출부에서만 shouldFinishApp = true를 명시하는 식으로 의도를 드러내 주면 더 읽기 편할 것 같습니다.

Also applies to: 78-84


86-96: 업데이트 판정 조건(UPDATE_AVAILABLE + isUpdateTypeAllowed)이 요구사항과 맞는지 한 번 더 확인이 필요합니다.

현재 로직은:

  • updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE 이고
  • AppUpdateType.IMMEDIATE 또는 AppUpdateType.FLEXIBLE 중 하나라도 isUpdateTypeAllowed(...) == true

일 때만 UpdateAvailableState.NEED_UPDATE를 반환하고, 그 외 성공 케이스는 모두 LATEST, 실패는 NONE으로 처리합니다.

즉, Play Core 입장에서 “업데이트는 있지만 현재 타입(IMMEDIATE/FLEXIBLE)으로는 진행할 수 없는 상태”가 될 경우, UI에는 "최신"으로 표기됩니다. Issue 설명 상 요구사항은 “현 버전이 낮을 경우 ‘업데이트’ 태그 표시”라서, 이런 edge case에서 실제로는 업데이트가 있음에도 "최신"으로 보일 수 있습니다.

만약 “업데이트만 존재하면 무조건 ‘업데이트’ 뱃지”가 더 맞는 기획이라면, isAllowed 체크를 제거하고 UPDATE_AVAILABLE 여부만 보는 게 더 직관적일 수 있습니다:

val isAvailable = appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE

val state = if (isAvailable) {
    UpdateAvailableState.NEED_UPDATE
} else {
    UpdateAvailableState.LATEST
}

반대로, 지금처럼 “실제로 사용할 수 있는 업데이트 타입이 없으면 ‘업데이트’ 뱃지를 숨긴다”는 UX가 의도라면 현재 구현도 괜찮습니다. 어느 쪽이 기획에 더 맞는지 한 번만 다시 확인해 주세요.

또한 실패 시 UpdateAvailableState.NONE으로 떨어뜨려 뱃지를 아예 숨기는 패턴은 PR 설명의 세 번째 스크린샷(업데이트 가능 여부를 불러오지 못한 경우)과 잘 맞는 구조로 보입니다.

Also applies to: 98-119

presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt (1)

53-54: 업데이트 상태 조회를 Composable에서 직접 하는 현재 구조는 충분히 괜찮지만, 필요시 ViewModel로 올리는 것도 고려해볼 수 있습니다.

지금은:

  • SettingScreenContainer에서 val updateAvailableState = updateAvailable()로 Compose 레벨에서 app-update 상태를 한 번 조회하고,
  • SettingScreenupdateAvailableState를 내려 UI만 담당하도록 분리되어 있습니다.

설정 화면처럼 진입 빈도가 상대적으로 낮고, 상태를 한 번만 알면 되는 경우에는 이 정도 수준의 Compose-side 호출로도 충분히 실용적입니다.

다만:

  • 다른 화면에서도 같은 정보를 재사용해야 하거나,
  • 설정 화면을 자주 왕복하면서 매번 Play Core를 다시 두드리는 것이 부담스럽다고 판단되면,

updateAvailableState를 ViewModel 상태로 끌어올려(예: init 블록에서 suspend 함수 한 번 호출) 여러 Composable에서 공유하는 구조도 고려해볼 수 있습니다. 지금 코드 구조라면 나중에 옮기기도 쉬운 편이라 “지금은 그대로, 필요해지면 ViewModel로 승격” 정도로 생각해 두시면 될 것 같습니다.

Also applies to: 90-91, 143-168

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 814b10b and 94a2d27.

📒 Files selected for processing (3)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/UpdateAvailableState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/UpdateAvailableState.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T09:06:19.028Z
Learnt from: wjdrjs00
Repo: YAPP-Github/Bitnagil-Android PR: 101
File: presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt:61-67
Timestamp: 2025-08-13T09:06:19.028Z
Learning: In Android ViewModels, when logout success triggers navigation to a different screen (like NavigateToLogin), the current ViewModel's lifecycle ends, so loading states don't need to be explicitly reset in the success case since the ViewModel will be destroyed.

Applied to files:

  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt
🧬 Code graph analysis (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt (2)
  • updateAvailable (86-96)
  • openAppInPlayStore (30-41)
⏰ 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: build
🔇 Additional comments (3)
presentation/src/main/java/com/threegap/bitnagil/presentation/common/playstore/PlayStoreUtils.kt (1)

1-119: 공통 Play Store 유틸로의 이동 및 app-update 연동 구조는 전반적으로 깔끔합니다.

  • Splash/설정 양쪽에서 재사용할 수 있도록 common.playstore로 분리한 점,
  • Play Store → Web 브라우저 fallback,
  • Compose updateAvailable()에서 suspendCancellableCoroutine로 app-update Task를 한 번 감싼 구조

모두 역할이 분리되어 있고 호출부 입장에서 쓰기 쉬운 API로 정리되어 있습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt (2)

50-55: SettingScreenContainer에서 Orbit + PlayStoreUtils 연동 구조는 자연스럽습니다.

  • viewModel.collectAsState() / collectSideEffect로 Orbit 컨테이너를 직접 사용하는 구조가 깔끔하고,
  • LocalContext.current as? ComponentActivity로 activity를 nullable로 가져와 openAppInPlayStore(activity = activity, shouldFinishApp = false)에 넘기는 것도 Preview·테스트 환경에서 안전하게 no-op이 되도록 잘 처리된 것 같습니다.
  • onClickWithdrawalviewModel::navigateToWithdrawalSettingSideEffect.NavigateToWithdrawalnavigateToWithdrawal() 흐름도 일관적입니다.

이 부분은 현재 형태 그대로 유지해도 무방해 보입니다.

Also applies to: 73-85


202-223: 프리뷰에 updateAvailableState까지 포함해 새 파라미터가 잘 반영되어 있습니다.

SettingScreenPreview에서 updateAvailableState = UpdateAvailableState.LATEST를 명시해 새 시그니처를 반영한 점이 좋습니다. 실제 UI에서의 최신/업데이트/미표시 세 상태를 각각 확인해보고 싶다면, 필요 시 프리뷰를 2–3개로 나눠 상태별로 추가하는 것도 선택지입니다.

@l5x5l l5x5l merged commit aeb85b7 into develop Dec 4, 2025
2 checks passed
@l5x5l l5x5l deleted the fix/#129-settings_version_text branch December 4, 2025 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 Fix 버그 수정 🔨 Refactor 기존 기능 개선

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FIX] 설정 화면에서 버전 표시 부분 문제

3 participants