Skip to content

Conversation

@rbqks529
Copy link
Contributor

@rbqks529 rbqks529 commented Sep 23, 2025

🚀 이슈번호

✏️ 변경사항

  • 공통 컴포넌트 구현
  • 스플래시 화면 구현
  • 로그인 화면 구현
  • 이메일 입력 화면 구현
  • 이메일 검증 화면 구현
  • 비밀번호 입력 화면 구현
  • 닉네임 입력 화면 구현
  • 동아리 번호 입력 화면 구현

📷 스크린샷

  • 스플래시 화면
스크린샷 2025-09-23 오후 2 31 03
  • 로그인 화면
스크린샷 2025-09-23 오후 2 31 49
  • 이메일 입력 화면 (회원가입)
스크린샷 2025-09-23 오후 2 33 46
  • 이메일 입력 화면 (비밀번호 찾기)
스크린샷 2025-09-23 오후 2 34 22
  • 이메일 검증 화면
스크린샷 2025-09-23 오후 2 35 14
  • 비밀번호 입력 화면
스크린샷 2025-09-23 오후 2 35 34
  • 닉네임 입력 화면
스크린샷 2025-09-23 오후 2 35 52 스크린샷 2025-09-23 오후 2 37 14

✍️ 사용법

  • 현재 네비게이션 경로를 스플래시로 해두었고 따로 버튼에 조건문을 걸어 놓지는 않고 네비게이션 로직만 넣어놨기에 화면이동은 확인 가능합니다!

🎸 기타

  • 인증 코드 입력을 현재는 숫자만 으로 했는데 피그마 코멘트 보니까 숫자 + 언어도 되게 하라고 되어있더라고요 이 부분 다시 말씀해 주시면 수정할게요

Summary by CodeRabbit

  • 신기능
    • 로그인/회원가입 플로우 확장: 스플래시, 이메일 인증, 비밀번호 입력·확인, 닉네임, 동아리 코드 입력, 비밀번호 찾기 화면 추가
    • 공통 UI 컴포넌트 추가: 로그인 버튼, 입력 필드, 숫자 입력 박스
    • 인증·홈 관련 다국어 문자열 대거 추가로 UI 텍스트 보강
  • 리팩터링
    • 내비게이션 그래프 확장 및 인증 시작 화면을 스플래시로 변경
    • 기존 로그인 화면을 신규 화면으로 교체하고 이동 경로 정리
  • 작업
    • 머티리얼 및 확장 아이콘 의존성 추가

@rbqks529 rbqks529 self-assigned this Sep 23, 2025
@rbqks529 rbqks529 added FEAT 기능 개발 UI UI 구현 작업 No Merge 아직 진행중인 PR labels Sep 23, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 23, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Auth 플로우 전면 확장: 네비게이션에 스플래시와 단계별 인증/가입 경로 추가, 관련 스크린/공통 컴포넌트 신규 도입, 문자열 리소스 다수 추가. 빌드에 Material/Icons 의존성 포함. 기존 LoginScreen(구 경로) 제거, ViewModel 패키지 이동 및 DI 임포트 갱신, NavGraph의 Auth 시작 지점이 Splash로 변경.

Changes

Cohort / File(s) Change summary
Build dependencies
composeApp/build.gradle.kts
commonMaincompose.material, compose.materialIconsExtended 의존성 추가
Resource strings (auth/home)
composeApp/src/commonMain/composeResources/values/strings.xml
로그인·회원가입·비밀번호 재설정·이메일 인증·닉네임·클럽코드 등 문자열 키 대거 추가
Navigation routes & graph
.../core/navigation/Route.kt, .../core/navigation/WhosInNavGraph.kt
Route에 Splash, FindPassword, Signup, EmailVerification, PasswordInput, NicknameInput, ClubCodeInput 추가. Auth 그래프 시작점을 LoginSplash로 변경. 각 스크린 라우트 및 콜백 기반 이동 흐름 연결, popUpTo/navigateUp 구성
DI & ViewModel 경로 정리
.../di/DIModules.kt, .../presentation/auth/login/viewmodel/LoginViewModel.kt
LoginViewModel 패키지 이동(org.whosin.client.presentation.auth...auth.login.viewmodel)에 따른 DI 임포트 갱신
Auth screens 추가
.../presentation/auth/login/SplashScreen.kt, .../presentation/auth/login/LoginScreen.kt, .../presentation/auth/login/SignupEmailInputScreen.kt, .../presentation/auth/login/EmailVerificationScreen.kt, .../presentation/auth/login/PasswordInputScreen.kt, .../presentation/auth/login/NicknameInputScreen.kt, .../presentation/auth/login/FindPasswordScreen.kt, .../presentation/auth/clubcode/ClubCodeInputScreen.kt, .../presentation/auth/clubcode/ClubCodeState.kt
스플래시/로그인/이메일 입력/이메일 인증/비밀번호 입력/닉네임 입력/비번 찾기/클럽코드 입력 스크린과 상태(enum) 신설. 각 스크린에 콜백 파라미터 및 Preview 포함
UI common components
.../presentation/auth/login/component/CommonLoginButton.kt, .../presentation/auth/login/component/CommonLoginInputField.kt, .../presentation/auth/login/component/NumberInputBox.kt
공통 버튼, 입력 필드, 단일 숫자 입력 박스 컴포넌트 추가(스타일·상태·가시성 토글 등 포함)
App import 정리
.../App.kt
androidx.compose.runtime.* 와일드카드 제거→Composable 단일 임포트, 불필요 리소스 임포트 삭제 (동작 변화 없음)
Legacy login 제거
.../presentation/auth/LoginScreen.kt
구 경로의 단순 LoginScreen 및 Preview 파일 삭제(신규 경로 스크린으로 대체)

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant NavHost as NavHost (Auth Graph)
  participant Splash as SplashScreen
  participant Login as LoginScreen
  participant Signup as SignupEmailInput
  participant Verify as EmailVerification
  participant Pwd as PasswordInput
  participant Nick as NicknameInput
  participant Club as ClubCodeInput
  participant Home as Home

  User->>NavHost: 앱 시작
  NavHost->>Splash: startDestination = Splash
  Splash-->>NavHost: onNavigateToLogin()
  NavHost->>Login: navigate(Route.Login)

  alt 로그인 성공
    Login-->>NavHost: onNavigateToHome()
    NavHost->>Home: navigate(Route.Home) and popUpTo(AuthGraph) { inclusive=true }
  else 비밀번호 찾기
    Login-->>NavHost: onNavigateToFindPassword()
    NavHost->>FindPwd: navigate(Route.FindPassword)
    FindPwd-->>NavHost: onPasswordResetComplete()
    NavHost->>Login: navigateUp()/navigate(Login)
  else 회원가입 플로우
    Login-->>NavHost: onNavigateToSignup()
    NavHost->>Signup: navigate(Route.Signup)
    Signup-->>NavHost: onNavigateToEmailVerification(email)
    NavHost->>Verify: navigate(Route.EmailVerification)
    Verify-->>NavHost: onVerificationComplete(code)
    NavHost->>Pwd: navigate(Route.PasswordInput)
    Pwd-->>NavHost: onPasswordComplete(pw, confirm)
    NavHost->>Nick: navigate(Route.NicknameInput)
    Nick-->>NavHost: onNavigateToClubCode(nickname)
    NavHost->>Club: navigate(Route.ClubCodeInput)
    Club-->>NavHost: onNavigateToHome(clubName)
    NavHost->>Home: navigate(Route.Home) and popUpTo(AuthGraph) { inclusive=true }
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • [CHORE] Ktor, Koin 세팅 #10 — Koin DI 모듈에 LoginViewModel 추가/배선 작업. 본 PR의 ViewModel 패키지 이동 및 DIModules 임포트 변경과 직접적으로 연관.

Poem

스플래시 한 점, 오렌지 빛 물결 🌊
여섯 자리 별들로 이메일이 반짝 ✨
닉네임 피어오르고, 비밀은 두 번 속삭여—
숫자 상자 톡톡, 클럽의 문이 열리고 🔐
이제 홈으로, 우리는 들어간다. 🏠

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "[UI] 로그인 화면 ui 구현"은 간결하고 읽기 쉬우며 로그인 UI 구현이라는 핵심 변경을 명확하게 전달합니다. PR에는 로그인 외에도 스플래시·회원가입·이메일 인증·비밀번호·닉네임·클럽 코드 등 여러 인증 관련 화면과 네비게이션 변경이 포함되어 있지만 제목은 적어도 PR에서 실제로 구현된 주요 부분 중 하나를 정확히 나타냅니다. 이모지나 파일 목록 같은 노이즈가 없고 모호한 용어도 사용하지 않아 스캔하기에 용이합니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements the login and authentication UI flow for the WhosIn application, including multiple screens for user registration and authentication. The implementation includes splash screen, login screen, signup flow with email verification, password input, nickname selection, and club code verification screens.

  • Implements comprehensive authentication UI flow with 7 different screens
  • Creates reusable login components for buttons, input fields, and number inputs
  • Adds navigation structure and routing for the complete signup/login process

Reviewed Changes

Copilot reviewed 21 out of 24 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
gradle/libs.versions.toml Adds material icons extended dependency
composeApp/build.gradle.kts Integrates material icons and material design dependencies
LoginViewModel.kt Fixes package structure for login view model
Component files Creates reusable UI components for login flow
Screen files Implements all authentication screens (splash, login, signup, etc.)
Navigation files Updates routing and navigation graph for auth flow
strings.xml Adds localized strings for all authentication screens

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

val isMaxLengthReached = maxLength != null && value.length >= maxLength
OutlinedTextField(
value = value,
onValueChange = onValueChange,
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

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

The isMaxLengthReached variable is calculated but never used in the component logic. Consider removing it or implementing the intended max length validation functionality.

Suggested change
onValueChange = onValueChange,
onValueChange = { newValue ->
if (maxLength == null || newValue.length <= maxLength) {
onValueChange(newValue)
}
},

Copilot uses AI. Check for mistakes.
.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = if (isMaxLengthReached) Color(0xFFE5E5E5) else Color(0xFFF89531),
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

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

The isMaxLengthReached variable is used here but was never properly implemented in the onValueChange logic. Text input is not actually limited by maxLength, making this condition ineffective.

Copilot uses AI. Check for mistakes.
val isMaxLengthReached = maxLength != null && value.length >= maxLength
OutlinedTextField(
value = value,
onValueChange = onValueChange,
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

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

The onValueChange callback should enforce maxLength limit when provided. Currently, users can input unlimited text despite the maxLength parameter. Add validation: onValueChange = { if (maxLength == null || it.length <= maxLength) onValueChange(it) }

Suggested change
onValueChange = onValueChange,
onValueChange = {
if (maxLength == null || it.length <= maxLength) {
onValueChange(it)
}
},

Copilot uses AI. Check for mistakes.
Comment on lines 90 to 93
// 8자 제한
if (newValue.length <= 8) {
nickname = newValue
}
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

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

The 8-character limit is implemented manually here but the CommonLoginInputField component also has a maxLength parameter. Use the component's maxLength feature instead: remove this manual validation and rely on the component's built-in functionality.

Suggested change
// 8자 제한
if (newValue.length <= 8) {
nickname = newValue
}
nickname = newValue

Copilot uses AI. Check for mistakes.
painter = painterResource(Res.drawable.ic_back),
contentDescription = stringResource(Res.string.back_button),
tint = Color.Black,
modifier = Modifier.size(14.dp)
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

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

Icon size inconsistency detected. Other screens use 18.dp for the back icon size (SignupEmailInputScreen.kt:67, NicknameInputScreen.kt:67). Consider using consistent sizing across all screens.

Suggested change
modifier = Modifier.size(14.dp)
modifier = Modifier.size(18.dp)

Copilot uses AI. Check for mistakes.
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: 2

🧹 Nitpick comments (29)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SplashScreen.kt (2)

37-41: 로고 이미지 a11y: 장식이면 contentDescription을 null로

장식 이미지는 스크린리더가 읽지 않도록 null이 적절합니다. 텍스트가 필요하면 stringResource로 지역화해주세요.

-        Image(
-            painter = painterResource(Res.drawable.img_logo_white),
-            contentDescription = "logo",
-            modifier = Modifier.size(width = 160.dp, height = 122.dp)
-        )
+        Image(
+            painter = painterResource(Res.drawable.img_logo_white),
+            contentDescription = null,
+            modifier = Modifier.size(width = 160.dp, height = 122.dp)
+        )

20-24: 테스트/프리뷰 친화성: 지연시간을 파라미터로 주입

지연시간 하드코딩 대신 파라미터로 받아 Preview/테스트에서 0으로 줄일 수 있게 하면 UX/개발 편의가 좋아집니다.

-fun SplashScreen(
+fun SplashScreen(
     modifier: Modifier = Modifier,
-    onNavigateToLogin: () -> Unit = {}
+    onNavigateToLogin: () -> Unit = {},
+    splashDelayMillis: Long = 2000
 ) {
 
-    LaunchedEffect(Unit) {
-        delay(2000)
+    LaunchedEffect(Unit) {
+        delay(splashDelayMillis)
         onNavigateToLogin()
     }

Also applies to: 26-29

composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (3)

33-38: 비활성 색 대비(Contrast) 개선 권장

회색 배경(0xD2D2D2) + 흰 텍스트는 대비가 낮습니다. 기본 disabled 색상을 사용하거나 대비를 높여주세요.

         colors = ButtonDefaults.buttonColors(
             containerColor = backgroundColor,
             contentColor = textColor,
-            disabledContainerColor = Color(0xFFD2D2D2),
-            disabledContentColor = Color.White
+            // 기본 disabled 색상 사용으로 대비 확보
         ),

41-45: 타이포그래피는 Theme를 통해 일관되게

MaterialTheme.typography를 사용하면 전역 테마 변경에 자연스럽게 따라갑니다.

-        Text(
-            text = text,
-            fontSize = 16.sp,
-            fontWeight = FontWeight.W600
-        )
+        Text(
+            text = text,
+            style = androidx.compose.material3.MaterialTheme.typography.titleMedium.copy(
+                fontWeight = FontWeight.W600
+            )
+        )

63-69: Preview도 Theme로 감싸기

Disabled 프리뷰도 테마 적용으로 실제 렌더와 일치시키는 것을 권장합니다.

 @Preview
 @Composable
 fun CommonLoginButtonDisabledPreview() {
-    CommonLoginButton(
-        text = "비활성화된 버튼",
-        onClick = {},
-        enabled = false
-    )
+    WhosInTheme {
+        CommonLoginButton(
+            text = "비활성화된 버튼",
+            onClick = {},
+            enabled = false
+        )
+    }
 }
composeApp/src/commonMain/composeResources/values/strings.xml (1)

4-5: 라벨 언어/톤 일관성

영문/한글 혼용으로 어색합니다. 아래처럼 통일 권장합니다.

-    <string name="email_label">E-mail</string>
-    <string name="password_label">password</string>
+    <string name="email_label">이메일</string>
+    <string name="password_label">비밀번호</string>
composeApp/build.gradle.kts (1)

60-60: 불필요한 compose.material 의존성 정리 검토

M3만 사용하는 경우 compose.material은 생략 가능입니다. 사용처 없으면 제거해 경량화하세요.

-            implementation(compose.material)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SignupEmailInputScreen.kt (4)

56-61: 터치 타겟 a11y: IconButton 크기 축소 지양

IconButton을 24dp로 줄이면 권장 최소 터치 영역(48dp)을 위반합니다. Icon 크기만 줄이고 버튼은 기본 크기를 유지하세요.

-            IconButton(
-                onClick = onNavigateBack,
-                modifier = Modifier
-                    .padding(bottom = 32.dp)
-                    .size(24.dp)
-            ) {
+            IconButton(
+                onClick = onNavigateBack,
+                modifier = Modifier
+                    .padding(bottom = 32.dp)
+            ) {
                 Icon(
                     painter = painterResource(Res.drawable.ic_back),
                     contentDescription = stringResource(Res.string.back_button),
                     tint = Color.Black,
-                    modifier = Modifier
-                        .size(18.dp)
+                    modifier = Modifier.size(24.dp)
                 )
             }

Also applies to: 63-68


86-94: IME에 가려지는 하단 버튼 대응

imePadding을 추가하고, 이메일은 trim 후 전달하는 것이 안전합니다.

-        CommonLoginButton(
+        CommonLoginButton(
             text = stringResource(Res.string.next_button),
-            onClick = { onNavigateToEmailVerification(email) },
+            onClick = { onNavigateToEmailVerification(email.trim()) },
             enabled = email.isNotBlank(),
             modifier = Modifier
                 .align(Alignment.BottomCenter)
                 .padding(horizontal = 16.dp)
-                .padding(bottom = 52.dp)
+                .padding(bottom = 52.dp)
+                .imePadding()
         )

42-42: 입력 값 보존성을 위해 rememberSaveable 고려

프로세스/구성 변경 시 값 유지가 필요하면 rememberSaveable이 유용합니다.

-    var email by remember { mutableStateOf("") }
+    var email by androidx.compose.runtime.saveable.rememberSaveable { mutableStateOf("") }

79-83: 이메일 입력 UX: 키보드 타입/IME 액션 지정

email 키보드/Next 액션을 지정하면 UX가 좋아집니다. 공용 입력 컴포넌트에 키보드 옵션을 전달 가능하게 확장하는 것을 권장합니다.

아래처럼 CommonLoginInputField에 옵션을 추가(외부 파일 변경)한 뒤 본 호출부에 전달하세요.

변경(외부 파일: CommonLoginInputField):

@Composable
fun CommonLoginInputField(
    modifier: Modifier = Modifier,
    value: String,
    onValueChange: (String) -> Unit,
    placeholder: String,
    isPassword: Boolean = false,
    maxLength: Int? = null,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions.Default
) {
    OutlinedTextField(
        value = value,
        onValueChange = onValueChange,
        ...
        keyboardOptions = keyboardOptions,
        keyboardActions = keyboardActions,
        singleLine = true
    )
}

본 파일 적용:

             CommonLoginInputField(
                 value = email,
                 onValueChange = { email = it },
-                placeholder = stringResource(Res.string.email_placeholder)
+                placeholder = stringResource(Res.string.email_placeholder),
+                keyboardOptions = androidx.compose.ui.text.input.KeyboardOptions(
+                    keyboardType = androidx.compose.ui.text.input.KeyboardType.Email,
+                    imeAction = androidx.compose.ui.text.input.ImeAction.Next
+                )
             )
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt (2)

41-44: maxLength 전달 시 입력 길이도 제한되도록 처리 권장

현재는 테두리 색만 바뀌고 실제 입력은 제한되지 않습니다. 아래처럼 onValueChange에서 컷팅해 주세요.

-        onValueChange = onValueChange,
+        onValueChange = { input ->
+            val limited = if (maxLength != null) input.take(maxLength) else input
+            onValueChange(limited)
+        },

75-79: 아이콘 contentDescription 문자열 리소스로 이동 권장

하드코딩된 한글 대신 stringResource 사용을 권장합니다(접근성/i18n).

composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/PasswordInputScreen.kt (1)

48-49: 버튼 활성 조건에 “두 비밀번호 일치” 추가

사용자 실수를 줄이기 위해 동일성 체크를 포함하세요.

-    val isComplete = password.isNotBlank() && confirmPassword.isNotBlank()
+    val isComplete = password.isNotBlank() && confirmPassword.isNotBlank() && password == confirmPassword
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt (1)

87-97: 중복 길이 제한 로직 정리

maxLength로 컴포넌트에서 컷팅하도록 하면 화면단 onValueChange 검사는 제거 가능합니다.

-            CommonLoginInputField(
-                value = nickname,
-                onValueChange = { newValue ->
-                    // 8자 제한
-                    if (newValue.length <= 8) {
-                        nickname = newValue
-                    }
-                },
-                placeholder = stringResource(Res.string.nickname_input_placeholder),
-                maxLength = 8
-            )
+            CommonLoginInputField(
+                value = nickname,
+                onValueChange = { nickname = it },
+                placeholder = stringResource(Res.string.nickname_input_placeholder),
+                maxLength = 8
+            )
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/LoginScreen.kt (3)

50-52: 로그인 버튼 활성 조건 추가

빈 값 로그인 방지를 위해 간단한 활성 조건을 둡니다.

     var email by remember { mutableStateOf("") }
     var password by remember { mutableStateOf("") }
 
+    val isLoginEnabled = email.isNotBlank() && password.isNotBlank()

109-113: 로그인 버튼 비활성화 처리 연결

위에서 정의한 활성 조건을 버튼에 반영하세요.

                 CommonLoginButton(
                     text = stringResource(Res.string.login_button),
-                    onClick = onNavigateToHome,
-                    modifier = Modifier.padding(bottom = 12.dp)
+                    onClick = onNavigateToHome,
+                    enabled = isLoginEnabled,
+                    modifier = Modifier.padding(bottom = 12.dp)
                 )

65-69: 장식용 이미지 접근성 처리

로고는 장식용이면 contentDescription = null 권장.

                 Image(
                     painter = painterResource(Res.drawable.img_logo_orange),
-                    contentDescription = "logo",
+                    contentDescription = null,
                     modifier = Modifier.size(width = 160.dp, height = 122.dp)
                 )
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/FindPasswordScreen.kt (2)

42-43: 간단한 이메일 유효성 체크 추가 제안

오타 제출 방지를 위해 기본 형식 검증을 추가하세요.

     var email by remember { mutableStateOf("") }
 
+    val isValidEmail = Regex("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$").matches(email.trim())

86-95: 버튼 활성 조건에 이메일 형식 반영

형식 불일치 시 비활성화.

         CommonLoginButton(
             text = stringResource(Res.string.send_email_button),
             onClick = { onPasswordResetComplete(email) },
-            enabled = email.isNotBlank(),
+            enabled = isValidEmail,
             modifier = Modifier
                 .align(Alignment.BottomCenter)
                 .padding(horizontal = 16.dp)
                 .padding(bottom = 52.dp)
         )
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt (2)

129-136: 백스페이스 시 이전 자리 값도 삭제되도록 UX 개선

포커스만 이동하면 사용자가 한 번 더 지워야 합니다. 이전 자리 값을 지워주세요.

                         onBackspace = {
                             // 빈 박스에서 백스페이스 시 이전 박스로 이동
                             if (index > 0) {
                                 val prevIndex = index - 1
+                                val newCode = verificationCode.copyOf()
+                                newCode[prevIndex] = ""
+                                verificationCode = newCode
                                 currentFocusIndex = prevIndex
                                 focusRequesters[prevIndex].requestFocus()
                             }
                         },

114-128: 인증코드 영문/숫자 허용 여부 확인 필요

현재 숫자만 허용합니다. Figma 코멘트대로 영문/숫자 혼합이 요구된다면 NumberInputBox의 필터/keyboardType을 파라미터화해 반영하겠습니다.

composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt (1)

160-166: 백스페이스 UX 개선

이전 칸으로 이동 시 해당 자리도 비워주세요.

                         onBackspace = {
                             if (index > 0) {
                                 val prevIndex = index - 1
+                                val newCode = clubCode.copyOf()
+                                newCode[prevIndex] = ""
+                                clubCode = newCode
                                 currentFocusIndex = prevIndex
                                 focusRequesters[prevIndex].requestFocus()
                             }
                         },
composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt (2)

47-56: 로그인 후 Auth 스택 제거 제안

뒤로가기 시 로그인으로 돌아가지 않도록 AuthGraph를 popUpTo로 정리하는 게 자연스럽습니다.

-                    onNavigateToHome = {
-                        navController.navigate(Route.Home)
-                    },
+                    onNavigateToHome = {
+                        navController.navigate(Route.Home) {
+                            popUpTo(Route.AuthGraph) { inclusive = true }
+                        }
+                    },

81-90: 불필요한 파라미터 제거로 경고 감소

사용하지 않는 backStackEntry는 제거 가능합니다.

-            composable<Route.EmailVerification> { backStackEntry ->
+            composable<Route.EmailVerification> {
                 
                 EmailVerificationScreen(
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt (4)

71-81: 백스페이스 처리와 숫자 필터 로직을 조금 더 견고하게 다듬기 제안

  • ZWSP 제거 후 비어 있는 경우를 우선 처리하면 “문자 삭제”와 “이전 칸으로 이동”을 더 일관되게 다룰 수 있습니다.
  • 서버/백엔드가 ASCII 숫자만 기대한다면 isDigit() 대신 '0'..'9'로 한정하는 편이 안전합니다. isDigit()은 유니코드 숫자(예: 아라비아 숫자)도 통과시킵니다.

아래처럼 정리하면 동작이 선명해집니다.

-            onValueChange = { newValue ->
-                val cleaned = newValue.text.replace("\u200B", "")
-                val filtered = cleaned.filter { it.isDigit() }.take(1)
-
-                if (newValue.text.isEmpty() && value.isEmpty()) {
-                    onBackspace?.invoke()
-                    return@BasicTextField
-                }
-
-                onValueChange(filtered)
-            },
+            onValueChange = { newValue ->
+                val cleaned = newValue.text.replace("\u200B", "")
+                // 비어 있으면 먼저 처리 (삭제/이동)
+                if (cleaned.isEmpty()) {
+                    if (value.isEmpty()) {
+                        onBackspace?.invoke()   // 이미 비어있던 상태에서의 백스페이스 → 이전 칸으로
+                    } else {
+                        onValueChange("")       // 내용이 있던 칸을 비우는 첫 번째 백스페이스
+                    }
+                    return@BasicTextField
+                }
+                // ASCII 숫자만 허용
+                val filtered = cleaned.filter { it in '0'..'9' }.take(1)
+                onValueChange(filtered)
+            },

86-87: IME 액션(Next)과 키보드 액션 연동(선택사항)

OTP/인증코드 UX에선 Next 버튼이 있는 편이 낫습니다. 외부에서 다음 포커스로 이동시키도록 콜백을 붙일 수 있게 해두면 좋아요.

-            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+            keyboardOptions = KeyboardOptions(
+                keyboardType = KeyboardType.Number,
+                imeAction = ImeAction.Next
+            ),
+            keyboardActions = KeyboardActions(
+                onNext = { /* TODO: 외부 콜백/FocusRequester로 다음 칸 포커스 이동 */ }
+            ),

추가 import:

import androidx.compose.ui.text.input.ImeAction
import androidx.compose.foundation.text.KeyboardActions

60-67: 포커스 표시 로컬 상태 지원 고려(선택사항)

현재는 isFocused를 외부에서 넘겨줘야 테두리 색이 바뀝니다. 내부에서 focusChanged 이벤트를 로컬 상태로도 잡아 val drawFocused = isFocused || localFocused처럼 처리하면, 부모가 별도 상태를 들고 있지 않아도 기본 포커스 하이라이트가 동작해 사용성이 좋아집니다.


69-99: 접근성(스크린리더) 힌트 추가 제안(선택사항)

단일 자리 입력 용도임을 스크린리더가 알 수 있도록 semantics 라벨을 부여하면 접근성이 좋아집니다.

-            modifier = Modifier.onFocusChanged { focusState ->
-                onFocusChanged?.invoke(focusState.isFocused)
-            },
+            modifier = Modifier
+                .onFocusChanged { focusState ->
+                    onFocusChanged?.invoke(focusState.isFocused)
+                }
+                .semantics { contentDescription = "인증코드 숫자 한 자리 입력" },

추가 import:

import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.contentDescription
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91855de and 0512cca.

⛔ Files ignored due to path filters (3)
  • composeApp/src/commonMain/composeResources/drawable/ic_back.png is excluded by !**/*.png
  • composeApp/src/commonMain/composeResources/drawable/img_logo_orange.png is excluded by !**/*.png
  • composeApp/src/commonMain/composeResources/drawable/img_logo_white.png is excluded by !**/*.png
📒 Files selected for processing (21)
  • composeApp/build.gradle.kts (2 hunks)
  • composeApp/src/commonMain/composeResources/values/strings.xml (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/App.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/Route.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt (2 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginScreen.kt (0 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeState.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/FindPasswordScreen.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/LoginScreen.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/PasswordInputScreen.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SignupEmailInputScreen.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SplashScreen.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt (1 hunks)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/LoginViewModel.kt (1 hunks)
  • gradle/libs.versions.toml (2 hunks)
💤 Files with no reviewable changes (1)
  • composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginScreen.kt
🧰 Additional context used
🧬 Code graph analysis (12)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/PasswordInputScreen.kt (2)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt (1)
  • CommonLoginInputField (30-85)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1)
  • CommonLoginButton (18-47)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SplashScreen.kt (1)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginScreen.kt (5)
  • {} (41-41)
  • LoginScreenPreview (36-43)
  • LoginScreen (13-34)
  • Text (23-32)
  • Column (20-33)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt (2)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt (1)
  • CommonLoginInputField (30-85)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1)
  • CommonLoginButton (18-47)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/FindPasswordScreen.kt (2)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt (1)
  • CommonLoginInputField (30-85)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1)
  • CommonLoginButton (18-47)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1)
composeApp/src/commonMain/kotlin/ui/theme/Theme.kt (1)
  • WhosInTheme (31-43)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SignupEmailInputScreen.kt (2)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt (1)
  • CommonLoginInputField (30-85)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1)
  • CommonLoginButton (18-47)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt (1)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginScreen.kt (5)
  • {} (41-41)
  • LoginScreenPreview (36-43)
  • LoginScreen (13-34)
  • Column (20-33)
  • Text (23-32)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt (2)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt (1)
  • NumberInputBox (31-101)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1)
  • CommonLoginButton (18-47)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/LoginScreen.kt (3)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt (1)
  • CommonLoginInputField (30-85)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1)
  • CommonLoginButton (18-47)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginScreen.kt (3)
  • LoginScreen (13-34)
  • Text (23-32)
  • Column (20-33)
composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt (8)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SplashScreen.kt (1)
  • SplashScreen (20-43)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/LoginScreen.kt (1)
  • LoginScreen (43-150)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/FindPasswordScreen.kt (1)
  • FindPasswordScreen (36-96)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SignupEmailInputScreen.kt (1)
  • SignupScreen (36-96)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt (1)
  • EmailVerificationScreen (43-162)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/PasswordInputScreen.kt (1)
  • PasswordInputScreen (39-133)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt (1)
  • NicknameInputScreen (37-111)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt (1)
  • ClubCodeInputScreen (51-268)
composeApp/src/commonMain/kotlin/org/whosin/client/App.kt (1)
composeApp/src/androidMain/kotlin/org/whosin/client/MainActivity.kt (2)
  • AppAndroidPreview (21-25)
  • onCreate (10-19)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt (2)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt (1)
  • NumberInputBox (31-101)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1)
  • CommonLoginButton (18-47)
🔇 Additional comments (7)
composeApp/src/commonMain/kotlin/org/whosin/client/App.kt (1)

5-5: import 정리 잘 하셨네요! 👍

wildcard import(*)를 명시적 import로 변경하고 사용하지 않는 import들을 제거한 것이 좋습니다. 코드가 더 명확해지고 빌드 성능에도 도움이 됩니다.

composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt (1)

14-14: DI 모듈 import 경로 업데이트 완료.

LoginViewModel의 새로운 패키지 경로에 맞춰 import 구문이 올바르게 업데이트되었습니다. 패키지 구조 변경에 따른 필수적인 수정사항이네요.

composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/LoginViewModel.kt (1)

1-1: 패키지 변경 일관성 확인 — 문제 없음

LoginViewModel이 composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/LoginViewModel.kt로 이동했고, composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt의 import 및 viewModelOf(::LoginViewModel)도 업데이트되어 이전 FQCN 잔재가 없습니다.

composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeState.kt (1)

3-7: LGTM

상태 모델 단순·명확합니다. 이후 UI state 호이스팅에도 적합해 보여요.

composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/Route.kt (1)

10-33: LGTM

Sealed Route 확장은 명확하고 직렬화 태깅도 적절합니다.

composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt (2)

31-42: 구성/의도 매우 깔끔합니다. API도 재사용성 높아요.

단일 문자 입력·포커스 테두리 처리·콜백 분리까지 전반 설계가 좋습니다. 이 상태로도 충분히 머지 가능한 퀄리티입니다.

디자인/기획 확인: 현재 컴포넌트는 숫자만 허용합니다. PR 본문에 “인증코드가 영숫자일 수 있음”이 언급돼 있는데, 실제 요구사항이 영숫자라면 필터 정책을 확정해 주세요(숫자만 vs 영숫자). 필요 시 확장 가능한 형태로 바꿔드릴 수 있어요.


45-51: ZWSP로 레이아웃/커서 유지하는 접근 OK. iOS/Android 동작만 한 번 확인 부탁

ZWSP → ""로의 변경 이벤트에 의존해 백스페이스 콜백을 트리거하는 패턴은 플랫폼별 IME 구현 차이를 조금 탈 수 있습니다. 실제 단말에서 “빈 상태에서 백스페이스 시 onBackspace가 안정적으로 호출되는지”만 확인해 주세요.

ktor = "3.2.3"
kotlinx-serialization = "1.9.0"
koin = "4.1.0"
materialIconsExtended = "1.7.8"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

버전 카탈로그에서 AndroidX icons 항목 제거

Compose MPP에 맞춰 JetBrains compose.materialIconsExtended를 사용하므로 해당 버전/라이브러리 항목은 제거하세요.

-materialIconsExtended = "1.7.8"
-androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "materialIconsExtended" }

Also applies to: 53-53

🤖 Prompt for AI Agents
In gradle/libs.versions.toml at lines 21 (and also at line 53), remove the
AndroidX materialIconsExtended version entry (materialIconsExtended = "1.7.8")
because Compose MPP uses JetBrains compose.materialIconsExtended; delete these
entries from the version catalog and ensure any build files that referenced the
removed catalog key are updated to depend on the JetBrains
compose.materialIconsExtended coordinate instead (or rely on the compose
plugin’s bundled icons), so no dangling references remain.

@rbqks529 rbqks529 added OK Merge 완료된 PR and removed No Merge 아직 진행중인 PR labels Sep 24, 2025
@ikseong00 ikseong00 merged commit 2a6247b into WhosInRoom:develop Sep 25, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FEAT 기능 개발 OK Merge 완료된 PR UI UI 구현 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants