Skip to content

프로필 설정 화면 구현#50

Closed
chanho0908 wants to merge 71 commits intodevelopfrom
feat/#35-profile-setting
Closed

프로필 설정 화면 구현#50
chanho0908 wants to merge 71 commits intodevelopfrom
feat/#35-profile-setting

Conversation

@chanho0908
Copy link
Copy Markdown
Contributor

@chanho0908 chanho0908 commented Feb 1, 2026

이슈 번호

#35

리뷰/머지 희망 기한 (선택)

작업내용

  • 프로필 설정 화면 구현
  • 온보딩 네비게이션 설정

결과물

default.mp4

리뷰어에게 추가로 요구하는 사항 (선택)

@chanho0908 chanho0908 self-assigned this Feb 1, 2026
@chanho0908 chanho0908 added the Feature Extra attention is needed label Feb 1, 2026
@chanho0908 chanho0908 linked an issue Feb 1, 2026 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

이 PR은 새로운 온보딩 기능 모듈을 추가하고 기존 시스템과 통합합니다. 온보딩 피처는 커플 연결(CoupleConnection), 초대 코드(InviteCode), 프로필 설정(Profile), D-day 등록(Dday) 등 네 단계의 네비게이션 그래프로 구성됩니다. 도메인 모델(NickName, InviteCode)을 새로 추가하고, 저장소 계층을 AppResult로 감싸서 에러 처리를 강화합니다. 로그인 플로우도 온보딩 상태를 확인한 뒤 적절한 단계로 라우팅하도록 수정했으며, API 엔드포인트도 api/v1 버전으로 업데이트합니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes


상세 리뷰 의견

긍정적인 부분 ✨

아키텍처 계층화가 명확합니다

  • Domain, Data, Feature 레이어가 잘 분리되어 있고, 각 계층이 책임을 가지고 있습니다.
  • ViewModel, Repository, UI Model이 명확한 역할 분담을 하고 있네요.

네비게이션 구조가 체계적입니다

  • NavGraphContributor 패턴으로 온보딩 그래프를 모듈화했으며, 시작점(startDestination)과 우선순위(priority)를 명확히 설정했습니다.
  • 4단계 플로우가 선형적으로 구성되어 있어 이해하기 쉽습니다.

에러 처리 개선

  • AppResult 래핑으로 API 호출 실패를 일관되게 처리하는 것은 좋은 개선입니다.
  • 상태 검증(InvalidLength)도 Result 타입으로 우아하게 처리하고 있습니다.

개선 제안 사항 🔍

1. 상태 관리 로직의 복잡도 검토 필요

현황:

  • OnBoardingViewModelhandleIntent에서 여러 인텐트를 처리하고 있습니다.
  • 특히 SubmitNickNamehandleSubmitNicknameprofileSetupfetchOnboardingStatus라는 긴 체인이 있습니다.

왜 문제인가:

  • 상태 전이 경로가 복잡해지면 유지보수와 테스트가 어려워집니다.
  • 각 단계에서의 에러 처리가 명확하지 않은 부분이 있습니다. 예를 들어 connectCouple에 TODO 코멘트가 있네요.

개선 방안:

// 상태 머신 패턴이나 Redux 같은 명확한 상태 전이를 고려해보세요
when (intent) {
    is OnBoardingIntent.SubmitNickName -> {
        // 단일 책임: nickname 제출만 처리
        // fetchOnboardingStatus는 side effect로 분리
    }
}

질문: connectCouple의 TODO("counterpart already connected")는 언제 처리될 예정인가요? 현재는 그 케이스에서 아무 feedback이 없어 보입니다.


2. 예외 처리 일관성

현황:

  • NickName.create()InviteCode.create()Result<T> 타입을 반환합니다.
  • 하지만 ProfileUiModel.updateNickname()에서는 fold를 사용해 동기적으로 처리합니다.

개선 방안:

// 현재 코드
InviteCode.create(value).fold(
    onSuccess = { copy(partnerInviteCode = value, isValid = true) },
    onFailure = { copy(partnerInviteCode = value, isValid = false) }
)

// 고려사항: 왜 실패한 이유(length vs regex)를 구분하지 않나요?
// UI에서 다양한 에러 메시지를 표시할 수 있도록 하면 UX가 개선될 것 같습니다.

질문: 초대 코드가 유효하지 않을 때 사용자에게 어떤 피드백을 주나요? "올바른 초대 코드를 입력하세요" 같은 일반적인 메시지인가요?


3. 네비게이션 백스택 관리

현황:

  • DdayRoute에서 navigateToBackProfileRoute로 이동합니다.
  • ProfileRoute에서는 back 네비게이션이 정의되어 있지 않네요.

왜 문제인가:

  • 불완전한 back 네비게이션은 사용자 경험을 저하시킵니다.
  • 각 단계에서 이전 단계로 돌아갈 수 있는지 명확하지 않습니다.

개선 방안:

// ProfileRoute에도 navigateToBack을 추가하는 것은 어떨까요?
data class ProfileRoute(
    val viewModel: OnBoardingViewModel,
    val navigateToDday: () -> Unit,
    val navigateToHome: () -> Unit,
    val navigateToBack: () -> Unit,  // 추가 제안
)

4. OnBoardingViewModel의 유닛 테스트 커버리지

현황:

  • 복잡한 상태 전이 로직이 있지만 테스트 코드가 보이지 않습니다.

제안:

  • fetchOnboardingStatus() 결과에 따른 side effect 분기 테스트
  • connectCouple 실패 시나리오 테스트 (현재 TODO 상태)
  • 동시성 문제(예: 빠른 연속 클릭) 테스트

5. API 엔드포인트 버전 업데이트

현황:

// Before: `@POST`("onboarding/anniversary")
// After: `@POST`("api/v1/onboarding/anniversary")

좋은 점: 버전 관리가 명확해집니다.

확인 사항:

  • 모든 서버가 api/v1 엔드포인트를 지원하나요?
  • 하위 호환성이 필요한가요?

6. 문자열 리소스의 순서

현황:

  • 온보딩 문자열이 26개 추가되었습니다.

제안:

<!-- 섹션별 주석으로 그룹화하면 유지보수가 쉬워집니다 -->
<!-- Onboarding - Profile Setup -->
<string name="onboarding_profile_title">...</string>

<!-- Onboarding - Couple Connection -->
<string name="onboarding_couple_connect">...</string>

보안 & 성능 체크리스트 ⚠️

  • 초대 코드 전송: 초대 코드가 네트워크로 전송될 때 HTTPS만 사용하나요?
  • 상태 저장: 온보딩 중단 후 재개할 때 부분 데이터를 어떻게 처리하나요? (예: 이미 입력된 닉네임)
  • D-day 검증: 미래 날짜만 허용하나요, 과거 날짜도 가능한가요?

최종 평가 👍

전반적으로 견고한 설계입니다. 모듈화가 잘되어 있고, 도메인 모델 검증도 체계적입니다. 위의 제안들(특히 back navigation과 예외 처리 세분화)을 보완하면 매우 좋은 PR이 될 것 같습니다. 👏

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 프로필 설정 화면 구현이 주요 변경이며, 온보딩 네비게이션 설정도 포함되어 PR 제목과 일치합니다.
Description check ✅ Passed PR 설명에서 프로필 설정 화면 구현과 온보딩 네비게이션 설정을 명시하고 있어 변경 내용과 관련이 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#35-profile-setting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt`:
- Around line 3-24: Replace hardcoded Korean strings in CoupleConnectRoute with
string resources: use stringResource(R.string.onboarding_couple_connect_title)
for the screen title (fix the typo to "커플 등록 화면") and
stringResource(R.string.onboarding_next) for the button label; add the import
androidx.compose.ui.res.stringResource and reference the resource IDs
onboarding_couple_connect_title and onboarding_next. Also add a simple Preview
composable (e.g., CoupleConnectRoutePreview) that calls CoupleConnectRoute {} so
the UI can be previewed during development.

In `@feature/onboarding/src/main/java/com/twix/onboarding/dday/DdayRouete.kt`:
- Around line 12-14: Rename the misspelled composable and file: change the
function name DdayRouete to DdayRoute and rename the file DdayRouete.kt to
DdayRoute.kt; update all references/usages and imports where DdayRouete is
referenced (including any navigation registration or tests) so they point to
DdayRoute, and run a quick compile to ensure no remaining symbol references
remain.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/profile/ProfileScreen.kt`:
- Around line 151-160: The AppButton currently looks disabled visually but
remains clickable; update its behavior so it is actually disabled when
uiModel.isValid is false by wiring the enabled state (or passing null/NO-OP for
onClick) from uiModel.isValid into the button control; locate the AppButton
usage in ProfileScreen.kt and either set an enabled parameter (or conditionally
supply onClick = if (uiModel.isValid) { onCompleted } else { null }) so visual
and interactive states match, ensuring any ripple/press behavior is suppressed
when disabled.
🧹 Nitpick comments (12)
core/design-system/src/main/res/drawable/ic_arrow1_m_left.xml (1)

11-11: Dark Mode 지원을 위해 테마 색상 사용을 고려해 주세요.

현재 #171717로 하드코딩된 색상은 다크 모드에서 어두운 배경과 대비가 부족하여 아이콘이 보이지 않을 수 있습니다.

개선 방안:

  • android:strokeColor="?attr/colorOnSurface" 와 같은 테마 속성을 사용하거나
  • @color/icon_primary 같은 색상 리소스를 정의하여 values-night 폴더에서 다크 모드 대응 색상을 지정할 수 있습니다.
-      android:strokeColor="#171717"
+      android:strokeColor="@color/icon_primary"

다크 모드 지원이 현재 범위에 포함되지 않는다면, 추후 대응을 위해 이슈로 트래킹하는 것도 좋은 방법입니다.

core/design-system/src/main/res/drawable/ic_check_success.xml (1)

8-8: 디자인 시스템 일관성을 위해 색상 리소스 사용을 권장합니다.

성공 색상 #1ED45A가 하드코딩되어 있습니다. 이 녹색은 밝은/어두운 테마 모두에서 비교적 잘 보일 수 있지만, 디자인 시스템의 일관성과 향후 유지보수를 위해 색상 리소스로 분리하는 것이 좋습니다.

이점:

  • 디자인 시스템에서 success 색상을 한 곳에서 관리할 수 있습니다
  • 브랜드 색상 변경 시 일괄 수정이 가능합니다
  • 접근성 요구사항에 따른 대비 조정이 용이합니다
♻️ 개선 제안

colors.xml에 색상 정의:

<color name="success_green">#1ED45A</color>

drawable에서 참조:

-      android:fillColor="#1ED45A"/>
+      android:fillColor="@color/success_green"/>
feature/onboarding/src/main/java/com/twix/onboarding/dday/DdayRouete.kt (1)

12-36: Preview Composable 추가를 고려해주세요.

코딩 가이드라인에 따르면 Compose UI에는 Preview Composable이 제공되어야 합니다. 현재는 placeholder 화면이지만, 향후 실제 구현 시 UI 검증을 위해 Preview를 추가하면 개발 편의성이 향상됩니다.

♻️ Preview 추가 예시
`@Preview`(showBackground = true)
`@Composable`
private fun DdayRoutePreview() {
    TwixTheme {
        DdayRoute(
            onComplete = {},
            onBack = {},
        )
    }
}
feature/onboarding/src/main/java/com/twix/onboarding/profile/component/OnBoardingTopbar.kt (2)

28-37: 접근성 개선: contentDescription을 리소스 문자열로 변경해주세요.

스크린 리더 사용자를 위해 contentDescription이 하드코딩된 영문 "back" 대신 다국어 지원이 가능한 리소스 문자열을 사용하는 것이 좋습니다. 디자인 시스템 리뷰 가이드라인에서 접근성(Accessibility) 고려를 권장하고 있습니다.

♻️ 수정 제안

strings.xml에 추가:

<string name="content_description_back">뒤로 가기</string>

코드 수정:

 Icon(
     painter = painterResource(id = DesR.drawable.ic_arrow1_m_left),
-    contentDescription = "back",
+    contentDescription = stringResource(id = R.string.content_description_back),
     modifier =
         Modifier
             .size(44.dp)
             .padding(start = 10.dp)
             .noRippleClickable { onBack() }
             .align(Alignment.CenterStart),
 )

17-39: Preview Composable 추가를 권장합니다.

코딩 가이드라인에 따르면 Compose UI에는 Preview가 제공되어야 합니다. TopBar 컴포넌트는 여러 화면에서 재사용될 수 있으므로 Preview를 통해 독립적으로 UI를 검증하면 좋겠습니다.

♻️ Preview 추가 예시
`@Preview`(showBackground = true)
`@Composable`
private fun OnBoardingTopbarPreview() {
    TwixTheme {
        OnBoardingTopbar(
            onBack = {},
        )
    }
}
core/design-system/src/main/java/com/twix/designsystem/components/button/AppButton.kt (2)

19-49: 비활성화 상태(enabled = false)에 대한 시각적 피드백이 필요합니다.

현재 구현에서는 enabled = false일 때도 동일한 backgroundColor가 사용됩니다. 사용자가 버튼이 비활성화되었음을 인지할 수 있도록 시각적 차이를 제공하는 것이 좋습니다.

♻️ 비활성화 상태 처리 예시
 `@Composable`
 fun AppButton(
     modifier: Modifier = Modifier,
     text: String,
     textColor: Color = CommonColor.White,
     backgroundColor: Color = GrayColor.C500,
+    disabledBackgroundColor: Color = GrayColor.C200,
+    disabledTextColor: Color = GrayColor.C300,
     enabled: Boolean = true,
     cornerRadius: Dp = 12.dp,
     border: BorderStroke? = null,
     onClick: () -> Unit = {},
 ) {
+    val actualBackgroundColor = if (enabled) backgroundColor else disabledBackgroundColor
+    val actualTextColor = if (enabled) textColor else disabledTextColor
+
     Surface(
-        color = backgroundColor,
+        color = actualBackgroundColor,
         shape = RoundedCornerShape(cornerRadius),
         border = border,
         onClick = onClick,
         enabled = enabled,
         modifier = modifier,
     ) {
         AppText(
             text = text,
-            color = textColor,
+            color = actualTextColor,
             style = AppTextStyle.T2,
             textAlign = TextAlign.Center,
             modifier =
                 Modifier
                     .fillMaxWidth()
                     .padding(vertical = 14.dp),
         )
     }
 }

19-49: Preview Composable과 Dark Mode 지원을 고려해주세요.

디자인 시스템 가이드라인에서 Dark Mode 지원과 Preview 제공을 권장하고 있습니다. AppButton은 여러 곳에서 재사용될 공통 컴포넌트이므로 다양한 상태의 Preview를 제공하면 디자인 검증에 도움이 됩니다.

♻️ Preview 추가 예시
`@Preview`(showBackground = true, name = "Enabled")
`@Preview`(showBackground = true, name = "Disabled")
`@Composable`
private fun AppButtonPreview() {
    TwixTheme {
        Column {
            AppButton(
                text = "다음",
                enabled = true,
                onClick = {},
            )
            AppButton(
                text = "다음",
                enabled = false,
                onClick = {},
            )
        }
    }
}
feature/onboarding/src/main/res/values/strings.xml (1)

3-9: 중복 문자열 재사용과 다국어 지원 여부를 확인해주세요.

동일 문구가 onboarding_name_*onboarding_profile_*로 중복되어 있어 추후 문구 변경 시 불일치가 생길 수 있습니다. 공통 키로 통합하거나 한쪽이 다른 키를 참조하도록 정리하는 방법을 검토해 주세요. 다국어 지원 계획이 있다면 values-en 등 번역 리소스도 추가할지 확인해 볼까요?
As per coding guidelines, "다국어 지원을 고려했는가?".

feature/onboarding/src/main/java/com/twix/onboarding/vm/OnBoardingViewModel.kt (1)

3-40: 사이드이펙트를 새 코루틴에 분리하면 순서 보장이 약해질 수 있어요.

handleIntent가 이미 suspend인데 viewModelScope.launch로 분리하면 이후 Intent가 먼저 처리되어 네비게이션/토스트 순서가 뒤바뀔 여지가 있습니다. handleSubmitNickname을 suspend로 변경하고 emitSideEffect를 직접 호출해 순차 흐름을 유지하는 방향은 어떨까요?

🔧 제안 수정안
-    private fun handleSubmitNickname() {
+    private suspend fun handleSubmitNickname() {
         val sideEffect =
             if (uiState.value.isValidNickName) {
                 OnBoardingSideEffect.ProfileSetting.NavigateToNext
             } else {
                 OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast
             }
 
-        viewModelScope.launch {
-            emitSideEffect(sideEffect)
-        }
+        emitSideEffect(sideEffect)
     }

As per coding guidelines, "단방향 데이터 플로우(Intent → ViewModel → State → View)가 유지되는가?".

feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt (1)

12-27: 간단한 Preview를 추가하면 화면 확인이 더 쉬워집니다.

현재는 앱 실행 없이 레이아웃을 확인하기 어려워요. 기본 상태용 @Preview를 하나 두면 빠르게 확인할 수 있습니다. 필요하시면 추가해볼까요?

🔧 제안 수정안
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(showBackground = true)
+@Composable
+private fun CoupleConnectRoutePreview() {
+    CoupleConnectRoute(onNext = {})
+}

As per coding guidelines, "Preview Composable이 제공되는가?".

domain/src/main/java/com/twix/domain/model/nickname/NickNameException.kt (1)

3-4: Throwable 대신 Exception 상속 의도가 있는지 확인 부탁드립니다.

Throwable은 Error까지 포함하는 최상위 타입이라 catch 범위가 넓어질 수 있습니다. 특별한 목적이 없다면 Exception(또는 RuntimeException) 상속으로 좁히는 편이 일반적입니다—에러까지 잡아야 하는 시나리오가 있을까요?

🔧 제안 수정안
-sealed class NickNameException : Throwable() {
+sealed class NickNameException : Exception() {
     data object InvalidLengthException : NickNameException()
 }
feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt (1)

10-11: DdayRouete 명칭은 의도된 건가요?
오탈자라면 DdayRoute로 정리하면 검색성과 유지보수성이 좋아집니다. 리네이밍 의도가 있는지 확인해도 될까요?

Also applies to: 38-40

Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt`:
- Line 10: Rename the misspelled route symbols from DdayRouete to DdayRoute:
rename the file DdayRouete.kt to DdayRoute.kt, update the composable function
declaration fun DdayRouete(...) to fun DdayRoute(...), change the import
com.twix.onboarding.dday.DdayRouete to com.twix.onboarding.dday.DdayRoute, and
update any call sites that reference DdayRouete (the composable
registration/usage that should match NavRoutes.DdayRoute) so names are
consistent.

Copy link
Copy Markdown

@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: 12

Caution

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

⚠️ Outside diff range comments (2)
data/src/main/java/com/twix/data/repository/DefaultOnboardingRepository.kt (1)

14-17: ⚠️ Potential issue | 🟡 Minor

서버에서 받은 초대 코드도 도메인 불변식을 통해 생성하기

검증 결과, InviteCode 클래스는 create() 팩토리 메서드를 통해 8자리 길이 검증을 수행합니다. 그런데 현재 코드에서는 생성자를 직접 호출하여 이 검증을 우회하고 있습니다:

// InviteCode.kt 의 create() 메서드
fun create(value: String): Result<InviteCode> =
    runCatching {
        if (value.length != INVITE_CODE_LENGTH) {
            throw InviteCodeException.InvalidLengthException
        }
        InviteCode(value)
    }

// 현재 repository 구현
return InviteCode(response.inviteCode)  // ← 검증 우회

왜 중요한가:

  • 도메인 모델의 불변식(invariant)을 항상 보장해야 합니다
  • 사용자 입력값은 create()로 검증하는데, 서버 응답값만 생성자로 직접 생성하는 것은 일관성이 떨어집니다
  • 테스트에서도 검증 로직을 확인할 때는 create()를 사용합니다

개선 방안:

override suspend fun fetchInviteCode(): InviteCode {
    val response = service.fetchInviteCode()
    return InviteCode.create(response.inviteCode).getOrThrow()
    // 또는 Result를 처리하여 에러 매핑
}

서버 데이터는 신뢰할 수 있지만, 도메인 모델의 일관성을 위해 항상 팩토리 메서드를 통한 생성을 권장합니다.

domain/src/test/java/com/twix/domain/model/InviteCodeTest.kt (1)

10-15: 🛠️ Refactor suggestion | 🟠 Major

유효한 코드 테스트에서 create() 팩토리 메서드를 사용하는 것이 좋겠습니다.

현재 InviteCode("AB12CD34")로 생성자를 직접 호출하고 있는데, InviteCodevalue class이므로 생성자가 public이고 검증 로직을 거치지 않습니다. Happy path 역시 InviteCode.create()를 통해 테스트해야 팩토리 메서드의 정상 동작을 검증할 수 있습니다.

또한 코딩 가이드라인에 따라 Given-When-Then 구조를 일관되게 적용하면 가독성이 높아집니다.

🔧 제안하는 수정
     `@Test`
     fun `유효한 초대 코드는 정상적으로 생성된다`() {
-        val inviteCode = InviteCode("AB12CD34")
+        // given
+        val code = "AB12CD34"
 
-        assertThat(inviteCode.value).isEqualTo("AB12CD34")
+        // when
+        val result = InviteCode.create(code)
+
+        // then
+        assertThat(result.isSuccess).isTrue()
+        assertThat(result.getOrNull()?.value).isEqualTo(code)
     }

As per coding guidelines, "[테스트 코드 리뷰 가이드] - Given-When-Then 구조를 따르는가? - Happy / Sad Path가 모두 커버되는가?"

🤖 Fix all issues with AI agents
In `@domain/src/main/java/com/twix/domain/model/nickname/NickName.kt`:
- Around line 10-11: 현재 닉네임 검증은 길이만 확인해 공백 문자만 있는 값이나 앞뒤 공백을 포함한 값이 통과합니다;
NickName 검증(값을 받는 생성자나 setValue 메서드에서, 참조되는 변수명 value)을 변경해 value.isBlank() 또는
value.trim().length 체크를 추가하여 공백 전용 입력을 거부하고, 공백으로 인한 실패 시 적절한 예외(예:
NickNameException.InvalidBlankException 또는 기존의
NickNameException.InvalidLengthException와 구분되게 메시지 포함)를 던지도록 하세요; 또한 trim()을 사용해
내부 저장 전에 앞뒤 공백을 제거하는 방안도 고려하세요.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/couple/component/ConnectButton.kt`:
- Around line 69-76: ConnectButton.kt에서 현재 직접 사용되고 있는 Text(문자열
"onboarding_couple_connect_direct")는 다른 텍스트들과 달리 AppText를 쓰지 않아 시스템 폰트 스케일과 다르게
동작하므로 동일하게 AppText로 교체하세요; 만약 AppText가 fontWeight 파라미터를 지원하지 않으면 AppText에
fontWeight: FontWeight? (또는 필요한 스타일 선택자)를 추가하거나 AppTextStyle에 ExtraBold 스타일을 정의한
뒤 ConnectButton의 해당 텍스트를 AppText(text = stringResource(...), fontWeight =
ExtraBold, color = GrayColor.C500, fontSize = 16.sp, lineHeight = 22.2.sp) 형태로
통일해 레이아웃/폰트 스케일 동작을 일관되게 유지하도록 수정하세요.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/couple/component/RestoreCoupleBottomSheetContent.kt`:
- Around line 53-64: Replace the fixed Modifier.height(78.dp) on the Column that
wraps the three BulletItem calls with a minimum-height approach so content can
expand for longer translations or larger font sizes; for example, change the
Modifier chain used in the Column (the one with .fillMaxWidth() and
.height(78.dp)) to use a min height (e.g., .heightIn(min = 78.dp) or
.defaultMinSize(minHeight = 78.dp)) and add appropriate vertical padding inside
the Column so items don’t get clipped when they expand; update the Column
modifier where BulletItem(stringResource(...)) is used and keep the existing
verticalArrangement and RoundedCornerShape.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt`:
- Around line 108-115: The bottom sheet is being conditionally composed with an
if (showRestoreSheet) which prevents CommonBottomSheet from observing visible
changes and running its exit animation; remove the surrounding if and always
compose CommonBottomSheet, passing showRestoreSheet into its visible parameter
(keep onDismissRequest = onDismissSheet and content = {
RestoreCoupleBottomSheetContent() }) so CommonBottomSheet can animate its
internalVisible → delay(200) → rendering = false exit sequence correctly.
- Around line 39-53: The CoupleConnectRoute currently injects an
OnBoardingViewModel via the viewModel parameter which is unused and causes
OnBoardingViewModel (and its fetchMyInviteCode()) to be created prematurely;
remove the unused parameter from the CoupleConnectRoute signature (the
viewModel: OnBoardingViewModel = koinViewModel() parameter) and any references
to it so the composable no longer instantiates the ViewModel on composition;
if/when you need the ViewModel later, obtain it lazily inside the relevant click
handlers (e.g., onClickSend) or restore the parameter then.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/invite/InviteCodeScreen.kt`:
- Around line 166-179: The clipboard copy handler in InviteCodeScreen only sets
the clipboard and does not dispatch the MVI intent, so add an onCopyInviteCode
callback and invoke it when the copy icon is clicked (inside the
noRippleClickable coroutine after clipboard.setClipEntry) to dispatch
OnBoardingIntent.CopyInviteCode to the ViewModel (handled by OnBoardingViewModel
to emit ShowCopyInviteCodeSuccessToast); wire InviteCodeRoute to pass a lambda
that calls viewModel.dispatch(OnBoardingIntent.CopyInviteCode) so the
single-direction Intent → ViewModel → State → View flow is preserved.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/invite/InViteCodeUiModel.kt`:
- Line 7: Rename the class InViteCodeUiModel to InviteCodeUiModel and update the
source file name to InviteCodeUiModel.kt; search for usages of InViteCodeUiModel
(e.g., where InviteCodeScreen, InviteCodeTextField or other components reference
it) and update those references to InviteCodeUiModel to maintain consistency
with Kotlin PascalCase naming conventions. Ensure constructor, imports, and any
serializers/parsers or tests that reference the old name are updated accordingly
and run a build to catch remaining references.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/model/OnBoardingIntent.kt`:
- Around line 5-19: OnBoardingIntent에 D-Day 화면용 Intent가 빠져 있어 D-Day가 MVI 플로우에
통합되지 않았습니다; 파일의 sealed interface OnBoardingIntent에 D-Day 입력과 제출을 표현할 Intent(예:
data class UpdateDday(val date: LocalDate) 또는 data object SubmitDday)를 추가하고, 해당
Intent를 받는 ViewModel의 handleIntent(...) 구현에 분기 케이스를 추가해 상태 갱신/유효성 검사/내비게이션 처리를
이어주십시오 (또는 D-Day를 별도 ViewModel로 유지하려면 그 설계 의도를 주석으로 명시).

In
`@feature/onboarding/src/main/java/com/twix/onboarding/model/OnBoardingSideEffect.kt`:
- Around line 14-18: The sealed type InviteCode has its nested object
NavigateToNext incorrectly inheriting CoupleConnection, causing pattern-match
and routing bugs; change the declaration of NavigateToNext so it implements
InviteCode (i.e., declare NavigateToNext as : InviteCode instead of :
CoupleConnection) so that OnBoardingSideEffect.InviteCode pattern checks
correctly, and keep any semantic comment that this event represents "navigate to
couple connection" but preserve the side-effect owner as InviteCode.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/profile/ProfileScreen.kt`:
- Around line 59-75: The current LaunchedEffect(viewModel.sideEffect) + collect
is not lifecycle-safe; replace that block with the same pattern used in
InviteCodeScreen by using ObserveAsEvents/observeAsEvents to observe
viewModel.sideEffect lifecycle-aware (uses repeatOnLifecycle STARTED). Move the
when-handling of OnBoardingSideEffect.ProfileSetting (including
toastManager.tryShow(...) for ShowInvalidNickNameToast and onNext() for
NavigateToNext) into the ObserveAsEvents callback so events are consumed only
while the UI is STARTED; keep the existing side-effect cases and remove the
direct LaunchedEffect.collect usage.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/vm/OnBoardingViewModel.kt`:
- Around line 18-23: fetchMyInviteCode currently makes a network call without
error handling, which can crash the coroutine if
onBoardingRepository.fetchInviteCode() throws; wrap the repository call inside a
try-catch in fetchMyInviteCode (the viewModelScope.launch block), catch
exceptions from onBoardingRepository.fetchInviteCode(), log the error and call
reduce to emit an error/empty state (e.g., call reduce with
updateMyInviteCode(null) or an updateError state) so the ViewModel handles
failures gracefully instead of letting the coroutine crash.
- Around line 58-67: connectCouple currently calls
onBoardingRepository.coupleConnection inside viewModelScope.launch with no error
handling, so failures stop the coroutine and never emit feedback; wrap the
network call in a try/catch inside connectCouple's viewModelScope.launch, catch
exceptions from onBoardingRepository.coupleConnection, emit a failure side
effect (e.g., OnBoardingSideEffect.ShowError or a toast side effect) on error
and only emit OnBoardingSideEffect.InviteCode.NavigateToNext on success, and
optionally log the exception for diagnostics.
🧹 Nitpick comments (16)
domain/src/main/java/com/twix/domain/model/nickname/NickName.kt (2)

3-6: value class의 public 생성자로 인해 create() 검증을 우회할 수 있습니다.

현재 NickName의 primary constructor가 public이므로, NickName.create()를 거치지 않고 NickName("a")처럼 직접 생성하면 길이 검증이 우회됩니다.

Kotlin의 value class는 언어 제약상 private constructor를 지원하지 않기 때문에, 만약 엄격한 불변식 보장이 필요하다면 일반 data class + private constructor + companion factory 패턴을 고려해 볼 수 있습니다. 현재 구조에서는 팀 내 컨벤션으로 "항상 create()를 통해 생성한다"는 규칙을 명시하는 것도 방법입니다.

이 부분은 트레이드오프(value class의 성능 이점 vs. 캡슐화)가 있으므로, 팀에서 어떤 방향이 더 적합한지 논의해 보시면 좋겠습니다.


8-14: runCatching 대신 명시적 Result 반환을 권장합니다.

runCatching은 내부에서 발생하는 모든 Throwable 을 포착합니다. 현재 블록 안에서는 직접 throw하는 예외만 존재하지만, 코루틴 컨텍스트에서 호출될 경우 CancellationException까지 삼켜버릴 수 있어 구조적 동시성(Structured Concurrency)을 깨뜨릴 위험이 있습니다.

검증 로직이 단순하므로, Result.success / Result.failure를 명시적으로 반환하면 의도가 더 명확해지고 예기치 않은 예외 포착을 방지할 수 있습니다.

♻️ 개선 제안
 companion object {
     fun create(value: String): Result<NickName> =
-        runCatching {
-            if (value.length !in MIN_LENGTH..MAX_LENGTH) {
-                throw NickNameException.InvalidLengthException
-            }
-            NickName(value)
-        }
+        if (value.length !in MIN_LENGTH..MAX_LENGTH) {
+            Result.failure(NickNameException.InvalidLengthException)
+        } else {
+            Result.success(NickName(value))
+        }

     private const val MIN_LENGTH = 2
     private const val MAX_LENGTH = 8
 }
core/design-system/src/main/java/com/twix/designsystem/keyboard/KeyBoardAsState.kt (2)

10-10: TODO가 남아 있습니다 — 추적 이슈 생성을 권장드립니다.

키보드 상태 감지 유틸리티는 디자인 시스템보다는 core:ui 모듈에 더 적합합니다. 개발자분도 이미 인지하고 계신 부분이지만, TODO가 잊히지 않도록 별도 이슈로 추적하시면 좋겠습니다.

이 TODO를 추적할 이슈를 생성해 드릴까요?


23-33: rememberUpdatedState 대신 derivedStateOf 사용을 고려해봐도 좋습니다.

현재 구현은 WindowInsets.ime.getBottom(density)을 컴포지션 본문에서 직접 읽고 있어, 키보드 애니메이션 중 매 프레임마다 불필요한 리컴포지션이 발생합니다. rememberUpdatedState는 장수명 사이드이펙트(LaunchedEffect 등) 내부에서 최신 값을 캡처하기 위한 API이므로, 여기서는 의미론적으로 적절하지 않습니다.

remember { derivedStateOf { ... } }를 사용하면:

  • 내부적으로 스냅샷 상태를 추적하여 파생된 enum 값이 실제로 변경될 때만 하위 리더를 업데이트합니다
  • 키보드 애니메이션 중 픽셀 단위 변화에도 불구하고 OpenedClosed 변화만 전파됩니다

다만 한 가지 더 고려할 점: Compose 가이드라인상 WindowInsets는 컴포지션 단계에서 읽지 않는 것이 권장됩니다. 가능하다면 Modifier.imePadding() 같은 WindowInsets 전용 Modifier를 활용하거나, 비-레이아웃 로직이라면 snapshotFlow를 통해 컴포지션 외부에서 관찰하는 방식을 검토해볼 가치가 있습니다.

현재 함수 설계상 위 패턴이 어렵다면, derivedStateOf는 합리적인 개선입니다.

♻️ derivedStateOf를 활용한 개선안
 `@Composable`
 fun keyboardAsState(): State<Keyboard> {
     val density = LocalDensity.current
-    val keyboard =
-        if (WindowInsets.ime.getBottom(density) > 0) {
-            Keyboard.Opened
-        } else {
-            Keyboard.Closed
-        }
-    return rememberUpdatedState(keyboard)
+    return remember {
+        derivedStateOf {
+            if (WindowInsets.ime.getBottom(density) > 0) {
+                Keyboard.Opened
+            } else {
+                Keyboard.Closed
+            }
+        }
+    }
 }

추가 import가 필요합니다:

import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
feature/onboarding/src/main/java/com/twix/onboarding/couple/component/InvitationButton.kt (1)

23-44: 접근성(Accessibility) 시맨틱 누락 가능성

noRippleClickable는 일반적으로 clickable의 indication을 제거하거나 pointerInput을 사용하는 커스텀 확장인데, 이 경우 Compose의 시맨틱 트리에 Role.Button이 설정되지 않을 수 있습니다. 스크린 리더(TalkBack)가 이 요소를 버튼으로 인식하지 못할 수 있어요.

Modifier.semantics { role = Role.Button } 또는 contentDescription을 추가하면 접근성이 개선됩니다. 다만, 현재 앱의 접근성 지원 범위에 따라 우선순위를 판단해 주세요.

feature/onboarding/src/main/java/com/twix/onboarding/couple/component/RestoreCoupleBottomSheetContent.kt (2)

29-30: 로컬 색상 상수 — 디자인 시스템과의 일관성 검토 필요

RestoreBgColorBulletColor가 파일 내 로컬로 정의되어 있습니다. 디자인 시스템(GrayColor, CommonColor 등)에 이미 유사한 색상이 있다면 해당 값을 재사용하는 것이 색상 일관성 유지에 도움됩니다. 만약 새로운 디자인 토큰이라면 디자인 시스템 모듈에 추가하는 것을 고려해 주세요. As per coding guidelines, "색상과 테마가 디자인 시스템에 포함되는가?"에 해당합니다.


91-97: Preview 함수가 public으로 선언됨 — private으로 변경 권장

다른 컴포넌트(InvitationButtonPreview, ConnectButtonPreview)는 private fun으로 선언되어 있는데, 여기만 fun(public)입니다. Preview 함수는 외부에서 호출될 필요가 없으므로 일관성을 위해 private으로 변경하는 것이 좋습니다.

💡 수정 제안
 `@Preview`(showBackground = true)
 `@Composable`
-fun RestoreCoupleBottomSheetContentPreview() {
+private fun RestoreCoupleBottomSheetContentPreview() {
     TwixTheme {
         RestoreCoupleBottomSheetContent()
     }
 }
domain/src/test/java/com/twix/domain/model/InviteCodeTest.kt (1)

24-30: 실패 시 예외 타입 검증도 추가하면 더 견고한 테스트가 됩니다.

현재 result.isFailure만 검증하고 있는데, 실패 원인이 InviteCodeException.InvalidLengthException인지도 확인하면 예외 계층이 의도대로 동작하는지 보장할 수 있습니다.

🔧 제안하는 수정
     fun `8자리가 아닌 초대 코드는 예외가 발생한다`(invalidLengthCode: String) {
         // when
         val result = InviteCode.create(invalidLengthCode)
 
         // then
         assertThat(result.isFailure).isTrue()
+        assertThat(result.exceptionOrNull())
+            .isInstanceOf(InviteCodeException.InvalidLengthException::class.java)
     }
domain/src/main/java/com/twix/domain/model/invitecode/InviteCode.kt (1)

3-17: runCatching 대신 명시적 Result 반환을 고려해보면 어떨까요?

현재 runCatching은 내부에서 throw한 예외뿐만 아니라 예상치 못한 모든 예외(예: OutOfMemoryError)까지 catch합니다. 이 경우처럼 검증 로직이 단순하면 Result.success/Result.failure를 명시적으로 반환하는 방식이 의도를 더 명확하게 표현합니다.

🔧 제안하는 수정
 companion object {
     fun create(value: String): Result<InviteCode> =
-        runCatching {
-            if (value.length != INVITE_CODE_LENGTH) {
-                throw InviteCodeException.InvalidLengthException
-            }
-            InviteCode(value)
-        }
+        if (value.length != INVITE_CODE_LENGTH) {
+            Result.failure(InviteCodeException.InvalidLengthException)
+        } else {
+            Result.success(InviteCode(value))
+        }

     const val INVITE_CODE_LENGTH = 8
 }

이렇게 하면 throw + runCatching 패턴의 불필요한 예외 생성/포착 비용도 피할 수 있고, 예외 처리 범위가 정확히 의도한 대로 한정됩니다.

feature/onboarding/src/main/java/com/twix/onboarding/invite/component/InviteCodeTextField.kt (1)

57-105: rememberInfiniteTransition이 모든 CodeBox에서 실행되고 있어 불필요한 리소스가 소비됩니다.

현재 8개의 CodeBox가 각각 rememberInfiniteTransition + animateFloat를 실행하지만, 실제로 애니메이션이 보이는 것은 isFocused인 박스 하나뿐입니다. 나머지 7개는 애니메이션 값을 계산만 하고 사용하지 않으므로, 불필요한 recomposition과 리소스 소비가 발생합니다.

애니메이션을 isFocused일 때만 실행하도록 조건부로 변경하면 개선됩니다.

🔧 제안하는 수정
 `@Composable`
 private fun CodeBox(
     index: Int,
     code: String,
 ) {
     val isFocused = code.length == index
-    val infiniteTransition = rememberInfiniteTransition()
-    val alpha by infiniteTransition.animateFloat(
-        0f,
-        1f,
-        infiniteRepeatable(tween(500), RepeatMode.Reverse),
-    )
 
     Box(
         modifier =
             Modifier
                 .width(36.dp)
                 .height(58.dp)
                 .border(
                     1.dp,
                     when {
                         isFocused -> GrayColor.C500
                         else -> GrayColor.C200
                     },
                     RoundedCornerShape(8.dp),
                 ),
         contentAlignment = Alignment.Center,
     ) {
         when {
             index < code.length -> {
                 AppText(
                     text = code[index].toString(),
                     style = AppTextStyle.H3,
                     color = GrayColor.C500,
                 )
             }
 
             isFocused -> {
+                val infiniteTransition = rememberInfiniteTransition()
+                val alpha by infiniteTransition.animateFloat(
+                    0f,
+                    1f,
+                    infiniteRepeatable(tween(500), RepeatMode.Reverse),
+                )
                 Box(
                     modifier =
                         Modifier
                             .width(2.dp)
                             .height(24.dp)
                             .background(GrayColor.C500.copy(alpha = alpha)),
                 )
             }
         }
     }
 }

이렇게 하면 포커스된 박스에서만 애니메이션이 생성/실행되어 불필요한 리컴포지션을 방지할 수 있습니다.

As per coding guidelines, "[Feature 모듈 - MVI 패턴 리뷰 가이드] - 불필요한 recomposition 가능성은 없는가?"

feature/onboarding/src/main/java/com/twix/onboarding/invite/InviteCodeScreen.kt (1)

96-109: InViteCodeUiModel 네이밍이 다소 일관성이 없어 보입니다 — 의도된 것인가요?

InViteCodeUiModel에서 V가 대문자인데, 일반적인 네이밍 컨벤션으로는 InviteCodeUiModel이 자연스럽습니다. 이 타입이 다른 파일에서 정의되어 있으므로 이 PR의 범위를 벗어날 수 있지만, 가능하다면 통일하면 좋겠습니다.

feature/onboarding/src/main/java/com/twix/onboarding/profile/ProfileScreen.kt (2)

49-53: ProfileRoute의 접근 제한자가 InviteCodeRoute와 일관되지 않습니다.

InviteCodeRouteinternal fun으로 선언되어 모듈 외부 접근을 제한하고 있지만, ProfileRoutepublic fun입니다. Route composable은 navigation graph 내부에서만 호출되므로 internal visibility가 적절해 보입니다. 이 부분은 다른 Route들(CoupleConnectRoute, DdayRoute)도 함께 통일하면 좋겠습니다.


171-187: Preview에서 불필요한 Row 래핑이 있습니다.

UnValidProfileScreenPreview에서 ProfileScreenRow로 감싸고 있는데, 특별한 이유가 없다면 불필요합니다. ValidProfileScreenPreview처럼 TwixTheme 안에 직접 배치하는 것이 자연스럽습니다.

feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt (1)

55-62: CoupleConnectScreen의 visibility를 private으로 변경하는 것을 고려해 보세요.

CoupleConnectRoute가 외부 진입점 역할을 하고 있으므로, CoupleConnectScreenprivate으로 제한하는 것이 캡슐화에 좋습니다. Preview에서 사용할 수 있도록 같은 파일 내에서 private이면 충분합니다.

feature/onboarding/src/main/java/com/twix/onboarding/vm/OnBoardingViewModel.kt (1)

41-52: handleSubmitNickname에서 viewModelScope.launch가 불필요할 수 있습니다.

handleIntent가 이미 suspend fun이므로, emitSideEffect가 suspend function이라면 직접 호출할 수 있습니다. viewModelScope.launch로 별도 코루틴을 시작하면 호출 순서가 보장되지 않을 수 있습니다.

♻️ 수정 제안
-    private fun handleSubmitNickname() {
+    private suspend fun handleSubmitNickname() {
         val sideEffect =
             if (currentState.isValidNickName) {
                 OnBoardingSideEffect.ProfileSetting.NavigateToNext
             } else {
                 OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast
             }
 
-        viewModelScope.launch {
-            emitSideEffect(sideEffect)
-        }
+        emitSideEffect(sideEffect)
     }
feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt (1)

23-64: 온보딩 화면 간 ViewModel 인스턴스가 분리되어 중복 API 호출이 발생하고 있습니다.

composable 목적지에서 koinViewModel()을 호출하면, 기본적으로 각 NavBackStackEntryViewModelStoreOwner에 ViewModel이 스코프됩니다. 현재 네비게이션 구조에서:

  1. CoupleConnectRoute: ViewModel 인스턴스 생성 → init{}에서 fetchMyInviteCode() 호출
  2. InviteRoute로 이동 (popUpTo inclusive 적용): CoupleConnectRoute 제거, 새로운 ViewModel 인스턴스 생성 → init{}에서 fetchMyInviteCode() 다시 호출
  3. ProfileRoute로 이동: 또 다른 새로운 ViewModel 인스턴스 생성

결과적으로 같은 데이터(inviteCode)를 여러 번 fetch하게 되어 불필요한 네트워크 요청이 발생합니다.

OnBoardingUiStateprofileinviteCode를 하나의 상태로 정의한 것은 화면 간 데이터 공유를 의도한 것으로 보이는데, 현재 ViewModel 스코핑 방식으로는 각 화면이 독립적으로 동작하고 있습니다.

개선 방안:

  • 네비게이션 그래프 단위 ViewModel 스코핑 사용: koinNavViewModel() 또는 Koin의 scope API를 활용하여 온보딩 그래프 전체에서 하나의 ViewModel 인스턴스를 공유하도록 변경
  • 또는 각 화면이 필요한 부분만 처리하도록 상태를 분리하고, 필요시 화면 간 데이터 전달 메커니즘 구현

이렇게 하면 불필요한 API 호출을 줄이고, 설계 의도와 구현을 일치시킬 수 있습니다.

Comment on lines +10 to +11
if (value.length !in MIN_LENGTH..MAX_LENGTH) {
throw NickNameException.InvalidLengthException
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

공백 문자에 대한 검증이 누락되어 있습니다.

현재 길이 검증만 수행하므로, " "(공백 2자)이나 " ab "(앞뒤 공백 포함)과 같은 입력도 유효한 닉네임으로 통과합니다. 의도된 동작인지 확인이 필요합니다.

일반적으로 닉네임 검증에서는 trim() 후 길이를 체크하거나, isBlank() 검사를 추가하는 것이 안전합니다.

🛡️ 개선 예시
 fun create(value: String): Result<NickName> =
-    if (value.length !in MIN_LENGTH..MAX_LENGTH) {
-        Result.failure(NickNameException.InvalidLengthException)
-    } else {
-        Result.success(NickName(value))
-    }
+    value.trim().let { trimmed ->
+        if (trimmed.length !in MIN_LENGTH..MAX_LENGTH) {
+            Result.failure(NickNameException.InvalidLengthException)
+        } else {
+            Result.success(NickName(trimmed))
+        }
+    }
🤖 Prompt for AI Agents
In `@domain/src/main/java/com/twix/domain/model/nickname/NickName.kt` around lines
10 - 11, 현재 닉네임 검증은 길이만 확인해 공백 문자만 있는 값이나 앞뒤 공백을 포함한 값이 통과합니다; NickName 검증(값을 받는
생성자나 setValue 메서드에서, 참조되는 변수명 value)을 변경해 value.isBlank() 또는 value.trim().length
체크를 추가하여 공백 전용 입력을 거부하고, 공백으로 인한 실패 시 적절한 예외(예:
NickNameException.InvalidBlankException 또는 기존의
NickNameException.InvalidLengthException와 구분되게 메시지 포함)를 던지도록 하세요; 또한 trim()을 사용해
내부 저장 전에 앞뒤 공백을 제거하는 방안도 고려하세요.

Comment on lines +69 to +76
Text(
text = stringResource(R.string.onboarding_couple_connect_direct),
fontFamily = NanumSquareNeoFamily,
fontWeight = ExtraBold,
color = GrayColor.C500,
fontSize = 16.sp,
lineHeight = 22.2.sp,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

AppText 대신 Text를 직접 사용 — 폰트 스케일 동작 불일치

이 컴포넌트 내 다른 텍스트는 모두 AppText를 사용하고 있는데, Line 69의 "직접 연결하기" 텍스트만 Text를 직접 사용하고 있습니다.

AppText는 내부적으로 fontScale = 1f을 강제하여 시스템 글꼴 크기 설정에 영향을 받지 않지만, 원시 Text는 시스템 설정에 따라 크기가 변합니다. 같은 버튼 안에서 일부 텍스트만 시스템 폰트 크기에 반응하면 레이아웃이 깨질 수 있습니다.

ExtraBold 같은 커스텀 weight가 필요하다면, AppTextfontWeight 파라미터를 추가하거나, AppTextStyle에 해당 스타일을 정의하는 방향을 검토해 보시면 어떨까요?

💡 AppText 사용으로 통일하는 방안

AppTextfontWeight 파라미터를 지원하도록 확장하는 것이 이상적이지만, 우선 현재 구조에서 빠르게 대응하려면:

-                Text(
-                    text = stringResource(R.string.onboarding_couple_connect_direct),
-                    fontFamily = NanumSquareNeoFamily,
-                    fontWeight = ExtraBold,
-                    color = GrayColor.C500,
-                    fontSize = 16.sp,
-                    lineHeight = 22.2.sp,
-                )
+                // TODO: AppText에 fontWeight 파라미터 지원 추가 후 통일
+                CompositionLocalProvider(
+                    LocalDensity provides Density(LocalDensity.current.density, fontScale = 1f)
+                ) {
+                    Text(
+                        text = stringResource(R.string.onboarding_couple_connect_direct),
+                        fontFamily = NanumSquareNeoFamily,
+                        fontWeight = ExtraBold,
+                        color = GrayColor.C500,
+                        fontSize = 16.sp,
+                        lineHeight = 22.2.sp,
+                    )
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Text(
text = stringResource(R.string.onboarding_couple_connect_direct),
fontFamily = NanumSquareNeoFamily,
fontWeight = ExtraBold,
color = GrayColor.C500,
fontSize = 16.sp,
lineHeight = 22.2.sp,
)
// TODO: AppText에 fontWeight 파라미터 지원 추가 후 통일
CompositionLocalProvider(
LocalDensity provides Density(LocalDensity.current.density, fontScale = 1f)
) {
Text(
text = stringResource(R.string.onboarding_couple_connect_direct),
fontFamily = NanumSquareNeoFamily,
fontWeight = ExtraBold,
color = GrayColor.C500,
fontSize = 16.sp,
lineHeight = 22.2.sp,
)
}
🤖 Prompt for AI Agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/couple/component/ConnectButton.kt`
around lines 69 - 76, ConnectButton.kt에서 현재 직접 사용되고 있는 Text(문자열
"onboarding_couple_connect_direct")는 다른 텍스트들과 달리 AppText를 쓰지 않아 시스템 폰트 스케일과 다르게
동작하므로 동일하게 AppText로 교체하세요; 만약 AppText가 fontWeight 파라미터를 지원하지 않으면 AppText에
fontWeight: FontWeight? (또는 필요한 스타일 선택자)를 추가하거나 AppTextStyle에 ExtraBold 스타일을 정의한
뒤 ConnectButton의 해당 텍스트를 AppText(text = stringResource(...), fontWeight =
ExtraBold, color = GrayColor.C500, fontSize = 16.sp, lineHeight = 22.2.sp) 형태로
통일해 레이아웃/폰트 스케일 동작을 일관되게 유지하도록 수정하세요.

Comment on lines +53 to +64
Column(
modifier =
Modifier
.fillMaxWidth()
.height(78.dp)
.background(RestoreBgColor, RoundedCornerShape(12.dp)),
verticalArrangement = Arrangement.Center,
) {
BulletItem(stringResource(R.string.onboarding_couple_restore_content_my_email))
BulletItem(stringResource(R.string.onboarding_couple_restore_content_partner_email))
BulletItem(stringResource(R.string.onboarding_couple_restore_content_restore_date))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

고정 높이(78.dp) 사용 — 콘텐츠 잘림 위험

Bullet 아이템 3개를 담는 컨테이너에 height(78.dp)가 고정되어 있습니다. 현재 한국어 텍스트에서는 괜찮을 수 있지만, 다국어 지원 시 텍스트가 길어지거나, 접근성 설정으로 폰트 크기가 커지면 콘텐츠가 잘릴 수 있습니다.

height(78.dp) 대신 최소 높이를 설정하고 콘텐츠에 맞게 늘어나도록 하는 것이 안전합니다. As per coding guidelines, "다국어 지원을 고려했는가?"에 해당합니다.

💡 최소 높이 + padding 방식으로 변경
         Column(
             modifier =
                 Modifier
                     .fillMaxWidth()
-                    .height(78.dp)
-                    .background(RestoreBgColor, RoundedCornerShape(12.dp)),
+                    .background(RestoreBgColor, RoundedCornerShape(12.dp))
+                    .padding(vertical = 12.dp),
             verticalArrangement = Arrangement.Center,
         ) {
🤖 Prompt for AI Agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/couple/component/RestoreCoupleBottomSheetContent.kt`
around lines 53 - 64, Replace the fixed Modifier.height(78.dp) on the Column
that wraps the three BulletItem calls with a minimum-height approach so content
can expand for longer translations or larger font sizes; for example, change the
Modifier chain used in the Column (the one with .fillMaxWidth() and
.height(78.dp)) to use a min height (e.g., .heightIn(min = 78.dp) or
.defaultMinSize(minHeight = 78.dp)) and add appropriate vertical padding inside
the Column so items don’t get clipped when they expand; update the Column
modifier where BulletItem(stringResource(...)) is used and keep the existing
verticalArrangement and RoundedCornerShape.

Comment on lines +39 to +53
@Composable
fun CoupleConnectRoute(
onNext: () -> Unit,
viewModel: OnBoardingViewModel = koinViewModel(),
) {
var showRestoreSheet by rememberSaveable { mutableStateOf(false) }

CoupleConnectScreen(
showRestoreSheet = showRestoreSheet,
onClickSend = { },
onClickConnect = onNext,
onClickRestore = { showRestoreSheet = true },
onDismissSheet = { showRestoreSheet = false },
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

viewModel 파라미터가 주입되었지만 사용되지 않습니다.

OnBoardingViewModel이 생성자에서 fetchMyInviteCode()를 호출하므로, 이 화면에서 불필요하게 ViewModel을 생성하면 아직 필요하지 않은 API 호출이 발생합니다. 현재 onClickSend도 빈 핸들러이므로, 실제 기능이 구현될 때 추가하는 것이 좋겠습니다.

🛠️ 수정 제안: 미사용 viewModel 제거
 `@Composable`
 fun CoupleConnectRoute(
     onNext: () -> Unit,
-    viewModel: OnBoardingViewModel = koinViewModel(),
 ) {
🤖 Prompt for AI Agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt`
around lines 39 - 53, The CoupleConnectRoute currently injects an
OnBoardingViewModel via the viewModel parameter which is unused and causes
OnBoardingViewModel (and its fetchMyInviteCode()) to be created prematurely;
remove the unused parameter from the CoupleConnectRoute signature (the
viewModel: OnBoardingViewModel = koinViewModel() parameter) and any references
to it so the composable no longer instantiates the ViewModel on composition;
if/when you need the ViewModel later, obtain it lazily inside the relevant click
handlers (e.g., onClickSend) or restore the parameter then.

Comment on lines +108 to +115
if (showRestoreSheet) {
CommonBottomSheet(
visible = true,
config = CommonBottomSheetConfig(showHandle = false),
onDismissRequest = onDismissSheet,
content = { RestoreCoupleBottomSheetContent() },
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

CommonBottomSheet의 exit 애니메이션이 동작하지 않을 수 있습니다.

현재 if (showRestoreSheet) 조건으로 CommonBottomSheet를 마운트/언마운트하고 있는데, CommonBottomSheet(CommonBottomSheet.kt Line 52-105)는 visible 파라미터의 변화를 감지하여 내부적으로 internalVisibledelay(200)rendering = false 순서로 exit 애니메이션을 처리합니다.

if문으로 제거하면 visiblefalse로 바뀌기 전에 Composable 자체가 composition에서 사라지기 때문에, exit 애니메이션(scrim fade-out 등)이 재생되지 않습니다.

CommonBottomSheetvisible 파라미터에 showRestoreSheet를 직접 전달하고, if문을 제거하는 방식이 더 적합합니다.

🛠️ 수정 제안: visible 파라미터 활용
-        if (showRestoreSheet) {
-            CommonBottomSheet(
-                visible = true,
-                config = CommonBottomSheetConfig(showHandle = false),
-                onDismissRequest = onDismissSheet,
-                content = { RestoreCoupleBottomSheetContent() },
-            )
-        }
+        CommonBottomSheet(
+            visible = showRestoreSheet,
+            config = CommonBottomSheetConfig(showHandle = false),
+            onDismissRequest = onDismissSheet,
+            content = { RestoreCoupleBottomSheetContent() },
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (showRestoreSheet) {
CommonBottomSheet(
visible = true,
config = CommonBottomSheetConfig(showHandle = false),
onDismissRequest = onDismissSheet,
content = { RestoreCoupleBottomSheetContent() },
)
}
CommonBottomSheet(
visible = showRestoreSheet,
config = CommonBottomSheetConfig(showHandle = false),
onDismissRequest = onDismissSheet,
content = { RestoreCoupleBottomSheetContent() },
)
🤖 Prompt for AI Agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt`
around lines 108 - 115, The bottom sheet is being conditionally composed with an
if (showRestoreSheet) which prevents CommonBottomSheet from observing visible
changes and running its exit animation; remove the surrounding if and always
compose CommonBottomSheet, passing showRestoreSheet into its visible parameter
(keep onDismissRequest = onDismissSheet and content = {
RestoreCoupleBottomSheetContent() }) so CommonBottomSheet can animate its
internalVisible → delay(200) → rendering = false exit sequence correctly.

Comment on lines +59 to +75
LaunchedEffect(viewModel.sideEffect) {
viewModel.sideEffect.collect { sideEffect ->
when (sideEffect) {
OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast -> {
toastManager.tryShow(
ToastData(
message = notValidNickNameMessage,
type = ToastType.ERROR,
),
)
}

OnBoardingSideEffect.ProfileSetting.NavigateToNext -> onNext()
else -> Unit
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Side effect 수집 방식이 lifecycle-safe하지 않고, InviteCodeScreen과 일관성이 없습니다.

현재 LaunchedEffect(viewModel.sideEffect) 내부에서 직접 collect를 호출하고 있는데, 이 방식은 repeatOnLifecycle을 사용하지 않기 때문에 앱이 백그라운드에 있을 때도 side effect를 수집합니다. 예를 들어, 백그라운드에서 NavigateToNext가 emit되면 비정상적인 네비게이션이 발생할 수 있습니다.

InviteCodeScreen.kt에서는 ObserveAsEvents를 사용하여 lifecycle-aware하게 처리하고 있으므로, 동일한 패턴을 적용하는 것이 좋겠습니다.

🛠️ 수정 제안: ObserveAsEvents 사용
-    val notValidNickNameMessage =
-        stringResource(R.string.onboarding_profile_invalid_name_length_toast)
-    LaunchedEffect(viewModel.sideEffect) {
-        viewModel.sideEffect.collect { sideEffect ->
-            when (sideEffect) {
-                OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast -> {
-                    toastManager.tryShow(
-                        ToastData(
-                            message = notValidNickNameMessage,
-                            type = ToastType.ERROR,
-                        ),
-                    )
-                }
-
-                OnBoardingSideEffect.ProfileSetting.NavigateToNext -> onNext()
-                else -> Unit
-            }
-        }
-    }
+    val notValidNickNameMessage =
+        stringResource(R.string.onboarding_profile_invalid_name_length_toast)
+    ObserveAsEvents(viewModel.sideEffect) { sideEffect ->
+        when (sideEffect) {
+            OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast -> {
+                toastManager.tryShow(
+                    ToastData(
+                        message = notValidNickNameMessage,
+                        type = ToastType.ERROR,
+                    ),
+                )
+            }
+
+            OnBoardingSideEffect.ProfileSetting.NavigateToNext -> onNext()
+            else -> Unit
+        }
+    }

ObserveAsEvents는 내부적으로 repeatOnLifecycle(Lifecycle.State.STARTED)를 사용하므로 (core/ui/src/main/java/com/twix/ui/base/ObserveAsEvents.kt 참조), STOPPED 상태에서는 자동으로 수집을 취소합니다.

As per coding guidelines, "SideEffect가 일회성 이벤트로 분리되어 있는가?"

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LaunchedEffect(viewModel.sideEffect) {
viewModel.sideEffect.collect { sideEffect ->
when (sideEffect) {
OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast -> {
toastManager.tryShow(
ToastData(
message = notValidNickNameMessage,
type = ToastType.ERROR,
),
)
}
OnBoardingSideEffect.ProfileSetting.NavigateToNext -> onNext()
else -> Unit
}
}
}
ObserveAsEvents(viewModel.sideEffect) { sideEffect ->
when (sideEffect) {
OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast -> {
toastManager.tryShow(
ToastData(
message = notValidNickNameMessage,
type = ToastType.ERROR,
),
)
}
OnBoardingSideEffect.ProfileSetting.NavigateToNext -> onNext()
else -> Unit
}
}
🤖 Prompt for AI Agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/profile/ProfileScreen.kt`
around lines 59 - 75, The current LaunchedEffect(viewModel.sideEffect) + collect
is not lifecycle-safe; replace that block with the same pattern used in
InviteCodeScreen by using ObserveAsEvents/observeAsEvents to observe
viewModel.sideEffect lifecycle-aware (uses repeatOnLifecycle STARTED). Move the
when-handling of OnBoardingSideEffect.ProfileSetting (including
toastManager.tryShow(...) for ShowInvalidNickNameToast and onNext() for
NavigateToNext) into the ObserveAsEvents callback so events are consumed only
while the UI is STARTED; keep the existing side-effect cases and remove the
direct LaunchedEffect.collect usage.

- 프로필 설정 후 온보딩 상태에 따라 분기 처리
- 불필요한 `launchSingleTop` 옵션 제거
- 파라미터명 `onNext`에서 `navigateTo...`로 명확하게 변경
- 디데이 등록 화면 UI 및 로직을 구현했습니다.
- 사용자는 기념일을 선택하고 등록할 수 있습니다.
- 날짜 선택을 위해 캘린더 바텀시트를 사용합니다.
- 디데이 등록 후 메인 화면으로 이동하도록 네비게이션 로직을 수정했습니다.
- 관련 ViewModel 로직, Intent, SideEffect 및 UiState를 추가했습니다.
Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@feature/onboarding/src/main/java/com/twix/onboarding/dday/DdayRoute.kt`:
- Around line 110-120: The AppButton currently only changes color based on
uiModel.isSelected but remains clickable; update the AppButton call (the
instance that passes text, onClick = { onCompleted() }, backgroundColor,
textColor, modifier) to also set enabled = uiModel.isSelected so the button is
disabled when no date is selected; keep onClick as-is (or optionally guard
inside onCompleted if needed) so the UI prevents emitting SubmitDday when
uiModel.isSelected is false.
- Around line 136-153: Rename the preview function DdayScreenPrview to the
correctly spelled DdayScreenPreview; update the function declaration and any
references/calls (keeping `@Preview` and `@Composable` annotations and existing
parameter setup) so the IDE preview and searches reflect the correct name.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/model/OnBoardingSideEffect.kt`:
- Around line 6-12: The two data objects inside the ProfileSetting sealed
interface are declared with the wrong supertype: change NavigateToDDaySetting
(currently declared as ": CoupleConnection") and NavigateToHome (currently
declared as ": InviteCode") so they inherit from ProfileSetting instead of
CoupleConnection/InviteCode; locate the sealed interface ProfileSetting and
update the declarations of NavigateToDDaySetting and NavigateToHome to extend
ProfileSetting (keeping ShowInvalidNickNameToast as-is) so pattern matching
against OnBoardingSideEffect.ProfileSetting works correctly.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt`:
- Around line 28-35: The current forward navigations (e.g., in
CoupleConnectRoute -> navigateToNext using
navController.navigate(NavRoutes.InviteRoute.route) { popUpTo(graphRoute.route)
{ inclusive = true } }) are removing previous back stack entries causing Profile
data loss when navigateToBack is used; remove the inclusive popUpTo for flows
that must preserve back navigation (specifically between Profile and Dday) or
alternatively implement the reviewer’s Direction 2: keep forward navigation
without popUpTo for Profile→Dday and change the back handler (navigateToBack) to
call navController.popBackStack() (referencing navigateToBack and
navController.popBackStack()) so the existing Profile entry is restored instead
of recreating it.
- Around line 23-76: The routes create new ViewModel instances because each
Route calls koinViewModel() with the screen's own backStackEntry; update each
composable block (CoupleConnectRoute, InviteCodeRoute, ProfileRoute, DdayRoute)
to obtain the parent graph backStackEntry via
navController.getBackStackEntry(graphRoute.route) (store it with
remember(backStackEntry)) and pass that as viewModelStoreOwner into
koinViewModel(viewModelStoreOwner = parentEntry) so all screens share the same
OnBoardingViewModel instance for the navigation graph.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/vm/OnBoardingViewModel.kt`:
- Around line 97-102: The anniversarySetup function must handle failures like
other network calls: wrap the call to onBoardingRepository.anniversarySetup(...)
inside runCatching within the existing viewModelScope.launch in
anniversarySetup, and on success call
emitSideEffect(OnBoardingSideEffect.DdaySetting.NavigateToHome) while on failure
emit an appropriate error side effect (or log) — update anniversarySetup to use
runCatching { onBoardingRepository.anniversarySetup(...) }.onSuccess {
emitSideEffect(OnBoardingSideEffect.DdaySetting.NavigateToHome) }.onFailure { /*
emit or log error side effect */ }.
- Around line 70-91: profileSetup currently launches a coroutine and calls
fetchOnboardingStatus which itself launches another coroutine and neither
handles errors; change fetchOnboardingStatus to a suspend function (remove its
internal viewModelScope.launch), have profileSetup call it directly so execution
is sequential, and add try/catch around both network calls
(onBoardingRepository.profileSetup and
onBoardingRepository.fetchOnboardingStatus) to catch exceptions and emit an
error side effect (e.g., emitSideEffect(OnBoardingSideEffect.Error(...))) or set
an error state via currentState; update OnBoardingViewModel methods profileSetup
and fetchOnboardingStatus accordingly to ensure exceptions don't crash the
coroutine and the UI receives feedback.
🧹 Nitpick comments (8)
feature/onboarding/src/main/java/com/twix/onboarding/dday/component/DdayTopBar.kt (1)

18-40: 접근성(Accessibility)을 위해 contentDescription을 제공하고, Preview Composable을 추가해 주세요.

  1. Line 33의 contentDescription = null은 뒤로가기 버튼처럼 인터랙티브한 요소에는 적절하지 않습니다. 스크린 리더 사용자가 해당 버튼의 용도를 알 수 없게 됩니다. stringResource를 통해 "뒤로가기" 등의 설명을 제공하는 것이 좋습니다.

  2. 코딩 가이드라인에 따라 Preview Composable이 필요합니다. 다른 컴포넌트(DDayField)에는 Preview가 제공되어 있으므로 일관성을 맞춰주세요.

🛠️ 개선 제안
         Image(
             imageVector = ImageVector.vectorResource(R.drawable.ic_arrow1_m_left),
-            contentDescription = null,
+            contentDescription = stringResource(R.string.navigate_back),
             modifier =
                 Modifier
                     .size(44.dp)
                     .noRippleClickable(onClick = navigateToBack),
         )
     }
 }
+
+@Preview(showBackground = true)
+@Composable
+private fun DdayTopBarPreview() {
+    TwixTheme {
+        DdayTopBar(navigateToBack = {})
+    }
+}

As per coding guidelines, "Preview Composable이 제공되는가?"

feature/onboarding/src/main/java/com/twix/onboarding/dday/component/DDayField.kt (2)

44-54: 날짜 표시 형식을 사용자 친화적으로 변경하는 것을 고려해 주세요.

uiModel.anniversaryDate.toString()은 ISO-8601 형식(2024-01-01)을 출력합니다. 한국 사용자에게는 2024년 1월 1일 또는 2024.01.01 같은 로컬라이즈된 형식이 더 자연스럽습니다.

♻️ 개선 제안
+import java.time.format.DateTimeFormatter
+
+private val dateFormatter = DateTimeFormatter.ofPattern("yyyy년 M월 d일")
+
 // DDayField 내부에서:
             text =
                 if (uiModel.isSelected) {
-                    uiModel.anniversaryDate.toString()
+                    uiModel.anniversaryDate.format(dateFormatter)
                 } else {

61-68: 캘린더 아이콘에 contentDescription을 추가해 주세요.

인터랙티브한 요소(noRippleClickable이 적용된 캘린더 아이콘)에 contentDescription = null은 스크린 리더 사용자가 해당 요소의 기능을 인식할 수 없게 합니다. "날짜 선택" 등의 설명을 제공하면 접근성이 향상됩니다.

feature/onboarding/src/main/java/com/twix/onboarding/profile/ProfileScreen.kt (1)

173-205: 유효/무효 상태에 대한 Preview가 모두 제공되어 있어 좋습니다. 👍

한 가지 사소한 점: UnValidProfileScreenPreview (Line 175)의 네이밍이 InvalidProfileScreenPreview가 더 자연스럽습니다. "UnValid"는 영어에서 일반적으로 사용되지 않는 표현입니다.

feature/onboarding/src/main/java/com/twix/onboarding/dday/DdayRoute.kt (1)

42-71: 잘 구성된 MVI 패턴의 Route 함수입니다 👍

ObserveAsEvents를 통한 SideEffect 처리, collectAsStateWithLifecycle을 통한 상태 수집, 그리고 State Hoisting이 잘 적용되어 있습니다.

다만 두 가지 개선 포인트가 있습니다:

  1. toastManager가 주입만 되고 사용되지 않습니다. 현재 사이드이펙트 핸들러에서 toast를 표시하는 로직이 없으므로, 실제로 사용하기 전까지는 제거하거나, 향후 에러 토스트 표시 등을 위해 남겨두시려면 주석으로 의도를 명시해 주세요.

  2. showCalendarBottomSheetremember로 선언되어 있어 configuration change(화면 회전 등) 시 상태가 유실됩니다. CoupleConnectRoute에서는 rememberSaveable을 사용하고 있으므로 일관성을 위해서도 rememberSaveable이 적절합니다.

♻️ 수정 제안
+import androidx.compose.runtime.saveable.rememberSaveable
+
 `@Composable`
 fun DdayRoute(
     navigateToHome: () -> Unit,
     navigateToBack: () -> Unit,
-    toastManager: ToastManager = koinInject(),
     viewModel: OnBoardingViewModel = koinViewModel(),
 ) {
     val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-    var showCalendarBottomSheet by remember { mutableStateOf(false) }
+    var showCalendarBottomSheet by rememberSaveable { mutableStateOf(false) }
feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt (2)

51-57: onClickSend가 빈 핸들러로 전달되고 있으며, InvitationButton은 주석 처리되어 있습니다.

Line 93에서 InvitationButton이 주석 처리되어 있고, onClickSend는 빈 람다({ })로 전달됩니다. CoupleConnectScreen의 파라미터로도 선언되어 있지만 실제 사용처가 없는 상태입니다. 추후 구현 예정이라면 TODO 주석을 남겨 의도를 명시하면 좋겠습니다. 구현 계획이 없다면 파라미터를 제거하여 혼란을 줄이는 것도 방법입니다.


60-121: 전반적으로 잘 구성된 Screen 함수입니다. State hoisting이 적절히 적용되었고, 디자인 시스템 컴포넌트(AppText, ConnectButton, CommonBottomSheet)를 잘 활용하고 있습니다.

한 가지 질문: Line 75와 84의 80.24.dp, 11.76.dp 같은 소수점 dp 값은 디자인 스펙에서 그대로 가져온 것인가요? 일반적으로 dp는 정수 단위로 사용하는 것이 관례이고, 하위 픽셀 렌더링에서 예측하기 어려운 결과가 나올 수 있습니다. 디자인 시스템과 확인 후 정수로 라운딩하는 것을 고려해 보세요.

feature/onboarding/src/main/res/values/strings.xml (1)

3-10: 중복 문자열 리소스가 있습니다.

onboarding_name_placeholder(Line 4)와 onboarding_profile_placeholder(Line 7)가 동일한 값("닉네임을 입력해 주세요.")이고, onboarding_name_helper(Line 5)와 onboarding_profile_helper(Line 8)도 동일합니다.

의도적으로 화면별 독립성을 위해 분리한 것일 수 있지만, 동일한 값을 두 곳에서 관리하면 향후 번역 시 불일치가 발생할 수 있습니다. 만약 항상 같은 값을 유지해야 한다면, 하나의 리소스로 통합하는 것을 고려해 보세요. 반대로 화면별로 다른 문구로 분기할 가능성이 있다면 현재 구조가 적절합니다.

Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt`:
- Around line 76-78: The navigateToBack lambda currently calls
navController.navigate(NavRoutes.ProfileRoute.route), which pushes a new
ProfileRoute onto the back stack; change it to use navController.popBackStack
(or navController::popBackStack) so the Dday screen is popped and the existing
ProfileRoute instance is resumed (preserving graph-scoped state). Update the
navigateToBack implementation where it references NavRoutes.ProfileRoute.route
to call popBackStack instead, matching the InviteRoute usage.
🧹 Nitpick comments (3)
feature/onboarding/src/main/java/com/twix/onboarding/model/OnBoardingSideEffect.kt (1)

14-14: CoupleConnection이 비어 있는 sealed interface인데, 현재 불필요한 상태입니다.

OnBoardingViewModel의 connectCouple() 메서드를 보면, 커플 연결 완료 후 OnBoardingSideEffect.InviteCode.NavigateToNext를 발행하고 있습니다(라인 52). CoupleConnection은 정의되어 있지만 실제로는 어떤 SideEffect도 포함하지 않으며, 어디에서도 사용되지 않습니다.

코딩 가이드의 MVI 패턴 요구사항("SideEffect가 일회성 이벤트로 분리되어 있는가?")을 고려하면, 다음 두 가지 중 선택할 수 있습니다:

  1. 현재 불필요하다면: YAGNI 원칙에 따라 제거하고, 향후 커플 연결 화면에서 추가 SideEffect가 필요하면 그 때 다시 정의합니다.
  2. 향후 확장을 위해 유지한다면: 주석으로 의도를 명확히 해 줍니다 (예: // 향후 에러 처리 등의 SideEffect 추가 예정).

빈 sealed interface는 코드를 읽는 사람에게 "무언가 빠진 건가?"라는 혼동을 주고, when 분기에서 불필요한 빈 케이스를 만들 수 있습니다.

feature/onboarding/src/main/java/com/twix/onboarding/dday/DdayRoute.kt (1)

42-69: toastManager가 주입되었지만 사용되지 않고 있습니다.

DdayRoute에서 ToastManagerkoinInject()로 주입받고 있지만, ObserveAsEvents 블록 내에서 토스트를 표시하는 로직이 없습니다. 현재는 NavigateToHome만 처리하고 있어 불필요한 의존성입니다.

향후 에러 토스트 등이 추가될 예정이라면 유지해도 되지만, 현재 시점에서는 YAGNI 원칙에 따라 제거하는 것을 권장합니다. 필요해질 때 다시 추가하면 됩니다.

🛠️ 수정 제안
 `@Composable`
 fun DdayRoute(
     viewModel: OnBoardingViewModel,
     navigateToHome: () -> Unit,
     navigateToBack: () -> Unit,
-    toastManager: ToastManager = koinInject(),
 ) {
feature/onboarding/src/main/java/com/twix/onboarding/invite/InviteCodeScreen.kt (1)

114-136: 뒤로가기 버튼이 별도 Box에 있어 Column 콘텐츠와 겹칠 수 있습니다.

뒤로가기 버튼은 Box 루트의 첫 번째 자식으로, Column은 두 번째 자식으로 배치되어 있습니다. Column 내부의 Spacer(8.dp) 다음에 오는 AnimatedVisibility 콘텐츠에 top = 80.dp 패딩이 있어 현재는 시각적으로 겹치지 않을 수 있지만, 키보드가 열려서 AnimatedVisibility가 숨겨지면 Column 상단이 72.dp 높이의 뒤로가기 버튼과 겹칠 수 있습니다.

DdayRoute에서처럼 DdayTopBar를 Column 내부에 배치하는 방식이 레이아웃 안정성 측면에서 더 안전합니다. 현재 동작에 문제가 없다면 참고 수준으로 봐주세요.

Comment on lines +76 to +78
navigateToBack = {
navController.navigate(NavRoutes.ProfileRoute.route)
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

navigateToBack에서 popBackStack()을 사용해야 합니다.

현재 Dday 화면의 뒤로가기는 navController.navigate(NavRoutes.ProfileRoute.route)를 사용하고 있습니다. 이 방식은 기존 ProfileRoute 엔트리로 돌아가는 것이 아니라 새로운 ProfileRoute 엔트리를 back stack에 추가합니다.

문제점:

  • Back stack이 계속 쌓입니다 (Profile → Dday → Profile → Dday → ...)
  • ProfileScreenLaunchedEffect(Unit)이 재실행되어 focusRequester가 다시 포커스를 요청합니다
  • 같은 파일의 InviteRoute (Line 47)에서는 올바르게 navController::popBackStack을 사용하고 있어 일관성도 맞지 않습니다

graphViewModel으로 ViewModel이 graph 스코프에서 공유되므로, popBackStack()을 사용해도 상태가 보존됩니다.

🛠️ popBackStack 사용
             DdayRoute(
                 viewModel = vm,
                 navigateToHome = {
                     navController.navigate(NavRoutes.MainGraph.route) {
                         popUpTo(graphRoute.route) { inclusive = true }
                     }
                 },
                 navigateToBack = {
-                    navController.navigate(NavRoutes.ProfileRoute.route)
+                    navController.popBackStack()
                 },
             )
🤖 Prompt for AI Agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/navigation/OnboardingNavGraph.kt`
around lines 76 - 78, The navigateToBack lambda currently calls
navController.navigate(NavRoutes.ProfileRoute.route), which pushes a new
ProfileRoute onto the back stack; change it to use navController.popBackStack
(or navController::popBackStack) so the Dday screen is popped and the existing
ProfileRoute instance is resumed (preserving graph-scoped state). Update the
navigateToBack implementation where it references NavRoutes.ProfileRoute.route
to call popBackStack instead, matching the InviteRoute usage.

- 초대 코드 조회 실패 시 에러 토스트를 표시합니다.
- 잘못된 초대 코드 입력 시 에러 토스트를 표시합니다.
- 기념일 설정 실패 시 에러 토스트를 표시합니다.
- API 통신 로직을 `launchResult`로 리팩터링하여 예외 처리를 구현합니다.
Copy link
Copy Markdown

@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 (1)
feature/login/src/main/java/com/twix/login/LoginViewModel.kt (1)

24-38: ⚠️ Potential issue | 🟠 Major

authRepository.login() 호출에 대한 예외 처리가 누락되어 있어 사용자 피드백이 부족합니다.

authRepository.login()DefaultAuthRepository에서 네트워크 호출과 토큰 저장 작업을 수행하는데, 실패 시 예외를 throw합니다. 현재 코드는 viewModelScope.launch 블록 내에서 직접 호출되고 있으며, BaseViewModelhandleIntent 예외 처리(try-catch)가 예외를 로깅하기만 할 뿐 사용자에게 에러 피드백을 제공하지 않습니다.

반면 checkOnboardingStatus()launchResult()를 통해 에러 시 onError 핸들러로 ShowFetchOnBoardingStatusFailToast 사이드 이펙트를 발생시킵니다. 두 메서드 모두 실패 가능성이 있으므로 일관된 에러 처리 패턴이 필요합니다.

개선 방안:

  • authRepository.login()try-catch로 감싸고 실패 시 emitSideEffect(LoginSideEffect.ShowLoginFailToast) 발생
  • 또는 authRepository.login()의 반환 타입을 AppResult<Unit>으로 변경하여 launchResult()로 래핑하기
🤖 Fix all issues with AI agents
In `@feature/login/src/main/res/values/strings.xml`:
- Around line 4-5: The two string resources are identical but represent
different failures; update the value of fetch_onboarding_status_fail_message
(currently identical to login_fail_message) to a message that reflects
onboarding-status retrieval failure—for example use a Korean string like "온보딩
상태를 불러오지 못했습니다. 잠시 후 다시 시도해주세요"—so UI shows the correct error context when
onBoarding status lookup (fetch_onboarding_status_fail_message) fails while
login (login_fail_message) succeeded.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/model/OnBoardingSideEffect.kt`:
- Around line 26-30: ShowAnniversarySetupFailToast is declared inside the sealed
interface DdaySetting but incorrectly implements ProfileSetting, causing it to
be misclassified in pattern matches; update ShowAnniversarySetupFailToast to
implement DdaySetting (i.e., change its supertype from ProfileSetting to
DdaySetting or make it a member object of DdaySetting) so that when matching
OnBoardingSideEffect.DdaySetting it will be correctly recognized; check the
sealed interfaces DdaySetting, ProfileSetting and the data object
ShowAnniversarySetupFailToast to ensure the inheritance aligns with the intended
side-effect grouping.

In
`@feature/onboarding/src/main/java/com/twix/onboarding/vm/OnBoardingViewModel.kt`:
- Around line 100-122: The onError block in fetchOnboardingStatus is empty and
must surface failure to the UI; update the launchResult(onError = { ... })
handler (in fetchOnboardingStatus) to emit a user-visible side effect or state,
e.g., call emitSideEffect with a new OnBoardingSideEffect.ProfileSetting.Error
or Retry action (or trigger a Toast via a UI event), and optionally include
logging of the throwable; ensure the chosen symbol (emitSideEffect and
OnBoardingSideEffect.ProfileSetting.*) is used so the UI can show an error
message and/or a retry option when fetchOnboardingStatus fails.
🧹 Nitpick comments (7)
feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectRoute.kt (1)

97-99: 소수점 dp 값(80.24.dp, 11.76.dp)이 의도된 값인지 확인이 필요합니다.

80.24.dp11.76.dp는 일반적이지 않은 소수점 값입니다. 디자인 시스템의 스펙에서 가져온 값이라면 괜찮지만, 렌더링 시 소수점 픽셀은 반올림되어 디바이스에 따라 미세한 차이가 발생할 수 있습니다. 혹시 80.dp + 12.dp 등 정수값이 의도된 것은 아닌지 디자인 팀과 확인해 보시는 것을 권장합니다.

feature/onboarding/src/main/java/com/twix/onboarding/dday/DdayRoute.kt (1)

51-51: showCalendarBottomSheetremember 대신 rememberSaveable 사용을 고려해 보세요.

CoupleConnectRoute에서는 유사한 bottom sheet 상태(showRestoreSheet)에 rememberSaveable을 사용하고 있는데, 여기서는 remember를 사용하고 있습니다. remember는 configuration change(화면 회전 등) 시 상태가 초기화되므로, 캘린더가 열린 상태에서 회전하면 닫히게 됩니다.

캘린더 bottom sheet가 열려 있는 도중 회전 시 닫히는 것이 의도된 UX라면 괜찮지만, 그렇지 않다면 일관성을 위해 rememberSaveable로 변경하는 것이 좋겠습니다.

♻️ 제안 수정안
-    var showCalendarBottomSheet by remember { mutableStateOf(false) }
+    var showCalendarBottomSheet by rememberSaveable { mutableStateOf(false) }

rememberSaveable import도 추가해주세요:

import androidx.compose.runtime.saveable.rememberSaveable
feature/login/src/main/java/com/twix/login/LoginViewModel.kt (1)

41-63: onSuccess 내부의 viewModelScope.launch 중첩이 개선 가능합니다.

launchResult가 이미 viewModelScope.launch 내부에서 실행되는데, onSuccess 콜백에서 다시 viewModelScope.launch를 사용하고 있습니다. 이는 onSuccess가 non-suspend 타입((D) -> Unit)으로 정의되어 있어 emitSideEffect(suspend)를 직접 호출할 수 없기 때문인 것으로 보입니다.

반면 onErrorsuspend (AppError) -> Unit으로 정의되어 있어 Line 60에서 emitSideEffect를 직접 호출할 수 있습니다. 이 비대칭이 onSuccess에서만 추가 코루틴 실행이 필요한 원인입니다.

BaseViewModel.launchResultonSuccess 타입을 suspend (D) -> Unit으로 변경하면 이 중첩을 제거할 수 있습니다. 당장은 동작에 문제가 없으나, 향후 리팩터링 시 고려해 보시면 좋겠습니다.

♻️ BaseViewModel 변경 시 LoginViewModel 개선안

BaseViewModel.launchResultonSuccess를 suspend로 변경한다면:

 // BaseViewModel.kt
-onSuccess: (D) -> Unit,
+onSuccess: suspend (D) -> Unit,

그러면 LoginViewModel에서 중첩을 제거할 수 있습니다:

     private fun checkOnboardingStatus() {
         launchResult(
             block = { onBoardingRepository.fetchOnboardingStatus() },
             onSuccess = { onboardingStatus ->
-                viewModelScope.launch {
-                    val sideEffect =
-                        when (onboardingStatus) {
-                            OnboardingStatus.COUPLE_CONNECTION,
-                            OnboardingStatus.PROFILE_SETUP,
-                            OnboardingStatus.ANNIVERSARY_SETUP,
-                            -> LoginSideEffect.NavigateToOnBoarding(onboardingStatus)
-
-                            OnboardingStatus.COMPLETED -> LoginSideEffect.NavigateToHome
-                        }
-
-                    emitSideEffect(sideEffect)
-                }
+                val sideEffect =
+                    when (onboardingStatus) {
+                        OnboardingStatus.COUPLE_CONNECTION,
+                        OnboardingStatus.PROFILE_SETUP,
+                        OnboardingStatus.ANNIVERSARY_SETUP,
+                        -> LoginSideEffect.NavigateToOnBoarding(onboardingStatus)
+
+                        OnboardingStatus.COMPLETED -> LoginSideEffect.NavigateToHome
+                    }
+                emitSideEffect(sideEffect)
             },
             onError = {
                 emitSideEffect(LoginSideEffect.ShowFetchOnBoardingStatusFailToast)
             },
         )
     }
feature/onboarding/src/main/java/com/twix/onboarding/invite/InviteCodeScreen.kt (1)

107-114: 클래스명 InViteCodeUiModelInviteCodeUiModel로 수정하세요.

Kotlin 명명 규칙(PascalCase)에 따르면 클래스명의 각 단어는 첫 글자만 대문자여야 합니다. 현재 InViteCodeUiModel에서 "Invite"의 V가 대문자로 되어 있어 규칙에 맞지 않습니다. 올바른 이름은 InviteCodeUiModel입니다.

다행히 이 클래스는 여러 파일에서 import되고 사용되지만, IDE의 Rename 리팩터링 기능으로 한 번에 수정할 수 있습니다:

  • 클래스 정의: InViteCodeUiModel.kt 파일의 클래스명
  • 영향받는 파일: OnBoardingUiState.kt, InviteCodeScreen.kt, InViteCodeUiModel.kt 내 메서드 반환 타입
feature/onboarding/src/main/java/com/twix/onboarding/vm/OnBoardingViewModel.kt (3)

51-59: onSuccess 내부의 viewModelScope.launchBaseViewModel.launchResultonSuccess 시그니처가 non-suspend이기 때문에 발생하는 패턴입니다

emitSideEffectsuspend 함수인 반면 launchResultonSuccess(D) -> Unit(non-suspend)으로 정의되어 있어, emitSideEffect 호출을 위해 viewModelScope.launch로 감싸야 하는 상황입니다. 이 패턴이 Line 54-56, 104-116, 132에서 반복됩니다.

현재 구조에서는 불가피하지만, onSuccesssuspend 람다로 변경하면 이 중첩을 제거할 수 있습니다. BaseViewModellaunchResult 시그니처 변경은 이 PR의 범위를 벗어나므로, 향후 개선 사항으로 고려해보시면 어떨까요?


62-77: 서버 에러 메시지 문자열 비교는 취약한 패턴입니다 — 서버 응답 변경 시 조용히 실패할 수 있습니다

서버 에러 메시지("이미 사용된 초대 코드입니다.", "유효하지 않은 초대 코드입니다.")를 문자열 비교로 분기하고 있습니다. 서버의 메시지 문구가 변경되거나 다국어 지원이 추가되면 이 분기 로직이 조용히 실패하게 됩니다.

가능하다면 AppError.Httpcode 필드(에러 코드)를 기준으로 분기하는 것이 더 안정적입니다. 만약 서버가 에러 코드를 아직 제공하지 않는다면, 서버 팀과 협의하여 에러 코드 기반 분기를 도입하는 것을 권장합니다.

♻️ 에러 코드 기반 분기 예시
 private suspend fun handleCoupleConnectException(error: AppError) {
-    if (error is AppError.Http && error.status == 404) {
-        if (error.message == INVALID_INVITE_CODE_MESSAGE) {
+    if (error is AppError.Http) {
+        when (error.code) {
+            INVALID_INVITE_CODE_ERROR_CODE -> {
                 emitSideEffect(OnBoardingSideEffect.InviteCode.ShowInvalidInviteCodeToast)
             }
-        if (error.message == ALREADY_USED_INVITE_CODE_MESSAGE) {
+            ALREADY_USED_INVITE_CODE_ERROR_CODE -> {
                 emitSideEffect(OnBoardingSideEffect.InviteCode.NavigateToNext)
             }
+        }
     }
 }

83-91: handleSubmitNickname에서 emitSideEffect 호출 시 viewModelScope.launch 사용에 대해

Line 87-89에서 viewModelScope.launch로 감싸서 emitSideEffect를 호출하고 있습니다. handleSubmitNicknamehandleIntent에서 호출되고, handleIntent는 이미 suspend 함수이므로 직접 emitSideEffect를 호출할 수 있지 않은지 확인이 필요합니다.

만약 handleIntent의 코루틴 컨텍스트에서 직접 호출 가능하다면 viewModelScope.launch를 제거하여 불필요한 코루틴 생성을 줄일 수 있습니다.

♻️ 제안
-    private fun handleSubmitNickname() {
+    private suspend fun handleSubmitNickname() {
         if (currentState.isValidNickName) {
             profileSetup()
         } else {
-            viewModelScope.launch {
-                emitSideEffect(OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast)
-            }
+            emitSideEffect(OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast)
         }
     }

Comment on lines 4 to +5
<string name="login_fail_message">로그인에 실패했습니다. 다시 시도해주세요</string>
<string name="fetch_onboarding_status_fail_message">로그인에 실패했습니다. 다시 시도해주세요</string>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

온보딩 상태 조회 실패 메시지가 로그인 실패 메시지와 동일합니다.

fetch_onboarding_status_fail_message의 값이 login_fail_message와 완전히 동일한 "로그인에 실패했습니다. 다시 시도해주세요"로 되어 있습니다. 이 메시지는 로그인은 성공했지만 온보딩 상태 조회에 실패한 경우에 표시되므로, 사용자 입장에서 실제 원인과 다른 피드백을 받게 됩니다.

예를 들어, 사용자가 로그인에 성공한 직후 "로그인에 실패했습니다"라는 토스트를 보면 혼란스러울 수 있습니다. 메시지를 실제 실패 원인에 맞게 변경하는 것이 좋겠습니다.

🛠️ 제안 수정안
-    <string name="fetch_onboarding_status_fail_message">로그인에 실패했습니다. 다시 시도해주세요</string>
+    <string name="fetch_onboarding_status_fail_message">초기 설정 정보를 불러오지 못했습니다. 다시 시도해주세요</string>

As per coding guidelines, **/res/**: "하드코딩 문자열이 없는가?" — 리소스 자체의 문자열 내용은 적절한 의미를 전달해야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<string name="login_fail_message">로그인에 실패했습니다. 다시 시도해주세요</string>
<string name="fetch_onboarding_status_fail_message">로그인에 실패했습니다. 다시 시도해주세요</string>
<string name="login_fail_message">로그인에 실패했습니다. 다시 시도해주세요</string>
<string name="fetch_onboarding_status_fail_message">초기 설정 정보를 불러오지 못했습니다. 다시 시도해주세요</string>
🤖 Prompt for AI Agents
In `@feature/login/src/main/res/values/strings.xml` around lines 4 - 5, The two
string resources are identical but represent different failures; update the
value of fetch_onboarding_status_fail_message (currently identical to
login_fail_message) to a message that reflects onboarding-status retrieval
failure—for example use a Korean string like "온보딩 상태를 불러오지 못했습니다. 잠시 후 다시
시도해주세요"—so UI shows the correct error context when onBoarding status lookup
(fetch_onboarding_status_fail_message) fails while login (login_fail_message)
succeeded.

Comment on lines +26 to +30
sealed interface DdaySetting : OnBoardingSideEffect {
data object NavigateToHome : DdaySetting

data object ShowAnniversarySetupFailToast : ProfileSetting
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

ShowAnniversarySetupFailToastDdaySetting이 아닌 ProfileSetting을 상속하고 있습니다 — 이전에 수정된 버그와 동일한 패턴입니다

Line 29에서 ShowAnniversarySetupFailToastDdaySetting sealed interface 내부에 선언되어 있지만, : ProfileSetting을 상속하고 있습니다. 이전 리뷰에서 InviteCode.NavigateToNext : CoupleConnectionProfileSetting.NavigateToDDaySetting : CoupleConnection 등 동일한 패턴의 버그가 지적되어 수정되었는데, 이 케이스는 아직 남아있습니다.

왜 문제인가요?

  • is OnBoardingSideEffect.DdaySetting으로 패턴 매칭 시 ShowAnniversarySetupFailToast매칭되지 않습니다.
  • 대신 is OnBoardingSideEffect.ProfileSetting으로 매칭되어, D-day 설정 실패 토스트가 프로필 설정 화면의 사이드이펙트로 잘못 처리될 수 있습니다.

As per coding guidelines, "SideEffect가 일회성 이벤트로 분리되어 있는가?"

🐛 수정 제안
     sealed interface DdaySetting : OnBoardingSideEffect {
         data object NavigateToHome : DdaySetting
 
-        data object ShowAnniversarySetupFailToast : ProfileSetting
+        data object ShowAnniversarySetupFailToast : DdaySetting
     }
🤖 Prompt for AI Agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/model/OnBoardingSideEffect.kt`
around lines 26 - 30, ShowAnniversarySetupFailToast is declared inside the
sealed interface DdaySetting but incorrectly implements ProfileSetting, causing
it to be misclassified in pattern matches; update ShowAnniversarySetupFailToast
to implement DdaySetting (i.e., change its supertype from ProfileSetting to
DdaySetting or make it a member object of DdaySetting) so that when matching
OnBoardingSideEffect.DdaySetting it will be correctly recognized; check the
sealed interfaces DdaySetting, ProfileSetting and the data object
ShowAnniversarySetupFailToast to ensure the inheritance aligns with the intended
side-effect grouping.

Comment on lines +100 to +122
private fun fetchOnboardingStatus() {
launchResult(
block = { onBoardingRepository.fetchOnboardingStatus() },
onSuccess = { onboardingStatus ->
viewModelScope.launch {
val sideEffect =
when (onboardingStatus) {
OnboardingStatus.ANNIVERSARY_SETUP ->
OnBoardingSideEffect.ProfileSetting.NavigateToDDaySetting

OnboardingStatus.COMPLETED ->
OnBoardingSideEffect.ProfileSetting.NavigateToHome

else -> return@launch
}
emitSideEffect(sideEffect)
}
},
onError = {
// 에러처리 추가
},
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

fetchOnboardingStatusonError 핸들러가 비어 있습니다 — 사용자가 진행 불가 상태에 빠질 수 있습니다

Line 118-120에서 onError 블록에 주석만 있고 실제 에러 처리가 없습니다. profileSetup()fetchOnboardingStatus() 순서로 호출되므로, 프로필 설정은 성공했지만 온보딩 상태 조회가 실패하면 사용자는 어떤 피드백도 없이 화면에 머무르게 됩니다.

최소한 에러 토스트를 표시하거나, 재시도 메커니즘을 제공하는 것이 사용자 경험 측면에서 중요합니다.

🤖 Prompt for AI Agents
In
`@feature/onboarding/src/main/java/com/twix/onboarding/vm/OnBoardingViewModel.kt`
around lines 100 - 122, The onError block in fetchOnboardingStatus is empty and
must surface failure to the UI; update the launchResult(onError = { ... })
handler (in fetchOnboardingStatus) to emit a user-visible side effect or state,
e.g., call emitSideEffect with a new OnBoardingSideEffect.ProfileSetting.Error
or Retry action (or trigger a Toast via a UI event), and optionally include
logging of the throwable; ensure the chosen symbol (emitSideEffect and
OnBoardingSideEffect.ProfileSetting.*) is used so the UI can show an error
message and/or a retry option when fetchOnboardingStatus fails.

@chanho0908 chanho0908 closed this Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

온보딩 화면 구현

2 participants