Skip to content

Conversation

@l5x5l
Copy link
Contributor

@l5x5l l5x5l commented Dec 6, 2025

[ PR Content ]

각 화면의 ViewModel을 구현할 때 기존 common/viewModel에 선언한 MviViewModel을 사용하는 방식 대신
각 ViewModel에서 직접적으로 orbit을 사용하는 방식으로 수정합니다.

Related issue

Screenshot 📸

x

Work Description

  • 앱 내 모든 ViewModel에서 직접적으로 Orbit의 ContainerHost를 사용하도록 수정
  • 기존 MviViewModel 및 관련 클래스, 테스트코드 제거

To Reviewers 📢

  • 이상한 부분 있다면 코멘트 부탁드립니다!

Summary by CodeRabbit

릴리스 노트

  • Refactor
    • 상태 관리 아키텍처 현대화로 앱의 내부 구조 개선
    • 화면 상태 및 부작용 처리 로직 최적화
    • 코드베이스 유지보수성 향상 및 성능 최적화

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 2025

Walkthrough

MviViewModel 기반 MVI 아키텍처에서 Orbit MVI로 마이그레이션하는 대규모 리팩토링입니다. 기존 MviIntent, MviSideEffect, MviState, MviViewModel 기본 클래스를 제거하고, 11개의 화면/ViewModel에서 Orbit의 ContainerHost와 ViewModel을 직접 사용하도록 변경하며, 기존 Intent 기반 상태 관리를 직접 메서드 호출로 대체합니다.

Changes

Cohort / File(s) 변경 요약
MVI 기본 클래스 제거
presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviState.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModel.kt
MVI 패턴의 기본 인터페이스 및 추상 클래스 완전 삭제
감정 구슬 선택 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt
ViewModel을 Orbit ContainerHost로 마이그레이션, Intent 제거, 상태 수집 방식 변경
가이드 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideState.kt
Orbit 컨테이너 기반 상태 관리로 변경, Intent 클래스 제거
로그인 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt
Orbit 기반 아키텍처 도입, Intent 제거
마이페이지 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt
Orbit 컨테이너로 마이그레이션, Intent 파일 제거
온보딩 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt
복잡한 상태 관리를 Orbit으로 리팩토링, Intent 시스템 제거
추천 루틴 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineState.kt
ViewModel 메서드 기반 이벤트 처리로 변경, Intent 삭제
루틴 리스트 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt
Orbit 마이그레이션, 공개 메서드 기반 이벤트 처리
스플래시 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt
Orbit 컨테이너 기반 초기화 로직, Intent 제거
약관 동의 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementState.kt
Orbit 기반 아키텍처, Intent 방식 제거
루틴 작성 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineIntent.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineSideEffect.kt
presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineState.kt
복잡한 폼 상태를 Orbit으로 마이그레이션, Intent 제거
설정 및 기타 화면
presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt
MviSideEffect 상속 제거, 독립형 sealed class로 변경
테스트 제거
presentation/src/test/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModelTest.kt
MviViewModel 테스트 파일 완전 삭제

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

추가 검토 사항:

  • EmotionViewModel: selectEmotion, selectRecommendRoutine, registerRecommendRoutines 등 복합 비동기 로직이 Orbit 패턴에 올바르게 마이그레이션되었는지 확인 필요
  • OnBoardingViewModel: 다양한 로딩 상태(loadIntro, loadUserOnBoarding, loadRecommendRoutines)와 상태 전이 로직이 intent/reduce 패턴에서 정확히 동작하는지 검증 필요
  • RoutineListViewModel: 날짜 계산 및 루틴 선택 관련 상태 관리 로직의 정확성 확인
  • WriteRoutineViewModel: 복잡한 폼 상태(시간, 날짜, 반복 설정 등) 관리가 Orbit 패턴에서 정확히 동작하는지 확인
  • 상태 초기값: 각 State 클래스에서 기본값이 companion object의 INIT으로 이동했으므로, 이를 사용하는 모든 호출 지점에서 올바르게 참조하는지 확인
  • MviSideEffect 제거: 일부 SideEffect 클래스(SettingSideEffect)에서만 MviSideEffect 상속이 제거됨. 다른 화면들의 일관성 확인 필요

Possibly related PRs

  • PR #159: 동일한 MviViewModel → Orbit 마이그레이션 작업으로, 동일한 파일들과 구체적인 치환 패턴이 일치하여 직접 관련

Poem

🐰 Intent 없이 Orbit 위로 춤을 추고
State 구슬들이 모여 반짝이네요
Container 속 마법으로 상태 변하고
SideEffect 흘러 화면 업데이트 ✨
번거로움 덜고 우아함 더한
새로운 패턴의 시작이에요!

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 The PR title clearly and specifically describes the main change: replacing MviViewModel usage with direct Orbit implementation across the app.
Linked Issues check ✅ Passed The code changes comprehensively address issue #138 objectives: all 11 screens' ViewModels have been migrated from MviViewModel to Orbit ContainerHost, MVI-related interfaces (MviIntent, MviSideEffect, MviState) and the base MviViewModel class have been removed.
Out of Scope Changes check ✅ Passed All changes are directly related to the PR objectives. State management modifications in Screens and ViewModels, removal of MVI infrastructure classes, and test cleanup are all within scope of the MviViewModel-to-Orbit migration.
Description check ✅ Passed PR 설명이 템플릿의 주요 섹션을 포함하고 있지만 일부 선택사항이 누락되었습니다.
✨ 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 refactor/#138-apply_orbit

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.

@l5x5l l5x5l requested a review from wjdrjs00 December 6, 2025 11:39
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt (1)

300-301: 빈 오류 처리 블록 개선 필요

onFailure 블록이 비어있어 오류 발생 시 사용자에게 피드백이 없습니다. 최소한 로깅이나 토스트 메시지 표시를 고려해주세요.

다음과 같이 오류 처리를 추가할 수 있습니다:

 onFailure = {
+    postSideEffect(OnBoardingSideEffect.ShowToast(message = it.message ?: "오류가 발생했습니다."))
 },

Also applies to: 357-359

presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt (1)

55-82: Fix loading flag handling in loadRoutine and loadRecommendRoutine onFailure blocks

Two items to address:

  1. Missing loading flag reset on failure
    Both loadRoutine (line 121–123) and loadRecommendRoutine (line 153–155) set loading = true on entry but have empty onFailure blocks. If the network call fails, the UI remains indefinitely in loading state. Add loading = false in both onFailure blocks:
onFailure = {
    reduce {
        state.copy(loading = false)
    }
},
  1. Nested intent pattern in initResource
    initResource is itself an intent { ... } block but calls loadRoutine/loadRecommendRoutine (also intent { ... } functions) at lines 66 and 78. While functional, this queues multiple intent blocks sequentially rather than executing a single unified intent. Consider refactoring loadRoutine and loadRecommendRoutine into internal suspend helper functions and consolidating their logic into a single intent { ... } block within initResource for better alignment with Orbit patterns.
🧹 Nitpick comments (9)
presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt (1)

49-51: 예외가 무시되고 있습니다.

자동 로그인 실패 시 예외를 catch하지만 로깅하지 않아 디버깅이 어려울 수 있습니다. 최소한 로그를 남기는 것을 권장합니다.

             } catch (e: Exception) {
+                // TODO: Consider logging the exception for debugging
+                // Log.w(TAG, "Auto login failed", e)
                 reduce { state.copy(userRole = null, isAutoLoginCompleted = true) }
             }
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt (1)

91-109: reduce 블록 내 부수 효과 분리를 권장합니다.

Line 97에서 recommendRoutines 프로퍼티 할당이 reduce 블록 내부에서 이루어지고 있습니다. reduce는 순수 상태 변환만 담당하는 것이 Orbit의 권장 패턴입니다.

     private fun loadRecommendRoutines() {
         intent {
             reduce { state.copy(isLoading = true) }
             fetchRecommendRoutinesUseCase().fold(
                 onSuccess = {
+                    recommendRoutines = it.toUiModel()
                     reduce {
-                        recommendRoutines = it.toUiModel()
                         state.copy(
                             isLoading = false,
                             currentRoutines = getCurrentRoutines(state.selectedCategory, state.selectedRecommendLevel),
                             emotionMarbleType = recommendRoutines.emotionMarbleType,
                         )
                     }
                 },
                 onFailure = {
                     reduce { state.copy(isLoading = false) }
                 },
             )
         }
     }
presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt (2)

65-87: intent {} 블록 내 viewModelScope.launch 중첩 사용 검토 필요

intent {} 블록 내에서 viewModelScope.launch를 사용하면, intent 블록이 완료된 후에도 launch된 코루틴이 독립적으로 실행됩니다. 기술적으로 동작하지만, GuideViewModel 등 다른 마이그레이션된 ViewModel과 패턴이 다릅니다.

delay() 사용이 필요한 경우, intent 블록 자체가 suspend context를 제공하므로 직접 호출이 가능합니다:

     fun selectEmotion(emotionType: String, minimumDelay: Long = 0) =
         intent {
             val isLoading = state.isLoading
             if (isLoading) return@intent

             reduce {
                 state.copy(
                     isLoading = true,
                     showLoadingView = true,
                 )
             }

-            viewModelScope.launch {
-                if (minimumDelay > 0) {
-                    delay(minimumDelay)
-                }
+            if (minimumDelay > 0) {
+                delay(minimumDelay)
+            }

-                registerEmotionUseCase(emotionType = emotionType).fold(
-                    onSuccess = { emotionRecommendRoutines ->
-                        val recommendRoutines = emotionRecommendRoutines.map { EmotionRecommendRoutineUiModel.fromEmotionRecommendRoutine(it) }
-                        reduce {
-                            state.copy(
-                                recommendRoutines = recommendRoutines,
-                                step = EmotionScreenStep.RecommendRoutines,
-                                isLoading = false,
-                                showLoadingView = false,
-                            )
-                        }
-                    },
-                    onFailure = {
-                        postSideEffect(EmotionSideEffect.ShowToast(message = it.message ?: "에러가 발생했습니다. 잠시 후 시도해주세요."))
-                        postSideEffect(EmotionSideEffect.NavigateToBack)
-                    },
-                )
-            }
+            registerEmotionUseCase(emotionType = emotionType).fold(
+                onSuccess = { emotionRecommendRoutines ->
+                    val recommendRoutines = emotionRecommendRoutines.map { EmotionRecommendRoutineUiModel.fromEmotionRecommendRoutine(it) }
+                    reduce {
+                        state.copy(
+                            recommendRoutines = recommendRoutines,
+                            step = EmotionScreenStep.RecommendRoutines,
+                            isLoading = false,
+                            showLoadingView = false,
+                        )
+                    }
+                },
+                onFailure = {
+                    postSideEffect(EmotionSideEffect.ShowToast(message = it.message ?: "에러가 발생했습니다. 잠시 후 시도해주세요."))
+                    postSideEffect(EmotionSideEffect.NavigateToBack)
+                },
+            )
         }

121-133: registerRecommendRoutines에서도 동일한 패턴 적용 가능

selectEmotion과 마찬가지로 viewModelScope.launch 중첩을 제거하여 일관성을 유지할 수 있습니다.

     fun registerRecommendRoutines() =
         intent {
             val isLoading = state.isLoading
             if (isLoading) return@intent

-            viewModelScope.launch {
-                reduce { state.copy(isLoading = true) }
+            reduce { state.copy(isLoading = true) }

-                val selectedRecommendRoutineIds = state.recommendRoutines.filter { it.selected }.map { it.id }
-                registerRecommendOnBoardingRoutinesUseCase(selectedRecommendRoutineIds).fold(
-                    onSuccess = {
-                        postSideEffect(EmotionSideEffect.NavigateToBack)
-                    },
-                    onFailure = {
-                        reduce { state.copy(isLoading = false) }
-                    },
-                )
-            }
+            val selectedRecommendRoutineIds = state.recommendRoutines.filter { it.selected }.map { it.id }
+            registerRecommendOnBoardingRoutinesUseCase(selectedRecommendRoutineIds).fold(
+                onSuccess = {
+                    postSideEffect(EmotionSideEffect.NavigateToBack)
+                },
+                onFailure = {
+                    reduce { state.copy(isLoading = false) }
+                },
+            )
         }
presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt (1)

274-304: loadRecommendRoutines 패턴 일관성 검토 권장

이 함수는 intent 블록을 사용하지 않고 viewModelScope.async를 직접 사용합니다. 다른 함수들은 모두 intent 블록을 사용하는데, 이 함수만 예외입니다.

현재 구현도 동작하지만(job 취소 로직 포함), 패턴 일관성을 위해 다음 중 하나를 고려해볼 수 있습니다:

  1. 전체를 intent 블록으로 감싸고 내부에서 async 사용
  2. 현재대로 유지하되 주석으로 의도 명시
presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt (1)

78-84: Orbit 패턴과의 일관성을 위해 intent/subIntent 사용을 고려해 보세요.

현재 viewModelScope.launch를 사용하고 있는데, HomeViewModelobserveWriteRoutineEvent 등에서는 subIntent를 사용하여 Flow를 수집합니다. 기능적으로는 동일하게 동작하지만, 프로젝트 전반의 일관성을 위해 Orbit DSL 사용을 고려해 볼 수 있습니다.

     private fun observeRoutineChanges() {
-        viewModelScope.launch {
-            getWriteRoutineEventFlowUseCase().collect {
-                fetchRoutines()
+        intent {
+            getWriteRoutineEventFlowUseCase().collect {
+                fetchRoutines()
+            }
         }
-        }
     }
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt (1)

105-122: 선택적 개선: 불필요한 람다 래핑을 제거할 수 있습니다.

Lines 105, 113, 121에서 onCheckedChange = { onToggleTermsOfService() } 형태로 람다를 사용하고 있는데, onCheckedChange = onToggleTermsOfService처럼 직접 전달하는 것이 더 간결합니다.

다음과 같이 수정할 수 있습니다:

 TermsAgreementItem(
     title = "(필수) 서비스 이용약관 동의",
-    onCheckedChange = { onToggleTermsOfService() },
+    onCheckedChange = onToggleTermsOfService,
     isChecked = uiState.agreedTermsOfService,
     showMore = true,
     onClickShowMore = onShowTermsOfService,
 )

 TermsAgreementItem(
     title = "(필수) 개인정보 수집·이용 동의",
-    onCheckedChange = { onTogglePrivacyPolicy() },
+    onCheckedChange = onTogglePrivacyPolicy,
     isChecked = uiState.agreedPrivacyPolicy,
     showMore = true,
     onClickShowMore = onShowPrivacyPolicy,
 )

 TermsAgreementItem(
     title = "(필수) 만 14세 이상입니다.",
-    onCheckedChange = { onToggleOverFourteen() },
+    onCheckedChange = onToggleOverFourteen,
     isChecked = uiState.agreedOverFourteen,
 )
presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt (1)

40-60: LoginScreenContainer의 ViewModel 주입과 사이드 이펙트 처리가 깔끔합니다.

  • viewModel: LoginViewModel = hiltViewModel()로 기본값을 둔 덕분에 NavGraph 쪽에서는 필요한 경우에만 명시적으로 주입을 덮어쓸 수 있고, 프리뷰/테스트 시에도 별도 오버로드 없이 대체 ViewModel을 주입하기 수월해졌습니다.
  • collectSideEffect에서 when (sideEffect) 분기를 is LoginSideEffect.NavigateToHome -> navigateToHome() 형태로 정리한 것도 가독성이 좋아졌고, sealed interface 기반이라 컴파일 타임에 분기 누락도 잡을 수 있겠습니다.
  • 프리뷰는 실제 UI만 렌더링하도록 LoginScreen만 호출하고 있어 Hilt 의존성 없이도 안전하게 동작할 것 같아요. @Preview(showBackground = true) 변경도 디자인 확인에 도움이 될 듯합니다.

현재 구조에서는 상태를 사용하지 않고 사이드 이펙트만 처리하고 있는데, 향후 로딩 표시나 버튼 비활성화 등이 필요해지면 container.stateFlow/collectAsState로 확장만 해주면 될 것 같습니다.

Also applies to: 133-140

presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt (1)

20-54: kakaoLogin 에지 케이스 및 실패 시 UX 보완 여지가 있습니다.

  1. token·error 둘 다 null인 경우 로딩 해제 누락 가능성

현재 분기:

intent {
    reduce { state.copy(isLoading = true) }
    when {
        token != null -> processKakaoLoginSuccess(token)
        error != null -> {
            reduce { state.copy(isLoading = false) }
            Log.e("KakaoLogin", "카카오 로그인 실패", error)
        }
    }
}

Kakao SDK가 보장해 준다면 문제가 없지만, 방어적으로 보면 token == null && error == null인 경우 isLoading이 계속 true로 남습니다. 에지 케이스 방지를 위해 else 분기를 추가하는 것을 권장합니다:

-        when {
-            token != null -> processKakaoLoginSuccess(token)
-
-            error != null -> {
-                reduce { state.copy(isLoading = false) }
-                Log.e("KakaoLogin", "카카오 로그인 실패", error)
-            }
-        }
+        when {
+            token != null -> processKakaoLoginSuccess(token)
+
+            error != null -> {
+                reduce { state.copy(isLoading = false) }
+                Log.e("KakaoLogin", "카카오 로그인 실패", error)
+            }
+
+            else -> {
+                reduce { state.copy(isLoading = false) }
+                Log.e("KakaoLogin", "토큰과 에러가 모두 null 입니다.")
+            }
+        }
  1. 로그인 실패 시 UI 피드백

processKakaoLoginSuccesserror != null 분기 모두 Log만 남기고 있어서, 사용자 입장에서는 “아무 일도 안 일어나는 것처럼” 보일 수도 있습니다. 나중에 여유가 되면 LoginSideEffect.ShowLoginError(message) 같은 사이드 이펙트를 추가해서 스낵바/다이얼로그로 노출하는 것도 고려해 볼 만합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb7a181 and ef9a971.

📒 Files selected for processing (56)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviSideEffect.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviState.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModel.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt (2 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideScreen.kt (3 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt (2 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt (2 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt (7 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt (2 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt (3 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt (4 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt (3 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt (3 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt (4 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementState.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt (6 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineIntent.kt (0 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineSideEffect.kt (1 hunks)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineState.kt (2 hunks)
  • presentation/src/test/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModelTest.kt (0 hunks)
💤 Files with no reviewable changes (15)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviSideEffect.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineIntent.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineIntent.kt
  • presentation/src/test/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModelTest.kt
🧰 Additional context used
🧠 Learnings (5)
📚 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/login/model/LoginState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingSideEffect.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideScreen.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt
📚 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/login/model/LoginState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt
📚 Learning: 2025-07-23T13:32:26.263Z
Learnt from: l5x5l
Repo: YAPP-Github/Bitnagil-Android PR: 41
File: presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageIntent.kt:6-6
Timestamp: 2025-07-23T13:32:26.263Z
Learning: In the Bitnagil Android project's MVI architecture, Intent classes like `LoadMyPageSuccess` are named to represent successful API response results that carry loaded data, not just user actions. This naming convention is used for future API integration where the intent will be triggered when my page data loading succeeds from the server.

Applied to files:

  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt
📚 Learning: 2025-11-22T08:34:05.454Z
Learnt from: l5x5l
Repo: YAPP-Github/Bitnagil-Android PR: 151
File: presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt:122-153
Timestamp: 2025-11-22T08:34:05.454Z
Learning: In Jetpack Compose LazyColumn, calling `stickyHeader` and `itemsIndexed` inside a `forEach` loop on a collection works correctly. The LazyListScope DSL properly handles these calls even when they are nested inside a forEach block, allowing sticky headers to function as expected.

Applied to files:

  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt
📚 Learning: 2025-07-21T10:38:49.104Z
Learnt from: l5x5l
Repo: YAPP-Github/Bitnagil-Android PR: 38
File: presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/component/atom/textbutton/TextButton.kt:30-35
Timestamp: 2025-07-21T10:38:49.104Z
Learning: presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/component/atom/textbutton/TextButton.kt의 TextButton 컴포넌트는 임시로 구현된 컴포넌트로, 디자인 시스템 구현시 대체 예정입니다.

Applied to files:

  • presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt
🧬 Code graph analysis (10)
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (1)
  • container (13-39)
presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (1)
  • container (13-39)
presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt (3)
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (1)
  • container (13-39)
data/src/main/java/com/threegap/bitnagil/data/routine/service/RoutineService.kt (2)
  • fetchRoutines (14-40)
  • fetchRoutines (15-19)
presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeViewModel.kt (1)
  • fetchWeeklyRoutinesUseCase (35-312)
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt (2)
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt (2)
  • navigateToEmotion (112-116)
  • navigateToRegisterRoutine (118-122)
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/component/template/RecommendLevelBottomSheet.kt (1)
  • RecommendLevelBottomSheet (24-75)
presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (1)
  • container (13-39)
presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (1)
  • container (13-39)
presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (1)
  • container (13-39)
presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (1)
  • container (13-39)
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt (2)
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt (3)
  • navigateToPrivacyPolicy (83-87)
  • navigateToTermsOfService (77-81)
  • navigateToBack (89-93)
presentation/src/main/java/com/threegap/bitnagil/presentation/terms/component/TermsAgreementItem.kt (1)
  • TermsAgreementItem (26-76)
presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt (1)
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (1)
  • container (13-39)
🪛 detekt (1.23.8)
presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt

[warning] 49-49: 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 (49)
presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideSideEffect.kt (1)

3-5: LGTM!

MviSideEffect 상속 제거가 올바르게 되었습니다. Orbit에서는 별도의 기본 side effect 인터페이스가 필요하지 않습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideState.kt (1)

3-13: LGTM!

Orbit 마이그레이션에 맞게 MviState 상속 및 @parcelize 제거가 올바르게 되었습니다. companion object의 INIT 패턴은 container 초기화 시 명확한 초기 상태를 제공합니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideScreen.kt (2)

29-35: LGTM!

Orbit Compose의 collectAsState()collectSideEffect를 올바르게 사용하고 있습니다. Intent 기반 상호작용에서 직접 메서드 참조로 전환하여 코드가 더 간결해졌습니다.


46-49: 직접 메서드 참조 사용이 깔끔합니다.

viewModel::onShowGuideBottomSheet, viewModel::navigateToBack 같은 메서드 참조 패턴이 Intent 객체 생성 대비 보일러플레이트를 줄여줍니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt (2)

14-16: LGTM!

ContainerHost<GuideState, GuideSideEffect> 구현과 container(initialState = GuideState.INIT) 초기화가 Orbit 패턴에 맞게 올바르게 작성되었습니다.


18-38: Orbit DSL 사용이 적절합니다.

intent { reduce { } } 패턴으로 상태 변경, intent { postSideEffect() } 패턴으로 사이드 이펙트 발행이 Orbit의 표준 패턴을 잘 따르고 있습니다. 기존 MviViewModel의 복잡한 Intent 처리 로직 대비 훨씬 간결해졌습니다.

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

3-8: LGTM!

MviSideEffect 상속 제거 및 standalone sealed interface로의 전환이 Orbit 패턴에 맞게 잘 구현되었습니다. NavigateToOnboarding 추가도 적절합니다.

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

5-19: LGTM!

MviState 상속 제거 및 INIT companion object 패턴 적용이 다른 ViewModel들(GuideViewModel 등)과 일관성 있게 잘 구현되었습니다.

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

33-52: LGTM!

Orbit의 collectAsState()collectSideEffect 사용이 올바르게 구현되었습니다. 모든 SplashSideEffect 케이스가 적절히 처리되고 있습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt (2)

19-25: LGTM!

ContainerHost 구현과 container 초기화가 GuideViewModel과 일관성 있게 잘 구현되었습니다.


55-73: 재귀 호출 패턴 검토 필요.

intent 블록 내부에서 viewModelScope.launch로 재귀 호출하는 패턴이 사용되고 있습니다. 동작은 하겠지만, Orbit의 intent 컨텍스트 밖에서 다시 onAnimationCompleted()를 호출하는 구조입니다.

대안으로 repeatOnLifecycle이나 Flow를 사용한 상태 관찰 패턴을 고려해볼 수 있지만, 현재 구현도 기능적으로 문제없이 동작합니다.

현재 구현이 의도한 대로 동작하는지 확인해주세요. auto login이 완료되지 않은 상태에서 애니메이션이 완료되면 100ms 간격으로 폴링하여 로그인 완료를 기다리는 것이 맞는지 확인 부탁드립니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt (1)

3-6: MviSideEffect base class removal is correctly implemented.

The migration of SettingSideEffect from extending MviSideEffect to a standalone sealed class is clean and compatible with Orbit MVI. Verification confirms:

  • SettingViewModel correctly implements ContainerHost<SettingState, SettingSideEffect> and uses postSideEffect() to emit both NavigateToLogin and NavigateToWithdrawal
  • SettingScreen properly collects side effects using Orbit's collectSideEffect and handles all cases in the when statement
  • No breaking changes—Orbit's ContainerHost handles the sealed class type parameters directly without requiring a custom base class
presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt (1)

3-10: LGTM!

MviState 상속 제거가 깔끔하게 완료되었습니다. Orbit container 초기화를 위한 Init companion object가 적절히 유지되어 있습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt (1)

32-43: LGTM!

Orbit의 collectAsState() 확장함수를 사용한 상태 수집 방식으로 올바르게 마이그레이션되었습니다. ContainerHost에서 직접 호출하는 패턴이 다른 ViewModel들(예: GuideViewModel)과 일관성 있게 적용되었습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageSideEffect.kt (1)

3-3: LGTM!

MviSideEffect 상속 제거가 완료되었습니다. 현재 MyPageViewModel에서 side effect를 사용하지 않으므로 빈 sealed class로 유지하는 것이 적절합니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt (1)

13-17: LGTM!

Orbit ContainerHost 패턴으로의 마이그레이션이 올바르게 구현되었습니다. GuideViewModel과 일관된 구조를 따르고 있습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineState.kt (1)

7-27: LGTM! Orbit 패턴에 맞는 깔끔한 마이그레이션입니다.

INIT companion object를 통한 초기 상태 제공 방식이 GuideState.INIT 등 다른 ViewModel과 일관성 있게 구현되었습니다. MviState/Parcelize 제거 후 순수 data class로 전환한 것이 적절합니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineSideEffect.kt (1)

3-6: LGTM! Sealed interface로 side effect를 깔끔하게 정의했습니다.

data objectdata class를 적절히 구분하여 사용했습니다. Orbit의 postSideEffect 패턴과 잘 맞습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt (2)

51-58: LGTM! Orbit Compose 통합이 올바르게 구현되었습니다.

collectAsState()collectSideEffect를 사용하여 상태와 사이드 이펙트를 적절히 처리하고 있습니다. when 분기에서 모든 side effect 케이스를 exhaustive하게 처리하는 점도 좋습니다.


63-74: 직접 메서드 참조 방식으로 깔끔하게 전환되었습니다.

sendIntent 패턴에서 viewModel::methodName 형태의 직접 메서드 참조로 변경하여 코드가 더 간결하고 타입 안전해졌습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt (2)

21-28: LGTM! Orbit ContainerHost 패턴이 올바르게 적용되었습니다.

GuideViewModel과 동일한 패턴으로 ContainerHost<State, SideEffect>를 구현하고 container(initialState = ...)로 초기화하는 방식이 일관성 있게 적용되었습니다.


112-122: LGTM! Side effect 발행 패턴이 올바릅니다.

intent { postSideEffect(...) } 패턴이 GuideViewModel.navigateToBack()과 동일하게 적용되어 일관성 있습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineState.kt (1)

3-3: MviState → Parcelable 직접 구현 전환은 방향성/동작 면에서 적절해 보입니다

기존에 MviStateParcelable 을 상속하던 구조에서, 이 상태 클래스가 직접 Parcelable 을 구현하도록 바꾼 것으로 보여서, @Parcelize 가 유지되는 한 직렬화 동작은 그대로 유지될 것 같습니다.

한 가지 정도만 확인 부탁드립니다:

  • Date, Time, RepeatType, SelectableDay, WriteRoutineType 등 이 상태가 들고 있는 타입들이 모두 Parcelable(또는 @Parcelize 등을 통해 지원되는 타입) 인지, 혹은 기본적으로 Parcel에 쓸 수 있는 타입인지 한 번 더 확인해 주세요. 특히 이 상태를 Intent/Bundle 로 넘기는 경로가 있다면, 여기에서만 Parcelable 구현 방식이 바뀐 것이기 때문에 컴파일/런타임 크래시가 없는지만 체크해 두면 좋겠습니다.

전반적으로는 MVI 공용 추상화 의존성을 제거하면서도 이전과 같은 직렬화 특성을 유지하는 깔끔한 리팩토링으로 보입니다. 과거 PR 리딩에서 MviStateParcelable 을 상속하고 있던 것으로 기억해서, 이번 변경은 타입 의존성만 줄인 동등 치환(refactor)로 이해했습니다. Based on learnings.

Also applies to: 33-33

presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt (1)

3-18: LGTM!

MviState에서 Parcelable로의 마이그레이션이 올바르게 수행되었습니다. @Parcelize 어노테이션과 함께 Parcelable 구현이 Orbit의 savedStateHandle 기반 상태 저장과 잘 호환됩니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionSideEffect.kt (1)

3-6: LGTM!

MviSideEffect 상속 제거가 Orbit 패턴에 맞게 올바르게 처리되었습니다. Orbit은 side effect 타입에 특별한 인터페이스를 요구하지 않으므로 plain sealed class가 적합합니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt (1)

19-20: LGTM!

Orbit compose 확장 함수(collectAsState, collectSideEffect)로의 마이그레이션이 올바르게 수행되었습니다. 이 패턴은 다른 마이그레이션된 ViewModel들과 일관성을 유지합니다.

Also applies to: 27-27, 33-38

presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt (1)

22-30: LGTM!

Orbit ContainerHost 구현이 올바르게 설정되었습니다. savedStateHandle을 사용한 상태 저장 지원이 프로세스 종료 후 복원에 유용합니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingSideEffect.kt (1)

3-7: LGTM! MVI 기반 클래스 제거 완료

MviSideEffect 상속을 제거하여 Orbit으로의 마이그레이션을 깔끔하게 완료했습니다. Public API는 그대로 유지되어 호환성에 문제가 없습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt (1)

9-12: LGTM! 상태 클래스 마이그레이션 적절

MviState 제거와 함께 objectdata object로 변경한 것은 최신 Kotlin sealed class 권장사항에 부합하며, Parcelable 구현도 명시적으로 유지되어 있습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt (1)

23-24: LGTM! Orbit Compose 확장 함수 적용 완료

Orbit의 collectAsStatecollectSideEffect를 사용하도록 깔끔하게 마이그레이션되었습니다. UI 로직은 변경 없이 상태 수집 방식만 Orbit 패턴으로 전환되었습니다.

Also applies to: 32-47

presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt (3)

40-48: LGTM! Orbit ContainerHost 구현 적절

MviViewModel에서 Orbit의 ContainerHost로 올바르게 마이그레이션되었습니다. savedStateHandle을 활용한 container 초기화와 초기 상태 설정이 적절합니다.


73-86: LGTM! intent/reduce 패턴 일관성 있게 적용

모든 상태 변경 로직이 Orbit의 intent/reduce 블록을 사용하도록 체계적으로 마이그레이션되었습니다. 특히 selectPrevious 함수의 복잡한 분기 처리도 적절하게 변환되었습니다.

Also applies to: 88-124, 126-146, 148-164, 166-204, 218-272, 306-318, 324-338, 340-360, 362-364


320-322: LGTM! Job 취소 로직 적절

이 함수는 단순히 Job을 취소하는 역할만 하므로 intent 블록이 불필요합니다. 상태 변경이 없는 적절한 구현입니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt (1)

3-7: LGTM!

Orbit MVI로 마이그레이션하면서 MviSideEffect 상속을 제거한 것은 올바른 변경입니다. Orbit은 side effect에 대해 별도의 기본 인터페이스를 요구하지 않습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt (1)

6-29: LGTM - INIT 패턴이 프로젝트 전반의 다른 State 클래스들과 일관성 있게 적용되었습니다.

GuideState.INIT, LoginState.INIT 등 다른 State 클래스들과 동일한 패턴으로 초기 상태를 정의했습니다.

한 가지 참고 사항: LocalDate.now()INIT에서 사용하면 테스트 시 시간 의존성이 생길 수 있습니다. 필요시 테스트에서 명시적으로 날짜를 지정한 State를 생성하여 사용하면 됩니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt (1)

31-46: Orbit ContainerHost 마이그레이션이 잘 구현되었습니다.

  • ContainerHost 인터페이스 구현과 container 초기화가 올바르게 설정됨
  • SavedStateHandle에서 날짜 파싱 시 안전한 fallback 처리
  • init 블록에서 초기 상태 설정과 데이터 로드가 적절하게 수행됨
presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt (2)

36-54: Orbit Compose 통합이 올바르게 구현되었습니다.

  • collectAsState()collectSideEffect를 사용한 상태 및 사이드 이펙트 수집이 Orbit 패턴에 맞게 적용됨
  • 모든 RoutineListSideEffect 케이스가 when 블록에서 처리됨
  • ViewModel 메서드 참조(viewModel::hideDeleteConfirmBottomSheet 등)를 콜백으로 사용하여 코드가 간결해짐

149-159: Preview에서 RoutineListState.INIT를 사용한 것이 적절합니다.

State의 INIT 인스턴스를 Preview에서 활용하여 일관된 초기 상태를 보여줍니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementSideEffect.kt (1)

3-8: LGTM! Orbit 마이그레이션이 올바르게 적용되었습니다.

MviSideEffect 상속을 제거하고 순수 sealed interface로 변경하여 Orbit 패턴에 맞게 잘 리팩토링되었습니다. NavigateToBack 추가도 적절합니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementState.kt (1)

3-23: LGTM! State 구조가 Orbit 패턴에 맞게 깔끔하게 리팩토링되었습니다.

MviState 상속 제거와 INIT companion object 추가가 적절하며, computed properties(isAllAgreed, submitEnabled)의 로직도 그대로 유지되어 기능 동작에 문제가 없습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt (1)

15-94: LGTM! Orbit 기반 ViewModel으로 깔끔하게 마이그레이션되었습니다.

ContainerHost 구현과 explicit action 메서드들이 올바르게 작성되었으며, 다음 사항들이 적절하게 처리되었습니다:

  • isLoading 체크를 통한 동시 수정 방지 (lines 24, 37, 44, 51)
  • submitTermsAgreement의 성공/실패 처리 및 side effect 발행
  • 모든 navigation helper 메서드의 올바른 구현

GuideViewModel 패턴과 일관성 있게 구현되었습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt (2)

27-57: LGTM! Orbit 기반 UI 연동이 올바르게 구현되었습니다.

collectAsStatecollectSideEffect를 사용한 Orbit 패턴 적용이 적절하며, ViewModel 메서드 참조를 통한 콜백 연결도 깔끔합니다.


140-154: LGTM! Preview가 올바르게 업데이트되었습니다.

TermsAgreementState.INIT를 사용하여 초기 상태를 명시적으로 설정한 것이 좋습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineSideEffect.kt (1)

3-5: Orbit 전환에 맞는 SideEffect 타입 변경으로 충분합니다

기존 MviSideEffect 의존성을 제거하고 순수 sealed class로 둔 구조면 Orbit ContainerHost의 sideEffect 타입으로 사용하기에 충분해 보입니다. 별다른 부작용 없이 깔끔한 정리 같습니다.

presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt (1)

46-47: Orbit Compose 확장 함수로의 전환이 패턴에 잘 맞습니다

viewModel.collectAsState()viewModel.collectSideEffect { ... } 사용 방식이 ContainerHost 기반 ViewModel 패턴과 일관적이고, sideEffect 콜백에서도 네비게이션/토스트만 처리하고 있어 안정적으로 보입니다.

현재 사용하는 Orbit 버전에서 collectAsState/collectSideEffect 확장 함수 시그니처가 동일한지만 한 번 빌드해서 확인해 주시면 좋겠습니다.

Also applies to: 54-66

presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt (1)

159-212: 전반적인 intent/reduce 및 등록/수정 플로우는 잘 마이그레이션되었습니다

  • 이름/서브루틴/요일/기간/시간/각종 UI 플래그 변경을 모두 intent { reduce { state.copy(...) } } 패턴으로 일관되게 옮겨주신 부분은 읽기 쉽고 Orbit 관점에서도 자연스럽습니다.
  • registerRoutine 쪽에서
    • currentState.loading 가드로 중복 클릭 방지,
    • repeatType에 따른 repeatDay 계산,
    • noRepeatRoutineDate.now() 사용,
    • Add/Edit 분기와 RoutineUpdateType.Today/Tomorrow 매핑,
    • 성공 시 네비게이션 + 토스트, 실패 시 loading = false 복구
      등이 모두 기존 의도를 잘 유지하고 있습니다.
  • 특히 성공 케이스에서 loading을 되돌리지 않고 바로 MoveToPreviousScreen sideEffect만 보내는 부분은, 이전에 공유해 주셨던 “성공 시 네비게이션으로 ViewModel 라이프사이클이 종료되므로 로딩 리셋이 굳이 필요 없다”는 패턴과 동일해서 괜찮아 보입니다. Based on learnings.

위에서 코멘트 드린 loading 실패 분기와 selectAllTime만 보완되면 나머지 로직은 그대로 가져가셔도 좋을 것 같습니다.

Also applies to: 222-280, 281-334, 335-430

presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt (1)

3-6: LoginSideEffect를 MVI 공용 베이스에서 분리한 방향 좋습니다.

Orbit ContainerHost와 느슨하게 결합된 순수 sealed interface + object 구조라, 네비게이션 사이드 이펙트만 명확하게 드러나고 있습니다. 현재 요구사항 기준으로 추가 액션은 없어 보여요.

presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt (1)

3-13: INIT 정적 상태 도입으로 초기 상태 관리가 명확해졌습니다.

생성자 기본값 대신 LoginState.INIT를 통해 초기 상태를 한 곳에서만 정의하도록 한 구조가 Orbit 컨테이너 초기화 패턴과 잘 맞습니다. 추가적인 null/디폴트 케이스도 없어져서 추적하기 수월할 것 같아요.

presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt (1)

3-13: No action needed. The Orbit DSL functions (intent, subIntent, reduce, postSideEffect) are available without explicit imports from org.orbitmvi.orbit.syntax.simple.*. GuideViewModel and other ViewModels in the project already use these functions successfully without importing them. The code compiles and runs without errors, confirming they are provided by the project's Orbit bundle configuration.

Likely an incorrect or invalid review comment.

@l5x5l l5x5l merged commit 17406d0 into develop Dec 6, 2025
2 checks passed
@l5x5l l5x5l deleted the refactor/#138-apply_orbit branch December 6, 2025 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] MviViewModel 사용부분을 orbit으로 교체

3 participants