Skip to content

[Mod/#16] 최초 회원가입 UI 수정#30

Open
choi-day wants to merge 135 commits intodevfrom
mod/#16-modify-signup-logic
Open

[Mod/#16] 최초 회원가입 UI 수정#30
choi-day wants to merge 135 commits intodevfrom
mod/#16-modify-signup-logic

Conversation

@choi-day
Copy link
Collaborator

@choi-day choi-day commented Jan 31, 2026

✨ 작업 내용

최초 회원가입 UI 수정

📄 상세 내용

상단 말풍선 멘트는 기획에따라 바뀔 예정입니다!!!

  1. Dev 브랜치와 머지하여 변경사항 적용 했습니다.

  2. 최초 회원가입 UI 수정했습니다.

  • SpeechBubble: 풍선 꼬리 방향을 결정할 수 있는 옵션(Bottom, Right)을 추가했습니다
  • DduruButton: cornersize, modifier, colors, borderColor 옵션을 추가했습니다
  • 펭귄 말풍선을 SpeechBubble을 이용하여 UI를 구현하였습니다
  • 타이포그래피에서 버튼의 폰트사이즈를 변경하였습니다(14sp -> 16sp)
  • 성별 선택 컴포넌트에 공용 버튼 컴포넌트인 DduruButton을 적용하였습니다
  • 닉네임 랜덤 버튼에 아이콘을 추가하였습니다
  • 전체 화면 배경 색상을 변경하였습니다
  • 닉네임 입력 이후부터 키보드 위 버튼을 추가하였습니다
  • 컴포넌트의 부드러운 삽입을 위해 애니메이션을 추가했습니다
  • 날짜 계산 모듈을 구현하고 적용했습니다
  • 닉네임, 인증번호, 전화번호 유효성 검사 코드를 구현하고 에러메세지를 보여줄 수 있도록 구현하였습니다
    -> 서버에서 검증하는 것과 별개로 프론트 내부에서 유효성 검사를 진행합니다(빈칸, 길이 등)

📎 관련 이슈

#16

📷 스크린샷 / 영상

  • 전체 흐름
Screen_recording_20260307_052340.mp4
  • 키보드 유무에따른 버튼 변화
Screen_recording_20260307_054243.mp4

✅ 체크리스트

PR을 보내기 전에 아래 항목들을 확인해주세요.

  • 코드 컨벤션 준수
  • 빌드 성공
  • 로직 자체 테스트 완료
  • 불필요한 파일/코드 제거(개행 정리)
  • 이슈 번호 연결 확인
  • EOL(End Of Line) 확인

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 전화번호 인증 기반 회원가입 흐름 추가(생년월일, 성별, 전화번호, 인증코드 입력, 자동포커스)
    • 인증 코드 전송/재전송, 타이머 및 검증 UI(유효성 메시지 포함)
    • 새로운 UI 컴포넌트: 말풍선, 인사 카드, 입력 필드, 검증 메시지 및 아이콘 자산 추가
  • Refactor

    • 인증·네트워크 및 상태 관리 흐름 개선(토큰 저장 및 처리 포함)

hyeminililo and others added 30 commits December 20, 2025 02:58
완료 버튼이나 다음으로 사용되는 PrimaryButton이 재사용성 가능성이 있어, 컴포넌트로 구현
현재 svg 파일이 적용되지 않아, 이모지를 png로 적용
Signup_screen에서 사용하는 State 구현
완료, 다음 버튼에 사용되는 Primary 버튼 재사용성 가능성이 있어 컴포넌트로 구현
성별, 생일에 따라 입력필드 컴포넌트 구현
기존에 GreetingCard 뷰모델 삭제 후, SignupScreen에서 쓰이는 하나의 ViewModel 구현
화면에 보여주기 위한 역할을 하기 위해 Presentation 계층 생성
-  UI State : UI 상태를 나타내는 State 생성
- Screen : 정적 화면 생성
- ViewModel : 화면 데이터 흐름 유지하는 ViewModel 생성
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (7)
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/util/ValidationMessageMapper.kt (1)

7-20: ⚠️ Potential issue | 🟠 Major

VALID 분기에서 런타임 예외를 던지고 있습니다

Line [12], Line [19]의 error("")는 호출부 선검증이 누락되면 즉시 크래시를 유발합니다. VALID는 빈 문자열 반환으로 통일하는 편이 안전합니다.

수정 예시
     fun PhoneValidationResult.toMessage(): String =
         when (this) {
             PhoneValidationResult.EMPTY -> "전화번호를 입력해주세요."
             PhoneValidationResult.NOT_DIGITS -> "숫자만 입력 가능해요."
             PhoneValidationResult.INVALID_LENGTH -> "전화번호를 정확히 입력해주세요."
-            PhoneValidationResult.VALID -> error("")
+            PhoneValidationResult.VALID -> ""
         }
@@
     fun AuthCodeValidationResult.toMessage(): String =
         when (this) {
             AuthCodeValidationResult.EMPTY -> "인증번호를 입력해주세요."
             AuthCodeValidationResult.EXPIRED_TIME -> "인증 시간이 만료되었어요."
-            AuthCodeValidationResult.VALID -> error("")
+            AuthCodeValidationResult.VALID -> ""
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/util/ValidationMessageMapper.kt`
around lines 7 - 20, The VALID branches in PhoneValidationResult.toMessage() and
AuthCodeValidationResult.toMessage() currently call error(""), causing runtime
crashes; change those branches to return an empty string ("") instead so VALID
maps to a harmless empty message; update the PhoneValidationResult.toMessage()
VALID branch and the AuthCodeValidationResult.toMessage() VALID branch
accordingly to avoid throwing an exception.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt (4)

256-263: ⚠️ Potential issue | 🟠 Major

등록 실패 시 사용자 피드백이 누락됩니다

Line [256]-Line [263]에서 실패 메시지 매핑이 주석 처리되어 있어, 실패 원인을 UI에서 알 수 없습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`
around lines 256 - 263, Re-enable and set the user-facing error message in the
SignupViewModel failure branch by restoring the commented mapping: when the
onFailure handler runs (in the SignupViewModel coroutine block), update uiState
to set isLoading = false and populate profileErrorMessage using
ProfileInitErrorMapper.toMessage(status = status, serverMessage = e.message ?:
e.localizedMessage ?: ""), ensuring the variable status is the correct status in
scope (or derive it before this block) so the UI receives the mapped error text.

220-223: ⚠️ Potential issue | 🟡 Minor

등록 시작 시 로딩 상태가 켜지지 않습니다

Line [222]의 uiState = uiState.copy()는 no-op이라 로딩 UI가 표시되지 않습니다.

수정 예시
     fun onRegisterComplete() {
         viewModelScope.launch {
-            uiState = uiState.copy()
+            uiState = uiState.copy(isLoading = true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`
around lines 220 - 223, The onRegisterComplete function currently calls
viewModelScope.launch { uiState = uiState.copy() } which is a no-op and doesn't
toggle the loading UI; replace that copy call to set the loading flag (e.g.,
uiState = uiState.copy(isLoading = true)) before starting the registration work
inside viewModelScope.launch, perform the registration, then update uiState
again to clear the loading flag and set success/error state as appropriate (use
the uiState property and onRegisterComplete method to locate where to set
isLoading true/false).

202-212: ⚠️ Potential issue | 🟡 Minor

인증 타이머 초기값/반복 조건이 흐름과 불일치합니다

Line [206]의 60초 fallback은 onPhoneComplete180초 설정과 충돌하고, Line [207]의 >= 0은 0초 이후 한 번 더 반복됩니다.

수정 예시
     private fun startAuthTimer() {
         authTimerJob?.cancel()
         authTimerJob = viewModelScope.launch {
-            var seconds = uiState.authTimerSeconds
-            if (seconds <= 0) seconds = 60
-            while (seconds >= 0) {
+            var seconds = uiState.authTimerSeconds.takeIf { it > 0 } ?: 180
+            while (seconds > 0) {
                 uiState = uiState.copy(authTimerSeconds = seconds)
                 delay(1000L)
                 seconds--
             }
+            uiState = uiState.copy(authTimerSeconds = 0)
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`
around lines 202 - 212, startAuthTimer() uses a hardcoded 60s fallback and a
while (seconds >= 0) loop which conflicts with onPhoneComplete's 180s initial
value and causes one extra tick at 0; change startAuthTimer() to use the same
fallback value as onPhoneComplete (use 180 when uiState.authTimerSeconds <= 0)
and adjust the loop to run while (seconds > 0), updating uiState each tick and
after the loop set uiState = uiState.copy(authTimerSeconds = 0) to ensure the
timer stops exactly at 0; reference symbols: startAuthTimer(),
uiState.authTimerSeconds, onPhoneComplete, authTimerJob, viewModelScope.launch.

37-37: ⚠️ Potential issue | 🟡 Minor

연도 상한 하드코딩으로 최신 연도 선택이 막힙니다

Line [37]이 2024로 고정되어 있어, 2026-02-28 기준 최신 연도 선택이 불가능합니다.

수정 예시
+import java.time.Year
@@
-    val years = (1940..2024).toList().reversed()
+    val years = (1940..Year.now().value).toList().reversed()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`
at line 37, Replace the hardcoded upper bound in the years list initialization
(val years in SignupViewModel) so it uses the runtime current year instead of
2024; specifically, compute the upper bound from the Java time API (e.g.,
LocalDate.now().year) and build the range
(1940..currentYear).toList().reversed(), and add the necessary import
(java.time.LocalDate) so the code compiles.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/NicknameInput.kt (2)

39-45: ⚠️ Potential issue | 🟡 Minor

자동 포커스 이펙트가 enabled 변경을 놓칩니다.

Line 39의 LaunchedEffect(shouldAutoFocus)enabledfalse -> true로 바뀌는 경우 재실행되지 않아 포커스/키보드 표시가 누락될 수 있습니다.

수정 제안
-    LaunchedEffect(shouldAutoFocus) {
+    LaunchedEffect(shouldAutoFocus, enabled) {
         if (shouldAutoFocus && enabled) {
             delay(200)
             focusRequester.requestFocus()
             keyboardController?.show()
         }
     }

As per coding guidelines, "Jetpack Compose 구조, 상태 관리, recomposition 최적화에 집중" 및 "불필요한 recomposition 가능성 ... 확인".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/NicknameInput.kt`
around lines 39 - 45, LaunchedEffect is only keyed on shouldAutoFocus so it
won't re-run when enabled flips true; update the effect in NicknameInput.kt to
also include enabled in the key (i.e., LaunchedEffect(shouldAutoFocus, enabled))
and keep the existing guard (if (shouldAutoFocus && enabled) { delay(200);
focusRequester.requestFocus(); keyboardController?.show() }) so the effect
retriggers when enabled changes and reliably requests focus and shows the
keyboard.

79-98: ⚠️ Potential issue | 🟡 Minor

버튼의 상태 색상 정책이 자식 Icon/Text에서 무시됩니다.

ButtonColors를 지정했지만 Line 90, Line 97에서 고정 색을 직접 주고 있어 enabled/disabled에 따른 contentColor 반영이 깨집니다.

수정 제안
         Icon(
             painter = painterResource(id = R.drawable.ic_random),
             contentDescription = "랜덤 닉네임 생성",
             modifier = Modifier.size(16.dp).padding(1.dp),
-            tint = DduruTheme.colors.Gray700
+            // Button의 contentColor/disabledContentColor를 따르도록 기본 tint 사용
         )

         Text(
             modifier = Modifier.padding(1.dp),
             text = "랜덤",
-            style = DduruTheme.typography.Button,
-            color = DduruTheme.colors.Gray700
+            style = DduruTheme.typography.Button
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/NicknameInput.kt`
around lines 79 - 98, The Button's contentColor policy set via
ButtonDefaults.buttonColors is being ignored because the child Icon and Text use
hardcoded tint/color; update the children in NicknameInput.kt (the Icon call and
the Text call) to stop passing fixed colors (remove tint =
DduruTheme.colors.Gray700 and color = DduruTheme.colors.Gray700) so they inherit
the button's contentColor, or explicitly set tint/color to
LocalContentColor.current (with LocalContentAlpha.current if needed) so
enabled/disabled states follow the ButtonColors policy.
🧹 Nitpick comments (1)
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt (1)

30-163: 두 TextField 컴포저블의 공통 UI를 추출하는 편이 유지보수에 유리합니다.

MaterialUnderlineTextFieldMaterialUnderlineNumberTextField의 라벨/레이아웃/색상 설정이 거의 동일해, 추후 스타일 변경 시 불일치가 생기기 쉽습니다. 공통 베이스 컴포저블 + 입력 전략(일반/숫자) 분리로 단순화하는 것을 권장합니다.

As per coding guidelines, "Jetpack Compose 구조 ... 가독성 ... Google 권장 Android 아키텍처 준수 여부".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt`
around lines 30 - 163, Extract the shared UI (label, spacing, Row, placeholder
styling, colors, action slot and TextField styling) into a new base composable
(e.g., BaseMaterialUnderlineField) and refactor MaterialUnderlineTextField and
MaterialUnderlineNumberTextField to call it; keep unique behavior by passing an
input transformation lambda (e.g., onRawValueChange: (String) -> Unit) and
optional KeyboardOptions/keyboardType and maxLength params into the base
composable so MaterialUnderlineNumberTextField supplies a digit-filtering
wrapper (the current onValueChange logic) plus
KeyboardOptions(KeyboardType.Number) while MaterialUnderlineTextField supplies a
passthrough onValueChange and default IME options; update uses of label,
placeholder, enabled, actionContent, textStyle, and colors to come from the
single base composable to avoid duplicated layout and styling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/gildongmu/dduru/data/di/NetworkModule.kt`:
- Around line 50-53: The shared OkHttpClient currently enables automatic retry
on connection failure via retryOnConnectionFailure(true) in the
OkHttpClient.Builder; this risks duplicate non-idempotent POST/PATCH calls
(e.g., login, token refresh, SMS) — change the common client to disable this by
removing or setting retryOnConnectionFailure(false) in the OkHttpClient.Builder
that adds authInterceptor and logging, and instead implement any needed retry
logic only at the call site or a dedicated per-endpoint client with server-side
idempotency (Idempotency-Key) and controlled retry/backoff.

In
`@app/src/main/java/com/gildongmu/dduru/data/register/repository/OnboardingRegisterRepositoryImpl.kt`:
- Around line 21-39: The current use of runCatching in
OnboardingRegisterRepositoryImpl (around the call that reads
verificationTokenLocalDataSource.verificationToken and calls api.initProfile
with OnboardingRegisterRequest) will capture CancellationException and prevent
coroutine cancellation from propagating; change the error handling so
CancellationException is rethrown instead of being wrapped: replace the
runCatching block with a try/catch that catches CancellationException and
rethrows it, and converts other exceptions into a Result.failure (or if you keep
runCatching, inspect the caught exception and rethrow if it is a
CancellationException before returning Result.failure).

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt`:
- Around line 101-125: The BaseLineInput composable currently calls
digitsOnly.take(maxLength) which will throw if maxLength is negative; modify the
onValueChange handling in BaseLineInput so maxLength is validated/normalized
before use (e.g., if maxLength is null or < 0 treat as no limit or coerce to 0)
and then apply take on the normalized non-negative limit (reference symbols:
BaseLineInput, maxLength, digitsOnly.take, onValueChange/lambda) to prevent
IllegalArgumentException for negative inputs.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt`:
- Around line 55-67: The step mapping in GreetingMessageCard.kt is missing
SignupStep.AUTH_VERIFICATION so when step == SignupStep.AUTH_VERIFICATION
currentIndex falls back to DONE; fix this by adding an entry for
SignupStep.AUTH_VERIFICATION into the stepContents list (e.g., place it after
SignupStep.AUTH with a proper title and description string), leaving the
existing currentIndex/totalSteps logic intact so the STEP count and displayed
(title, desc) come from the new mapping.

---

Duplicate comments:
In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/NicknameInput.kt`:
- Around line 39-45: LaunchedEffect is only keyed on shouldAutoFocus so it won't
re-run when enabled flips true; update the effect in NicknameInput.kt to also
include enabled in the key (i.e., LaunchedEffect(shouldAutoFocus, enabled)) and
keep the existing guard (if (shouldAutoFocus && enabled) { delay(200);
focusRequester.requestFocus(); keyboardController?.show() }) so the effect
retriggers when enabled changes and reliably requests focus and shows the
keyboard.
- Around line 79-98: The Button's contentColor policy set via
ButtonDefaults.buttonColors is being ignored because the child Icon and Text use
hardcoded tint/color; update the children in NicknameInput.kt (the Icon call and
the Text call) to stop passing fixed colors (remove tint =
DduruTheme.colors.Gray700 and color = DduruTheme.colors.Gray700) so they inherit
the button's contentColor, or explicitly set tint/color to
LocalContentColor.current (with LocalContentAlpha.current if needed) so
enabled/disabled states follow the ButtonColors policy.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/util/ValidationMessageMapper.kt`:
- Around line 7-20: The VALID branches in PhoneValidationResult.toMessage() and
AuthCodeValidationResult.toMessage() currently call error(""), causing runtime
crashes; change those branches to return an empty string ("") instead so VALID
maps to a harmless empty message; update the PhoneValidationResult.toMessage()
VALID branch and the AuthCodeValidationResult.toMessage() VALID branch
accordingly to avoid throwing an exception.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`:
- Around line 256-263: Re-enable and set the user-facing error message in the
SignupViewModel failure branch by restoring the commented mapping: when the
onFailure handler runs (in the SignupViewModel coroutine block), update uiState
to set isLoading = false and populate profileErrorMessage using
ProfileInitErrorMapper.toMessage(status = status, serverMessage = e.message ?:
e.localizedMessage ?: ""), ensuring the variable status is the correct status in
scope (or derive it before this block) so the UI receives the mapped error text.
- Around line 220-223: The onRegisterComplete function currently calls
viewModelScope.launch { uiState = uiState.copy() } which is a no-op and doesn't
toggle the loading UI; replace that copy call to set the loading flag (e.g.,
uiState = uiState.copy(isLoading = true)) before starting the registration work
inside viewModelScope.launch, perform the registration, then update uiState
again to clear the loading flag and set success/error state as appropriate (use
the uiState property and onRegisterComplete method to locate where to set
isLoading true/false).
- Around line 202-212: startAuthTimer() uses a hardcoded 60s fallback and a
while (seconds >= 0) loop which conflicts with onPhoneComplete's 180s initial
value and causes one extra tick at 0; change startAuthTimer() to use the same
fallback value as onPhoneComplete (use 180 when uiState.authTimerSeconds <= 0)
and adjust the loop to run while (seconds > 0), updating uiState each tick and
after the loop set uiState = uiState.copy(authTimerSeconds = 0) to ensure the
timer stops exactly at 0; reference symbols: startAuthTimer(),
uiState.authTimerSeconds, onPhoneComplete, authTimerJob, viewModelScope.launch.
- Line 37: Replace the hardcoded upper bound in the years list initialization
(val years in SignupViewModel) so it uses the runtime current year instead of
2024; specifically, compute the upper bound from the Java time API (e.g.,
LocalDate.now().year) and build the range
(1940..currentYear).toList().reversed(), and add the necessary import
(java.time.LocalDate) so the code compiles.

---

Nitpick comments:
In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt`:
- Around line 30-163: Extract the shared UI (label, spacing, Row, placeholder
styling, colors, action slot and TextField styling) into a new base composable
(e.g., BaseMaterialUnderlineField) and refactor MaterialUnderlineTextField and
MaterialUnderlineNumberTextField to call it; keep unique behavior by passing an
input transformation lambda (e.g., onRawValueChange: (String) -> Unit) and
optional KeyboardOptions/keyboardType and maxLength params into the base
composable so MaterialUnderlineNumberTextField supplies a digit-filtering
wrapper (the current onValueChange logic) plus
KeyboardOptions(KeyboardType.Number) while MaterialUnderlineTextField supplies a
passthrough onValueChange and default IME options; update uses of label,
placeholder, enabled, actionContent, textStyle, and colors to come from the
single base composable to avoid duplicated layout and styling.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b422e0f and 46ec30b.

📒 Files selected for processing (15)
  • app/src/main/java/com/gildongmu/dduru/data/di/NetworkModule.kt
  • app/src/main/java/com/gildongmu/dduru/data/register/model/OnboardingRegisterRequest.kt
  • app/src/main/java/com/gildongmu/dduru/data/register/repository/OnboardingRegisterRepositoryImpl.kt
  • app/src/main/java/com/gildongmu/dduru/domain/register/OnboardingRegisterRepository.kt
  • app/src/main/java/com/gildongmu/dduru/domain/repository/AuthRepository.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/AuthCodeInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BirthInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/NicknameInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/state/SignupUiState.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/util/ValidationMessageMapper.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/gildongmu/dduru/domain/repository/AuthRepository.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt
  • app/src/main/java/com/gildongmu/dduru/data/register/model/OnboardingRegisterRequest.kt

Comment on lines +50 to +53
return OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.addInterceptor(authInterceptor)
.addInterceptor(logging)
Copy link

@coderabbitai coderabbitai bot Feb 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) OkHttp 재시도 설정 사용 위치 확인"
rg -n --type=kt -C2 'retryOnConnectionFailure\s*\('

echo
echo "2) 비멱등 Retrofit 엔드포인트(`@POST/`@PUT/@PATCH/@DELETE) 확인"
rg -n --type=kt -C2 '@(POST|PUT|PATCH|DELETE)\b'

echo
echo "3) Idempotency-Key 사용 여부 확인"
rg -n --type=kt -C2 'Idempotency-Key|@Header\("Idempotency-Key"\)'

Repository: ddu-ru/ddu-ru-android

Length of output: 134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) OkHttp 재시도 설정 사용 위치 확인"
rg -n 'retryOnConnectionFailure\s*\(' --glob '*.kt' -C2

echo
echo "2) 비멱등 Retrofit 엔드포인트(`@POST/`@PUT/@PATCH/@DELETE) 확인"
rg -n '@(POST|PUT|PATCH|DELETE)\b' --glob '*.kt' -C2

echo
echo "3) Idempotency-Key 사용 여부 확인"
rg -n 'Idempotency-Key|@Header\("Idempotency-Key"\)' --glob '*.kt' -C2

echo
echo "4) NetworkModule.kt 파일 내용 확인"
find . -name 'NetworkModule.kt' -exec cat -n {} \;

Repository: ddu-ru/ddu-ru-android

Length of output: 4087


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Repository 재시도 전략 확인 (retry, retryWhen 등)"
rg -n 'retry|retryWhen|catch|onFailure' --glob '*.kt' app/src/main/java/com/gildongmu/dduru/data/repository/ -A3

echo
echo "2) AuthRepository 구현 확인"
find . -name '*AuthRepository*.kt' -exec cat -n {} \;

echo
echo "3) PhoneVerification 관련 재시도 처리"
find . -path '*/phone/*' -name '*.kt' -exec cat -n {} \;

Repository: ddu-ru/ddu-ru-android

Length of output: 221


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) 디렉토리 구조 확인"
find . -type d -name '*repository*' | head -20

echo
echo "2) Repository 인터페이스/구현 찾기"
find . -name '*Repository*.kt' -type f | grep -v 'node_modules\|build'

echo
echo "3) AuthService 호출 위치 확인"
rg -n 'AuthService|loginWithKakao|loginWithGoogle' --glob '*.kt' -B2 -A2 | head -50

Repository: ddu-ru/ddu-ru-android

Length of output: 6528


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== 1) AuthRepositoryImpl.kt 파일 전체 확인 ==="
cat -n app/src/main/java/com/gildongmu/dduru/data/repositoryimpl/AuthRepositoryImpl.kt

echo
echo "=== 2) PhoneVerificationRepositoryImpl.kt 파일 전체 확인 ==="
cat -n app/src/main/java/com/gildongmu/dduru/data/phone/repository/PhoneVerificationRepositoryImpl.kt

echo
echo "=== 3) OnboardingRegisterRepositoryImpl.kt 파일 전체 확인 ==="
cat -n app/src/main/java/com/gildongmu/dduru/data/register/repository/OnboardingRegisterRepositoryImpl.kt

echo
echo "=== 4) 전체 codebase에서 retry 관련 키워드 검색 ==="
rg -n 'retry|retryWhen|backoff|exponential' --glob '*.kt' | head -30

Repository: ddu-ru/ddu-ru-android

Length of output: 6869


retryOnConnectionFailure(true)는 비멱등 요청 중복 전송 위험을 야기합니다.

Line 51의 설정으로 인해 POST/PATCH 요청이 연결 복구 시 자동으로 재전송됩니다. 코드베이스의 카카오/구글 로그인, 토큰 갱신, 휴대폰 인증번호 발송 등 중요한 작업들이 모두 @POST/@patch 방식이며, 현재 idempotency 처리(Idempotency-Key 헤더)나 repository 레벨의 재시도 로직이 없어 중복 처리 리스크가 높습니다. 특히 휴대폰 인증번호는 중복 발송 시 사용자가 같은 코드를 두 번 수신하게 되는 문제가 발생할 수 있습니다. 공통 클라이언트에서는 기본 비활성화하고, 필요한 경우 개별 엔드포인트에 서버 idempotency 보장과 함께 재시도 전략을 적용하는 것이 안전합니다.

제안 수정
         return OkHttpClient.Builder()
-            .retryOnConnectionFailure(true)
+            .retryOnConnectionFailure(false)
             .addInterceptor(authInterceptor)
             .addInterceptor(logging)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/gildongmu/dduru/data/di/NetworkModule.kt` around lines
50 - 53, The shared OkHttpClient currently enables automatic retry on connection
failure via retryOnConnectionFailure(true) in the OkHttpClient.Builder; this
risks duplicate non-idempotent POST/PATCH calls (e.g., login, token refresh,
SMS) — change the common client to disable this by removing or setting
retryOnConnectionFailure(false) in the OkHttpClient.Builder that adds
authInterceptor and logging, and instead implement any needed retry logic only
at the call site or a dedicated per-endpoint client with server-side idempotency
(Idempotency-Key) and controlled retry/backoff.

Copy link
Collaborator

Choose a reason for hiding this comment

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

맞아요 만약에 갑자기 네트워크 연결이 끊어져있다면 인증번호가 2번 보내질 수도 있어요 ,,!

Copy link

Choose a reason for hiding this comment

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

@hyeminililo 맞습니다! 정확히 그 시나리오가 문제입니다. OkHttp의 retryOnConnectionFailure(true)"연결이 끊어진 후 복구될 때" 요청을 자동으로 재전송하는데, 이 시점에 서버가 이미 첫 번째 요청을 처리했을 수 있어서 SMS가 중복 발송될 수 있습니다.

아래와 같이 수정하시면 됩니다:

return OkHttpClient.Builder()
    .retryOnConnectionFailure(false) // 비멱등 요청(POST/PATCH) 중복 전송 방지
    .addInterceptor(authInterceptor)
    .addInterceptor(logging)
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(30, TimeUnit.SECONDS)
    .build()

추가로, 향후 멱등성이 보장되어야 하는 API(예: 인증번호 재전송)에는 서버 측에서 Idempotency-Key 헤더를 지원하거나, 클라이언트에서 재시도 버튼 비활성화 같은 UI 레벨 가드를 함께 적용하면 더욱 안전합니다.


🧠 Learnings used
Learnt from: hyeminililo
Repo: GIL-DONG-MU/ddu-ru-android PR: 31
File: app/src/test/java/com/gildongmu/dduru/ExampleUnitTest.kt:1-17
Timestamp: 2026-02-01T17:55:41.210Z
Learning: In all Kotlin source files (*.kt) across the repository, avoid wildcard imports. This project enforces ktlint_standard_no-wildcard-imports via .editorconfig. Always prefer explicit imports (e.g., import org.junit.Assert.assertEquals instead of import org.junit.Assert.*) to improve readability and tooling support.

Learnt from: choi-day
Repo: ddu-ru/ddu-ru-android PR: 30
File: app/src/main/java/com/gildongmu/dduru/data/phone/repository/PhoneVerificationRepositoryImpl.kt:22-26
Timestamp: 2026-02-07T08:54:09.298Z
Learning: Backend API in the ddu-ru-android project uses HTTP 200 for success only; 201, 204, etc. should be treated as non-success. In Kotlin API calls (e.g., PhoneVerificationRepositoryImpl.kt and similar), validate success with response.status == 200 (or HttpStatusCode.OK) and use response.status != 200 as a guard for failure. Apply this consistently across Kotlin files handling HTTP responses.

Comment on lines +21 to +39
return runCatching {
val verificationToken = verificationTokenLocalDataSource.verificationToken.first()
?: throw IllegalStateException("인증 토큰이 없습니다.")

val response = api.initProfile(
OnboardingRegisterRequest(
gender = gender,
phoneNumber = phoneNumber,
birthday = birthday,
verificationToken = verificationToken
)
)

if (response.status != 200) {
throw ExceptionResponse(
status = response.status
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "OnboardingRegisterRepositoryImpl.kt" app/

Repository: ddu-ru/ddu-ru-android

Length of output: 163


🏁 Script executed:

cat -n app/src/main/java/com/gildongmu/dduru/data/register/repository/OnboardingRegisterRepositoryImpl.kt | head -60

Repository: ddu-ru/ddu-ru-android

Length of output: 1852


🏁 Script executed:

rg -n "suspend\s+fun.*Result<" app/src/main/java/com/gildongmu/dduru/data --type=kotlin -A 5 | grep -E "(suspend fun|runCatching)" | head -20

Repository: ddu-ru/ddu-ru-android

Length of output: 694


🏁 Script executed:

rg -n "runCatching\s*\{" app/src/main/java/com/gildongmu/dduru/data --type=kotlin -B 3 | head -30

Repository: ddu-ru/ddu-ru-android

Length of output: 1629


🏁 Script executed:

cat -n app/src/main/java/com/gildongmu/dduru/data/phone/repository/PhoneVerificationRepositoryImpl.kt | head -45

Repository: ddu-ru/ddu-ru-android

Length of output: 2061


🏁 Script executed:

rg -n "runCatching" app/src/main/java/com/gildongmu/dduru --type=kotlin | wc -l

Repository: ddu-ru/ddu-ru-android

Length of output: 66


🏁 Script executed:

rg -B5 "runCatching\s*\{" app/src/main/java/com/gildongmu/dduru --type=kotlin -c

Repository: ddu-ru/ddu-ru-android

Length of output: 262


🏁 Script executed:

rg "CancellationException" app/src/main/java/com/gildongmu/dduru --type=kotlin

Repository: ddu-ru/ddu-ru-android

Length of output: 47


🏁 Script executed:

cat -n app/src/main/java/com/gildongmu/dduru/data/register/repository/OnboardingRegisterRepositoryImpl.kt

Repository: ddu-ru/ddu-ru-android

Length of output: 1852


runCatching에서 코루틴 취소 예외가 억제됩니다

Line 21의 runCatchingCancellationException까지 Result.failure로 감싸기 때문에 취소 신호가 상위 호출자에게 전파되지 않습니다. 이는 코루틴의 취소 의도를 무효화하며 리소스 누수를 초래할 수 있습니다. suspend 함수에서 Result<T> 반환이 필요할 경우 취소 예외를 명시적으로 재던지도록 처리하세요.

수정 예시
 import javax.inject.Inject
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.flow.first
@@
-    ): Result<Unit> {
-        return runCatching {
+    ): Result<Unit> = try {
             val verificationToken = verificationTokenLocalDataSource.verificationToken.first()
                 ?: throw IllegalStateException("인증 토큰이 없습니다.")
@@
             if (response.status != 200) {
                 throw ExceptionResponse(
                     status = response.status
                 )
             }
-        }
-    }
+        Result.success(Unit)
+    } catch (e: CancellationException) {
+        throw e
+    } catch (t: Throwable) {
+        Result.failure(t)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/data/register/repository/OnboardingRegisterRepositoryImpl.kt`
around lines 21 - 39, The current use of runCatching in
OnboardingRegisterRepositoryImpl (around the call that reads
verificationTokenLocalDataSource.verificationToken and calls api.initProfile
with OnboardingRegisterRequest) will capture CancellationException and prevent
coroutine cancellation from propagating; change the error handling so
CancellationException is rethrown instead of being wrapped: replace the
runCatching block with a try/catch that catches CancellationException and
rethrows it, and converts other exceptions into a Result.failure (or if you keep
runCatching, inspect the caught exception and rethrow if it is a
CancellationException before returning Result.failure).

Comment on lines +101 to +125
maxLength: Int? = null,
actionContent: (@Composable (() -> Unit))? = null
) {
Column {
val labelColor = if (enabled) Color.Black else Color.Black.copy(alpha = 0.4f)
val placeholderColor = Color.Black.copy(alpha = if (enabled) 0.4f else 0.2f)

Text(
text = label,
style = DduruTheme.typography.BodyMedium,
color = labelColor
)

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

Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = value,
onValueChange = { input ->
val digitsOnly = input.filter { it.isDigit() }
val limited = maxLength?.let { digitsOnly.take(it) } ?: digitsOnly
onValueChange(limited)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt | head -130

Repository: ddu-ru/ddu-ru-android

Length of output: 5672


🏁 Script executed:

python3 << 'EOF'
# Kotlin의 take() 함수 동작 검증
# Kotlin stdlib에서 take(n)은 n이 음수일 때 어떻게 처리하는지 확인

# Kotlin 소스코드 문서에 따르면:
# public fun <T> Iterable<T>.take(n: Int): List<T>
# Returns a list containing first n elements.
# 
# 특히 CharSequence (String 포함)의 take():
# public fun CharSequence.take(n: Int): CharSequence
# 
# 동작:
# - n < 0: 일반적으로 IllegalArgumentException이 발생할 수 있음
# - n == 0: 빈 시퀀스/문자열 반환
# - n > length: 전체 시퀀스 반환

# 실제 Kotlin 구현을 확인해보자
print("Kotlin take() function behavior:")
print("- When n < 0: typically throws IllegalArgumentException")
print("- When n == 0: returns empty sequence")
print("- When n > length: returns full sequence")
print("\nThe issue in the code:")
print("maxLength?.let { digitsOnly.take(it) }")
print("If maxLength is negative, take(negative_value) will throw exception")
EOF

Repository: ddu-ru/ddu-ru-android

Length of output: 363


🌐 Web query:

Kotlin String take() function negative value behavior

💡 Result:

In Kotlin, take(n) on String (and CharSequence) throws IllegalArgumentException when n is negative. (kotlinlang.org)

Example:

"abc".take(-1) // throws IllegalArgumentException

Citations:


🏁 Script executed:

rg "MaterialUnderlineNumberTextField" --type kt -B 2 -A 8

Repository: ddu-ru/ddu-ru-android

Length of output: 91


🏁 Script executed:

rg "MaterialUnderlineNumberTextField" -B 2 -A 8

Repository: ddu-ru/ddu-ru-android

Length of output: 4175


maxLength 음수 입력 시 런타임 크래시 방지 필요

Line 124의 digitsOnly.take(it)maxLength가 음수일 때 IllegalArgumentException을 발생시킵니다. 파라미터가 공개 API이므로 음수 입력에 대비한 방어 로직이 필요합니다.

수정 제안
                 onValueChange = { input ->
                     val digitsOnly = input.filter { it.isDigit() }
-                    val limited = maxLength?.let { digitsOnly.take(it) } ?: digitsOnly
+                    val limited = maxLength
+                        ?.takeIf { it >= 0 }
+                        ?.let { digitsOnly.take(it) }
+                        ?: digitsOnly
                     onValueChange(limited)
                 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt`
around lines 101 - 125, The BaseLineInput composable currently calls
digitsOnly.take(maxLength) which will throw if maxLength is negative; modify the
onValueChange handling in BaseLineInput so maxLength is validated/normalized
before use (e.g., if maxLength is null or < 0 treat as no limit or coerce to 0)
and then apply take on the normalized non-negative limit (reference symbols:
BaseLineInput, maxLength, digitsOnly.take, onValueChange/lambda) to prevent
IllegalArgumentException for negative inputs.

Comment on lines +55 to +67
val stepContents = listOf(
SignupStep.BIRTH to ("생년월일을 알려주세요" to "정확한 나이를 확인해요"),
SignupStep.GENDER to ("성별을 선택해주세요" to "추천에 사용돼요"),
SignupStep.PHONE to ("전화번호를 입력해주세요" to "본인 확인을 위해 필요해요"),
SignupStep.AUTH to ("인증번호를 입력해주세요" to "문자로 받은 번호를 입력하세요"),
SignupStep.DONE to ("가입이 완료됐어요" to "이제 두루두루 즐겨봐요!")
)
val totalSteps = stepContents.size
val currentIndex = stepContents.indexOfFirst { it.first == step }.let {
if (it == -1) totalSteps - 1 else it
}
val (title, desc) = stepContents.getOrElse(currentIndex) { stepContents.last() }.second

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

AUTH_VERIFICATION 단계가 누락되어 안내 문구가 잘못 표시됩니다

Line [55]-Line [67]에서 SignupStep.AUTH_VERIFICATION 매핑이 없어 해당 단계 진입 시 DONE 문구로 fallback 됩니다. 단계 안내/STEP 카운트가 실제 흐름과 어긋납니다.

수정 예시
     val stepContents = listOf(
         SignupStep.BIRTH to ("생년월일을 알려주세요" to "정확한 나이를 확인해요"),
         SignupStep.GENDER to ("성별을 선택해주세요" to "추천에 사용돼요"),
         SignupStep.PHONE to ("전화번호를 입력해주세요" to "본인 확인을 위해 필요해요"),
         SignupStep.AUTH to ("인증번호를 입력해주세요" to "문자로 받은 번호를 입력하세요"),
+        SignupStep.AUTH_VERIFICATION to ("인증이 완료됐어요" to "가입을 마무리해볼게요"),
         SignupStep.DONE to ("가입이 완료됐어요" to "이제 두루두루 즐겨봐요!")
     )
📝 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 stepContents = listOf(
SignupStep.BIRTH to ("생년월일을 알려주세요" to "정확한 나이를 확인해요"),
SignupStep.GENDER to ("성별을 선택해주세요" to "추천에 사용돼요"),
SignupStep.PHONE to ("전화번호를 입력해주세요" to "본인 확인을 위해 필요해요"),
SignupStep.AUTH to ("인증번호를 입력해주세요" to "문자로 받은 번호를 입력하세요"),
SignupStep.DONE to ("가입이 완료됐어요" to "이제 두루두루 즐겨봐요!")
)
val totalSteps = stepContents.size
val currentIndex = stepContents.indexOfFirst { it.first == step }.let {
if (it == -1) totalSteps - 1 else it
}
val (title, desc) = stepContents.getOrElse(currentIndex) { stepContents.last() }.second
val stepContents = listOf(
SignupStep.BIRTH to ("생년월일을 알려주세요" to "정확한 나이를 확인해요"),
SignupStep.GENDER to ("성별을 선택해주세요" to "추천에 사용돼요"),
SignupStep.PHONE to ("전화번호를 입력해주세요" to "본인 확인을 위해 필요해요"),
SignupStep.AUTH to ("인증번호를 입력해주세요" to "문자로 받은 번호를 입력하세요"),
SignupStep.AUTH_VERIFICATION to ("인증이 완료됐어요" to "가입을 마무리해볼게요"),
SignupStep.DONE to ("가입이 완료됐어요" to "이제 두루두루 즐겨봐요!")
)
val totalSteps = stepContents.size
val currentIndex = stepContents.indexOfFirst { it.first == step }.let {
if (it == -1) totalSteps - 1 else it
}
val (title, desc) = stepContents.getOrElse(currentIndex) { stepContents.last() }.second
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt`
around lines 55 - 67, The step mapping in GreetingMessageCard.kt is missing
SignupStep.AUTH_VERIFICATION so when step == SignupStep.AUTH_VERIFICATION
currentIndex falls back to DONE; fix this by adding an entry for
SignupStep.AUTH_VERIFICATION into the stepContents list (e.g., place it after
SignupStep.AUTH with a proper title and description string), leaving the
existing currentIndex/totalSteps logic intact so the STEP count and displayed
(title, desc) come from the new mapping.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt (1)

55-67: ⚠️ Potential issue | 🟠 Major

AUTH_VERIFICATION 단계 누락으로 안내 문구가 잘못 노출됩니다.

Line [55]-Line [67]에서 SignupStep.AUTH_VERIFICATION 매핑이 빠져 있어, 해당 단계 진입 시 DONE으로 fallback 됩니다. 이로 인해 제목/설명과 STEP 표시가 실제 단계와 어긋납니다.

수정 예시
     val stepContents = listOf(
         SignupStep.BIRTH to ("생년월일을 알려주세요" to "정확한 나이를 확인해요"),
         SignupStep.GENDER to ("성별을 선택해주세요" to "추천에 사용돼요"),
         SignupStep.PHONE to ("전화번호를 입력해주세요" to "본인 확인을 위해 필요해요"),
         SignupStep.AUTH to ("인증번호를 입력해주세요" to "문자로 받은 번호를 입력하세요"),
+        SignupStep.AUTH_VERIFICATION to ("인증이 완료됐어요" to "가입을 마무리해볼게요"),
         SignupStep.DONE to ("가입이 완료됐어요" to "이제 두루두루 즐겨봐요!")
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt`
around lines 55 - 67, The stepContents list in GreetingMessageCard.kt is missing
a mapping for SignupStep.AUTH_VERIFICATION, causing that step to fall back to
DONE; add an entry for SignupStep.AUTH_VERIFICATION in the stepContents list
(with the correct title/description for the verification step) so that
currentIndex, totalSteps and the getOrElse/select logic (stepContents,
currentIndex, and the tuple deconstruction of title/desc) correctly reflect the
AUTH_VERIFICATION state and no longer fall back to DONE.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt (2)

19-37: ⚠️ Potential issue | 🟡 Minor

enabled 파라미터가 전달되지 않아 비활성화가 동작하지 않습니다.

GenderSegmentenabled를 받지만 실제 버튼에 전달하지 않아 비활성 상태 제어가 깨집니다.

수정 제안
         DduruButton(
             modifier = Modifier.weight(1f),
             text = "남자",
-//            enabled = enabled,
+            enabled = enabled,
             onClick = { onSelect(Gender.MALE) },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt`
around lines 19 - 37, The GenderInput component receives the enabled parameter
but never forwards it to the buttons, so the disabled state never takes effect;
update both DduruButton instances used in GenderInput (the ones with text "남자"
and "여자") to include enabled = enabled so the buttons respect the parent enabled
state, and ensure any visual disabled styling still comes from the existing
unselectedColors/selectedColors logic when enabled is false.

52-70: ⚠️ Potential issue | 🔴 Critical

여성 선택 버튼이 주석 처리되어 성별 선택 기능이 사실상 한 방향으로 고정됩니다.

현재 UI에서 "남자"만 선택 가능하므로, 회원가입 성별 입력이 왜곡됩니다. 이 상태로는 Gender.FEMALE 경로가 사용자 입력으로 도달 불가합니다.

수정 제안
-        val isFemaleSelected = selected == Gender.FEMALE
-//        DduruButton(
-//            modifier = Modifier.weight(1f),
-//            text = "여자",
-//            enabled = enabled,
-//            onClick = { onSelect(Gender.FEMALE) },
-//            colors = if (isFemaleSelected) {
-//                ButtonDefaults.buttonColors(
-//                    containerColor = DduruTheme.colors.primary,
-//                    contentColor = Color.White,
-//                    disabledContainerColor = DduruTheme.colors.primary.copy(alpha = 0.3f),
-//                    disabledContentColor = Color.White.copy(alpha = 0.7f)
-//                )
-//            } else {
-//                unselectedColors
-//            },
-//            borderColor = if (isFemaleSelected) DduruTheme.colors.WhiteAlpha30 else DduruTheme.colors.Gray200,
-//            cornerSize = 16.dp
-//        )
+        val isFemaleSelected = selected == Gender.FEMALE
+        DduruButton(
+            modifier = Modifier.weight(1f),
+            text = "여자",
+            enabled = enabled,
+            onClick = { onSelect(Gender.FEMALE) },
+            colors = if (isFemaleSelected) {
+                ButtonDefaults.buttonColors(
+                    containerColor = DduruTheme.colors.primary,
+                    contentColor = Color.White,
+                    disabledContainerColor = DduruTheme.colors.primary.copy(alpha = 0.3f),
+                    disabledContentColor = Color.White.copy(alpha = 0.7f)
+                )
+            } else {
+                unselectedColors
+            },
+            borderColor = if (isFemaleSelected) DduruTheme.colors.WhiteAlpha30 else DduruTheme.colors.Gray200,
+            cornerSize = 16.dp
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt`
around lines 52 - 70, The female selection DduruButton in GenderInput (the
isFemaleSelected / onSelect(Gender.FEMALE) block) was commented out, leaving
only the male path reachable; restore/uncomment the DduruButton for the female
option so Gender.FEMALE can be selected, ensuring it uses the same props as the
male button (modifier = Modifier.weight(1f), enabled = enabled, onClick = {
onSelect(Gender.FEMALE) }, colors conditional on isFemaleSelected, borderColor
conditional, cornerSize = 16.dp) and that isFemaleSelected is used to drive its
visual state and enabled/disabled styling.
🧹 Nitpick comments (3)
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/YearDropdownField.kt (1)

74-77: 접근성을 위한 contentDescription 제공 권장

드롭다운 아이콘에 contentDescription = null이 설정되어 있어 스크린 리더 사용자가 해당 요소의 기능을 인식하기 어렵습니다. 드롭다운 토글 기능을 설명하는 문자열을 제공하세요.

♿ 접근성 개선 제안
                    Icon(
                        imageVector = Icons.Default.ArrowDropDown,
-                       contentDescription = null
+                       contentDescription = "드롭다운 열기"
                    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/YearDropdownField.kt`
around lines 74 - 77, The Icon in YearDropdownField currently sets
contentDescription = null which prevents screen readers from conveying its
purpose; update the Icon(...) call to provide a descriptive, localised string
(e.g., use stringResource(R.string.toggle_year_dropdown) or a suitable string
from your resources) instead of null, and add the corresponding string resource
(toggle_year_dropdown) to strings.xml so the dropdown toggle is announced to
assistive technologies.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt (1)

12-23: 빈 문자열 방어를 추가하면 불필요한 여백 렌더링을 줄일 수 있습니다.

호출부 실수로 text가 비어 들어와도 조용히 빠져나가게 하면 UI 안정성이 더 좋아집니다.

제안 수정안
 `@Composable`
 fun ValidationMessage(
     modifier: Modifier = Modifier,
     text: String,
     color: Color = DduruTheme.colors.ValidationRed
 ) {
+    if (text.isBlank()) return
+
     Text(
         text = text,
         modifier = modifier.padding(top = 4.dp),
         style = DduruTheme.typography.BodySmall,
         color = color
     )
 }

As per coding guidelines, "**/*.kt: Jetpack Compose 구조, 상태 관리, recomposition 최적화에 집중".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt`
around lines 12 - 23, ValidationMessage should skip rendering when the provided
text is empty/blank to avoid unnecessary padding; update the ValidationMessage
function to early-return (no UI emitted) if text.isBlank() before calling Text
so that the modifier.padding(top = 4.dp) and Text are not created for empty
strings, preserving composable performance and avoiding extra layout spacing.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt (1)

36-70: 대규모 주석 처리된 UI 분기 코드는 정리하거나 feature flag로 명시해 주세요.

현재 주석 블록이 커서 실제 동작 코드 파악이 어렵습니다. 임시 비활성화가 목적이라면 분기 조건/플래그로 의도를 남기고, 아니면 제거가 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt`
around lines 36 - 70, The large commented UI branches around DduruButton (the
male/female button blocks) make the component hard to read; either remove the
dead code or convert it into an explicit feature flag/parameter so the intent is
clear. Refactor GenderInput to take a boolean/feature flag (e.g.,
useDetailedStyling or enableLegacyGenderStyles) and move the commented styling
branches (the colors/borderColor/cornerSize logic that references
isMaleSelected/isFemaleSelected, selected and onSelect) behind that flag, or
delete the commented blocks entirely if they are no longer needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt`:
- Around line 15-21: The ValidationMessage composable currently renders the Text
with default styling because the style and color tokens are commented out;
restore those tokens or expose them as parameters. In the ValidationMessage
function re-enable the DduruTheme.typography.BodySmall and
DduruTheme.colors.ValidationRed (or add parameters like style: TextStyle =
DduruTheme.typography.BodySmall and color: Color =
DduruTheme.colors.ValidationRed) and pass them into the Text call so the error
message has the intended typography and red color.

---

Duplicate comments:
In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt`:
- Around line 19-37: The GenderInput component receives the enabled parameter
but never forwards it to the buttons, so the disabled state never takes effect;
update both DduruButton instances used in GenderInput (the ones with text "남자"
and "여자") to include enabled = enabled so the buttons respect the parent enabled
state, and ensure any visual disabled styling still comes from the existing
unselectedColors/selectedColors logic when enabled is false.
- Around line 52-70: The female selection DduruButton in GenderInput (the
isFemaleSelected / onSelect(Gender.FEMALE) block) was commented out, leaving
only the male path reachable; restore/uncomment the DduruButton for the female
option so Gender.FEMALE can be selected, ensuring it uses the same props as the
male button (modifier = Modifier.weight(1f), enabled = enabled, onClick = {
onSelect(Gender.FEMALE) }, colors conditional on isFemaleSelected, borderColor
conditional, cornerSize = 16.dp) and that isFemaleSelected is used to drive its
visual state and enabled/disabled styling.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt`:
- Around line 55-67: The stepContents list in GreetingMessageCard.kt is missing
a mapping for SignupStep.AUTH_VERIFICATION, causing that step to fall back to
DONE; add an entry for SignupStep.AUTH_VERIFICATION in the stepContents list
(with the correct title/description for the verification step) so that
currentIndex, totalSteps and the getOrElse/select logic (stepContents,
currentIndex, and the tuple deconstruction of title/desc) correctly reflect the
AUTH_VERIFICATION state and no longer fall back to DONE.

---

Nitpick comments:
In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt`:
- Around line 36-70: The large commented UI branches around DduruButton (the
male/female button blocks) make the component hard to read; either remove the
dead code or convert it into an explicit feature flag/parameter so the intent is
clear. Refactor GenderInput to take a boolean/feature flag (e.g.,
useDetailedStyling or enableLegacyGenderStyles) and move the commented styling
branches (the colors/borderColor/cornerSize logic that references
isMaleSelected/isFemaleSelected, selected and onSelect) behind that flag, or
delete the commented blocks entirely if they are no longer needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt`:
- Around line 12-23: ValidationMessage should skip rendering when the provided
text is empty/blank to avoid unnecessary padding; update the ValidationMessage
function to early-return (no UI emitted) if text.isBlank() before calling Text
so that the modifier.padding(top = 4.dp) and Text are not created for empty
strings, preserving composable performance and avoiding extra layout spacing.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/YearDropdownField.kt`:
- Around line 74-77: The Icon in YearDropdownField currently sets
contentDescription = null which prevents screen readers from conveying its
purpose; update the Icon(...) call to provide a descriptive, localised string
(e.g., use stringResource(R.string.toggle_year_dropdown) or a suitable string
from your resources) instead of null, and add the corresponding string resource
(toggle_year_dropdown) to strings.xml so the dropdown toggle is announced to
assistive technologies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: df60ae6e-8a5e-4f3a-b00f-8ae00245db59

📥 Commits

Reviewing files that changed from the base of the PR and between 46ec30b and fb2fb11.

📒 Files selected for processing (7)
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/AuthCodeInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/YearDropdownField.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/AuthCodeInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt

Comment on lines +15 to +21
// color: Color = DduruTheme.colors.ValidationRed
) {
Text(
text = text,
modifier = modifier.padding(top = 4.dp),
// style = DduruTheme.typography.BodySmall,
// color = color
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

검증 메시지의 스타일/색상이 빠져 UI 의도가 약해집니다.

Line 17-22에서 기본 Text 스타일로만 렌더링되어 에러 메시지로서의 시각적 구분이 약합니다. 주석 처리된 디자인 토큰은 복구하거나 명시적으로 파라미터화하는 편이 좋습니다.

제안 수정안
 `@Composable`
 fun ValidationMessage(
     modifier: Modifier = Modifier,
     text: String,
-//    color: Color = DduruTheme.colors.ValidationRed
+    color: Color = DduruTheme.colors.ValidationRed
 ) {
     Text(
         text = text,
         modifier = modifier.padding(top = 4.dp),
-//        style = DduruTheme.typography.BodySmall,
-//        color = color
+        style = DduruTheme.typography.BodySmall,
+        color = color
     )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt`
around lines 15 - 21, The ValidationMessage composable currently renders the
Text with default styling because the style and color tokens are commented out;
restore those tokens or expose them as parameters. In the ValidationMessage
function re-enable the DduruTheme.typography.BodySmall and
DduruTheme.colors.ValidationRed (or add parameters like style: TextStyle =
DduruTheme.typography.BodySmall and color: Color =
DduruTheme.colors.ValidationRed) and pass them into the Text call so the error
message has the intended typography and red color.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt (1)

40-41: ⚠️ Potential issue | 🟡 Minor

hiltViewModel()로 변경 필요

SignupViewModel@HiltViewModel로 주석 처리되어 있으므로, viewModel() 대신 hiltViewModel()을 사용해야 합니다.

🔧 hiltViewModel 사용
+import androidx.hilt.navigation.compose.hiltViewModel
+
 `@Composable`
 fun SignupScreen(
-    viewModel: SignupViewModel = viewModel()
+    viewModel: SignupViewModel = hiltViewModel()
 ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt`
around lines 40 - 41, The SignupScreen composable currently uses viewModel() as
the default for the SignupViewModel parameter but the view model is annotated
with `@HiltViewModel`; replace viewModel() with hiltViewModel() so Hilt provides
the instance (update the default parameter in SignupScreen to use
hiltViewModel() and add the necessary import for
androidx.hilt.navigation.compose.hiltViewModel if missing), keeping the
parameter type SignupViewModel unchanged.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt (4)

37-37: ⚠️ Potential issue | 🟡 Minor

하드코딩된 연도 범위

연도 범위가 2024로 하드코딩되어 있어 2025년 이후에는 최신 연도를 선택할 수 없습니다.

🔧 동적 연도 범위 적용
+import java.time.Year
+
 val years = (1940..2024).toList().reversed()
+// 다음으로 변경:
+val years = (1940..Year.now().value).toList().reversed()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`
at line 37, The years list in SignupViewModel (val years) is hardcoded to
1940..2024; change it to compute the end year dynamically (e.g., using
java.time.Year.now().value or LocalDate.now().year) so the range always includes
the current year, e.g. (1940..currentYear).toList().reversed(); update the
declaration of years accordingly and ensure imports for java.time are added if
needed.

220-229: ⚠️ Potential issue | 🟡 Minor

불필요한 상태 복사 및 isLoading 미설정

Line 222의 uiState = uiState.copy()는 아무 변경이 없어 불필요합니다. 등록 시작 시 isLoading = true로 설정해야 합니다.

🔧 로딩 상태 설정
 fun onRegisterComplete() {
     viewModelScope.launch {
-        uiState = uiState.copy()
+        uiState = uiState.copy(isLoading = true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`
around lines 220 - 229, In onRegisterComplete() remove the no-op uiState =
uiState.copy() and instead set loading at the start of the registration flow by
updating uiState = uiState.copy(isLoading = true); keep the existing branch that
sets isLoading = false when gender is null; update references to uiState within
the coroutine so the UI shows loading while the registration logic (including
genderValue mapping) runs.

256-260: ⚠️ Potential issue | 🟠 Major

실패 시 에러 메시지 미표시

onFailure 블록에서 isLoading = false만 설정하고 에러 메시지를 사용자에게 표시하지 않습니다. UX 저하로 이어질 수 있습니다.

🐛 에러 처리 구현
 }.onFailure { e ->
+    val status = (e as? ExceptionResponse)?.status
     uiState = uiState.copy(
-        isLoading = false
+        isLoading = false,
+        // TODO: OnboardingRegisterErrorMapper 구현 후 적용
+        // registerErrorMessage = OnboardingRegisterErrorMapper.toMessage(status)
     )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`
around lines 256 - 260, The onFailure block in SignupViewModel (the lambda
passed to .onFailure) only clears isLoading and never surfaces the error to the
UI; update the handler so it sets the uiState with an error field/message (e.g.,
uiState = uiState.copy(isLoading = false, errorMessage = e.message ?: "Unknown
error")) or invokes the existing error-display mechanism in this ViewModel,
referencing the onFailure lambda, uiState and whatever error field/property
(e.g., errorMessage or showError) is used by the UI so users see the failure
reason.

202-212: ⚠️ Potential issue | 🟡 Minor

타이머 로직 불일치

  1. Line 206: seconds <= 0일 때 60초로 설정하지만, onPhoneComplete에서는 180초로 설정합니다.
  2. Line 207: while (seconds >= 0) 조건으로 인해 0초에서 한 번 더 반복 후 -1이 됩니다.
🔧 타이머 로직 수정
 private fun startAuthTimer() {
     authTimerJob?.cancel()
     authTimerJob = viewModelScope.launch {
         var seconds = uiState.authTimerSeconds
-        if (seconds <= 0) seconds = 60
-        while (seconds >= 0) {
+        while (seconds > 0) {
             uiState = uiState.copy(authTimerSeconds = seconds)
             delay(1000L)
             seconds--
         }
+        uiState = uiState.copy(authTimerSeconds = 0)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`
around lines 202 - 212, The timer initialization and loop in startAuthTimer are
inconsistent with onPhoneComplete and allow one extra iteration to go negative;
change the initial reset to use 180 seconds (match onPhoneComplete) by setting
seconds = 180 when uiState.authTimerSeconds <= 0, and change the loop to while
(seconds > 0) so it doesn't run after reaching 0, then after the loop set
uiState = uiState.copy(authTimerSeconds = 0); modify symbols: startAuthTimer,
authTimerJob, uiState.authTimerSeconds, viewModelScope.launch to implement these
adjustments.
🧹 Nitpick comments (9)
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt (1)

26-32: Preview에 DduruTheme 래퍼 추가를 권장합니다.

현재 Preview 함수가 DduruTheme으로 감싸져 있지 않습니다. staticCompositionLocalOf의 기본값 덕분에 동작하지만, 실제 앱 환경과 동일한 테마 컨텍스트에서 미리보기하려면 DduruTheme으로 감싸는 것이 좋습니다.

♻️ 제안 수정안
 `@Preview`(showBackground = true)
 `@Composable`
 fun ValidationMessagePreview() {
+    DduruTheme {
         ValidationMessage(
             text = "닉네임을 입력하세요"
         )
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt`
around lines 26 - 32, The preview function ValidationMessagePreview is not
wrapped in the app theme, so update ValidationMessagePreview to call DduruTheme
{ ValidationMessage(text = "닉네임을 입력하세요") } (ensuring the DduruTheme import is
present) so the Preview renders using the same theme/context as the real app;
keep the existing ValidationMessage composable unchanged.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt (3)

108-113: 접근성을 위해 contentDescription을 제공하세요.

펭귄 이미지의 contentDescriptionnull로 설정되어 있습니다. 스크린 리더 사용자를 위해 적절한 설명을 추가하거나, 순수 장식용 이미지라면 명시적으로 빈 문자열("")과 함께 semantics { invisibleToUser() }를 사용하는 것이 좋습니다.

♻️ contentDescription 추가 예시
         Image(
             painter = painterResource(id = R.drawable.haha),
-            contentDescription = null,
+            contentDescription = "두루 캐릭터",
             modifier = Modifier
                 .size(penguinSize)
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt`
around lines 108 - 113, Update the Image in GreetingMessageCard so it is
accessible: replace the null contentDescription with an appropriate descriptive
string for the penguin (e.g., "penguin illustration") if it conveys meaningful
information, or if it is purely decorative set contentDescription to "" and add
semantics { invisibleToUser() } to the Image's modifier; edit the Image call
within GreetingMessageCard (the painterResource(R.drawable.haha) Image)
accordingly to ensure screen readers behave correctly.

56-62: stepContentsremember로 감싸거나 top-level 상수로 추출하세요.

stepContents 리스트가 composable 내부에서 매 recomposition마다 재생성됩니다. 불변 데이터이므로 remember { ... }로 감싸거나 composable 외부 top-level 상수로 추출하면 불필요한 객체 할당을 방지할 수 있습니다.

♻️ Top-level 상수로 추출 예시
+private val stepContents = listOf(
+    SignupStep.BIRTH to ("생년월일을 알려주세요" to "정확한 나이를 확인해요"),
+    SignupStep.GENDER to ("성별을 선택해주세요" to "추천에 사용돼요"),
+    SignupStep.PHONE to ("전화번호를 입력해주세요" to "본인 확인을 위해 필요해요"),
+    SignupStep.AUTH to ("인증번호를 입력해주세요" to "문자로 받은 번호를 입력하세요"),
+    SignupStep.AUTH_VERIFICATION to ("인증이 완료됐어요" to "가입을 마무리해볼게요"),
+    SignupStep.DONE to ("가입이 완료됐어요" to "이제 두루두루 즐겨봐요!")
+)
+
 `@Composable`
 fun GreetingMessageCard(
     modifier: Modifier = Modifier,
     step: SignupStep
 ) {
-    val stepContents = listOf(
-        SignupStep.BIRTH to ("생년월일을 알려주세요" to "정확한 나이를 확인해요"),
-        SignupStep.GENDER to ("성별을 선택해주세요" to "추천에 사용돼요"),
-        SignupStep.PHONE to ("전화번호를 입력해주세요" to "본인 확인을 위해 필요해요"),
-        SignupStep.AUTH to ("인증번호를 입력해주세요" to "문자로 받은 번호를 입력하세요"),
-        SignupStep.DONE to ("가입이 완료됐어요" to "이제 두루두루 즐겨봐요!")
-    )
     val totalSteps = stepContents.size
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt`
around lines 56 - 62, The stepContents list in GreetingMessageCard is recreated
on every recomposition; wrap its initialization in remember { ... } inside the
composable or move it to a top-level immutable constant to avoid repeated
allocations. Locate the stepContents declaration (the list of
SignupStep.BIRTH/GENDER/PHONE/AUTH/DONE pairs) and either change it to val
stepContents = remember { listOf(...) } inside the GreetingMessageCard
composable or extract the list to a top-level val (e.g., STEP_CONTENTS) so the
list is allocated once and reused.

34-34: 하드코딩된 색상 대신 테마 컬러 사용을 권장합니다.

Color(0xFFBBD8FF)가 직접 하드코딩되어 있습니다. DduruTheme.colors에서 적절한 색상을 사용하거나, 없다면 테마에 추가하여 일관성을 유지하고 다크모드 등 테마 변경에 대응할 수 있도록 하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt`
at line 34, The badgeBackground value in GreetingMessageCard.kt is hardcoded as
Color(0xFFBBD8FF); replace it with a themed color from DduruTheme.colors (or add
a new color token to the theme and use that) so the component responds to theme
changes and dark mode; update references in GreetingMessageCard
(badgeBackground) to use the new DduruTheme.colors.<appropriateColorName> and,
if necessary, add that color entry to the theme providers and their light/dark
palettes.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt (1)

32-36: 들여쓰기 불일치

enabledColors의 if-else 블록에서 들여쓰기가 일관되지 않습니다. Line 35의 whiteEnabledColors가 다른 브랜치와 맞지 않습니다.

♻️ 들여쓰기 수정
             enabledColors = if (maleSelected) {
                 filledEnabledColors
             } else {
-            whiteEnabledColors
-        },
+                whiteEnabledColors
+            },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt`
around lines 32 - 36, The if-else for enabledColors in GenderInput.kt has
inconsistent indentation causing `whiteEnabledColors` to be misaligned; open the
enabledColors assignment inside the GenderInput composable (the if
(maleSelected) { filledEnabledColors } else { whiteEnabledColors }, block) and
reformat so both branches use the same indentation and brace alignment—place
`whiteEnabledColors` inside the else block aligned with `filledEnabledColors`
and ensure the trailing comma and braces match the surrounding code style.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt (3)

115-119: AnimatedVisibilityvisible = true 고정

visible이 항상 true로 설정되어 있어 실제 visibility 토글이 없습니다. 이 경우 AnimatedVisibility 대신 animateItem()만으로도 충분할 수 있습니다. 의도적인 진입 애니메이션이라면 현재 구조도 동작합니다.

♻️ 간소화 제안
-                androidx.compose.animation.AnimatedVisibility(
-                    visible = true,
-                    enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(),
-                    modifier = Modifier.animateItem()
-                ) {
+                Column(modifier = Modifier.animateItem()) {
                     when (step) {
                         // ... content ...
                     }
-                }
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt`
around lines 115 - 119, The AnimatedVisibility wrapper in SignupScreen.kt uses
visible = true permanently (in the block containing
androidx.compose.animation.AnimatedVisibility and Modifier.animateItem()), so it
provides no toggle — either remove the AnimatedVisibility and keep
Modifier.animateItem() for a simple entrance animation, or change visible to the
actual visibility state (e.g., a Boolean from view state or local composable
state) to enable show/hide animations; update the code around AnimatedVisibility
and animateItem() accordingly so visibility is driven by the intended state
rather than a hardcoded true.

81-86: formatTimer 함수 위치

Composable 내부의 로컬 함수로 정의되어 있습니다. 상태를 캡처하지 않으므로 현재 위치도 괜찮지만, 재사용성을 위해 파일 레벨 또는 유틸리티로 분리하는 것도 고려해 볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt`
around lines 81 - 86, The formatTimer function is defined as a local
composable-internal function but does not capture state; extract it to a
top-level function (e.g., move formatTimer(seconds: Int): String out of
SignupScreen composable) or into a shared util object so it’s reusable across
files; update any callers in SignupScreen to reference the new top-level/util
function and keep the implementation identical (using safeSeconds, minutes, secs
and String.format with Locale.getDefault()) to preserve behavior.

176-185: 버튼 활성화 조건과 ViewModel 검증 불일치

  • Line 179: state.phoneNumber.length >= 10으로 버튼 활성화
  • Line 184: state.authCode.isNotEmpty()로 버튼 활성화

하지만 ViewModel의 PhoneValidatorAuthCodeValidator에서 더 정교한 검증을 수행합니다. 버튼 활성화 조건을 uiState의 validation 결과(phoneValidation, authCodeValidation)와 동기화하면 일관된 UX를 제공할 수 있습니다.

♻️ 검증 상태 활용
 SignupStep.PHONE -> Triple(
     "인증번호 전송",
-    state.phoneNumber.length >= 10,
+    state.phoneValidation == PhoneValidationResult.VALID || state.phoneNumber.length >= 10,
     viewModel::onPhoneComplete
 )

또는 ViewModel에서 onPhoneChange 시 실시간 검증을 수행하고 phoneValidation 상태를 업데이트하는 방식을 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt`
around lines 176 - 185, The button enablement logic in SignupScreen (the when
block that sets buttonText, buttonEnabled, onButtonClick) uses raw checks
(state.phoneNumber.length >= 10 and state.authCode.isNotEmpty()) which are
inconsistent with the ViewModel validators; change buttonEnabled to use the
uiState validation results (e.g., uiState.phoneValidation.isValid and
uiState.authCodeValidation.isValid or equivalent flags) so the UI enablement
matches PhoneValidator and AuthCodeValidator, and ensure the onButtonClick
handlers (viewModel::onPhoneComplete, viewModel::onAuthComplete) remain
unchanged.
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt (1)

128-157: API 호출 중 로딩 상태 미설정

sendVerificationCode 호출 전후로 isLoading 상태가 설정되지 않아 사용자에게 진행 중임을 알리지 못합니다.

♻️ 로딩 상태 추가
     viewModelScope.launch {
-        uiState = uiState.copy(phoneErrorMessage = "")
+        uiState = uiState.copy(phoneErrorMessage = "", isLoading = true)

         phoneVerificationRepository.sendVerificationCode(uiState.phoneNumber)
             .onSuccess {
                 // ... existing code ...
                 uiState = uiState.copy(
+                    isLoading = false,
                     phoneErrorMessage = "",
                     // ... rest of fields ...
                 )
             }
             .onFailure { e ->
                 val status = (e as? ExceptionResponse)?.status
                 uiState = uiState.copy(
+                    isLoading = false,
                     phoneErrorMessage = PhoneVerificationErrorMapper.sendErrorMessage(
                         status = status
                     )
                 )
             }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`
around lines 128 - 157, Before calling
phoneVerificationRepository.sendVerificationCode in the viewModelScope.launch
block, set uiState.isLoading = true and then reset uiState.isLoading = false in
both the onSuccess and onFailure branches so the UI shows a loading state during
the API call; update the code in the SignupViewModel around
viewModelScope.launch / phoneVerificationRepository.sendVerificationCode to set
isLoading true immediately before the call and ensure both the success branch
that updates completedItems/step/startAuthTimer and the failure branch that sets
phoneErrorMessage also clear isLoading so it never remains stuck.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt`:
- Around line 163-169: The UI currently only renders Text for SignupStep.DONE
and never triggers navigation; update the composable to detect the DONE state
and invoke a navigation callback (or emit a one-shot event from the ViewModel).
Specifically, when SignupStep.DONE is reached in the screen that reads the
ViewModel state, use a LaunchedEffect(key1 = viewModel.signupStep) to call a
passed-in onSignupComplete() lambda or consume a SharedFlow/Channel event from
the ViewModel (emit from ViewModel on success) so navigation is performed
exactly once; ensure you clear/mark the event to avoid repeated navigation.

---

Duplicate comments:
In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt`:
- Around line 40-41: The SignupScreen composable currently uses viewModel() as
the default for the SignupViewModel parameter but the view model is annotated
with `@HiltViewModel`; replace viewModel() with hiltViewModel() so Hilt provides
the instance (update the default parameter in SignupScreen to use
hiltViewModel() and add the necessary import for
androidx.hilt.navigation.compose.hiltViewModel if missing), keeping the
parameter type SignupViewModel unchanged.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`:
- Line 37: The years list in SignupViewModel (val years) is hardcoded to
1940..2024; change it to compute the end year dynamically (e.g., using
java.time.Year.now().value or LocalDate.now().year) so the range always includes
the current year, e.g. (1940..currentYear).toList().reversed(); update the
declaration of years accordingly and ensure imports for java.time are added if
needed.
- Around line 220-229: In onRegisterComplete() remove the no-op uiState =
uiState.copy() and instead set loading at the start of the registration flow by
updating uiState = uiState.copy(isLoading = true); keep the existing branch that
sets isLoading = false when gender is null; update references to uiState within
the coroutine so the UI shows loading while the registration logic (including
genderValue mapping) runs.
- Around line 256-260: The onFailure block in SignupViewModel (the lambda passed
to .onFailure) only clears isLoading and never surfaces the error to the UI;
update the handler so it sets the uiState with an error field/message (e.g.,
uiState = uiState.copy(isLoading = false, errorMessage = e.message ?: "Unknown
error")) or invokes the existing error-display mechanism in this ViewModel,
referencing the onFailure lambda, uiState and whatever error field/property
(e.g., errorMessage or showError) is used by the UI so users see the failure
reason.
- Around line 202-212: The timer initialization and loop in startAuthTimer are
inconsistent with onPhoneComplete and allow one extra iteration to go negative;
change the initial reset to use 180 seconds (match onPhoneComplete) by setting
seconds = 180 when uiState.authTimerSeconds <= 0, and change the loop to while
(seconds > 0) so it doesn't run after reaching 0, then after the loop set
uiState = uiState.copy(authTimerSeconds = 0); modify symbols: startAuthTimer,
authTimerJob, uiState.authTimerSeconds, viewModelScope.launch to implement these
adjustments.

---

Nitpick comments:
In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt`:
- Around line 32-36: The if-else for enabledColors in GenderInput.kt has
inconsistent indentation causing `whiteEnabledColors` to be misaligned; open the
enabledColors assignment inside the GenderInput composable (the if
(maleSelected) { filledEnabledColors } else { whiteEnabledColors }, block) and
reformat so both branches use the same indentation and brace alignment—place
`whiteEnabledColors` inside the else block aligned with `filledEnabledColors`
and ensure the trailing comma and braces match the surrounding code style.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt`:
- Around line 108-113: Update the Image in GreetingMessageCard so it is
accessible: replace the null contentDescription with an appropriate descriptive
string for the penguin (e.g., "penguin illustration") if it conveys meaningful
information, or if it is purely decorative set contentDescription to "" and add
semantics { invisibleToUser() } to the Image's modifier; edit the Image call
within GreetingMessageCard (the painterResource(R.drawable.haha) Image)
accordingly to ensure screen readers behave correctly.
- Around line 56-62: The stepContents list in GreetingMessageCard is recreated
on every recomposition; wrap its initialization in remember { ... } inside the
composable or move it to a top-level immutable constant to avoid repeated
allocations. Locate the stepContents declaration (the list of
SignupStep.BIRTH/GENDER/PHONE/AUTH/DONE pairs) and either change it to val
stepContents = remember { listOf(...) } inside the GreetingMessageCard
composable or extract the list to a top-level val (e.g., STEP_CONTENTS) so the
list is allocated once and reused.
- Line 34: The badgeBackground value in GreetingMessageCard.kt is hardcoded as
Color(0xFFBBD8FF); replace it with a themed color from DduruTheme.colors (or add
a new color token to the theme and use that) so the component responds to theme
changes and dark mode; update references in GreetingMessageCard
(badgeBackground) to use the new DduruTheme.colors.<appropriateColorName> and,
if necessary, add that color entry to the theme providers and their light/dark
palettes.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt`:
- Around line 26-32: The preview function ValidationMessagePreview is not
wrapped in the app theme, so update ValidationMessagePreview to call DduruTheme
{ ValidationMessage(text = "닉네임을 입력하세요") } (ensuring the DduruTheme import is
present) so the Preview renders using the same theme/context as the real app;
keep the existing ValidationMessage composable unchanged.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt`:
- Around line 115-119: The AnimatedVisibility wrapper in SignupScreen.kt uses
visible = true permanently (in the block containing
androidx.compose.animation.AnimatedVisibility and Modifier.animateItem()), so it
provides no toggle — either remove the AnimatedVisibility and keep
Modifier.animateItem() for a simple entrance animation, or change visible to the
actual visibility state (e.g., a Boolean from view state or local composable
state) to enable show/hide animations; update the code around AnimatedVisibility
and animateItem() accordingly so visibility is driven by the intended state
rather than a hardcoded true.
- Around line 81-86: The formatTimer function is defined as a local
composable-internal function but does not capture state; extract it to a
top-level function (e.g., move formatTimer(seconds: Int): String out of
SignupScreen composable) or into a shared util object so it’s reusable across
files; update any callers in SignupScreen to reference the new top-level/util
function and keep the implementation identical (using safeSeconds, minutes, secs
and String.format with Locale.getDefault()) to preserve behavior.
- Around line 176-185: The button enablement logic in SignupScreen (the when
block that sets buttonText, buttonEnabled, onButtonClick) uses raw checks
(state.phoneNumber.length >= 10 and state.authCode.isNotEmpty()) which are
inconsistent with the ViewModel validators; change buttonEnabled to use the
uiState validation results (e.g., uiState.phoneValidation.isValid and
uiState.authCodeValidation.isValid or equivalent flags) so the UI enablement
matches PhoneValidator and AuthCodeValidator, and ensure the onButtonClick
handlers (viewModel::onPhoneComplete, viewModel::onAuthComplete) remain
unchanged.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt`:
- Around line 128-157: Before calling
phoneVerificationRepository.sendVerificationCode in the viewModelScope.launch
block, set uiState.isLoading = true and then reset uiState.isLoading = false in
both the onSuccess and onFailure branches so the UI shows a loading state during
the API call; update the code in the SignupViewModel around
viewModelScope.launch / phoneVerificationRepository.sendVerificationCode to set
isLoading true immediately before the call and ensure both the success branch
that updates completedItems/step/startAuthTimer and the failure branch that sets
phoneErrorMessage also clear isLoading so it never remains stuck.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b831b49-b9bc-4c48-ab18-aecc8c38474f

📥 Commits

Reviewing files that changed from the base of the PR and between fb2fb11 and e129e85.

📒 Files selected for processing (8)
  • app/src/main/java/com/gildongmu/dduru/core/designsystem/color/Color.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/AuthCodeInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/viewmodel/SignupViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/AuthCodeInput.kt

Comment on lines +163 to +169
SignupStep.DONE -> {
Text(
"회원가입 완료 🎉",
color = Color.Blue,
modifier = Modifier.padding(16.dp)
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

SignupStep.DONE 후 네비게이션 처리 누락

회원가입 완료 후 단순히 텍스트만 표시되고 다음 화면으로의 네비게이션이 없습니다. LaunchedEffectDONE 상태를 감지하여 콜백을 호출하거나, ViewModel에서 단발성 이벤트(SharedFlow/Channel)를 통해 네비게이션을 트리거해야 합니다.

🔧 네비게이션 콜백 추가 제안
 `@Composable`
 fun SignupScreen(
-    viewModel: SignupViewModel = hiltViewModel()
+    viewModel: SignupViewModel = hiltViewModel(),
+    onSignupComplete: () -> Unit = {}
 ) {
     // ... existing code ...
     
+    LaunchedEffect(currentStep) {
+        if (currentStep == SignupStep.DONE) {
+            onSignupComplete()
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.kt`
around lines 163 - 169, The UI currently only renders Text for SignupStep.DONE
and never triggers navigation; update the composable to detect the DONE state
and invoke a navigation callback (or emit a one-shot event from the ViewModel).
Specifically, when SignupStep.DONE is reached in the screen that reads the
ViewModel state, use a LaunchedEffect(key1 = viewModel.signupStep) to call a
passed-in onSignupComplete() lambda or consume a SharedFlow/Channel event from
the ViewModel (emit from ViewModel on success) so navigation is performed
exactly once; ensure you clear/mark the event to avoid repeated navigation.

Copy link
Collaborator

@hyeminililo hyeminililo left a comment

Choose a reason for hiding this comment

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

헉 ,, 이번 것 코리가 좀 복잡할 수도 있을 것 같네요 !! 그래도 너무 수고하셨습니다 !! 코드래빗이 적어준 코리도 같이 적용해주시면 좋을 것 같아요! !

val Gray928 = Color(0xFF17181B)
val Gray958 = Color(0xFF0E1013)
val Black = Color(0xFF000000)
val ValidationRed = Color(0xFFFF2828)
Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 요거 따로 디자이너분이 색상 지정을 안 해주셨나요
?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

지정해주신 색상으로 구현했습니다~!

enabledColors: DduruButtonEnabledColors = DduruButtonEnabledColors(
backgroundColor = DduruTheme.colors.primary,
textColor = DduruTheme.colors.gray0,
borderColor = DduruTheme.colors.whiteAlpha30
Copy link
Collaborator

Choose a reason for hiding this comment

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

요거는 다연님이 하신 부분은 아니지만 DduruTheme.colors.white.copy(alpha = 0.3f) 이런식으로 기존 white를 사용하는게 좋을 것 같네요 ! ( 이러고 나중에 DS 정리 한 번 합시당 ㅎㅎ)

disabledColors: DduruButtonDisabledColors = DduruButtonDisabledColors(
backgroundColor = DduruTheme.colors.primary.copy(alpha = 0.5f),
textColor = DduruTheme.colors.gray0.copy(alpha = 0.5f),
borderColor = DduruTheme.colors.whiteAlpha30.copy(alpha = 0.5f)
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 친구도 그러면 동일하게 수정하면 되겠죠 ?? 52번줄 처럼요 !!

shape: Shape = RoundedCornerShape(16.dp)
) {
val backgroundColor = if (enabled) enabledColors.backgroundColor else disabledColors.backgroundColor
val textColor = if (enabled) enabledColors.textColor else disabledColors.textColor
Copy link
Collaborator

Choose a reason for hiding this comment

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

요기 다 줄바꿈해주세용 !!

textColor = DduruTheme.colors.gray0,
borderColor = DduruTheme.colors.whiteAlpha30
),
disabledColors: DduruButtonDisabledColors = DduruButtonDisabledColors(
Copy link
Collaborator

Choose a reason for hiding this comment

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

이건 진짜 개인적인 궁금증인데 따로 이렇게 기본값을 데이터 클래스로 선언해서 사용하신 이유가 있으신가용??

Copy link
Collaborator

Choose a reason for hiding this comment

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

enabled/disabled 색상을 따로 묶어서 관리하면 편리할거라고 생각했고 프리셋으로 만들어두면 호출하는 쪽에서 편하게 쓸 수 있어서 이렇게 선언하여 사용했는데 수정을 원하시면 수정하는 방법도 좋다고생각합니다

@Composable
fun BirthInputRow(
state: SignupUiState,
viewModel: SignupViewModel,
Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 되면 컴포넌트가 ViewModel 이 있는 화면에서만 재사용 가능하고, 미리보기/테스트에도 뷰모델 인스턴스가도 추가적으로 필요해요 ! ViewModel 의 데이터를 상위에서 풀어서 years, months, onYearSelected 같은 순수 값/콜백만 내려주도록 분리해주세요 ! 이건 모든 컴포넌트에 적용해주세용 !!

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun SignupScreen(
viewModel: SignupViewModel = viewModel()
Copy link
Collaborator

Choose a reason for hiding this comment

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

코드래빗 말이 맞아요! ! UI는 uiState와 이벤트 콜백만 받도록 state hoisting을 적용해주세요! !

var uiState by mutableStateOf(SignupUiState())
private set

val years = (1940..2024).toList().reversed()
Copy link
Collaborator

Choose a reason for hiding this comment

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

요거 서버 값에서 받는 것인지 확인 한 번 더 해주세요 !!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

범위값은 직접 정의해주고 있습니다~

Comment on lines +55 to +69
val SIGNUP_FLOW = remember {
listOf(
SignupStep.BIRTH,
SignupStep.GENDER,
SignupStep.PHONE,
SignupStep.AUTH,
SignupStep.AUTH_VERIFICATION,
SignupStep.DONE
)
}

val visibleSteps = remember(currentStep) {
val idx = SIGNUP_FLOW.indexOf(currentStep).takeIf { it >= 0 } ?: 0
SIGNUP_FLOW.take(idx + 1)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

SignupScreen 안에서 SIGNUP_FLOW, visibleSteps, 버튼 텍스트/활성 조건, 타이머 포맷을 직접 계산하니까, 나중에 다른 화면에서도 같은 정보를 보여주려면 로직을 계속 복붙해야 하고 ViewModel 레벨의 단위 테스트도 어려워져요 ,, ! 😭😭

이런 파생 상태는 ViewModel이나 SignupUiState 에서 미리 계산해서 내려주고, Screen 쪽은 state.visibleSteps, state.bottomButton, state.timerText 처럼 단순히 읽어 렌더링만 하도록 구조를 정리해주는 방향이 좋을 것 같아여,, !!🫶🏻

shape = buttonShape,
modifier = Modifier.fillMaxWidth()
)
if (!isImeVisible) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

줄 바꿈 해주세용

Copy link
Collaborator

@dlwl224 dlwl224 left a comment

Choose a reason for hiding this comment

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

일차 코드리뷰합니다. 집에가서 다시보겠습니다.

import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp

// 1. 방향을 정의하는 Enum 추가
Copy link
Collaborator

Choose a reason for hiding this comment

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

주석 지워주세요

textColor = DduruTheme.colors.gray0,
borderColor = DduruTheme.colors.whiteAlpha30
),
disabledColors: DduruButtonDisabledColors = DduruButtonDisabledColors(
Copy link
Collaborator

Choose a reason for hiding this comment

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

enabled/disabled 색상을 따로 묶어서 관리하면 편리할거라고 생각했고 프리셋으로 만들어두면 호출하는 쪽에서 편하게 쓸 수 있어서 이렇게 선언하여 사용했는데 수정을 원하시면 수정하는 방법도 좋다고생각합니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature ✨ 새로운 기능 추가, 신규 화면, API 연동 mod 🎉 큰 단위 수정 - 로직 수정 다연 👻 다연씨 전용 라벨

Projects

Status: Todo
Status: No status

Development

Successfully merging this pull request may close these issues.

[mod] 최초가입 관련 로직 수정

3 participants