[FEAT] 로그인 기능 구현#15
Conversation
Walkthrough로그인·회원가입 흐름이 토큰 기반으로 재설계되고 회원가입 화면·뷰모델·유스케이스가 추가되었습니다. 네트워크(베이스 URL, Gson, OkHttp 로깅) 및 서버 에러 파싱 유틸이 도입되었고, 공통 UI(버튼/텍스트필드/스낵바), 테마(컬러·타입) 및 리소스가 대대적으로 갱신되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor U as 사용자
participant L as LoginScreen
participant VM as LoginViewModel
participant UC as LoginUseCase
participant R as AuthRepository
participant API as AuthApi
participant P as AuthPreferences
participant Nav as NavHost
U->>L: 이메일/비밀번호 입력
L->>VM: EmailChanged/PasswordChanged
VM-->>L: 상태(오류/유효성)
U->>L: 로그인 클릭
L->>VM: Submit
VM->>UC: invoke(email,password)
UC->>R: signIn(...)
R->>API: POST /login
API-->>R: ApiResponse<LoginResponseDto>
R->>P: saveToken(accessToken, refreshToken)
R-->>UC: User
UC-->>VM: User
VM-->>L: success=true
L->>Nav: 홈으로 이동 (백스택 정리)
alt 오류
VM->>VM: serverMessageOrNull() 호출
VM-->>L: errorMessage
L->>L: ShowErrorSnackBar 표시
end
sequenceDiagram
actor U as 사용자
participant S as SignUpScreen
participant VM as SignUpViewModel
participant UC as SignUpUseCase
participant R as AuthRepository
participant API as AuthApi
participant P as AuthPreferences
participant Nav as NavHost
U->>S: 가입 폼 입력
S->>VM: 필드별 Changed 이벤트
VM-->>S: 상태(오류/유효성)
U->>S: 가입 클릭
S->>VM: Submit
VM->>UC: invoke(...)
UC->>R: signUp(...)
R->>API: POST /signup
API-->>R: ApiResponse<SignUpResponseDto>
R->>R: signIn(email,password)
R->>API: POST /login
API-->>R: ApiResponse<LoginResponseDto>
R->>P: saveToken(accessToken, refreshToken)
R-->>UC: User
UC-->>VM: User
VM-->>S: success=true
S->>Nav: 홈으로 이동 (백스택 정리)
alt 오류
VM->>VM: serverMessageOrNull() 호출
VM-->>S: errorMessage
S->>S: ShowErrorSnackBar 표시
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (20)
app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt (1)
6-13: 시그니처 추가 방향은 적절합니다. 다만 파라미터 객체로 캡슐화하는 게 안전합니다.6개 위치 파라미터는 순서 실수·회귀 위험이 큽니다. SignUpParams 같은 DTO로 묶고 한 파라미터만 받는 형태를 권장합니다. 향후 필드 추가/옵셔널 처리도 유연해집니다. 에러 표준화가 필요하다면 Result/Either 반환도 고려해 주세요.
적용 예(diff):
- suspend fun signUp( - name: String, - workspace: String, - branch: String, - position: String, - email: String, - password: String - ): User + suspend fun signUp(params: SignUpParams): User파일 외 코드(예시):
data class SignUpParams( val name: String, val workspace: String, val branch: String, val position: String, val email: String, val password: String )app/src/main/res/drawable/square_logo.xml (1)
1-12: 벡터 리소스 추가 좋습니다. 색상 하드코딩은 리소스/테마 참조로 바꾸세요.
#8080FF를@color/…로 추출해 라이트/다크 테마 일관성을 확보하는 것을 권장합니다. 필요 시 뷰에서 tint로 테마 색을 적용하는 방식도 고려해 주세요.app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt (2)
3-3: 불필요한 import로 보입니다.
import android.R.attr.fontFamily는 사용되지 않아 보이며 혼동을 줄 수 있으니 제거를 권장합니다.
12-16: 폰트 웨이트 매핑 보정: medium 리소스를 FontWeight.Medium으로val GmarketSansFamily = FontFamily( Font(R.font.gmarket_sans_light, FontWeight.Light), - Font(R.font.gmarket_sans_medium, FontWeight.Normal), + Font(R.font.gmarket_sans_medium, FontWeight.Medium), Font(R.font.gmarket_sans_bold, FontWeight.Bold) )app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/SignUpResponseDto.kt (1)
1-7: 직렬화·난독화 대비
- 현재
release { isMinifyEnabled = false }로 난독화가 비활성화되어 있어 당장은 필드명이 변경되지 않지만, 향후 minify 활성화 시 DTO 필드가 변경될 수 있으므로@Keep또는 ProGuard keep 룰에 클래스 추가를 권장합니다.- API 응답 필드명이 코드와 달라질 수 있는 경우
@SerializedName("서버필드명")명시도 고려해주세요.app/src/main/res/drawable/ic_arrow_back.xml (1)
1-5: XML 포맷팅 개선을 권장합니다.벡터 드로어블이 기능적으로는 올바르며,
android:autoMirrored="true"를 사용하여 RTL 언어를 지원하는 것도 좋습니다. 하지만 가독성을 위해 XML을 여러 줄로 포맷팅하는 것이 좋습니다.다음과 같이 포맷팅을 개선할 수 있습니다:
-<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp"> - - <path android:fillColor="@color/text" android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/> - -</vector> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:autoMirrored="true" + android:tint="#000000"> + <path + android:fillColor="@color/text" + android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/> +</vector>app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt (2)
19-22: 타이포 수정:singUp→signUp.가독성을 위해 의존성 이름을 표준 철자로 교정하세요. Hilt는 타입으로 주입하므로 리네임에 안전합니다.
다음과 같이 변경:
-class SignUpViewModel @Inject constructor( - private val singUp: SignUpUseCase, +class SignUpViewModel @Inject constructor( + private val signUp: SignUpUseCase, private val application: Application ) : ViewModel() { @@ - singUp( + signUp( name = s.name, workspace = s.workspace, branch = s.branch,
55-63: 빈 else 블록 제거(Detekt 경고).
PasswordChanged처리부의 else가 비어 있습니다. 불필요 블록을 제거하세요. detekt.empty-blocks.EmptyElseBlock 경고와 일치합니다.detekt 경고 기반
is SignUpUiEvent.PasswordChanged -> { _state.value = _state.value.copy(password = e.password) validatePassword() if (_state.value.passwordCheck.isNotBlank()) { validatePasswordCheck() - } else { - - } + } }app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt (1)
193-201: 제출 버튼 활성화 정책 재고(선택).현재
enabled = state.isValid && !state.loading이라 미입력 시 제출 시도가 불가해 에러 노출 타이밍이 늦습니다. UX상 에러를 제출 시점에 보여주려면!state.loading만으로 두고 VM에서 검증/에러 세팅을 권장합니다.- enabled = state.isValid && !state.loading, + enabled = !state.loading,app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt (2)
20-21: 타이포 수정:singIn→signIn.의미 보존 및 가독성 향상을 위해 변수명을 표준 철자로 변경하세요.
-class LoginViewModel @Inject constructor( - private val singIn: LoginUseCase, +class LoginViewModel @Inject constructor( + private val signIn: LoginUseCase, private val application: Application ) : ViewModel() { @@ - runCatching { singIn(s.email, s.password) } + runCatching { signIn(s.email, s.password) }
48-60: 로그인 입력 검증 강화(이메일 형식/비밀번호 정책 적용).현재
validateNotEmpty만 사용합니다. 회원가입과 동일한validateEmail/validatePassword를 사용해 일관성/정확성을 높이세요.- private fun validateEmail() { - val result = AuthValidator.validateNotEmpty(_state.value.email, emailLabel) + private fun validateEmail() { + val result = AuthValidator.validateEmail(_state.value.email) _state.value = _state.value.copy( emailError = result.toErrorMessage() ) } - private fun validatePassword() { - val result = AuthValidator.validateNotEmpty(_state.value.password, passwordLabel) + private fun validatePassword() { + val result = AuthValidator.validatePassword(_state.value.password) _state.value = _state.value.copy( passwordError = result.toErrorMessage() ) }app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt (3)
69-73: SnackbarHost는 Scaffold의 snackbarHost 슬롯에 배치하세요.현재 TopSnackBarHost를 content 내부에 렌더링합니다. Scaffold의 snackbarHost 슬롯을 사용하면 레이아웃/인셋 및 z-order가 일관됩니다.
- Scaffold( -// contentWindowInsets = WindowInsets.ime, -// snackbarHost = { CommonSnackBarHost(snackBarHostState) } - ) { innerPadding -> + Scaffold( + snackbarHost = { TopSnackBarHost(snackBarHostState, extraTopPadding = 16.dp) } + ) { innerPadding -> @@ - TopSnackBarHost(snackBarHostState, extraTopPadding = 16.dp)Also applies to: 165-166
132-147: 회원가입 텍스트: 주석(annotation)만 추가되고 실제 클릭 처리가 문장 전체에만 적용됩니다.
- 문장 전체를 클릭 가능하게 둘 계획이면 pushStringAnnotation/pop은 불필요합니다.
- “회원가입” 부분만 클릭 가능해야 한다면 ClickableText로 범위를 처리하세요.
아래는 주석 제거(간소화) 예시입니다.
- pushStringAnnotation(tag = "SIGNUP", annotation = "signup") withStyle( style = SpanStyle( color = Main500, textDecoration = TextDecoration.Underline ) ) { append(stringResource(R.string.login_signup)) } - append(stringResource(R.string.login_do)) - pop() + append(stringResource(R.string.login_do))또는 ClickableText로 부분 클릭 처리 원하시면 알려주세요. 필요한 코드 제공 가능합니다.
Also applies to: 153-159
99-106: 이메일 입력의 키보드 타입 지정 권장.CommonTextField는 비밀번호가 아니면 KeyboardType.Text를 사용합니다. 로그인 UX를 위해 이메일 필드에 KeyboardType.Email 적용을 권합니다. 현재 CommonTextField에 keyboardOptions 파라미터가 없어 직접 지정이 어려우니, API에 keyboardOptions: KeyboardOptions = KeyboardOptions.Default를 추가하고 호출부에서 Email을 전달하는 개선을 제안합니다.
원하시면 CommonTextField와 호출부(LoginScreen)까지 포함한 패치 제안 드리겠습니다.
Also applies to: 108-116
app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt (1)
71-71: 다국어 메시지 포맷 인자를 확장 가능하게.단일 Any 보다는 가변 인자(vararg)가 향후 확장에 유리합니다(예: "%1$s (%2$s) ...").
- data class ErrorWithArgs(val messageResId: Int, val args: Any) : ValidationResult() + data class ErrorWithArgs(val messageResId: Int, vararg val args: Any) : ValidationResult()호출부 예:
ValidationResult.ErrorWithArgs(R.string.validation_field_required, fieldName)app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (3)
3-4: 불필요/혼동을 유발할 수 있는 import는 제거하세요.
android.R.attr.onClick,android.R.attr.text는 사용되지 않고 혼동을 줍니다.-import android.R.attr.onClick -import android.R.attr.text
29-47: KDoc 사용 예시가 최신 API와 불일치합니다.현재는
content슬롯 기반입니다.text = "Button"예시는 혼란을 줍니다. 최신 사용법으로 갱신해주세요.
120-131: Ghost 버튼의 활성 색상으로 disableColor 사용은 비직관적입니다.활성 상태에서는
textSecondaryColor()등 의도된 온서피스 색상이 더 자연스럽습니다.- colors = ButtonDefaults.textButtonColors( - contentColor = disableColor(), - disabledContentColor = textSecondaryColor() - ) + colors = ButtonDefaults.textButtonColors( + contentColor = textSecondaryColor(), + disabledContentColor = disableColor() + )app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt (2)
3-5: 불필요/오류 가능성이 있는 import 정리 필요.
android.R.attr.singleLine,android.R.attr.text는 사용되지 않습니다.
InputTransformation.Companion.keyboardOptions는 사용되지 않고 버전에 따라 존재하지 않을 수 있어 컴파일 이슈를 유발할 수 있습니다.-import android.R.attr.singleLine -import android.R.attr.text @@ -import androidx.compose.foundation.text.input.InputTransformation.Companion.keyboardOptionsAlso applies to: 10-11
31-41: keyboardOptions를 파라미터로 노출하는 확장 제안(이메일 키보드 등).호출부에서 이메일/전화/숫자 등 키보드 타입을 지정할 수 있도록 API를 확장하면 재사용성이 올라갑니다.
예시:
fun CommonTextField( modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, placeholder: String, enabled: Boolean = true, isPassword: Boolean = false, variant: TextFieldVariant = TextFieldVariant.Outlined, isError: Boolean = false, - errorMessage: String? = null + errorMessage: String? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default ) { @@ - keyboardOptions = KeyboardOptions(keyboardType = if (isPassword) KeyboardType.Password else KeyboardType.Text), + keyboardOptions = if (isPassword) + KeyboardOptions(keyboardType = KeyboardType.Password) + else keyboardOptions, @@ - keyboardOptions = KeyboardOptions(keyboardType = if (isPassword) KeyboardType.Password else KeyboardType.Text), + keyboardOptions = if (isPassword) + KeyboardOptions(keyboardType = KeyboardType.Password) + else keyboardOptions,LoginScreen의 이메일 입력에서는
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)로 지정 가능.Also applies to: 91-93, 121-123
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
app/src/main/res/font/gmarket_sans_bold.otfis excluded by!**/*.otfapp/src/main/res/font/gmarket_sans_light.otfis excluded by!**/*.otfapp/src/main/res/font/gmarket_sans_medium.otfis excluded by!**/*.otf
📒 Files selected for processing (36)
app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt(3 hunks)app/src/main/java/com/sampoom/android/core/network/ErrorHandling.kt(1 hunks)app/src/main/java/com/sampoom/android/core/network/NetworkModule.kt(1 hunks)app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt(14 hunks)app/src/main/java/com/sampoom/android/core/ui/component/CommonSnackBar.kt(1 hunks)app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt(3 hunks)app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt(1 hunks)app/src/main/java/com/sampoom/android/core/ui/theme/Theme.kt(1 hunks)app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/local/preferences/AuthPreferences.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/LoginResponseDto.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/SignUpRequestDto.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/SignUpResponseDto.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UserDto.kt(0 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt(2 hunks)app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/ui/LoginUiState.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt(2 hunks)app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiEvent.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiState.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt(1 hunks)app/src/main/res/drawable/ic_arrow_back.xml(1 hunks)app/src/main/res/drawable/oneline_logo.xml(1 hunks)app/src/main/res/drawable/outline_close.xml(1 hunks)app/src/main/res/drawable/square_logo.xml(1 hunks)app/src/main/res/values-night/colors.xml(1 hunks)app/src/main/res/values/colors.xml(1 hunks)app/src/main/res/values/strings.xml(2 hunks)
💤 Files with no reviewable changes (1)
- app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UserDto.kt
🧰 Additional context used
🧬 Code graph analysis (9)
app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt (1)
app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt (1)
signIn(15-15)
app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt (1)
app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt (1)
backgroundColor(34-35)
app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt (3)
app/src/main/java/com/sampoom/android/core/ui/component/CommonSnackBar.kt (3)
rememberCommonSnackBarHostState(35-36)ShowErrorSnackBar(38-60)TopSnackBarHost(62-111)app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt (1)
CommonTextField(29-138)app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (1)
CommonButton(48-154)
app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt (2)
app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt (3)
validateEmail(7-18)validatePassword(21-43)validatePasswordCheck(46-56)app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt (3)
validateEmail(48-53)validatePassword(55-60)submit(70-87)
app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt (2)
app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt (2)
validateEmail(7-18)validatePassword(21-43)app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt (3)
validateEmail(92-97)validatePassword(99-104)submit(121-151)
app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (1)
app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt (2)
disableColor(46-47)textSecondaryColor(43-44)
app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt (3)
app/src/main/java/com/sampoom/android/core/ui/component/CommonSnackBar.kt (3)
rememberCommonSnackBarHostState(35-36)ShowErrorSnackBar(38-60)TopSnackBarHost(62-111)app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt (1)
CommonTextField(29-138)app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (1)
CommonButton(48-154)
app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt (1)
app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt (1)
SignUpScreen(45-209)
app/src/main/java/com/sampoom/android/core/ui/component/CommonSnackBar.kt (1)
app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt (2)
backgroundCardColor(37-38)textColor(40-41)
🪛 detekt (1.23.8)
app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt
[warning] 60-62: This empty block of code can be removed.
(detekt.empty-blocks.EmptyElseBlock)
🔇 Additional comments (15)
app/src/main/res/drawable/oneline_logo.xml (1)
1-12: 벡터 드로어블 리소스가 올바르게 정의되었습니다.로고 리소스가 적절히 구성되어 있으며, @color/text를 사용하여 테마 시스템과 일관성을 유지하고 있습니다.
app/src/main/res/values/strings.xml (4)
5-5: 네비게이션 문자열 추가가 적절합니다.뒤로가기 문자열이 추가되어 접근성을 개선합니다.
13-21: 로그인 관련 문자열이 잘 구성되었습니다.플레이스홀더 텍스트가 "이메일"에서 "이메일 입력"으로 변경되어 사용자에게 더 명확한 안내를 제공합니다. 회원가입 링크 관련 문자열도 적절히 추가되었습니다.
23-38: 회원가입 문자열이 포괄적으로 추가되었습니다.이름, 지점, 직급, 이메일, 비밀번호 등 모든 필수 필드에 대한 타이틀과 플레이스홀더가 일관되게 정의되어 있습니다.
48-57: 유효성 검증 메시지가 명확하게 정의되었습니다.이메일, 비밀번호 유효성 검증에 필요한 모든 메시지가 포함되어 있으며, validation_field_required는 형식 문자열(%s)을 사용하여 재사용성을 높였습니다.
app/src/main/java/com/sampoom/android/core/ui/theme/Theme.kt (2)
13-17: 다크 모드 컬러 스킴이 새로운 Main 팔레트로 업데이트되었습니다.Purple/Pink 계열에서 Main500/Main300/Main100으로 변경되어 일관된 브랜드 컬러를 적용합니다.
19-23: 라이트 모드 컬러 스킴이 다크 모드와 일관되게 업데이트되었습니다.동일한 Main 팔레트를 사용하여 테마 간 일관성을 유지합니다.
app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt (1)
7-12: 유스케이스가 깔끔하게 구현되었습니다.
SignInUseCase에서LoginUseCase로 명확하게 이름이 변경되었으며, 리포지토리에 대한 단순한 위임을 통해 도메인 계층의 관심사를 잘 분리하고 있습니다.app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt (2)
15-16: 회원가입 API가 올바르게 추가되었습니다.새로운 회원가입 엔드포인트가 적절하게 정의되었으며,
ApiResponse래퍼를 사용하여 일관된 응답 처리를 지원합니다.
12-13: ```bash
#!/bin/bashKotlin 소스에서 이전 로그인 엔드포인트 참조 검색
echo "Kotlin(.kt) 파일에서 auth/login 검색:"
rg -n "auth/login" -g '*.kt' || echo "Kotlin 파일에서 참조 없음"echo -e "\n설정 파일에서 auth/login 검색:"
fd -e properties -e xml -e json | xargs -r rg -l "auth/login" 2>/dev/null || echo "설정 파일에서 참조 없음"</blockquote></details> <details> <summary>app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiEvent.kt (2)</summary><blockquote> `3-11`: **이벤트 모델링 전반은 적절합니다.** 입력 변경/제출 이벤트 분리가 명확합니다. 전반 LGTM입니다. --- `10-10`: **data object 사용 호환성 확인** `data object Submit`은 Kotlin 1.9+ 기능입니다. build.gradle(.kts) 또는 gradle.properties에서 Kotlin 플러그인 버전이 1.9 이상인지 확인하거나, 호환성을 우선한다면 `object Submit : SignUpUiEvent`로 변경하세요. ```shell #!/bin/bash rg -n 'kotlin_version' -g 'gradle.properties' rg -n 'id\("org.jetbrains.kotlin' -g '**/*.gradle.kts' -g '**/*.gradle'app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpUiState.kt (1)
3-37: 상태 모델링 LGTM.필드/에러/플래그 구성과
isValid파생 속성 구성이 명확합니다.app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt (1)
62-68: ErrorWithArgs.args는 Any 타입으로 spread 불필요
현재args가 배열이 아니므로*args사용 시 컴파일 에러가 발생합니다. 여러 포맷 인자를 지원하려면args를vararg args: Any또는Array<Any>로 변경한 뒤 spread를 적용하세요.Likely an incorrect or invalid review comment.
app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt (1)
113-119:ErrorWithArgs.args는 Any 타입이므로 spread 연산 불필요
args가 vararg가 아닌Any로 선언되어 있어*args사용 시 컴파일 에러가 발생합니다. 기존 코드 그대로 유지하세요.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (1)
29-47: 문서를 업데이트하여 변경된 API를 반영하세요.KDoc이 여전히 제거된
text파라미터를 참조하고 있습니다. 새로운content파라미터를 반영하도록 문서를 업데이트해야 합니다.다음 diff를 적용하여 문서를 업데이트하세요:
* Usage * ----- * CommonButton( - * text = "Button", + * content = { Text("Button", fontWeight = FontWeight.Bold) }, * variant = ButtonVariant.Primary, * onClick = { ... } * ) * * Optionally pass a leading icon: * CommonButton( - * text = "Button", + * content = { Text("Button", fontWeight = FontWeight.Bold) }, * variant = ButtonVariant.Primary, * leadingIcon = { Icon(painterResource(R.drawable.parts), contentDescription = null) }, * onClick = { ... } * )
♻️ Duplicate comments (1)
app/src/main/java/com/sampoom/android/feature/auth/data/local/preferences/AuthPreferences.kt (1)
28-34: 보안: 토큰은 암호화 저장이 필요합니다 (DataStore에서도 평문 저장 지양)현재 DataStore에 평문으로 저장됩니다. 탈취 위험이 있어 Android Keystore 기반 암호화( security-crypto )로 저장/복호화를 권장합니다.
적용 예시(핵심 아이디어):
- 저장 시: prefs[ACCESS_TOKEN] = cipher.encrypt(accessToken)
- 조회 시: cipher.decrypt(prefs[ACCESS_TOKEN])
- suspend fun saveToken(accessToken: String, refreshToken: String) { - dataStore.edit { prefs -> - prefs[Keys.ACCESS_TOKEN] = accessToken - prefs[Keys.REFRESH_TOKEN] = refreshToken - } - } + suspend fun saveToken(accessToken: String, refreshToken: String) { + dataStore.edit { prefs -> + prefs[Keys.ACCESS_TOKEN] = cipher.encrypt(accessToken) + prefs[Keys.REFRESH_TOKEN] = cipher.encrypt(refreshToken) + } + } - fun getAccessToken(): String? = runBlocking { - dataStore.data.first()[Keys.ACCESS_TOKEN] - } + fun getAccessToken(): String? = runBlocking { + dataStore.data.first()[Keys.ACCESS_TOKEN]?.let(cipher::decrypt) + } - fun getRefreshToken(): String? = runBlocking { - dataStore.data.first()[Keys.REFRESH_TOKEN] - } + fun getRefreshToken(): String? = runBlocking { + dataStore.data.first()[Keys.REFRESH_TOKEN]?.let(cipher::decrypt) + }파일 외부 보조 코드(예시, 실제 프로젝트에 맞게 조정 가능):
// 보조: 간단한 토큰 암복호화 헬퍼 (Android Keystore + AES/GCM) import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import android.util.Base64 import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec class TokenCipher(context: Context) { private val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() // 간단화를 위해 키 파생/관리 로직은 생략. 프로젝트 표준에 맞게 키관리 구현 권장. fun encrypt(plain: String): String { /* ... */ return plain /* placeholder */ } fun decrypt(enc: String?): String? { /* ... */ return enc /* placeholder */ } } // AuthPreferences 내부 private val cipher by lazy { TokenCipher(context) }원하시면 실제 동작 가능한 Keystore 기반 구현과 예외 처리/마이그레이션까지 포함해 PR에 패치로 넣어드릴게요.
Also applies to: 36-43
🧹 Nitpick comments (11)
app/build.gradle.kts (1)
81-83: DataStore 의존성 추가 LGTM. 보안 강화를 위해 security-crypto 추가 권장토큰을 평문으로 저장하지 않도록 Jetpack Security Crypto를 함께 도입해 주세요. toml에 alias 추가 후 아래와 같이 의존성도 추가하는 것을 권장합니다.
dependencies { // DataStore implementation(libs.androidx.datastore.preferences) + // Security Crypto (토큰 암호화) + implementation(libs.androidx.security.crypto) }libs.versions.toml에 androidx-security-crypto alias가 없다면 함께 추가해 주세요(아래 코멘트 참고). 최신 버전은 확인이 필요합니다.
app/src/main/java/com/sampoom/android/feature/auth/data/local/preferences/AuthPreferences.kt (3)
14-15: 기존 SharedPreferences → DataStore 마이그레이션 추가 제안기존에 SharedPreferences를 사용했다면 DataStore로 안전하게 이관하기 위해 SharedPreferencesMigration을 붙이는 것을 권장합니다. 데이터 유실을 방지합니다.
import androidx.datastore.preferences.preferencesDataStore +import androidx.datastore.preferences.SharedPreferencesMigration -// Per official guidance, DataStore instance should be single and at top-level. -private val Context.authDataStore by preferencesDataStore(name = "auth_prefs") +// Per official guidance, DataStore instance should be single and at top-level. +private val Context.authDataStore by preferencesDataStore( + name = "auth_prefs", + produceMigrations = { context -> + listOf( + // 기존 SharedPreferences 파일명 확인 필요 + SharedPreferencesMigration(context, "auth_prefs") + ) + } +)기존 SP 파일명/키가 다르다면 맞게 바꿔야 합니다. 알려주시면 마이그레이션 코드 정확히 잡아드리겠습니다.
36-43: runBlocking 지양: suspend/Flow 기반으로 전환 권장UI 스레드에서 호출될 경우 블로킹 위험이 있습니다. suspend 함수로 바꾸거나 Flow를 노출하세요.
- // Synchronous getters backed by runBlocking for minimal surface change - fun getAccessToken(): String? = runBlocking { - dataStore.data.first()[Keys.ACCESS_TOKEN] - } + // Suspend getters + suspend fun getAccessToken(): String? = + dataStore.data.first()[Keys.ACCESS_TOKEN] - fun getRefreshToken(): String? = runBlocking { - dataStore.data.first()[Keys.REFRESH_TOKEN] - } + suspend fun getRefreshToken(): String? = + dataStore.data.first()[Keys.REFRESH_TOKEN] - fun hasToken(): Boolean = !getAccessToken().isNullOrEmpty() && !getRefreshToken().isNullOrEmpty() + suspend fun hasToken(): Boolean = + dataStore.data.first().let { prefs -> + !prefs[Keys.ACCESS_TOKEN].isNullOrEmpty() && + !prefs[Keys.REFRESH_TOKEN].isNullOrEmpty() + }호출부가 동기 API를 요구(예: OkHttp Interceptor)한다면 그 경로에서만 runBlocking을 최소화해 래핑하는 방식으로 국소화하는 것을 권장합니다.
Also applies to: 49-49
19-19: 주입 어노테이션 통일성(nit): @ApplicationContext 사용의미 차이는 없지만, 일반적으로 파라미터에 직접 @ApplicationContext를 붙입니다.
- @param:ApplicationContext private val context: Context + @ApplicationContext private val context: Contextgradle/libs.versions.toml (1)
4-4: DataStore 버전 추가 LGTM 및 security-crypto 라이브러리 등록 제안DataStore 도입과 토큰 암호화를 위해
security-crypto라이브러리(alias:securityCrypto)를 다음과 같이 추가해 주세요.[versions] datastorePreferences = "1.1.7" +securityCrypto = "1.1.0" [libraries] androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" }app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt (2)
55-57: isLoggedIn 하드코딩 제거하고 실제 인증 상태로 결정하세요앱 시작 목적지 결정은 저장된 토큰/세션 기반으로 ViewModel/Repository의 Flow를 구독해 반영하는 것이 안전합니다. 로딩 상태(스플래시)도 고려해 주세요.
예시 (개념 스케치):
@Composable fun AppNavHost() { val authViewModel: AuthViewModel = hiltViewModel() val isLoggedIn by authViewModel.isLoggedIn.collectAsState(initial = false) NavHost( navController = navController, startDestination = if (isLoggedIn) ROUTE_HOME else ROUTE_LOGIN ) { /* ... */ } }
- 저장소: DataStore(토큰 존재), 또는 remote refresh 결과를 반영하는 Flow를 권장
- 로딩: isLoading이 true면 Splash/Progress 화면을 startDestination로 두고, 결과에 따라 로그인/홈으로 전환
116-123: PartsFab의 popUpTo는 불필요하며, 부모 그래프 startDestinationId 사용은 잠재적 리스크입니다부모 그래프의 startDestinationId(현재 로그인/홈 그래프)를 기준으로 popUpTo 하는 것은 의도치 않은 백스택 변형을 유발할 수 있습니다. 상세 화면 진입은 단순
navigate만으로 충분합니다.navController.navigate(ROUTE_PARTS) { - popUpTo(navController.graph.startDestinationId) { - saveState = true - } - launchSingleTop = true - restoreState = true + launchSingleTop = true }
- 필요 시 뒤로가기로 홈 복귀를 원하면 popUpTo 없이 단순 push가 자연스럽습니다.
- state 복원은 바텀탭 전환 시 내부 자식 NavHost에서만 사용하세요(이미 그렇게 구현되어 있음).
app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt (2)
135-151: CTA 문자열에 공백 추가(가독성 개선) + 부분 클릭을 원하면 ClickableText 고려현재
"계정이 없으신가요?회원가입하기"처럼 붙어 보입니다. 간단히 공백을 추가하세요.- val annotatedText = buildAnnotatedString { - append(stringResource(R.string.login_need_account)) + val annotatedText = buildAnnotatedString { + append(stringResource(R.string.login_need_account)) + append(" ") // 클릭 가능한 회원가입 텍스트 pushStringAnnotation(tag = "SIGNUP", annotation = "signup") withStyle( style = SpanStyle( color = Main500, textDecoration = TextDecoration.Underline ) ) { append(stringResource(R.string.login_signup)) } - append(stringResource(R.string.login_do)) pop() }선택(부분 클릭만 허용): ClickableText로
SIGNUP주석 영역만 처리import androidx.compose.foundation.text.ClickableText ClickableText( text = annotatedText, style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center), modifier = Modifier.padding(vertical = 24.dp), onClick = { offset -> annotatedText.getStringAnnotations(tag = "SIGNUP", start = offset, end = offset) .firstOrNull()?.let { onNavigateSignUp() } } )
72-75: IME 인셋 적용 정책을 화면 간 일관되게 맞추세요SignUpScreen은
contentWindowInsets = WindowInsets.ime을 쓰고, LoginScreen은 Column에imePadding()을 사용 중입니다. 중복 적용만 피하면 둘 다 가능하지만, 같은 패턴으로 통일하면 유지보수에 유리합니다.권장:
- 스캐폴드에
contentWindowInsets = WindowInsets.ime지정- 내부 레이아웃에서는 별도의
imePadding()제거- 두 화면 모두 동일 정책 적용 후 키보드 표시/숨김 시 레이아웃 이동 여부를 확인
Also applies to: 88-93
app/src/main/res/values/strings.xml (2)
37-38: 문구 일관성: “회원가입 중…”으로 통일 권장현재
"회원 가입 중…"(띄어쓰기)로 되어 있어"회원가입"과 불일치합니다. 다음처럼 수정하세요.- <string name="signup_button_signup_loading">회원 가입 중…</string> + <string name="signup_button_signup_loading">회원가입 중…</string>
19-22: CTA 문자열 분절 대신 단일 문자열로 관리 권장
login_need_account+login_signup+login_do분리 구성은 언어별 어순/공백 처리에 취약합니다. “계정이 없으신가요? 회원가입하기” 같은 단일 리소스로 관리하고, 강조/클릭 범위는 Span으로 처리하는 방식을 추천합니다.예:
- strings.xml:
<string name="login_cta_signup">계정이 없으신가요? 회원가입하기</string>- UI:
buildAnnotatedString로 “회원가입하기” 부분에만 스타일/클릭 부여
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
app/build.gradle.kts(1 hunks)app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt(3 hunks)app/src/main/java/com/sampoom/android/core/network/NetworkModule.kt(1 hunks)app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt(14 hunks)app/src/main/java/com/sampoom/android/core/ui/component/CommonSnackBar.kt(1 hunks)app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/local/preferences/AuthPreferences.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt(2 hunks)app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt(1 hunks)app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt(1 hunks)app/src/main/res/drawable/outline_close.xml(1 hunks)app/src/main/res/values/strings.xml(2 hunks)gradle/libs.versions.toml(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- app/src/main/res/drawable/outline_close.xml
- app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt
- app/src/main/java/com/sampoom/android/core/ui/component/CommonSnackBar.kt
🧰 Additional context used
🧬 Code graph analysis (4)
app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt (1)
app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt (1)
SignUpScreen(45-212)
app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt (3)
app/src/main/java/com/sampoom/android/core/ui/component/CommonSnackBar.kt (3)
rememberCommonSnackBarHostState(36-37)ShowErrorSnackBar(39-61)TopSnackBarHost(63-112)app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt (1)
CommonTextField(29-138)app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (1)
CommonButton(48-157)
app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt (1)
app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt (1)
signIn(15-15)
app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (1)
app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt (2)
disableColor(46-47)textSecondaryColor(43-44)
🔇 Additional comments (9)
app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (1)
66-155: LGTM! leadingIcon 일관성 문제가 해결되었습니다.모든 버튼 variant가 이제
leadingIcon을 일관되게 렌더링합니다. 이전 리뷰에서 지적된 Outlined/Ghost/Neutral variant에서 leadingIcon이 무시되던 문제가 수정되었습니다.app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt (2)
19-35: 이전 리뷰 이슈가 올바르게 수정되었습니다!
baseTypography를 활용하여 Material3의 기본 값(fontSize, lineHeight, letterSpacing)을 보존하면서 fontFamily만 GmarketSansFamily로 교체하는 방식으로 정확하게 구현되었습니다. 이전 리뷰에서 제기된 UI 깨짐 위험이 해결되었습니다.Based on past review comments.
12-16: 폰트 리소스 확인 완료
app/src/main/res/font/에 필요한gmarket_sans_light,gmarket_sans_medium,gmarket_sans_bold파일이 모두 존재합니다. 해당 정의를 그대로 유지하면 됩니다.app/src/main/java/com/sampoom/android/core/network/NetworkModule.kt (1)
21-23: ⚠ LOWER_CASE_WITH_UNDERSCORES 전역 적용 — DTO 매핑 호환성 검증 필요
- 프로젝트의 모든
data class에@SerializedName이 없어, 서버 JSON 필드 네이밍(snake_case)과 정확히 일치하는지 확인하세요- 불일치 위험이 있는 필드는
@SerializedName("json_field_name")을 명시적으로 지정하는 방안을 고려하세요app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt (3)
40-41: 변경사항 확인 완료DTO 구조가 중첩된
data필드를 사용하도록 변경된 것에 맞춰 토큰 저장과 모델 변환 로직이 일관되게 업데이트되었습니다.
44-44: 코드 스타일 개선단일 라인으로 간결하게 리팩토링되었습니다.
16-33: SignUpResponseDto를 무시하고 곧바로 signIn 호출하는 것은 의도된 설계입니다
SignUp API는 토큰을 반환하지 않으므로, signUp 성공 후 즉시 signIn을 통해 User를 반환하도록 한 레포지토리 인터페이스 설계에 부합합니다.app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt (1)
63-70: 인증 성공 시 백스택 정리 LGTM
popUpTo(ROUTE_LOGIN) { inclusive = true }로 로그인·회원가입 스택이 모두 제거되어 홈에서 뒤로가기도 자연스럽습니다. 회원가입 플로우까지 동일하게 처리된 점 좋습니다.Also applies to: 73-82
app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt (1)
62-64: 네비게이션 트리거 처리 적절합니다
LaunchedEffect(state.success)로 재조합 중복 호출을 방지한 점 좋습니다.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (1)
26-44: KDoc 예제를 현재 API에 맞게 업데이트하세요.주석의 사용 예제가 여전히 제거된
text파라미터를 참조하고 있습니다. 실제 API는content파라미터를 사용하므로 문서를 업데이트해야 합니다.다음 diff를 적용하여 KDoc을 업데이트하세요:
/** * Sampoom common button with multiple visual variants. * * Usage * ----- * CommonButton( - * text = "Button", * variant = ButtonVariant.Primary, * onClick = { ... } + * ) { + * Text("Button") + * } * * Optionally pass a leading icon: * CommonButton( - * text = "Button", * variant = ButtonVariant.Primary, * leadingIcon = { Icon(painterResource(R.drawable.parts), contentDescription = null) }, * onClick = { ... } + * ) { + * Text("Button") + * } */
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
app/build.gradle.kts(2 hunks)app/src/main/java/com/sampoom/android/core/network/NetworkModule.kt(1 hunks)app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt(14 hunks)app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt(1 hunks)gradle/libs.versions.toml(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- gradle/libs.versions.toml
- app/src/main/java/com/sampoom/android/core/network/NetworkModule.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (1)
app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt (2)
disableColor(46-47)textSecondaryColor(43-44)
🔇 Additional comments (4)
app/build.gradle.kts (2)
56-58: BuildConfig 옵션 활성화 확인 완료로그인/네트워크 설정에 필요한
BuildConfig상수 사용을 위한 설정으로 보이며, 기존 설정과 충돌도 없습니다. 그대로 진행하셔도 됩니다.
82-87: 신규 의존성 추가 적절합니다네트워크 로깅 및 DataStore 기반 인증 정보 저장 흐름과 맞물린 의존성 추가로 보이며, 스코프나 구성에서도 문제 없습니다. 👍
app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt (2)
82-98: Secondary variant의 색상이 디자인 의도와 일치하는지 확인하세요.Secondary variant가 "Filled tonal (light purple)"로 문서화되어 있지만, Primary와 동일한
Main500을containerColor로 사용하고 있습니다.FilledTonalButton은 일반적으로 Primary보다 연한 색상(예: Main100 또는 Main200)을 사용하여 시각적 위계를 구분합니다.디자인 시스템에서 Secondary variant가 Primary와 동일한 색상을 의도한 것인지 확인하고, 만약 다른 색상을 사용해야 한다면 다음과 같이 수정하세요:
colors = ButtonDefaults.filledTonalButtonColors( containerColor = Main100, // 또는 디자인에 맞는 더 연한 톤 contentColor = White, disabledContainerColor = disableColor(), disabledContentColor = textSecondaryColor() )
62-152: 이전 리뷰의 모든 문제가 해결되었습니다!과거 리뷰에서 지적된 문제들이 모두 수정되었습니다:
- ✅
leadingIcon이 이제 모든 variant(Primary, Secondary, Outlined, Ghost, Neutral)에서 일관되게 렌더링됩니다.- ✅ 사용되지 않는 import(
android.R.attr.onClick,android.R.attr.text)가 제거되었습니다.- ✅ Ghost variant가 활성화 상태에서 올바른 색상(
textSecondaryColor())을 사용합니다.현재 구현은 일관되고 명확합니다.
📝 Summary
로그인 기능 구현
🙏 Question & PR point
📬 Reference
Summary by CodeRabbit
New Features
UI/UX
Chores