-
Notifications
You must be signed in to change notification settings - Fork 4
[FEAT] 로그인 화면 API 연결 #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FEAT] 로그인 화면 API 연결 #32
Conversation
Walkthrough토큰 만료 처리와 재발급 흐름을 도입하고, 인증 전용 원격 데이터소스/레포지토리/DI/뷰모델/화면을 추가·개편했다. 라우트에 파라미터를 추가하여 회원가입/검증 플로우를 연결했고, 스플래시에서 토큰 확인 후 분기한다. 다양한 요청/응답 DTO를 추가하고 기존 로그인/재발급 응답 스키마를 갱신했다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant S as SplashScreen
participant VM as SplashViewModel
participant T as TokenManager
participant N as NavController
S->>VM: checkToken()
VM->>T: getAccessToken()
T-->>VM: token or null
VM-->>S: uiState(isLoading=false, hasToken=bool)
alt hasToken
S->>N: navigate(Home)
else
S->>N: navigate(Login)
end
sequenceDiagram
autonumber
participant L as LoginScreen
participant V as LoginViewModel
participant R as AuthRepository
participant DS as RemoteAuthDataSource
participant TM as TokenManager
participant N as NavController
L->>V: login(email, password)
V->>R: login(...)
R->>DS: POST auth/login
DS-->>R: ApiResult<LoginResponseDto>
R-->>V: ApiResult
alt Success with token
V->>TM: saveTokens(access, refresh)
V-->>L: uiState(isSuccess=true)
L->>N: navigate(Home)
else Error/No token
V-->>L: uiState(errorMessage)
end
sequenceDiagram
autonumber
participant C as HttpClient
participant SR as Server
participant TM as TokenManager
participant EM as TokenExpiredManager
C->>SR: Request with AccessToken
SR-->>C: 401 Unauthorized
C->>TM: getRefreshToken()
alt refresh token exists
C->>SR: POST auth/reissue
SR-->>C: ReissueTokenResponseDto
alt success & tokens
C->>TM: saveTokens(new)
C->>SR: Retry original request
SR-->>C: Response
else failure
C->>TM: clearTokens()
C->>EM: setTokenExpired()
C-->>C: Return failure
end
else no refresh
C->>TM: clearTokens()
C->>EM: setTokenExpired()
C-->>C: Return failure
end
sequenceDiagram
autonumber
participant SE as SignupEmailInput
participant AR as AuthRepository
participant DS as RemoteAuthDS
participant EV as EmailVerificationScreen
participant NR as NicknameInputScreen
SE->>AR: sendEmailVerification(email)
AR->>DS: POST auth/email/send
DS-->>AR: ApiResult<EmailVerificationResponseDto>
AR-->>SE: ApiResult
alt success
SE->>EV: navigate(email)
EV->>AR: validateEmailCode(email, code)
AR->>DS: POST auth/email/validation
DS-->>AR: ApiResult
AR-->>EV: ApiResult
alt success
EV->>NR: navigate(email, password?)
else error
EV-->>EV: show error
end
else error
SE-->>SE: show error
end
sequenceDiagram
autonumber
participant A as App
participant EM as TokenExpiredManager
participant N as NavController
EM-->>A: isTokenExpired=true (StateFlow)
A->>N: navigate(Login, popUpTo=all, inclusive=true)
A->>EM: reset()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/FindPasswordScreen.kt (1)
52-131: 스낵바 플래그가 계속 true로 남아요.
showSuccessToast를 true로만 세팅하고 다시 false로 돌리지 않아서, 상태가 다른 값으로 바뀌어도 Snackbar가 계속 붙어 있을 수 있어요. delay 이후와 다른 상태 분기에서 false로 리셋 한번 해주면 깔끔하게 사라집니다.when (uiState) { is FindPasswordUiState.Success -> { showSuccessToast = true delay(2000) // 2초 후 로그인 화면으로 이동 + showSuccessToast = false onPasswordResetComplete() } - else -> {} + else -> { + showSuccessToast = false + } }
♻️ Duplicate comments (3)
composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteAuthDataSource.kt (3)
83-109: 에러 응답 파싱 시 예외 처리가 없습니다.100줄에서 에러 응답 파싱 실패 시 안전장치가 없어요. 이는
sendEmailVerification과 동일한 문제입니다.
111-138: 에러 응답 파싱 시 예외 처리가 없습니다.129줄에서 에러 응답 파싱 실패 시 안전장치가 없어요. 다른 함수들과 동일한 문제입니다.
140-163: 에러 응답 파싱 시 예외 처리가 없습니다.154줄에서 에러 응답 파싱 실패 시 안전장치가 없어요. 다른 함수들과 동일한 문제입니다.
🧹 Nitpick comments (4)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt (1)
69-69: 중복된 clickable 수정자를 검토해주세요.외부 Box(line 69)와 decorationBox의 내부 Box(lines 101-103) 모두에 clickable이 적용되어 있습니다. 이것이 의도적인 것인지, 아니면 터치 영역을 넓히기 위한 것인지 확인해주세요. 일반적으로 하나만 있어도 충분할 수 있습니다.
외부 Box의 clickable만 유지하는 것을 고려해보세요:
BasicTextField( ... decorationBox = { innerTextField -> Box( - Modifier - .fillMaxSize() - .clickable { onClick?.invoke() }, + Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { innerTextField() } } )Also applies to: 101-103
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/LoginScreen.kt (1)
133-140: 로딩 중 시각적 피드백 추가 제안로그인 요청 중에는 버튼만 비활성화되고 별도의 로딩 피드백이 없어 사용자가 진행 상황을 파악하기 어렵습니다.
uiState.isLoading이true일 때 버튼 내부나 바로 아래에CircularProgressIndicator를 노출하는 식으로 보완하면 UX가 한층 나아질 것 같아요.composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt (1)
62-64: UI와 데이터 레이어 결합 분리 제안화면 컴포저블 안에서 바로
koinInject<AuthRepository>()를 호출하면 프리뷰나 분리된 테스트 환경에서 의존성 주입이 깨지고, UI가 도메인 레이어를 직접 다루게 됩니다. 뷰모델로 로직을 위임하거나, 최소한 외부에서AuthRepository를 주입받도록 바꿔서 UI와 데이터 레이어의 경계를 명확히 해두는 편이 유지보수에 더 안전해 보여요.composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt (1)
53-55: 로그인 성공 시 백스택 정리 제안로그인 후
Home으로 이동해도 뒤로가기를 누르면 다시 로그인 화면이 나타나 UX가 어색합니다.AuthGraph를popUp해서 인증 플로우를 스택에서 제거하는 편이 자연스럽습니다.다음과 같이 수정해 주세요:
onNavigateToHome = { - navController.navigate(Route.Home) + navController.navigate(Route.Home) { + popUpTo(Route.AuthGraph) { inclusive = true } + } },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (33)
.gitignore(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/App.kt(2 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/core/auth/TokenExpiredManager.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/core/network/HttpClientFactory.kt(4 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/EmailValidationRequestDto.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/EmailVerificationRequestDto.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/FindPasswordRequestDto.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/SignupRequestDto.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/EmailVerificationResponseDto.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/FindPasswordResponseDto.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/LoginResponseDto.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ReissueTokenResponseDto.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/SignupResponseDto.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteAuthDataSource.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteMemberDataSource.kt(0 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/AuthRepository.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/MemberRepository.kt(0 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt(2 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt(2 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt(7 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/FindPasswordScreen.kt(5 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/LoginScreen.kt(4 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt(5 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(4 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/NumberInputBox.kt(4 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/FindPasswordViewModel.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/LoginViewModel.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/SignupViewModel.kt(1 hunks)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/SplashViewModel.kt(1 hunks)
💤 Files with no reviewable changes (2)
- composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/MemberRepository.kt
- composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteMemberDataSource.kt
🧰 Additional context used
🧬 Code graph analysis (12)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/EmailValidationRequestDto.kt (2)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/LoginRequestDto.kt (1)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/ReissueTokenRequestDto.kt (1)
refreshToken(6-10)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/EmailVerificationResponseDto.kt (3)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/UpdateMyInfoResponseDto.kt (1)
success(6-16)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/AddClubResponseDto.kt (1)
success(6-16)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ErrorResponseDto.kt (1)
success(6-16)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/FindPasswordRequestDto.kt (1)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/LoginRequestDto.kt (1)
composeApp/src/commonMain/kotlin/org/whosin/client/core/network/HttpClientFactory.kt (2)
composeApp/src/commonMain/kotlin/org/whosin/client/core/datastore/TokenManagerImpl.kt (1)
dataStore(9-28)composeApp/src/commonMain/kotlin/org/whosin/client/core/datastore/TokenManager.kt (2)
getRefreshToken(5-5)getAccessToken(3-8)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ReissueTokenResponseDto.kt (3)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/ReissueTokenRequestDto.kt (1)
refreshToken(6-10)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/AddClubResponseDto.kt (1)
success(6-16)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ErrorResponseDto.kt (1)
success(6-16)
composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt (5)
composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteMemberDataSource.kt (2)
client(19-115)login(22-45)composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteClubDataSource.kt (1)
client(19-165)composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/MemberRepository.kt (1)
dataSource(9-21)composeApp/src/commonMain/kotlin/org/whosin/client/data/repository/ClubRepository.kt (1)
dataSource(10-30)composeApp/src/jvmMain/kotlin/org/whosin/client/di/DIModules.jvm.kt (1)
single(14-18)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/SignupResponseDto.kt (2)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/AddClubResponseDto.kt (1)
success(6-16)composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/ErrorResponseDto.kt (1)
success(6-16)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/LoginViewModel.kt (1)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/dummy/TokenTestViewModel.kt (1)
tokenManager(11-55)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt (1)
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 (1)
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/EmailVerificationScreen.kt (1)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt (1)
CommonLoginButton(18-47)
composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt (3)
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt (1)
EmailVerificationScreen(48-224)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/PasswordInputScreen.kt (1)
PasswordInputScreen(37-132)composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt (1)
NicknameInputScreen(43-153)
🪛 Gitleaks (8.28.0)
composeApp/src/commonMain/kotlin/org/whosin/client/core/network/HttpClientFactory.kt
[high] 53-53: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ios-build
- GitHub Check: android-build
🔇 Additional comments (10)
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/SignupRequestDto.kt (1)
6-14: 깔끔한 DTO 구현입니다!회원가입에 필요한 필드들이 적절하게 정의되어 있고, 다른 요청 DTO들과 일관된 패턴을 따르고 있네요.
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt (2)
39-39: onClick 콜백 추가 좋습니다!클럽 코드 입력 등 다른 화면에서 포커스 제어가 필요한 경우를 위한 좋은 확장이네요.
93-93: 투명한 커서로 인한 UX 영향을 확인해주세요.커서를 투명하게 만들면 사용자가 현재 입력 위치를 파악하기 어려울 수 있습니다. 특히 숫자 입력 중 피드백이 부족할 수 있으니, 실제 사용성 테스트를 통해 이 변경이 의도한 대로 동작하는지 확인하는 것을 권장합니다.
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/response/EmailVerificationResponseDto.kt (1)
6-18: 일관된 응답 DTO 구조입니다!기존 응답 DTO들(AddClubResponseDto, ErrorResponseDto)과 동일한 패턴을 따르고 있어 일관성이 좋습니다. 선택적 필드들도 적절하게 null 기본값으로 처리되어 있네요.
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/PasswordInputScreen.kt (1)
46-48: 비밀번호 유효성 검증 강화 좋습니다!두 비밀번호 모두 최소 8자 이상이면서 동일해야만 완료되도록 로직이 개선되었네요. 사용자가 실수로 짧은 비밀번호를 입력하는 것을 방지할 수 있어 보안에도 도움이 됩니다.
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/EmailValidationRequestDto.kt (1)
6-12: 깔끔한 요청 DTO입니다!이메일 인증 코드 검증을 위한 필드들이 명확하게 정의되어 있고, 다른 요청 DTO들과 일관된 패턴을 따르고 있습니다.
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/FindPasswordRequestDto.kt (1)
6-10: 심플하고 명확한 DTO입니다!비밀번호 찾기에 필요한 이메일 필드만 포함하고 있어 목적이 명확하고, 기존 DTO 패턴을 잘 따르고 있네요.
composeApp/src/commonMain/kotlin/org/whosin/client/data/dto/request/EmailVerificationRequestDto.kt (1)
6-10: 간결한 요청 DTO입니다!이메일 인증 요청에 필요한 필드만 포함하고 있고, 다른 DTO들과 일관된 구조를 가지고 있습니다.
composeApp/src/commonMain/kotlin/org/whosin/client/App.kt (1)
23-32: TokenExpiredManager는 MutableStateFlow로 스레드 안전하며 중복 네비게이션 우려가 없습니다composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SplashScreen.kt (1)
38-45: 초기 로딩 상태 확인 필요이
LaunchedEffect는isLoading이false가 되는 순간 바로 실행되어 홈/로그인으로 분기합니다. 만약SplashViewModel의 초기 상태가isLoading = false라면,checkToken()이 끝나기도 전에 로그인 화면으로 이동해 버릴 수 있어요. 초기값이true로 설정되어 있는지, 혹은 토큰 검사 완료 시점에만false가 되도록 보장되는지 한 번 더 확인 부탁드립니다.
| !src/**/build/ | ||
| local.properties | ||
| .idea | ||
| **/network_security_config.xml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
network_security_config.xml은 버전 관리 대상입니다.
앱의 네트워크 보안 정책을 정의하는 파일을 전역으로 무시하면, 환경마다 다른 설정이 조용히 적용되고 보안 검토도 불가능해집니다. 규정·릴리스 안정성 모두 큰 리스크이니 해당 ignore 항목을 제거하거나, 필요한 경우 별도의 샘플 파일/로컬 경로만 선택적으로 무시해 주세요.
🤖 Prompt for AI Agents
In .gitignore around line 9, the global ignore entry for
**/network_security_config.xml causes the app's network security policy file to
be excluded from version control; remove this ignore line so
network_security_config.xml files are tracked, or replace it with a scoped
ignore that only excludes a sample/local path (e.g.,
/app/src/main/res/xml/network_security_config.sample.xml or a developer-specific
path) and add a tracked canonical network_security_config.xml (or a template
checked into repo) so environments and security reviews remain consistent.
| val accessToken = tokenManager.getAccessToken() | ||
| ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk" | ||
| val refreshToken = tokenManager.getRefreshToken() ?: "no_token" | ||
| BearerTokens(accessToken = accessToken, refreshToken = refreshToken) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하드코딩된 JWT 제거 필요
Line 52~55에서 액세스 토큰과 리프레시 토큰이 하드코딩된 문자열로 대체되고 있습니다. 인증이 되지 않은 상태에서도 이 더미 토큰이 모든 요청 헤더에 실리기 때문에, 실서버로 빌드가 나가면 불특정 사용자에게 동일한 권한이 노출되는 치명적인 보안 사고로 이어질 수 있습니다. 또한 정적 분석(gitleaks)도 JWT 유출로 경고하고 있습니다. 토큰이 없으면 null을 반환해 Bearer 헤더를 보내지 않도록 고쳐 주세요.
- loadTokens {
- val accessToken = tokenManager.getAccessToken()
- ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk"
- val refreshToken = tokenManager.getRefreshToken() ?: "no_token"
- BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
- }
+ loadTokens {
+ val accessToken = tokenManager.getAccessToken()
+ val refreshToken = tokenManager.getRefreshToken()
+ if (accessToken != null && refreshToken != null) {
+ BearerTokens(accessToken = accessToken, refreshToken = refreshToken)
+ } else {
+ null
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val accessToken = tokenManager.getAccessToken() | |
| ?: "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJ1c2VySWQiOjUsInByb3ZpZGVySWQiOiJsb2NhbGhvc3QiLCJuYW1lIjoi7Iug7KKF7JykIiwicm9sZSI6IlJPTEVfTUVNQkVSIiwiaWF0IjoxNzU5MzgyMzg3LCJleHAiOjE3NTk5ODcxODd9.kT9IH60aCA-6ByEITb-_qPAJY0Oik1bbPKqcBWXzHIk" | |
| val refreshToken = tokenManager.getRefreshToken() ?: "no_token" | |
| BearerTokens(accessToken = accessToken, refreshToken = refreshToken) | |
| loadTokens { | |
| val accessToken = tokenManager.getAccessToken() | |
| val refreshToken = tokenManager.getRefreshToken() | |
| if (accessToken != null && refreshToken != null) { | |
| BearerTokens(accessToken = accessToken, refreshToken = refreshToken) | |
| } else { | |
| null | |
| } | |
| } |
🧰 Tools
🪛 Gitleaks (8.28.0)
[high] 53-53: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/org/whosin/client/core/network/HttpClientFactory.kt
around lines 52 to 55, remove the hardcoded JWT and "no_token" fallback;
instead, do not fabricate tokens — if tokenManager.getAccessToken() returns null
then return null (or omit constructing BearerTokens) so no Authorization header
is sent, and only construct BearerTokens when a real access token exists
(refreshToken may be null). Ensure no static token strings remain and update any
callers to handle a nullable BearerTokens result.
| suspend fun login(email: String, password: String): ApiResult<LoginResponseDto> { | ||
| return try { | ||
| val response: HttpResponse = client | ||
| .post("auth/login") { | ||
| setBody( | ||
| LoginRequestDto(email = email, password = password) | ||
| ) | ||
| } | ||
| if (response.status.isSuccess()) { | ||
| ApiResult.Success( | ||
| data = response.body(), | ||
| statusCode = response.status.value | ||
| ) | ||
| } else { | ||
| // 에러 응답 파싱 시도 | ||
| try { | ||
| val errorResponse: ErrorResponseDto = response.body() | ||
| ApiResult.Error( | ||
| code = response.status.value, | ||
| message = errorResponse.message | ||
| ) | ||
| } catch (e: Exception) { | ||
| // 파싱 실패 시 기본 에러 메시지 | ||
| ApiResult.Error( | ||
| code = response.status.value, | ||
| message = "HTTP Error: ${response.status.value}" | ||
| ) | ||
| } | ||
| } | ||
| } catch (t: Throwable) { | ||
| ApiResult.Error(message = t.message, cause = t) | ||
| } | ||
| } | ||
|
|
||
| suspend fun sendEmailVerification(email: String): ApiResult<EmailVerificationResponseDto> { | ||
| return try { | ||
| val response: HttpResponse = client | ||
| .post("auth/email/send") { | ||
| setBody( | ||
| EmailVerificationRequestDto(email = email) | ||
| ) | ||
| } | ||
| if (response.status.isSuccess()) { | ||
| ApiResult.Success( | ||
| data = response.body(), | ||
| statusCode = response.status.value | ||
| ) | ||
| } else { | ||
| val errorResponse: EmailVerificationResponseDto = response.body() | ||
| ApiResult.Error( | ||
| code = errorResponse.status, | ||
| message = errorResponse.message | ||
| ) | ||
| } | ||
| } catch (t: Throwable) { | ||
| ApiResult.Error(message = t.message, cause = t) | ||
| } | ||
| } | ||
|
|
||
| suspend fun validateEmailCode( | ||
| email: String, | ||
| authCode: String | ||
| ): ApiResult<EmailVerificationResponseDto> { | ||
| return try { | ||
| val response: HttpResponse = client | ||
| .post("auth/email/validation") { | ||
| setBody( | ||
| EmailValidationRequestDto(email = email, authCode = authCode) | ||
| ) | ||
| } | ||
| if (response.status.isSuccess()) { | ||
| ApiResult.Success( | ||
| data = response.body(), | ||
| statusCode = response.status.value | ||
| ) | ||
| } else { | ||
| val errorResponse: EmailVerificationResponseDto = response.body() | ||
| ApiResult.Error( | ||
| code = errorResponse.status, | ||
| message = errorResponse.message | ||
| ) | ||
| } | ||
| } catch (t: Throwable) { | ||
| ApiResult.Error(message = t.message, cause = t) | ||
| } | ||
| } | ||
|
|
||
| suspend fun signup( | ||
| email: String, | ||
| password: String, | ||
| nickName: String | ||
| ): ApiResult<SignupResponseDto> { | ||
| return try { | ||
| val response: HttpResponse = client | ||
| .post("users/signup") { | ||
| setBody( | ||
| SignupRequestDto(email = email, password = password, nickName = nickName) | ||
| ) | ||
| } | ||
| if (response.status.isSuccess()) { | ||
| ApiResult.Success( | ||
| data = response.body(), | ||
| statusCode = response.status.value | ||
| ) | ||
| } else { | ||
| val errorResponse: SignupResponseDto = response.body() | ||
| ApiResult.Error( | ||
| code = errorResponse.status, | ||
| message = errorResponse.message | ||
| ) | ||
| } | ||
| } catch (t: Throwable) { | ||
| ApiResult.Error(message = t.message, cause = t) | ||
| } | ||
| } | ||
|
|
||
| suspend fun sendPasswordResetEmail(email: String): ApiResult<FindPasswordResponseDto> { | ||
| return try { | ||
| val response: HttpResponse = client | ||
| .post("auth/email/find-password") { | ||
| setBody( | ||
| FindPasswordRequestDto(email = email) | ||
| ) | ||
| } | ||
| if (response.status.isSuccess()) { | ||
| ApiResult.Success( | ||
| data = response.body(), | ||
| statusCode = response.status.value | ||
| ) | ||
| } else { | ||
| val errorResponse: FindPasswordResponseDto = response.body() | ||
| ApiResult.Error( | ||
| code = errorResponse.status, | ||
| message = errorResponse.message | ||
| ) | ||
| } | ||
| } catch (t: Throwable) { | ||
| ApiResult.Error(message = t.message, cause = t) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
함수들 간 코드 중복이 많습니다.
5개 함수가 거의 동일한 구조를 가지고 있어요. 엔드포인트 URL과 DTO만 다를 뿐입니다. 이런 중복은 유지보수를 어렵게 만들고 버그 발생 가능성도 높입니다.
제네릭 함수로 공통 로직을 추출하면 코드가 훨씬 깔끔해지고, 위에서 지적한 에러 처리 불일치 문제도 한 번에 해결할 수 있어요.
예시:
private suspend inline fun <reified T, reified E> executePost(
endpoint: String,
request: Any,
useErrorDto: Boolean = true
): ApiResult<T> {
return try {
val response: HttpResponse = client.post(endpoint) {
setBody(request)
}
if (response.status.isSuccess()) {
ApiResult.Success(
data = response.body(),
statusCode = response.status.value
)
} else {
try {
val errorResponse: E = response.body()
// errorResponse에서 code와 message 추출 로직
ApiResult.Error(
code = response.status.value,
message = extractErrorMessage(errorResponse)
)
} catch (e: Exception) {
ApiResult.Error(
code = response.status.value,
message = "HTTP Error: ${response.status.value}"
)
}
}
} catch (t: Throwable) {
ApiResult.Error(message = t.message ?: "Unknown error", cause = t)
}
}
suspend fun login(email: String, password: String): ApiResult<LoginResponseDto> {
return executePost<LoginResponseDto, ErrorResponseDto>(
endpoint = "auth/login",
request = LoginRequestDto(email, password)
)
}🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteAuthDataSource.kt
around lines 24-163, multiple POST functions repeat the same
request/response/error-handling logic; extract a single private suspend inline
reified generic helper (e.g., executePost<T, E>) that accepts endpoint and
request payload, performs the client.post call, returns ApiResult.Success on
success, and on non-success attempts to parse an error DTO E (with fallback to a
generic "HTTP Error: <status>" message) and returns ApiResult.Error; ensure the
helper normalizes throwable handling (use t.message ?: "Unknown error") and
provides a small extractor to pull code/message from different error DTO shapes,
then refactor
login/sendEmailVerification/validateEmailCode/signup/sendPasswordResetEmail to
call this helper with appropriate request and response DTO types.
| suspend fun login(email: String, password: String): ApiResult<LoginResponseDto> { | ||
| return try { | ||
| val response: HttpResponse = client | ||
| .post("auth/login") { | ||
| setBody( | ||
| LoginRequestDto(email = email, password = password) | ||
| ) | ||
| } | ||
| if (response.status.isSuccess()) { | ||
| ApiResult.Success( | ||
| data = response.body(), | ||
| statusCode = response.status.value | ||
| ) | ||
| } else { | ||
| // 에러 응답 파싱 시도 | ||
| try { | ||
| val errorResponse: ErrorResponseDto = response.body() | ||
| ApiResult.Error( | ||
| code = response.status.value, | ||
| message = errorResponse.message | ||
| ) | ||
| } catch (e: Exception) { | ||
| // 파싱 실패 시 기본 에러 메시지 | ||
| ApiResult.Error( | ||
| code = response.status.value, | ||
| message = "HTTP Error: ${response.status.value}" | ||
| ) | ||
| } | ||
| } | ||
| } catch (t: Throwable) { | ||
| ApiResult.Error(message = t.message, cause = t) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
login 함수의 에러 처리 방식이 다른 함수들과 다릅니다.
login 함수는 ErrorResponseDto를 사용해 에러를 파싱하고 실패 시 폴백 처리까지 제공하는데(39-51줄), 나머지 함수들은 각자의 응답 DTO를 직접 사용하고 있어요. 이런 불일치가 있으면 나중에 에러 응답 구조가 달라질 때 문제가 생길 수 있습니다.
전체 함수에서 일관된 에러 처리 전략을 사용하는 걸 권장합니다.
🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteAuthDataSource.kt
around lines 24 to 56, the login function implements special-case error parsing
using ErrorResponseDto with a fallback, while other functions use their own
response DTOs directly; make error handling consistent by extracting a shared
helper that, given an HttpResponse, returns ApiResult.Error by attempting to
parse ErrorResponseDto and falling back to a generic message if parsing fails,
then refactor login and all other remote methods to use that helper for
non-success responses and keep try/catch to convert thrown exceptions into
ApiResult.Error (preserving message and cause).
| } | ||
| } | ||
| } catch (t: Throwable) { | ||
| ApiResult.Error(message = t.message, cause = t) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 메시지가 null일 수 있습니다.
t.message는 null일 수 있는데, 이 경우 더 유용한 기본 메시지를 제공하는 게 좋을 것 같아요.
예시:
-ApiResult.Error(message = t.message, cause = t)
+ApiResult.Error(message = t.message ?: "Unknown error occurred", cause = t)Also applies to: 79-79, 107-107, 136-136, 161-161
🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/org/whosin/client/data/remote/RemoteAuthDataSource.kt
around line 54 (and similarly at lines 79, 107, 136, 161), the code passes
t.message directly to ApiResult.Error which can be null; change each occurrence
to provide a non-null fallback (for example use t.localizedMessage ?:
"RemoteAuthDataSource: unexpected error" or build a message combining
t::class.simpleName and a default string) so ApiResult.Error always receives a
usable message while still attaching the throwable as the cause.
| suspend fun sendEmailVerification(email: String): ApiResult<EmailVerificationResponseDto> { | ||
| return try { | ||
| val response: HttpResponse = client | ||
| .post("auth/email/send") { | ||
| setBody( | ||
| EmailVerificationRequestDto(email = email) | ||
| ) | ||
| } | ||
| if (response.status.isSuccess()) { | ||
| ApiResult.Success( | ||
| data = response.body(), | ||
| statusCode = response.status.value | ||
| ) | ||
| } else { | ||
| val errorResponse: EmailVerificationResponseDto = response.body() | ||
| ApiResult.Error( | ||
| code = errorResponse.status, | ||
| message = errorResponse.message | ||
| ) | ||
| } | ||
| } catch (t: Throwable) { | ||
| ApiResult.Error(message = t.message, cause = t) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 응답 파싱 시 예외 처리가 없습니다.
72줄에서 EmailVerificationResponseDto로 에러 응답을 파싱할 때, 파싱에 실패하면 예외가 발생해 78줄의 catch 블록에서 처리됩니다. 이렇게 되면 실제 HTTP 에러 코드 정보가 손실될 수 있어요.
login 함수처럼 중첩된 try-catch로 파싱 실패를 처리하거나, 공통 함수로 추출하는 방법을 고려해보세요.
| _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) | ||
|
|
||
| when (val result = repository.login(email, password)) { | ||
| is ApiResult.Success -> { | ||
| _uiState.value = LoginUiState.Success(result.data.data) | ||
| val tokenData = result.data.data | ||
| if (tokenData != null) { | ||
| tokenManager.saveTokens( | ||
| accessToken = tokenData.accessToken, | ||
| refreshToken = tokenData.refreshToken | ||
| ) | ||
| _uiState.value = _uiState.value.copy( | ||
| isLoading = false, | ||
| isSuccess = true, | ||
| errorMessage = null | ||
| ) | ||
| } else { | ||
| _uiState.value = _uiState.value.copy( | ||
| isLoading = false, | ||
| errorMessage = "토큰 데이터를 받지 못했습니다." | ||
| ) | ||
| } | ||
| } | ||
| is ApiResult.Error -> { | ||
| val message = result.message ?: result.cause?.message | ||
| _uiState.value = LoginUiState.Error(message) | ||
| _uiState.value = _uiState.value.copy( | ||
| isLoading = false, | ||
| errorMessage = result.message ?: "로그인에 실패했습니다." | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isSuccess 상태 초기화 누락으로 재로그인 시 오동작
이전에 성공한 뒤 다시 로그인 시도에서 실패하면 isSuccess가 계속 true로 남아 UI가 성공 흐름을 재실행할 수 있습니다. 로딩 시작과 실패 분기에서 isSuccess를 명시적으로 false로 초기화해 주세요.
아래와 같이 수정하면 됩니다:
- _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
+ _uiState.value = _uiState.value.copy(
+ isLoading = true,
+ isSuccess = false,
+ errorMessage = null
+ )
...
- _uiState.value = _uiState.value.copy(
- isLoading = false,
- errorMessage = "토큰 데이터를 받지 못했습니다."
- )
+ _uiState.value = _uiState.value.copy(
+ isLoading = false,
+ isSuccess = false,
+ errorMessage = "토큰 데이터를 받지 못했습니다."
+ )
...
- _uiState.value = _uiState.value.copy(
- isLoading = false,
- errorMessage = result.message ?: "로그인에 실패했습니다."
- )
+ _uiState.value = _uiState.value.copy(
+ isLoading = false,
+ isSuccess = false,
+ errorMessage = result.message ?: "로그인에 실패했습니다."
+ )📝 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.
| _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) | |
| when (val result = repository.login(email, password)) { | |
| is ApiResult.Success -> { | |
| _uiState.value = LoginUiState.Success(result.data.data) | |
| val tokenData = result.data.data | |
| if (tokenData != null) { | |
| tokenManager.saveTokens( | |
| accessToken = tokenData.accessToken, | |
| refreshToken = tokenData.refreshToken | |
| ) | |
| _uiState.value = _uiState.value.copy( | |
| isLoading = false, | |
| isSuccess = true, | |
| errorMessage = null | |
| ) | |
| } else { | |
| _uiState.value = _uiState.value.copy( | |
| isLoading = false, | |
| errorMessage = "토큰 데이터를 받지 못했습니다." | |
| ) | |
| } | |
| } | |
| is ApiResult.Error -> { | |
| val message = result.message ?: result.cause?.message | |
| _uiState.value = LoginUiState.Error(message) | |
| _uiState.value = _uiState.value.copy( | |
| isLoading = false, | |
| errorMessage = result.message ?: "로그인에 실패했습니다." | |
| ) | |
| _uiState.value = _uiState.value.copy( | |
| isLoading = true, | |
| isSuccess = false, | |
| errorMessage = null | |
| ) | |
| when (val result = repository.login(email, password)) { | |
| is ApiResult.Success -> { | |
| val tokenData = result.data.data | |
| if (tokenData != null) { | |
| tokenManager.saveTokens( | |
| accessToken = tokenData.accessToken, | |
| refreshToken = tokenData.refreshToken | |
| ) | |
| _uiState.value = _uiState.value.copy( | |
| isLoading = false, | |
| isSuccess = true, | |
| errorMessage = null | |
| ) | |
| } else { | |
| _uiState.value = _uiState.value.copy( | |
| isLoading = false, | |
| isSuccess = false, | |
| errorMessage = "토큰 데이터를 받지 못했습니다." | |
| ) | |
| } | |
| } | |
| is ApiResult.Error -> { | |
| _uiState.value = _uiState.value.copy( | |
| isLoading = false, | |
| isSuccess = false, | |
| errorMessage = result.message ?: "로그인에 실패했습니다." | |
| ) |
🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/LoginViewModel.kt
around lines 27 to 53, isSuccess is not reset when starting a new login or when
login fails, causing the success UI flow to persist after a subsequent failure;
set isSuccess = false at the start of the login (alongside isLoading = true,
errorMessage = null) and also set isSuccess = false in every failure branch
(when tokenData is null and in ApiResult.Error) so the UI does not mistakenly
remain in a success state.
🚀 이슈번호
✏️ 변경사항
📷 스크린샷
로그인 로직 구현
https://github.com/user-attachments/assets/7c13b52a-b2c8-4226-8e02-32869e16ebb3
회원가입 로직 구현
https://github.com/user-attachments/assets/76455aa9-a5af-4f4d-a1cc-0adb6c436584
✍️ 사용법
🎸 기타
Summary by CodeRabbit