Conversation
- windowSplashScreenBackground 속성과 일치하기 위해 colors.xml 에 색상 추가
Walkthrough인증 상태 관리를 AuthState/UserType에서 SessionStore/SessionState/TokenManager로 개편하고, Splash 화면을 XML에서 Compose로 마이그레이션하며, 로그인 및 초기 세션 결정 플로우를 재구성하고, 관련 DI 모듈과 네트워크 계층을 업데이트합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant SplashScreen as SplashScreen<br/>(Compose)
participant SplashViewModel as SplashViewModel
participant DetermineUseCase as DetermineInitialSessionUseCase
participant TokenManager as TokenManager
participant AuthRepo as AuthRepository
participant SessionStore as SessionStore
User->>SplashScreen: 앱 시작
SplashScreen->>SplashViewModel: determineStartDestination() 호출
SplashViewModel->>DetermineUseCase: invoke() 호출
DetermineUseCase->>TokenManager: initialize()
DetermineUseCase->>TokenManager: currentTokens() 조회
alt 토큰 없음
DetermineUseCase->>SessionStore: setGuest()
DetermineUseCase-->>SplashViewModel: SessionState.Guest
else 토큰 있음
DetermineUseCase->>AuthRepo: verifyToken()
alt 토큰 유효
DetermineUseCase->>SessionStore: setMember()
DetermineUseCase-->>SplashViewModel: SessionState.Member
else 인증 오류
DetermineUseCase->>SwitchToGuestUseCase: invoke()
DetermineUseCase-->>SplashViewModel: SessionState.Guest
else 기타 오류
DetermineUseCase->>SessionStore: setMember()
DetermineUseCase-->>SplashViewModel: SessionState.Member
end
end
alt Member
SplashViewModel->>SplashScreen: NavigateToMain 이펙트 발송
SplashScreen->>User: MainActivity로 이동
else Guest/Uninitialized
SplashViewModel->>SplashScreen: NavigateToLogin 이펙트 발송
SplashScreen->>User: LoginActivity로 이동
end
sequenceDiagram
actor User
participant LoginScreen as LoginScreen<br/>(Compose)
participant LoginViewModel as LoginViewModel
participant GoogleCredMgr as GoogleCredentialManager
participant LoginUseCase as LoginUseCase
participant AuthRepo as AuthRepository
participant TokenManager as TokenManager
participant SessionStore as SessionStore
User->>LoginScreen: Google 로그인 버튼 클릭
LoginScreen->>LoginViewModel: loginWithGoogle()
LoginViewModel->>GoogleCredMgr: getIdToken()
GoogleCredMgr-->>LoginViewModel: ID Token (String)
LoginViewModel->>LoginUseCase: invoke(idToken)
LoginUseCase->>AuthRepo: login(idToken)
AuthRepo-->>LoginUseCase: AuthResult (tokens)
LoginUseCase->>TokenManager: setTokens()
TokenManager-->>LoginUseCase: Result.Success
LoginUseCase->>SessionStore: setMember()
LoginUseCase-->>LoginViewModel: TuripResult.Success(isNewMember)
alt 신규 회원
LoginViewModel->>LoginScreen: 마이그레이션 다이얼로그 표시
User->>LoginScreen: 확인 클릭
LoginScreen->>LoginViewModel: confirmMigration()
LoginViewModel->>LoginScreen: MainActivity로 이동
else 기존 회원
LoginViewModel->>LoginScreen: MainActivity로 이동
end
sequenceDiagram
actor User
participant MyPageScreen as MyPageScreen<br/>(Compose)
participant SessionStore as SessionStore
participant TokenManager as TokenManager
participant SwitchToGuestUseCase as SwitchToGuestUseCase
User->>MyPageScreen: 로그아웃 클릭
MyPageScreen->>SwitchToGuestUseCase: invoke()
SwitchToGuestUseCase->>TokenManager: clearTokens()
TokenManager-->>SwitchToGuestUseCase: Result.Success
SwitchToGuestUseCase->>SessionStore: setGuest()
SessionStore-->>SwitchToGuestUseCase: 상태 업데이트
SwitchToGuestUseCase-->>MyPageScreen: 완료
MyPageScreen->>User: LoginActivity로 이동
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
android/app/src/main/java/com/on/turip/ui/compose/login/LoginViewmodel.kt (1)
73-75:⚠️ Potential issue | 🟡 Minor마이그레이션 실패 시 사용자 피드백이 누락됩니다.
Line [73]~Line [75]에서 실패를 로그로만 처리하면 다이얼로그가 유지된 채 원인을 알기 어렵습니다.
errorType을 받아handleError로 전달해 주세요.💡 제안 수정안
- }.onFailure { - Timber.e("마이그레이션 실패") + }.onFailure { errorType: ErrorType -> + handleError(errorType) + Timber.e("마이그레이션 실패: $errorType") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/main/java/com/on/turip/ui/compose/login/LoginViewmodel.kt` around lines 73 - 75, The onFailure block currently only logs the migration failure; update the onFailure handler in LoginViewmodel.kt (the onFailure { ... } following the migration call) to construct or extract an errorType and pass it to handleError(errorType) so the UI receives feedback, and also update any dialog/progress state (e.g., hide the migration dialog or set showProgress=false) so the dialog does not remain displayed; keep the Timber.e logging but add handleError(errorType) and the dialog dismissal/state change in that same onFailure branch.
🧹 Nitpick comments (6)
android/app/src/main/java/com/on/turip/data/login/datasource/CredentialProvider.kt (1)
6-6: 토큰 문자열의 유효성 계약을 인터페이스에 명시해 주세요.
TuripResult<String>로 단순화된 건 좋지만, 빈 문자열/공백 토큰 허용 여부를 KDoc으로 명시하면 호출부 방어 로직이 일관됩니다.계약 명시 예시
interface CredentialProvider { + /** + * 성공 시 비어있지 않은 Google ID Token 문자열을 반환한다. + */ suspend fun getIdToken(): TuripResult<String> }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/main/java/com/on/turip/data/login/datasource/CredentialProvider.kt` at line 6, Add a KDoc comment to the CredentialProvider.getIdToken() declaration that clearly specifies the token validity contract: state whether empty or whitespace-only strings are allowed or must be treated as failure, what TuripResult variants will be returned for success vs invalid/missing token, and any normalization (e.g., trimmed) or lifetime expectations callers can rely on; update the suspend fun getIdToken(): TuripResult<String> declaration's KDoc to reflect these rules so callers implement consistent defensive checks.android/app/src/main/java/com/on/turip/ui/compose/turipdetail/TuripDetailViewModel.kt (1)
302-317:shareTurip()이펙트 전송 중복은 한 번의launch로 줄일 수 있습니다.동작은 맞지만 분기별 코루틴 생성이 중복되어 가독성이 떨어집니다. 이펙트를 먼저 결정하고 한 번만 전송하면 더 깔끔합니다.
중복 제거 예시
fun shareTurip() { - when (sessionState.value) { - SessionState.Member -> { - val turipShareModel = - TuripShareModel( - name = uiState.value.selectedTurip.name, - places = uiState.value.places.map { it.toUiModel() }, - ) - viewModelScope.launch { - _uiEffect.send(TuripPlaceUiEffect.ShareTurip(turipShareModel)) - } - } - - SessionState.Guest, SessionState.Uninitialized -> { - viewModelScope.launch { - _uiEffect.send(TuripPlaceUiEffect.ShowTuripShareNotAllowed) - } - } - } + val effect = + when (sessionState.value) { + SessionState.Member -> { + val turipShareModel = + TuripShareModel( + name = uiState.value.selectedTurip.name, + places = uiState.value.places.map { it.toUiModel() }, + ) + TuripPlaceUiEffect.ShareTurip(turipShareModel) + } + SessionState.Guest, SessionState.Uninitialized -> TuripPlaceUiEffect.ShowTuripShareNotAllowed + } + viewModelScope.launch { _uiEffect.send(effect) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/main/java/com/on/turip/ui/compose/turipdetail/TuripDetailViewModel.kt` around lines 302 - 317, The branches in TuripDetailViewModel.kt duplicate coroutine launches when sending effects; instead determine the TuripPlaceUiEffect first (using sessionState.value to choose between TuripPlaceUiEffect.ShareTurip(TuripShareModel(...)) and TuripPlaceUiEffect.ShowTuripShareNotAllowed) and then call a single viewModelScope.launch { _uiEffect.send(chosenEffect) } to send it; build the TuripShareModel only when needed and reference sessionState, TuripShareModel, _uiEffect, and viewModelScope to locate the code to refactor.android/app/src/main/java/com/on/turip/ui/compose/login/component/GuestModeSection.kt (1)
37-37: 접근성 개선 권장: 클릭 피드백 추가
Modifier.clickable(onClick = ...)에 ripple 효과가 기본 적용되지만, 명시적으로indication과interactionSource를 설정하면 더 나은 터치 피드백을 제공할 수 있습니다. 현재 텍스트는 밑줄이 있어 클릭 가능함을 암시하지만, 아이콘은 시각적 힌트가 부족할 수 있습니다.Also applies to: 46-46
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/main/java/com/on/turip/ui/compose/login/component/GuestModeSection.kt` at line 37, The clickables in GuestModeSection (the Modifier.clickable usages for the text and the icon) lack explicit touch feedback; update both Modifier.clickable(onClick = onTextClick) and the other clickable at the second occurrence to pass an explicit indication and interactionSource — e.g., create a remember { MutableInteractionSource() } and use rememberRipple() as the indication — so the icon and text provide consistent ripple/interaction feedback for accessibility and touch affordance.android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewmodel.kt (1)
21-29: 초기 목적지 결정 호출을 1회로 가드하는 편이 안전합니다.현재 구조에서는 재호출 시 동일한 네비게이션 effect가 중복 발행될 수 있습니다.
중복 effect 발행 방지 예시
class SplashViewmodel `@Inject` constructor( private val determineInitialSessionUseCase: DetermineInitialSessionUseCase, ) : ViewModel() { + private var hasDeterminedStartDestination: Boolean = false + private val _uiEffect: Channel<SplashUiEffect> = Channel(Channel.BUFFERED) val uiEffect: Flow<SplashUiEffect> = _uiEffect.receiveAsFlow() fun determineStartDestination() { + if (hasDeterminedStartDestination) return + hasDeterminedStartDestination = true + viewModelScope.launch { val sessionState: SessionState = determineInitialSessionUseCase() when (sessionState) { is SessionState.Member -> _uiEffect.send(SplashUiEffect.NavigateToMain) SessionState.Guest, SessionState.Uninitialized -> _uiEffect.send(SplashUiEffect.NavigateToLogin) } } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewmodel.kt` around lines 21 - 29, determineStartDestination can emit duplicate navigation effects on repeated calls; add a one-time guard (e.g., a private boolean or AtomicBoolean like hasDeterminedStartDestination) checked at the start of determineStartDestination to return early if already run, and set it to true before launching the session check so that viewModelScope.launch and subsequent _uiEffect.send(SplashUiEffect.NavigateToMain / SplashUiEffect.NavigateToLogin) only execute once.android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt (1)
74-79: 업데이트 재개 조건은 helper 재사용으로 통일하는 게 좋습니다.동일한 조건 의미를
canResumeUpdate(...)로 모아두면 분기 일관성이 좋아지고 추후 변경 시 누락 가능성을 줄일 수 있습니다.조건 재사용 정리 예시
when (appUpdateInfo.updateAvailability()) { UpdateAvailability.UPDATE_AVAILABLE -> { if (appUpdateInfo.isUpdateTypeAllowed(UPDATE_TYPE)) { appUpdateInfo.startUpdate(UPDATE_TYPE) } else { proceed() } } UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> { - if (appUpdateInfo.isUpdateTypeAllowed(UPDATE_TYPE)) { + if (appUpdateInfo.canResumeUpdate(UPDATE_TYPE)) { appUpdateInfo.startUpdate(UPDATE_TYPE) } else { proceed() } }Also applies to: 107-109
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt` around lines 74 - 79, Extract the shared condition used before resuming an update into a helper method canResumeUpdate(appUpdateInfo: AppUpdateInfo): Boolean and use it in both branches that currently check appUpdateInfo.isUpdateTypeAllowed(UPDATE_TYPE) (the UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS branch and the other similar branch around startUpdate calls), then replace the inline checks with if (canResumeUpdate(appUpdateInfo)) { appUpdateInfo.startUpdate(UPDATE_TYPE) } else { proceed() } so both places call the same helper and maintain consistent behavior.android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt (1)
314-338: Preview에서 SessionState 변형에 대한 커버리지 개선을 고려해 보세요.현재 Preview는
SessionState.Uninitialized만 사용하고 있습니다.MyPageSettingsSection이 세션 상태에 따라 다른 UI를 보여주므로, Member/Guest 상태에 대한 Preview도 추가하면 더 포괄적인 UI 검증이 가능합니다.♻️ SessionState PreviewParameterProvider 추가 제안
private class SessionStatePreviewProvider : PreviewParameterProvider<SessionState> { override val values: Sequence<SessionState> = sequenceOf( SessionState.Member, SessionState.Guest, SessionState.Uninitialized, ) }또는 기존 Preview에서
sessionState파라미터를 변경하여 여러 상태를 테스트할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt` around lines 314 - 338, Add previews that cover different SessionState values so MyPageSettingsSection is validated for Member/Guest/Uninitialized: create a PreviewParameterProvider (e.g., SessionStatePreviewProvider) that yields SessionState.Member, SessionState.Guest, SessionState.Uninitialized, then change MyPageScreenPreview to accept a sessionState parameter (annotated with `@PreviewParameter`(SessionStatePreviewProvider::class)) and pass that sessionState into MyPageScreenContent; keep existing MyPageScreenPreview name and all other preview params the same so only session coverage is expanded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@android/app/src/main/java/com/on/turip/data/session/DefaultTokenManager.kt`:
- Around line 54-68: The clearTokens() implementation clears the in-memory token
and cache before attempting to clear the persistent storage, causing state
mismatch on storage failures; change clearTokens() to call
userStorageRepository.clearTokens() first and only onSuccess set tokens = null
and call authCacheController.clear(), returning Result.success(Unit) there,
while preserving the onFailure branch to log the error and return
Result.failure(it); reference functions/fields: clearTokens(), setTokens(),
tokens, authCacheController, and userStorageRepository.clearTokens() to mirror
setTokens()'s success-first update pattern.
- Around line 21-22: The tokens field is only marked `@Volatile` which ensures
visibility but not atomicity, so protect concurrent access to tokens by adding a
Mutex and wrapping all reads/writes in withLock blocks: replace or keep the
`@Volatile` var tokens: AuthTokens? but ensure setTokens(), clearTokens(), and
currentTokens() acquire the same Mutex before mutating or returning tokens to
eliminate races during simultaneous 401 refreshes; use a single private val
mutex = Mutex() in DefaultTokenManager and call mutex.withLock { ... } around
the logic in setTokens(), clearTokens(), and currentTokens() (and any other
places that touch tokens) so updates/clears are atomic.
In `@android/app/src/main/java/com/on/turip/di/NetworkModule.kt`:
- Around line 119-122: The onFailure lambda currently returns null for all
failures; update it to distinguish ErrorType.Auth vs transient errors: in the
onFailure handler (the onFailure = { ... } block) inspect the provided error,
and if it's ErrorType.Auth call your token cleanup and guest-transition routines
(e.g., invoke the stored-token removal function and the GUEST state hook) so the
token is cleared and user switched to GUEST; if the error is transient (e.g.,
ErrorType.Network) preserve the token and return the existing token/response as
before; ensure other error types keep existing semantics.
In `@android/app/src/main/java/com/on/turip/domain/login/usecase/LoginUseCase.kt`:
- Line 31: The log in LoginUseCase.kt currently uses Timber.e("로그인 성공") which is
the error level; update the logging call in the login success path (the place
where Timber.e is called inside the LoginUseCase class / relevant login method)
to use an appropriate non-error level such as Timber.i(...) or Timber.d(...),
preserving the message content and any surrounding context variables.
In
`@android/app/src/main/java/com/on/turip/domain/session/usecase/SwitchToGuestUseCase.kt`:
- Around line 11-14: tokenManager.clearTokens() returns a Result<Unit> but its
result is ignored; update the suspend operator fun invoke() in
SwitchToGuestUseCase to handle the Result from tokenManager.clearTokens() (check
isSuccess/isFailure or use fold/runCatching) and only call
sessionStore.setGuest() when clearing tokens succeeds; on failure propagate or
log the error (e.g., return/throw the failure or call sessionStore.setGuest()
conditionally after success) so you avoid leaving tokens in storage while the
session is set to Guest.
---
Outside diff comments:
In `@android/app/src/main/java/com/on/turip/ui/compose/login/LoginViewmodel.kt`:
- Around line 73-75: The onFailure block currently only logs the migration
failure; update the onFailure handler in LoginViewmodel.kt (the onFailure { ...
} following the migration call) to construct or extract an errorType and pass it
to handleError(errorType) so the UI receives feedback, and also update any
dialog/progress state (e.g., hide the migration dialog or set
showProgress=false) so the dialog does not remain displayed; keep the Timber.e
logging but add handleError(errorType) and the dialog dismissal/state change in
that same onFailure branch.
---
Nitpick comments:
In
`@android/app/src/main/java/com/on/turip/data/login/datasource/CredentialProvider.kt`:
- Line 6: Add a KDoc comment to the CredentialProvider.getIdToken() declaration
that clearly specifies the token validity contract: state whether empty or
whitespace-only strings are allowed or must be treated as failure, what
TuripResult variants will be returned for success vs invalid/missing token, and
any normalization (e.g., trimmed) or lifetime expectations callers can rely on;
update the suspend fun getIdToken(): TuripResult<String> declaration's KDoc to
reflect these rules so callers implement consistent defensive checks.
In
`@android/app/src/main/java/com/on/turip/ui/compose/login/component/GuestModeSection.kt`:
- Line 37: The clickables in GuestModeSection (the Modifier.clickable usages for
the text and the icon) lack explicit touch feedback; update both
Modifier.clickable(onClick = onTextClick) and the other clickable at the second
occurrence to pass an explicit indication and interactionSource — e.g., create a
remember { MutableInteractionSource() } and use rememberRipple() as the
indication — so the icon and text provide consistent ripple/interaction feedback
for accessibility and touch affordance.
In `@android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt`:
- Around line 314-338: Add previews that cover different SessionState values so
MyPageSettingsSection is validated for Member/Guest/Uninitialized: create a
PreviewParameterProvider (e.g., SessionStatePreviewProvider) that yields
SessionState.Member, SessionState.Guest, SessionState.Uninitialized, then change
MyPageScreenPreview to accept a sessionState parameter (annotated with
`@PreviewParameter`(SessionStatePreviewProvider::class)) and pass that
sessionState into MyPageScreenContent; keep existing MyPageScreenPreview name
and all other preview params the same so only session coverage is expanded.
In `@android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewmodel.kt`:
- Around line 21-29: determineStartDestination can emit duplicate navigation
effects on repeated calls; add a one-time guard (e.g., a private boolean or
AtomicBoolean like hasDeterminedStartDestination) checked at the start of
determineStartDestination to return early if already run, and set it to true
before launching the session check so that viewModelScope.launch and subsequent
_uiEffect.send(SplashUiEffect.NavigateToMain / SplashUiEffect.NavigateToLogin)
only execute once.
In
`@android/app/src/main/java/com/on/turip/ui/compose/turipdetail/TuripDetailViewModel.kt`:
- Around line 302-317: The branches in TuripDetailViewModel.kt duplicate
coroutine launches when sending effects; instead determine the
TuripPlaceUiEffect first (using sessionState.value to choose between
TuripPlaceUiEffect.ShareTurip(TuripShareModel(...)) and
TuripPlaceUiEffect.ShowTuripShareNotAllowed) and then call a single
viewModelScope.launch { _uiEffect.send(chosenEffect) } to send it; build the
TuripShareModel only when needed and reference sessionState, TuripShareModel,
_uiEffect, and viewModelScope to locate the code to refactor.
In `@android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt`:
- Around line 74-79: Extract the shared condition used before resuming an update
into a helper method canResumeUpdate(appUpdateInfo: AppUpdateInfo): Boolean and
use it in both branches that currently check
appUpdateInfo.isUpdateTypeAllowed(UPDATE_TYPE) (the
UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS branch and the other
similar branch around startUpdate calls), then replace the inline checks with if
(canResumeUpdate(appUpdateInfo)) { appUpdateInfo.startUpdate(UPDATE_TYPE) } else
{ proceed() } so both places call the same helper and maintain consistent
behavior.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (42)
android/app/src/main/java/com/on/turip/common/AuthState.ktandroid/app/src/main/java/com/on/turip/common/UserType.ktandroid/app/src/main/java/com/on/turip/data/login/TokenAuthenticator.ktandroid/app/src/main/java/com/on/turip/data/login/datasource/CredentialProvider.ktandroid/app/src/main/java/com/on/turip/data/login/datasource/GoogleCredentialManager.ktandroid/app/src/main/java/com/on/turip/data/session/DefaultAuthTokenCacheController.ktandroid/app/src/main/java/com/on/turip/data/session/DefaultSessionStore.ktandroid/app/src/main/java/com/on/turip/data/session/DefaultTokenManager.ktandroid/app/src/main/java/com/on/turip/di/LocalStorageModule.ktandroid/app/src/main/java/com/on/turip/di/NetworkModule.ktandroid/app/src/main/java/com/on/turip/di/SessionModule.ktandroid/app/src/main/java/com/on/turip/domain/login/usecase/CheckUserSignedInUseCase.ktandroid/app/src/main/java/com/on/turip/domain/login/usecase/LoginUseCase.ktandroid/app/src/main/java/com/on/turip/domain/login/usecase/LoginUserUseCase.ktandroid/app/src/main/java/com/on/turip/domain/session/AuthTokenCacheController.ktandroid/app/src/main/java/com/on/turip/domain/session/SessionState.ktandroid/app/src/main/java/com/on/turip/domain/session/SessionStore.ktandroid/app/src/main/java/com/on/turip/domain/session/TokenManager.ktandroid/app/src/main/java/com/on/turip/domain/session/usecase/DetermineInitialSessionUseCase.ktandroid/app/src/main/java/com/on/turip/domain/session/usecase/SwitchToGuestUseCase.ktandroid/app/src/main/java/com/on/turip/ui/common/error/ErrorUiState.ktandroid/app/src/main/java/com/on/turip/ui/compose/designsystem/component/ErrorScreen.ktandroid/app/src/main/java/com/on/turip/ui/compose/login/LoginScreen.ktandroid/app/src/main/java/com/on/turip/ui/compose/login/LoginViewmodel.ktandroid/app/src/main/java/com/on/turip/ui/compose/login/component/GoogleLoginButton.ktandroid/app/src/main/java/com/on/turip/ui/compose/login/component/GuestModeSection.ktandroid/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.ktandroid/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageViewModel.ktandroid/app/src/main/java/com/on/turip/ui/compose/mypage/component/MyPageSettingsSection.ktandroid/app/src/main/java/com/on/turip/ui/compose/splash/SplashScreen.ktandroid/app/src/main/java/com/on/turip/ui/compose/splash/SplashUiEffect.ktandroid/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewmodel.ktandroid/app/src/main/java/com/on/turip/ui/compose/trip/turipselection/PlaceTuripSelectionViewModel.ktandroid/app/src/main/java/com/on/turip/ui/compose/turipdetail/TuripDetailViewModel.ktandroid/app/src/main/java/com/on/turip/ui/login/LoginActivity.ktandroid/app/src/main/java/com/on/turip/ui/splash/SplashActivity.ktandroid/app/src/main/java/com/on/turip/ui/splash/SplashViewmodel.ktandroid/app/src/main/res/layout/activity_splash.xmlandroid/app/src/main/res/values-v31/themes.xmlandroid/app/src/main/res/values/colors.xmlandroid/app/src/main/res/values/strings.xmlandroid/gradle/libs.versions.toml
💤 Files with no reviewable changes (7)
- android/app/src/main/java/com/on/turip/domain/login/usecase/CheckUserSignedInUseCase.kt
- android/app/src/main/java/com/on/turip/common/AuthState.kt
- android/app/src/main/java/com/on/turip/common/UserType.kt
- android/app/src/main/java/com/on/turip/domain/login/usecase/LoginUserUseCase.kt
- android/app/src/main/res/layout/activity_splash.xml
- android/app/src/main/java/com/on/turip/ui/splash/SplashViewmodel.kt
- android/app/src/main/java/com/on/turip/data/login/TokenAuthenticator.kt
android/app/src/main/java/com/on/turip/data/session/DefaultTokenManager.kt
Outdated
Show resolved
Hide resolved
android/app/src/main/java/com/on/turip/data/session/DefaultTokenManager.kt
Show resolved
Hide resolved
android/app/src/main/java/com/on/turip/domain/login/usecase/LoginUseCase.kt
Outdated
Show resolved
Hide resolved
android/app/src/main/java/com/on/turip/domain/session/usecase/SwitchToGuestUseCase.kt
Outdated
Show resolved
Hide resolved
| import javax.inject.Inject | ||
|
|
||
| @HiltViewModel | ||
| class SplashViewmodel @Inject constructor( |
There was a problem hiding this comment.
c: 컨벤션 통일해야 될 거 같아유 SplashViewModel로 변경하는 게 좋아보이네용
There was a problem hiding this comment.
오, 매의 눈 제리! 👍
스플래시 화면 처음 구현했을 때부터 지금까지 전혀 몰랐네요, 😅
반영 커밋 : b107a94
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewModel.kt`:
- Around line 22-27: Wrap the call to determineInitialSessionUseCase() inside a
try/catch within the existing viewModelScope.launch so any exceptions from
tokenManager.initialize(), sessionStore.setGuest/setMember(),
switchToGuestUseCase(), or the use case itself are caught; in the catch block
send a fallback effect via _uiEffect.send(SplashUiEffect.NavigateToLogin) (or
another agreed fallback) to ensure the splash never deadlocks, and keep the
original when handling for successful SessionState.Member/Guest/Uninitialized
paths unchanged.
In `@android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt`:
- Around line 111-117: The current AppUpdateInfo.startUpdate extension calls
appUpdateManager.startUpdateFlowForResult(...) without checking preconditions or
handling a false return; update this flow to first verify updateAvailability()
== UPDATE_AVAILABLE and isUpdateTypeAllowed(updateType) before calling
appUpdateManager.startUpdateFlowForResult, then check its boolean result and if
it returns false navigate to the main screen (rather than leaving the splash
stuck or calling finish()), and add logic in onResume() to detect
DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS and resume the update when appropriate;
reference the AppUpdateInfo.startUpdate function,
appUpdateManager.startUpdateFlowForResult,
updateAvailability()/UPDATE_AVAILABLE, isUpdateTypeAllowed(), updateLauncher,
and onResume()/DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS when applying the changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7b19e3f8-6830-49a3-8cee-d5844ba0730b
📒 Files selected for processing (2)
android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewModel.ktandroid/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt
| viewModelScope.launch { | ||
| val sessionState: SessionState = determineInitialSessionUseCase() | ||
| when (sessionState) { | ||
| is SessionState.Member -> _uiEffect.send(SplashUiEffect.NavigateToMain) | ||
| SessionState.Guest, SessionState.Uninitialized -> _uiEffect.send(SplashUiEffect.NavigateToLogin) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "SplashViewModel.kt" -type fRepository: woowacourse-teams/2025-Turip
Length of output: 149
🏁 Script executed:
fd -t f "SplashViewModel\.kt"Repository: woowacourse-teams/2025-Turip
Length of output: 147
🏁 Script executed:
cat -n android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewModel.ktRepository: woowacourse-teams/2025-Turip
Length of output: 1474
🏁 Script executed:
find . -name "*DetermineInitialSessionUseCase*" -type fRepository: woowacourse-teams/2025-Turip
Length of output: 169
🏁 Script executed:
rg "class DetermineInitialSessionUseCase" -t kotlin -A 20Repository: woowacourse-teams/2025-Turip
Length of output: 2826
🏁 Script executed:
cat -n android/app/src/main/java/com/on/turip/domain/session/usecase/DetermineInitialSessionUseCase.ktRepository: woowacourse-teams/2025-Turip
Length of output: 2117
🏁 Script executed:
rg "viewModelScope" android/app/src/main/java/com/on/turip/ui/compose -t kotlin -B 5 -A 5 | head -80Repository: woowacourse-teams/2025-Turip
Length of output: 8325
🏁 Script executed:
rg "CoroutineExceptionHandler" android/app/src/main/java/com/on/turip -t kotlin -B 2 -A 2Repository: woowacourse-teams/2025-Turip
Length of output: 54
🏁 Script executed:
rg "viewModelScope.launch \{" -A 20 android/app/src/main/java/com/on/turip/ui/compose -t kotlin | grep -A 20 "try \{"Repository: woowacourse-teams/2025-Turip
Length of output: 90
🏁 Script executed:
rg "runCatching" android/app/src/main/java/com/on/turip/ui/compose -t kotlin -B 2 -A 5Repository: woowacourse-teams/2025-Turip
Length of output: 54
🏁 Script executed:
rg "try \{" android/app/src/main/java/com/on/turip/ui/compose -t kotlin -B 3 -A 8 | head -60Repository: woowacourse-teams/2025-Turip
Length of output: 6260
🏁 Script executed:
rg "\.fold\(" android/app/src/main/java/com/on/turip/ui/compose -t kotlin -B 2 -A 5 | head -80Repository: woowacourse-teams/2025-Turip
Length of output: 54
세션 판별 실패 시 스플래시에서 크래시가 날 수 있습니다.
determineInitialSessionUseCase() 내부의 tokenManager.initialize(), sessionStore.setGuest/setMember(), switchToGuestUseCase() 호출에서 예외가 발생하면 아무 처리 없이 coroutine이 실패합니다. 결과적으로 _uiEffect가 전송되지 않아 스플래시 화면이 멈추게 됩니다. 예외 발생 시 fallback effect를 명시적으로 전송하도록 수정하세요.
제안 수정안
fun determineStartDestination() {
viewModelScope.launch {
- val sessionState: SessionState = determineInitialSessionUseCase()
- when (sessionState) {
- is SessionState.Member -> _uiEffect.send(SplashUiEffect.NavigateToMain)
- SessionState.Guest, SessionState.Uninitialized -> _uiEffect.send(SplashUiEffect.NavigateToLogin)
- }
+ val nextEffect: SplashUiEffect = runCatching {
+ val sessionState: SessionState = determineInitialSessionUseCase()
+ when (sessionState) {
+ is SessionState.Member -> SplashUiEffect.NavigateToMain
+ SessionState.Guest, SessionState.Uninitialized -> SplashUiEffect.NavigateToLogin
+ }
+ }.getOrElse {
+ SplashUiEffect.NavigateToLogin
+ }
+
+ _uiEffect.send(nextEffect)
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewModel.kt`
around lines 22 - 27, Wrap the call to determineInitialSessionUseCase() inside a
try/catch within the existing viewModelScope.launch so any exceptions from
tokenManager.initialize(), sessionStore.setGuest/setMember(),
switchToGuestUseCase(), or the use case itself are caught; in the catch block
send a fallback effect via _uiEffect.send(SplashUiEffect.NavigateToLogin) (or
another agreed fallback) to ensure the splash never deadlocks, and keep the
original when handling for successful SessionState.Member/Guest/Uninitialized
paths unchanged.
There was a problem hiding this comment.
예외 발생해도 다 타입으로 내려주도록 되어 있는데요..?
There was a problem hiding this comment.
@yrsel, 맞습니다. DetermineInitialSessionUseCase의 AI 요약을 다시 확인하니, 해당 유스케이스는 토큰 없음 → Guest, 토큰 검증 성공 → Member, 인증 오류 → Guest, 그 외 오류 → Member 등 모든 경로에서 예외를 내부적으로 처리하여 항상 SessionState 타입으로 반환하도록 설계되어 있네요. 제 리뷰 코멘트가 잘못되었습니다. 죄송합니다!
🧠 Learnings used
Learnt from: yrsel
Repo: woowacourse-teams/2025-Turip PR: 483
File: android/app/src/main/java/com/on/turip/ui/compose/login/LoginScreen.kt:30-30
Timestamp: 2025-11-30T12:23:26.672Z
Learning: In Android projects using Hilt with Jetpack Compose, the correct import for hiltViewModel() as of 2025 is androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel from the androidx.hilt:hilt-lifecycle-viewmodel-compose artifact. The old import androidx.hilt.navigation.compose.hiltViewModel from androidx.hilt:hilt-navigation-compose is deprecated.
Learnt from: yrsel
Repo: woowacourse-teams/2025-Turip PR: 483
File: android/app/src/main/java/com/on/turip/ui/main/favorite/FavoriteContentFragment.kt:92-99
Timestamp: 2025-12-02T06:06:33.426Z
Learning: In Android Kotlin Fragments, when collecting Flow or StateFlow in onViewCreated or setupObservers, always use viewLifecycleOwner.lifecycleScope.launch with viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) pattern instead of just lifecycleScope.launch. This prevents multiple overlapping collectors when the view is detached and reattached, and ensures collection stops when the view is destroyed. Example: viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { flow.collect { } } }
| private fun AppUpdateInfo.startUpdate(updateType: Int) { | ||
| appUpdateManager.startUpdateFlowForResult( | ||
| this, | ||
| updateLauncher, | ||
| AppUpdateOptions.newBuilder(updateType).build(), | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Google Play Core In-App Updates AppUpdateManager.startUpdateFlowForResult return value boolean documentation
💡 Result:
Per the official Play In-App Updates API reference, AppUpdateManager.startUpdateFlowForResult(...) returns a boolean indicating whether the requested update flow was successfully started (i.e., the update UI launch was initiated). If it returns false, the update flow did not start. [1]
Documentation: AppUpdateManager → startUpdateFlowForResult(AppUpdateInfo, ActivityResultLauncher<IntentSenderRequest>, AppUpdateOptions) → Returns: “boolean whether the requested update has started.” [1]
Source(s)
[1] Android Developers – AppUpdateManager API reference (method startUpdateFlowForResult(...) return value) (developer.android.com)
Citations:
🌐 Web query:
Google Play In-App Updates startUpdateFlowForResult false return handling best practices what to do
💡 Result:
AppUpdateManager.startUpdateFlowForResult(...) returning false means the Play UI update flow did not start, so you should treat it as “no update UI was launched” and continue your normal app UX (optionally with a fallback). The method’s return value is explicitly “whether the requested update has started”. [1]
Best-practice handling when it returns false
-
Always pre-check before starting the flow
- Only call
startUpdateFlowForResultwhen:updateAvailability()isUPDATE_AVAILABLE, or you’re resuming an already-started update inDEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS, and- the chosen update type/options are allowed via
isUpdateTypeAllowed(...). [2][3]
If the type isn’t allowed, the flow won’t start. [1]
- Only call
-
If you need to retry, fetch a fresh
AppUpdateInfo- Each
AppUpdateInfoinstance is single-use for starting the update flow; reusing it for a retry can prevent the flow from starting (or throw) — so onfalse, callgetAppUpdateInfo()again and re-evaluate availability/allowed-ness before retrying. [1][2]
- Each
-
Resume stalled/in-progress updates on foreground
- In
onResume/when returning to foreground, ifupdateAvailability()isDEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS, callstartUpdateFlowForResult(...)again to resume. [3]
- In
-
Handle the result separately from the boolean
- The boolean only says whether the UI launch started.
- The user outcome comes from the activity result (accepted / canceled / failed), and your app “should determine the best way to proceed” when the Play UI closes. [2]
-
Have a fallback when the flow can’t be started
- Common fallback: open your Play Store listing (e.g.,
market://details?id=...), or show a non-blocking “Update available” UI and let the user try later—especially ifstartUpdateFlowForResultreturnsfalserepeatedly.
- Common fallback: open your Play Store listing (e.g.,
-
Testing sanity check
- In-app updates generally won’t behave as expected unless the app was installed from Google Play (internal/closed/open testing, internal app sharing, etc.). [4]
Practical default: if startUpdateFlowForResult(...) returns false, log it (with updateAvailability + isUpdateTypeAllowed), fetch a fresh AppUpdateInfo, and if it still can’t start, fall back to opening the Play Store listing or defer the prompt.
Sources
[1] https://developer.android.com/reference/com/google/android/play/core/appupdate/AppUpdateManager
[2] https://developer.android.com/guide/playcore/in-app-updates/kotlin-java
[3] https://developer.android.com/guide/playcore/in-app-updates/kotlin-java (resume DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS)
[4] https://stackoverflow.com/questions/57534545/appupdatemanager-startupdateflowforresult-not-showing-dialog
🌐 Web query:
splash screen in-app update flow handling when startUpdateFlowForResult returns false Google Play
💡 Result:
If AppUpdateManager.startUpdateFlowForResult(...) returns false, it means the requested update flow did not start—either because the update isn’t allowed for the provided AppUpdateInfo/AppUpdateOptions, or because an error/invalid request occurred. You should treat it as “no Play UI will appear” and continue your normal app navigation (e.g., leave the splash screen), unless you have a hard requirement to block usage. [1]
What to do (typical splash-screen handling)
- Only call
startUpdateFlowForResultwhen:updateAvailability() == UPDATE_AVAILABLE, andisUpdateTypeAllowed(AppUpdateType.IMMEDIATE/FLEXIBLE)is true (or equivalent withAppUpdateOptions). (This prevents many “returns false” cases.)
- If it returns false:
- Don’t keep the splash stuck waiting—navigate to your main screen.
- Optionally retry later (next app entry point / next
onResume) rather than looping on splash.
- Also implement the resume path:
- On
onResume(), ifupdateAvailability() == DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS, callstartUpdateFlowForResult(...)again to resume an immediate update the user already started. [2]
- On
- For a mandatory update experience:
- If starting the in-app flow fails (returns false) you can show an explanatory screen and offer a button to open the Play Store listing as a fallback—but avoid trapping users on splash without a way forward.
Sources: Android Developers Play in-app updates reference (return value semantics) [1], Android Developers Kotlin/Java in-app updates guide (resume + cancel/decline handling guidance) [2].
반환값 확인 및 실패 시 처리 방식 개선 필요
startUpdateFlowForResult()는 boolean을 반환하여 업데이트 플로우가 성공적으로 시작되었는지를 나타냅니다. false 반환 시 플로우가 시작되지 않았으므로 스플래시 화면에 사용자가 갇히게 됩니다. 다음과 같이 개선하세요:
- 호출 전에 사전 조건 확인:
updateAvailability() == UPDATE_AVAILABLE이고isUpdateTypeAllowed()가 true인 경우에만 호출 - 반환값 확인 후 false 시:
finish()호출 대신 메인 화면으로 진행하여 사용자가 앱을 계속 사용할 수 있도록 함 onResume()에서DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS상태를 확인하여 진행 중인 업데이트 재개
또는 유연한 업데이트 사용 시에는 false 반환 상황에 대해 나중에 재시도할 수 있도록 처리하는 것이 권장됩니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt` around
lines 111 - 117, The current AppUpdateInfo.startUpdate extension calls
appUpdateManager.startUpdateFlowForResult(...) without checking preconditions or
handling a false return; update this flow to first verify updateAvailability()
== UPDATE_AVAILABLE and isUpdateTypeAllowed(updateType) before calling
appUpdateManager.startUpdateFlowForResult, then check its boolean result and if
it returns false navigate to the main screen (rather than leaving the splash
stuck or calling finish()), and add logic in onResume() to detect
DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS and resume the update when appropriate;
reference the AppUpdateInfo.startUpdate function,
appUpdateManager.startUpdateFlowForResult,
updateAvailability()/UPDATE_AVAILABLE, isUpdateTypeAllowed(), updateLauncher,
and onResume()/DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS when applying the changes.
Issues
✔️ Check-list
🗒️ Work Description
#618 PR 에 이어서 작업해서 커밋 내역이 섞여 있어요
당장 확인할 때는 ❗️링크를 통해 변경사항 확인 부탁드려요!
Navigation 적용하며 단일 Activity 구조에 Splash Screen API 로 구현하는 게 좋을 것 같아서 Compose 로 화면만 보여주도록 수정했습니다!
📷 Screenshot
정상 작동
record.webm
📚 Reference
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항