Skip to content

feat: 온보딩-온보딩 UI 구현 - #41

Merged
codebidoof merged 25 commits into
developfrom
feature/21-onboarding-ui
Jul 31, 2026
Merged

feat: 온보딩-온보딩 UI 구현#41
codebidoof merged 25 commits into
developfrom
feature/21-onboarding-ui

Conversation

@dada4679

@dada4679 dada4679 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

📄 작업 내용 요약

-온보딩 UI 구현

📎 Issue 번호


✅ 작업 목록

  • 기능 구현
  • 코드 리뷰 반영
  • 테스트 코드 작성
  • 문서 업데이트

📝 기타 참고사항

스크린샷 2026-07-28 005817 스크린샷 2026-07-28 005809 스크린샷 2026-07-28 005757 스크린샷 2026-07-28 005849 스크린샷 2026-07-28 005834 스크린샷 2026-07-28 005824

Summary by CodeRabbit

  • 새로운 기능
    • 온보딩 1~6단계를 분리해 성/이름/생년월일·나이, 직업·MBTI, 말투·관계·연애 온도, 키워드(최대 5개), 선호 시간까지 단계별 입력·선택이 가능합니다.
    • 6단계 완료 후 “지금 전화할래/조금 이따할래”로 홈으로 이동합니다.
  • 디자인
    • 디자인 시스템 색상·그라데이션과 작은 텍스트 스타일을 확장하고, 진행률/선택 카드/날짜·온도·키워드 시각 요소를 강화했습니다.
  • 개선
    • 온보딩 화면 전환 시 백스택 처리와 안전영역/키보드 영역 반영을 안정화했습니다.

@dada4679 dada4679 linked an issue Jul 27, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

온보딩 1~6단계 화면과 입력·선택 컴포넌트를 추가하고, 단계별 네비게이션 키와 상태 저장, 앱 진입 및 완료 후 홈 이동을 연결했다. 날짜·온도·키워드·선호 시간 선택과 통화 확인 팝업도 포함한다.

Changes

온보딩 계약과 기반 UI

Layer / File(s) Summary
네비게이션 계약과 디자인 시스템
feature/onboarding/api/..., core/designsystem/..., feature/onboarding/impl/build.gradle.kts
6개 단계별 네비게이션 키, 색상·그라데이션·텍스트 스타일, Coil Compose 의존성을 추가했다.
온보딩 벡터 리소스
feature/onboarding/impl/src/main/res/drawable/*
뒤로가기·좌우 화살표·달력·프로필 벡터 리소스를 추가했다.

재사용 온보딩 컴포넌트

Layer / File(s) Summary
입력과 날짜 선택
feature/onboarding/impl/src/main/java/kr/co/call/impl/component/{AgeInputField,MessageInputField,NameBox,MemberChoice,BirthdayChoice,BottomWheel}.kt
나이·이름·메시지·드롭다운 입력과 nullable 생년월일 휠 선택 UI를 구현했다.
프로필과 선택 UI
feature/onboarding/impl/src/main/java/kr/co/call/impl/component/{ProfileChoice,KeywordChoice,PreferTime,SpeechChoice,RelatonshipChoiceCard}.kt
상태 기반 프로필 이미지, 키워드, 선호 시간, 말투와 관계 선택 UI를 추가했다.
진행 및 온도 조절
feature/onboarding/impl/src/main/java/kr/co/call/impl/component/{BackStepBar,TopTitle,TempControl}.kt
뒤로가기 바, 진행률 제목, 10 단위 스냅 방식의 드래그 온도 조절을 구현했다.

온보딩 화면

Layer / File(s) Summary
온보딩 1~3단계
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding{1,2,3}Screen.kt
기본 정보와 대화 설정을 상태 객체 및 공통 enum으로 전달하도록 구성했다.
온보딩 4~6단계
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding{4,5,6}Screen.kt
최대 5개 키워드, 선호 시간, 등록 완료 및 통화 선택 화면을 추가했다.

상태와 네비게이션

Layer / File(s) Summary
온보딩 상태 모델과 ViewModel
feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/...
단계별 상태 객체와 공통 enum을 추가하고, ViewModel 갱신 메서드로 사용자·AI 프로필 및 선택값을 저장했다.
단계별 엔트리 등록
feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt
6개 네비게이션 키에 화면을 등록하고 단계별 상태 갱신, 다음·뒤로가기 및 완료 콜백을 연결했다.
앱 네비게이션 연결
app/src/main/java/kr/co/call/callfromai/AppScreen.kt
로그인·온보딩·홈·약관 이동을 replaceAll 중심으로 조정하고 단계별 이동과 안전영역 inset을 연결했다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: 🧩 Component, ✨ Feature, 🎨 UI/Style

Sequence Diagram(s)

sequenceDiagram
  participant AppScreen
  participant OnboardingEntryBuilder
  participant OnboardingScreens
  participant OnboardingViewModel
  participant Home

  AppScreen->>OnboardingEntryBuilder: 단계별 엔트리와 콜백 등록
  AppScreen->>OnboardingScreens: Onboarding1NavKey로 시작
  OnboardingScreens->>OnboardingViewModel: 입력 상태 갱신
  OnboardingScreens->>AppScreen: 다음 단계 콜백 전달
  AppScreen->>OnboardingScreens: 다음 단계 NavKey로 이동
  OnboardingScreens->>AppScreen: 지금 전화 또는 나중 전화 이벤트 전달
  AppScreen->>Home: HomeNavKey로 replaceAll
Loading
🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
모듈 의존성 방향 검증 ⚠️ Warning core/domain의 ChatRepository.kt가 androidx.paging.PagingData를 import해 순수 Kotlin 유지 조건을 위반했습니다. core/domain에서 PagingData 의존을 제거하고, 페이지네이션 타입은 domain 바깥(data/impl)에서 변환하거나 순수 도메인 모델로 추상화하세요.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Conventional Commits 형식의 feat 제목이며 온보딩 UI 구현 내용을 잘 요약합니다.
Linked Issues check ✅ Passed 직접 링크된 #21의 온보딩 페이지 UI 구현 요구를 화면·컴포넌트·네비게이션 추가로 충족합니다.
Out of Scope Changes check ✅ Passed 온보딩 UI 구현에 필요한 테마, 리소스, 상태 모델, 의존성 변경만 보여 별도 이탈 범위는 보이지 않습니다.
Ui 변경 시 스크린샷 첨부 확인 ✅ Passed 화면 파일(Onboarding1~6Screen.kt)이 변경되었고, PR 본문에 img 태그 6개로 스크린샷이 첨부되어 있어 조건을 충족합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/21-onboarding-ui

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.

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요! 시니어 Android 개발자로서 GitHub Pull Request 리뷰를 시작하겠습니다. 전반적으로 온보딩 플로우를 위한 컴포저블들을 잘 분리하고 각 기능을 구현한 것으로 보입니다. 몇 가지 개선 사항과 피드백을 드릴게요.


General Feedback

  • Positive: 온보딩 플로우에 필요한 다양한 UI 컴포넌트들을 각각의 파일로 잘 분리하여 재사용성을 높이고 코드 관리가 용이하도록 구성한 점이 좋습니다. enum class를 활용하여 UI에 필요한 데이터를 깔끔하게 정의한 부분도 인상적입니다.
  • Missing Scope: 현재 PR diff에서는 Repository/DataSource, ViewModel, FCM/SSE 관련 코드 변경이 보이지 않습니다. 따라서 해당 레이어들에 대한 리뷰는 진행하기 어렵습니다. 만약 온보딩 플로우와 연관된 비즈니스 로직이나 데이터 처리가 있다면, 다음 PR에서 관련 코드를 함께 리뷰할 수 있도록 요청드립니다.

File-specific Feedback

app/src/main/java/kr/co/call/callfromai/AppScreen.kt

  • State Hoisting (Jetpack Compose Rule 2):
    • var aiFirstName by rememberSaveable { mutableStateOf("") }
    • aiFirstName은 온보딩 2단계에서 입력받는 사용자의 이름으로 보입니다. 이 상태는 AppScreen과 같이 애플리케이션의 최상위 컴포저블에서 관리하기보다는, 해당 이름을 필요로 하는 특정 온보딩 화면(예: Onboarding2Screen)의 ViewModel 또는 Onboarding2Screen 자체에서 상태를 호이스팅하여 관리하는 것이 State Hoisting 원칙에 더 부합합니다.
    • AppScreenaiFirstName이 있으면 AppScreen이 특정 온보딩 화면의 상세 구현에 의존하게 되어 컴포넌트 간 결합도가 높아지고, 불필요한 리컴포지션을 유발할 가능성이 있습니다.
    • 권장 사항: aiFirstName은 해당 상태를 사용하는 온보딩 화면의 ViewModel에서 관리하고, 필요한 경우 인텐트나 NavKeyarguments를 통해 다음 화면으로 전달하는 방식을 고려해주세요.
  • Hardcoded Navigation Start Point:
    • val backStack = rememberNavBackStack(Onboarding1NavKey)
    • 온보딩 플로우 테스트를 위한 임시 변경으로 이해합니다. TODO 주석과 동일하게, 실제 로그인 구현 후에는 사용자의 로그인 여부 또는 온보딩 완료 여부에 따라 적절한 시작점을 동적으로 결정해야 합니다.

core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt & Type.kt

  • Good: 새로운 온보딩 UI에 필요한 컬러 및 타이포그래피를 디자인 시스템에 추가하여 일관성과 재사용성을 확보한 점은 매우 좋습니다. DragGradient와 같은 Brush 정의도 깔끔합니다.

feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt

  • Good: 각 온보딩 단계별로 NavKeydata object로 명확하게 분리하고 @Serializable을 사용한 것은 Jetpack Navigation3의 모범 사례를 따른 것으로 보입니다.

feature/onboarding/impl/build.gradle.kts

  • Good: 이미지 로딩을 위한 Coil 라이브러리 추가는 적절합니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/AgeInputField.kt

  • State Hoisting (Jetpack Compose Rule 2): age: String, onAgeChange: (String) -> Unit으로 상태를 호이스팅한 점이 좋습니다.
  • Input Validation: input.all(Char::isDigit)을 사용하여 숫자만 입력받도록 제한한 것은 훌륭합니다.
  • Hardcoded String/Magic Number (Kotlin Rule 1): Text(text = "*", color = SubRed)에서 *는 하드코딩된 문자열입니다. 필수 입력 필드를 나타내는 시각적 요소로 사용되었지만, 유사한 패턴이 반복될 경우 stringResource로 관리하거나, isRequired: Boolean 파라미터를 받아 내부에서 처리하는 컴포저블로 만들면 더 좋습니다. 현재는 가독성을 위해 허용될 수 있습니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackStepBar.kt

  • Accessibility: contentDescription="뒤로가기"를 명시하여 스크린 리더 사용자에게 정보를 제공하는 점이 좋습니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt

  • State Hoisting (Jetpack Compose Rule 2): selectedDate, onDateSelected를 파라미터로 받아 상태를 호이스팅한 점이 좋습니다.
  • Internal UI State: showBottomWheel과 같이 컴포넌트 내부에서만 관리되는 UI 상태는 rememberSaveable을 사용하여 화면 회전과 같은 설정 변경에도 상태를 유지하도록 한 점이 적절합니다.
  • Accessibility: clickable(role=Role.Button)을 사용하여 클릭 가능한 영역의 의미를 명확히 한 점이 좋습니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt

  • State Hoisting (Jetpack Compose Rule 2): selectedDate, onConfirmClick, onDismissRequest를 파라미터로 받아 상태를 호이스팅한 점이 좋습니다.
  • Robust Date Handling: coerceAtMost를 사용하여 월 변경 시 일자가 유효 범위를 벗어나지 않도록 처리한 로직은 견고성을 높여줍니다.
  • Preview: 여러 파라미터를 가진 컴포저블의 다양한 상태를 미리보기로 제공한 점이 유용합니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt

  • UI State Representation (Jetpack Compose Rule 2): enum class Trait를 사용하여 키워드 관련 데이터를 응집력 있게 관리한 점이 매우 좋습니다. 이는 UI 상태를 data classenum class로 표현하는 좋은 예시입니다.
  • Semantic Role: toggleable(role=Role.Checkbox)를 사용한 것은 접근성 측면에서 올바른 선택입니다.
  • Hardcoded Magic Number (Kotlin Rule 1):
    • x=(-5).dp, y=(-5).dp (selectedOrder 표시 위치)
    • width = 1.dp, width = 1.5.dp (BorderStroke)
    • 이러한 값들이 반복적으로 사용되거나, 디자인 시스템의 일부로 관리될 필요가 있다면 Dimens 객체 등을 통해 상수로 정의하는 것을 고려해볼 수 있습니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt

  • State Hoisting (Jetpack Compose Rule 2): selectedOption, onOptionSelected를 파라미터로 받아 상태를 호이스팅한 점이 좋습니다.
  • Accessibility: clickable(role=Role.Button) 사용은 훌륭합니다.
  • DropdownMenu Positioning: BoxWithConstraintsDpOffset을 활용하여 드롭다운 메뉴가 올바른 위치에 표시되도록 조정한 점이 인상적입니다.
  • Hardcoded Magic Number (Kotlin Rule 1):
    • width(114.dp), heightIn(max=420.dp) (DropdownMenu 크기)
    • x=menuWidth-114.dp, y=4.dp (DropdownMenu 오프셋)
    • 이러한 값들은 특정 디자인 요구사항일 수 있지만, 만약 재사용되는 드롭다운 컴포넌트라면 크기나 위치 조정을 위한 파라미터를 추가하거나 디자인 시스템 내에서 표준화된 값을 사용하는 것이 좋습니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt

  • State Hoisting (Jetpack Compose Rule 2): value, onValueChange, onSendClick을 파라미터로 받아 상태를 호이스팅한 점이 좋습니다.
  • Side Effect (Jetpack Compose Rule 2): LaunchedEffect(autoFocus)를 사용하여 특정 조건에서만 키보드 포커스를 요청하고 키보드를 올리는 것은 LaunchedEffect의 적절한 사용 예시입니다.
  • Focus Management: LocalFocusManager, LocalSoftwareKeyboardController, FocusRequester를 사용하여 키보드 및 포커스를 효과적으로 제어하는 점이 좋습니다.
  • Accessibility: 전송 버튼에 contentDescription="전송"을 제공하여 접근성을 높였습니다.
  • Hardcoded Magic Number (Kotlin Rule 1): val maxLength=10은 하드코딩된 매직 넘버입니다. 이 값은 Constants 객체 등으로 분리하여 관리하는 것이 좋습니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt

  • State Hoisting (Jetpack Compose Rule 2): value, onClick을 파라미터로 받아 상태를 호이스팅한 점이 좋습니다.
  • Defensive UI: value.ifBlank { " " }를 사용하여 빈 문자열일 때도 최소한의 공간을 차지하도록 한 점이 UI 안정성에 기여합니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt

  • UI State Representation (Jetpack Compose Rule 2): enum class PreferTime을 사용하여 선호 시간대 관련 데이터를 응집력 있게 관리한 점이 매우 좋습니다.
  • Semantic Role: selectable(role = Role.RadioButton)을 사용한 것은 접근성 측면에서 올바른 선택입니다.
  • UI Logic: selected 여부에 따라 배경색, 테두리, 내부 아이콘의 색상을 동적으로 변경하는 로직이 깔끔하게 구현되었습니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt

  • State Hoisting (Jetpack Compose Rule 2): imageUrl, onClick을 파라미터로 받아 상태를 호이스팅한 점이 좋습니다.
  • Interaction Source: MutableInteractionSourcecollectIsPressedAsState()를 사용하여 커스텀 프레스 상태를 구현한 점이 좋습니다.
  • Image Loading: Coil의 AsyncImage를 사용하여 이미지 로딩을 처리한 점이 올바릅니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt

  • UI State Representation (Jetpack Compose Rule 2): RelationshipChoiceCard의 파라미터들이 UI 상태를 잘 표현하고 있습니다.
  • Semantic Role: selectable(role = Role.RadioButton)을 사용한 것은 접근성 측면에서 올바른 선택입니다.
  • Content Description: ImagecontentDescription = null을 명시한 것은 이미지가 순수하게 장식적인 역할을 할 때 스크린 리더가 불필요하게 읽지 않도록 하는 좋은 관행입니다.

Summary of Recommendations

  1. AppScreen.ktaiFirstName 상태 관리: 이 상태는 AppScreen보다는 실제 정보를 입력받는 온보딩 화면의 ViewModel에서 관리하는 것이 좋습니다. 상태 호이스팅 원칙에 따라 특정 화면의 상태를 너무 상위로 끌어올리지 않도록 해주세요.
  2. Hardcoded Magic Numbers: MessageInputField.ktmaxLength=10과 같이 중요한 비즈니스 로직에 사용되는 숫자는 상수로 분리하여 관리하는 것을 권장합니다.
  3. UI Dimen & Color Consistency: 많은 컴포넌트에서 dp 단위의 패딩, 크기 등이 하드코딩되어 있습니다. 현재는 개별 컴포넌트의 디자인 요구사항에 따라 적절해 보이지만, 애플리케이션 규모가 커질 경우 디자인 시스템 내에 Dimens 객체 등을 정의하여 일관된 값들을 재사용하는 것을 고려해볼 수 있습니다.
  4. ViewModel 및 데이터 레이어: 온보딩 플로우가 완료되면 사용자 정보 저장 등의 비즈니스 로직이 필요할 것입니다. 이 부분에 대한 ViewModelRepository 구현 시, 리뷰 규칙 3, 4번에 명시된 사항들을 준수해주시면 감사하겠습니다.

전반적으로 Jetpack Compose와 Kotlin의 모범 사례를 잘 따르고 있으며, 깔끔하게 잘 구현된 PR이라고 생각합니다. 위에 제시된 피드백들을 반영하여 더욱 견고하고 유지보수하기 쉬운 코드를 만들어 나갈 수 있기를 바랍니다.

궁금한 점이 있다면 언제든지 질문해주세요!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Nitpick comments (10)
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt (1)

38-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

SpeechStyle/Relationship enum을 Screen 파일 밖으로 분리 고려.

두 enum이 Onboarding3Screen.kt(UI 파일)에 정의되어 있고 OnboardingEntryBuilder, AppScreen에서 직접 import됩니다. Trait, PreferTime처럼 component/model 파일로 옮기면 UI 파일과 도메인성 타입 간 결합이 줄어듭니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt`
around lines 38 - 48, Onboarding3Screen의 SpeechStyle와 Relationship enum을 UI 파일
밖의 component/model 영역으로 이동하고, OnboardingEntryBuilder와 AppScreen을 포함한 모든 사용처의
import를 새 위치로 갱신하세요. Trait와 PreferTime이 배치된 기존 모델 구조와 명명 규칙을 따르며 enum 정의와 동작은
그대로 유지하세요.
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt (1)

204-251: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

이름 편집 오버레이 로직이 두 화면에 거의 동일하게 중복되어 있습니다. EditingNameField/Onboarding2EditingNameField enum 정의부터 배경 dismiss, MessageInputField 연동까지 동일한 패턴이 두 파일에 복사되어 있어, 향후 한쪽만 수정되면(예: 이번 리뷰의 focus/keyboard hide 누락처럼) 동작이 갈릴 위험이 있습니다.

  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt#L204-L251: enum·상태·오버레이 UI를 재사용 가능한 공용 컴포넌트(예: NameEditOverlay(editingField, nameInput, onValueChange, onSend, onDismiss, placeholder))로 추출.
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt#L215-L254: 동일 공용 컴포넌트를 사용하도록 교체.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt`
around lines 204 - 251, Extract the duplicated
EditingNameField/Onboarding2EditingNameField definitions and name-edit overlay
behavior into one reusable NameEditOverlay component, including background
dismissal, MessageInputField integration, placeholder handling, and callbacks.
Update
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt
lines 204-251 to use the shared component, and update
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt
lines 215-254 likewise; preserve each screen’s existing state updates through
the component callbacks.
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt (2)

34-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Modifier 파라미터가 다른 옵셔널 파라미터 뒤에 위치합니다. 두 화면 모두 modifier: Modifier = Modifier가 필수 파라미터 바로 뒤가 아니라 initialSelectedKeywords/initialPreferTime 같은 다른 옵셔널 파라미터보다 뒤에, 즉 파라미터 목록의 맨 마지막에 위치합니다.

  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt#L34-L39: modifieronNextClick 바로 뒤, initialSelectedKeywords 앞으로 이동.
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt#L29-L34: modifieronNextClick 바로 뒤, initialPreferTime 앞으로 이동.

As per path instructions for **/*Screen.kt, "Modifier 파라미터 위치(파라미터 앞쪽) 및 기본값 없이 전달 여부."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt`
around lines 34 - 39, Reorder the parameters in Onboarding4Screen
(feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt,
lines 34-39) so modifier: Modifier = Modifier immediately follows onNextClick
and precedes initialSelectedKeywords. Apply the same ordering in
Onboarding5Screen
(feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt,
lines 29-34), placing modifier before initialPreferTime.

Source: Path instructions


46-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

두 화면이 거의 동일한 스캐폴드 구조를 반복합니다. BackStepBarTopTitleSubGray2 배경의 스크롤 가능한 콘텐츠 컬럼(navigationBarsPadding 포함) → 하단 고정 SecondaryButton으로 이어지는 구조가 두 파일에서 그대로 복제되어 있어 공통 스캐폴드 컴포저블로 추출할 여지가 있습니다.

  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt#L46-L116: 이 레이아웃 골격을 공통 OnboardingStepScaffold(topTitle, bottomButton, content) 형태로 추출하는 후보로 사용.
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt#L38-L97: 동일한 공통 스캐폴드를 재사용하도록 리팩터링.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt`
around lines 46 - 116, Onboarding4Screen.kt lines 46-116와 Onboarding5Screen.kt
lines 38-97의 중복 레이아웃을 공통 OnboardingStepScaffold(topTitle, bottomButton, content)
컴포저블로 추출하세요. BackStepBar, TopTitle, SubGray2 콘텐츠 영역의 navigationBarsPadding, 하단
고정 SecondaryButton 구조를 스캐폴드가 담당하게 하고, 각 화면은 고유 콘텐츠와 버튼 동작만 content 및 인자로 전달하도록 두
파일을 리팩터링하세요.
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt (1)

90-117: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Onboarding6Screen의 custom trackPressState를 제거하세요.

PrimaryButton 이미 collectIsPressedAsState()로 내부 press 상태를 토글하고 이미지 버튼의 state도 토글 중이라 contentColor 조건은 쓸모없습니다. PrimaryButtoninteractionSource로 접근 가능한 public API가 아니라 현재 구조에서 커스텀 modifier를 붙일 필요가 없습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt`
around lines 90 - 117, Onboarding6Screen의 두 PrimaryButton에서 custom
trackPressState 사용과 isCallNowPressed/isCallLaterPressed 기반 contentColor 조건을
제거하세요. 각 버튼의 contentColor는 기존 비활성 색상(첫 번째는 White, 두 번째는 Gray600)으로 고정하고,
PrimaryButton의 내부 press 상태 처리와 pressedContainerColor 동작은 그대로 유지하세요.
feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt (1)

10-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

빈 객체 본문을 제거하세요.

Onboarding2NavKey만 불필요한 본문을 가져 detekt EmptyClassBlock 경고를 발생시킵니다. 다른 단계 키와 동일하게 본문 없는 선언으로 정리하세요.

수정 예시
-data object Onboarding2NavKey: NavKey {
-}
+data object Onboarding2NavKey : NavKey
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt`
around lines 10 - 11, Remove the empty body from the Onboarding2NavKey
declaration and make it a bodyless object declaration, matching the style of the
other onboarding navigation keys.

Source: Linters/SAST tools

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

파일명 오타: RelatonshipChoiceCard.ktRelationshipChoiceCard.kt.

파일명이 실제 컴포저블 이름(RelationshipChoiceCard, L39)과 철자가 다릅니다(“i” 누락). Kotlin 관례상 최상위 선언명과 파일명을 일치시키는 것이 탐색/일관성 측면에서 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt`
at line 1, Rename the file containing the RelationshipChoiceCard composable from
RelatonshipChoiceCard.kt to RelationshipChoiceCard.kt so the filename matches
the top-level declaration and corrects the missing “i”.
feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt (1)

31-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

keyword 필드가 enum 이름과 항상 동일해 중복됩니다.

각 항목의 keyword 값이 자기 자신의 enum 상수명과 완전히 같습니다(HUMOROUS("HUMOROUS", ...)). 이후 리팩터링 시 한쪽만 수정하면 fromKeyword 매핑이 조용히 깨질 수 있습니다. Trait.name을 직접 사용하도록 단순화하는 것을 권장합니다.

♻️ 제안 리팩터링
 enum class Trait(
-    val keyword: String,
     val emoji: String,
     val label: String,
 ){
-    HUMOROUS("HUMOROUS", "😃", "유머러스한"),
-    PLAYFUL("PLAYFUL", "🤡", "장난기 많은"),
+    HUMOROUS("😃", "유머러스한"),
+    PLAYFUL("🤡", "장난기 많은"),
     ...
     companion object {
         fun fromKeyword(key: String): Trait? {
-            return entries.find { it.keyword == key }
+            return entries.find { it.name == key }
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt`
around lines 31 - 63, Remove the redundant keyword constructor property and its
duplicated string arguments from Trait, and update fromKeyword to compare each
entry’s name directly with the supplied key. Preserve the existing enum names,
emojis, labels, and nullable lookup behavior.
feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt (2)

32-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

modifier 파라미터를 필수 파라미터 뒤로 옮겨주세요.

title, description 같은 필수 파라미터가 현재 modifier 뒤에 있죠. Compose 컴포지블 파라미터 기준에서는 modifier는 필수 파라미터 뒤의 첫 번째 선택적 인수여야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt`
around lines 32 - 39, TopTitle의 파라미터 순서를 수정해 필수 파라미터인 title과 description을 먼저
배치하고, 첫 번째 선택적 파라미터로 modifier를 배치하세요. currentStep, totalStep, horizontalPadding의
기존 순서와 기본값은 유지하세요.

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Compose 컴포저블의 modifier를 필수 파라미터 뒤로 배치하세요. Compose 가이드라인에서 modifier는 필수 파라미터 이후 첫 번째 선택적 파라미터여야 합니다.

  • TopTitle.kt: title/description을 먼저 두고 modifier를 그 뒤에 배치하세요.
  • MemberChoice.kt: label, selected, placeholder, options, onOptionSelected를 먼저 두고 modifier를 그 뒤에 배치하세요.
  • TempControl.kt: onTemperatureChange를 먼저 두고 modifier를 그 뒤에 배치하세요.
  • ProfileChoice.kt: imageUrl, onClick을 먼저 두고 modifier를 그 이후에 배치하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt`
at line 1, Reorder the parameters of the Compose functions in TopTitle.kt,
MemberChoice.kt, TempControl.kt, and ProfileChoice.kt so all required parameters
come first and modifier is the first optional parameter afterward:
title/description, label/selected/placeholder/options/onOptionSelected,
onTemperatureChange, and imageUrl/onClick respectively. Preserve the existing
parameter types, defaults, and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt`:
- Around line 59-60: Update the comment above the rememberNavBackStack call to
accurately state that navigation currently starts at Onboarding1NavKey, while
retaining the note that login-based branching will be implemented later.
- Around line 118-157: Update the onboardingEntry callbacks in AppScreen so
every value received by onOnboarding1Next, onOnboarding2Next, onOnboarding3Next,
onOnboarding4Next, and onOnboarding5Next is retained and propagated through the
existing state or domain/data persistence pipeline; do not discard the
birthday/job/MBTI, speech style/temperature, keyword, preference-time, or other
onboarding fields while navigating. If no persistence path exists yet,
explicitly document the intentional data discard at these callbacks.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt`:
- Around line 48-56: Reorder the MemberChoice function parameters so the
required label, selectedOption, placeholder, options, and onOptionSelected
parameters come first, followed by modifier, then the remaining optional
required parameter.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt`:
- Around line 29-34: Update the ProfileChoice function signature so the required
imageUrl and onClick parameters come first, followed by modifier with a default
of Modifier, then the existing size default. Preserve the function behavior and
update any affected call sites to use the revised parameter order.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt`:
- Around line 55-58: TempControl의 파라미터 순서를 변경해 필수 파라미터인 onTemperatureChange를
modifier보다 앞에 배치하세요. modifier의 기본값과 나머지 동작은 그대로 유지해 Compose 권장 순서와 컴파일 가능성을
보장합니다.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt`:
- Line 57: Rename the profileImageURl parameter to profileImageUrl in the
Onboarding1Screen declaration and update all references within that file,
including the usages around lines 115 and 268, while preserving the existing
behavior.
- Around line 46-58: Onboarding1Screen 변경에 대한 PR 설명에 해당 화면(사진 선택/나를 소개하기)의 스크린샷
또는 화면 녹화를 추가했는지 확인하고, 누락되었다면 Onboarding2/4/6 캡처와 함께 첨부한다.
- Around line 204-236: Update the name-edit overlay dismissal paths in
Onboarding1Screen so both the background clickable handler and
MessageInputField.onSendClick clear focus through focusManager and hide the
keyboard through keyboardController before resetting the editing state. Match
the existing handling used by Onboarding2Screen while preserving the current
name update behavior.
- Around line 65-67: Update the birthday state declaration in Onboarding1Screen
to use rememberSaveable instead of remember, persisting LocalDate through a
Bundle-compatible representation such as an ISO-8601 string or dayOfYear and
restoring it to LocalDate. Preserve 2000-01-01 as the initial default while
retaining the user-selected birthday across recomposition and state restoration.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt`:
- Around line 50-59: Onboarding3Screen의 말투·관계 선택 화면이 PR 설명에 포함되도록 해당 화면의 스크린샷 또는
화면 녹화를 추가하세요. 기존 Onboarding2/4/6 캡처와 별도로 Onboarding3Screen이 명확히 식별되어야 합니다.
- Around line 193-198: Replace the non-null assertions in the onNextClick
invocation with nested or combined let calls on selectedSpeech and
selectedRelationship, invoking onNextClick only when both values are available.
Preserve the existing temperature argument and canMoveNext behavior while
removing the runtime-unsafe !! operators.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt`:
- Around line 1-114: Update the PR description to include a screenshot or screen
recording of the Onboarding5Screen preference-time selection UI, alongside the
existing onboarding screen evidence; no code changes are required.
- Around line 35-37: Initialize selectedPreferTime in Onboarding5Screen’s
rememberSaveable state from the initialPreferTime parameter instead of always
using null. Preserve saved state restoration while ensuring the supplied initial
value, such as PreferTime.DAY in the preview, renders as selected when no saved
value exists.
- Around line 86-94: Update the SecondaryButton in the onboarding screen to pass
enabled = selectedPreferTime != null, keeping the existing onClick behavior so
the “다음” button is disabled until a preferred time is selected.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt`:
- Around line 159-174: Update the TwoButtonPopup invocation in the
showCallDialog block so onDismissRequest sets showCallDialog to false, allowing
back-button and outside-tap dismissal while preserving the existing positive and
negative click behavior.
- Line 61: Rename the Boolean state variable showCallDialog in the onboarding
screen to an is-prefixed name such as isCallDialogVisible, and update every
usage accordingly, including its initialization, conditional checks, and
assignments in the dialog click handlers.

---

Nitpick comments:
In `@feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt`:
- Around line 10-11: Remove the empty body from the Onboarding2NavKey
declaration and make it a bodyless object declaration, matching the style of the
other onboarding navigation keys.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt`:
- Around line 31-63: Remove the redundant keyword constructor property and its
duplicated string arguments from Trait, and update fromKeyword to compare each
entry’s name directly with the supplied key. Preserve the existing enum names,
emojis, labels, and nullable lookup behavior.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt`:
- Line 1: Rename the file containing the RelationshipChoiceCard composable from
RelatonshipChoiceCard.kt to RelationshipChoiceCard.kt so the filename matches
the top-level declaration and corrects the missing “i”.

In `@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt`:
- Around line 32-39: TopTitle의 파라미터 순서를 수정해 필수 파라미터인 title과 description을 먼저
배치하고, 첫 번째 선택적 파라미터로 modifier를 배치하세요. currentStep, totalStep, horizontalPadding의
기존 순서와 기본값은 유지하세요.
- Line 1: Reorder the parameters of the Compose functions in TopTitle.kt,
MemberChoice.kt, TempControl.kt, and ProfileChoice.kt so all required parameters
come first and modifier is the first optional parameter afterward:
title/description, label/selected/placeholder/options/onOptionSelected,
onTemperatureChange, and imageUrl/onClick respectively. Preserve the existing
parameter types, defaults, and behavior.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt`:
- Around line 204-251: Extract the duplicated
EditingNameField/Onboarding2EditingNameField definitions and name-edit overlay
behavior into one reusable NameEditOverlay component, including background
dismissal, MessageInputField integration, placeholder handling, and callbacks.
Update
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt
lines 204-251 to use the shared component, and update
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt
lines 215-254 likewise; preserve each screen’s existing state updates through
the component callbacks.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt`:
- Around line 38-48: Onboarding3Screen의 SpeechStyle와 Relationship enum을 UI 파일 밖의
component/model 영역으로 이동하고, OnboardingEntryBuilder와 AppScreen을 포함한 모든 사용처의
import를 새 위치로 갱신하세요. Trait와 PreferTime이 배치된 기존 모델 구조와 명명 규칙을 따르며 enum 정의와 동작은
그대로 유지하세요.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt`:
- Around line 34-39: Reorder the parameters in Onboarding4Screen
(feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt,
lines 34-39) so modifier: Modifier = Modifier immediately follows onNextClick
and precedes initialSelectedKeywords. Apply the same ordering in
Onboarding5Screen
(feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt,
lines 29-34), placing modifier before initialPreferTime.
- Around line 46-116: Onboarding4Screen.kt lines 46-116와 Onboarding5Screen.kt
lines 38-97의 중복 레이아웃을 공통 OnboardingStepScaffold(topTitle, bottomButton, content)
컴포저블로 추출하세요. BackStepBar, TopTitle, SubGray2 콘텐츠 영역의 navigationBarsPadding, 하단
고정 SecondaryButton 구조를 스캐폴드가 담당하게 하고, 각 화면은 고유 콘텐츠와 버튼 동작만 content 및 인자로 전달하도록 두
파일을 리팩터링하세요.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt`:
- Around line 90-117: Onboarding6Screen의 두 PrimaryButton에서 custom
trackPressState 사용과 isCallNowPressed/isCallLaterPressed 기반 contentColor 조건을
제거하세요. 각 버튼의 contentColor는 기존 비활성 색상(첫 번째는 White, 두 번째는 Gray600)으로 고정하고,
PrimaryButton의 내부 press 상태 처리와 pressedContainerColor 동작은 그대로 유지하세요.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8fb2d5e-9010-4ffc-834d-1ebe16c0f9cc

📥 Commits

Reviewing files that changed from the base of the PR and between cdb233f and c8d63f7.

⛔ Files ignored due to path filters (7)
  • feature/onboarding/impl/src/main/res/drawable/ic_down.png is excluded by !**/*.png
  • feature/onboarding/impl/src/main/res/drawable/ic_up.png is excluded by !**/*.png
  • feature/onboarding/impl/src/main/res/drawable/onboarding_check.png is excluded by !**/*.png
  • feature/onboarding/impl/src/main/res/drawable/relationship_first.png is excluded by !**/*.png
  • feature/onboarding/impl/src/main/res/drawable/relationship_old.png is excluded by !**/*.png
  • feature/onboarding/impl/src/main/res/drawable/relationship_some.png is excluded by !**/*.png
  • feature/onboarding/impl/src/main/res/drawable/spendbutton.png is excluded by !**/*.png
📒 Files selected for processing (33)
  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt
  • core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt
  • core/designsystem/src/main/java/kr/co/call/designsystem/theme/Type.kt
  • feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt
  • feature/onboarding/impl/build.gradle.kts
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/AgeInputField.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackStepBar.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/SpeechChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/OnboardingScreen.kt
  • feature/onboarding/impl/src/main/res/drawable/ic_back.xml
  • feature/onboarding/impl/src/main/res/drawable/ic_calendar.xml
  • feature/onboarding/impl/src/main/res/drawable/ic_left.xml
  • feature/onboarding/impl/src/main/res/drawable/ic_right.xml
  • feature/onboarding/impl/src/main/res/drawable/onboarding_profile.xml
  • feature/onboarding/impl/src/main/res/drawable/onboarding_profile2.xml
💤 Files with no reviewable changes (1)
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/OnboardingScreen.kt

Comment on lines +59 to +60
// TODO: 로그인 구현 후 로그인 여부에 따른 분기처리 필요. 일단은 시작점을 홈 화면으로 설정
val backStack = rememberNavBackStack(HomeNavKey)
val backStack = rememberNavBackStack(Onboarding1NavKey)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

주석과 코드가 불일치합니다.

주석은 "시작점을 홈 화면으로 설정"이라고 되어 있지만, 실제로는 Onboarding1NavKey로 시작하도록 변경되었습니다. 주석을 갱신해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt` around lines 59 - 60,
Update the comment above the rememberNavBackStack call to accurately state that
navigation currently starts at Onboarding1NavKey, while retaining the note that
login-based branching will be implemented later.

Comment thread app/src/main/java/kr/co/call/callfromai/AppScreen.kt
Comment on lines +48 to +56
fun MemberChoice(
modifier: Modifier=Modifier,
label: String,
selectedOption: String, //드롭다운이 보여줄 값
placeholder: String,
options:List<String>, //선택 가능한 목록
onOptionSelected: (String)->Unit,//선택됐을 때 실행할 동작
required: Boolean=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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the target file around the function signature.
if [ -f feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt ]; then
  sed -n '1,90p' feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt
else
  echo "Target file not found"
fi

# Search for relevant Compose guideline/docs references in repo if any local copy exists.
rg -n "Modifier must be|modifier parameter|first optional|optional parameter" . \
  --glob '!build/**' \
  --glob '!**/build/**' \
  | head -100 || true

Repository: call-from-ai/CallFromAi_Android

Length of output: 3327


🌐 Web query:

Jetpack Compose developer guidelines modifier parameter order first optional parameter after required parameters

💡 Result:

According to the official Jetpack Compose API guidelines, the modifier parameter must be the first optional parameter in a composable function's parameter list [1][2][3]. The required parameter order for a composable function is as follows [1][2]: 1. Required parameters (parameters without default values) [1][2]. 2. Single modifier: Modifier = Modifier parameter [1][2][4]. 3. Optional parameters (parameters with default values) [1][2]. 4. (Optional) Trailing @Composable lambda (typically named content) [1][2]. This convention is enforced to ensure consistency across the API surface, allowing developers to predictably pass a modifier to any composable as the first optional argument without needing to use named parameters [4][5]. The modifier parameter must be named exactly modifier, have a default value of Modifier, and be applied to the root-most layout element within the composable implementation [1][6][7][3]. This guideline is also enforced by automated lint checks in Android Studio [8].

Citations:


Modifier 파라미터 위치를 필수 파라미터 뒤로 옮기세요.

Compose API 가이드에 따라 modifier는 필수 파라미터 후, 다른 선택적 파라미터 전에 있어야 합니다. 현재 MemberChoicelabel, selectedOption, placeholder, options, onOptionSelected 뒤에 위치하도록 정리하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt`
around lines 48 - 56, Reorder the MemberChoice function parameters so the
required label, selectedOption, placeholder, options, and onOptionSelected
parameters come first, followed by modifier, then the remaining optional
required parameter.

Comment on lines +1 to +114
package kr.co.call.impl.screen

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kr.co.call.designsystem.component.button.SecondaryButton
import kr.co.call.designsystem.theme.SubGray2
import kr.co.call.designsystem.theme.White
import kr.co.call.impl.component.BackStepBar
import kr.co.call.impl.component.PreferTime
import kr.co.call.impl.component.TopTitle
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.Preview
import kr.co.call.designsystem.theme.CallFromAiTheme

@Composable
fun Onboarding5Screen(
onBackClick: () -> Unit,
onNextClick: (PreferTime) -> Unit,
initialPreferTime: PreferTime? = null,
modifier: Modifier = Modifier,
){
var selectedPreferTime by rememberSaveable {
mutableStateOf<PreferTime?>(null)
}
Column(
modifier = modifier
.fillMaxSize()
.padding(
top = 54.dp,
bottom = 18.dp,
)
.background(White),
) {
BackStepBar(onBackClick = onBackClick)

TopTitle(
title = "언제 먼저 통화를\n걸어드리면 좋을까요?",
description = "편하게 통화하기 좋은 시간대를 선택해주세요.",
currentStep = 4,
totalStep = 4,
horizontalPadding = 27.dp,
)

Spacer(modifier = Modifier.height(5.dp))

Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.background(SubGray2)
.navigationBarsPadding(),
) {
Column(
modifier = Modifier.padding(
start = 27.dp,
end = 27.dp,
top = 26.dp,
),
verticalArrangement = Arrangement.spacedBy(22.dp),
) {
PreferTime.entries
.forEach { preferTime ->
PreferTime(
preferTime = preferTime,
selected = selectedPreferTime == preferTime,
onClick = { selectedPreferTime = preferTime },
)
}
}

Spacer(modifier = Modifier.weight(1f))

SecondaryButton(
modifier = Modifier.padding(
start = 27.dp,
end = 27.dp,
bottom = 18.dp,
),
text = "다음",
onClick = { selectedPreferTime?.let(onNextClick) },
)
}
}
}

@Preview(
name = "Onboarding 5",
showBackground = true,
widthDp = 393,
heightDp = 852,
)
@Composable
private fun Onboarding5ScreenPreview() {
CallFromAiTheme {
Onboarding5Screen(
onBackClick = {},
onNextClick = {},
initialPreferTime = PreferTime.DAY,
)
}
} No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

변경된 Screen.kt 파일에 대한 스크린샷 누락 가능성.

PR 설명에는 온보딩 2단계(Image 1), 4단계(Image 2), 6단계(Image 3) 화면 이미지만 첨부되어 있고, 이 파일이 구현하는 5단계(선호 시간 선택) 화면에 대한 스크린샷/화면 녹화는 보이지 않습니다.

As per coding guidelines, "Jetpack Compose 화면 파일(예: *Screen.kt...)이 변경된 경우, PR 설명에 스크린샷 또는 화면 녹화가 첨부되었는지 확인하고 없으면 경고한다."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt`
around lines 1 - 114, Update the PR description to include a screenshot or
screen recording of the Onboarding5Screen preference-time selection UI,
alongside the existing onboarding screen evidence; no code changes are required.

Source: Coding guidelines

Comment thread feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt Outdated
Comment on lines +159 to +174
if (showCallDialog){
TwoButtonPopup(
label="통화 연결",
title="${firstName}에게 바로\n통화를 연결할까요?",
positiveText = "연결",
negativeText = "취소",
onPositiveClick = {
showCallDialog=false
onCallNowClick()
},
onNegativeClick = {
showCallDialog=false
},
onDismissRequest = {},
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

팝업이 뒤로가기/바깥 탭으로 닫히지 않습니다.

onDismissRequest = {}로 비워져 있어 시스템 뒤로가기나 다이얼로그 바깥을 탭해도 닫히지 않고, "취소" 버튼을 눌러야만 닫힙니다. 의도적인 강제 선택이 아니라면 onDismissRequest = { showCallDialog = false }로 맞추는 것이 일반적인 다이얼로그 UX에 부합합니다.

🛠️ 제안하는 수정
             onNegativeClick = {
                 showCallDialog=false
             },
-            onDismissRequest = {},
+            onDismissRequest = { showCallDialog = false },
         )
📝 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 (showCallDialog){
TwoButtonPopup(
label="통화 연결",
title="${firstName}에게 바로\n통화를 연결할까요?",
positiveText = "연결",
negativeText = "취소",
onPositiveClick = {
showCallDialog=false
onCallNowClick()
},
onNegativeClick = {
showCallDialog=false
},
onDismissRequest = {},
)
}
if (showCallDialog){
TwoButtonPopup(
label="통화 연결",
title="${firstName}에게 바로\n통화를 연결할까요?",
positiveText = "연결",
negativeText = "취소",
onPositiveClick = {
showCallDialog=false
onCallNowClick()
},
onNegativeClick = {
showCallDialog=false
},
onDismissRequest = { showCallDialog = false },
)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt`
around lines 159 - 174, Update the TwoButtonPopup invocation in the
showCallDialog block so onDismissRequest sets showCallDialog to false, allowing
back-button and outside-tap dismissal while preserving the existing positive and
negative click behavior.

@dada4679 dada4679 changed the title Feature/21 onboarding UI feat: 온보딩-온보딩 UI 구현 Jul 27, 2026
@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요! PR을 잘 만드셨네요. 시니어 개발자로서 PR diff를 꼼꼼히 리뷰해 드리겠습니다. 전반적으로 새로운 온보딩 플로우를 잘 구현하신 것 같고, Compose 컴포넌트들을 재사용성 높게 잘 분리하신 점이 인상 깊습니다. 몇 가지 개선 사항과 주의 깊게 봐야 할 부분을 짚어드리겠습니다.


🔍 PR Diff 코드 리뷰

1. Kotlin & General

  • null 안전성 (!! 사용 지양): 새로 추가된 컴포넌트 코드에서는 !! 사용이 보이지 않습니다. 좋습니다! 기존 코드나 ViewModel 등에서 혹시 사용되는 부분이 없는지 항상 확인하는 습관을 들이면 좋습니다.
  • data class에 불필요한 var 사용 여부: diff에 data class의 변경은 없지만, enum class TraitPreferTime처럼 불변 데이터를 잘 정의했습니다. 좋습니다.
  • 하드코딩된 문자열/매직 넘버:
    • 새로 추가된 컴포넌트들 (AgeInputField, BirthdayChoice, MessageInputField, MemberChoice 등)에서 사용자에게 보여지는 텍스트(예: "만 나이", "(세)", "생년월일", "직업", "메시지를 입력해주세요.")들이 문자열 리소스(strings.xml)로 분리되지 않고 하드코딩되어 있습니다. 국제화(i18n) 및 유지보수를 위해 strings.xml로 분리하는 것이 좋습니다.
    • MessageInputFieldmaxLength=10과 같은 매직 넘버는 상수로 정의하거나 비즈니스 로직에 따라 ViewModel/Repository에서 관리하는 것이 더 좋을 수 있습니다. 현재는 UI 컴포넌트 내부 로직이므로, 필요시 상수로 분리하는 것을 고려해 보세요.
    • dp 값들은 디자인 시스템의 일부로 간주될 수 있으나, 만약 공통적으로 사용되는 패딩이나 크기가 있다면 디자인 시스템의 Spacing 객체 등을 통해 관리하는 것도 좋습니다.

2. Jetpack Compose

  • AppScreen.kt 변경 사항:
    • OnboardingViewModel 사용 위치 문제 (Critical): AppScreen에서 hiltViewModel()을 사용하여 OnboardingViewModel을 주입받는 것은 매우 심각한 문제입니다. AppScreen은 앱의 최상단 컴포저블이며, 이 곳에서 ViewModel을 주입받으면 OnboardingViewModel의 라이프사이클이 앱 전체와 동일하게 됩니다. 온보딩이 완료된 후에도 ViewModel 인스턴스가 계속 메모리에 남아있게 되어 리소스 낭비를 초래하고, 의도치 않은 상태 공유 문제를 일으킬 수 있습니다.
      • 개선 방안: OnboardingViewModel은 온보딩 화면(예: Onboarding1Screen, Onboarding2Screen)에서 hiltViewModel()로 주입받아 해당 화면의 라이프사이클에 맞춰 생성 및 소멸되도록 해야 합니다. 각 온보딩 화면은 필요한 ViewModel 인스턴스를 직접 가져와 사용해야 합니다.
    • entryDecorators 제거 (Critical): rememberSaveableStateHolderNavEntryDecorator()rememberViewModelStoreNavEntryDecorator()가 제거되었습니다. rememberSaveableStateHolderNavEntryDecorator는 내비게이션 스택에서 화면이 pop되었다가 다시 push될 때 해당 화면의 Compose UI 상태(예: 스크롤 위치, 입력 필드 값)를 보존하는 데 필수적입니다. 이 데코레이터가 없으면 뒤로 가기 후 다시 들어왔을 때 UI 상태가 초기화될 수 있습니다. hiltViewModel()을 사용하더라도 ViewModelStoreNavEntryDecorator는 ViewModel 인스턴스의 라이프사이클을 내비게이션 스택 엔트리에 바인딩하는 중요한 역할을 합니다. 이 변경이 의도된 것인지, 어떤 이유로 제거되었는지 확인이 필요합니다. 만약 의도치 않은 제거라면 다시 추가해야 합니다.
    • WindowInsets.ime exclude 제거: contentWindowInsets에서 exclude(WindowInsets.ime)가 제거되었습니다. 이는 키보드가 올라올 때 UI가 자동으로 밀려나지 않고 키보드에 가려질 수 있음을 의미합니다. 의도된 동작인지 확인해야 합니다. 보통 입력 필드가 있는 화면에서는 exclude(WindowInsets.ime)를 유지하거나 imePadding() modifier를 사용하여 키보드에 맞춰 UI를 조정하는 것이 사용자 경험에 좋습니다.
    • homeEntry 중복 정의 및 사용 혼란: onboardingEntry에서 onOnboarding6CallLaterHomeNavKey로 이동하는데, AppScreen에서는 homeEntry()를 파라미터 없이 호출하고 있습니다. 기존 homeEntry(navigateToCallRecord = { ... }, onCallRecordBack = { ... })는 이제 AppScreenentryProvider에서 직접 사용되지 않는 것으로 보입니다. 온보딩 완료 후 진입하는 HomeNavKey가 어떤 homeEntry 정의를 따르는지 명확하게 하고, 불필요한 정의는 제거하는 것이 좋습니다.
  • 새로 추가된 컴포저블들 (AgeInputField, BirthdayChoice, KeywordChoice 등):
    • Composable 함수의 불필요한 recomposition 유발 여부 (remember, key 사용): remember, rememberSaveable 등이 적절하게 사용되어 내부 상태를 관리하고 있습니다. BottomWheel에서 rememberLazyListState를 사용하여 스크롤 상태를 보존하는 것도 좋습니다. 전반적으로 불필요한 리컴포지션을 유발하는 요소는 크게 보이지 않습니다.
    • State hoisting 원칙 준수: 대부분의 컴포넌트가 value, onValueChange, selected, onClick 등 상태와 이벤트를 상위 컴포저블로 호이스팅하는 패턴을 잘 따르고 있습니다. 이는 컴포넌트의 재사용성을 높이고 테스트를 용이하게 합니다.
    • side effect (LaunchedEffect, DisposableEffect) 사용의 적절성: MessageInputField에서 LaunchedEffect(autoFocus)를 사용하여 특정 조건에서 키보드를 올리고 포커스를 요청하는 것은 Side Effect의 적절한 사용 예시입니다. 좋습니다.
    • UI 상태를 data class로 표현했는지: Trait, PreferTime 같은 enum class를 사용하여 선택 가능한 항목들을 잘 표현했습니다. 이는 좋습니다. 각 온보딩 화면의 복잡한 UI 상태는 data class로 정의하여 ViewModel에서 관리하는 것이 바람직합니다. (ViewModel diff가 없으므로 추정입니다.)
    • 로딩/에러 상태를 Boolean 대신 LoadStatus로 관리하는지: UI 컴포넌트 수준에서는 직접적인 LoadStatus 관리가 보이지 않지만, 이는 주로 ViewModel과 화면 수준에서 관리될 내용입니다.

3. Repository/DataSource (Diff에 없음)

  • 해당 레이어에 대한 변경 사항이 PR에 포함되어 있지 않아 리뷰할 내용이 없습니다. 하지만 온보딩 과정에서 사용자 정보를 저장하거나 네트워크 통신을 해야 한다면, 이 규칙들을 염두에 두고 구현해야 합니다.
    • Retrofit 에러 핸들링 (try-catch, Result 래핑)
    • 네트워크 응답과 도메인 모델 매핑 분리 여부
    • 캐싱 전략 (로컬 DB vs 메모리)

4. ViewModel (OnboardingViewModel 관련)

  • AppScreen.kt에서 OnboardingViewModelhiltViewModel()로 주입하는 방식 문제 (Critical): 위 Compose 섹션에서 언급했듯이, 이는 ViewModel의 라이프사이클 관리 측면에서 매우 잘못된 사용입니다. 온보딩 플로우 내의 특정 화면에서 hiltViewModel()로 주입해야 합니다.
  • UI 상태와 비즈니스 로직 분리: OnboardingViewModel의 코드가 없으므로 직접 확인할 수는 없지만, 새로 추가된 컴포넌트들이 UI 상태를 잘 호이스팅하고 있으므로, ViewModel은 비즈니스 로직에 집중하고 UI 상태를 관리하는 역할을 잘 수행하고 있을 것으로 기대합니다.
  • Orbit ContainerHost 패턴 준수 여부: OnboardingViewModelContainerHost가 적용되었는지 확인이 필요합니다. Orbit을 사용한다면 ContainerHost를 상속받고 intent 블록 내에서 reducepostSideEffect를 사용하여 상태 변경 및 일회성 이벤트를 처리하는지 확인해야 합니다.
  • intent 내부에서 상태 변경(reduce)만 수행하는지: intent 블록 내에서 reduce를 통해 상태를 변경하고, postSideEffect는 UI에서 한 번만 처리되어야 하는 이벤트(예: 토스트 메시지, 내비게이션)에 사용하는지 확인이 필요합니다.
  • postSideEffect 남용 여부: postSideEffect는 신중하게 사용해야 합니다. 상태(State)로 표현될 수 있는 모든 것은 reduce를 통해 관리하는 것이 좋습니다.
  • blocking 작업을 intent 내부에서 직접 수행하지 않는지: 네트워크 요청이나 DB 접근 등 시간이 걸리는 작업은 viewModelScope.launch 내에서 비동기로 처리되어야 합니다.

5. FCM/SSE/실시간 통신 (Diff에 없음)

  • 해당 PR에는 관련 코드 변경이 없으므로 해당 규칙은 적용되지 않습니다.

총평

새로운 온보딩 플로우를 위한 컴포넌트들이 매우 체계적이고 재사용 가능하게 잘 구현되었습니다. enum class를 활용한 상태 정의, rememberSaveableLaunchedEffect의 적절한 사용 등 Compose 모범 사례를 잘 따르고 있습니다.

가장 중요한 개선 사항은 AppScreen.kt에서 OnboardingViewModel을 직접 주입하는 방식과 entryDecorators 제거 문제입니다. 이 두 가지는 앱의 안정성과 성능, 그리고 사용자 경험에 직접적인 영향을 미칠 수 있으므로 반드시 수정되어야 합니다.

또한, 사용자에게 보여지는 텍스트는 strings.xml로 분리하여 관리하는 것을 권장합니다.

전반적으로 훌륭한 작업이며, 지적된 사항들을 개선하면 더욱 견고하고 유지보수하기 쉬운 코드가 될 것입니다. 수고 많으셨습니다!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
app/src/main/java/kr/co/call/callfromai/AppScreen.kt (1)

151-153: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

완료 후 온보딩 백스택을 정리하세요.

appNavigator.navigate(HomeNavKey)는 단순 push라서 홈에서 뒤로가기를 하면 온보딩 6 단계 및 이전 온보딩 단계가 다시 노출됩니다. 완료 시 homeEntrynavigateToHome처럼 appNavigator.replaceAll(HomeNavKey)를 사용하거나, 온보딩 경로를 제거하는 별도 내비게이션 함수로 교체하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt` around lines 151 - 153,
온보딩 완료 후 백스택이 남지 않도록 onOnboarding6CallLater의 appNavigator.navigate(HomeNavKey)를
appNavigator.replaceAll(HomeNavKey)로 변경하세요. homeEntry의 navigateToHome과 동일하게 홈만
남기는 동작을 유지하세요.
🧹 Nitpick comments (1)
feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt (1)

36-102: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

저장한 온보딩 상태를 각 단계의 초기값으로 다시 주입하세요.

현재 uiState는 6단계의 이름 표시에만 사용됩니다. 앞 단계 destination이 재생성되면 입력 UI가 기본값으로 시작합니다. 4·5단계에는 각각 traitspreferTime을 초기값으로 전달하고, 1~3단계도 동일하게 상태 호이스팅 가능한 초기값 계약을 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt`
around lines 36 - 102, Update OnboardingEntryBuilder so each onboarding screen
receives its saved onboardingViewModel uiState values as initial inputs when
destinations are recreated. Add initial-value parameters to Onboarding1Screen,
Onboarding2Screen, and Onboarding3Screen, and pass the corresponding saved
profile, AI profile, and conversation-style state; also pass traits and
preferTime to Onboarding4Screen and Onboarding5Screen. Preserve the existing
update callbacks while ensuring every step can restore its prior inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt`:
- Around line 148-150: Update the onOnboarding6CallNow callback in AppScreen so
tapping “연결” either navigates to the existing call route or disables the CTA
until that destination is implemented; remove the no-op TODO behavior and
preserve the onboarding flow.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt`:
- Around line 16-83: Update OnboardingViewModel to use the Orbit MVI
ContainerHost pattern used by other feature implementation ViewModels: replace
the MutableStateFlow fields and each direct update call in updateUserProfile,
updateAiProfile, updateConversationStyle, updateTraits, and updatePreferTime
with intent blocks that reduce state via state.copy(...). Preserve the existing
field assignments and expose the Orbit container state through the established
pattern.

---

Outside diff comments:
In `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt`:
- Around line 151-153: 온보딩 완료 후 백스택이 남지 않도록 onOnboarding6CallLater의
appNavigator.navigate(HomeNavKey)를 appNavigator.replaceAll(HomeNavKey)로 변경하세요.
homeEntry의 navigateToHome과 동일하게 홈만 남기는 동작을 유지하세요.

---

Nitpick comments:
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt`:
- Around line 36-102: Update OnboardingEntryBuilder so each onboarding screen
receives its saved onboardingViewModel uiState values as initial inputs when
destinations are recreated. Add initial-value parameters to Onboarding1Screen,
Onboarding2Screen, and Onboarding3Screen, and pass the corresponding saved
profile, AI profile, and conversation-style state; also pass traits and
preferTime to Onboarding4Screen and Onboarding5Screen. Preserve the existing
update callbacks while ensuring every step can restore its prior inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e329b28-c6e9-4d4d-b0c1-826e95e30648

📥 Commits

Reviewing files that changed from the base of the PR and between c8d63f7 and 40252a7.

📒 Files selected for processing (9)
  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt

Comment thread app/src/main/java/kr/co/call/callfromai/AppScreen.kt
@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요, 시니어 Android 개발자입니다. Pull Request를 검토해 보았습니다. 전반적으로 새로운 온보딩 플로우와 관련 컴포넌트들을 추가하는 작업으로 보이며, Compose와 Hilt 사용에 대한 이해도가 높게 반영된 것 같습니다. 몇 가지 개선 사항과 잠재적인 문제를 발견하여 코멘트를 남깁니다.


AppScreen.kt

  1. entryDecorators 누락 (Critical)

    • 문제점: rememberSaveableStateHolderNavEntryDecorator()rememberViewModelStoreNavEntryDecorator() 가 제거되었습니다. 이 데코레이터들은 Jetpack Compose Navigation에서 ViewModel의 생명주기를 관리하고 Composable의 상태를 rememberSaveable 등을 통해 저장하는 데 필수적입니다. 이들이 없으면, 내비게이션 스택에서 화면이 popBackStack 되었다가 다시 push될 때 ViewModel이 재생성되거나, Composable의 UI 상태가 유지되지 않고 초기화될 수 있습니다. 이는 사용자 경험에 심각한 영향을 미칠 수 있습니다.
    • 해결책: 해당 데코레이터들을 다시 추가해야 합니다.
    • 코드 예시:
      NavDisplay(
          backStack = backStack,
          modifier = Modifier.fillMaxSize().padding(padding),
          entryDecorators = listOf( // 다시 추가
              rememberSaveableStateHolderNavEntryDecorator(),
              rememberViewModelStoreNavEntryDecorator(),
          ),
          entryProvider = entryProvider {
              // ...
          }
      )
  2. WindowInsets.ime 제거로 인한 키보드 간섭 가능성 (Major)

    • 문제점: contentWindowInsets 에서 WindowInsets.ime (키보드 인셋) 제외 로직이 사라졌습니다. 이는 키보드가 화면에 나타날 때 UI가 키보드에 의해 가려지거나 밀려 올라가지 않도록 하는 중요한 설정입니다. 온보딩 화면에는 입력 필드가 많으므로, 이 변경으로 인해 키보드와 UI 요소가 겹칠 가능성이 있습니다.
    • 질문: 이 변경이 의도된 것인지, 아니면 실수로 제거된 것인지 확인이 필요합니다. 만약 의도된 것이 아니라면, 다시 추가하는 것을 고려해야 합니다.
    • 코드 예시:
      contentWindowInsets = WindowInsets.safeDrawing
          .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
          .exclude(WindowInsets.ime), // 다시 추가 고려
  3. onboardingEntry 내비게이션 콜백 관리 (Major)

    • 문제점: onboardingEntry 에 온보딩의 각 단계별로 onOnboarding1Next, onBackFromOnboarding2, onOnboarding6CallNow 등 너무 많은 개별 콜백이 전달되고 있습니다. 이는 AppScreen 컴포넌트가 온보딩 플로우의 내부적인 상세 내비게이션 로직에 과도하게 의존하게 만듭니다. 만약 온보딩 단계가 추가되거나 변경되면 AppScreen 코드도 수정해야 하므로, 유지보수성이 저하됩니다.
    • 개선 방안: AppScreen 에서는 온보딩 플로우의 시작과 끝, 또는 특정 중요한 이벤트(예: 온보딩 완료)만 인지하도록 추상화하는 것이 좋습니다.
      • 제안 1 (Sealed Class Action): OnboardingEntry 가 단일 onOnboardingAction: (OnboardingAction) -> Unit 콜백을 받도록 하고, OnboardingAction 을 Sealed Class로 정의하여 내부적으로 각 단계별 내비게이션 액션을 구분합니다.
        // interface OnboardingEntry
        fun onboardingEntry(onOnboardingAction: (OnboardingAction) -> Unit) =
            navEntry(Onboarding1NavKey) {
                val onboardingViewModel: OnboardingViewModel = hiltViewModel()
                OnboardingScreen(
                    viewModel = onboardingViewModel,
                    onAction = onOnboardingAction
                )
            }
        
        // In AppScreen
        onboardingEntry(
            onOnboardingAction = { action ->
                when (action) {
                    OnboardingAction.Next1 -> appNavigator.navigate(Onboarding2NavKey)
                    OnboardingAction.Back2 -> appNavigator.popBackStack()
                    // ...
                    OnboardingAction.CompleteAndCallNow -> appNavigator.navigate(HomeNavKey)
                }
            }
        )
        
        // OnboardingAction.kt
        sealed class OnboardingAction {
            data object Next1 : OnboardingAction()
            data object Back2 : OnboardingAction()
            // ...
            data object CompleteAndCallNow : OnboardingAction()
            data object CompleteAndCallLater : OnboardingAction()
            data class ProfileClick(val profileId: String) : OnboardingAction() // 예시
        }
      • 제안 2 (High-Level Callback): 온보딩 완료 시 HomeNavKey로 이동하는 단일 onOnboardingFinished: () -> Unit 콜백만 전달하고, 온보딩 내부의 단계별 내비게이션은 온보딩 feature 모듈 내부에서 처리합니다. OnboardingNavKey 들을 OnboardingEntry 내에서 사용하여 appNavigator 에 직접 노출시키지 않는 방식입니다. 이는 온보딩이 Sub-Graph 형태로 구성될 때 더 자연스러울 수 있습니다.

feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt

  1. NavKey 분리 전략 (Suggestion)
    • 배경: 현재 온보딩 각 단계를 Onboarding1NavKey, Onboarding2NavKey 등으로 개별 NavKey 객체로 분리했습니다. 이는 명확성을 높일 수 있습니다.
    • 질문/제안: 온보딩 플로우가 더 복잡해지거나 단계가 자주 변경될 가능성이 있다면, 하나의 data class OnboardingNavKey(val step: OnboardingStep) 형태로 step 파라미터를 통해 단계를 구분하는 것이 더 유연하고 확장 가능성이 높을 수 있습니다.
      • OnboardingStepenum classsealed class 로 정의하여 각 단계에 대한 정보를 포함할 수 있습니다.
    • 현재 방식도 유효하지만, 미래 확장성을 고려하여 제안합니다.

feature/onboarding/impl/src/main/java/kr/co/call/impl/component/*.kt (신규 컴포넌트 공통)

대부분의 신규 컴포넌트(AgeInputField, BackStepBar, BirthdayChoice, BottomWheel, KeywordChoice, MemberChoice, MessageInputField, NameBox, PreferTime, ProfileChoice)에서 공통적으로 다음과 같은 개선 사항이 있습니다.

  1. 하드코딩된 문자열 (Major)

    • 문제점: 사용자에게 보여지는 텍스트 ("만 나이", "(세)", "생년월일 선택", "년", "월", "일", "전송", "프로필 사진", PreferTime enum 내 title, time, description 등)가 코드 내에 하드코딩되어 있습니다. 이는 다국어 지원, 텍스트 변경 시 유연성, 그리고 전반적인 유지보수성을 저해합니다.
    • 해결책: 모든 사용자에게 보여지는 텍스트는 strings.xml 또는 관련 리소스 파일에 정의하여 stringResource() 함수를 통해 사용해야 합니다.
    • 예시:
      // AgeInputField.kt
      Text(
          text = stringResource(R.string.onboarding_age_label), // "만 나이"
          // ...
      )
      Text(
          text = stringResource(R.string.onboarding_age_unit), // "(세)"
          // ...
      )
    • PreferTime enum의 경우, title, time, description 필드를 Int (string resource ID) 타입으로 변경하고, Composable에서 stringResource(preferTime.titleResId) 와 같이 사용하면 됩니다.
  2. 매직 넘버 (dp 값) (Major)

    • 문제점: 레이아웃, 패딩, 간격, 크기 (7.dp, 51.dp, 10.dp, 19.dp, 8.dp, 15.dp 등)가 코드 내에 매직 넘버로 하드코딩되어 있습니다. 이는 디자인 시스템의 일관성을 해치고, 디자인 변경 시 여러 파일을 일일이 수정해야 하는 어려움을 야기합니다.
    • 해결책: 디자인 시스템 모듈(core/designsystem)에 Dimensions object를 정의하거나, CallTheme.spacing과 같이 MaterialTheme 에 확장하여 사용해야 합니다.
    • 예시:
      // core/designsystem/src/main/java/kr/co/call/designsystem/theme/Dimens.kt (새 파일)
      object CallDimens {
          val spacingSmall = 4.dp
          val spacingMedium = 8.dp
          val spacingLarge = 16.dp
          val inputFieldHeight = 51.dp
          val cornerRadiusMedium = 10.dp
          // ...
      }
      
      // 컴포저블에서 사용
      Spacer(modifier = Modifier.height(CallDimens.spacingMedium))
      .background(
          color = Gray100,
          shape = RoundedCornerShape(CallDimens.cornerRadiusMedium),
      )
  3. State Hoisting 및 Recomposition (Good Practice)

    • 대부분의 컴포넌트에서 value, onValueChange, selected, onClickState Hoisting 원칙을 잘 준수하고 있습니다. rememberSaveable 이나 remember 를 사용하여 UI 내부 상태를 관리하는 방식도 적절합니다.
    • LaunchedEffectFocusRequester 를 사용한 키보드 자동 포커스 처리(MessageInputField.kt)도 적절한 Side Effect 처리입니다.

ProfileChoice.kt

  1. 프로필 이미지 로딩 (Critical)
    • 문제점: profileImageUrl: String? 파라미터를 받고 있음에도 불구하고, Image(painter = painterResource(R.drawable.ic_profile)) 를 사용하여 정적 드로어블 리소스만 표시하고 있습니다. 만약 profileImageUrl이 실제 프로필 이미지의 URL이라면, 이미지 로딩 라이브러리(예: Coil)의 AsyncImage 컴포저블을 사용하여 이미지를 비동기적으로 로드하고 표시해야 합니다. 현재 코드로는 동적 이미지 URL을 처리할 수 없습니다.
    • 해결책: Coil의 AsyncImage 를 사용하도록 변경합니다.
    • 코드 예시:
      @Composable
      fun ProfileChoice(
          profileImageUrl: String?, // URL
          onClick: () -> Unit,
          modifier: Modifier = Modifier,
      ) {
          val interactionSource = remember { MutableInteractionSource() }
          val isPressed by interactionSource.collectIsPressedAsState()
      
          Box(
              modifier = modifier
                  .size(80.dp)
                  .clip(CircleShape)
                  .clickable(
                      interactionSource = interactionSource,
                      indication = null, // Custom visual feedback if needed
                      onClick = onClick,
                  ),
              contentAlignment = Alignment.Center,
          ) {
              if (!profileImageUrl.isNullOrBlank()) {
                  AsyncImage( // Coil AsyncImage 사용
                      model = profileImageUrl,
                      contentDescription = stringResource(R.string.profile_image_description), // 문자열 리소스
                      contentScale = ContentScale.Crop,
                      modifier = Modifier.fillMaxSize(),
                  )
              } else {
                  // 기본 이미지 표시 또는 빈 상태 처리
                  Image(
                      imageVector = ImageVector.vectorResource(R.drawable.ic_profile),
                      contentDescription = stringResource(R.string.profile_image_description),
                      modifier = Modifier.fillMaxSize(),
                      contentScale = ContentScale.Crop,
                      colorFilter = ColorFilter.tint(Gray400) // 예시
                  )
              }
              // ... (오버레이 아이콘 등)
          }
      }

KeywordChoice.kt

  1. emojitext 파라미터 (Suggestion)
    • 배경: Trait enum 클래스에 emojilabel 이 이미 정의되어 있습니다. KeywordChoice 컴포저블에서 emoji: String, text: String 을 개별적으로 받는 대신, trait: Trait 객체를 직접 파라미터로 받으면 Trait 내부의 정보를 활용하여 일관성을 높일 수 있습니다.
    • 코드 예시:
      @Composable
      fun KeywordChoice(
          trait: Trait, // Trait 객체 직접 받기
          selectedOrder: Int?,
          onClick: () -> Unit,
          modifier: Modifier = Modifier,
      ) {
          // ...
          Row {
              Text(text = trait.emoji, /* ... */)
              Text(text = trait.label, /* ... */)
          }
          // ...
      }

MemberChoice.kt

  1. 드롭다운 메뉴 위치 및 크기 (Major)
    • 문제점: DropdownMenuoffset=DpOffset(x=menuWidth-114.dp, y=4.dp)width(114.dp) 가 하드코딩된 값으로 설정되어 있습니다.
      • menuWidth-114.dp 는 드롭다운 메뉴가 항상 maxWidth 의 오른쪽 끝에 위치하도록 하려는 의도로 보이지만, 114.dp 라는 고정된 너비는 다양한 화면 크기나 기기 해상도에서 레이아웃이 깨지거나 어색하게 보일 수 있습니다.
      • 4.dpy 오프셋도 마찬가지입니다.
    • 개선 방안:
      • 드롭다운 메뉴의 너비는 fillMaxWidth() 를 사용하여 부모(BoxWithConstraints)의 너비를 따르거나, IntrinsicSize.Min 또는 IntrinsicSize.Max 를 사용하여 내용물에 맞춰 동적으로 조정되도록 하는 것이 일반적입니다.
      • offsetDropdownMenu 를 감싸는 anchor Composable의 layoutCoordinates 를 사용하여 동적으로 계산하는 것이 더 안정적입니다.
      • 디자인 시스템에서 드롭다운 메뉴의 위치 및 크기에 대한 명확한 가이드라인이 있다면 그에 따라 구현하는 것이 좋습니다.

OnboardingViewModel (추정)

이 PR diff에는 OnboardingViewModel의 코드가 포함되어 있지 않지만, AppScreen.kt 에서 hiltViewModel() 로 주입하여 사용하고 있으므로, 리뷰 규칙에 따라 몇 가지 사항을 미리 짚어드립니다.

  • 생성자 주입 (Hilt DI): OnboardingViewModel은 Hilt를 통해 주입될 것이므로, 의존성은 생성자 주입 원칙을 준수해야 합니다.
    @HiltViewModel
    class OnboardingViewModel @Inject constructor(
        private val someRepository: SomeRepository,
        // ...
    ) : ViewModel() { /* ... */ }
  • UI 상태 및 비즈니스 로직 분리: OnboardingViewModel은 온보딩 화면의 UI 상태(age, selectedDate, selectedTraits 등)를 관리하고, 사용자 인터랙션(intent)에 따른 비즈니스 로직(예: 온보딩 데이터 유효성 검사, 서버 전송)을 수행해야 합니다.
  • Blocking 작업 방지: intent 블록 내에서 Dispatchers.IODispatchers.Default를 명시적으로 사용하지 않고 네트워크/DB 등의 blocking 작업을 직접 수행하지 않도록 주의해야 합니다. viewModelScope 내에서 withContext(Dispatchers.IO) 등을 사용하여 백그라운드 스레드에서 작업을 수행해야 합니다.
  • LoadStatus 사용: 로딩/에러 상태는 Boolean 플래그 대신 LoadStatus (또는 이와 유사한 Sealed Class)로 관리하는 것이 좋습니다.

전반적으로 많은 온보딩 컴포넌트들을 추가하느라 고생하셨습니다. 위에 언급된 Critical 및 Major 이슈들을 우선적으로 해결해 주시면 좋을 것 같습니다. 특히 AppScreen.ktentryDecorators 누락과 ProfileChoice.kt의 이미지 로딩 방식은 기능에 직접적인 영향을 미치므로 꼭 확인해 주세요.

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요! 시니어 Android 개발자로서 Pull Request를 리뷰해 드리겠습니다.

이번 PR은 온보딩 플로우를 새로 추가하고, 이에 필요한 UI 컴포넌트들을 구현한 내용으로 보입니다. 전반적으로 Jetpack Compose의 모범 사례들을 잘 따르고 있어서 인상 깊었습니다. 특히 State Hoisting 원칙을 거의 모든 컴포넌트에서 준수하고 있는 점, 그리고 rememberSaveable이나 LaunchedEffect 등 Compose의 Side Effect를 적절하게 사용하고 있는 점은 매우 긍정적입니다.

몇 가지 개선점과 추가로 고려하면 좋을 사항들을 정리해 보았습니다.


1. AppScreen.kt (Application Main Entry Point)

  • 시작 내비게이션 키 변경 (HomeNavKey -> Onboarding1NavKey):
    • Good: 온보딩 플로우 구현에 맞춰 앱의 시작점을 변경한 것은 적절합니다. 향후 로그인 여부에 따라 분기하는 로직을 추가해야 할 것입니다.
  • 온보딩 진입점 콜백 폭발 (onboardingEntry):
    • 개선 필요: onboardingEntryonProfileClick, onOnboarding1Next, onBackFromOnboarding2 등 너무 많은 콜백 함수를 인자로 받고 있습니다. 이는 AppScreen.kt가 온보딩 플로우의 세부적인 내비게이션 로직을 모두 알고 있어야 함을 의미하며, AppScreen.kt의 책임이 과도하게 커지는 문제입니다.
    • 제안: 온보딩 플로우 내에서의 내비게이션은 온보딩 모듈 내부에서 처리하도록 하는 것이 좋습니다. onboardingEntryonOnboardingCompleted: () -> Unit (예: HomeNavKey로 이동)과 같은 최소한의 콜백만 받도록 하고, 온보딩 단계별 이동은 온보딩 ViewModel이나 내부 Composable에서 자체적으로 처리하게 하는 것이 모듈 간 결합도를 낮추고 각 모듈의 독립성을 높이는 방법입니다.
      // feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntry.kt (가상)
      fun NavGraphBuilder.onboardingEntry(
          onboardingViewModel: OnboardingViewModel,
          onOnboardingCompleted: () -> Unit,
          onPopBackStack: () -> Unit // 외부로 popBackStack 요청 시
      ) {
          composable(Onboarding1NavKey) {
              Onboarding1Screen(
                  viewModel = onboardingViewModel,
                  onNext = { navHostController.navigate(Onboarding2NavKey) },
                  onProfileClick = { /* ViewModel에서 처리하거나, 필요한 경우 외부 콜백 사용 */ }
              )
          }
          // ... 다른 온보딩 스크린들
      }
      
      // AppScreen.kt
      onboardingEntry(
          onboardingViewModel = onboardingViewModel,
          onOnboardingCompleted = { appNavigator.navigate(HomeNavKey) },
          onPopBackStack = { appNavigator.popBackStack() }
      )
  • WindowInsets 사용:
    • Good: WindowInsets.safeDrawing, only, exclude(WindowInsets.ime) 등을 사용하여 시스템 바 및 키보드에 따른 UI 레이아웃을 적절히 처리하고 있습니다. Compose에서 인셋을 다루는 좋은 예시입니다.

2. Design System (Color.kt, Type.kt)

  • 새로운 컬러 및 타이포그래피 추가:
    • Good: SubFire, SubSoup, DragGradient, captionSmall, miniMedium 등 새로운 디자인 토큰을 추가한 것은 디자인 시스템을 확장하는 과정으로 보입니다. 기존 규칙을 잘 따르고 있습니다.
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • 색상 정의는 Design System의 핵심이므로 하드코딩된 RGB 값은 여기서 허용됩니다.

3. Onboarding API (OnboardingRoute.kt)

  • data object를 통한 내비게이션 키 정의:
    • Good: Onboarding1NavKey부터 Onboarding6NavKey까지 data object로 정의한 것은 매우 좋습니다. 인자가 없는 내비게이션 라우트를 정의하는 가장 깔끔하고 타입 세이프한 방법입니다.

4. Onboarding Implementation (impl/component 하위 컴포넌트들)

새로 추가된 모든 UI 컴포넌트(AgeInputField, BackStepBar, BirthdayChoice, BottomWheel, KeywordChoice, MemberChoice, MessageInputField, NameBox, PreferTime, ProfileChoice)에서 공통적으로 다음과 같은 좋은 패턴을 발견했습니다.

  • Jetpack Compose 규칙 2.1 (State Hoisting 원칙 준수):
    • 모든 컴포넌트가 value, onValueChange 또는 selected, onClick과 같이 상태와 이벤트를 분리하여 외부로 호이스팅하고 있습니다. 이는 Composable 함수의 재사용성을 높이고 테스트를 용이하게 하며, 불필요한 리컴포지션을 방지하는 데 기여합니다.
  • Jetpack Compose 규칙 2.2 (UI 상태를 data class로 표현):
    • TraitPreferTime과 같이 복잡한 UI 요소를 표현하기 위해 Enum Class를 사용한 것은 아주 좋은 접근 방식입니다. 각 요소의 데이터(emoji, label, time, description)를 명확하게 정의하여 가독성과 유지보수성을 높였습니다.

이제 각 컴포넌트별로 좀 더 자세히 살펴보겠습니다.

4.1. AgeInputField.kt

  • Input Validation: if (input.all(Char::isDigit))를 사용하여 숫자만 입력받도록 한 것은 좋지만, 최대 나이 등의 추가적인 범위 제한이 필요할 수도 있습니다 (예: 1~120세).
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • "만 나이", "*, "(세)": 이 문자열들은 strings.xml로 이동하여 지역화를 지원해야 합니다.
    • 7.dp, 51.dp, 10.dp, 19.dp, 16.dp, 15.dp: UI 요소 간 간격이나 크기 관련 매직 넘버가 다수 있습니다. Design System에 CallTheme.spacing.medium와 같이 정의하여 일관성을 유지하는 것을 권장합니다.

4.2. BackStepBar.kt

  • 아이콘 tint:
    • tint = Black으로 직접 지정했습니다. ic_back이 단색 아이콘이라면 괜찮지만, 일반적으로 Design System에서 아이콘의 기본 색상을 관리하는 토큰(예: CallTheme.colors.iconDefault)을 사용하는 것이 더 좋습니다. 혹은 아이콘이 그 자체로 색상을 가지고 있다면 Color.Unspecified를 사용합니다.
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • "뒤로가기": strings.xml로 이동 필요.
    • 58.dp, 8.dp, 15.dp: Design System의 spacing 또는 size 토큰 사용 고려.

4.3. BirthdayChoice.kt

  • toDisplayText() 확장 함수:
    • Good: LocalDate에 대한 확장 함수로 디스플레이 형식을 분리한 것은 깔끔합니다. 다만 "$year / $monthValue / $dayOfMonth"는 날짜 형식에 대한 하드코딩된 문자열이므로, DateTimeFormatter를 사용하여 지역화 및 유연한 형식 지정을 지원하도록 개선하는 것이 좋습니다.
    • 예시: selectedDate.format(DateTimeFormatter.ofPattern("yyyy / MM / dd", Locale.getDefault()))
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • "생년월일", "*, "생년월일 선택": strings.xml로 이동 필요.
    • 7.dp, 55.dp, 10.dp, 16.dp: Design System의 spacing 또는 size 토큰 사용 고려.

4.4. BottomWheel.kt

  • remember vs rememberSaveable for draftDate:
    • Good: var draftDate by remember(selectedDate)를 사용한 것은 적절합니다. selectedDate가 변경되면 draftDate도 초기화되어야 하고, BottomWheel 컴포넌트 자체는 바텀 시트 내부에 있어 보통 컴포넌트가 사라지면 상태도 사라지므로 rememberSaveable까지 필요하지 않을 수 있습니다.
  • 날짜 로직 (coerceAtMost, YearMonth.of):
    • Good: 윤년이나 각 월의 마지막 날짜를 정확하게 계산하여 dayOfMonth를 조정하는 로직은 견고한 구현입니다.
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • "생년월일 선택", "년", "월", "일": strings.xml로 이동 필요.
    • 195.dp, (-22).dp, 22.dp, 35.dp: Design System의 spacing 또는 size 토큰 사용 고려.
  • ConfirmBottomSheetWheelPicker 컴포넌트:
    • 이들은 diff에 포함되지 않았지만, Design System 패키지에 있는 것으로 보입니다. 이들도 앞선 규칙들을 준수하고 있는지 확인하는 것이 좋습니다.

4.5. KeywordChoice.kt

  • Trait Enum:
    • Good: Trait Enum을 사용하여 키워드 데이터를 명확하게 정의한 것은 매우 좋습니다.
    • companion objectfromKeyword도 유용합니다.
  • 순서 표시 (selectedOrder):
    • Good: 선택된 순서를 나타내는 작은 원형 뷰를 구현한 것이 인상 깊습니다. 디자인 요구사항을 잘 반영했습니다.
    • CallTheme.typography.miniMedium과 같은 세부적인 타이포그래피 스타일을 정의하고 사용한 점도 좋습니다.
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • Trait enum 내의 emojilabel은 데이터의 일부로 볼 수 있어 여기서는 허용됩니다. 그러나 만약 이 label이 화면에 표시되는 사용자 언어에 따라 변경되어야 한다면, stringRes ID를 인자로 받는 방식으로 변경하는 것을 고려할 수 있습니다.
    • 33.dp, 30.dp, 1.dp, 15.dp, 5.dp, 7.dp, (-5).dp, 20.dp, 1.5.dp: Design System의 spacing 또는 size 토큰 사용 고려.

4.6. MemberChoice.kt

  • BoxWithConstraints를 사용한 드롭다운 위치 조정:
    • Good: DropdownMenu의 위치를 menuWidth를 이용하여 동적으로 조정한 것은 좋은 접근입니다.
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • label, placeholder에 들어가는 문자열 ("직업", "직업을 선택해주세요") 및 options 내의 문자열("대학생", "직장인", "무직")은 strings.xml로 이동하여 지역화를 지원해야 합니다.
    • 3.dp, 7.dp, 51.dp, 10.dp, 17.dp, 12.dp, 6.dp, 114.dp, 420.dp, 4.dp, 15.dp, 9.dp: Design System의 spacing 또는 size 토큰 사용 고려.

4.7. MessageInputField.kt

  • Focus 및 Keyboard 제어:
    • FocusRequester, LocalFocusManager, LocalSoftwareKeyboardController, LaunchedEffect(autoFocus)를 사용하여 포커스와 키보드를 제어하는 방식은 모범적인 구현입니다.
  • maxLength 처리:
    • 개선 필요: val maxLength = 10는 매직 넘버입니다. 이 값은 ViewModel에서 관리하거나, const val로 정의하여 상수화하는 것이 좋습니다.
  • 전송 버튼 활성화/비활성화 로직 (canSend):
    • Good: value.isNotBlank() && value.length <= maxLength로 명확하게 조건을 명시하여 버튼의 활성화 상태를 관리하는 것은 좋습니다.
  • BasicTextFielddecorationBox:
    • Good: decorationBox를 사용하여 플레이스홀더 텍스트와 현재 글자 수/최대 글자 수를 커스텀하여 보여주는 방식은 유연하고 효과적입니다.
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • "메시지를 입력해주세요.", "전송": strings.xml로 이동 필요.
    • ${value.length}/$maxLength: 이 형식 문자열도 strings.xml에 정의하는 것이 좋습니다. (예: <string name="character_count_format">%1$d/%2$d</string>)
    • 56.dp, 50.dp, 23.dp, 4.dp, 5.dp, 48.dp: Design System의 spacing 또는 size 토큰 사용 고려.

4.8. NameBox.kt

  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • label("이름"), " *": strings.xml로 이동 필요.
    • 125.dp, 6.dp, 51.dp, 10.dp, 16.dp: Design System의 spacing 또는 size 토큰 사용 고려.

4.9. PreferTime.kt

  • PreferTime Enum:
    • Good: emoji, title, time, description을 포함하는 Enum Class로 시간대 옵션을 구조화한 것은 훌륭합니다.
  • 라디오 버튼 UI (selectable, Role.RadioButton):
    • Good: selectable 수식어와 Role.RadioButton을 사용하여 접근성(accessibility)을 높인 것은 매우 중요합니다.
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • PreferTime enum 내의 emoji, title, description은 데이터의 일부로 볼 수 있습니다. KeywordChoice와 마찬가지로 지역화가 필요하다면 stringRes ID를 인자로 받는 방식을 고려할 수 있습니다.
    • 93.dp, 20.dp, 1.dp, 17.dp, 16.dp, 3.dp, 20.dp, 8.dp, 13.dp, 4.dp: Design System의 spacing 또는 size 토큰 사용 고려.

4.10. ProfileChoice.kt

  • AsyncImage (Coil) 사용:
    • Good: 이미지 로딩 라이브러리 CoilAsyncImage를 사용한 것은 적절합니다.
  • Placeholder/Error 이미지:
    • 개선 필요: 현재 AsyncImageimageUrl이 null이거나 로딩에 실패했을 때를 위한 Placeholder 또는 Error 이미지가 없습니다. 사용자 경험을 위해 기본 프로필 이미지나 에러 이미지를 지정해 주는 것이 좋습니다.
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(imageUrl)
            .crossfade(true)
            .placeholder(R.drawable.ic_default_profile) // 기본 프로필 이미지
            .error(R.drawable.ic_error_profile)        // 에러 시 이미지
            .build(),
        contentDescription = "프로필 사진 변경",
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .size(80.dp)
            .clip(CircleShape)
            .clickable(
                interactionSource = interactionSource,
                indication = null,
                onClick = onClick,
            ),
    )
  • MutableInteractionSource 사용:
    • 현재 interactionSource는 정의되어 있지만, collectIsPressedAsState()를 통해 얻은 isPressed 상태가 UI에 반영되지 않고 있습니다. 만약 누름 상태에 따른 시각적 피드백이 필요하다면 이를 활용하여 구현해야 합니다. 아니라면, 단순히 Modifier.clickable(onClick = onClick)만 사용하는 것이 더 간결합니다.
  • 리뷰 규칙 1.1 (하드코딩된 문자열/매직 넘버):
    • "프로필 사진 변경": strings.xml로 이동 필요.
    • 80.dp, 5.dp, 1.dp: Design System의 spacing 또는 size 토큰 사용 고려.

5. ViewModel (OnboardingViewModel)

PR diff에는 OnboardingViewModel의 구현 코드가 포함되어 있지 않아, ViewModel 관련 규칙(Orbit ContainerHost, intent 내부 상태 변경, postSideEffect, blocking 작업)은 현재 리뷰할 수 없습니다. 하지만 AppScreen.kt에서 hiltViewModel()로 주입받아 사용하는 방식 자체는 올바릅니다.


종합 의견

전반적으로 Jetpack Compose의 모범 사례들을 잘 적용하여 컴포넌트들을 깔끔하게 구현하고 있습니다. 특히 상태 호이스팅과 UI 상태를 데이터 클래스로 표현하는 방식은 매우 훌륭합니다.

가장 중요한 개선 사항은 strings.xml을 이용한 문자열 리소스 분리입니다. 현재 대부분의 UI 텍스트가 하드코딩되어 있어 지역화 및 유지보수에 어려움이 예상됩니다. 또한, UI 간격이나 크기와 관련된 매직 넘버들을 Design System의 토큰으로 관리하는 것을 고려하면 더욱 견고하고 일관성 있는 UI를 구축할 수 있을 것입니다.

이 리뷰가 도움이 되기를 바랍니다. 좋은 코드 감사합니다!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
app/src/main/java/kr/co/call/callfromai/AppScreen.kt (1)

128-130: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

프로필 사진 선택 콜백을 무동작으로 두지 마세요.

onProfileClick이 빈 람다라서 온보딩의 + 프로필 선택 UI를 눌러도 아무 동작이 없습니다. 사진 선택이 이번 요구사항이면 picker와 상태 저장을 연결하고, 범위 밖이면 버튼을 비활성화해 오작동처럼 보이지 않게 해 주세요.

PR 설명의 프로필 사진 선택 UI와 현재 콜백 구현을 기준으로 판단했습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt` around lines 128 - 130,
Replace the empty onProfileClick callback in AppScreen with a working
profile-image picker flow that updates and persists the selected photo state; if
photo selection is out of scope, disable the corresponding profile-selection
button instead of leaving the callback inert.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt`:
- Around line 128-130: Replace the empty onProfileClick callback in AppScreen
with a working profile-image picker flow that updates and persists the selected
photo state; if photo selection is out of scope, disable the corresponding
profile-selection button instead of leaving the callback inert.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fcfc307d-72a2-44e3-9dd4-233be62e34b8

📥 Commits

Reviewing files that changed from the base of the PR and between 40252a7 and 82f64a2.

📒 Files selected for processing (4)
  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt

@JiwonLee42 JiwonLee42 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

수고했서용~

val aiAge: String = "",
val aiJob: String = "",
val aiMbti: String = "",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

state에서는 기본값 null 말고 디폴트로 맞춰주면 좋을 것 같아요~!

fun Onboarding2Screen(
onBackClick: () -> Unit,
onProfileClick: () -> Unit,
onNextClick: (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

변수들 묶음으로 전달하는 것보다 Onboarding2State 이런식으로 상태로 보내는것도 좋아보입니당

selectedOption = mbti,
placeholder = "MBTI를 선택해주세요",
options = listOf(
"INFP", "INFJ", "ENFP", "ENFJ",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

별도 enum으로 분리하는 게 좋을 것 같아요 서버에 보내야 하는 값이라서!

import kr.co.call.impl.component.TempControl
import kr.co.call.impl.component.TopTitle
import kr.co.call.onboarding.impl.R

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

요것도 스크린보다는 분리하는게 좋을 것 같긴 합니다..!
저는 상태나 도메인에 같이 분리해두는 편입니당

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요, 팀원 여러분!

멀티스텝 온보딩 플로우를 통합하고 새로운 UI 컴포넌트들을 추가해주셔서 감사합니다. 온보딩 과정을 명확한 단계와 전용 내비게이션 키로 나눈 접근 방식은 훌륭한 아키텍처 결정이며, 새롭게 추가된 컴포넌트들도 깔끔하게 구현되었습니다.

PR을 검토하면서 몇 가지 의견과 제안 사항을 드립니다. 주로 유지보수성, 현지화, 그리고 모범 사례 준수에 중점을 두었습니다.


AppScreen.kt

  • 긍정적 피드백:
    • LoginNavKey, HomeNavKey, AgreementNavKey로 이동할 때 replaceAll을 사용한 것은 좋습니다. 주요 진입점으로 전환 시 백스택이 깨끗하게 정리되어 예상치 못한 뒤로 가기 동작을 방지할 수 있습니다.
    • OnboardingNavKeyOnboarding1NavKey부터 Onboarding6NavKey까지 세분화한 것은 명확하며, 다단계 온보딩 흐름을 잘 준비했습니다.
    • onboardingViewModel과 특정 onXxx 람다들을 onboardingEntry에 전달하는 것은 상태 호이스팅(State Hoisting)과 관심사 분리(Separation of Concerns)를 잘 지키는 접근 방식입니다.

core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt & Type.kt

  • 긍정적 피드백: 새로운 색상과 타이포그래피 스타일이 디자인 시스템의 기존 패턴을 따르며 잘 정의되어 있습니다.

feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt

  • 긍정적 피드백: data object를 내비게이션 키에 사용하는 방식은 간결하고 정확합니다.

feature/onboarding/impl/build.gradle.kts

  • 긍정적 피드백: Coil Compose 라이브러리를 추가한 것은 Compose에서 이미지 로딩을 위한 표준적이고 권장되는 방법입니다.

새로운 UI 컴포넌트 (AgeInputField.kt, BackStepBar.kt, BirthdayChoice.kt, BottomWheel.kt, KeywordChoice.kt, MemberChoice.kt, MessageInputField.kt, NameBox.kt, PreferTime.kt)

전반적으로 새로운 컴포넌트들은 잘 구성되어 있고, 상태 호이스팅을 효과적으로 사용하며, 프리뷰도 포함되어 있어 개발에 큰 도움이 됩니다. 이 컴포넌트들에 대한 몇 가지 공통적인 제안 사항이 있습니다.

  1. 하드코딩된 문자열 (현지화 및 유지보수를 위한 높은 우선순위)

    • 대부분의 새로운 컴포저블에서 UI 텍스트(예: 라벨, 플레이스홀더, 에러 메시지, 설명)가 코드에 직접 하드코딩되어 있습니다 (예: "만 나이", "(세)", "뒤로가기", "생년월일", "생년월일을 입력해주세요", "직업", "*", "전송", "${value.length}/$maxLength", enum의 label/title/description 필드).
    • 권장사항: 모든 사용자에게 보여지는 문자열은 strings.xml 리소스로 추출하고 stringResource(R.string.my_string)를 사용하여 참조해주십시오. 이는 향후 현지화를 위해 필수적이며, 텍스트 변경 시 코드 수정 없이도 쉽게 업데이트할 수 있게 합니다.
      • 예시: "만 나이"stringResource(R.string.onboarding_age_label)로 변경합니다.
      • TraitPreferTime 같은 enum의 경우, label/title/description 필드에 직접 문자열이나 이모지를 사용하는 대신, 문자열 리소스 ID를 제공하여 현지화가 가능하도록 하는 것을 고려해볼 수 있습니다. 이모지는 순전히 장식적이거나 보편적으로 이해되는 경우 그대로 두어도 좋습니다.
  2. 치수 매직 넘버 (디자인 일관성을 위한 중간 우선순위)

    • 많은 레이아웃 및 크기 값(예: padding(16.dp), height(51.dp), RoundedCornerShape(10.dp))이 하드코딩되어 있습니다. 특정 컴포넌트에 매우 특화된 경우는 허용될 수 있지만, 일반적으로는 중앙화된 디자인 시스템의 치수 값을 사용하는 것이 좋습니다.
    • 권장사항: CallTheme 또는 전용 Dimens 객체(예: CallTheme.dimens.spacingMedium, CallTheme.shapes.cornerMedium)에 일반적인 간격, 크기, 코너 반지름 값을 정의하는 것을 고려하십시오. 이는 앱 전체의 디자인 일관성을 높이고, 전역적인 스타일 변경을 용이하게 합니다.
  3. NameBox.kt - 플레이스홀더 텍스트 처리

    • 플레이스홀더 처리를 위해 value.ifBlank { " " }를 사용한 것은 일반적인 해결책이지만, 전용 Text 컴포저블을 플레이스홀더로 사용하는 것보다 견고하지 않을 수 있습니다.
    • 제안사항: 더 나은 의미론(semantic)과 레이아웃 안정성을 위해 BasicTextFielddecorationBox와 유사한 구조를 고려해보세요. value.isBlank()일 때만 플레이스홀더 Text 컴포저블이 조건부로 표시되도록 합니다. 이렇게 하면 콘텐츠가 의미적으로 정확하고(빈 공간을 실제 콘텐츠로 해석하지 않음) 잠재적인 줄 높이 또는 접근성 도구 관련 문제를 방지할 수 있습니다.
  4. MessageInputField.kt - maxLength 상수화

    • maxLength = 10은 매직 넘버입니다.
    • 권장사항: 이 값을 파일 내부에 const val MAX_MESSAGE_LENGTH = 10과 같이 상수로 정의하거나, 다른 메시지 필드에도 적용될 수 있도록 파라미터로 전달하는 것을 고려하십시오.
  5. 접근성 개선 (낮은 우선순위 - MemberChoice.kt 아이콘)

    • MemberChoice.ktDropdownMenu 토글 아이콘에 현재 contentDescription = null이 사용되어 있습니다.
    • 제안사항: 주변 텍스트가 컨텍스트를 제공한다면 큰 문제는 아니지만, expanded 상태 변화에 따라 "드롭다운 메뉴 확장" 또는 "드롭다운 메뉴 접기"와 같이 더 구체적인 contentDescription을 추가하면 스크린 리더 사용자에게 더 풍부한 경험을 제공할 수 있습니다.
  6. BottomWheel.kt - 연도 범위

    • 1900..LocalDate.now().year에서 1900은 매직 넘버입니다.
    • 제안사항: const val MIN_BIRTH_YEAR = 1900과 같이 상수로 정의하여 명확성을 높이십시오.

대부분의 개선 사항은 코드의 가독성과 유지보수성에 대한 제안이며, 핵심 구현은 견고하게 보입니다. 현지화 및 유지보수를 위해 하드코딩된 문자열 부분을 우선적으로 처리해주시면 감사하겠습니다.

질문이 있으시면 언제든지 알려주세요!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt (1)

115-117: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

날짜 표시 포맷을 화면 명세와 맞춰 주세요.

현재 2000 / 1 / 1처럼 슬래시 주변에 공백이 표시됩니다. PR 설명의 화면 명세인 2000/1/1과 맞추려면 구분자를 공백 없이 반환해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt`
around lines 115 - 117, Update the LocalDate.toDisplayText() formatting to
return year, month, and day separated by "/" without surrounding spaces,
matching the specified display format such as 2000/1/1.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt`:
- Around line 101-106: Update the BottomWheel flow in the showBottomWheel block
so the display fallback LocalDate.now() is not treated as the selected value.
Track a nullable draft/explicit user selection separately and have
onConfirmClick call onDateSelected only when that value was explicitly chosen;
otherwise keep the picker open or reject confirmation, while preserving the
existing dismissal behavior after a valid selection.

---

Outside diff comments:
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt`:
- Around line 115-117: Update the LocalDate.toDisplayText() formatting to return
year, month, and day separated by "/" without surrounding spaces, matching the
specified display format such as 2000/1/1.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d0a8a41-05f5-41a1-85fe-cf7333678b8f

📥 Commits

Reviewing files that changed from the base of the PR and between 82f64a2 and 94fb158.

📒 Files selected for processing (7)
  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt
  • core/designsystem/src/main/java/kr/co/call/designsystem/theme/Type.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt
  • core/designsystem/src/main/java/kr/co/call/designsystem/theme/Type.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt

@codebidoof codebidoof left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

고생많았엉 지원이 피드백이랑 뷰모델 주입위치만 수정해 줘~~

with(density) { bottomBarHeightPx.toDp() }
}

val onboardingViewModel: OnboardingViewModel = hiltViewModel()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

온보딩 뷰모델 인스턴스는 여기서 생성해서 주입하지 말고 OnboardingEntryBuilder에서 생성해 주입해줘야 할 것 같아~~

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

PR 리뷰 요청에 따라 시니어 Android 개발자의 관점에서 코드 리뷰를 진행하겠습니다.


🟢 Overall Review Summary

새로운 온보딩 플로우를 위한 컴포넌트 추가 및 내비게이션 구조 변경이 주요 내용입니다. 전반적으로 Jetpack Compose의 모범 사례인 State hoisting, side effect 관리, UI 상태 표현 등을 잘 따르고 있습니다. data object를 활용한 내비게이션 키 관리, enum class를 활용한 UI 선택지 모델링, rememberSaveable을 통한 UI 상태 유지 등은 매우 훌륭합니다.

몇 가지 개선점은 주로 하드코딩된 문자열 및 매직 넘버, 그리고 사소한 코드 스타일 및 잠재적 UX 흐름에 대한 것입니다.


📝 Files Review

1. AppScreen.kt

  • Jetpack Compose Navigation:
    • navigateToLogin, navigateToHome, navigateToAgreement, onOnboarding6CallNow, onOnboarding6CallLater에서 appNavigator.navigate 대신 appNavigator.replaceAll을 사용하도록 변경된 점은 좋습니다. 로그인/온보딩/동의 과정 후에는 이전 스택이 필요 없는 경우가 많으므로 replaceAll이 올바른 선택입니다.
    • navigateToOnboardingnavigateAfterAgreementOnboardingNavKey에서 Onboarding1NavKey로 변경되면서 appNavigator.navigate를 사용하고 있습니다. 이는 온보딩 진입 시 이전 화면(예: 로그인)이 백 스택에 남아있도록 하겠다는 의도로 보입니다. 온보딩을 반드시 완료해야 하는 플로우이고, 도중에 뒤로가기가 이전 앱 상태로 돌아가는 것이 아니라 온보딩 내에서만 작동해야 한다면 replaceAll을 고려해볼 수 있습니다. 현재 로직 상으로는 온보딩 첫 화면에서 뒤로가기 시 loginEntry로 돌아갈 수 있습니다. 이 UX가 의도된 것인지 확인이 필요합니다.
    • onboardingEntry 컴포저블이 여러 개의 onXXXClick 콜백을 받도록 변경되었습니다. 이는 다단계 온보딩 흐름에 맞춰 개별 화면의 이벤트를 상위로 호이스팅하는 좋은 방식입니다.
  • Kotlin: OnboardingNavKey 대신 Onboarding1NavKey부터 Onboarding6NavKey까지 여러 개의 data object가 사용된 것은 다단계 온보딩을 명확하게 분리하는 좋은 디자인입니다.
  • Hardcoded Strings: CallRecordNavKey(callId = callId) 부분의 포맷팅 변경 외에 특별한 문제는 없습니다.
  • contentWindowInsets: 가독성을 위해 개행 및 포맷팅이 개선되었습니다. 좋은 변경입니다.

2. core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt

  • Kotlin: SubFire, SubSoup 색상 및 DragGradient 브러시가 추가되었습니다. 디자인 시스템에 새로운 색상이 추가되는 일반적인 과정이며, 16진수 값으로 직접 명시하는 방식은 디자인 토큰 정의에 적합합니다.

3. core/designsystem/src/main/java/kr/co/call/designsystem/theme/Type.kt

  • Kotlin: captionSmallminiMedium 스타일이 추가되었습니다. 이 역시 디자인 시스템의 폰트 확장이며, 정의 방식은 적절합니다.

4. feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt

  • Kotlin: OnboardingNavKey가 제거되고 Onboarding1NavKey부터 Onboarding6NavKey까지 각각의 data object로 분리된 점은 다단계 온보딩 흐름을 명확히 하고, 각 단계를 독립적인 내비게이션 대상으로 정의하는 훌륭한 방법입니다.

5. feature/onboarding/impl/build.gradle.kts

  • Kotlin: Coil Compose 라이브러리가 추가되었습니다. Jetpack Compose 환경에서 이미지 로딩을 위해 널리 사용되고 권장되는 라이브러리입니다.
  • Code Style: 파일의 마지막에 줄바꿈(\n)이 없습니다. Git Diff에서 문제를 일으키거나 일부 IDE/lint 도구에서 경고를 발생시킬 수 있으므로 파일 끝에 줄바꿈을 추가하는 것을 권장합니다.

6. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/AgeInputField.kt (New File)

  • Jetpack Compose:
    • ageonAgeChange를 파라미터로 받아 상태 호이스팅을 잘 준수하고 있습니다. rememberSaveableage 상태를 관리하는 프리뷰 코드도 적절합니다.
    • onValueChange에서 input.all(Char::isDigit)을 통해 숫자만 입력받도록 검증하는 로직이 좋습니다.
    • BasicTextField를 사용하여 커스텀 입력 필드를 구현한 방식이 좋습니다.
  • Hardcoded Strings/Magic Numbers:
    • "만 나이", "(세)", "*"와 같은 텍스트는 하드코딩되어 있습니다. 사용자에게 노출되는 문자열은 strings.xml 리소스 파일로 분리하여 다국어 지원 및 유지보수성을 높여야 합니다.
    • 7.dp, 51.dp, 10.dp 등 대부분의 dp 값은 하드코딩되어 있습니다. 디자인 시스템에 정의된 Dimen 값을 사용하거나, 재사용되는 경우 상수로 정의하는 것을 고려해볼 수 있습니다. 현재로서는 개별 컴포넌트의 레이아웃에 직접 사용된 것으로 보아 큰 문제는 아닙니다.
  • Null Safety: ageonAgeChange 모두 non-null 타입으로 선언되어 !! 사용을 지양하고 있습니다.

7. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackStepBar.kt (New File)

  • Jetpack Compose: onBackClick 콜백을 통해 상태 호이스팅을 준수하고 있습니다.
  • Hardcoded Strings/Magic Numbers:
    • contentDescription="뒤로가기"는 하드코딩된 문자열입니다. strings.xml 리소스 파일로 분리해야 합니다.
    • 58.dp, 8.dp, 15.dp 등의 dp 값은 하드코딩되어 있습니다.

8. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt (New File)

  • Jetpack Compose:
    • selectedDate, onDateSelected를 통해 상태 호이스팅을 잘 따르고 있습니다. showBottomWheel은 컴포저블 내부 상태로 rememberSaveable로 관리되는 것이 적절합니다.
    • selectedDate?.toDisplayText() ?: "생년월일을 입력해주세요"와 같이 null-safety 처리를 잘하고 있습니다.
    • clickablerole=Role.Button을 명시하여 접근성을 높인 점이 좋습니다.
    • LocalDate.toDisplayText()와 같은 확장 함수를 통해 날짜 포맷팅 로직을 분리한 점이 깔끔합니다.
  • Hardcoded Strings/Magic Numbers:
    • "생년월일", "생년월일을 입력해주세요", "생년월일 선택" 등 사용자에게 노출되는 문자열은 strings.xml 리소스 파일로 분리해야 합니다.
    • 7.dp, 55.dp 등의 dp 값은 하드코딩되어 있습니다.

9. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt (New File)

  • Jetpack Compose:
    • selectedDate, onConfirmClick, onDismissRequest를 파라미터로 받아 상태 호이스팅을 준수하고 있습니다.
    • LaunchedEffectsnapshotFlow { ... }.filter { it }.collect { ... }를 사용하여 사용자의 스크롤 시작 여부를 감지하는 방식은 Flow를 활용한 Side Effect 처리의 좋은 예시입니다.
    • 날짜 선택 시 coerceAtMost를 사용하여 유효하지 않은 날짜(예: 31일에서 2월로 변경 시)를 자동으로 보정하는 로직은 날짜 유효성 관리에 훌륭한 접근 방식입니다.
    • rememberLazyListState를 사용하여 초기 스크롤 위치를 설정하는 것도 적절합니다.
  • Kotlin: LocalDate.now().year를 사용하여 현재 연도를 가져오는 것은 좋지만, 시작 연도 1900은 매직 넘버입니다. 상수로 분리하여 관리하는 것이 좋습니다.
  • Hardcoded Strings/Magic Numbers:
    • ConfirmBottomSheettitle = "생년월일 선택"itemText 람다 내의 "${it}년", "%02d월", "%02d일"과 같은 문자열은 strings.xml 리소스 파일로 분리해야 합니다.
    • 195.dp, (-22).dp, 22.dp, 35.dp 등의 dp 값은 하드코딩되어 있습니다.
  • draftDate remember vs rememberSaveable: 현재 draftDateremember(selectedDate)로 선언되어 selectedDate가 변경될 때마다 초기화됩니다. 이는 picker의 현재 선택 상태를 나타내는데, 만약 사용자가 picker를 열고 값을 변경했지만, onConfirmClick을 누르기 전에 화면 회전이나 프로세스 종료가 발생했을 때 변경 중이던 상태를 복원하고 싶다면 rememberSaveable을 고려해볼 수 있습니다. 현재 로직에서는 hasUserChangedDaterememberSaveable이라 onConfirmClickselectedDate가 null이 아닌 경우 draftDate를 사용하게 되므로 큰 문제는 없을 수 있지만, UX 요구사항에 따라 달라질 수 있습니다.

10. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt (New File)

  • UI State as Data Class: Trait enum class를 사용하여 키워드, 이모지, 레이블을 함께 관리하는 방식은 UI 상태를 명확하게 표현하는 좋은 패턴입니다. fromKeyword 정적 팩토리 함수도 유용합니다.
  • Jetpack Compose:
    • selectedOrder, onClick을 파라미터로 받아 상태 호이스팅을 준수하고 있습니다.
    • toggleablerole=Role.Checkbox를 명시하여 접근성을 높인 점이 좋습니다.
    • selectedOrder != null에 따라 배경색과 테두리를 조건부로 변경하는 방식이 좋습니다.
    • 선택 순서를 나타내는 작은 원형 뱃지를 Boxoffset을 이용해 구현한 방식도 깔끔합니다.
  • Hardcoded Strings/Magic Numbers:
    • Trait enum 내의 모든 label 값은 하드코딩되어 있습니다. 이들은 사용자에게 노출되는 문자열이므로 strings.xml 리소스 파일로 분리해야 합니다.
    • emoji 값도 이모지 문자열로 직접 하드코딩되어 있습니다. 이모지는 일반적으로 텍스트로 처리되지만, 만약 디자인 시스템의 아이콘처럼 관리하거나 변경이 잦다면 리소스화 하는 것을 고려할 수 있습니다.
    • 33.dp, 30.dp, 1.dp, (-5).dp 등 많은 dp 값이 하드코딩되어 있습니다.

11. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt (New File)

  • Jetpack Compose:
    • label, selectedOption, placeholder, options, onOptionSelected를 파라미터로 받아 상태 호이스팅을 잘 따르고 있습니다. expanded는 컴포저블 내부 상태로 rememberSaveable로 관리하는 것이 적절합니다.
    • BoxWithConstraints를 사용하여 DropdownMenuoffset을 동적으로 계산하여 정확한 위치에 표시되도록 한 점이 좋습니다.
    • selectedOption.ifBlank { placeholder }를 통해 선택된 값이 없을 때 플레이스홀더를 표시하는 방식이 좋습니다.
    • DropdownMenu 내의 DropdownMenuItem에 선택된 항목에 따라 배경색을 변경하는 방식이 좋습니다.
  • Hardcoded Strings/Magic Numbers:
    • labelplaceholder는 파라미터로 받아 좋지만, requiredtrue일 때 표시되는 "*"와 같은 문자열은 하드코딩되어 있습니다. strings.xml 리소스 파일로 분리해야 합니다.
    • 프리뷰의 options 리스트에 "대학생", "직장인", "무직" 등의 문자열이 하드코딩되어 있습니다. 실제 앱에서 사용되는 경우 strings.xml 리소스 파일로 분리해야 합니다.
    • 7.dp, 51.dp, 10.dp, 114.dp 등 많은 dp 값이 하드코딩되어 있습니다.
  • Accessibility: contentDescription=null로 설정된 Icon은 시각적으로 의미를 전달하지만 스크린 리더 사용자에게는 정보가 부족할 수 있습니다. 드롭다운 아이콘의 경우 확장/축소 상태를 나타낸다면 contentDescription에 "드롭다운 메뉴 확장" 또는 "드롭다운 메뉴 축소"와 같은 동적인 설명을 제공하는 것이 더 좋습니다.

12. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt (New File)

  • Jetpack Compose:
    • value, onValueChange, onSendClick 등 상태 호이스팅을 잘 준수하고 있습니다. message 상태를 remember로 관리하는 프리뷰 코드가 적절합니다.
    • LocalFocusManager, LocalSoftwareKeyboardController, FocusRequester를 사용하여 키보드 및 포커스 관리를 유연하게 한 점이 좋습니다.
    • LaunchedEffect를 사용하여 autoFocus에 따라 키보드를 자동으로 올리고 포커스를 요청하는 Side Effect 처리가 적절합니다.
    • BasicTextField를 통해 플레이스홀더, 글자 수 제한 등의 커스텀 동작을 구현한 점이 좋습니다.
    • canSend 변수를 통해 전송 버튼의 활성화/비활성화 상태를 관리하는 로직이 명확합니다.
  • Kotlin: maxLength = 10은 매직 넘버입니다. 상수로 분리하여 관리하는 것이 좋습니다.
  • Hardcoded Strings/Magic Numbers:
    • contentDescription = "전송"은 하드코딩된 문자열입니다. strings.xml 리소스 파일로 분리해야 합니다.
    • placeholder = "메시지를 입력해주세요."는 파라미터로 제공되므로 좋지만, 프리뷰 코드에서 사용된 문자열은 하드코딩되어 있습니다. strings.xml 리소스 파일로 분리해야 합니다.
    • 56.dp, 50.dp, 23.dp, 4.dp, 48.dp 등 많은 dp 값이 하드코딩되어 있습니다.
  • Null Safety: keyboardController?.show()와 같이 안전 호출을 사용하여 null-safety를 준수하고 있습니다.

13. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt (New File)

  • Jetpack Compose:
    • label, value, onClick을 파라미터로 받아 상태 호이스팅을 준수하고 있습니다.
    • value.ifBlank { " " }를 통해 빈 문자열일 경우 공백을 표시하여 UI 렌더링 문제를 방지한 점이 좋습니다.
    • BasicTextField 임포트가 있지만 실제 코드에서 사용되지 않습니다. 불필요한 임포트이므로 제거해야 합니다.
  • Hardcoded Strings/Magic Numbers:
    • label은 파라미터로 제공되므로 좋지만, "*"와 같은 문자열은 하드코딩되어 있습니다. AgeInputField와 마찬가지로 strings.xml 리소스 파일로 분리하거나 required 파라미터로 관리하는 것을 고려해야 합니다.
    • 125.dp, 6.dp, 51.dp, 10.dpdp 값은 하드코딩되어 있습니다.

14. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt (New File)

  • UI State as Data Class: PreferTime enum class를 사용하여 선호 시간대, 이모지, 제목, 시간 범위, 설명을 함께 관리하는 방식은 UI 상태를 명확하게 표현하는 좋은 패턴입니다.
  • Jetpack Compose:
    • preferTime, selected, onClick을 파라미터로 받아 상태 호이스팅을 잘 준수하고 있습니다. selectedPreferTime 상태를 rememberSaveable로 관리하는 프리뷰 코드가 적절합니다.
    • selectablerole=Role.RadioButton을 명시하여 접근성을 높인 점이 좋습니다.
    • selected 여부에 따라 배경색, 테두리, 그리고 커스텀 라디오 버튼 모양을 조건부로 변경하는 로직이 잘 구현되어 있습니다.
    • preferTime.time?.let { time -> ... }을 사용하여 선택적(null 가능)인 시간을 안전하게 표시하는 점이 좋습니다.
  • Hardcoded Strings/Magic Numbers:
    • PreferTime enum 내의 모든 title, time, description, emoji 값은 하드코딩되어 있습니다. 이들은 사용자에게 노출되는 문자열이므로 strings.xml 리소스 파일로 분리해야 합니다.
    • 93.dp, 20.dp, 1.dp, 3.dp 등 많은 dp 값이 하드코딩되어 있습니다.
  • Preview Code: PreferTimePreview 컴포저블의 onClick 람다 마지막 부분이 잘려 있습니다.
    onClick = {
        selectedPreferT // <-- 여기를 완성해야 합니다.
    }
    이 부분을 selectedPreferTime = preferTime으로 완성해야 합니다.

✅ Summary of Recommendations

  1. Hardcoded Strings (Critical):

    • AgeInputField.kt: "만 나이", "(세)", "*"
    • BackStepBar.kt: contentDescription="뒤로가기"
    • BirthdayChoice.kt: "생년월일", "생년월일을 입력해주세요", "생년월일 선택", "*"
    • BottomWheel.kt: ConfirmBottomSheet의 title, WheelPickeritemText 내 "년", "월", "일" 접미사
    • KeywordChoice.kt: Trait enum 내의 모든 label (및 emoji도 고려)
    • MemberChoice.kt: "*", 프리뷰의 options 리스트 ("대학생", "직장인", "무직")
    • MessageInputField.kt: contentDescription = "전송", 프리뷰의 placeholder
    • NameBox.kt: "*"
    • PreferTime.kt: PreferTime enum 내의 모든 title, time, description (및 emoji도 고려)
    • 모든 사용자에게 노출되는 문자열은 strings.xml로 분리해야 합니다.
  2. Magic Numbers (High):

    • BottomWheel.kt: years 범위의 시작 값 1900
    • MessageInputField.kt: maxLength = 10
    • 이러한 값들은 상수로 분리하여 관리하는 것이 좋습니다.
  3. Code Style / Minor Fixes (Medium):

    • feature/onboarding/impl/build.gradle.kts: 파일 마지막에 줄바꿈 추가.
    • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt: 불필요한 import androidx.compose.foundation.text.BasicTextField 제거.
    • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt: PreferTimePreviewonClick 람다 완성.
  4. UX / Architecture Clarification (Optional but Recommended):

    • AppScreen.ktnavigateToOnboardingnavigateAfterAgreement에서 appNavigator.navigate 대신 appNavigator.replaceAll(Onboarding1NavKey)를 사용해야 할지 현재 UX에 맞춰 검토해 주세요. (온보딩 시작 시 이전 백스택이 필요 없다면 replaceAll이 적합)
    • BottomWheel.ktdraftDateremember 대신 rememberSaveable로 사용하여 화면 회전 시에도 사용자가 변경 중이던 날짜가 유지되도록 할지 UX 관점에서 고려해 볼 수 있습니다.

전반적으로 매우 잘 작성된 코드입니다. 위에 제시된 권장 사항들을 반영하여 코드의 견고함과 유지보수성, 그리고 다국어 지원 가능성을 더욱 높일 수 있을 것입니다.

리뷰 끝!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt (1)

14-14: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

userBirthday 기본값이 실제 선택 가능한 날짜입니다.

다른 3단계 이후 필드들(speechStyle, relationship, preferTime 등)은 null로 "미입력"을 표현하는데, userBirthdayLocalDate.of(2000, 1, 1)라는 유효한 날짜를 기본값으로 사용합니다. 현재는 Onboarding1Screen에서 birthday != null일 때만 updateUserProfile을 호출해 실사용 리스크는 없지만, 모델만 보면 "미선택"과 "2000-01-01 선택"을 구분할 수 없습니다.

-    val userBirthday: LocalDate = LocalDate.of(2000, 1, 1),
+    val userBirthday: LocalDate? = null,

Based on learnings, "Onboarding 화면에서 생년월일(DOB) 입력의 '미선택/미완료' 상태를 표현할 때, 실제로 사용자가 선택할 수 있는 값(예: 2000/1/1)을 기본값으로 쓰지 마세요."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt`
at line 14, Update the userBirthday property in OnboardingUiState to use a
nullable type and a null default, matching the unset-state representation used
by later onboarding fields. Preserve the existing selected-date behavior when a
birthday is provided, while ensuring an unselected birthday cannot be confused
with 2000-01-01.

Source: Learnings

♻️ Duplicate comments (3)
feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt (3)

62-64: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

birthday가 구성 변경 시 유실됩니다 - rememberSaveable을 사용하세요.

기본값을 null로 바꿔 "확인만 눌러도 오늘 날짜 저장" 문제는 해결됐지만, remember만 사용해 화면 회전 등 구성 변경 시 사용자가 선택한 생일이 초기화됩니다. LocalDateSerializable이므로 커스텀 Saver 없이 rememberSaveable만으로 충분합니다.

🛠️ 제안 수정
-    var birthday by remember {
+    var birthday by rememberSaveable {
         mutableStateOf<LocalDate?>(null)
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt`
around lines 62 - 64, Update the birthday state declaration in Onboarding1Screen
to use rememberSaveable instead of remember, while preserving its nullable
LocalDate type and null initial value so the selected birthday survives
configuration changes without restoring today’s date.

54-54: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

profileImageURl 오타 - profileImageUrl로 수정하세요 (기존 지적 미반영).

camelCase 컨벤션 위반이며, Onboarding2ScreenprofileImageUrl로 올바르게 사용 중입니다.

🛠️ 제안 수정
-    profileImageURl: String?=null,
+    profileImageUrl: String? = null,

그리고 사용처(114-117번 줄 부근)도 함께 변경:

                 ProfileChoice(
                     state= ProfileChoiceState(
-                    imageUrl = profileImageURl,
+                    imageUrl = profileImageUrl,
                         size=99.dp,
                     ),

As per path instructions, **/*.kt: "네이밍 컨벤션 준수 여부: 클래스/인터페이스 PascalCase, 함수/변수 camelCase..."

Also applies to: 113-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt`
at line 54, Rename the parameter profileImageURl to profileImageUrl in the
onboarding screen declaration and update every corresponding usage around the
referenced call sites. Keep the existing nullable type and behavior unchanged,
matching the naming used by Onboarding2Screen.

Source: Path instructions


204-251: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

이름 편집 오버레이 dismiss 시 키보드/포커스 정리 누락 (기존 지적 미반영).

배경 클릭과 전송 처리 모두에서 focusManager.clearFocus()/keyboardController?.hide()를 호출하지 않습니다. Onboarding2Screen의 동일 로직(218-219, 235-236)은 두 경로 모두 처리하는데 여기서는 빠져 있어 오버레이가 사라진 뒤에도 키보드가 남을 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt`
around lines 204 - 251, Update the name-edit overlay dismissal paths in the
editingNameField block, including the background clickable handler and
MessageInputField.onSendClick, to clear focus through the screen’s focus manager
and hide the keyboard through its keyboard controller before resetting the
editing state. Match the existing Onboarding2Screen behavior while preserving
the current name assignment and input reset logic.
🧹 Nitpick comments (1)
feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt (1)

59-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

스크롤 감지용 LaunchedEffect 3개 중복 - 헬퍼로 추출하세요.

yearState/monthState/dayState에 대해 동일한 snapshotFlow { it.isScrollInProgress }.filter{it}.collect{ hasUserChangedDate = true } 패턴이 세 번 반복됩니다. 사설 컴포저블 헬퍼로 추출하면 유지보수가 쉬워집니다.

♻️ 제안 리팩터
+@Composable
+private fun TrackWheelInteraction(listState: LazyListState, onChanged: () -> Unit) {
+    LaunchedEffect(listState) {
+        snapshotFlow { listState.isScrollInProgress }
+            .filter { it }
+            .collect { onChanged() }
+    }
+}
+
 ...
-    LaunchedEffect(yearState) {
-        snapshotFlow { yearState.isScrollInProgress }
-            .filter { it }
-            .collect {
-                hasUserChangedDate = true
-            }
-    }
-
-    LaunchedEffect(monthState) {
-        snapshotFlow { monthState.isScrollInProgress }
-            .filter { it }
-            .collect {
-                hasUserChangedDate = true
-            }
-    }
-
-    LaunchedEffect(dayState) {
-        snapshotFlow { dayState.isScrollInProgress }
-            .filter { it }
-            .collect {
-                hasUserChangedDate = true
-            }
-    }
+    TrackWheelInteraction(yearState) { hasUserChangedDate = true }
+    TrackWheelInteraction(monthState) { hasUserChangedDate = true }
+    TrackWheelInteraction(dayState) { hasUserChangedDate = true }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt`
around lines 59 - 85, Extract the repeated scroll-detection logic from the three
LaunchedEffect blocks into a private composable helper that accepts a wheel
state and updates hasUserChangedDate when scrolling begins. Replace the
yearState, monthState, and dayState blocks with calls to that helper while
preserving the existing snapshotFlow and filtering behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding1State.kt`:
- Line 8: Update the birthday property in Onboarding1State to represent an
unselected birth date as null, matching the nullable selection flow in
Onboarding1Screen.kt; remove the LocalDate.now() default and ensure the
next-step submission validation rejects a null birthday before proceeding.

---

Outside diff comments:
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt`:
- Line 14: Update the userBirthday property in OnboardingUiState to use a
nullable type and a null default, matching the unset-state representation used
by later onboarding fields. Preserve the existing selected-date behavior when a
birthday is provided, while ensuring an unselected birthday cannot be confused
with 2000-01-01.

---

Duplicate comments:
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt`:
- Around line 62-64: Update the birthday state declaration in Onboarding1Screen
to use rememberSaveable instead of remember, while preserving its nullable
LocalDate type and null initial value so the selected birthday survives
configuration changes without restoring today’s date.
- Line 54: Rename the parameter profileImageURl to profileImageUrl in the
onboarding screen declaration and update every corresponding usage around the
referenced call sites. Keep the existing nullable type and behavior unchanged,
matching the naming used by Onboarding2Screen.
- Around line 204-251: Update the name-edit overlay dismissal paths in the
editingNameField block, including the background clickable handler and
MessageInputField.onSendClick, to clear focus through the screen’s focus manager
and hide the keyboard through its keyboard controller before resetting the
editing state. Match the existing Onboarding2Screen behavior while preserving
the current name assignment and input reset logic.

---

Nitpick comments:
In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt`:
- Around line 59-85: Extract the repeated scroll-detection logic from the three
LaunchedEffect blocks into a private composable helper that accepts a wheel
state and updates hasUserChangedDate when scrolling begins. Replace the
yearState, monthState, and dayState blocks with calls to that helper while
preserving the existing snapshotFlow and filtering behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9db1ed60-28d8-4153-8ae9-ed4fc9ad85fe

📥 Commits

Reviewing files that changed from the base of the PR and between 94fb158 and 19cbbe0.

📒 Files selected for processing (16)
  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Mbti.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Relationship.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/SpeechStyle.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding1State.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding2State.kt
  • feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/ProfileChoiceState.kt
💤 Files with no reviewable changes (1)
  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt

data class Onboarding1State (
val lastName: String="",
val firstName: String="",
val birthday: LocalDate=LocalDate.now(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

생년월일 미선택 상태에 오늘 날짜를 사용하지 마세요.

LocalDate.now()는 사용자가 생년월일을 선택하지 않은 기본 생성·상태 복원·부분 입력 경로에서 오늘 날짜를 실제 생년월일로 오인하게 만들 수 있습니다. LocalDate? = null 또는 별도의 선택 여부 상태로 모델링하고, 다음 단계에서 미선택 상태를 검증하세요.

Based on learnings, 생년월일 미선택 상태는 실제 선택 가능한 날짜가 아닌 별도 상태로 표현해야 하며, 제공된 Onboarding1Screen.kt의 nullable 선택 흐름과도 일치시켜야 합니다.

제안 수정
-    val birthday: LocalDate=LocalDate.now(),
+    val birthday: LocalDate? = null,
📝 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
val birthday: LocalDate=LocalDate.now(),
val birthday: LocalDate? = null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding1State.kt`
at line 8, Update the birthday property in Onboarding1State to represent an
unselected birth date as null, matching the nullable selection flow in
Onboarding1Screen.kt; remove the LocalDate.now() default and ensure the
next-step submission validation rejects a null birthday before proceeding.

Source: Learnings

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요, 시니어 Android 개발자입니다. PR diff 잘 검토했습니다.
이번 PR은 새로운 온보딩 화면과 관련 UI 컴포넌트들을 추가하는 내용이군요. 전반적으로 Jetpack Compose 원칙을 잘 따르고 있으며, 내비게이션 구조 변경도 타당해 보입니다. 몇 가지 개선할 점과 주의사항을 알려드리겠습니다.


[전반적인 피드백]

  • Jetpack Compose 원칙 준수: remember, rememberSaveable, LaunchedEffect, snapshotFlow 등을 사용하여 Recomposition, State hoisting, Side effect 관리가 전반적으로 잘 되어 있습니다. Role.Button, Role.Checkbox, Role.RadioButton 등 접근성(Accessibility) 관련 속성도 잘 적용하고 있습니다.
  • Previews: 모든 새로운 Composables에 Preview가 잘 작성되어 있어 개발 및 테스트에 용이해 보입니다. 다양한 상태를 보여주는 것도 훌륭합니다.
  • Navigation: OnboardingNavKey를 여러 단계의 OnboardingXNavKey로 분리하여 다단계 온보딩 흐름을 명확하게 정의한 점이 좋습니다. replaceAll을 사용하여 스택 관리를 명확히 한 점도 긍정적입니다.

[세부 코드 리뷰]

1. AppScreen.kt

  • WindowInsets 사용:
    • contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal).exclude(WindowInsets.ime)
    • 시스템 인셋(safeDrawing)을 적절히 처리하고 IME(키보드)를 정확히 제외하여 UI가 키보드에 의해 가려지거나 밀려나는 문제를 잘 방지하고 있습니다. 훌륭한 접근입니다.
  • Navigation Key 사용:
    • CallRecordNavKey(callId = callId)처럼 NavKey에 인자를 전달하여 타입 안정성을 확보한 점은 매우 좋습니다.

2. core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt

  • 새로운 컬러 네이밍 (SubFire, SubSoup):
    • SubRed, SubBlue처럼 "Sub" 접두사가 붙어 있는데, SubFire, SubSoup는 불과 스프라는 고유한 개념을 담고 있습니다. 만약 이 컬러들이 특정 기능에 한정되지 않고 디자인 시스템 전반에 사용될 보조 컬러라면, SubOrange, SubYellow 또는 AccentOrange, AccentYellow처럼 좀 더 일반적인 의미의 이름으로 바꾸는 것을 고려해 보세요.
    • DragGradientBrush 형태로 잘 정의되었습니다.

3. core/designsystem/src/main/java/kr/co/call/designsystem/theme/Type.kt

  • 새로운 Typography (captionSmall, miniMedium):
    • 기존 디자인 시스템의 pretendard 함수를 활용하여 새로운 텍스트 스타일을 일관성 있게 추가한 점은 좋습니다.

4. feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt

  • 다단계 온보딩 라우트:
    • Onboarding1NavKey부터 Onboarding6NavKey까지 각 온보딩 단계를 data object NavKey로 명확히 분리한 것은 내비게이션 관리와 코드 가독성 측면에서 매우 좋은 변화입니다.

5. feature/onboarding/impl/build.gradle.kts

  • Coil Compose 추가:
    • 이미지 로딩 라이브러리인 Coil Compose가 추가된 것을 확인했습니다. ProfileChoice 컴포넌트 등에서 사용될 것으로 보이며, Compose 환경에서 이미지 로딩을 처리하는 좋은 선택입니다.

6. feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ (새로운 컴포넌트들)

  • AgeInputField.kt

    • 하드코딩된 문자열 (Critical): "만 나이", "*", "(세)" 등의 텍스트는 strings.xml 리소스 파일로 이동하여 지역화를 지원해야 합니다.
    • Magic Number (Recommendation): 7.dp, 51.dp, 10.dp 등 직접 사용된 dp 값들은 디자인 시스템의 스페이싱/패딩 토큰으로 정의하여 일관성을 높이고 재사용성을 확보할 수 있습니다. 예를 들어 CallTheme.spacing.small, CallTheme.dimens.inputFieldHeight 등.
  • BackStepBar.kt

    • 하드코딩된 문자열 (Critical): "뒤로가기"는 strings.xml 리소스 파일로 이동해야 합니다.
  • BirthdayChoice.kt

    • 하드코딩된 문자열 (Critical): "생년월일", "*", "생년월일을 입력해주세요", "생년월일 선택" 등 모든 사용자에게 노출되는 텍스트는 strings.xml 리소스 파일로 이동해야 합니다.
    • 날짜 표시 형식 (toDisplayText): "$year / $monthValue / $dayOfMonth"는 현재 문제 없지만, 향후 다양한 지역화(Locale) 요구사항을 고려한다면 java.time.format.DateTimeFormatter를 사용하여 날짜 형식을 유연하게 처리하는 것이 좋습니다.
  • BottomWheel.kt

    • 하드코딩된 문자열 (Critical): "생년월일 선택" 텍스트는 strings.xml 리소스 파일로 이동해야 합니다.
    • LaunchedEffect를 통한 스크롤 감지: snapshotFlow { yearState.isScrollInProgress }.filter { it }.collect { hasUserChangedDate = true } 와 같이 사용자 스크롤 여부를 정확히 감지하여 초기값과 사용자 선택을 구분하는 로직은 매우 훌륭합니다.
    • 날짜 유효성 처리: coerceAtMost(YearMonth.of(...).lengthOfMonth())를 사용하여 월 변경 시 날짜 유효성을 자동으로 맞춰주는 로직은 견고합니다.
  • KeywordChoice.kt

    • 하드코딩된 문자열 (Critical): Trait enum의 emojilabel 필드에 직접 할당된 문자열들은 strings.xml 리소스 파일로 이동하여 지역화를 지원해야 합니다. Trait enum 자체는 UI 상태를 나타내므로 괜찮지만, 포함된 텍스트는 분리해야 합니다.
    • Magic Number (Recommendation): 33.dp, 30.dp 등의 dp 값들에 대해 AgeInputField.kt와 동일한 권장 사항을 적용합니다.
  • MemberChoice.kt

    • 하드코딩된 문자열 (Critical): "직업", "*", "직업을 선택해주세요" 등의 텍스트는 strings.xml 리소스 파일로 이동하여 지역화를 지원해야 합니다.
    • Dropdown 메뉴 위치 조정: BoxWithConstraints를 활용하여 maxWidth를 얻어 DropdownMenuoffset을 계산하는 방식은 유연하고 정확한 UI 배치를 위해 좋은 구현입니다.
    • Magic Number (Recommendation): 7.dp, 51.dp, 10.dp, 114.dp, 420.dp 등의 dp 값들에 대해 동일한 권장 사항을 적용합니다.
  • MessageInputField.kt

    • 하드코딩된 문자열 (Critical): "전송", "메시지를 입력해주세요.", "${value.length}/$maxLength" 문자열은 strings.xml 리소스 파일로 이동해야 합니다. 특히 %1$d/%2$d와 같은 형식으로 포매팅해야 합니다.
    • Magic Number maxLength=10 (Critical): maxLength는 코드 내에 하드코딩된 매직 넘버입니다. 이는 OnboardingConstants와 같은 상수 파일 또는 strings.xml의 Integer 리소스로 정의하여 관리하는 것이 좋습니다.
    • Focus 관리: LaunchedEffectFocusRequester, LocalSoftwareKeyboardController를 사용하여 키보드 및 포커스 관리를 동적으로 처리하는 것은 좋은 접근입니다.
  • NameBox.kt

    • 하드코딩된 문자열 (Critical): "이름", " *" 등의 텍스트는 strings.xml 리소스 파일로 이동해야 합니다.
    • value.ifBlank { " " }: 비어있는 텍스트를 처리하기 위한 일반적인 방법이지만, Modifier.defaultMinSize(minHeight = 텍스트라인높이)Spacer를 활용하여 텍스트가 비어있을 때도 컴포넌트의 높이가 유지되도록 하는 것이 더 명시적이고 깔끔할 수 있습니다.
  • PreferTime.kt

    • Enum PreferTime 내 하드코딩된 문자열/이모지 (Critical): PreferTime enum의 emoji, title, time, description 필드에 직접 할당된 모든 문자열과 이모지는 strings.xml 리소스 파일로 이동해야 합니다. enum은 해당 리소스의 ID (예: R.string.prefer_time_morning_emoji, R.string.prefer_time_morning_title)를 참조하도록 수정해야 합니다. 이는 지역화 및 콘텐츠 관리에 매우 중요합니다.
    • Magic Number (Recommendation): 93.dp, 20.dp 등의 dp 값들에 대해 동일한 권장 사항을 적용합니다.
  • ProfileChoice.kt

    • Diff 누락: PR diff가 interactionSource=remember{MutableInteractionSo에서 잘려 있습니다. 전체 코드를 확인하여 ProfileChoiceState의 정의 (data class/enum)와 AsyncImage의 설정이 적절한지 확인해야 합니다.
    • interactionSource.collectIsPressedAsState(): 클릭 시 시각적 피드백(눌림 상태)을 제공하는 좋은 방법입니다.

[종합적인 권장 사항]

  1. 모든 사용자 노출 텍스트 지역화 (Critical): 이 PR에서 새로 추가된 모든 컴포넌트의 사용자 노출 텍스트(AgeInputField, BackStepBar, BirthdayChoice, BottomWheel, KeywordChoice, MemberChoice, MessageInputField, NameBox, PreferTime 내의 모든 텍스트 및 이모지)는 strings.xml 파일로 이동해야 합니다. PreferTime enum의 경우, 문자열 리소스 ID를 필드로 가지도록 수정하는 것을 권장합니다.
  2. 매직 넘버 제거 (MessageInputField.kt maxLength) (Critical): MessageInputFieldmaxLength=10은 상수로 분리하여 관리해야 합니다.
  3. 디자인 시스템 토큰 활용 (Recommendation): 컴포넌트 내부에서 사용되는 dp 단위의 치수(패딩, 높이, 너비, 간격 등)는 가능하다면 디자인 시스템의 CallThemespacing, dimens 등의 이름으로 정의된 토큰을 활용하는 것을 고려해 보세요. 이는 디자인 일관성을 유지하고 유지보수를 용이하게 합니다.
  4. 컬러 네이밍 컨벤션 재검토 (Recommendation): SubFire, SubSoup와 같은 컬러 이름이 디자인 시스템 전반의 컨벤션과 일치하는지 다시 한번 검토해 주세요.

이번 PR은 큰 기능 구현에 앞서 UI 컴포넌트와 내비게이션 기반을 잘 다진 것으로 보입니다. 위 피드백을 반영하여 코드의 견고성과 확장성을 더욱 높여주시면 좋겠습니다.

수고하셨습니다!

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요! 시니어 안드로이드 개발자로서 GitHub Pull Request 리뷰를 시작하겠습니다. 온보딩 플로우를 개선하고 새로운 컴포넌트들을 도입하는 작업으로 보이네요. 전반적으로 코드 품질이 좋고 Jetpack Compose의 모범 사례를 잘 따르려고 노력한 흔적이 보입니다. 몇 가지 개선점을 제안해 드릴게요.


PR 요약

이 PR은 주로 온보딩(Onboarding) 플로우를 여러 단계로 세분화하고, 이를 위한 새로운 Jetpack Compose UI 컴포넌트들을 추가합니다.

  • 내비게이션 변경: 단일 OnboardingNavKeyOnboarding1NavKey부터 Onboarding6NavKey까지 여러 개의 키로 분리하여 다단계 온보딩을 구현했습니다.
  • 디자인 시스템 확장: 새로운 색상과 폰트 스타일을 디자인 시스템에 추가했습니다.
  • 새로운 UI 컴포넌트: AgeInputField, BackStepBar, BirthdayChoice, BottomWheel(날짜 선택 휠), KeywordChoice(키워드 선택), MemberChoice(드롭다운 선택), MessageInputField(메시지 입력), NameBox, PreferTime(선호 시간 선택), ProfileChoice(프로필 이미지 선택) 등 다양한 재사용 가능한 컴포넌트들을 추가했습니다.
  • Coil 라이브러리 추가: 이미지 로딩을 위해 coil-compose를 의존성에 추가했습니다.

코드 리뷰 상세

1. Kotlin 코드 리뷰 (일반)

  • Coroutine 사용 시 Dispatcher 명시 여부, viewModelScope/lifecycleScope 오남용:
    • 현재 PR에는 ViewModel 코드가 포함되어 있지 않아 viewModelScopelifecycleScope의 오남용 여부는 확인할 수 없습니다.
    • BottomWheel.ktLaunchedEffectsnapshotFlow는 Composable의 Lifecycle을 따르므로 적절한 사용입니다. UI 스크롤 이벤트를 관찰하고 플래그를 설정하는 데 Dispatcher 명시는 필요하지 않습니다.
    • MessageInputField.ktLaunchedEffect(autoFocus) 또한 UI 관련 로직이므로 적절합니다.
  • Flow/StateFlow 사용 시 collect 시점 (repeatOnLifecycle 사용 여부):
    • 현재 PR에는 collect 로직이 직접적으로 포함되어 있지 않습니다. 추후 ViewModel에서 UI로 데이터를 노출할 때 Flow를 사용한다면 repeatOnLifecycle을 활용하여 효율적인 수집을 하도록 가이드라인을 준수해야 합니다.
  • null 안전성 (!! 사용 지양):
    • PR diff 내에서 !! (non-null assertion operator) 사용은 발견되지 않았습니다. 매우 좋습니다!
  • data class에 불필요한 var 사용 여부:
    • OnboardingRoute.kt의 새로운 NavKey들은 모두 data object로 선언되어 불필요한 var 사용이 없습니다. 모범 사례를 잘 따르고 있습니다.
    • PreferTime enum class도 좋습니다.
  • Hilt DI 시 생성자 주입 원칙 준수 여부:
    • 현재 PR에는 Hilt DI 관련 코드가 포함되어 있지 않습니다.
  • 하드코딩된 문자열/매직 넘버:
    • 하드코딩된 문자열: 많은 텍스트들이 하드코딩되어 있습니다. strings.xml 리소스 파일로 이동하여 관리하는 것이 좋습니다.
      • AgeInputField.kt: "만 나이", "*" "(세)"
      • BackStepBar.kt: "뒤로가기" (contentDescription)
      • BirthdayChoice.kt: "생년월일 ", "*", "생년월일을 입력해주세요", "$year / $monthValue / $dayOfMonth" (날짜 포맷도 string resource로 관리하는 것이 좋습니다), "생년월일 선택" (contentDescription)
      • BottomWheel.kt: "생년월일 선택", "년", "월", "일" (날짜 단위)
      • KeywordChoice.kt: Traitemojilabel은 enum에 정의되어 있지만, Trait 자체의 정의가 이 diff에 없어 확실하지는 않습니다. 하지만 컴포넌트 레벨에서는 문자열로 주입받고 있으므로, Trait enum도 strings.xml을 참조하도록 개선할 수 있습니다.
      • MemberChoice.kt: "직업", "*", "직업을 선택해주세요" (placeholder)
      • MessageInputField.kt: "전송" (contentDescription), "메시지를 입력해주세요." (placeholder), "${value.length}/$maxLength" (포맷 스트링)
      • NameBox.kt: " *", label로 전달되는 "이름" 등.
      • PreferTime.kt enum class의 모든 emoji, title, time, description 필드는 strings.xml로 이동해야 합니다.
    • 매직 넘버:
      • MessageInputField.kt: maxLength = 10은 상수로 추출하여 사용하는 것이 좋습니다 (예: private const val MAX_MESSAGE_LENGTH = 10).

2. Jetpack Compose 코드 리뷰

  • Composable 함수의 불필요한 recomposition 유발 여부 (remember, key 사용):
    • 전반적으로 rememberrememberSaveable을 사용하여 UI 상태를 적절히 관리하고 있습니다.
    • AppScreen.kt에서 bottomBarPadding 계산을 remember 블록 안에 넣어 불필요한 재계산을 방지한 점이 좋습니다.
    • BottomWheel.kt에서 snapshotFlow를 사용하여 스크롤 진행 상황을 관찰하는 방식은 효과적입니다.
    • MemberChoice.ktDropdownMenuoffset 계산 ( menuWidth-114.dp)과 width ( 114.dp ) 등은 상수로 추출하여 관리하는 것이 좋습니다. UI 스펙에 따라 달라질 수 있는 값입니다.
  • State hoisting 원칙 준수:
    • 새로 추가된 모든 컴포넌트(예: AgeInputField, BirthdayChoice, MessageInputField, MemberChoice, KeywordChoice, PreferTime)는 value/selectedonValueChange/onClick과 같은 콜백을 인자로 받아 상태를 외부에서 관리하도록 잘 구현되어 있습니다. 이는 Compose의 단방향 데이터 흐름 및 상태 호이스팅 원칙을 잘 따르고 있습니다.
    • AppScreen.kt에서 온보딩 플로우의 각 단계별 내비게이션 콜백(예: onOnboarding1Next, onBackFromOnboarding2 등)을 onboardingEntry 함수에 인자로 전달한 방식도 상태 호이스팅을 잘 구현한 예시입니다.
  • side effect (LaunchedEffect, DisposableEffect) 사용의 적절성:
    • BottomWheel.kt에서 LaunchedEffect를 사용하여 스크롤 진행 상태를 감지하고 hasUserChangedDate 상태를 업데이트하는 것은 적절한 side effect 처리입니다.
    • MessageInputField.kt에서 LaunchedEffect(autoFocus)를 사용하여 특정 조건에서 키보드를 자동으로 올리고 포커스를 요청하는 것 또한 적절한 side effect 사용입니다.
  • UI 상태를 data class로 표현했는지:
    • ProfileChoice.kt에서 ProfileChoiceState를 사용하는 것을 볼 수 있습니다. data class로 정의되어 있다면 좋습니다. (정의 코드가 없어서 확인 불가).
    • PreferTime enum class도 UI 상태를 명확하게 표현하는 좋은 방법입니다.
  • 로딩/에러 상태를 Boolean 대신 LoadStatus로 관리하는지:
    • 이 PR diff에서는 로딩/에러 상태를 다루는 UI가 명확하게 보이지 않습니다. 만약 추후에 데이터 로딩이 포함된 화면을 추가한다면 Boolean 플래그 대신 LoadStatus (혹은 Result 타입)와 같은 sealed class를 사용하여 Loading, Success(data), Error(exception) 등의 상태를 명확하게 표현하는 것이 좋습니다.

3. Repository/DataSource 레이어

  • 이 PR에는 Repository/DataSource 레이어에 대한 변경 사항이 없습니다.

4. ViewModel

  • 이 PR에는 ViewModel 코드가 포함되어 있지 않습니다.
  • AppScreen.kt에서 rememberViewModelStoreNavEntryDecorator를 사용하는 것은 내비게이션 라이브러리에서 ViewModel의 생명주기를 관리하기 위한 것이므로 적절합니다.

5. FCM/SSE/실시간 통신 관련 코드

  • 이 PR에는 FCM/SSE/실시간 통신 관련 코드가 포함되어 있지 않습니다.

전반적인 개선 제안

  1. Hardcoded String Resource 관리:
    • 가장 시급한 개선 사항입니다. 모든 사용자에게 노출되는 문자열은 app/src/main/res/values/strings.xml 또는 적절한 피처 모듈의 strings.xml 파일로 이동해야 합니다. 이는 다국어 지원, 재사용성, 그리고 코드 가독성 측면에서 필수적입니다.
    • 예시: AgeInputField.kt의 "만 나이"를 @string/onboarding_age_label 등으로 변경.
  2. Magic Number 상수화:
    • MessageInputField.ktmaxLength = 10과 같이 의미 있는 숫자는 const val로 추출하여 코드 상단이나 별도의 Constants 파일에 정의하는 것이 좋습니다.
  3. 디자인 시스템 유틸리티 활용:
    • 새로 추가된 Color.kt, Type.kt의 요소들을 디자인 시스템으로 잘 관리하고 있습니다. UI 컴포넌트에서 10.dp, 51.dp와 같은 특정 Dp 값들이 자주 등장하는데, 혹시 특정 spacing이나 size에 대한 디자인 토큰이 있다면 이를 활용하는 것도 고려해볼 수 있습니다. (CallTheme.spacing.medium 등) 현재는 컴포넌트 내부에서만 사용되고 있어 큰 문제는 아니지만, 더 큰 규모의 디자인 시스템에서는 유용할 수 있습니다.
  4. ProfileChoiceState 정의 확인:
    • ProfileChoice.kt에서 사용된 ProfileChoiceStatedata class로 정의되어 UI 상태를 잘 표현하고 있는지 확인해주세요.

긍정적인 측면

  • 컴포저블 재사용성: 온보딩 각 단계를 위한 다양한 UI 컴포넌트들을 재사용 가능한 형태로 잘 분리하여 구현했습니다. 이는 향후 유지보수 및 확장성에 크게 기여할 것입니다.
  • Jetpack Compose 모범 사례 준수: 상태 호이스팅, remember/rememberSaveable, LaunchedEffect 등의 사용이 적절하여 Compose의 기본 원칙을 잘 따르고 있습니다.
  • 세분화된 온보딩 플로우: OnboardingNavKey를 여러 단계로 나눈 것은 사용자 경험을 개선하고 각 단계의 로직을 명확하게 분리하는 데 도움이 됩니다.
  • Coil 도입: 이미지 로딩을 위한 표준 라이브러리를 도입한 것은 좋은 선택입니다.

결론:

전반적으로 매우 훌륭한 PR입니다. 특히 Jetpack Compose의 모범 사례를 잘 따르며 재사용 가능한 컴포넌트를 효과적으로 구현한 점이 인상 깊습니다. 위에서 언급한 하드코딩된 문자열과 매직 넘버들을 리소스 및 상수로 분리하는 작업을 해주시면 더욱 완벽한 코드가 될 것 같습니다.

리뷰해주셔서 감사합니다!

@codebidoof codebidoof left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

머지머지머지머지머지머지

@codebidoof
codebidoof merged commit 23a77c7 into develop Jul 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 온보딩페이지 UI 구현

3 participants