인증 API 연동 및 로그인 상태 관리 구조 구현#104
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughKakao OAuth 인증으로 ID 토큰을 획득하고 서버 로그인 API를 호출하여 액세스/리프레시 토큰을 로컬 저장소에 저장하는 완전한 인증 흐름을 구현합니다. 세션 만료 감지 및 토큰 자동 갱신도 추가됩니다. Changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Prezel/core/auth/src/main/java/com/team/prezel/core/auth/AuthManager.kt (1)
32-45:⚠️ Potential issue | 🟡 Minor
currentProvider폴백 로직과 스레드 안전성 검토 필요두 가지 우려가 있습니다.
폴백의 조건부 동작:
currentProvider는 메모리 상의var로만 유지되어 프로세스 재시작 후에는null이 됩니다. 저장된 토큰으로 이미 로그인된 상태에서 사용자가 로그아웃을 호출하는 경로가 바로 이 케이스인데,authClients.keys.singleOrNull()폴백은 등록된AuthProvider가 정확히 1개일 때만 동작합니다. 추후 다른 공급자(예: Google, Apple)가 추가되면 폴백이 조용히null을 반환해 정상적인 로그아웃 플로우가IllegalStateException으로 실패하게 됩니다. 로그인 성공 시currentProvider를AuthTokenStore등에 함께 영속화하여 재시작 후에도 복원하는 방식을 권장합니다.동시성:
@Singleton에서currentProvider를 일반var로 노출하고login()/logout()(둘 다suspend)에서 읽고 쓰므로, 서로 다른 디스패처/스레드에서 호출 시 가시성·경합 이슈가 있을 수 있습니다.@Volatile또는Mutex로 보호하는 것을 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/auth/src/main/java/com/team/prezel/core/auth/AuthManager.kt` around lines 32 - 45, 현재 logout()의 provider 폴백(authClients.keys.singleOrNull())과 일반 var currentProvider의 동시성 문제가 있습니다: 로그인 시 currentProvider를 AuthTokenStore(또는 유사한 영속소)에 함께 저장하고 앱/클래스 초기화 시 복원하도록 구현하여 프로세스 재시작 뒤에도 올바른 provider를 복원하도록 하며, logout()/login()에서 currentProvider가 null일 때는 keys.singleOrNull()에 의존하지 말고 저장된 토큰에서 복원하거나 각 authClients를 조회하여 유효한 세션/토큰을 가진 클라이언트를 찾아 사용하도록 변경하세요; 또한 currentProvider의 가시성/경합을 해결하기 위해 currentProvider를 `@Volatile로` 표시하거나 AuthManager의 login()/logout()/초기화/읽기·쓰기 경로를 Mutex로 보호하여 스레드 안전성을 확보하세요(참조 심볼: currentProvider, authClients.keys.singleOrNull(), logout(), login(), AuthTokenStore, `@Volatile`, Mutex).
🧹 Nitpick comments (10)
Prezel/core/auth/src/main/java/com/team/prezel/core/auth/model/AuthResult.kt (1)
4-6:idToken블랭크 검증을 타입 계약에 반영 고려
KakaoAuthClient에서 이미idToken.isNullOrBlank()검증 후에만Success(idToken = idToken)을 생성하고 있으므로 현재 흐름상 문제는 없습니다. 다만Success의 계약을 타입으로 보장하려면 생성자에require(idToken.isNotBlank())를 넣어 다른 호출 경로에서도 불변식이 유지되도록 하는 것을 검토해 보세요. 필수는 아닙니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/auth/src/main/java/com/team/prezel/core/auth/model/AuthResult.kt` around lines 4 - 6, Add a runtime contract to the Success data class so idToken cannot be blank: inside the Success constructor add a require check (e.g., require(idToken.isNotBlank())) to enforce the invariant at creation time; update any callers if they relied on blank tokens (KakaoAuthClient already guards idToken.isNullOrBlank()), and consider trimming the token before validation if needed to normalize input.Prezel/core/ui/src/main/java/com/team/prezel/core/ui/StatusView.kt (1)
80-96:modifier.size(80.dp)순서로 인해 호출자 사이즈가 무시됨
LottieAnimation의modifier = modifier.size(80.dp)는 체이닝 마지막에size(80.dp)가 오므로, 호출자가StatusLottie(modifier = Modifier.size(120.dp))와 같이 크기를 지정해도 항상 80.dp로 고정됩니다. 기본값을Modifier로 바꾼 의도가 호출자에게 수정 자유도를 주기 위한 것이라면 순서를 뒤집어 호출자 값이 우선되도록 하는 편이 자연스럽습니다. 반대로 80.dp를 강제해야 하는 것이라면 현재 구현은 의도대로 동작하지만, 그 경우 파라미터에 사이즈 변경 여지를 남긴 것은 오해를 유발할 수 있습니다.♻️ 제안: 호출자 지정이 우선되도록 변경
LottieAnimation( composition = composition, progress = { progress }, - modifier = modifier.size(80.dp), + modifier = Modifier + .size(80.dp) + .then(modifier), )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/StatusView.kt` around lines 80 - 96, The current StatusLottie implementation force-applies size last via modifier.size(80.dp) so callers like StatusLottie(modifier = Modifier.size(120.dp)) are overridden; fix by making the default size apply before the caller modifier (so caller wins) — e.g. change the call site to use Modifier.size(80.dp).then(modifier) or set the parameter default to modifier: Modifier = Modifier.size(80.dp) and pass modifier directly; refer to function StatusLottie and the modifier.size(80.dp) usage and ensure the caller-provided modifier is applied last so its size takes precedence.Prezel/core/data/src/main/java/com/team/prezel/core/data/ApiResponseExt.kt (1)
5-10:transform내부 예외 처리 고려
transform(data)에서 예외가 발생하면Result.failure로 감싸지 않고 그대로 호출자에게 전파됩니다. 매핑 중 발생할 수 있는 예외(필드 누락, 파싱 실패 등)를Result로 일관되게 처리하려면runCatching을 활용하는 것을 고려해 보세요.♻️ 제안 수정
internal suspend inline fun <T, R> ApiResponse<T>.toResult(crossinline transform: suspend (T) -> R): Result<R> = when (this) { - is ApiResponse.Success -> Result.success(transform(data)) + is ApiResponse.Success -> runCatching { transform(data) } is ApiResponse.Failure.HttpError -> Result.failure(throwable) is ApiResponse.Failure.NetworkError -> Result.failure(throwable) }단,
CancellationException이 삼켜지지 않도록 주의가 필요하므로 호출 컨텍스트에 따라 유지하는 것도 합리적입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/data/src/main/java/com/team/prezel/core/data/ApiResponseExt.kt` around lines 5 - 10, The current ApiResponse<T>.toResult extension calls transform(data) directly so any exception from transform escapes instead of being wrapped in Result; change it to invoke transform inside runCatching and return Result.success for the mapped value or Result.failure for mapping exceptions, while rethrowing CancellationException so it isn’t swallowed. Update the ApiResponse.Success branch (inside ApiResponseExt.kt, function ApiResponse<T>.toResult) to use runCatching { transform(data) } and convert the outcome to a Result, leaving the ApiResponse.Failure.HttpError and NetworkError branches unchanged.Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.kt (1)
7-12: 불필요한@SerialName어노테이션프로퍼티명과 동일한 값(
reasonCategory,reasonText)을 지정하는@SerialName은 kotlinx.serialization 기본 동작과 같아 생략 가능합니다. 서버 JSON 키가 프로퍼티명과 다르게 변경될 가능성이 있을 때만 남겨두세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.kt` around lines 7 - 12, The WithdrawRequest data class uses redundant `@SerialName` annotations for properties reasonCategory and reasonText; remove the `@SerialName` annotations from those properties in the WithdrawRequest class (leave the property names as-is), and if present remove the unused kotlinx.serialization.SerialName import to keep imports clean — only keep `@SerialName` if the JSON key differs from the property name.Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/AuthToken.kt (1)
1-6: 토큰 값의 로깅 노출 방지를 위한toString()오버라이드 권장
AuthToken은accessToken/refreshToken같은 민감 자격증명을 담는 모델입니다. Kotlindata class의 기본toString()은 필드 값을 그대로 노출하므로, 로그·크래시 리포트·디버거 출력 등에서 실수로 토큰이 새어나갈 위험이 있습니다. PR 목표 중 "토큰 값 미노출" 로깅 보안 강화와 일관되게,toString()을 마스킹 처리해 두는 것을 권장합니다.♻️ 제안 예시
-data class AuthToken( - val accessToken: String, - val refreshToken: String, -) +data class AuthToken( + val accessToken: String, + val refreshToken: String, +) { + override fun toString(): String = "AuthToken(accessToken=***, refreshToken=***)" +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/AuthToken.kt` around lines 1 - 6, Override AuthToken.toString() to avoid printing sensitive values: replace the default data class-generated toString by adding an explicit toString() implementation in the AuthToken class that does not return accessToken or refreshToken (e.g., return a masked string like "AuthToken(accessToken=***, refreshToken=***)") so logs/debuggers/crash reports cannot leak the tokens; update the AuthToken data class (AuthToken, accessToken, refreshToken, toString()) accordingly.Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginResponse.kt (1)
6-12:toString()메서드를 재정의하여 토큰 값 마스킹 권장.현재 코드의
logTokenResponse메서드는 토큰 값을 직접 로그하지 않으므로 명시적인 노출은 없습니다. 그러나 향후 코드 수정 시 실수로 객체 전체를 로그하거나, 예외 메시지에LoginResponse가 포함될 경우toString()이 자동 호출되어 민감한 정보가 노출될 수 있습니다. 방어적 프로그래밍 차원에서 다음과 같이 마스킹하는 것을 권장합니다.제안 변경
data class LoginResponse( `@SerialName`("accessToken") val accessToken: String, `@SerialName`("refreshToken") val refreshToken: String, -) +) { + override fun toString(): String = "LoginResponse(accessToken=***, refreshToken=***)" +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginResponse.kt` around lines 6 - 12, Override LoginResponse.toString() to avoid exposing raw tokens by returning a masked representation; locate the data class LoginResponse and add an override fun toString() that prints a non-sensitive summary (e.g., show token prefixes or fixed masked text like "accessToken=****", "refreshToken=****" or first N chars + masked tail) so any accidental logging or exception formatting won’t reveal full accessToken/refreshToken values.Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/CheckLoginStatusUseCase.kt (1)
19-27: 액세스 토큰 존재 여부만으로 로그인 상태를 판단하는 한계.현재 로직은 access token이 비어있지 않으면 곧바로
true를 반환하므로, 만료된 access token만 저장되어 있고 refresh token이 없는 상태에서도 로그인 상태로 판단됩니다. PR 설명상TokenRefreshAuthenticator가 401에 대응해 재발급을 수행하므로 최종적으로는 복구되지만, 스플래시 단계에서 홈으로 진입한 뒤 첫 API 호출에서 실패/재시도가 발생할 수 있습니다.만료 판단이 가능한 메타데이터(예: 만료 시각)가 있다면 여기서 미리 reissue를 유도하거나, 반대로 최소한의 가벼운 "토큰 유효성 확인" 호출을 고려해볼 수 있습니다. 현 구현 의도대로 유지한다면 동작 확인 차원에서 코멘트로 가정만 명시해두시면 좋겠습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/CheckLoginStatusUseCase.kt` around lines 19 - 27, The current CheckLoginStatusUseCase.operator fun invoke() returns true whenever authRepository.getAccessToken() is non-blank, which misclassifies expired tokens as logged-in; update invoke() to either (a) inspect token expiry metadata (if available from getAccessToken() or a new authRepository.getAccessTokenExpiry()/parseAccessTokenExpiry()) and call authRepository.reissueToken(refreshToken) proactively when expired, or (b) perform a lightweight token validation call (e.g., authRepository.validateAccessToken()) before returning true; if you choose to keep the current fast-path behavior, add an explicit comment in CheckLoginStatusUseCase.invoke() documenting the assumption that TokenRefreshAuthenticator will handle 401 and why a proactive check was omitted.Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt (1)
16-20:logTokenResponse의 실패 로그가 항상 "Server login failed"로 고정되어 있어, 재발급 경로의 로그가 오인될 수 있습니다.
logTokenResponse는login과reissueToken양쪽에서 호출되는데 실패 메시지가 "Server login failed: ..."로 하드코딩되어 있어 재발급 실패를 디버깅할 때 혼동될 수 있습니다. 호출 컨텍스트를 인자로 받거나 메시지를 중립적으로 바꾸는 것을 권장합니다.♻️ 제안 리팩터
- override suspend fun reissueToken(refreshToken: String): ApiResponse<LoginResponse> = - authService - .reissueToken( - request = ReissueTokenRequest(refreshToken = refreshToken), - ).also(::logTokenResponse) + override suspend fun reissueToken(refreshToken: String): ApiResponse<LoginResponse> = + authService + .reissueToken( + request = ReissueTokenRequest(refreshToken = refreshToken), + ).also { logTokenResponse(it, "reissue") } @@ - }.also(::logTokenResponse) + }.also { logTokenResponse(it, "login") } @@ - private fun logTokenResponse(response: ApiResponse<LoginResponse>) { + private fun logTokenResponse(response: ApiResponse<LoginResponse>, context: String) { if (!BuildConfig.DEBUG) return when (response) { - is ApiResponse.Success -> Timber.tag("AuthToken").d("서버 인증 응답에 성공했습니다.") - - is ApiResponse.Failure.HttpError -> { - Timber.tag("AuthToken").e(response.throwable, "Server login failed: http error") - } - - is ApiResponse.Failure.NetworkError -> { - Timber.tag("AuthToken").e(response.throwable, "Server login failed: network error") - } + is ApiResponse.Success -> Timber.tag("AuthToken").d("[$context] 성공") + is ApiResponse.Failure.HttpError -> + Timber.tag("AuthToken").e(response.throwable, "[$context] HTTP error") + is ApiResponse.Failure.NetworkError -> + Timber.tag("AuthToken").e(response.throwable, "[$context] Network error") } }Also applies to: 27-31, 46-60
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt` around lines 16 - 20, The failure log in logTokenResponse is hardcoded as "Server login failed" and misleads when called from reissueToken; update logTokenResponse to accept a caller/context parameter (e.g., operation: String) or make the message neutral, then pass "login" from the login call and "reissueToken" from reissueToken so the logged error uses the correct context; locate and update the logTokenResponse function and all call sites (including AuthRemoteDataSourceImpl.reissueToken and the login path) to supply the operation string and adjust the log message accordingly.Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt (1)
45-72: Timber 태그 "ProfileTest"는 출시 전 정리 대상으로 보입니다."ProfileTest"는 개발 중 임시 태그처럼 보입니다.
ProfileViewModel같은 의미 있는 태그로 바꾸거나, 클래스 단위의 태그 컨벤션을 맞추는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt` around lines 45 - 72, The Timber.tag("ProfileTest") occurrences look like leftover dev tags—replace them with a meaningful, consistent tag for this class (e.g., a class-level TAG or use ProfileViewModel::class.simpleName) wherever used; update the Timber calls in the logout() and withdraw() flows (both d(...) and e(...)) to use that TAG so logging follows the project convention and can be cleaned up before release.Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt (1)
29-34:login()을 다른 메서드와 동일하게toResult로 통일하고 중복 오버로드 제거를 제안합니다.현재
reissueToken/logout/withdraw는toResult { ... }패턴을 쓰는 반면,login만when분기를 직접 사용하고 있습니다. 그리고 이를 위해saveTokens(ApiResponse.Success<LoginResponse>)와ApiResponse.Success<LoginResponse>.toAuthToken()이 별도로 존재하지만, 실질적으로LoginResponse버전의 단순 래퍼입니다. 단일 오버로드로 정리하면 중복이 줄고 API 전환 시 실수가 적어집니다.♻️ 제안 리팩터
- override suspend fun login(idToken: String): Result<AuthToken> = - when (val response = authRemoteDataSource.login(idToken = idToken)) { - is ApiResponse.Success -> Result.success(saveTokens(response)) - is ApiResponse.Failure.HttpError -> Result.failure(response.throwable) - is ApiResponse.Failure.NetworkError -> Result.failure(response.throwable) - } + override suspend fun login(idToken: String): Result<AuthToken> = + authRemoteDataSource.login(idToken = idToken).toResult(::saveTokens) @@ - private suspend fun saveTokens(response: ApiResponse.Success<LoginResponse>): AuthToken = - response - .toAuthToken() - .also { token -> - authTokenStore.saveTokens( - accessToken = token.accessToken, - refreshToken = token.refreshToken, - ) - } - private suspend fun saveTokens(response: LoginResponse): AuthToken = response .toAuthToken() .also { token -> authTokenStore.saveTokens( accessToken = token.accessToken, refreshToken = token.refreshToken, ) } - - private fun ApiResponse.Success<LoginResponse>.toAuthToken(): AuthToken = - AuthToken( - accessToken = data.accessToken, - refreshToken = data.refreshToken, - )Also applies to: 49-79
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt` around lines 29 - 34, The login method diverges from other methods by using a manual when over ApiResponse instead of the shared toResult wrapper and duplicates helpers (saveTokens and ApiResponse.Success<LoginResponse>.toAuthToken); change login(idToken: String) to use the same toResult { ... } pattern as reissueToken/logout/withdraw, remove the redundant saveTokens/LoginResponse-specific toAuthToken overloads, and convert the ApiResponse.Success handling to call the unified toAuthToken/on-success mapper so all token conversion is done by a single helper used across AuthRepositoryImpl.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Prezel/app/src/main/AndroidManifest.xml`:
- Line 13: 현재 AndroidManifest.xml의
android:networkSecurityConfig="@xml/network_security_config" 속성은 평문 HTTP 허용을 참조할
수 있으므로 보안 취약점을 활성화합니다; 네트워크 보안 구성을 제거하려면 AndroidManifest.xml에서
android:networkSecurityConfig 속성(참조되는 `@xml/network_security_config`) 항목을 삭제하거나 기본
보안 설정을 사용하도록 되돌리고, 모든 네트워크 호출이 HTTPS로 전환되었는지 확인한 후 필요하다면
network_security_config.xml의 예외나 cleartextPermitted 설정도 삭제하세요.
In `@Prezel/app/src/main/res/xml/network_security_config.xml`:
- Around line 1-6: Remove the permissive cleartext configuration from production
by deleting or excluding Prezel/app/src/main/res/xml/network_security_config.xml
(which currently permits cleartext for domain "prezel.p-e.kr"); instead, place
an equivalent network_security_config.xml with cleartextTrafficPermitted="true"
only under src/debug/res/xml/ so it applies to debug builds, and ensure release
builds have no cleartext-permitted domain entries and rely on HTTPS-only
endpoints (verify the BASE_URL used by your app supports HTTPS).
In
`@Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/LoginUseCase.kt`:
- Around line 7-15: KDoc contains an incorrect `@link` path for the repository
reference: replace the wrong
com.team.prezel.core.domain.repository.auth.AuthRepository.login reference in
LoginUseCase.kt with the actual package path used in the project (the real
AuthRepository class) so the KDoc correctly links to AuthRepository.login;
locate the KDoc block in LoginUseCase.kt and update the `@link` target to the
exact package and symbol name of AuthRepository.login used elsewhere in the
codebase.
In
`@Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/LogoutUseCase.kt`:
- Around line 6-14: Update the KDoc reference for the logout link in the
LogoutUseCase file: replace the incorrect package path
`com.team.prezel.core.domain.repository.auth.AuthRepository.logout` with the
actual import path `com.team.prezel.core.domain.AuthRepository.logout` so the
IDE can resolve the link; ensure the referenced symbol name `logout` and class
`AuthRepository` in the KDoc match the exact identifiers used in the file.
- Around line 18-25: The failure path in LogoutUseCase.invoke currently returns
Result.failure when authRepository.getAccessToken() is null without clearing
local state; update LogoutUseCase.invoke to call authManager.logout() (the same
cleanup used on success) before returning the failure so currentProvider and
other local auth state are reset, then return Result.failure; locate the logic
around authRepository.getAccessToken(), authRepository.logout(accessToken), and
the existing success path to mirror the cleanup call used there and ensure
ProfileViewModel's onFailure sees the cleared local state.
In
`@Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/ReissueTokenUseCase.kt`:
- Line 12: Update the KDoc reference to point to the actual imported
AuthRepository type instead of the non-existent package; in
ReissueTokenUseCase.kt's KDoc replace the link target
`com.team.prezel.core.domain.repository.auth.AuthRepository.reissueToken` with
the real package/type you imported (the AuthRepository symbol used in this file)
so the KDoc link resolves correctly for AuthRepository.reissueToken.
In
`@Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/WithdrawUseCase.kt`:
- Around line 7-15: The KDoc `@link` in WithdrawUseCase incorrectly references
com.team.prezel.core.domain.repository.auth.AuthRepository.withdraw; update the
reference to the correct package/path (the same path used in LogoutUseCase) so
the `@link` points to the real AuthRepository.withdraw symbol; locate
WithdrawUseCase and replace the invalid fully-qualified link with the corrected
fully-qualified package/class/method used by LogoutUseCase to ensure doc links
resolve.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.kt`:
- Around line 49-55: The catch-all in AuthTokenRefresher (catch (Throwable) ...)
is swallowing CancellationException and turning cancellations into null; change
the error handling so you first catch and rethrow CancellationException (or more
generally kotlinx.coroutines.CancellationException) before the general Throwable
handler, then keep the existing isInvalidRefreshToken() check,
authTokenStore.clear(), Timber.e(...) and return null for other exceptions so
cancellations propagate correctly.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenStore.kt`:
- Around line 3-14: 인터페이스 AuthTokenStore에 KDoc을 추가해 동기/비동기 계약을 명확히 하세요:
getAccessToken() 및 getRefreshToken()는 메모리 캐시에서 즉시(동기) 반환함을 명시하고, suspend인
saveTokens(...)와 clear()는 완료 즉시 메모리 캐시가 최신 상태가 되며 이후의 동기 getter 호출이 최신값을 반환함을
보장한다고 문서화하세요; 또한 OkHttp Authenticator가 네트워크 스레드에서 호출해도 블로킹 없이 작동해야 한다는 점을 명시하고,
현재 구현인 DataStoreAuthTokenStore가 volatile(휘발성) 필드와 Mutex로 이 계약을 만족하도록 설계되어 있음을
언급하세요.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/DataStoreAuthTokenStore.kt`:
- Around line 42-46: 현재 DataStoreAuthTokenStore의 init 블록에서 runBlocking {
readPreferences() }로 동기 초기화를 수행하면 메인 스레드에서 ANR을 유발할 수 있으니 runBlocking 호출을 제거하고
초기화는 앱의 background scope에서 비동기로 수행하도록 변경하세요: DataStoreAuthTokenStore의 init에서
applicationScope.launch { val preferences = readPreferences(); accessToken =
preferences[KEY_ACCESS_TOKEN]; refreshToken = preferences[KEY_REFRESH_TOKEN] }
식으로 미리 prefetch 하되 기존의 동기 getter(예: getAccessToken())를 바로 유지해야 한다면 getter가 토큰이
아직 없으면 null을 반환하도록 바꾸고 인증 인터셉터가 null일 때 Authorization 헤더를 건너뛰게 하거나, 더 좋은 대안으로
getAccessToken()를 suspend fun으로 변경해 호출부가 suspend 컨텍스트에서 안전하게 읽도록 리팩토링하세요.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt`:
- Around line 75-80: The authorization-path check is duplicated and brittle;
create a single shared extension (e.g., fun String.requiresAuthorization():
Boolean) in the core.network.auth package, move the LOGIN_PATH and REISSUE_PATH
constants from TokenRefreshAuthenticator to that same shared file, and implement
the check using path-suffix matching (e.g., encodedPath.endsWith(LOGIN_PATH) /
REISSUE_PATH logic inverted) so it handles BASE_URL path prefixes; then update
NetworkModule's use of String.requiresAuthorization() and
TokenRefreshAuthenticator to call the new shared extension/constants instead of
their local hard-coded checks.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginRequest.kt`:
- Around line 6-10: The three authentication data classes (LoginRequest,
LoginResponse, ReissueTokenRequest) expose sensitive token fields via the
default toString(); override toString() in each class (e.g., LoginRequest,
LoginResponse, ReissueTokenRequest) to return a masked representation that omits
or replaces token values (like "idToken=***" or "accessToken=***",
"refreshToken=***") while preserving non-sensitive fields, ensuring any logging
or accidental stringification cannot leak actual token contents.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/ReissueTokenRequest.kt`:
- Around line 7-10: ReissueTokenRequest currently exposes the sensitive
refreshToken in its generated toString and is public; make the class internal
(change data class ReissueTokenRequest to internal data class
ReissueTokenRequest) and override its toString() to avoid leaking the token
(e.g., return "ReissueTokenRequest(refreshToken=******)" or otherwise mask/omit
the refreshToken field). Keep the property name refreshToken and its
`@SerialName`("refresh-token") annotation so serialization remains unchanged, but
ensure the custom toString suppresses the real token value.
In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiIntent.kt`:
- Around line 12-15: OnLoginResult 데이터 클래스에 정의된 사용되지 않는 provider 필드를 제거하세요:
LoginUiIntent의 data class OnLoginResult에서 provider 프로퍼티를 삭제하고 생성자 시그니처를 단일 val
result: AuthResult로 변경한 다음, 이를 생성하는 곳(예: LoginScreen)에서 provider를 전달하지 않도록 호출부를
수정하고 LoginViewModel.onIntent → handleLoginResult 호출 흐름은 변경 없이 result만 전달되도록
유지하세요; 관련된 모든 참조/임포트를 정리해 불필요한 인자 전달이나 컴파일 오류가 없도록 합니다.
In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt`:
- Around line 63-66: The onSuccess block in LoginViewModel currently always
sends LoginUiEffect.NavigateToTerms; change it to first persist the returned
auth token (call your token persistence API, e.g.,
authRepository.persistAuthToken(...) or sessionManager.saveAuthToken(...)),
updateState to clear loading/pendingProvider, then branch on the server/domain
response flag that indicates whether terms acceptance is required (e.g.,
response.requiresTerms, response.isNewUser or similar) and send
LoginUiEffect.NavigateToTerms only when true, otherwise send
LoginUiEffect.NavigateToHome; update references in the onSuccess handler,
keeping updateState and sendEffect calls in the same method.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt`:
- Around line 63-69: The withdraw Button currently calls viewModel::withdraw
directly (onClick = viewModel::withdraw) which can trigger accidental account
deletion; change the flow so the Button toggles a confirmation dialog state
(e.g., showConfirmDialog boolean) and only invoke viewModel.withdraw from the
dialog's confirm action, keeping the Button disabled when uiState.isLoading and
ensuring the dialog's confirm button also respects uiState.isLoading; update
ProfileScreen's composable state to hold the dialog visibility and wire the
dialog confirm/cancel callbacks to call viewModel.withdraw or close the dialog
accordingly.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt`:
- Around line 54-75: The withdraw() method currently hardcodes
WithdrawReason.Other("임시 테스트 탈퇴"); update it to accept the user-selected reason
(or read it from ViewModel state) and pass that into withdrawUseCase instead of
the hardcoded string: modify the signature of withdraw() (or add a parameter
like selectedReason: WithdrawReason), ensure the ViewModel stores/receives the
UI-selected reason, and replace WithdrawReason.Other("임시 테스트 탈퇴") with the
actual selectedReason when calling withdrawUseCase; keep the existing
success/failure handling (authManager.logout(), _uiEffect emits) intact.
- Around line 33-75: Both logout() and withdraw() set _uiState.isLoading = true
but may skip resetting it if authManager.logout() or _uiEffect.emit(...) throws;
wrap the body of each viewModelScope.launch in a try/finally so that
_uiState.update { it.copy(isLoading = false) } runs in the finally block.
Specifically, in ProfileViewModel.logout() and ProfileViewModel.withdraw(), keep
the success/failure handling and calls to logoutUseCase()/withdrawUseCase() and
authManager.logout() inside try, and move the isLoading = false update into
finally; ensure exceptions from authManager.logout() or _uiEffect.emit(...)
don’t prevent clearing the loading flag.
---
Outside diff comments:
In `@Prezel/core/auth/src/main/java/com/team/prezel/core/auth/AuthManager.kt`:
- Around line 32-45: 현재 logout()의 provider 폴백(authClients.keys.singleOrNull())과
일반 var currentProvider의 동시성 문제가 있습니다: 로그인 시 currentProvider를 AuthTokenStore(또는
유사한 영속소)에 함께 저장하고 앱/클래스 초기화 시 복원하도록 구현하여 프로세스 재시작 뒤에도 올바른 provider를 복원하도록 하며,
logout()/login()에서 currentProvider가 null일 때는 keys.singleOrNull()에 의존하지 말고 저장된
토큰에서 복원하거나 각 authClients를 조회하여 유효한 세션/토큰을 가진 클라이언트를 찾아 사용하도록 변경하세요; 또한
currentProvider의 가시성/경합을 해결하기 위해 currentProvider를 `@Volatile로` 표시하거나 AuthManager의
login()/logout()/초기화/읽기·쓰기 경로를 Mutex로 보호하여 스레드 안전성을 확보하세요(참조 심볼:
currentProvider, authClients.keys.singleOrNull(), logout(), login(),
AuthTokenStore, `@Volatile`, Mutex).
---
Nitpick comments:
In
`@Prezel/core/auth/src/main/java/com/team/prezel/core/auth/model/AuthResult.kt`:
- Around line 4-6: Add a runtime contract to the Success data class so idToken
cannot be blank: inside the Success constructor add a require check (e.g.,
require(idToken.isNotBlank())) to enforce the invariant at creation time; update
any callers if they relied on blank tokens (KakaoAuthClient already guards
idToken.isNullOrBlank()), and consider trimming the token before validation if
needed to normalize input.
In `@Prezel/core/data/src/main/java/com/team/prezel/core/data/ApiResponseExt.kt`:
- Around line 5-10: The current ApiResponse<T>.toResult extension calls
transform(data) directly so any exception from transform escapes instead of
being wrapped in Result; change it to invoke transform inside runCatching and
return Result.success for the mapped value or Result.failure for mapping
exceptions, while rethrowing CancellationException so it isn’t swallowed. Update
the ApiResponse.Success branch (inside ApiResponseExt.kt, function
ApiResponse<T>.toResult) to use runCatching { transform(data) } and convert the
outcome to a Result, leaving the ApiResponse.Failure.HttpError and NetworkError
branches unchanged.
In
`@Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt`:
- Around line 29-34: The login method diverges from other methods by using a
manual when over ApiResponse instead of the shared toResult wrapper and
duplicates helpers (saveTokens and
ApiResponse.Success<LoginResponse>.toAuthToken); change login(idToken: String)
to use the same toResult { ... } pattern as reissueToken/logout/withdraw, remove
the redundant saveTokens/LoginResponse-specific toAuthToken overloads, and
convert the ApiResponse.Success handling to call the unified
toAuthToken/on-success mapper so all token conversion is done by a single helper
used across AuthRepositoryImpl.
In
`@Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/CheckLoginStatusUseCase.kt`:
- Around line 19-27: The current CheckLoginStatusUseCase.operator fun invoke()
returns true whenever authRepository.getAccessToken() is non-blank, which
misclassifies expired tokens as logged-in; update invoke() to either (a) inspect
token expiry metadata (if available from getAccessToken() or a new
authRepository.getAccessTokenExpiry()/parseAccessTokenExpiry()) and call
authRepository.reissueToken(refreshToken) proactively when expired, or (b)
perform a lightweight token validation call (e.g.,
authRepository.validateAccessToken()) before returning true; if you choose to
keep the current fast-path behavior, add an explicit comment in
CheckLoginStatusUseCase.invoke() documenting the assumption that
TokenRefreshAuthenticator will handle 401 and why a proactive check was omitted.
In
`@Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/AuthToken.kt`:
- Around line 1-6: Override AuthToken.toString() to avoid printing sensitive
values: replace the default data class-generated toString by adding an explicit
toString() implementation in the AuthToken class that does not return
accessToken or refreshToken (e.g., return a masked string like
"AuthToken(accessToken=***, refreshToken=***)") so logs/debuggers/crash reports
cannot leak the tokens; update the AuthToken data class (AuthToken, accessToken,
refreshToken, toString()) accordingly.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt`:
- Around line 16-20: The failure log in logTokenResponse is hardcoded as "Server
login failed" and misleads when called from reissueToken; update
logTokenResponse to accept a caller/context parameter (e.g., operation: String)
or make the message neutral, then pass "login" from the login call and
"reissueToken" from reissueToken so the logged error uses the correct context;
locate and update the logTokenResponse function and all call sites (including
AuthRemoteDataSourceImpl.reissueToken and the login path) to supply the
operation string and adjust the log message accordingly.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginResponse.kt`:
- Around line 6-12: Override LoginResponse.toString() to avoid exposing raw
tokens by returning a masked representation; locate the data class LoginResponse
and add an override fun toString() that prints a non-sensitive summary (e.g.,
show token prefixes or fixed masked text like "accessToken=****",
"refreshToken=****" or first N chars + masked tail) so any accidental logging or
exception formatting won’t reveal full accessToken/refreshToken values.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.kt`:
- Around line 7-12: The WithdrawRequest data class uses redundant `@SerialName`
annotations for properties reasonCategory and reasonText; remove the `@SerialName`
annotations from those properties in the WithdrawRequest class (leave the
property names as-is), and if present remove the unused
kotlinx.serialization.SerialName import to keep imports clean — only keep
`@SerialName` if the JSON key differs from the property name.
In `@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/StatusView.kt`:
- Around line 80-96: The current StatusLottie implementation force-applies size
last via modifier.size(80.dp) so callers like StatusLottie(modifier =
Modifier.size(120.dp)) are overridden; fix by making the default size apply
before the caller modifier (so caller wins) — e.g. change the call site to use
Modifier.size(80.dp).then(modifier) or set the parameter default to modifier:
Modifier = Modifier.size(80.dp) and pass modifier directly; refer to function
StatusLottie and the modifier.size(80.dp) usage and ensure the caller-provided
modifier is applied last so its size takes precedence.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt`:
- Around line 45-72: The Timber.tag("ProfileTest") occurrences look like
leftover dev tags—replace them with a meaningful, consistent tag for this class
(e.g., a class-level TAG or use ProfileViewModel::class.simpleName) wherever
used; update the Timber calls in the logout() and withdraw() flows (both d(...)
and e(...)) to use that TAG so logging follows the project convention and can be
cleaned up before release.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1e10636e-8124-4610-a2f0-f9ccf6625fb7
📒 Files selected for processing (55)
.gitignorePrezel/app/src/main/AndroidManifest.xmlPrezel/app/src/main/res/xml/network_security_config.xmlPrezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidFeatureImplConventionPlugin.ktPrezel/build.gradle.ktsPrezel/core/auth/src/main/java/com/team/prezel/core/auth/AuthManager.ktPrezel/core/auth/src/main/java/com/team/prezel/core/auth/KakaoAuthClient.ktPrezel/core/auth/src/main/java/com/team/prezel/core/auth/model/AuthResult.ktPrezel/core/data/build.gradle.ktsPrezel/core/data/src/main/java/com/team/prezel/core/data/ApiResponseExt.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.ktPrezel/core/domain/.gitignorePrezel/core/domain/build.gradle.ktsPrezel/core/domain/src/main/java/com/team/prezel/core/domain/AuthRepository.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/CheckLoginStatusUseCase.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/LoginUseCase.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/LogoutUseCase.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/ReissueTokenUseCase.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/WithdrawUseCase.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/auth/AuthToken.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/auth/WithdrawReason.ktPrezel/core/network/build.gradle.ktsPrezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenStore.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/DataStoreAuthTokenStore.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/TokenRefreshAuthenticator.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/DataSourceModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/TokenStoreModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginRequest.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginResponse.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/ReissueTokenRequest.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/service/AuthService.ktPrezel/core/ui/src/main/java/com/team/prezel/core/ui/StatusView.ktPrezel/feature/login/impl/build.gradle.ktsPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiIntent.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.ktPrezel/feature/profile/impl/build.gradle.ktsPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileUiEffect.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileUiState.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.ktPrezel/feature/splash/impl/build.gradle.ktsPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.ktPrezel/gradle/libs.versions.tomlPrezel/settings.gradle.kts
b6665b5 to
8cbbf69
Compare
* **feat: 인증 관련 네트워크 데이터 소스 및 API 구현**
* `AuthService`: `login`, `logout`, `reissueToken`, `withdraw` 등 인증 관련 Ktorfit API 엔드포인트 정의
* `AuthRemoteDataSourceImpl`: 서버 API 호출 및 인증 토큰 응답 로깅 로직 구현
* `LoginRequest`, `LoginResponse` 등 인증 관련 데이터 모델(DTO) 추가
* **feat: 도메인 및 데이터 레이어 내 AuthRepository 추가**
* `core:domain` 모듈에 `AuthRepository` 인터페이스를 정의하고, `core:data`에 `AuthRepositoryImpl` 구현체를 추가했습니다.
* Hilt를 이용한 `RepositoryModule` 및 `DataSourceModule` 의존성 주입 설정을 완료했습니다.
* **feat: Kakao 로그인 idToken 연동 및 서버 로그인 로직 추가**
* `KakaoAuthClient`: 로그인 성공 시 SDK로부터 받은 `idToken`을 `AuthResult.Success`에 포함하여 반환하도록 수정했습니다.
* `LoginViewModel`: 소셜 로그인 성공 후 받은 `idToken`을 `AuthRepository.login`을 통해 서버에 전달하고 인증을 완료하는 흐름을 구현했습니다.
* `LoginUiState` 및 `Intent`: 로그인 요청 중인 제공자(`pendingProvider`) 상태를 관리하도록 개선했습니다.
* **refactor: 네트워크 보안 설정 및 UI 컴포넌트 개선**
* `network_security_config.xml`: 개발 서버 도메인(`prezel.p-e.kr`)에 대해 명시적으로 Cleartext 트래픽 허용 설정을 추가했습니다.
* `StatusView`: `StatusLottie`의 `modifier` 적용 방식을 개선하여 크기 지정 로직을 수정했습니다.
* **build: 모듈 구조 변경 및 의존성 추가**
* `core:domain` 모듈을 신규 생성하고 `settings.gradle.kts`에 등록했습니다.
* `feature:login:impl`, `core:data` 등 주요 모듈에 `core:domain` 및 인증 관련 의존성을 추가했습니다.
* `.gitignore`: `.kotlin`, `.DS_Store` 등 불필요한 파일 제외 설정을 추가했습니다.
* **feat: 인증 관련 UseCase 및 도메인 모델 추가**
* 인증 상태를 관리하기 위한 `AuthToken` 및 `WithdrawReason` 도메인 모델을 `core:model`에 추가했습니다.
* `LoginUseCase`, `LogoutUseCase`, `ReissueTokenUseCase`, `WithdrawUseCase`를 추가하여 인증 관련 비즈니스 로직을 구현했습니다.
* **feat: 네트워크 응답 모델 추가**
* 서버 통신을 위한 `LoginResponse`, `ReissueTokenRequest`, `WithdrawRequest` DTO를 `core:network`에 추가했습니다.
* **refactor: AuthRepository 및 구현체 개선**
* `AuthRepository` 인터페이스에 `reissueToken`, `logout`, `withdraw` 메서드를 추가하고, `login`의 반환 타입을 `Result<AuthToken>`으로 변경했습니다.
* `AuthRepositoryImpl`에서 `WithdrawReason`을 서버 카테고리 문자열로 변환하는 매핑 로직을 구현했습니다.
* `ApiResponse`를 `Result`로 변환하는 공통 확장 함수 `toResult`를 `core:data`에 추가하여 코드 중복을 제거했습니다.
* **refactor: LoginViewModel 내 로그인 호출 로직 수정**
* 서버 로그인 요청 시 불필요한 `provider` 파라미터를 제거하고 `idToken`만 전달하도록 수정했습니다.
* **build: 모듈 의존성 및 플러그인 설정 업데이트**
* `feature:login:impl`에 `core:model` 의존성을 추가했습니다.
* `AndroidFeatureImplConventionPlugin`에 `timber` 라이브러리 의존성을 추가했습니다.
* **feat: `AuthTokenStore` 인터페이스 및 DataStore 기반 구현체 추가**
* 인증 토큰(Access/Refresh Token)을 안전하게 저장하고 관리하기 위한 구조를 설계했습니다.
* `AuthTokenStore`: 토큰 조회, 저장, 삭제를 위한 인터페이스 정의
* `DataStoreAuthTokenStore`: `Jetpack DataStore`를 사용하여 로컬에 토큰을 영구 저장하는 구현체 추가 (메모리 캐싱 및 비동기 저장 로직 포함)
* `AuthTokenModule`: Hilt를 이용한 `AuthTokenStore` 의존성 주입 설정
* `libs.versions.toml` 및 `core:network` 모듈에 `datastore-preferences` 의존성 추가
* **feat: 토큰 자동 재발급 및 인증 로직 구현**
* 네트워크 요청 시 인증 상태를 관리하고, 401 오류 발생 시 토큰을 자동으로 갱신하는 로직을 추가했습니다.
* `AuthTokenRefresher`: Refresh Token을 사용하여 새로운 Access Token을 발급받는 기능 구현 (Mutex를 이용한 중복 요청 방지 및 실패 시 토큰 삭제)
* `TokenRefreshAuthenticator`: OkHttp `Authenticator`를 구현하여 401 Unauthorized 발생 시 토큰 재발급 및 재시도 로직 처리
* `requiresAuthorization()`: 로그인, 토큰 재발급 등 인증 헤더가 불필요한 경로를 판별하는 확장 함수 추가
* **refactor: `NetworkModule` 구조 개선 및 인증 인터셉터 적용**
* 인증이 필요한 요청과 토큰 재발급을 위한 클라이언트를 분리하고 공통 설정을 통합했습니다.
* `provideHttpClient`: `TokenRefreshAuthenticator`를 등록하고, 요청 헤더에 `Authorization` 토큰을 자동으로 추가하도록 개선
* `provideRefreshHttpClient`: 토큰 재발급 시 무한 루프를 방지하기 위해 별도의 `@Named("refresh")` 클라이언트 추가
* `configureBaseClient`: 공통적인 Ktor 클라이언트 설정(ContentNegotiation, Logging 등)을 별도 함수로 추출
* **refactor: `AuthRepositoryImpl` 내 토큰 관리 로직 연동**
* 인증 관련 API 호출 결과에 따라 로컬 토큰 상태를 동기화하도록 수정했습니다.
* `login`, `reissueToken` 성공 시 발급된 토큰을 `AuthTokenStore`에 저장하도록 `saveTokens` 도우미 함수 추가
* `logout`, `withdraw` 성공 시 저장된 토큰 정보를 삭제(`clear`)하도록 변경
* **refactor: `AuthTokenStore` 및 `DataStoreAuthTokenStore` 비동기화 및 안정성 개선**
* `saveTokens`와 `clear` 메서드를 `suspend`로 변경하여 비동기 데이터 저장을 보장하도록 개선했습니다.
* `DataStoreAuthTokenStore` 내부 로직에 `Mutex`를 도입하여 토큰 읽기/쓰기 시 발생할 수 있는 race condition을 방지했습니다.
* `init` 블록에서 `runBlocking`으로 초기화하던 로직을 별도의 `initializeCache` 메서드로 분리하고 `applicationScope`를 사용하도록 수정했습니다.
* **refactor: `AuthTokenRefresher` 예외 처리 및 토큰 갱신 로직 개선**
* `runCatching` 대신 `try-catch` 블록을 사용하여 토큰 재발급 로직의 명시성을 높였습니다.
* 토큰 재발급 성공 시 로직을 정돈하고 `Result` 반환 흐름을 개선했습니다.
* **refactor: `ApiResponse` 확장 함수 및 저장 로직 비동기 대응**
* `ApiResponse.toResult` 확장 함수에 `suspend` 및 `crossinline` 키워드를 추가하여 비동기 `transform` 함수를 지원하도록 변경했습니다.
* `AuthRepositoryImpl`의 `saveTokens`를 `suspend`로 변경하여 변경된 `AuthTokenStore` 인터페이스를 반영했습니다.
* **security: 디버그 로그 내 민감 정보 출력 제거**
* `AuthRemoteDataSourceImpl`, `KakaoAuthClient`, `AuthTokenRefresher`에서 디버그 시 출력하던 실제 토큰 값(`accessToken`, `refreshToken`, `idToken`)을 로그에서 삭제하고, 성공 여부만 출력하도록 변경하여 보안을 강화했습니다.
* **refactor: Ktor HTTP 클라이언트 로깅 설정 변경**
* 보안 강화를 위해 `Authorization` 헤더 값이 로그에 노출되지 않도록 `sanitizeHeader` 설정을 추가했습니다.
* 디버그 모드에서의 로그 레벨을 `LogLevel.BODY`에서 `LogLevel.HEADERS`로 변경하여 출력 정보의 범위를 조정했습니다.
* **refactor: DataStoreAuthTokenStore 초기화 방식 개선**
* `AuthTokenStore` 인터페이스에서 불필요한 `initializeCache()` 메서드를 제거했습니다.
* `DataStoreAuthTokenStore` 초기화 시 `CoroutineScope.launch`를 통한 비동기 방식 대신 `runBlocking`을 사용하여 인스턴스 생성 시점에 토큰 캐싱이 완료되도록 변경했습니다.
* `AtomicBoolean`을 이용한 중복 초기화 방지 로직을 삭제하여 코드를 단순화했습니다.
* **feat: CheckLoginStatusUseCase 추가**
* 로컬에 저장된 토큰 상태를 확인하여 로그인 여부를 판단하는 UseCase를 추가했습니다.
* Access Token이 없더라도 Refresh Token이 존재하면 토큰 재발급을 시도하여 로그인 상태를 유지하도록 로직을 구현했습니다.
* **feat: SplashViewModel 내 로그인 상태에 따른 화면 전환 로직 적용**
* `CheckLoginStatusUseCase`를 사용하여 초기 진입 시 사용자 상태를 확인합니다.
* 성공적으로 로그인 상태가 확인되면 홈 화면(`NavigateToHome`)으로, 그렇지 않으면 로그인 화면(`NavigateToLogin`)으로 이동하도록 분기 로직을 추가했습니다.
* **refactor: AuthRepository 토큰 조회 메서드 추가**
* `AuthRepository` 인터페이스 및 `AuthRepositoryImpl`에 `getAccessToken()`, `getRefreshToken()` 메서드를 추가하여 저장된 토큰에 접근할 수 있도록 개선했습니다.
* **build: feature:splash:impl 모듈 의존성 추가**
* `CheckLoginStatusUseCase` 사용을 위해 `core:domain` 모듈에 대한 의존성을 추가했습니다.
* **feat: ProfileViewModel 내 인증 관련 비즈니스 로직 추가**
* `LogoutUseCase` 및 `WithdrawUseCase`를 호출하여 로그아웃과 회원 탈퇴 기능을 구현했습니다.
* `ProfileUiState`를 로딩 상태를 관리하는 `data class` 구조로 개편하고, 화면 이동을 위한 `ProfileUiEffect`를 추가했습니다.
* 작업 성공 시 `NavigateToLogin` 이펙트를 발생시켜 로그인 화면으로 이동하도록 처리했습니다.
* **feat: ProfileScreen UI 구현 및 ViewModel 연동**
* 로그아웃 및 회원 탈퇴 버튼을 추가하고 클릭 이벤트를 ViewModel에 연결했습니다.
* `LaunchedEffect`를 통해 `ProfileUiEffect`를 수집하고, `LocalNavigator`를 사용하여 루트 화면을 로그인으로 전환하는 로직을 추가했습니다.
* **refactor: 네트워크 설정 및 상태 모델 정리**
* `NetworkModule`: JSON 직렬화 설정에서 불필요한 `coerceInputValues` 옵션을 제거했습니다.
* `ProfileUiState`: 기존 `sealed interface` 방식에서 `isLoading` 필드를 가진 `data class` 방식으로 단순화했습니다.
* **build: 모듈 의존성 추가**
* `feature:profile:impl` 모듈에 `core:domain`, `core:model`, `feature:login:api` 의존성을 추가했습니다.
* **refactor: Logout 및 Withdraw UseCase 내 토큰 처리 로직 개선**
* `ProfileViewModel`에서 직접 수행하던 액세스 토큰 조회 로직을 UseCase 내부로 이동하여 캡슐화를 강화했습니다.
* `LogoutUseCase`: `invoke()` 시 파라미터 대신 내부적으로 `AuthRepository`를 통해 토큰을 가져오도록 변경했습니다.
* `WithdrawUseCase`: `invoke()` 시 `WithdrawReason`만 받도록 변경하고, 내부에서 토큰 유효성 검사 로직을 추가했습니다.
* **refactor: ProfileViewModel 리팩터링**
* UseCase 변경에 따라 더 이상 필요하지 않은 `AuthRepository` 의존성을 제거했습니다.
* 로그아웃 및 회원탈퇴 실패 시 `ProfileUiEffect.ShowSnackbar`를 통해 에러 메시지를 전달하도록 예외 처리를 추가했습니다.
* **feat: 프로필 화면 UI 피드백 및 Scaffold 적용**
* `ProfileScreen`에 `Scaffold`와 `SnackbarHost`를 도입하여 에러 발생 시 사용자에게 알림을 표시할 수 있도록 개선했습니다.
* `ProfileUiEffect`에 `ShowSnackbar` 타입을 추가하여 ViewModel에서 UI로 메시지 전달이 가능하도록 정의했습니다.
* 불필요한 "Profile" 텍스트를 제거하고 레이아웃을 정돈했습니다.
* **docs: UseCase KDoc 업데이트**
* 변경된 토큰 획득 방식(내부 조회)에 맞춰 `LogoutUseCase`와 `WithdrawUseCase`의 주석 내용을 수정했습니다.
* **feat: `AuthManager` 로그아웃 로직 강화**
* `currentProvider`가 명시되지 않은 상태더라도 `authClients`에 등록된 공급자가 하나라면 해당 공급자를 통해 로그아웃을 진행할 수 있도록 예외 처리 로직을 개선했습니다.
* **refactor: `ProfileViewModel` 내 로그아웃 및 탈퇴 처리 로직 수정**
* 서버 측 로그아웃(`logoutUseCase`) 및 탈퇴(`withdrawUseCase`) 요청이 성공한 후, 클라이언트의 로컬 인증 세션을 정리하기 위해 `AuthManager.logout()`을 호출하도록 변경했습니다.
* UseCase의 결과를 `fold`로 처리하여 성공 시에만 로컬 인증 해제 로직이 수행되도록 구현했습니다.
* **build: `feature:profile:impl` 모듈 의존성 추가**
* `AuthManager`를 참조하기 위해 `core:auth` 모듈에 대한 의존성을 추가했습니다.
* **feat: 로그인 성공 시 홈 화면 내비게이션 추가**
* `LoginUiEffect`에 `NavigateToHome` 상태를 추가했습니다.
* `LoginScreen` 및 `LoginEntryBuilder`에서 로그인 성공 시 `HomeNavKey`로 루트를 교체하는 로직을 구현했습니다.
* **refactor: AuthRepositoryImpl 로그인 및 토큰 저장 로직 개선**
* `login` 메서드에서 `ApiResponse` 타입에 따른 명시적인 결과 처리를 수행하도록 수정했습니다.
* `ApiResponse.Success<LoginResponse>`를 직접 처리하여 토큰을 저장하고 도메인 모델(`AuthToken`)로 변환하는 오버로딩 함수를 추가했습니다.
* **refactor: ApiResponseConverterFactory 및 네트워크 관련 수정**
* `ApiResponseConverterFactory`에서 `ApiResponse.Success` 생성 시 파라미터 이름을 명시적으로 지정하도록 변경했습니다.
* `AuthRepositoryImpl`에서 불필요하게 사용되던 기존 `toResult` 확장 함수 의존성을 제거하고 직접 응답 상태를 분기 처리합니다.
* **refactor: 네트워크 DI 모듈 통합 및 이름 변경**
* `AuthNetworkModule`을 삭제하고, `AuthService` 제공 로직을 공통 `NetworkModule`로 통합하여 관리 효율성을 높였습니다.
* `AuthTokenModule`을 `TokenStoreModule`로 이름을 변경하여 토큰 저장소 관련 의존성 주입 역할을 명확히 했습니다.
* **docs: CheckLoginStatusUseCase KDoc 주석 추가**
* `core:domain` 모듈의 `CheckLoginStatusUseCase` 클래스에 로그인 상태 확인 로직의 동작 흐름(액세스 토큰 확인, 리프레시 토큰을 통한 재발급 시도 등)을 설명하는 한글 주석을 추가했습니다.
* refactor: `AuthPathPolicy` 싱글톤 객체 도입 인증이 필요한 API 경로 판별 로직을 별도의 객체로 분리하여 코드 중복을 제거하고 관리를 중앙화했습니다. * `AuthPathPolicy`: `/auth/login`, `/auth/reissue` 경로를 제외한 요청에 대해 인증 필요 여부(`requiresAuthorization`)를 판단하는 로직 추가 * `NetworkModule` 및 `TokenRefreshAuthenticator`에서 개별적으로 구현되어 있던 경로 확인 로직을 `AuthPathPolicy`로 교체 * refactor: `AuthRepositoryImpl` 코드 정리 및 데이터 변환 로직 개선 데이터 레이어의 가독성을 높이고 불필요한 코드를 정리했습니다. * `login`: `toResult` 확장 함수를 사용하여 응답 처리 로직을 간결하게 변경 * `saveTokens`: `ApiResponse.Success` 래퍼 대신 내부 데이터 모델(`LoginResponse`)을 직접 받도록 수정 * `WithdrawReason` 매핑: 기존 커스텀 Getter 형식을 `toCategory()`, `toReasonText()` 메서드로 변경하여 도메인 모델 변환 로직 명확화 * 불필요한 `ApiResponse` 및 관련 확장 함수(`toAuthToken`) 정리
* **refactor: Auth 관련 인터페이스 및 UseCase 패키지 경로 변경**
* 도메인 모델의 응집도를 높이기 위해 인증 관련 파일들을 세부 패키지로 이동했습니다.
* `AuthRepository`: `core.domain` -> `core.domain.repository.auth`
* `LogoutUseCase`, `ReissueTokenUseCase`, `WithdrawUseCase`, `CheckLoginStatusUseCase`, `LoginUseCase`: `core.domain.usecase` -> `core.domain.usecase.auth`
* 패키지 이동에 따른 `RepositoryModule` 및 각 기능별(Splash, Login, Profile) `ViewModel` 내 참조 경로를 수정했습니다.
* **refactor: AuthTokenRefresher 예외 처리 및 안정성 강화**
* 토큰 재발급 중 코루틴이 취소될 경우 `CancellationException`을 명시적으로 다시 던지도록 개선했습니다.
* 일반적인 `Exception` 발생 시에만 `isInvalidRefreshToken()` 여부를 확인하여 토큰 저장소를 정리하고 로그를 기록하도록 변경했습니다.
* **docs: AuthTokenStore KDoc 주석 추가**
* `AuthTokenStore` 인터페이스에 메모리 캐시 동기 조회 및 영속성 저장소 갱신에 관한 동작 계약을 설명하는 주석을 추가했습니다.
* **style: 코드 포맷팅 정리**
* `AuthRepositoryImpl`, `AuthPathPolicy`, `TokenRefreshAuthenticator` 등 일부 클래스의 줄바꿈 및 불필요한 공백을 수정했습니다.
8cbbf69 to
0e23931
Compare
* **feat: 공통 API 에러 응답 모델 및 예외 클래스 추가**
* 서버 응답 규격에 맞춘 `ApiErrorResponse` 모델을 `core:network`에 추가했습니다.
* HTTP 에러 발생 시 상태 코드와 서비스 에러 코드를 포함하는 `ApiHttpException` 도메인 예외 클래스를 정의했습니다.
* `ApiResponseConverterFactory`에서 HTTP 에러 응답 바디를 파싱하여 `ApiResponse.Failure.HttpError`에 포함하도록 개선했습니다.
* `ApiResponse.toResult` 확장 함수에서 `ResponseException`을 `ApiHttpException`으로 변환하여 반환하도록 수정했습니다.
* **refactor: AuthTokenRefresher 로직 개선**
* `AuthTokenRefresher`가 직접 `HttpClient`를 사용하지 않고 `AuthRemoteDataSource`를 의존하도록 변경했습니다.
* 토큰 재발급 실패 시, 에러 코드(`T001`, `U003`)를 확인하여 복구 불가능한 세션인 경우에만 로컬 토큰을 삭제하도록 로직을 정교화했습니다.
* **feat: 프로필(Profile) 기능 내 인증 만료 및 에러 처리 구현**
* 로그아웃 및 회원 탈퇴 실패 시, 인증 만료 코드(`U001`) 여부에 따라 로그인 화면으로 강제 이동시키고 안내 메시지를 표시하는 로직을 추가했습니다.
* `ProfileUiState`, `ProfileUiEffect`를 `contract` 패키지로 이동하고 `UiState`, `UiEffect` 인터페이스를 상속받도록 리팩터링했습니다.
* `ProfileUiMessage` 열거형과 관련 문자열 리소스(`logout_failed`, `withdraw_failed`, `authentication_expired`)를 추가했습니다.
* **refactor: 로그인(Login) UI 메시지 네이밍 컨벤션 적용**
* `LoginUiMessage`의 상수 이름을 대문자 스네이크 케이스(`LOGIN_CANCELLED` 등)로 변경하여 통일성을 높였습니다.
* **misc: 기타 설정 변경**
* `AndroidManifest.xml`에 `usesCleartextTraffic="true"` 설정을 추가했습니다.
* `AuthManager`에 현재 인증 프로바이더 정보를 초기화하는 `clearCurrentProvider()` 메서드를 추가했습니다.
* 루트 `build.gradle.kts`에서 불필요한 `kotlin.jvm` 플러그인 설정을 제거했습니다.
* **feat: `core:datastore` 모듈 생성 및 `AuthTokenStore` 이전**
* `core:network`에 위치하던 `AuthTokenStore` 인터페이스와 `DataStoreAuthTokenStore` 구현체를 신규 생성한 `core:datastore` 모듈로 이동하여 책임을 분리했습니다.
* DataStore 관련 의존성을 `core:network`에서 `core:datastore`로 재배치했습니다.
* `CoroutineScopesModule`을 이동하고 `Dispatchers.IO`를 기본값으로 사용하도록 수정했습니다.
* **refactor: `AuthRepository` 및 `UseCase` 인증 로직 개선**
* `AuthRepository` 인터페이스에서 `logout`, `withdraw` 호출 시 외부에서 `accessToken`을 전달받던 방식을 내부 `AuthTokenStore`에서 직접 참조하는 방식으로 변경했습니다.
* 이에 따라 `LogoutUseCase`와 `WithdrawUseCase`에서 토큰 존재 여부를 확인하던 중복 로직을 제거했습니다.
* `AuthRepositoryImpl`에서 인증 실패(U001) 코드 응답 시, 로컬 토큰을 삭제하고 `AuthenticationRequiredException`을 반환하도록 개선했습니다.
* **feat: `AuthenticationRequiredException` 도메인 에러 추가**
* 인증이 필요한 상황을 명시적으로 처리하기 위한 커스텀 예외 클래스를 `core:domain`에 추가했습니다.
* `ProfileViewModel`에서 하드코딩된 에러 코드로 판단하던 로직을 해당 예외 타입을 확인하도록 리팩터링했습니다.
* **refactor: `NetworkModule` 정리**
* 사용하지 않는 `refresh` 전용 HttpClient 설정을 제거하여 의존성 구조를 단순화했습니다.
* **refactor: `DataStoreAuthTokenStore` 초기화 방식 변경**
* 초기 토큰 로드 시 사용하던 `runBlocking`을 제거하고 `applicationScope.launch`를 이용한 비동기 방식으로 변경하여 메인 스레드 차단 가능성을 방지했습니다.
* **refactor: `AuthRepositoryImpl` 인증 예외 처리 강화**
* 로그아웃 및 회원 탈퇴 시 액세스 토큰이 없는 경우, 단순히 에러를 반환하던 방식에서 로컬 토큰을 초기화(`clear`)한 후 인증 필요 예외를 반환하도록 `clearTokensAndAuthenticationRequired` 메서드를 추가했습니다.
* **refactor: `ProfileViewModel` 로딩 상태 관리 개선**
* 로그아웃(`logout`) 및 회원 탈퇴(`withdraw`) 로직에 `try-finally` 블록을 적용했습니다. 이를 통해 작업 성공 여부나 예외 발생과 관계없이 `isLoading` 상태가 `false`로 안전하게 변경되도록 보장했습니다.
* **style: `AuthRepositoryImpl` 코드 포맷팅 수정**
* `login` 메서드의 줄바꿈 등 가독성 향상을 위한 코드 스타일을 정리했습니다.
* **feat: `AuthActionResult` sealed interface 추가**
* 인증 기반 작업(로그아웃, 회원 탈퇴)의 결과를 명확하게 정의하기 위해 `Success`, `AuthenticationRequired`, `Failure` 상태를 가지는 `AuthActionResult`를 `core:domain`에 추가했습니다.
* 기존의 `AuthenticationRequiredException`을 제거하고 도메인 모델을 통한 상태 처리를 지향하도록 변경했습니다.
* **refactor: `LogoutUseCase` 및 `WithdrawUseCase` 반환 타입 변경**
* 기존 `Result<Unit>` 대신 `AuthActionResult`를 반환하도록 수정했습니다.
* UseCase 내부에 있던 토큰 조회 로직을 Repository로 위임하여 책임을 분리하고 설명을 보완하는 KDoc을 업데이트했습니다.
* **refactor: `AuthRepository` 및 `AuthRepositoryImpl` 로직 개선**
* `logout` 및 `withdraw` 메서드의 반환 타입을 `AuthActionResult`로 변경했습니다.
* 토큰 부재 시 `clearTokensAndAuthenticationRequired`를 호출하여 상태를 일관되게 관리하도록 수정했습니다.
* API 응답 유형(Success, HttpError, NetworkError)에 따라 `AuthActionResult`를 매핑하는 `toAuthActionResult` 확장 함수를 추가했습니다.
* **refactor: `ProfileViewModel` 내 인증 결과 처리 로직 고도화**
* `handleAuthActionResult`를 추가하여 `AuthActionResult` 상태에 따른 UI 에펙트(`NavigateToLogin`, `ShowMessage`) 및 `AuthManager` 상태 제어 로직을 통합했습니다.
* 로그아웃 및 회원 탈퇴 호출 시 중복되던 후처리 로직을 간소화했습니다.
* refactor: `AuthActionResult` 클래스 패키지 경로 변경 `AuthActionResult` 인터페이스를 UseCase 패키지에서 Result 관련 공통 패키지로 이동하여 도메인 레이어의 구조를 개선했습니다. * `core:domain` 내 `usecase.auth` -> `result.auth`로 파일 이동 및 패키지 명 수정 * refactor: 패키지 이동에 따른 의존성 참조 수정 `AuthActionResult`의 위치가 변경됨에 따라 이를 참조하는 Repository, UseCase, ViewModel 등의 import 문을 업데이트했습니다. * `AuthRepository`, `AuthRepositoryImpl` 내 import 수정 * `WithdrawUseCase`, `LogoutUseCase` 내 import 수정 * `feature:profile:impl` 모듈의 `ProfileViewModel` 내 import 수정
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (2)
Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt (1)
58-72:⚠️ Potential issue | 🟠 Major서버 로그인 성공 시 항상 약관 화면으로 이동하는 이슈가 여전히 남아 있습니다.
PR 목표는 "로그인 성공 시 토큰 저장 후 홈으로 이동"인데, Line 65는 모든 성공 케이스를 약관 화면으로 보냅니다. 신규 사용자만 약관 동의가 필요하다면
authRepository.login(...)응답/도메인 모델에서 신규 여부(예:isNewUser,requiresTerms)를 구분해 분기해 주세요. 기존/신규 구분 없이는 기존 사용자도 매 로그인마다 약관 화면으로 튕기게 됩니다.또한
onFailure가 원인과 무관하게 항상LOGIN_FAILED_UNKNOWN으로만 메시지를 노출하고 있어, 네트워크/레이트리밋 같은 구체적인 실패 사유가 UI에 반영되지 않습니다. 레포지토리 레이어에서 실패 타입을 노출할 수 있다면 함께 매핑해 두는 편이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt` around lines 58 - 72, handleServerLogin currently treats every successful authRepository.login(...) result as a new user and always sends LoginUiEffect.NavigateToTerms; change it to inspect the login response (add or read flags like isNewUser or requiresTerms on the domain/model returned by authRepository.login) and branch: if requiresTerms/isNewUser then send NavigateToTerms, otherwise persist the token via your token storage method and send NavigateToHome; also update the onFailure branch to map repository-level error types (network, rateLimit, unauthorized, etc.) to specific LoginUiMessage constants instead of always returning LOGIN_FAILED_UNKNOWN by exposing error types from authRepository.login and switching on those to send more specific UI effects.Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt (1)
55-74:⚠️ Potential issue | 🟠 Major회원 탈퇴 사유가 여전히 하드코딩되어 있습니다.
withdraw()가 계속WithdrawReason.Other("임시 테스트 탈퇴")를 고정값으로 전달하고 있어, 실제 사용자가 선택/입력한 사유가 서버에 전달되지 않습니다. 이 상태로 배포될 경우 탈퇴 사유 통계/운영 데이터가 테스트 문자열로 오염될 수 있으니, 탈퇴 사유 선택 UI에서 전달된 값을 파라미터로 받도록 수정이 필요합니다.🛠️ 제안 수정
- fun withdraw() { + fun withdraw(reason: WithdrawReason) { if (_uiState.value.isLoading) return viewModelScope.launch { try { _uiState.update { it.copy(isLoading = true) } val result = - withdrawUseCase( - reason = WithdrawReason.Other("임시 테스트 탈퇴"), - ) + withdrawUseCase(reason = reason) handleAuthActionResult(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt` around lines 55 - 74, The withdraw() function currently sends a hardcoded WithdrawReason.Other("임시 테스트 탈퇴"); change withdraw() to accept the actual user-selected reason (e.g., add a parameter like reason: WithdrawReason) and pass that into withdrawUseCase instead of the fixed value; update any callers (UI layer) to provide the selected reason from the withdrawal selection/input screen, keep the same loading/failure handling (calls to _uiState.update, viewModelScope.launch, handleAuthActionResult and ProfileUiMessage.WITHDRAW_FAILED) intact.
🧹 Nitpick comments (5)
Prezel/core/ui/src/main/java/com/team/prezel/core/ui/StatusView.kt (1)
82-96:StatusLottie의 고정 사이즈 처리 관련 개선 제안
modifier기본값을Modifier로 완화한 것은 좋으나, 호출부에서modifier.size(80.dp)를 항상 뒤에 강제로 체이닝하므로 호출자가modifier로 사이즈를 전달해도 내부의 80.dp 제약이 그대로 적용되어 사실상 재정의가 어렵습니다. 사이즈를 외부에서 주입 가능하도록 파라미터화하거나, 기본값을Modifier.size(80.dp)로 두고 호출부에서는modifier만 전달하는 기존 패턴이 더 유연합니다.♻️ 제안 예시
`@Composable` fun StatusLottie( `@RawRes` lottieJsonResId: Int, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier.size(80.dp), ) { ... LottieAnimation( composition = composition, progress = { progress }, - modifier = modifier.size(80.dp), + modifier = modifier, ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/StatusView.kt` around lines 82 - 96, StatusLottie currently forces size by chaining modifier.size(80.dp) inside the LottieAnimation which prevents callers from overriding size; change the API so size is injectable by either (A) adding a sizeDp (or size: Dp) parameter with a default of 80.dp and use modifier.size(sizeDp) in LottieAnimation, or (B) make the default parameter modifier = Modifier.size(80.dp) and remove the hard-coded .size(80.dp) from the LottieAnimation call; update the function signature (StatusLottie) and the LottieAnimation invocation (composition = composition, progress = { progress }, modifier = modifier) accordingly so external callers can set size via the passed modifier or explicit size param.Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt (1)
94-94: Timber 태그가 디버그용으로 보입니다.
"ProfileTest"태그는 임시 디버깅용으로 남은 것 같습니다. 로그 필터링/집계 시 다른 화면과 구분이 어렵고 의미도 불명확하니,ProfileViewModel등 클래스명을 사용하거나 태그 없이 기본 태그에 맡기는 형태로 정리하는 것을 권장합니다.♻️ 제안 리팩터
- Timber.tag("ProfileTest").e(result.throwable, failureLog) + Timber.e(result.throwable, failureLog)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt` at line 94, The log call uses a debug-only tag Timber.tag("ProfileTest").e(result.throwable, failureLog); replace the hardcoded "ProfileTest" with a meaningful tag such as ProfileViewModel::class.java.simpleName or remove the explicit tag and call Timber.e(...) so logs use the default class tag; update the Timber call located in ProfileViewModel to use the class-based tag for consistent, production-appropriate logging.Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt (1)
20-25:encodeDefaults는 디코딩 전용Json에서 불필요이
Json인스턴스는parseErrorResponse에서decodeFromString에만 사용됩니다.encodeDefaults = true는 인코딩에만 영향을 주는 설정이므로 제거하는 것이 의도 명확화에 도움이 됩니다.♻️ 제안
- private val json = - Json { - ignoreUnknownKeys = true - encodeDefaults = true - prettyPrint = false - } + private val json = Json { ignoreUnknownKeys = true }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt` around lines 20 - 25, The Json instance declared as "json" is only used for decoding in parseErrorResponse, so remove the encodeDefaults = true setting to clarify intent; update the Json initializer (refer to the json property and parseErrorResponse function) to omit encodeDefaults and keep ignoreUnknownKeys and prettyPrint as-is.Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/di/CoroutineScopesModule.kt (1)
19-24:Dispatchers.IO하드코딩으로 Dispatcher 주입 일관성 저하기존에는
@Dispatcher(PrezelDispatchers.Default)기반 주입이었으나, 현재는Dispatchers.IO를 직접 참조합니다. DataStore가 I/O 바운드이므로 선택 자체는 합리적이지만, 프로젝트에 이미DispatchersModule이@Dispatcher(PrezelDispatchers.IO)를 제공하고 있으므로 테스트 치환/일관성을 위해 해당 Qualifier를 주입받는 방식이 더 좋습니다.♻️ 제안
- fun providesCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + fun providesCoroutineScope( + `@Dispatcher`(PrezelDispatchers.IO) ioDispatcher: CoroutineDispatcher, + ): CoroutineScope = CoroutineScope(SupervisorJob() + ioDispatcher)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/di/CoroutineScopesModule.kt` around lines 19 - 24, Replace the hardcoded Dispatchers.IO with the project's Dispatcher-qualified provider: update CoroutineScopesModule.providesCoroutineScope to accept an injected CoroutineDispatcher annotated `@Dispatcher`(PrezelDispatchers.IO) (provided by DispatchersModule) and construct the scope as CoroutineScope(SupervisorJob() + injectedDispatcher) so tests can swap the dispatcher and keep injection consistent.Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthPathPolicy.kt (1)
3-8:endsWith매칭으로 인한 오탐 가능성 확인 필요
encodedPath.endsWith("/auth/login")방식은/v1/auth/login같이 prefix가 붙는 정상 경로에는 잘 동작하지만, 만약 향후/xauth/login같은 관련 없는 경로가 추가되면 슬래시 경계 덕분에 실제로는 안전합니다. 다만 query string이encodedPath에 포함되지 않는다는 Ktor/OkHttp 동작에 의존하고 있으니, 이 정책이 KtorUrl.encodedPath와 OkHttpRequest.url.encodedPath양쪽에서 동일하게 path-only를 반환한다는 전제를 확인해 주세요. 또한 대소문자 민감성(서버가 대소문자를 구분하지 않는 경우)도 확인 대상입니다.경로 집합을 명시적으로 두는 편이 의도가 더 드러납니다:
♻️ 대안
-internal object AuthPathPolicy { - private const val LOGIN_PATH = "/auth/login" - private const val REISSUE_PATH = "/auth/reissue" - - fun requiresAuthorization(encodedPath: String): Boolean = !encodedPath.endsWith(LOGIN_PATH) && !encodedPath.endsWith(REISSUE_PATH) -} +internal object AuthPathPolicy { + private val NO_AUTH_SUFFIXES = listOf("/auth/login", "/auth/reissue") + + fun requiresAuthorization(encodedPath: String): Boolean = + NO_AUTH_SUFFIXES.none { encodedPath.endsWith(it) } +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthPathPolicy.kt` around lines 3 - 8, 현재 AuthPathPolicy.requiresAuthorization는 encodedPath.endsWith(LOGIN_PATH) 방식에 의존해 경로 매칭을 하고 있어 쿼리 문자열 포함 여부나 대소문자 민감성에 따라 오탐이 발생할 수 있으므로, requiresAuthorization/LOGIN_PATH/REISSUE_PATH를 수정해 허용된 경로 집합으로 정확히 비교하거나 명시적으로 URL의 path-only(쿼리 제외)를 검사하고 케이스를 통일(lowercase)한 뒤 비교하도록 변경하세요; 즉 AuthPathPolicy.requiresAuthorization에서 encodedPath를 직접 endsWith로 비교하지 않고 Ktor/OkHttp의 Url.encodedPath가 path-only임을 보장할 수 없으면 URL을 파싱해 path 부분만 추출하거나 미리 정의한 Set<String> (예: setOf(LOGIN_PATH, REISSUE_PATH))을 사용해 정확히 일치하는지 검사하고 필요시 비교 전에 toLowerCase()로 정규화하도록 구현하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/DataStoreAuthTokenStore.kt`:
- Around line 42-47: The init asynchronous preload (applicationScope.launch {
val preferences = readPreferences() ... accessToken = ... refreshToken = ... })
must be performed inside the same mutex-protected critical section used by
saveTokens()/clear() to avoid a late read overwriting newer values; wrap the
initial readPreferences() result assignment to accessToken and refreshToken
inside the existing mutex.withLock (use the same mutex instance) so that
initialization, saveTokens(), and clear() are serialized; apply the same change
to the other two similar blocks referenced (the blocks around lines 58-65 and
68-75) to ensure all initial cache loads use the mutex.
In
`@Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/auth/CheckLoginStatusUseCase.kt`:
- Around line 23-26: In CheckLoginStatusUseCase (the logic around
authRepository.getRefreshToken() and authRepository.reissueToken(refreshToken)),
change the simple isSuccess check to inspect the reissueToken result/error
reason and only return false for authentication failures (e.g., HTTP 401/invalid
refresh token) while treating transient/network errors as optimistic true;
specifically, update the code in the CheckLoginStatusUseCase method that calls
authRepository.reissueToken to branch on the result type or error code (or
exception type) and return false for auth errors but return true for transient
errors so the stored refresh token is preserved for later Authenticator retry.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt`:
- Around line 82-86: The parseErrorResponse function currently swallows all
exceptions via runCatching including CancellationException; change its error
handling to rethrow CancellationException and only absorb other failures.
Replace the runCatching usage with an explicit try/catch around
json.decodeFromString<ApiErrorResponse>(...) that catches Throwable (or
Exception) but if the caught throwable is a CancellationException rethrow it,
otherwise return null; reference the parseErrorResponse method and the
ResponseException parameter when making this change.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt`:
- Around line 85-90: The HttpClient configured in configureBaseClient currently
sets expectSuccess = true which causes Ktor to throw ResponseException on
non-2xx responses before ApiResponseConverterFactory can map them to
ApiResponse.Failure.HttpError; change this by turning off expectSuccess (remove
the assignment or set expectSuccess = false) in configureBaseClient so
ApiResponse-based converters (see ApiResponseConverterFactory) can receive raw
responses and callers like logout and reissueToken can handle
ApiResponse.Failure.HttpError as intended.
- Around line 63-65: AuthPathPolicy.requiresAuthorization currently returns true
for /auth/logout and /auth/withdraw which causes NetworkModule's defaultRequest
to inject an Authorization header in addition to explicit
`@Header`("Authorization") passed from AuthRemoteDataSourceImpl.logout and
withdraw; to fix, update AuthPathPolicy.requiresAuthorization to explicitly
exclude the "/auth/logout" and "/auth/withdraw" paths (so
NetworkModule.defaultRequest will not auto-inject for those methods) and keep
AuthRemoteDataSourceImpl.logout and withdraw as the single source of the header;
ensure you check the request path normalization used by requiresAuthorization to
match how logout/withdraw are called.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt`:
- Around line 82-85: In ProfileViewModel inside the AuthActionResult.Success
branch, handle the Result returned by authManager.logout() instead of ignoring
it: call authManager.logout(), inspect if it's failure (Result.isFailure) and
emit the same ProfileUiEffect.NavigateToLogin only after calling it, but log a
warning with Timber.w(...) including the failure.exceptionOrNull() message when
logout fails; apply the same pattern for the account withdrawal flow where
authManager.logout() is invoked so failures are at least logged rather than
silently swallowed.
---
Duplicate comments:
In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt`:
- Around line 58-72: handleServerLogin currently treats every successful
authRepository.login(...) result as a new user and always sends
LoginUiEffect.NavigateToTerms; change it to inspect the login response (add or
read flags like isNewUser or requiresTerms on the domain/model returned by
authRepository.login) and branch: if requiresTerms/isNewUser then send
NavigateToTerms, otherwise persist the token via your token storage method and
send NavigateToHome; also update the onFailure branch to map repository-level
error types (network, rateLimit, unauthorized, etc.) to specific LoginUiMessage
constants instead of always returning LOGIN_FAILED_UNKNOWN by exposing error
types from authRepository.login and switching on those to send more specific UI
effects.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt`:
- Around line 55-74: The withdraw() function currently sends a hardcoded
WithdrawReason.Other("임시 테스트 탈퇴"); change withdraw() to accept the actual
user-selected reason (e.g., add a parameter like reason: WithdrawReason) and
pass that into withdrawUseCase instead of the fixed value; update any callers
(UI layer) to provide the selected reason from the withdrawal selection/input
screen, keep the same loading/failure handling (calls to _uiState.update,
viewModelScope.launch, handleAuthActionResult and
ProfileUiMessage.WITHDRAW_FAILED) intact.
---
Nitpick comments:
In
`@Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/di/CoroutineScopesModule.kt`:
- Around line 19-24: Replace the hardcoded Dispatchers.IO with the project's
Dispatcher-qualified provider: update
CoroutineScopesModule.providesCoroutineScope to accept an injected
CoroutineDispatcher annotated `@Dispatcher`(PrezelDispatchers.IO) (provided by
DispatchersModule) and construct the scope as CoroutineScope(SupervisorJob() +
injectedDispatcher) so tests can swap the dispatcher and keep injection
consistent.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.kt`:
- Around line 20-25: The Json instance declared as "json" is only used for
decoding in parseErrorResponse, so remove the encodeDefaults = true setting to
clarify intent; update the Json initializer (refer to the json property and
parseErrorResponse function) to omit encodeDefaults and keep ignoreUnknownKeys
and prettyPrint as-is.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthPathPolicy.kt`:
- Around line 3-8: 현재 AuthPathPolicy.requiresAuthorization는
encodedPath.endsWith(LOGIN_PATH) 방식에 의존해 경로 매칭을 하고 있어 쿼리 문자열 포함 여부나 대소문자 민감성에 따라
오탐이 발생할 수 있으므로, requiresAuthorization/LOGIN_PATH/REISSUE_PATH를 수정해 허용된 경로 집합으로
정확히 비교하거나 명시적으로 URL의 path-only(쿼리 제외)를 검사하고 케이스를 통일(lowercase)한 뒤 비교하도록 변경하세요; 즉
AuthPathPolicy.requiresAuthorization에서 encodedPath를 직접 endsWith로 비교하지 않고
Ktor/OkHttp의 Url.encodedPath가 path-only임을 보장할 수 없으면 URL을 파싱해 path 부분만 추출하거나 미리
정의한 Set<String> (예: setOf(LOGIN_PATH, REISSUE_PATH))을 사용해 정확히 일치하는지 검사하고 필요시 비교
전에 toLowerCase()로 정규화하도록 구현하세요.
In `@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/StatusView.kt`:
- Around line 82-96: StatusLottie currently forces size by chaining
modifier.size(80.dp) inside the LottieAnimation which prevents callers from
overriding size; change the API so size is injectable by either (A) adding a
sizeDp (or size: Dp) parameter with a default of 80.dp and use
modifier.size(sizeDp) in LottieAnimation, or (B) make the default parameter
modifier = Modifier.size(80.dp) and remove the hard-coded .size(80.dp) from the
LottieAnimation call; update the function signature (StatusLottie) and the
LottieAnimation invocation (composition = composition, progress = { progress },
modifier = modifier) accordingly so external callers can set size via the passed
modifier or explicit size param.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt`:
- Line 94: The log call uses a debug-only tag
Timber.tag("ProfileTest").e(result.throwable, failureLog); replace the hardcoded
"ProfileTest" with a meaningful tag such as
ProfileViewModel::class.java.simpleName or remove the explicit tag and call
Timber.e(...) so logs use the default class tag; update the Timber call located
in ProfileViewModel to use the class-based tag for consistent,
production-appropriate logging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c091040e-7b37-4210-8d0c-a0c337a29c89
📒 Files selected for processing (64)
.gitignorePrezel/app/src/main/AndroidManifest.xmlPrezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidFeatureImplConventionPlugin.ktPrezel/core/auth/src/main/java/com/team/prezel/core/auth/AuthManager.ktPrezel/core/auth/src/main/java/com/team/prezel/core/auth/KakaoAuthClient.ktPrezel/core/auth/src/main/java/com/team/prezel/core/auth/model/AuthResult.ktPrezel/core/data/build.gradle.ktsPrezel/core/data/src/main/java/com/team/prezel/core/data/ApiResponseExt.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.ktPrezel/core/datastore/build.gradle.ktsPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthTokenStore.ktPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/DataStoreAuthTokenStore.ktPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/di/CoroutineScopesModule.ktPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/di/TokenStoreModule.ktPrezel/core/domain/.gitignorePrezel/core/domain/build.gradle.ktsPrezel/core/domain/src/main/java/com/team/prezel/core/domain/error/ApiHttpException.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/repository/auth/AuthRepository.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/result/auth/AuthActionResult.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/auth/CheckLoginStatusUseCase.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/auth/LoginUseCase.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/auth/LogoutUseCase.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/auth/ReissueTokenUseCase.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/auth/WithdrawUseCase.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/auth/AuthToken.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/auth/WithdrawReason.ktPrezel/core/network/build.gradle.ktsPrezel/core/network/src/main/java/com/team/prezel/core/network/ApiResponseConverterFactory.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthPathPolicy.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/TokenRefreshAuthenticator.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/DataSourceModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiErrorResponse.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiResponse.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginRequest.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginResponse.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/ReissueTokenRequest.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/service/AuthService.ktPrezel/core/ui/src/main/java/com/team/prezel/core/ui/StatusView.ktPrezel/feature/login/impl/build.gradle.ktsPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiIntent.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/model/LoginUiMessage.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.ktPrezel/feature/profile/impl/build.gradle.ktsPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileUiState.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiEffect.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiState.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/model/ProfileUiMessage.ktPrezel/feature/profile/impl/src/main/res/values/strings.xmlPrezel/feature/splash/impl/build.gradle.ktsPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.ktPrezel/gradle/libs.versions.tomlPrezel/settings.gradle.kts
💤 Files with no reviewable changes (1)
- Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileUiState.kt
✅ Files skipped from review due to trivial changes (30)
- .gitignore
- Prezel/core/network/build.gradle.kts
- Prezel/app/src/main/AndroidManifest.xml
- Prezel/core/domain/.gitignore
- Prezel/feature/login/impl/build.gradle.kts
- Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidFeatureImplConventionPlugin.kt
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.kt
- Prezel/core/domain/build.gradle.kts
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt
- Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/AuthToken.kt
- Prezel/feature/profile/impl/build.gradle.kts
- Prezel/feature/splash/impl/build.gradle.kts
- Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiState.kt
- Prezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginRequest.kt
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/model/LoginUiMessage.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/model/ApiErrorResponse.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.kt
- Prezel/feature/profile/impl/src/main/res/values/strings.xml
- Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/ReissueTokenRequest.kt
- Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/model/ProfileUiMessage.kt
- Prezel/settings.gradle.kts
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.kt
- Prezel/core/data/src/main/java/com/team/prezel/core/data/ApiResponseExt.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/LoginResponse.kt
- Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/WithdrawReason.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/service/AuthService.kt
- Prezel/gradle/libs.versions.toml
- Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt
🚧 Files skipped from review as they are similar to previous changes (11)
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiIntent.kt
- Prezel/core/auth/src/main/java/com/team/prezel/core/auth/model/AuthResult.kt
- Prezel/core/auth/src/main/java/com/team/prezel/core/auth/KakaoAuthClient.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/di/DataSourceModule.kt
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.kt
- Prezel/core/data/build.gradle.kts
- Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.kt
- Prezel/core/auth/src/main/java/com/team/prezel/core/auth/AuthManager.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/TokenRefreshAuthenticator.kt
- Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.kt
* **refactor: AuthRemoteDataSource 및 AuthService 내 토큰 파라미터 제거**
* `logout` 및 `withdraw` API 호출 시 명시적으로 전달하던 `accessToken` 파라미터를 제거했습니다.
* 이는 공통 인터셉터나 `Authenticator`를 통해 인증 헤더가 자동으로 처리되도록 개선됨에 따른 변경입니다.
* **feat: 토큰 재발급 전용 HttpClient 및 서비스 분리**
* 토큰 재발급(`reissueToken`) 시 무한 루프를 방지하기 위해 `@Named("refresh")`를 사용하여 별도의 `HttpClient`와 `AuthService`를 생성하도록 `NetworkModule`을 수정했습니다.
* `AuthTokenRefresher`가 `AuthRemoteDataSource` 대신 분리된 `refresh` 전용 `AuthService`를 직접 의존하도록 변경했습니다.
* **refactor: WithdrawReason 카테고리 매핑 문자열 수정**
* `AuthRepositoryImpl`에서 `WithdrawReason` 도메인 모델을 서버 스펙에 맞는 상수 문자열로 변환하는 로직을 업데이트했습니다.
* 예: `TOO_DIFFICULT_OR_COMPLEX` -> `TOO_COMPLEX`, `OTHER` -> `ETC` 등.
* **fix: DataStoreAuthTokenStore 초기화 시 스레드 안전성 확보**
* `applicationScope`에서 초기 토큰 로드 시 `mutex`를 사용하여 동시성 이슈를 방지했습니다.
* **refactor: ApiResponseConverterFactory 에러 처리 개선**
* `parseErrorResponse`에서 `runCatching` 대신 `try-catch`를 사용하고, `CancellationException` 발생 시 코루틴 취소 흐름이 유지되도록 `rethrowIfCancellation()`을 추가했습니다.
* **chore: ProfileViewModel 로그 및 예외 처리 추가**
* 로그아웃 실패 시 Timber를 이용한 경고 로그를 추가하여 로컬 세션 정리 실패 상황을 모니터링할 수 있도록 개선했습니다.
* **refactor: 네트워크 관련 클래스 및 주입 함수를 `internal`로 변경**
* 외부 모듈에 노출될 필요가 없는 인증 및 네트워크 설정 관련 코드의 캡슐화를 강화하기 위해 `internal` 접근 제어자를 추가했습니다.
* `NetworkModule.kt`: `provideHttpClient` 함수를 `internal`로 변경
* `AuthTokenRefresher.kt`: `AuthTokenRefresher` 클래스를 `internal`로 변경 및 `@Named` 어노테이션 위치 수정 (`@param:Named`)
* `TokenRefreshAuthenticator.kt`: `TokenRefreshAuthenticator` 클래스를 `internal`로 변경
* `.kotlin/` 디렉토리 및 `.DS_Store` 파일 제외 설정 제거
* **refactor: `@Named("refresh")`를 커스텀 한정자 `@RefreshNetwork`로 대체**
* 의존성 주입 시 문자열 기반의 `@Named` 대신 타입 안정성을 보장하는 `@RefreshNetwork` 커스텀 한정자(Qualifier)를 정의하고 적용했습니다.
* `NetworkModule` 및 `AuthTokenRefresher`에서 토큰 재발급용 클라이언트 식별 방식을 변경했습니다.
* **refactor: `NetworkModule` 내 HTTP 클라이언트 및 Ktorfit 설정 로직 구조화**
* 중복되는 클라이언트 생성 로직을 `createHttpClient` 및 `createKtorfit` 공통 함수로 추출하여 가독성을 높였습니다.
* 인증이 필요한 클라이언트 설정을 `configureAuthenticatedClient` 확장 함수로 분리했습니다.
* `AuthTokenStore`의 토큰 정보를 `BearerTokens`로 변환하는 `toBearerTokens` 도우미 함수를 추가하여 코드를 간결화했습니다.
* **refactor: UserAgent 빌더 로직 개선**
* `buildUserAgent` 함수 내 문자열 결합 방식을 `buildString`을 사용하는 구조로 개선했습니다.
* **cleanup: `LoginViewModel` 내 불필요한 메서드 제거**
* 사용되지 않는 `fetchMyInfo` 메서드를 삭제하여 코드를 정리했습니다.
# Conflicts: # Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/StatusView.kt # Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt # Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.kt # Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.kt
* **refactor: `AuthTokenStore` 및 `AuthLocalDataSource` 구조 개선**
* `AuthTokenStore`에서 메모리 캐싱 및 `awaitInitialized` 로직을 제거하고 DataStore를 통한 비동기 조회(`getToken`) 방식으로 단순화했습니다.
* `core:data` 모듈에 있던 `AuthLocalDataSource`를 `core:network` 모듈로 이동하고, `AuthToken` 도메인 모델을 사용하도록 변경했습니다.
* `core:datastore` 모듈에 `core:model` 의존성을 추가했습니다.
* **refactor: Ktor Bearer Auth 및 토큰 재발급 로직 고도화**
* Ktor `BearerAuthProvider`의 `cacheTokens = true` 설정을 적용하여 라이브러리 수준의 캐싱을 활용하도록 개선했습니다.
* `AuthTokenRefresher`가 `String?` 대신 `BearerTokens?`를 반환하도록 변경하여 갱신된 Access/Refresh 토큰 쌍을 한 번에 적용합니다.
* `NetworkModule` 내에서 `AuthLocalDataSource`를 직접 주입받아 인증 클라이언트를 구성하도록 수정했습니다.
* **refactor: `AuthRepositoryImpl` 내 토큰 관리 및 HttpClient 연동**
* 토큰 저장(`saveToken`) 또는 삭제(`clearTokens`) 시 `httpClient.clearAuthTokens()`를 호출하여 Ktor 클라이언트의 인증 캐시를 동기화하도록 개선했습니다.
* 인증이 필요한 동작(로그아웃, 탈퇴) 전 토큰 존재 여부를 확인하는 로직을 `AuthToken` 모델 기반으로 수정했습니다.
* **build: 의존성 및 초기화 로직 정리**
* `ktor` 버전을 `3.3.3`에서 `3.4.3`으로 업데이트했습니다.
* `PrezelApplication`에서 불필요해진 `AuthTokenStore` 강제 초기화 로직을 제거했습니다.
* `core:data` 및 `app` 모듈의 불필요한 의존성을 정리했습니다.
* **refactor: `AuthTokenRefresher`의 토큰 재발급 방식 변경**
* 전용 `AuthService` 대신 Ktor `RefreshTokensParams`에서 제공하는 클라이언트를 사용하여 직접 API를 호출하도록 변경했습니다.
* `@RefreshNetwork` 한정자 및 관련 의존성 주입 설정을 제거하여 구조를 단순화했습니다.
* `RefreshTokensParams`의 기존 토큰 정보를 활용하도록 개선하고, 재발급 요청 시 `markAsRefreshTokenRequest()`를 호출하여 무한 루프를 방지합니다.
* `ResponseException` 발생 시 에러 응답 코드를 분석하여 세션 복구 불가능 여부를 판단하고 토큰을 삭제하는 예외 처리 로직을 강화했습니다.
* **refactor: `NetworkModule` 내 중복 클라이언트 제거 및 설정 조정**
* 토큰 재발급을 위해 별도로 유지하던 `provideRefreshHttpClient`, `provideRefreshKtorfit`, `provideRefreshAuthService` 등을 모두 삭제했습니다.
* `HttpTimeout` 설정의 하드코딩된 밀리초 값을 제거하고 기본 설정을 사용하도록 변경했습니다.
* **cleanup: 불필요한 파일 삭제**
* 더 이상 사용되지 않는 `@RefreshNetwork` 어노테이션 정의 파일(`RefreshNetwork.kt`)을 삭제했습니다.
* **feat: `AuthTokenRefreshResult` sealed interface 추가**
* 토큰 재발급 결과를 명확하게 처리하기 위해 `Success`, `Failure.Retryable`, `Failure.Unrecoverable` 상태를 정의했습니다.
* **refactor: `AuthTokenRefresher` 로직 분리 및 가시성 변경**
* 클래스 가시성을 `internal`에서 `public`으로 변경했습니다.
* Ktor `Authenticator` 내부에서만 사용되던 로직을 외부에서도 호출 가능하도록 `refreshToken` 메서드로 추출했습니다.
* `isSessionRecoveryUnrecoverable` 판단 로직에 인증 필요 코드(`U001`)를 추가하여 세션 만료 처리를 강화했습니다.
* **refactor: `AuthRepositoryImpl` 내 로그인 상태 확인 로직 개선**
* 기존 `authRemoteDataSource.reissueToken`을 직접 호출하던 방식에서 `AuthTokenRefresher`를 사용하도록 변경했습니다.
* `AuthTokenRefreshResult`에 따라 로컬 토큰 삭제 및 인증 상태(`LoginStatusResult`) 반환 로직을 일관성 있게 수정했습니다.
* **style: 불필요한 코드 정리**
* `AuthRepositoryImpl`에서 사용하지 않는 `AUTHENTICATION_REQUIRED_CODE` 상수를 제거하고 `AuthTokenRefresher` 내부 상수를 사용하도록 변경했습니다.
* **refactor: 디자인 시스템 및 UI 모듈 패키지 경로 변경**
* 디자인 시스템의 스낵바 경로를 `component.modal.snackbar`에서 `component.feedback.snackbar`로 변경했습니다.
* `UiIntent`, `UiState`, `BaseViewModel` 등 공통 UI 기반 클래스들의 패키지를 `core.ui`에서 `core.ui.base`로 이동했습니다.
* `LocalSnackbarHostState`의 패키지 경로를 `core.ui`에서 `core.ui.state`로 변경했습니다.
* **refactor: Feature 모듈 내 변경된 패키지 참조 수정**
* `splash`, `my`, `login` 기능 모듈에서 변경된 `BaseViewModel`, `UiIntent`, `UiState`, `showPrezelSnackbar` 등의 import 경로를 일괄 업데이트했습니다.
* **refactor: AuthTokenStore와 AuthLocalDataSource 통합 및 위치 변경**
* `core:network`에 위치하던 `AuthLocalDataSource` 인터페이스 및 구현체를 `core:datastore` 모듈로 이동하고 `AuthTokenStore`를 흡수 통합했습니다.
* `AuthLocalDataSource.getToken()`의 반환 타입을 단일 객체(`AuthToken?`)에서 `Flow<AuthToken?>`으로 변경하여 데이터 스트림을 지원하도록 개선했습니다.
* `saveToken` 및 `clear` 메서드의 반환 타입을 `Result<Unit>`으로 변경하여 작업 성공 여부를 캡처할 수 있도록 강화했습니다.
* **feat: JSON 직렬화 기반 토큰 저장 방식 도입**
* `AuthToken` 모델에 `@Serializable` 어노테이션을 추가하고, DataStore 저장 시 개별 키 저장 방식에서 JSON 문자열로 직렬화하여 하나의 키(`auth_token`)로 관리하도록 변경했습니다.
* `core:model` 및 `core:datastore` 모듈에 `kotlinx-serialization` 의존성을 추가했습니다.
* **refactor: AuthRepositoryImpl 및 AuthTokenRefresher 로직 수정**
* `AuthLocalDataSource`의 인터페이스 변경(`Flow` 및 `Result` 반환)에 따라 인증 체크, 로그인, 로그아웃, 탈퇴 로직을 리팩터링했습니다.
* `AuthTokenRefresher`에서 재발급된 토큰 저장 실패 시 예외 처리 및 로그 출력을 추가했습니다.
* `NetworkModule`에서 토큰 로드 시 `getToken().first()`를 사용하여 최신 토큰을 가져오도록 수정했습니다.
* **build: 의존성 주입(Hilt) 모듈 정리**
* `TokenStoreModule`을 제거하고 `DataSourceModule`을 통해 `AuthLocalDataSource`를 주입하도록 설정을 업데이트했습니다.
* **feat: AuthTokenCacheInvalidator 인터페이스 및 구현체 추가**
* Ktor의 `clearAuthTokens()`를 호출하여 메모리 내 인증 토큰 캐시를 비우는 `AuthTokenCacheInvalidator` 인터페이스를 추가했습니다.
* `KtorAuthTokenCacheInvalidator`: `HttpClient`를 주입받아 실제 무효화 로직을 수행하는 구현체입니다.
* `AuthTokenCacheModule`: Hilt를 사용하여 `AuthTokenCacheInvalidator` 의존성을 바인딩했습니다.
* **refactor: AuthLocalDataSource를 통한 토큰 캐시 무효화 일원화**
* 토큰 저장(`saveToken`) 또는 삭제(`clear`) 시, `AuthTokenCacheInvalidator`를 호출하여 네트워크 클라이언트의 토큰 상태를 동기화하도록 개선했습니다.
* 이로 인해 `AuthRepositoryImpl`에서 수동으로 `httpClient.clearAuthTokens()`를 호출하던 중복 로직을 제거했습니다.
* **refactor: AuthRepository 및 RemoteDataSource 내 불필요 로직 제거**
* `AuthRepositoryImpl`: `checkLoginStatus`에서 수행하던 수동 토큰 재발급(Reissue) 로직을 제거하고 로컬 토큰 존재 여부만 확인하도록 간소화했습니다. (Ktor `Auth` 플러그인에 역할 위임)
* `AuthRemoteDataSource`: 사용하지 않는 `reissueToken` 메서드 및 관련 DTO 의존성을 제거했습니다.
* **style: NetworkModule 설정 변경**
* `NetworkModule`의 Json 설정에서 불필요한 `prettyPrint = false` 옵션을 제거했습니다.
* **refactor: `AuthLocalDataSource` 내 토큰 저장 및 캐시 무효화 로직 개선**
* `saveToken` 메서드에 `invalidateCache` 파라미터를 추가하여 토큰 저장 시 캐시 무효화 여부를 선택할 수 있도록 수정했습니다.
* `saveToken` 및 `clear` 성공 시에만 캐시 무효화 로직(`invalidateAuthTokenCaches`)이 실행되도록 변경했습니다.
* `runSuspendCatching` 내 예외 처리 순서를 조정하여 `CancellationException`이 올바르게 전파되도록 수정하고, 캐시 무효화 실패 시 Timber 로그를 남기도록 개선했습니다.
* **refactor: `AuthTokenRefresher` 토큰 재발급 로직 최적화**
* 불필요한 `reissueToken` 메서드를 삭제했습니다.
* 토큰 재발급 성공 후 `saveToken` 호출 시 `invalidateCache = false` 옵션을 적용하여, 재발급 과정 중 불필요한 캐시 무효화가 발생하지 않도록 개선했습니다.
* **remove: `AuthService` 내 미사용 엔드포인트 제거**
* `AuthService` 인터페이스에서 더 이상 사용하지 않는 `reissueToken` API 정의와 관련 DTO(`ReissueTokenRequest`) 참조를 삭제했습니다.
* **build: `core:datastore` 모듈 의존성 추가**
* 로깅 처리를 위해 `libs.timber` 의존성을 추가했습니다.
* **feat: 인증 세션 만료 알림 시스템 구현**
* 인증 만료 이벤트를 정의하는 `AuthSessionEvent` sealed interface 추가
* 이벤트를 발행하고 구독하기 위한 `AuthSessionEventPublisher`, `AuthSessionEventStream` 인터페이스 추가
* `DefaultAuthSessionEventBus`를 통해 앱 전역에서 세션 만료를 감지하고 처리할 수 있는 구조 구현
* `MainActivity` 및 `PrezelApp`에서 세션 만료 시 로그인 화면으로 전환하고 사용자 정보를 정리하도록 연동
* **refactor: `AuthTokenStore` 도입 및 데이터 레이어 추상화**
* `core:network` 모듈에 `AuthTokenStore` 인터페이스를 정의하여 `core:datastore`에 대한 직접적인 의존성 제거
* `core:data` 모듈에서 `AuthTokenStore`를 구현(`DataAuthTokenStore`)하여 `AuthLocalDataSource`와 네트워크 레이어 연결
* 불필요해진 `AuthTokenCacheInvalidator` 관련 로직 및 모듈 제거
* **refactor: `AuthTokenRefresher` 및 네트워크 보안 로직 강화**
* `AuthTokenRefresher`가 `AuthTokenStore`를 사용하도록 수정
* 토큰 재발급 실패(Unrecoverable) 시 `AuthSessionExpiredNotifier`를 호출하여 세션 만료 이벤트를 전파하도록 개선
* Ktor `HttpClient` 설정에서 `cacheTokens` 옵션을 `false`로 변경하여 토큰 관리 신뢰성 향상
* **refactor: DataStore 설정 및 모듈 분리**
* `DataStoreModule`을 통해 `DataStore<Preferences>` 주입 설정을 별도로 분리
* `AuthLocalDataSourceImpl`에서 직접 수행하던 DataStore 빌드 로직을 삭제하고 주입받은 인스턴스를 사용하도록 변경
* **build: 모듈 간 의존성 정렬**
* `core:network`에서 `core:datastore` 의존성 제거
* `app` 모듈에 `core:domain` 의존성 추가 등 레이어 설계에 따른 의존성 최적화
* **refactor: `AuthSessionEventBus` 구현 및 이벤트 처리 로직 개선**
* `AuthSessionEventStream`의 `events` 타입을 `SharedFlow`에서 일반 `Flow`로 변경하고, 이벤트를 초기화할 수 있는 `clearSessionExpiredEvent()` 메서드를 추가했습니다.
* `DefaultAuthSessionEventBus` 구현체를 `MutableSharedFlow`에서 `MutableStateFlow` 기반으로 변경하여 상태 관리 효율성을 높였습니다.
* `PrezelApp`에서 세션 만료 이벤트 처리 후 `clearSessionExpiredEvent()`를 호출하여 중복 처리를 방지하도록 수정했습니다.
* **feat: `AuthDataModule` 분리 및 의존성 주입 설정**
* 기존 `DataModule`에 포함되어 있던 인증 관련 의존성(TokenStore, Notifier, EventBus)을 신규 생성한 `AuthDataModule`로 분리하여 모듈성을 강화했습니다.
* **refactor: `AuthTokenRefresher` 예외 처리 로직 강화**
* 토큰 재발급 실패 시 토큰 삭제(`clear`) 성공 여부에 따라 세션 만료 알림을 보내도록 흐름을 개선했습니다.
* 삭제 실패 시 에러 로그를 출력하고 세션 알림을 건너뛰도록 예외 처리를 구체화했습니다.
…Monitor`)
* **refactor: 인증 세션 모니터링 인터페이스 및 구현체 변경**
* 기존의 `AuthSessionEventStream` 및 `DefaultAuthSessionEventBus`를 삭제하고, 보다 명확한 역할을 수행하는 `AuthSessionMonitor`와 `DefaultAuthSessionMonitor`를 도입했습니다.
* `clearSessionExpiredEvent()` 메서드명을 `acknowledgeSessionEvent()`로 변경하여 이벤트 확인 처리에 대한 의미를 강화했습니다.
* `AuthSessionEventPublisher` 인터페이스를 별도 파일로 분리했습니다.
* **refactor: 앱 수준의 세션 관리 구조 개선**
* `PrezelAppState`에서 `AuthSessionMonitor`를 직접 보유하도록 수정하여 상태 관리의 응집도를 높였습니다.
* `MainActivity`에서 `AuthSessionMonitor`를 주입받아 `PrezelAppState` 생성 시 전달하도록 변경했습니다.
* `PrezelApp` UI 컴포넌트에서 `appState`를 통해 세션 이벤트를 구독하고, 세션 만료 시 로그인 화면 이동 및 이벤트 확인 로직을 수행하도록 리팩터링했습니다.
* **build: 의존성 주입(Hilt) 설정 업데이트**
* `AuthDataModule`에서 새로운 인터페이스인 `AuthSessionMonitor`와 그 구현체인 `DefaultAuthSessionMonitor`를 바인딩하도록 수정했습니다.
* test: `AuthTokenRefresher` 클래스에 대한 단위 테스트 추가 Ktor `MockEngine`을 사용하여 토큰 재발급 및 세션 만료 로직의 시나리오별 동작을 검증하는 테스트 코드를 작성했습니다. * **토큰 재발급 성공**: 서버로부터 새 토큰을 받았을 때 `AuthTokenStore`에 저장하고 `BearerTokens`를 반환하는지 확인 * **토큰 재발급 실패(401 Unauthorized)**: Refresh Token 만료 시 로컬 토큰을 삭제하고 `AuthSessionExpiredNotifier`를 통해 세션 만료 알림이 수행되는지 확인 * `FakeAuthTokenStore`, `FakeAuthSessionExpiredNotifier` 등 테스트용 더블 구현 * build: 테스트를 위한 Ktor Mock 엔진 의존성 추가 * `libs.versions.toml`에 `ktor-client-mock` 라이브러리 추가 * `core:network` 모듈의 `testImplementation`에 해당 의존성 적용
|
@coderabbitai review. |
|
🔴 Critical1.
|
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (10)
Prezel/app/src/main/java/com/team/prezel/ui/PrezelApp.kt (1)
45-55: StateFlow 기반 세션 이벤트 잠재적 재방출 위험 검토.
authSessionMonitor.sessionEvents는MutableStateFlow<AuthSessionEvent?>+filterNotNull()기반(DefaultAuthSessionMonitor.kt)이므로, 새 구독자는 마지막 값을 즉시 받습니다.LaunchedEffect의 키에navigator가 포함되어 있고,navigator는appState.navigationState변경 시 재생성됩니다. 현재 코드에서replaceRoot(LoginNavKey)가NavigationState를 수정한 후acknowledgeSessionEvent()가 호출되므로, 만약 상태 변경이 즉시 recomposition을 트리거하고 LaunchedEffect가 acknowledge 전에 취소·재시작되면, StateFlow에 여전히 남아있는Expired이벤트가 새 수집자에게 재전달될 수 있습니다.실무에서는 LaunchedEffect 블록 내의 모든 연산이 동기적으로 순차 실행되므로 실제 발생 확률은 낮지만, 안전하게 하려면 acknowledge를 먼저 실행하기를 권장합니다.
♻️ 제안 수정
AuthSessionEvent.Expired -> { + authSessionMonitor.acknowledgeSessionEvent() onSessionExpired() navigator.replaceRoot(LoginNavKey) - authSessionMonitor.acknowledgeSessionEvent() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/app/src/main/java/com/team/prezel/ui/PrezelApp.kt` around lines 45 - 55, The LaunchedEffect collection may re-emit the last StateFlow value if the effect restarts, so change the operation order inside the authSessionMonitor.sessionEvents collector (the LaunchedEffect block) to call authSessionMonitor.acknowledgeSessionEvent() first, then perform onSessionExpired() and navigator.replaceRoot(LoginNavKey); locate the collector in PrezelApp.kt (the LaunchedEffect that collects sessionEvents) and reorder the calls to acknowledge the event before triggering navigation or session handling to avoid re-delivery on restart.Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.kt (2)
63-88: 불필요한 중첩Column제거 고려외부
Column은fillMaxSize()외에 별다른 정렬/배치 인자가 없고, 내부Column이 동일하게fillMaxSize()+padding+verticalArrangement = Arrangement.Center로 콘텐츠를 모두 배치합니다. 외부 래퍼는 사실상 무의미하므로 단일Column으로 평탄화하는 편이 가독성과 컴포지션 비용 측면에서 좋습니다.♻️ 제안 변경
- Column( - modifier = modifier.fillMaxSize(), - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - verticalArrangement = Arrangement.Center, - ) { - Button( - onClick = onLogout, - enabled = !uiState.isLoading, - modifier = Modifier.fillMaxWidth(), - ) { - Text("로그아웃") - } - Spacer(modifier = Modifier.height(12.dp)) - Button( - onClick = onWithdraw, - enabled = !uiState.isLoading, - modifier = Modifier.fillMaxWidth(), - ) { - Text("회원탈퇴") - } - } - } + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.Center, + ) { + Button( + onClick = onLogout, + enabled = !uiState.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("로그아웃") + } + Spacer(modifier = Modifier.height(12.dp)) + Button( + onClick = onWithdraw, + enabled = !uiState.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("회원탈퇴") + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.kt` around lines 63 - 88, Remove the redundant outer Column and flatten to a single Column: the outer Column only calls modifier.fillMaxSize() so delete it and apply that modifier, padding, and verticalArrangement = Arrangement.Center to the remaining Column; ensure Buttons (onLogout, onWithdraw) still use enabled = !uiState.isLoading and modifiers (Modifier.fillMaxWidth()) and keep the Spacer between them so behavior and state usage (uiState, onLogout, onWithdraw) are unchanged.
77-85: 버튼 라벨을 문자열 리소스로 추출 권장스낵바 메시지는 이미
MyUiMessage.toTextRes()로 리소스화되어 있는데, "로그아웃"/"회원탈퇴" 라벨만 하드코딩되어 일관성이 깨지고 향후 다국어/번역 작업 시 누락되기 쉽습니다.feature_my_impl_*리소스로 추출해stringResource(...)로 사용하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.kt` around lines 77 - 85, Replace the hardcoded button labels in MyScreen.kt with string resources: add keys like feature_my_impl_logout and feature_my_impl_withdraw to the module's strings.xml, then update the Text composables inside the logout and withdrawal Buttons to use stringResource(R.string.feature_my_impl_logout) and stringResource(R.string.feature_my_impl_withdraw) (keep the existing Button/Modifier/onClick usages such as onWithdraw intact); this makes labels consistent with MyUiMessage.toTextRes() and prepares the UI for localization.Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthLocalDataSourceImpl.kt (1)
60-64: 역직렬화 실패를 조용히 삼키는 대신 진단 가능한 로그 추가 권장
runCatching { ... }.getOrNull()은 손상되거나 호환되지 않는 토큰 JSON이 저장돼 있을 때 어떤 정보도 남기지 않고null을 반환해 디버깅이 어렵습니다. Timber 등으로 경고 로그를 남기고, 손상된 항목은 차후에 클리어하는 것을 고려해 주세요.♻️ 제안 변경
private fun Preferences.toAuthToken(): AuthToken? = this[KEY_AUTH_TOKEN] ?.let { tokenJson -> - runCatching { json.decodeFromString<AuthToken>(tokenJson) }.getOrNull() + runCatching { json.decodeFromString<AuthToken>(tokenJson) } + .onFailure { Timber.w(it, "AuthToken 역직렬화 실패") } + .getOrNull() }?.takeIf { token -> token.accessToken.isNotBlank() && token.refreshToken.isNotBlank() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthLocalDataSourceImpl.kt` around lines 60 - 64, The Preferences.toAuthToken() extension silently swallows JSON deserialization failures via runCatching { json.decodeFromString<AuthToken>(...) }.getOrNull(), which makes debugging impossible; update this function to catch the exception, log a warning with the exception and the offending tokenJson (use Timber or your project logger) and then remove or clear the corrupt entry under KEY_AUTH_TOKEN from Preferences so future reads don't repeatedly fail; keep the same return behavior (null on failure) but ensure you log the error and clear KEY_AUTH_TOKEN when deserialization throws inside Preferences.toAuthToken().Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyViewModel.kt (1)
32-33:MutableSharedFlow버퍼 설정 검토 권장기본값(
replay=0,extraBufferCapacity=0,BufferOverflow.SUSPEND)이라 컬렉터가 일시적으로 분리된 시점에emit이 서스펜드될 수 있습니다. 화면 상태가 스냅샷되거나 회전 중에NavigateToLogin이 발행되면 사용자가 로그인 화면으로 이동하지 못하고 멈춰 있는 듯한 현상이 발생할 가능성이 있습니다.extraBufferCapacity = 1또는BufferOverflow.DROP_OLDEST적용을 검토해 주세요.♻️ 제안 변경
- private val _uiEffect = MutableSharedFlow<MyUiEffect>() + private val _uiEffect = MutableSharedFlow<MyUiEffect>( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyViewModel.kt` around lines 32 - 33, _uiEffect가 MutableSharedFlow 기본 버퍼 설정(replay=0, extraBufferCapacity=0, BufferOverflow.SUSPEND)으로 선언되어 있어 emit 시 컬렉터가 일시적으로 분리되면 발행이 서스펜드될 수 있으니, MyViewModel의 _uiEffect를 선언할 때 MutableSharedFlow<MyUiEffect>(extraBufferCapacity = 1) 또는 MutableSharedFlow<MyUiEffect>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)처럼 버퍼 확장을 적용해 NavigateToLogin 등 UI 이펙트가 손실되거나 서스펜드되지 않도록 변경하세요; 해당 변경은 _uiEffect와 uiEffect( _uiEffect.asSharedFlow() ) 선언부를 수정하면 됩니다.Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt (1)
88-90:refreshTokens블록에서 엘비스 + return 패턴이 불필요합니다.
refreshBearerTokens(this)는 이미BearerTokens?를 반환하므로?: return@refreshTokens null은 동일한 값을 다시 반환할 뿐입니다.♻️ 제안 수정
refreshTokens { - authTokenRefresher.refreshBearerTokens(this) ?: return@refreshTokens null + authTokenRefresher.refreshBearerTokens(this) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt` around lines 88 - 90, The refreshTokens block is returning the same nullable value via an unnecessary Elvis + return pattern; replace the current block body so it directly returns the result of authTokenRefresher.refreshBearerTokens(this) (which already returns BearerTokens?), removing the "?: return@refreshTokens null" and simply using the call's value as the block result.Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt (2)
17-21:login()체이닝이 불필요하게 복잡합니다.
.let { ... }.also(::logTokenResponse)체인은 단순한 지역 변수로도 충분히 표현 가능합니다.♻️ 제안 수정
- override suspend fun login(idToken: String): ApiResponse<LoginResponse> = - LoginRequest(idToken = idToken) - .let { request -> - authService.login(request = request) - }.also(::logTokenResponse) + override suspend fun login(idToken: String): ApiResponse<LoginResponse> = + authService + .login(request = LoginRequest(idToken = idToken)) + .also(::logTokenResponse)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt` around lines 17 - 21, The login() implementation uses an unnecessary .let { ... }.also(::logTokenResponse) chain; simplify by creating a local LoginRequest and calling authService.login(request) into a val (e.g., response), call logTokenResponse(response) explicitly, and return the response; update the function body around LoginRequest, authService.login, and logTokenResponse to use a straightforward local variable instead of the let/also chain.
38-46: 로그 메시지의 언어 통일을 권장합니다.성공 로그는 한국어("서버 인증 응답에 성공했습니다."), 실패 로그는 영문("Server login failed: ...")으로 혼재되어 있어 일관성이 떨어집니다. 프로젝트 내 다른 Timber 로그(
AuthTokenRefresher의 "토큰 재발급에 실패했습니다." 등)와 통일하는 편이 가독성에 유리합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt` around lines 38 - 46, The log messages in the ApiResponse handling block are mixed-language; change the English error messages to match the project's Korean convention used elsewhere. Update the Timber calls in the ApiResponse.Success, ApiResponse.Failure.HttpError and ApiResponse.Failure.NetworkError branches (the Timber.tag("AuthToken").d(...) and Timber.tag("AuthToken").e(...)) so the success and both failure logs use consistent Korean phrasing (e.g., make the error messages mirror other logs like "토큰 재발급에 실패했습니다." style), keeping the throwable passed to Timber.tag(...).e(...) unchanged.Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.kt (2)
90-104: 복구 불가능한 401 외에도 일시 장애를 처리할 retry 정책 검토.
isSessionRecoveryUnrecoverable()이 false면 모두Retryable로 분류되지만, Ktor Auth 플러그인은refreshTokens가 null을 반환한 시점부터 재시도하지 않고 원요청을 401 그대로 호출자에게 전달합니다. 즉 5xx/네트워크 일시 장애에서 발급에 실패해도 사용자에겐 "로그인 만료"처럼 보일 수 있습니다.상위 계층에서 일시 실패와 영구 실패를 구분해 사용자 메시지/세션 만료 알림을 다르게 처리하고 있다면 문제 없지만, 그렇지 않다면 호출 측에서
Retryable결과에 한해 재시도/백오프를 적용하거나 상위 UI에서 별도 메시지를 노출하는 정책을 검토해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.kt` around lines 90 - 104, The code currently maps all non-unrecoverable errors to AuthTokenRefreshResult.Failure.Retryable, but Ktor stops retrying once refreshTokens returns null so transient 5xx/network errors may be treated as session expiry; update AuthTokenRefresher to perform bounded retry-with-backoff for transient failures before returning a failure: inside the refresh logic (where isSessionRecoveryUnrecoverable() is checked and where you currently return AuthTokenRefreshResult.Failure.Retryable), detect transient errors (network/5xx) and attempt a few retries with exponential/backoff, only returning Retryable after exhausting retries (or return Unrecoverable immediately for isSessionRecoveryUnrecoverable() cases), and keep existing cleanup calls (authTokenStore.clear() and authSessionExpiredNotifier.notifySessionExpired()) for true unrecoverable paths so callers/Ktor see the correct outcome.
80-83:onFailure내부의 비지역 return은 정상 케이스의 성공 로그를 우회시킵니다.
saveToken실패 시return AuthTokenRefreshResult.Failure.Retryable(throwable)로 함수에서 즉시 반환됩니다.onFailure가 인라인이므로 동작은 정상이나, 가독성을 위해 명시적 분기로 작성하는 편이 의도가 명확합니다.♻️ 제안 수정
- authTokenStore - .saveToken(token) - .onFailure { throwable -> - Timber.e(throwable, "재발급된 토큰 저장에 실패했습니다.") - return AuthTokenRefreshResult.Failure.Retryable(throwable) - } + authTokenStore.saveToken(token).getOrElse { throwable -> + Timber.e(throwable, "재발급된 토큰 저장에 실패했습니다.") + return@requestTokenReissue AuthTokenRefreshResult.Failure.Retryable(throwable) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.kt` around lines 80 - 83, The non-local return inside the onFailure lambda (in AuthTokenRefresher.kt around saveToken) bypasses the normal success logging; change the flow to use an explicit local result variable instead of returning from the lambda: create a mutable var result: AuthTokenRefreshResult? = null, replace the onFailure { throwable -> return AuthTokenRefreshResult.Failure.Retryable(throwable) } with onFailure { throwable -> result = AuthTokenRefreshResult.Failure.Retryable(throwable); Timber.e(throwable, "...") }, and after the runCatching / saveToken block check if result != null then return result!! else proceed to log the success; this preserves semantics while making control flow explicit and readable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Prezel/app/src/main/java/com/team/prezel/MainActivity.kt`:
- Line 47: The onSessionExpired callback currently points to
authManager::clearCurrentProvider which only clears the in-memory
currentProvider field in AuthManager and does not perform provider-specific SDK
logout (leaving idToken/session in Kakao/other SDKs); add a new helper in
AuthManager (e.g., logoutCurrentProvider or performProviderLogout) that performs
the SDK-level sign-out for the active provider (call provider-specific logout
APIs such as UserApiClient.instance.logout for Kakao) and then clears
currentProvider, and change the MainActivity binding (onSessionExpired) to call
that new helper instead of clearCurrentProvider.
In
`@Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt`:
- Around line 24-40: The logout() in AuthRepositoryImpl preserves local tokens
on non-authentication remote failures; update logout() so that when
authRemoteDataSource.logout() returns any failure (ApiResponse.Failure.HttpError
or ApiResponse.Failure.NetworkError) you perform a best-effort local token clear
(call clearTokens() or clearTokensAndAuthenticationRequired() as appropriate)
and then return a Failure result that also indicates the remote sync failed
(e.g., return clearTokens().toAuthActionSuccessResult() or return
AuthActionResult.Failure(response.throwable) after clearing tokens), keeping the
existing special-case for AUTHENTICATION_REQUIRED_CODE(U001) but ensuring local
tokens are cleared on other HTTP/Network errors; locate this logic around the
logout(), authRemoteDataSource.logout(), clearTokens(),
clearTokensAndAuthenticationRequired(), and ApiResponse handling to implement
the change.
- Around line 42-47: In login, handle the case where saveTokens(response.data)
fails by ensuring local state is cleaned and a clear domain-level failure is
returned: call clearTokens() immediately when saveTokens returns a failure and
then return Result.failure(Exception("Failed to persist tokens")) (or wrap
response.throwable with that message) instead of leaving partial token state;
update the login implementation (referencing login, saveTokens, clearTokens,
ApiResponse.Success) to perform this cleanup-and-return behavior.
- Around line 118-122: WithdrawRequest.reasonText should be nullable and the
helper and remote call should propagate nulls: change WithdrawRequest.reasonText
from String to String?, change the extension/private converter private fun
WithdrawReason.toReasonText() to return String? (return null for non-Other cases
instead of ""), and update AuthRemoteDataSourceImpl.withdraw(reasonText: String)
to accept reasonText: String?; then update all call sites in AuthRepositoryImpl
and AuthRemoteDataSourceImpl to pass/handle nullable reasonText accordingly
(notably replace places calling WithdrawReason.toReasonText() and passing into
withdraw()).
In
`@Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthLocalDataSourceImpl.kt`:
- Around line 36-48: The current saveToken and clear implementations store the
AuthToken JSON plaintext under KEY_AUTH_TOKEN in DataStore and are vulnerable to
device/cloud/adb backups; update saveToken/clear to persist an encrypted blob
instead: integrate an EncryptedDataStore or encrypt/decrypt the token using
Tink/AEAD with Android Keystore before writing/after reading from dataStore (use
the same KEY_AUTH_TOKEN key for the encrypted bytes), and ensure any DataStore
file path is excluded from backups via backup_rules.xml or set
android:allowBackup="false" if exclusion is not possible; also update related
read methods to decrypt the stored value and handle decryption errors
gracefully.
In
`@Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/di/DataStoreModule.kt`:
- Around line 19-31: The current providePreferencesDataStore creates a plain
Preferences DataStore (PREFERENCES_NAME) and AuthLocalDataSourceImpl writes
plain JSON to KEY_AUTH_TOKEN, exposing tokens; fix by replacing the plain
Preferences storage with an encrypted solution: either implement an encrypted
DataStore (e.g., Proto DataStore with a custom Serializer that uses AndroidX
Security MasterKey/Tink to encrypt/decrypt payloads) or store tokens separately
in EncryptedSharedPreferences backed by MasterKey, or at minimum encrypt/decrypt
the token inside AuthLocalDataSourceImpl before writing/after reading using a
Keystore-backed symmetric key; also ensure app backup is disabled
(android:allowBackup="false" and appropriate dataExtractionRules) so tokens are
not included in backups.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.kt`:
- Around line 34-60: Within refreshBearerTokens suspend function, after
acquiring mutex.withLock and before calling requestTokenReissue, re-load the
current stored token from authTokenStore (e.g.,
authTokenStore.getToken().first()) and compare its refreshToken (and/or
accessToken) with params.oldTokens?.refreshToken; if they differ (meaning
another coroutine already rotated and saved new tokens), return the current
stored tokens (or null if none) instead of attempting reissue. Update the logic
around refreshToken selection and the requestTokenReissue call in
refreshBearerTokens to use this post-lock re-check to skip duplicate reissuance
and avoid clearing a just-saved valid session.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt`:
- Around line 99-106: The current implementation in
AuthTokenStore.toBearerTokens() causes a fresh DataStore subscription on every
request (via authTokenStore.getToken().first() ->
AuthLocalDataSourceImpl.getToken() -> dataStore.data), which can cause disk I/O
on hot paths; either enable cacheTokens = true where loadTokens is invoked or
add a lightweight in-memory cache inside AuthTokenStore (e.g., cache fields with
atomic access and an invalidation/update hook when tokens are written) so
loadTokens/toBearerTokens reads the cached tokens first and only falls back to
DataStore when the cache is empty/stale; update DataAuthTokenStore/Token write
paths to refresh/invalidate the cache accordingly.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/service/AuthService.kt`:
- Around line 20-23: The DELETE endpoint in AuthService (suspend fun
withdraw(`@Body` request: WithdrawRequest): ApiResponse<String>) may be
incompatible with some servers/proxies because DELETE with a body can be ignored
or rejected; confirm with the backend team that the server parses a JSON body
for DELETE auth/withdraw correctly, and if not, change the client/server
contract to use POST auth/withdraw or move WithdrawRequest fields into query
parameters/path body-less form; update the AuthService method signature and any
callers accordingly (e.g., replace `@DELETE` with `@POST` or use `@Query/`@Path) to
match the agreed-upon backend behavior.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyViewModel.kt`:
- Around line 59-62: The code currently passes a hardcoded
WithdrawReason.Other("임시 테스트 탈퇴") into withdrawUseCase from MyViewModel (risking
polluted production data); change it to accept an injected/selected reason
instead: update the ViewModel API to withdraw(reason: WithdrawReason) and call
withdrawUseCase(reason = reason) so callers supply the real reason, or if UI is
not ready send a sentinel like WithdrawReason.Unspecified (and add a TODO
comment) instead of the temporary string; ensure references to
WithdrawReason.Other and withdrawUseCase are updated accordingly so no hardcoded
test string is sent to the server.
- Around line 80-87: The handling of AuthActionResult.Success should distinguish
between logout and withdrawal flows: update handleAuthActionResult to accept a
discriminator (e.g., an AuthFlow or boolean) or detect the originating action,
and when the result corresponds to a withdrawal call invoke
authManager.clearCurrentProvider() instead of authManager.logout(), while
keeping the existing onFailure logging behavior for logout flows; ensure
AuthActionResult.Success handling still emits MyUiEffect.NavigateToLogin and
preserve current failure logging via Timber.w for logout attempts.
---
Nitpick comments:
In `@Prezel/app/src/main/java/com/team/prezel/ui/PrezelApp.kt`:
- Around line 45-55: The LaunchedEffect collection may re-emit the last
StateFlow value if the effect restarts, so change the operation order inside the
authSessionMonitor.sessionEvents collector (the LaunchedEffect block) to call
authSessionMonitor.acknowledgeSessionEvent() first, then perform
onSessionExpired() and navigator.replaceRoot(LoginNavKey); locate the collector
in PrezelApp.kt (the LaunchedEffect that collects sessionEvents) and reorder the
calls to acknowledge the event before triggering navigation or session handling
to avoid re-delivery on restart.
In
`@Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthLocalDataSourceImpl.kt`:
- Around line 60-64: The Preferences.toAuthToken() extension silently swallows
JSON deserialization failures via runCatching {
json.decodeFromString<AuthToken>(...) }.getOrNull(), which makes debugging
impossible; update this function to catch the exception, log a warning with the
exception and the offending tokenJson (use Timber or your project logger) and
then remove or clear the corrupt entry under KEY_AUTH_TOKEN from Preferences so
future reads don't repeatedly fail; keep the same return behavior (null on
failure) but ensure you log the error and clear KEY_AUTH_TOKEN when
deserialization throws inside Preferences.toAuthToken().
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.kt`:
- Around line 90-104: The code currently maps all non-unrecoverable errors to
AuthTokenRefreshResult.Failure.Retryable, but Ktor stops retrying once
refreshTokens returns null so transient 5xx/network errors may be treated as
session expiry; update AuthTokenRefresher to perform bounded retry-with-backoff
for transient failures before returning a failure: inside the refresh logic
(where isSessionRecoveryUnrecoverable() is checked and where you currently
return AuthTokenRefreshResult.Failure.Retryable), detect transient errors
(network/5xx) and attempt a few retries with exponential/backoff, only returning
Retryable after exhausting retries (or return Unrecoverable immediately for
isSessionRecoveryUnrecoverable() cases), and keep existing cleanup calls
(authTokenStore.clear() and authSessionExpiredNotifier.notifySessionExpired())
for true unrecoverable paths so callers/Ktor see the correct outcome.
- Around line 80-83: The non-local return inside the onFailure lambda (in
AuthTokenRefresher.kt around saveToken) bypasses the normal success logging;
change the flow to use an explicit local result variable instead of returning
from the lambda: create a mutable var result: AuthTokenRefreshResult? = null,
replace the onFailure { throwable -> return
AuthTokenRefreshResult.Failure.Retryable(throwable) } with onFailure { throwable
-> result = AuthTokenRefreshResult.Failure.Retryable(throwable);
Timber.e(throwable, "...") }, and after the runCatching / saveToken block check
if result != null then return result!! else proceed to log the success; this
preserves semantics while making control flow explicit and readable.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt`:
- Around line 17-21: The login() implementation uses an unnecessary .let { ...
}.also(::logTokenResponse) chain; simplify by creating a local LoginRequest and
calling authService.login(request) into a val (e.g., response), call
logTokenResponse(response) explicitly, and return the response; update the
function body around LoginRequest, authService.login, and logTokenResponse to
use a straightforward local variable instead of the let/also chain.
- Around line 38-46: The log messages in the ApiResponse handling block are
mixed-language; change the English error messages to match the project's Korean
convention used elsewhere. Update the Timber calls in the ApiResponse.Success,
ApiResponse.Failure.HttpError and ApiResponse.Failure.NetworkError branches (the
Timber.tag("AuthToken").d(...) and Timber.tag("AuthToken").e(...)) so the
success and both failure logs use consistent Korean phrasing (e.g., make the
error messages mirror other logs like "토큰 재발급에 실패했습니다." style), keeping the
throwable passed to Timber.tag(...).e(...) unchanged.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.kt`:
- Around line 88-90: The refreshTokens block is returning the same nullable
value via an unnecessary Elvis + return pattern; replace the current block body
so it directly returns the result of
authTokenRefresher.refreshBearerTokens(this) (which already returns
BearerTokens?), removing the "?: return@refreshTokens null" and simply using the
call's value as the block result.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.kt`:
- Around line 63-88: Remove the redundant outer Column and flatten to a single
Column: the outer Column only calls modifier.fillMaxSize() so delete it and
apply that modifier, padding, and verticalArrangement = Arrangement.Center to
the remaining Column; ensure Buttons (onLogout, onWithdraw) still use enabled =
!uiState.isLoading and modifiers (Modifier.fillMaxWidth()) and keep the Spacer
between them so behavior and state usage (uiState, onLogout, onWithdraw) are
unchanged.
- Around line 77-85: Replace the hardcoded button labels in MyScreen.kt with
string resources: add keys like feature_my_impl_logout and
feature_my_impl_withdraw to the module's strings.xml, then update the Text
composables inside the logout and withdrawal Buttons to use
stringResource(R.string.feature_my_impl_logout) and
stringResource(R.string.feature_my_impl_withdraw) (keep the existing
Button/Modifier/onClick usages such as onWithdraw intact); this makes labels
consistent with MyUiMessage.toTextRes() and prepares the UI for localization.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyViewModel.kt`:
- Around line 32-33: _uiEffect가 MutableSharedFlow 기본 버퍼 설정(replay=0,
extraBufferCapacity=0, BufferOverflow.SUSPEND)으로 선언되어 있어 emit 시 컬렉터가 일시적으로 분리되면
발행이 서스펜드될 수 있으니, MyViewModel의 _uiEffect를 선언할 때
MutableSharedFlow<MyUiEffect>(extraBufferCapacity = 1) 또는
MutableSharedFlow<MyUiEffect>(extraBufferCapacity = 1, onBufferOverflow =
BufferOverflow.DROP_OLDEST)처럼 버퍼 확장을 적용해 NavigateToLogin 등 UI 이펙트가 손실되거나 서스펜드되지
않도록 변경하세요; 해당 변경은 _uiEffect와 uiEffect( _uiEffect.asSharedFlow() ) 선언부를 수정하면 됩니다.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: cddec400-8d25-42c7-90f1-79e9f791b319
📒 Files selected for processing (56)
Prezel/app/build.gradle.ktsPrezel/app/src/main/AndroidManifest.xmlPrezel/app/src/main/java/com/team/prezel/MainActivity.ktPrezel/app/src/main/java/com/team/prezel/ui/PrezelApp.ktPrezel/app/src/main/java/com/team/prezel/ui/PrezelAppState.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/auth/DataAuthSessionExpiredNotifier.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/auth/DataAuthTokenStore.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/auth/DefaultAuthSessionMonitor.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/di/AuthDataModule.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.ktPrezel/core/datastore/build.gradle.ktsPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthLocalDataSource.ktPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthLocalDataSourceImpl.ktPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/di/DataSourceModule.ktPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/di/DataStoreModule.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/repository/auth/AuthRepository.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/session/AuthSessionEvent.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/session/AuthSessionEventPublisher.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/session/AuthSessionMonitor.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/auth/CheckLoginStatusUseCase.ktPrezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/auth/LoginUseCase.ktPrezel/core/model/build.gradle.ktsPrezel/core/model/src/main/java/com/team/prezel/core/model/auth/AuthToken.ktPrezel/core/network/build.gradle.ktsPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthSessionExpiredNotifier.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefreshResult.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenRefresher.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthTokenStore.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/service/AuthService.ktPrezel/core/network/src/test/java/com/team/prezel/core/network/auth/AuthTokenRefresherTest.ktPrezel/feature/login/impl/build.gradle.ktsPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiIntent.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.ktPrezel/feature/my/impl/build.gradle.ktsPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyViewModel.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/contract/MyUiEffect.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/contract/MyUiIntent.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/contract/MyUiState.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/model/MyUiMessage.ktPrezel/feature/my/impl/src/main/res/values/strings.xmlPrezel/feature/profile/impl/build.gradle.ktsPrezel/feature/profile/impl/src/main/res/values/strings.xmlPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashScreen.ktPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.ktPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/contract/SplashUiEffect.ktPrezel/gradle/libs.versions.tomlPrezel/settings.gradle.kts
✅ Files skipped from review due to trivial changes (18)
- Prezel/app/build.gradle.kts
- Prezel/feature/profile/impl/src/main/res/values/strings.xml
- Prezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthSessionExpiredNotifier.kt
- Prezel/app/src/main/AndroidManifest.xml
- Prezel/feature/login/impl/build.gradle.kts
- Prezel/core/domain/src/main/java/com/team/prezel/core/domain/session/AuthSessionEvent.kt
- Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/AuthToken.kt
- Prezel/feature/my/impl/src/main/res/values/strings.xml
- Prezel/settings.gradle.kts
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.kt
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt
- Prezel/core/model/build.gradle.kts
- Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/model/MyUiMessage.kt
- Prezel/core/domain/src/main/java/com/team/prezel/core/domain/repository/auth/AuthRepository.kt
- Prezel/feature/my/impl/build.gradle.kts
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.kt
- Prezel/core/datastore/build.gradle.kts
- Prezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthLocalDataSource.kt
🚧 Files skipped from review as they are similar to previous changes (8)
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiIntent.kt
- Prezel/feature/profile/impl/build.gradle.kts
- Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/contract/SplashUiEffect.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.kt
- Prezel/core/domain/src/main/java/com/team/prezel/core/domain/usecase/auth/CheckLoginStatusUseCase.kt
- Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashScreen.kt
- Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.kt
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.kt
* **feat: URL 기반에서 속성(Attribute) 기반 인증 정책으로 전환**
* 특정 URL 패턴을 하드코딩하여 인증을 제외하던 `AuthPathPolicy`를 삭제했습니다.
* `AuthRequestAttributes`를 추가하여 Ktor `AttributeKey`를 통해 요청별로 인증 제외 여부(`SkipAuthKey`)를 설정할 수 있도록 개선했습니다.
* `AuthService`의 `login` API에 `@Tag`를 사용하여 인증 제외 속성을 적용했습니다.
* **refactor: 로그아웃 및 탈퇴 시 토큰 관리 로직 강화**
* `AuthRepositoryImpl`: 로그아웃 요청 시 HTTP 에러나 네트워크 에러가 발생하더라도 로컬 토큰을 강제로 삭제하도록 변경했습니다.
* `AuthManager`: 세션 만료 시 각 소셜 로그인 제공자(SDK)의 로그아웃 로직을 일괄 처리하는 `clearAuthSession` 메서드를 추가했습니다.
* `MyViewModel`: 로그아웃 및 회원 탈퇴 성공 시 로컬 세션 정리 로직을 `onSuccess` 콜백으로 분리하여 구조를 개선했습니다.
* **refactor: 토큰 재발급(Refresh) 및 예외 처리 개선**
* `AuthTokenRefresher`: 재발급 요청 시 `SkipAuthKey` 속성을 명시적으로 추가하여 무한 루프를 방지했습니다.
* `ResponseException` 분석 시 취소 예외(`CancellationException`)에 대한 전파 로직을 정교화했습니다.
* `AuthRemoteDataSourceImpl`: 로그인 실패 로그 메시지를 한글로 변경하고 명확하게 개선했습니다.
* **test: 네트워크 테스트 코드 구조 정리**
* `AuthTokenRefresherTest` 파일 위치를 `com.team.prezel.core.network` 패키지로 이동하고 불필요한 `.gitkeep` 파일을 삭제했습니다.
* **ui: 세션 만료 처리 로직 개선**
* `MainActivity` 및 `PrezelApp`: 세션 만료 이벤트 수신 시 `AuthManager.clearAuthSession`을 비동기로 호출하여 외부 SDK 세션까지 안전하게 정리하도록 수정했습니다.
* **feat: `AuthLocalDataSource` 내 메모리 캐시 계층 도입**
* `DataStore` 접근 횟수를 줄이기 위해 `MutableStateFlow`와 `Mutex`를 이용한 토큰 캐싱 로직을 추가했습니다.
* 초기 1회 로드 이후에는 캐시된 데이터를 사용하며, 저장/삭제 시 캐시가 동기화되도록 개선했습니다.
* **refactor: `AuthTokenRefresher` 중복 재발급 방지 로직 개선**
* 토큰 재발급 요청 전, 현재 저장된 토큰이 이미 갱신되었는지 확인하는 로직을 추가했습니다.
* 요청 보낸 토큰과 저장된 토큰이 다를 경우, 추가 API 호출 없이 즉시 최신 토큰을 반환하도록 수정했습니다.
* 관련 시나리오에 대한 단위 테스트(`AuthTokenRefresherTest`)를 추가했습니다.
* **refactor: `AuthRepositoryImpl` 로그인 실패 처리 강화**
* `login` 성공 후 토큰 저장 과정에서 실패(`onFailure`)할 경우, 무결성을 위해 `clearTokens()`를 호출하도록 예외 처리를 추가했습니다.
* **style: `AuthRepositoryImpl` 응답 처리 가독성 개선**
* `login` 메서드 내 `when` 절의 각 브랜치를 블록 형식으로 변경하여 가독성을 높였습니다.
* **refactor: 주요 인터페이스 구현체의 클래스명 변경**
* 기존 `Default-` 접두사를 사용하던 구현체들을 `-Impl` 접미사 형태로 변경하여 명명 규칙을 통일했습니다.
* `DefaultAuthRepository` -> `AuthRepositoryImpl`
* `DefaultGlobalEventBus` -> `GlobalEventBusImpl`
* `DefaultTokenProvider` -> `TokenProviderImpl`
* **refactor: DI 모듈 내 변경된 구현체 참조 반영**
* `AuthModule`, `RepositoryModule`, `GlobalEventModule`에서 `@Binds`를 통해 주입되는 클래스명을 새로운 구현체 이름으로 업데이트했습니다.
* **refactor: GlobalEventModule을 core:common 모듈로 이동**
* `GlobalEventModule`을 `app` 모듈에서 `core:common` 모듈의 `di` 패키지로 이동했습니다.
* 패키지 경로를 `com.team.prezel`에서 `com.team.prezel.core.common.di`로 변경했습니다.
* **refactor: `MyViewModel` 생성자 및 의존성 정리**
* 현재 사용되지 않는 `LogoutUseCase` 및 `WithdrawUseCase` 의존성을 생성자에서 제거했습니다.
* 의존성 제거에 따라 불필요해진 관련 import 구문을 삭제했습니다.
|
c5cae27 이후에는 인증/토큰 기반 구조 자체를 다시 바꾸기보다는, 그 위에서 앱 전역 흐름과 모듈 구성을 정리하는 작업을 진행했습니다. 주요 내용은 아래와 같습니다.
|
* **refactor: `HttpClientFactory` 추가를 통한 클라이언트 설정 로직 캡슐화**
* `NetworkModule`에 집중되어 있던 Ktor `HttpClient` 구성 로직을 전담 클래스인 `HttpClientFactory`로 이동했습니다.
* Default Request, Content Negotiation, UserAgent, Logging, Auth 등 설정을 개별 메서드로 분리하여 가독성과 유지보수성을 높였습니다.
* `buildUserAgent` 메서드를 팩토리 내부로 이동하여 Android OS 버전, SDK 버전, 제조사 및 모델명을 포함한 정형화된 User-Agent 형식을 정의했습니다.
* **refactor: 인증 및 토큰 재발급 로직 고도화**
* `Bearer` 인증 설정 내 `refreshTokens` 로직을 개선하여 토큰 재발급 실패 시 `tokenProvider`를 초기화하고 `GlobalEventBus`를 통해 `ForceLogout` 이벤트를 전송하도록 수정했습니다.
* `AuthTokens` 및 `ReissueResponse` 모델을 Ktor의 `BearerTokens`로 변환하는 확장 함수를 추가했습니다.
* `authServiceProvider`를 `Provider<AuthService>` 형태로 주입받아 순환 참조 문제를 방지하고 필요한 시점에 `AuthService`를 호출하도록 개선했습니다.
* **refactor: `NetworkModule` 구조 단순화**
* `HttpClient` 생성 시 `HttpClientFactory.create()`를 사용하도록 변경하여 모듈 내 불필요한 설정 코드를 제거했습니다.
* `NetworkModule`에서 직접 관리하던 `networkJson` 및 로깅 설정을 팩토리 내부로 이동했습니다.
📌 작업 내용
카카오 SDK 로그인부터 서버 로그인, 토큰 저장/재발급, 인증 예외 처리까지 인증 플로우 전반을 정리했습니다.
인증 플로우 전반 정리
앱 시작 시 토큰 초기화 완료 후 로그인 상태 판정
인증 관련 네트워크/도메인 계층 추가
로그인/스플래시/프로필 화면 인증 흐름 정리
🧩 관련 이슈
📸 스크린샷
Summary by CodeRabbit
릴리즈 노트
새로운 기능
버그 수정
보안