Conversation
완료 버튼이나 다음으로 사용되는 PrimaryButton이 재사용성 가능성이 있어, 컴포넌트로 구현
Signup_screen에서 사용하는 State 구현
완료, 다음 버튼에 사용되는 Primary 버튼 재사용성 가능성이 있어 컴포넌트로 구현
성별, 생일에 따라 입력필드 컴포넌트 구현
기존에 GreetingCard 뷰모델 삭제 후, SignupScreen에서 쓰이는 하나의 ViewModel 구현
화면에 보여주기 위한 역할을 하기 위해 Presentation 계층 생성 - UI State : UI 상태를 나타내는 State 생성 - Screen : 정적 화면 생성 - ViewModel : 화면 데이터 흐름 유지하는 ViewModel 생성
There was a problem hiding this comment.
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은onPhoneComplete의180초 설정과 충돌하고, 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)는enabled가false -> 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를 추출하는 편이 유지보수에 유리합니다.
MaterialUnderlineTextField와MaterialUnderlineNumberTextField의 라벨/레이아웃/색상 설정이 거의 동일해, 추후 스타일 변경 시 불일치가 생기기 쉽습니다. 공통 베이스 컴포저블 + 입력 전략(일반/숫자) 분리로 단순화하는 것을 권장합니다.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
📒 Files selected for processing (15)
app/src/main/java/com/gildongmu/dduru/data/di/NetworkModule.ktapp/src/main/java/com/gildongmu/dduru/data/register/model/OnboardingRegisterRequest.ktapp/src/main/java/com/gildongmu/dduru/data/register/repository/OnboardingRegisterRepositoryImpl.ktapp/src/main/java/com/gildongmu/dduru/domain/register/OnboardingRegisterRepository.ktapp/src/main/java/com/gildongmu/dduru/domain/repository/AuthRepository.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/AuthCodeInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BirthInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/NicknameInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/state/SignupUiState.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/util/ValidationMessageMapper.ktapp/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
| return OkHttpClient.Builder() | ||
| .retryOnConnectionFailure(true) | ||
| .addInterceptor(authInterceptor) | ||
| .addInterceptor(logging) |
There was a problem hiding this comment.
🧩 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 -50Repository: 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 -30Repository: 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.
There was a problem hiding this comment.
맞아요 만약에 갑자기 네트워크 연결이 끊어져있다면 인증번호가 2번 보내질 수도 있어요 ,,!
There was a problem hiding this comment.
@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.
| 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 | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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 -60Repository: 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 -20Repository: 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 -30Repository: 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 -45Repository: ddu-ru/ddu-ru-android
Length of output: 2061
🏁 Script executed:
rg -n "runCatching" app/src/main/java/com/gildongmu/dduru --type=kotlin | wc -lRepository: ddu-ru/ddu-ru-android
Length of output: 66
🏁 Script executed:
rg -B5 "runCatching\s*\{" app/src/main/java/com/gildongmu/dduru --type=kotlin -cRepository: ddu-ru/ddu-ru-android
Length of output: 262
🏁 Script executed:
rg "CancellationException" app/src/main/java/com/gildongmu/dduru --type=kotlinRepository: ddu-ru/ddu-ru-android
Length of output: 47
🏁 Script executed:
cat -n app/src/main/java/com/gildongmu/dduru/data/register/repository/OnboardingRegisterRepositoryImpl.ktRepository: ddu-ru/ddu-ru-android
Length of output: 1852
runCatching에서 코루틴 취소 예외가 억제됩니다
Line 21의 runCatching은 CancellationException까지 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).
| 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) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.kt | head -130Repository: 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")
EOFRepository: 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 IllegalArgumentExceptionCitations:
🏁 Script executed:
rg "MaterialUnderlineNumberTextField" --type kt -B 2 -A 8Repository: ddu-ru/ddu-ru-android
Length of output: 91
🏁 Script executed:
rg "MaterialUnderlineNumberTextField" -B 2 -A 8Repository: 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.
| 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 | ||
|
|
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
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파라미터가 전달되지 않아 비활성화가 동작하지 않습니다.
GenderSegment가enabled를 받지만 실제 버튼에 전달하지 않아 비활성 상태 제어가 깨집니다.수정 제안
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
📒 Files selected for processing (7)
app/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/AuthCodeInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/YearDropdownField.ktapp/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
| // color: Color = DduruTheme.colors.ValidationRed | ||
| ) { | ||
| Text( | ||
| text = text, | ||
| modifier = modifier.padding(top = 4.dp), | ||
| // style = DduruTheme.typography.BodySmall, | ||
| // color = color |
There was a problem hiding this comment.
검증 메시지의 스타일/색상이 빠져 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.
There was a problem hiding this comment.
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타이머 로직 불일치
- Line 206:
seconds <= 0일 때 60초로 설정하지만,onPhoneComplete에서는 180초로 설정합니다.- 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을 제공하세요.펭귄 이미지의
contentDescription이null로 설정되어 있습니다. 스크린 리더 사용자를 위해 적절한 설명을 추가하거나, 순수 장식용 이미지라면 명시적으로 빈 문자열("")과 함께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:stepContents를remember로 감싸거나 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:AnimatedVisibility의visible = 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의
PhoneValidator와AuthCodeValidator에서 더 정교한 검증을 수행합니다. 버튼 활성화 조건을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
📒 Files selected for processing (8)
app/src/main/java/com/gildongmu/dduru/core/designsystem/color/Color.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/AuthCodeInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/BaseLineInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GenderInput.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/GreetingMessageCard.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/component/ValidationMessage.ktapp/src/main/java/com/gildongmu/dduru/presentation/feature/register/screen/SignupScreen.ktapp/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
| SignupStep.DONE -> { | ||
| Text( | ||
| "회원가입 완료 🎉", | ||
| color = Color.Blue, | ||
| modifier = Modifier.padding(16.dp) | ||
| ) | ||
| } |
There was a problem hiding this comment.
SignupStep.DONE 후 네비게이션 처리 누락
회원가입 완료 후 단순히 텍스트만 표시되고 다음 화면으로의 네비게이션이 없습니다. LaunchedEffect로 DONE 상태를 감지하여 콜백을 호출하거나, 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.
hyeminililo
left a comment
There was a problem hiding this comment.
헉 ,, 이번 것 코리가 좀 복잡할 수도 있을 것 같네요 !! 그래도 너무 수고하셨습니다 !! 코드래빗이 적어준 코리도 같이 적용해주시면 좋을 것 같아요! !
| val Gray928 = Color(0xFF17181B) | ||
| val Gray958 = Color(0xFF0E1013) | ||
| val Black = Color(0xFF000000) | ||
| val ValidationRed = Color(0xFFFF2828) |
There was a problem hiding this comment.
혹시 요거 따로 디자이너분이 색상 지정을 안 해주셨나요
?
There was a problem hiding this comment.
지정해주신 색상으로 구현했습니다~!
| enabledColors: DduruButtonEnabledColors = DduruButtonEnabledColors( | ||
| backgroundColor = DduruTheme.colors.primary, | ||
| textColor = DduruTheme.colors.gray0, | ||
| borderColor = DduruTheme.colors.whiteAlpha30 |
There was a problem hiding this comment.
요거는 다연님이 하신 부분은 아니지만 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) |
There was a problem hiding this comment.
이 친구도 그러면 동일하게 수정하면 되겠죠 ?? 52번줄 처럼요 !!
| shape: Shape = RoundedCornerShape(16.dp) | ||
| ) { | ||
| val backgroundColor = if (enabled) enabledColors.backgroundColor else disabledColors.backgroundColor | ||
| val textColor = if (enabled) enabledColors.textColor else disabledColors.textColor |
| textColor = DduruTheme.colors.gray0, | ||
| borderColor = DduruTheme.colors.whiteAlpha30 | ||
| ), | ||
| disabledColors: DduruButtonDisabledColors = DduruButtonDisabledColors( |
There was a problem hiding this comment.
이건 진짜 개인적인 궁금증인데 따로 이렇게 기본값을 데이터 클래스로 선언해서 사용하신 이유가 있으신가용??
There was a problem hiding this comment.
enabled/disabled 색상을 따로 묶어서 관리하면 편리할거라고 생각했고 프리셋으로 만들어두면 호출하는 쪽에서 편하게 쓸 수 있어서 이렇게 선언하여 사용했는데 수정을 원하시면 수정하는 방법도 좋다고생각합니다
| @Composable | ||
| fun BirthInputRow( | ||
| state: SignupUiState, | ||
| viewModel: SignupViewModel, |
There was a problem hiding this comment.
이렇게 되면 컴포넌트가 ViewModel 이 있는 화면에서만 재사용 가능하고, 미리보기/테스트에도 뷰모델 인스턴스가도 추가적으로 필요해요 ! ViewModel 의 데이터를 상위에서 풀어서 years, months, onYearSelected 같은 순수 값/콜백만 내려주도록 분리해주세요 ! 이건 모든 컴포넌트에 적용해주세용 !!
| @OptIn(ExperimentalLayoutApi::class) | ||
| @Composable | ||
| fun SignupScreen( | ||
| viewModel: SignupViewModel = viewModel() |
There was a problem hiding this comment.
코드래빗 말이 맞아요! ! UI는 uiState와 이벤트 콜백만 받도록 state hoisting을 적용해주세요! !
| var uiState by mutableStateOf(SignupUiState()) | ||
| private set | ||
|
|
||
| val years = (1940..2024).toList().reversed() |
There was a problem hiding this comment.
요거 서버 값에서 받는 것인지 확인 한 번 더 해주세요 !!
There was a problem hiding this comment.
범위값은 직접 정의해주고 있습니다~
| 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) | ||
| } |
There was a problem hiding this comment.
SignupScreen 안에서 SIGNUP_FLOW, visibleSteps, 버튼 텍스트/활성 조건, 타이머 포맷을 직접 계산하니까, 나중에 다른 화면에서도 같은 정보를 보여주려면 로직을 계속 복붙해야 하고 ViewModel 레벨의 단위 테스트도 어려워져요 ,, ! 😭😭
이런 파생 상태는 ViewModel이나 SignupUiState 에서 미리 계산해서 내려주고, Screen 쪽은 state.visibleSteps, state.bottomButton, state.timerText 처럼 단순히 읽어 렌더링만 하도록 구조를 정리해주는 방향이 좋을 것 같아여,, !!🫶🏻
| shape = buttonShape, | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) | ||
| if (!isImeVisible) { |
dlwl224
left a comment
There was a problem hiding this comment.
일차 코드리뷰합니다. 집에가서 다시보겠습니다.
| import androidx.compose.ui.unit.LayoutDirection | ||
| import androidx.compose.ui.unit.dp | ||
|
|
||
| // 1. 방향을 정의하는 Enum 추가 |
| textColor = DduruTheme.colors.gray0, | ||
| borderColor = DduruTheme.colors.whiteAlpha30 | ||
| ), | ||
| disabledColors: DduruButtonDisabledColors = DduruButtonDisabledColors( |
There was a problem hiding this comment.
enabled/disabled 색상을 따로 묶어서 관리하면 편리할거라고 생각했고 프리셋으로 만들어두면 호출하는 쪽에서 편하게 쓸 수 있어서 이렇게 선언하여 사용했는데 수정을 원하시면 수정하는 방법도 좋다고생각합니다
✨ 작업 내용
최초 회원가입 UI 수정
📄 상세 내용
상단 말풍선 멘트는 기획에따라 바뀔 예정입니다!!!
Dev 브랜치와 머지하여 변경사항 적용 했습니다.
최초 회원가입 UI 수정했습니다.
-> 서버에서 검증하는 것과 별개로 프론트 내부에서 유효성 검사를 진행합니다(빈칸, 길이 등)
📎 관련 이슈
#16
📷 스크린샷 / 영상
Screen_recording_20260307_052340.mp4
Screen_recording_20260307_054243.mp4
✅ 체크리스트
PR을 보내기 전에 아래 항목들을 확인해주세요.
Summary by CodeRabbit
릴리스 노트
New Features
Refactor