[Fix] 액세스 토큰 만료되기 전, 앱 재실행 시 자동로그인 되도록 수정#121
[Fix] 액세스 토큰 만료되기 전, 앱 재실행 시 자동로그인 되도록 수정#121Nico1eKim merged 10 commits intoTHIP-TextHip:developfrom
Conversation
Walkthrough의존성 일부를 주석 처리하고 TokenManager를 DataStore 주입 방식으로 리팩터링했으며, Splash/인증 네비게이션을 콜백·목적지 기반으로 재구성하고 여러 응답 모델의 Gson 어노테이션을 Kotlinx Serialization(@SerialName)으로 교체했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant App as MainActivity/NavHost
participant Splash as SplashScreen
participant VM as SplashViewModel
participant TM as TokenManager
participant Nav as NavController
User->>App: 앱 실행
App->>Splash: SplashScreen 표시(onNavigateToLogin, onNavigateToHome 전달)
Splash->>VM: destination 관찰
VM->>TM: getTokenOnce()
TM-->>VM: token? (값 또는 null)
VM-->>Splash: destination 업데이트(NavigateToHome / NavigateToLogin)
alt 홈으로
Splash->>App: onNavigateToHome()
App->>Nav: navigate(Main) + popUpTo(Splash, inclusive)
else 로그인으로
Splash->>App: onNavigateToLogin()
App->>Nav: navigate(Login) + popUpTo(Splash, inclusive)
end
sequenceDiagram
autonumber
participant OkHttp as OkHttpClient
participant Intc as AuthInterceptor
participant TM as TokenManager
participant API as Server
OkHttp->>Intc: intercept(request)
alt Authorization 헤더 존재
Intc-->>OkHttp: proceed(original request)
else 헤더 없음
Intc->>TM: getTokenOnce() (runBlocking)
TM-->>Intc: token?
alt 토큰 없음
Intc->>TM: getTempTokenOnce()
TM-->>Intc: tempToken?
end
Intc-->>OkHttp: proceed(request + Authorization: Bearer tokenToSend?)
OkHttp->>API: 요청 전송
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt (1)
125-129: AuthInterceptor와 signup 요청의 Authorization 헤더 이중 주입 문제 확인현재
- AuthInterceptor(
app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt28행)에서 모든 요청에 Authorization 헤더를 추가합니다.- UserService.signup(
app/src/main/java/com/texthip/thip/data/service/UserService.kt65–68행)에@Header("Authorization")파라미터로도 별도 헤더를 주입하고 있어, signup 호출 시 동일 헤더가 중복됩니다.이로 인해 서버에서 헤더 충돌이나 예기치 않은 동작이 발생할 가능성이 있으므로 반드시 수정이 필요합니다.
수정 방안 (아래 중 하나 선택):
• AuthInterceptor에서 이미
Authorization헤더가 존재하면 추가를 건너뛰도록 조건문을 추가val newRequest = original.newBuilder().apply { if (original.header("Authorization") == null) { tokenToSend?.let { addHeader("Authorization", "Bearer $it") } } }.build()• 또는 signup 메서드의
@Header("Authorization")파라미터를 제거하고 인터셉터만 사용하도록 변경대상 위치:
- AuthInterceptor: app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt (25–31행)
- UserService.signup: app/src/main/java/com/texthip/thip/data/service/UserService.kt (65–68행)
- UserRepository.signup 호출: app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt (125–129행)
🧹 Nitpick comments (13)
app/build.gradle.kts (2)
77-77: AI 요약과 실제 의존성 상태가 다릅니다: DataStore-Preferences는 비활성화되지 않음요약에는 datastore-preferences 비활성화로 기재되어 있으나, 실제로는
implementation(libs.androidx.datastore.preferences)가 활성화되어 있습니다(주석 처리된 직접 좌표 라인은 별개). 리뷰어 혼선을 줄이기 위해 PR 설명을 보정하거나 불필요한 주석 라인을 정리해 주세요.Also applies to: 105-105
70-70: 주석 처리된 의존성 라인 정리 제안장기간 유지할 계획이 없다면 주석 라인은 제거해 변경 의도를 명확히 하는 편이 좋습니다. 필요 시 PR 설명/커밋 메시지로 사유를 남기는 것을 권장합니다.
Also applies to: 94-94, 105-105
app/src/main/java/com/texthip/thip/data/model/users/response/SignupResponse.kt (1)
8-9: 불필요한 @SerialName 제거로 보일러플레이트 축소 가능프로퍼티명과 JSON 키가 동일(accessToken, userId)하므로 @SerialName은 생략 가능합니다. 유지 관리 비용을 줄이기 위해 제거를 권장합니다.
- @SerialName("accessToken") val accessToken: String, - @SerialName("userId") val userId: Long + val accessToken: String, + val userId: Longapp/src/main/java/com/texthip/thip/data/di/DataStoreModule.kt (1)
18-24: preferencesDataStore 위임을 모듈 내부 확장 프로퍼티로 두는 패턴은 비표준 — top-level 또는 Factory 사용 권장현재 패턴도 동작은 하지만, Android 공식 가이드는 Context 확장
val을 파일 top-level로 두거나 DI에서는PreferenceDataStoreFactory를 통해 명시적으로 생성하는 방식을 권장합니다. 후자는 마이그레이션/CorruptionHandler/파일명 제어에 유리합니다.권장 예시(참고용, 전체 교체가 필요할 때):
// imports 예시 import androidx.datastore.core.dataStoreFile import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import androidx.datastore.core.DataStore @Provides @Singleton fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> = PreferenceDataStoreFactory.create( produceFile = { context.dataStoreFile("thip_tokens.preferences_pb") } )참고: 기존 위임 패턴을 유지한다면 확장 프로퍼티를 파일 top-level로 이동해(모듈 객체 바깥) 재사용성을 높이고, 테스트 모듈에서도 동일 인스턴스를 주입할 수 있게 해주세요.
app/src/main/java/com/texthip/thip/data/model/users/response/OthersFollowersResponse.kt (1)
8-11: @SerialName 축소로 보일러플레이트 정리 가능키 이름이 필드명과 동일하므로 @SerialName은 불필요합니다. 유지 보수성 향상을 위해 제거를 권장합니다.
- @SerialName("followers") val followers: List<FollowerList>, - @SerialName("totalFollowerCount") val totalFollowerCount: Int, - @SerialName("nextCursor") val nextCursor: String?, - @SerialName("isLast") val isLast: Boolean + val followers: List<FollowerList>, + val totalFollowerCount: Int, + val nextCursor: String?, + val isLast: Boolean- @SerialName("userId") val userId: Long, - @SerialName("nickname") val nickname: String, - @SerialName("profileImageUrl") val profileImageUrl: String?, - @SerialName("aliasName") val aliasName: String, - @SerialName("aliasColor") val aliasColor: String, - @SerialName("followerCount") val followerCount: Int + val userId: Long, + val nickname: String, + val profileImageUrl: String?, + val aliasName: String, + val aliasColor: String, + val followerCount: IntAlso applies to: 16-21
app/src/main/java/com/texthip/thip/data/model/users/response/AliasChoiceResponse.kt (1)
8-8: @SerialName 생략으로 간결화 가능JSON 키와 필드명이 동일하므로 @SerialName 없이도 역직렬화됩니다. 보일러플레이트를 줄이기 위해 제거를 제안합니다.
- @SerialName("aliasChoices") val aliasChoices: List<AliasChoice> + val aliasChoices: List<AliasChoice>- @SerialName("aliasName") val aliasName: String, - @SerialName("categoryName") val categoryName: String, - @SerialName("imageUrl") val imageUrl: String, - @SerialName("aliasColor") val aliasColor: String + val aliasName: String, + val categoryName: String, + val imageUrl: String, + val aliasColor: StringAlso applies to: 13-16
app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt (1)
4-4: 동일 키명에 대한 @SerialName 주석은 중복(선택)입니다.필드명이 JSON 키와 동일하므로 @SerialName은 생략 가능해 보입니다. 팀 합의에 따라 가독성을 위해 유지할 수도 있으나, 보일러플레이트를 줄이려면 제거를 제안합니다.
-import kotlinx.serialization.SerialName ... - @SerialName("userId") val userId: Long, - @SerialName("nickname") val nickname: String, - @SerialName("profileImageUrl") val profileImageUrl: String?, - @SerialName("aliasName") val aliasName: String, - @SerialName("aliasColor") val aliasColor: String, - @SerialName("followerCount") val followerCount: Int + val userId: Long, + val nickname: String, + val profileImageUrl: String?, + val aliasName: String, + val aliasColor: String, + val followerCount: IntAlso applies to: 12-17
app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt (2)
65-67: clearTokens()가 DataStore 전체 키를 삭제합니다 — 범위 축소 권장동일 DataStore 인스턴스를 다른 설정에도 사용한다면, prefs.clear()는 비의도적 설정 삭제를 유발할 수 있습니다. 토큰 키만 제거하도록 변경을 권장합니다.
- suspend fun clearTokens() { - dataStore.edit { prefs -> prefs.clear() } - } + suspend fun clearTokens() { + dataStore.edit { prefs -> + prefs.remove(APP_TOKEN_KEY) + prefs.remove(TEMP_TOKEN_KEY) + prefs.remove(REFRESH_TOKEN_KEY) + } + }
35-37: first() 직접 접근으로 간단/효율화 가능map { ... }.first() 대신 data.first()[KEY]가 더 간단하고 불필요한 map 연산을 줄일 수 있습니다. (암호화 적용 시에는 해당 제안과 충돌하니 둘 중 하나만 선택)
- suspend fun getTokenOnce(): String? { - return dataStore.data.map { prefs -> prefs[APP_TOKEN_KEY] }.first() - } + suspend fun getTokenOnce(): String? = + dataStore.data.first()[APP_TOKEN_KEY] @@ - suspend fun getTempTokenOnce(): String? { - return dataStore.data.map { prefs -> prefs[TEMP_TOKEN_KEY] }.first() - } + suspend fun getTempTokenOnce(): String? = + dataStore.data.first()[TEMP_TOKEN_KEY] @@ - suspend fun getRefreshTokenOnce(): String? { - return dataStore.data.map { prefs -> prefs[REFRESH_TOKEN_KEY] }.first() - } + suspend fun getRefreshTokenOnce(): String? = + dataStore.data.first()[REFRESH_TOKEN_KEY]Also applies to: 48-50, 61-63
app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt (1)
3-3: 동일 키명에 대한 @SerialName은 선택 사항(보일러플레이트 제거 제안)필드명과 JSON 키가 동일하여 주석이 없어도 동작합니다. 팀 가이드에 따라 유지 가능하나, 제거 시 가독성 ↑/노이즈 ↓ 효과가 있습니다.
-import kotlinx.serialization.SerialName ... - @SerialName("followings") val followings: List<FollowingList>, - @SerialName("totalFollowingCount") val totalFollowingCount: Int, - @SerialName("nextCursor") val nextCursor: String?, - @SerialName("isLast") val isLast: Boolean + val followings: List<FollowingList>, + val totalFollowingCount: Int, + val nextCursor: String?, + val isLast: Boolean @@ - @SerialName("userId") val userId: Long, - @SerialName("nickname") val nickname: String, - @SerialName("profileImageUrl") val profileImageUrl: String?, - @SerialName("aliasName") val aliasName: String, - @SerialName("aliasColor") val aliasColor: String, - @SerialName("isFollowing") val isFollowing: Boolean + val userId: Long, + val nickname: String, + val profileImageUrl: String?, + val aliasName: String, + val aliasColor: String, + val isFollowing: BooleanAlso applies to: 8-21
app/src/main/java/com/texthip/thip/ThipApplication.kt (1)
12-13: 주입된 TokenManager 미사용 — 불필요하면 제거 고려Application에서 주입만 하고 사용하지 않습니다. 의도적으로 초기화를 보장하려는 목적이 아니라면 제거해 Lint 경고/의존성 결합을 줄이는 것을 권장합니다.
- @Inject - lateinit var tokenManager: TokenManagerapp/src/main/java/com/texthip/thip/MainActivity.kt (1)
43-63: 중복 목적지 방지를 위한 launchSingleTop 권장동일 화면으로 연속 내비게이션될 가능성을 줄이기 위해 launchSingleTop을 함께 지정하는 것을 권장합니다. 사용자 빠른 탭이나 중복 이벤트에서 안전합니다.
- navController.navigate(CommonRoutes.Login) { + navController.navigate(CommonRoutes.Login) { popUpTo(CommonRoutes.Splash) { inclusive = true } + launchSingleTop = true } ... - navController.navigate(CommonRoutes.Main) { + navController.navigate(CommonRoutes.Main) { popUpTo(CommonRoutes.Splash) { inclusive = true } + launchSingleTop = true } ... - navController.navigate(CommonRoutes.SignupFlow) + navController.navigate(CommonRoutes.SignupFlow) { + launchSingleTop = true + } ... - navController.navigate(CommonRoutes.Main) { // 혹은 MainGraph + navController.navigate(CommonRoutes.Main) { // 혹은 MainGraph popUpTo(CommonRoutes.Splash) { inclusive = true } + launchSingleTop = true }app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SplashViewModel.kt (1)
31-42: 에러 핸들링 및 토큰 만료 고려 권장
- 현재는 토큰 조회 실패(예: DataStore I/O 예외) 시 로딩 상태에서 멈출 수 있습니다. 실패 시 로그인으로 유도하도록 try/catch를 권장합니다.
- PR 목적상 "만료 전 자동로그인"을 보장하려면 단순 존재 여부가 아닌 만료(exp)도 함께 확인하는 편이 안전합니다(저장 시 만료 시각을 함께 저장하거나 JWT exp 디코딩).
다음과 같이 최소한의 예외 처리와 공백 토큰 처리 보완을 제안합니다.
private fun checkLoginStatus() { viewModelScope.launch { delay(2000L) // 스플래시 최소 노출 시간 - - val token = tokenManager.getTokenOnce() - - if (token.isNullOrBlank()) { - _destination.value = SplashDestination.NavigateToLogin - } else { - _destination.value = SplashDestination.NavigateToHome - } + try { + val token = tokenManager.getTokenOnce() + _destination.value = + if (token.isNullOrBlank() /* || isExpired(token) */) { + SplashDestination.NavigateToLogin + } else { + SplashDestination.NavigateToHome + } + } catch (e: Exception) { + _destination.value = SplashDestination.NavigateToLogin + } } }만료 체크는 TokenManager에 저장 시(expiryEpochSeconds) 함께 저장해 검증하는 방식을 추천드립니다. 필요하시면 해당 보조 메서드/저장 포맷까지 제안 드리겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (15)
app/build.gradle.kts(3 hunks)app/src/main/java/com/texthip/thip/MainActivity.kt(1 hunks)app/src/main/java/com/texthip/thip/ThipApplication.kt(1 hunks)app/src/main/java/com/texthip/thip/data/di/DataStoreModule.kt(1 hunks)app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/AliasChoiceResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/OthersFollowersResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/SignupResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/navigator/navigations/AuthNavigation.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt(2 hunks)app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SplashViewModel.kt(1 hunks)app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/texthip/thip/MainActivity.kt (2)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/AuthNavigation.kt (1)
authNavigation(22-101)app/src/main/java/com/texthip/thip/MainScreen.kt (1)
MainScreen(16-39)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/AuthNavigation.kt (2)
app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt (1)
SplashScreen(31-71)app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt (1)
LoginScreen(47-119)
🔇 Additional comments (14)
app/build.gradle.kts (1)
94-96: Gson → Kotlinx 컨버터 전환은 방향성 OKGson 컨버터 주석 처리 후 Kotlinx 컨버터를 사용하도록 한 점은 이번 직렬화 마이그레이션과 일관됩니다. Retrofit 빌더에서 컨버터 순서 및 JSON 설정(ignoreUnknownKeys 등)만 확인하면 됩니다.
app/src/main/java/com/texthip/thip/data/model/users/response/SignupResponse.kt (1)
3-3: Kotlinx Serialization 마이그레이션 전반은 깔끔합니다Gson → Kotlinx로의 전환이 일관되며, 데이터 클래스 구조도 서버 스키마와 매칭되어 보입니다. 정상입니다.
Also applies to: 8-9
app/src/main/java/com/texthip/thip/data/model/users/response/OthersFollowersResponse.kt (1)
3-3: Kotlinx Serialization 전환 자체는 문제 없습니다변경된 import와 @serializable 적용이 일관됩니다. 이 파일 범위 내에서는 직렬화 관점에서 이슈 없어 보입니다.
app/src/main/java/com/texthip/thip/data/model/users/response/AliasChoiceResponse.kt (1)
3-3: 직렬화 마이그레이션 방향성은 적절합니다Gson → Kotlinx로의 전환에 맞게 import/애노테이션이 정돈되었습니다.
app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt (1)
35-37: AuthInterceptor의 runBlocking 사용 주의현재
AuthInterceptor에서 토큰 조회(getTokenOnce()/getTempTokenOnce()) 시runBlocking을 사용하고 있습니다.
runBlocking은 호출된 스레드를 블로킹하므로, 이 인터셉터가 메인(UI) 스레드에서 실행되지 않도록 반드시 검토해 주세요.• 대상 위치
- app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt:20–22
• 검토 포인트
- OkHttp 호출이 모두 비동기(
enqueue)로만 이루어지고, 절대newCall(...).execute()형태로 메인 스레드에서 실행되지 않는지 확인- 불확실할 경우, 인터셉터 내부에서 토큰을 직접 suspend 호출하지 않도록 미리 캐싱 또는 콜백/비동기 구조로 리팩토링 고려
앱 실행 중 UI 프레임워크 스레드가 블로킹되지 않도록 위 사항을 점검해 주시기 바랍니다.
app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt (1)
3-3: Kotlinx Serialization 마이그레이션 깔끔합니다.Gson → Kotlinx 전환이 일관되고 타입 변경 없이 적용되었습니다. LGTM.
Also applies to: 8-21
app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt (1)
118-118: getTempTokenOnce() 사용으로 일회 조회 전환 — 목적에 부합합니다.Flow 수집 없이 일회 조회로 간결하고 의도가 명확해졌습니다. LGTM.
app/src/main/java/com/texthip/thip/ThipApplication.kt (1)
18-24: KakaoSdk 초기화 예외 처리 유지 — 안정성 측면에서 적절합니다.런타임 예외 방어가 되어 있어 부팅 크래시 리스크를 낮춥니다. LGTM.
app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt (1)
15-17: Authorization 헤더 존재 시 조기 반환 처리 좋아요이미 Authorization 헤더가 있는 요청은 추가 가공 없이 그대로 진행하는 판단은 적절합니다. 다른 인터셉터와의 중복 주입을 예방합니다.
app/src/main/java/com/texthip/thip/ui/navigator/navigations/AuthNavigation.kt (2)
22-28: 콜백 기반 시그니처 확장 적절합니다NavGraphBuilder 확장 함수에 네비게이션 콜백을 주입해 화면 모듈이 NavController에 직접 의존하지 않도록 한 방향성은 좋습니다. MainActivity와의 연결도 자연스럽습니다.
31-39: Splash/Login 스크린 콜백 연결 LGTMSplash는 onNavigateToLogin/Home, Login은 onNavigateToSignup/onLoginSuccess로 분리되어 테스트와 재사용성이 좋아졌습니다.
app/src/main/java/com/texthip/thip/MainActivity.kt (2)
43-63: 루트 네비게이션에서 콜백 주입 방식 전환 LGTMSplash→Login/Home, Login→Signup/MainAfterSignup 흐름을 콜백으로 캡슐화한 점이 일관성 있고 명확합니다. popUpTo로 백스택 정리하는 처리도 적절합니다.
72-73: Main로의 popUpTo(Main) 정리 OKMain 진입 시 백스택을 비워 재로그인 전환 시 회귀 경로가 남지 않도록 한 처리는 의도에 부합합니다.
app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt (1)
34-45: 목적지 기반 상태 관찰 + LaunchedEffect 처리 적절destination StateFlow를 수명 주기 인지 수집으로 관찰하고, LaunchedEffect로 단일 사이드이펙트를 트리거하는 설계가 깔끔합니다. Loading 상태 무시도 명확합니다.
| implementation(libs.androidx.material3) | ||
| implementation(libs.androidx.navigation.compose) | ||
| implementation(libs.androidx.foundation) | ||
| //implementation(libs.androidx.foundation) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Compose BOM과 직접 버전 고정 혼용으로 호환성/런타임 이슈 위험
BOM을 사용 중임에도 foundation를 개별 좌표(1.5.0)로 고정하고 있습니다. 이 조합은 컴파일/런타임 버전 불일치(예: NoSuchMethodError, ClassNotFoundException)로 쉽게 이어집니다. foundation는 BOM에 위임하거나 하나의 좌표만 유지하세요.
권장: 개별 고정(1.5.0) 제거 후, 카탈로그/BOM 기반 의존성만 유지.
다음 중 하나를 적용하세요.
옵션 A) 개별 고정 제거(권장):
- implementation("androidx.compose.foundation:foundation:1.5.0")옵션 B) 카탈로그 항목 복귀(필요 시):
- //implementation(libs.androidx.foundation)
+ implementation(libs.androidx.foundation)추가로 libs.foundation과 libs.androidx.foundation이 서로 다른 별칭으로 같은 모듈을 가리킨다면 중복 하나를 정리해 단일 소스만 유지하세요.
Also applies to: 85-85
🤖 Prompt for AI Agents
In app/build.gradle.kts around line 70 (and also line 85), you are pinning
androidx.foundation to a specific version while using the Compose BOM, which
risks compile/runtime mismatches; remove the hard-coded
implementation(libs.androidx.foundation) version so the BOM controls the
foundation version (Option A, recommended), or if you intentionally need an
explicit dependency restore it via the version-catalog alias (Option B) instead
of hard-coding the version; also check for duplicate aliases (libs.foundation vs
libs.androidx.foundation) pointing to the same module and consolidate to a
single catalog entry to avoid inconsistent sources.
| private val APP_TOKEN_KEY = stringPreferencesKey("app_token") // 정식 액세스토큰 | ||
| private val TEMP_TOKEN_KEY = stringPreferencesKey("temp_token") // 임시 토큰 | ||
| private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token") |
There was a problem hiding this comment.
🛠️ Refactor suggestion
민감 토큰을 평문 Preferences DataStore에 저장 — 암호화 필요
access/refresh/temp 토큰이 암호화 없이 저장됩니다. 루팅/백업/디버그 로그 유출 시 보안사고로 직결됩니다. 최소한 refresh 토큰은 암호화 저장을 권장합니다. AndroidX Security Crypto로 간단히 래핑하는 방식을 제안드립니다.
권장 접근(예): 암호화 래퍼 주입 후 저장/조회 시 encrypt/decrypt 적용.
class TokenManager @Inject constructor(
- private val dataStore: DataStore<Preferences>
+ private val dataStore: DataStore<Preferences>,
+ private val secure: SecureStorage
) {
@@
suspend fun saveToken(token: String) {
- dataStore.edit { prefs ->
- prefs[APP_TOKEN_KEY] = token
- }
+ dataStore.edit { prefs -> prefs[APP_TOKEN_KEY] = secure.encrypt(token) }
}
@@
- suspend fun getTokenOnce(): String? {
- return dataStore.data.map { prefs -> prefs[APP_TOKEN_KEY] }.first()
- }
+ suspend fun getTokenOnce(): String? =
+ dataStore.data.first()[APP_TOKEN_KEY]?.let(secure::decrypt)
@@
suspend fun saveTempToken(token: String) {
- dataStore.edit { prefs -> prefs[TEMP_TOKEN_KEY] = token }
+ dataStore.edit { prefs -> prefs[TEMP_TOKEN_KEY] = secure.encrypt(token) }
}
@@
- suspend fun getTempTokenOnce(): String? {
- return dataStore.data.map { prefs -> prefs[TEMP_TOKEN_KEY] }.first()
- }
+ suspend fun getTempTokenOnce(): String? =
+ dataStore.data.first()[TEMP_TOKEN_KEY]?.let(secure::decrypt)
@@
suspend fun saveRefreshToken(token: String) {
- dataStore.edit { prefs -> prefs[REFRESH_TOKEN_KEY] = token }
+ dataStore.edit { prefs -> prefs[REFRESH_TOKEN_KEY] = secure.encrypt(token) }
}
@@
- suspend fun getRefreshTokenOnce(): String? {
- return dataStore.data.map { prefs -> prefs[REFRESH_TOKEN_KEY] }.first()
- }
+ suspend fun getRefreshTokenOnce(): String? =
+ dataStore.data.first()[REFRESH_TOKEN_KEY]?.let(secure::decrypt)참고 구현(새 파일): 간단한 SecureStorage 인터페이스와 기본 구현
package com.texthip.thip.security
interface SecureStorage {
fun encrypt(plain: String): String
fun decrypt(cipher: String): String
}package com.texthip.thip.security
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class EncryptedPrefsSecureStorage(context: Context) : SecureStorage {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val prefs = EncryptedSharedPreferences.create(
context,
"secure_kv",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
override fun encrypt(plain: String): String {
// 값을 prefs에 임시 저장 후 키로 접근할 수도 있으나,
// 간단화를 위해 여기선 그대로 반환하고 TokenManager에서 prefs를 쓰도록 변경 가능.
return plain
}
override fun decrypt(cipher: String): String = cipher
}위 예시는 방향성 제시용입니다. 실제로는 TokenManager가 DataStore 대신 보안 저장소를 직접 쓰거나, Proto DataStore + 커스텀 Serializer 암호화를 고려해 주세요.
Also applies to: 24-37, 43-50, 56-63
| @SerializedName("aliasName") val aliasName: String, | ||
| @SerializedName("aliasColor") val aliasColor: String, | ||
| @SerializedName("isFollowing") val isFollowing: Boolean | ||
| @SerialName("userId") val userId: Long, |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
userId(Long) ↔ 다른 모델(Int) 불일치 — 전역 정합성 확보 필요
본 파일은 Long, UserSearchResponse는 Int로 상이합니다. 서버가 Long을 반환한다면, 전역 Long 통일을 권장합니다.
전역 점검 스크립트:
🏁 Script executed:
#!/bin/bash
# userId 타입 불일치 탐지
rg -nP --type=kt -C2 '\b(userId)\s*:\s*(Int|Long)\b' app/src/main/javaLength of output: 99
타입 불일치 재검증 스크립트
아래 명령으로 .kt 파일 전역에서 userId 선언부를 Int 또는 Long으로 탐지해 주세요:
🏁 Script executed:
#!/bin/bash
# userId 타입(Int vs Long) 불일치 탐지
rg -nP -g '*.kt' -C2 '\buserId\b\s*:\s*(Int|Long)\b' app/src/main/javaLength of output: 20592
전역 userId 타입 정합성 확보 필요
전역 검토 결과 UserSearchResponse 에서만 userId 를 Int 로 선언하고, 나머지 모든 모델 및 네비게이션, ViewModel, 서비스 레이어에서 Long 을 사용하고 있습니다. 서버가 Long 범위의 값을 반환할 경우 파싱 오류 또는 타입 불일치 문제가 발생할 수 있으므로, 아래와 같이 UserSearchResponse 의 userId 타입을 Long 으로 통일하시기 바랍니다.
수정 대상:
- app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt
제안하는 변경(diff):
--- a/app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt
+++ b/app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt
@@ -11,7 +11,7 @@
@Serializable
data class UserItem(
- @SerialName("userId") val userId: Int,
+ @SerialName("userId") val userId: Long,
@SerialName("nickname") val nickname: String,
@SerialName("profileImageUrl") val profileImageUrl: String?,
)이외에도 UserSearchResponse 를 사용하는 곳(예: 검색 결과 화면)에서 .userId 값을 그대로 NavHostController.navigateToUserProfile(userId: Long) 등에 넘길 수 있도록 타입 호환을 확인해 주세요.
| @SerialName("userId") val userId: Long, | ||
| @SerialName("nickname") val nickname: String, | ||
| @SerialName("profileImageUrl") val profileImageUrl: String?, | ||
| @SerialName("aliasName") val aliasName: String, | ||
| @SerialName("aliasColor") val aliasColor: String, | ||
| @SerialName("followerCount") val followerCount: Int |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
FollowerList.isMyself 제거는 호환성 파괴 변경 — 사용처 전수 확인 필요
isMyself: Boolean 필드가 제거되었습니다. UI/로직에서 해당 플래그를 참조하던 곳이 있다면 컴파일/런타임 문제가 발생합니다. 클라이언트에서 userId == currentUserId로 파생 계산하도록 전환했는지 확인해 주세요.
제안: 서버 응답 → 도메인 모델 변환 시 파생 플래그를 계산
data class FollowerUi(
val userId: Long,
val nickname: String,
val profileImageUrl: String?,
val aliasName: String,
val aliasColor: String,
val followerCount: Int,
val isMyself: Boolean,
)
fun FollowerList.toUi(currentUserId: Long) = FollowerUi(
userId, nickname, profileImageUrl, aliasName, aliasColor, followerCount,
isMyself = userId == currentUserId
)원클릭 점검 스크립트(사용처 탐색):
🏁 Script executed:
#!/bin/bash
# FollowerList.isMyself 사용 흔적 탐색
rg -n -C2 -P '\bisMyself\b'Length of output: 3215
FollowerList.isMyself 제거로 인한 호환성 파괴 — 매핑·UI 호출부 전수 수정 필요
네트워크 응답 모델에서 isMyself: Boolean 필드를 제거하면서, 기존 UI/로직에서 이를 직접 참조하던 부분이 컴파일 오류나 런타임 이슈를 일으킵니다.
아래 사용처를 전부 점검하고, 네트워크 모델 → UI 모델 변환 단계에서 isMyself 를 파생 계산해 주입하도록 변경하세요.
-
FeedNavigation.kt (app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt)
• 231–234행:onProfileClick = { userId, isMyself -> if (isMyself) navController.navigate(FeedRoutes.My) else … }
→ 네트워크 모델에
isMyself가 사라졌으므로, UI 모델(예:FollowerUi)의isMyself값을 넘기도록 변경 필요 -
OthersSubscriptionListScreen.kt (app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt)
• 46행, 79행:onProfileClick: (userId: Long, isMyself: Boolean) -> Unit시그니처 검토
• 125행:onClick = { onProfileClick(user.userId, user.isMyself) }→
user.isMyself가 매핑된 값인지, 적절히 계산된 값인지 확인
• 168행 (미리보기용 더미 데이터):isMyself = true
→ 가능한 실제 로직과 동일하게
userId == currentUserId조건으로 변경 권장
제안: 데이터 계층에서 아래와 같이 확장 함수를 구현·확인해 주세요.
data class FollowerUi(
val userId: Long,
val nickname: String,
val profileImageUrl: String?,
val aliasName: String,
val aliasColor: String,
val followerCount: Int,
val isMyself: Boolean
)
fun FollowerList.toUi(currentUserId: Long) = FollowerUi(
userId = userId,
nickname = nickname,
profileImageUrl = profileImageUrl,
aliasName = aliasName,
aliasColor = aliasColor,
followerCount = followerCount,
isMyself = userId == currentUserId
)이 매핑 로직을 적용한 뒤, UI 호출부에서는 onProfileClick(user.userId, user.isMyself) 형태로 안전하게 전달할 수 있습니다.
🤖 Prompt for AI Agents
app/src/main/java/com/texthip/thip/data/model/users/response/OthersFollowersResponse.kt
lines 16–21: removing isMyself from the network model breaks callers that still
expect it; reintroduce isMyself at the UI/data-mapping layer by creating a UI
model (e.g., FollowerUi) and a mapping function that computes isMyself = (userId
== currentUserId), then update all call sites (FeedNavigation.kt around 231–234,
OthersSubscriptionListScreen.kt at 46, 79, 125 and preview data at 168) to
accept and pass the derived isMyself from the UI model (replace any direct
references to the network model with the mapped FollowerUi and change dummy
previews to compute isMyself via currentUserId equality).
| @SerialName("userId") val userId: Int, | ||
| @SerialName("nickname") val nickname: String, | ||
| @SerialName("profileImageUrl") val profileImageUrl: String?, | ||
| @SerialName("aliasName") val aliasName: String, | ||
| @SerialName("aliasColor") val aliasColor: String, | ||
| @SerialName("followerCount") val followerCount: Int |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
userId 타입이 다른 모델과 불일치합니다(Long 권장).
동일 도메인 모델(MyFollowingsResponse.FollowingList.userId: Long)과 타입이 다릅니다. 백엔드 스키마가 Long(또는 64-bit ID)이라면 현재 Int는 오버플로/역직렬화 실패 위험이 있습니다. 전역적으로 Long으로 통일을 권장합니다.
아래 스크립트로 userId 타입 불일치를 스캔해 주세요.
권장 수정 예시(diff):
- @SerialName("userId") val userId: Int,
+ @SerialName("userId") val userId: Long,🏁 Script executed:
#!/bin/bash
# userId 타입 정의/사용 현황 점검
rg -nP --type=kt -C2 '\b(userId)\s*:\s*(Int|Long)\b' app/src/main/javaLength of output: 99
아래 스크립트로 .kt 파일 전체를 확장자 기반으로 다시 스캔해 보겠습니다.
🏁 Script executed:
#!/bin/bash
# userId 타입 정의/사용 현황 점검 (확장자 기반 검색)
rg -nC2 '\b(userId)\s*:\s*(Int|Long)\b' -g '*.kt' app/src/main/javaLength of output: 20589
UserSearchResponse의 userId 타입을 Long으로 통일하세요.
검증 결과, 대부분의 도메인 모델 및 API 응답에서 userId가 Long으로 정의된 반면, UserSearchResponse.UserItem만 Int를 사용하고 있습니다. 이로 인해 64비트 ID 처리 중 오버플로우 또는 역직렬화 오류가 발생할 수 있으므로 반드시 수정해야 합니다.
• 수정 대상
- 파일:
app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt- 12행:
@SerialName("userId") val userId: Int,
- 12행:
• 권장 수정 예시
- @SerialName("userId") val userId: Int,
+ @SerialName("userId") val userId: Long,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @SerialName("userId") val userId: Int, | |
| @SerialName("nickname") val nickname: String, | |
| @SerialName("profileImageUrl") val profileImageUrl: String?, | |
| @SerialName("aliasName") val aliasName: String, | |
| @SerialName("aliasColor") val aliasColor: String, | |
| @SerialName("followerCount") val followerCount: Int | |
| @SerialName("userId") val userId: Long, | |
| @SerialName("nickname") val nickname: String, | |
| @SerialName("profileImageUrl") val profileImageUrl: String?, | |
| @SerialName("aliasName") val aliasName: String, | |
| @SerialName("aliasColor") val aliasColor: String, | |
| @SerialName("followerCount") val followerCount: Int |
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt
around lines 12 to 17, the userId property in UserItem is declared as Int but
must be Long to match other domain models and API responses; change the property
type from Int to Long, update any related constructor usages and tests to use
Long values, and ensure any places deserializing or mapping UserItem expect a
Long (no other code changes should be required if types are consistently Long
elsewhere).
| Log.d("SignupDebug", "UserRepository.signup() 호출됨. 요청 닉네임: ${request.nickname}") | ||
|
|
||
| val tempToken = tokenManager.getTempToken() | ||
| val tempToken = tokenManager.getTempTokenOnce() | ||
|
|
||
| Log.d("SignupDebug", "가져온 임시 토큰: $tempToken") | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
토큰 값을 로그로 노출하지 마세요(보안 이슈).
"가져온 임시 토큰: $tempToken" 로그는 토큰 유출 위험이 큽니다. 존재 여부 등 비식별 정보만 남기거나 로그를 제거하세요.
- Log.d("SignupDebug", "가져온 임시 토큰: $tempToken")
+ Log.d("SignupDebug", "임시 토큰 존재 여부: ${!tempToken.isNullOrBlank()}")추가 점검 스크립트(전역 토큰 로그 탐지):
🏁 Script executed:
#!/bin/bash
# 토큰/Authorization 관련 민감 정보 로그 사용 탐지
rg -nP -C2 '\bLog\.(d|i|w|e)\s*\(.*(token|Token|Authorization|Bearer).*\)' app/src/main/javaLength of output: 612
임시 토큰 정보 로그 제거 필요
UserRepository.kt에서 평문 토큰이 로그에 그대로 노출되고 있어 보안 이슈가 발생합니다. 아래 위치를 확인 후, 민감 정보가 유출되지 않도록 즉시 수정해주세요.
- 파일:
app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt
문제 라인: 118–121
제안된 수정(diff):
- Log.d("SignupDebug", "가져온 임시 토큰: $tempToken")
+ Log.d("SignupDebug", "임시 토큰 존재 여부: ${!tempToken.isNullOrBlank()}")이후에도 토큰·Authorization·Bearer 등 민감 정보가 로그에 남지 않도록 전역 검색(rg -nP '\bLog\.(d|i|w|e)\s*\(.*(token|Token|Authorization|Bearer).*\)')을 수행해 추가 노출 여부를 반드시 점검하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Log.d("SignupDebug", "UserRepository.signup() 호출됨. 요청 닉네임: ${request.nickname}") | |
| val tempToken = tokenManager.getTempToken() | |
| val tempToken = tokenManager.getTempTokenOnce() | |
| Log.d("SignupDebug", "가져온 임시 토큰: $tempToken") | |
| Log.d("SignupDebug", "UserRepository.signup() 호출됨. 요청 닉네임: ${request.nickname}") | |
| val tempToken = tokenManager.getTempTokenOnce() | |
| // 민감 정보 노출 방지를 위해 실제 토큰이 아닌 존재 여부만 로깅 | |
| Log.d("SignupDebug", "임시 토큰 존재 여부: ${!tempToken.isNullOrBlank()}") |
| @@ -1,5 +1,6 @@ | |||
| package com.texthip.thip.ui.navigator.navigations | |||
|
|
|||
| import SplashScreen | |||
There was a problem hiding this comment.
잘못된 import로 인한 컴파일 실패 가능성
import SplashScreen은 유효한 FQCN이 아닙니다. SplashScreen은 com.texthip.thip.ui.signin.screen 패키지에 존재하므로 아래처럼 수정이 필요합니다.
- import SplashScreen
+ import com.texthip.thip.ui.signin.screen.SplashScreen🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/navigator/navigations/AuthNavigation.kt
around line 3, the import statement "import SplashScreen" is not a valid FQCN
and will cause a compile error; replace it with the correct fully-qualified
import for SplashScreen in the com.texthip.thip.ui.signin.screen package (i.e.,
import com.texthip.thip.ui.signin.screen.SplashScreen) and remove the invalid
import so the file compiles.
| import androidx.lifecycle.viewModelScope | ||
| import com.texthip.thip.data.manager.TokenManager | ||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||
| import jakarta.inject.Inject |
There was a problem hiding this comment.
Hilt 주입 패키지 오입력(jakarta→javax)으로 인한 주입 실패
Hilt(Dagger)는 javax.inject.Inject를 사용합니다. jakarta.inject.Inject로는 주입이 이뤄지지 않아 컴파일/실행 시 오류가 발생할 수 있습니다. 아래와 같이 수정해 주세요.
- import jakarta.inject.Inject
+ import javax.inject.Inject🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SplashViewModel.kt
around line 7, the import uses jakarta.inject.Inject which prevents Hilt/Dagger
injection; replace it with javax.inject.Inject (i.e., change the import
statement to javax.inject.Inject) so the DI framework recognizes the annotation
and rebuild to verify injection succeeds.
| // 1. 정식 토큰을 먼저 확인합니다. | ||
| val token = runBlocking { tokenManager.getTokenOnce() } | ||
| // 2. 정식 토큰이 없으면, 임시 토큰을 확인합니다. | ||
| val tempToken = runBlocking { tokenManager.getTempTokenOnce() } | ||
|
|
||
| if (!tokenToSend.isNullOrBlank()) { | ||
| requestBuilder.addHeader( | ||
| "Authorization", | ||
| "Bearer $tokenToSend" | ||
| ) | ||
| } | ||
| // 보낼 토큰을 결정합니다 (정식 토큰 우선). | ||
| val tokenToSend = token ?: tempToken | ||
|
|
||
| val newRequest = original.newBuilder().apply { | ||
| tokenToSend?.let { addHeader("Authorization", "Bearer $it") } | ||
| }.build() | ||
|
|
||
| return chain.proceed(requestBuilder.build()) | ||
| return chain.proceed(newRequest) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
빈 문자열 토큰 처리 누락 + 불필요한 2회 runBlocking 호출로 인한 I/O 낭비
- getTokenOnce()가 빈 문자열("")을 반환할 경우, 현재 로직은 tempToken으로의 폴백 없이 "Bearer " 헤더를 전송합니다. 이는 인증 실패(401) 및 임시 토큰 사용 불가로 이어질 수 있습니다.
- 정식 토큰이 존재하는지 확인하기 전에 임시 토큰을 매번 조회하여 DataStore I/O를 2번 발생시키고 있습니다.
아래처럼 한 번의 runBlocking 블록 안에서 공백 토큰을 무시하고(빈 문자열 포함) 필요한 경우에만 임시 토큰을 조회하도록 정리해 주세요.
- // 1. 정식 토큰을 먼저 확인합니다.
- val token = runBlocking { tokenManager.getTokenOnce() }
- // 2. 정식 토큰이 없으면, 임시 토큰을 확인합니다.
- val tempToken = runBlocking { tokenManager.getTempTokenOnce() }
-
- // 보낼 토큰을 결정합니다 (정식 토큰 우선).
- val tokenToSend = token ?: tempToken
-
- val newRequest = original.newBuilder().apply {
- tokenToSend?.let { addHeader("Authorization", "Bearer $it") }
- }.build()
+ // 공백 토큰은 무시하고, 필요한 경우에만 임시 토큰을 조회합니다.
+ val tokenToSend = runBlocking {
+ tokenManager.getTokenOnce()?.takeIf { it.isNotBlank() }
+ ?: tokenManager.getTempTokenOnce()?.takeIf { it.isNotBlank() }
+ }
+
+ val newRequest = if (tokenToSend != null) {
+ original.newBuilder()
+ .header("Authorization", "Bearer $tokenToSend")
+ .build()
+ } else {
+ original
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 1. 정식 토큰을 먼저 확인합니다. | |
| val token = runBlocking { tokenManager.getTokenOnce() } | |
| // 2. 정식 토큰이 없으면, 임시 토큰을 확인합니다. | |
| val tempToken = runBlocking { tokenManager.getTempTokenOnce() } | |
| if (!tokenToSend.isNullOrBlank()) { | |
| requestBuilder.addHeader( | |
| "Authorization", | |
| "Bearer $tokenToSend" | |
| ) | |
| } | |
| // 보낼 토큰을 결정합니다 (정식 토큰 우선). | |
| val tokenToSend = token ?: tempToken | |
| val newRequest = original.newBuilder().apply { | |
| tokenToSend?.let { addHeader("Authorization", "Bearer $it") } | |
| }.build() | |
| return chain.proceed(requestBuilder.build()) | |
| return chain.proceed(newRequest) | |
| // 공백 토큰은 무시하고, 필요한 경우에만 임시 토큰을 조회합니다. | |
| val tokenToSend = runBlocking { | |
| tokenManager.getTokenOnce()?.takeIf { it.isNotBlank() } | |
| ?: tokenManager.getTempTokenOnce()?.takeIf { it.isNotBlank() } | |
| } | |
| val newRequest = if (tokenToSend != null) { | |
| original.newBuilder() | |
| .header("Authorization", "Bearer $tokenToSend") | |
| .build() | |
| } else { | |
| original | |
| } | |
| return chain.proceed(newRequest) |
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt around lines
19 to 31, the current code calls runBlocking twice and treats an empty string
token as a valid token, causing a "Bearer " header to be sent and unnecessary
DataStore I/O; refactor to use a single runBlocking block that retrieves the
primary token, treats null or blank (empty or whitespace) as absent, and only if
absent fetches the temp token, then set Authorization only when a non-blank
token is available — this eliminates the double runBlocking, ignores empty
tokens, and prevents sending an empty Bearer header.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (1)
62-83: confirmLogout 호출부 리팩토링 및 ApplicationContext 주입 반영다음 위치에서 시그니처 변경 및 호출부 수정을 진행하세요.
- app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt
•confirmLogout(context: Context)→confirmLogout()로 시그니처 변경
• ViewModel 생성자에@ApplicationContext로 주입된Context필드 추가- app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt
•viewModel.confirmLogout(context = context)→viewModel.confirmLogout()로 호출부 수정ripgrep(
rg -nP '\bconfirmLogout\s*\(' -C2) 결과 MypageScreen.kt의 한 곳에서만 호출되고 있어, 안전하게 시그니처를 바꿀 수 있습니다.// MyPageViewModel.kt - class MyPageViewModel @Inject constructor( - private val tokenManager: TokenManager - ) : ViewModel() { + @HiltViewModel + class MyPageViewModel @Inject constructor( + @ApplicationContext private val appContext: Context, + private val tokenManager: TokenManager + ) : ViewModel() { … - fun confirmLogout(context: Context) { + fun confirmLogout() { viewModelScope.launch { tokenManager.clearTokens() - // 기존: SDK 호출 예시 (context 사용) + // SDK 호출 시 appContext 사용 } }// MypageScreen.kt - val context = LocalContext.current … - onConfirmLogout = { viewModel.confirmLogout( - context = context - ) }, + // context 인자 제거 + onConfirmLogout = { viewModel.confirmLogout() },
🧹 Nitpick comments (3)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (3)
3-3: 미사용 import 제거android.content.Context import가 이 파일 내에서 직접 사용되지 않습니다. 불필요한 import는 Lint 경고와 잡음만 늘립니다.
-import android.content.Context
57-58: 사용되지 않는 onCustomerService 콜백 제거 또는 실제 사용으로 정리현재 MyPageScreen 파라미터(onCustomerService) → MyPageContent 파라미터(onCustomerServiceClick)로 전달하고 있으나, 실제 UI에서는 외부 URL Intent를 직접 실행하여 이 콜백이 전혀 쓰이지 않습니다. API 표면을 단순화하려면 제거하는 편이 낫습니다.
제거 예시(diff는 본 파일 내 관련 지점만 표시):
fun MyPageScreen( navController: NavController, viewModel: MyPageViewModel = hiltViewModel(), onNavigateToEditProfile: () -> Unit, onNavigateToSavedFeeds: () -> Unit, - onCustomerService: () -> Unit, onNavigateToNotificationSettings: () -> Unit, onDeleteAccount: () -> Unit, onNavigateToLogin: () -> Unit ) { @@ MyPageContent( uiState = uiState, onEditProfileClick = onNavigateToEditProfile, onSavedFeedsClick = onNavigateToSavedFeeds, onNotificationSettingsClick = onNavigateToNotificationSettings, - onCustomerServiceClick = onCustomerService, onLogoutClick = { viewModel.onLogoutClick() }, onDismissLogoutDialog = { viewModel.onDismissLogoutDialog() }, onConfirmLogout = { viewModel.confirmLogout() }, onDeleteAccount = onDeleteAccount ) @@ fun MyPageContent( uiState: MyPageUiState, onEditProfileClick: () -> Unit, onSavedFeedsClick: () -> Unit, onNotificationSettingsClick: () -> Unit, - onCustomerServiceClick: () -> Unit, onLogoutClick: () -> Unit, onDismissLogoutDialog: () -> Unit, onConfirmLogout: () -> Unit, onDeleteAccount: () -> Unit ) {미래에 내부/외부 라우팅을 교체할 가능성이 있다면, 반대로 콜백을 실제로 사용하도록 바꾸는 것도 방법입니다.
Also applies to: 73-84, 92-96
196-199: 외부 URL 열기 로직 중복 및 예외 처리 부족 → 공통 헬퍼로 통합5곳에서 동일한 Intent.ACTION_VIEW 패턴을 반복하며, ActivityNotFoundException/해당 Activity 부재 시 크래시 위험이 있습니다. 패키지 매니저로 확인하거나 runCatching/Toast 처리로 안전하게 감싸고, NEW_TASK 플래그를 추가해 비-Activity 컨텍스트에서도 안전하게 동작하도록 권장합니다.
치환 예시:
- onClick = { - val intent = - Intent(Intent.ACTION_VIEW, URL_CUSTOMER_SERVICE.toUri()) - context.startActivity(intent) - } + onClick = { launchUrl(context, URL_CUSTOMER_SERVICE) } @@ - onClick = { - val intent = - Intent(Intent.ACTION_VIEW, URL_NOTICE.toUri()) - context.startActivity(intent) - } + onClick = { launchUrl(context, URL_NOTICE) } @@ - onClick = { - val intent = Intent(Intent.ACTION_VIEW, URL_TERMS_OF_USE.toUri()) - context.startActivity(intent) - } + onClick = { launchUrl(context, URL_TERMS_OF_USE) } @@ - onClick = { - val intent = Intent(Intent.ACTION_VIEW, URL_GUIDE.toUri()) - context.startActivity(intent) - } + onClick = { launchUrl(context, URL_GUIDE) } @@ - onClick = { - val intent = Intent(Intent.ACTION_VIEW, URL_VERSION.toUri()) - context.startActivity(intent) - } + onClick = { launchUrl(context, URL_VERSION) }추가 함수(파일 하단 등 적절한 위치에 정의):
private fun launchUrl(context: Context, url: String) { val intent = Intent(Intent.ACTION_VIEW, url.toUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } val pm = context.packageManager if (intent.resolveActivity(pm) != null) { runCatching { context.startActivity(Intent.createChooser(intent, null)) } .onFailure { /* TODO: 사용자 피드백/로깅 처리 */ } } else { // TODO: 사용자 피드백(Toast/스낵바) 또는 로깅 } }Also applies to: 210-213, 224-226, 237-239, 250-252
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt(2 hunks)
🔇 Additional comments (1)
app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt (1)
3-4: 필요한 import 추가는 적절합니다Context/Log 도입은 하단 변경과 일치합니다. 다만 이후 제안대로 ApplicationContext 주입으로 전환 시 import 위치가 일부 바뀔 수 있습니다.
| import com.google.android.gms.auth.api.signin.GoogleSignIn | ||
| import com.google.android.gms.auth.api.signin.GoogleSignInOptions | ||
| import com.kakao.sdk.user.UserApiClient |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
SDK 의존성(Play Services Auth, Kakao) 및 코루틴 KTX(Task.await) 사용 여부 확인 필요
Google/Kakao 로그아웃 연동을 위해 다음 의존성이 Gradle에 선언되어 있는지 확인해 주세요. 특히 코루틴으로 Task.await을 사용할 계획이라면 kotlinx-coroutines-play-services가 필요합니다.
아래 스크립트로 리포지토리에서 의존성 선언을 확인할 수 있습니다.
🏁 Script executed:
#!/bin/bash
# Gradle 의존성 점검
rg -nP -C2 '(com\.google\.android\.gms:play-services-auth|com\.kakao\.sdk:user|kotlinx-coroutines-play-services)'Length of output: 442
앱 모듈 Gradle 의존성 점검 결과
– com.google.android.gms:play-services-auth (v21.2.0) → 이미 선언되어 있습니다 (app/build.gradle.kts:110)
– com.kakao.sdk:user → 선언 없음
– org.jetbrains.kotlinx:kotlinx-coroutines-play-services → 선언 없음
따라서 아래 의존성을 Gradle에 추가해 주세요:
// 카카오 로그인/로그아웃용
implementation("com.kakao.sdk:user:<latest-version>")
// Play Services Task.await() 사용을 위한 Coroutines KTX
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:<latest-version>")🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt
around lines 7 to 9, the code imports Kakao and Play Services coroutine
utilities but the project Gradle dependencies are missing for com.kakao.sdk:user
and org.jetbrains.kotlinx:kotlinx-coroutines-play-services; open
app/build.gradle.kts and add implementation entries for the latest versions of
com.kakao.sdk:user (for Kakao login/logout) and
org.jetbrains.kotlinx:kotlinx-coroutines-play-services (for Task.await()
coroutine support), then sync the project.
| fun confirmLogout(context: Context) { | ||
| viewModelScope.launch { | ||
| tokenManager.clearTokens() | ||
| // 2. 카카오 SDK에서 로그아웃 | ||
| UserApiClient.instance.unlink { error -> | ||
| if (error != null) { | ||
| Log.e("MyPageViewModel", "카카오 로그아웃 실패", error) | ||
| } else { | ||
| Log.d("MyPageViewModel", "카카오 로그아웃 성공") | ||
| } | ||
| } | ||
|
|
||
| // 3. 구글 SDK에서 로그아웃 | ||
| val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).build() | ||
| GoogleSignIn.getClient(context, gso).signOut() | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
카카오 unlink 사용은 계정 연결 해제(회원탈퇴 성격)입니다; 비동기 완료 대기 없이 즉시 완료 표시하는 점도 문제
- UserApiClient.instance.unlink(...)은 단순 로그아웃이 아니라 서비스 연결을 끊는 동작입니다. 일반 로그아웃 의도라면 logout(...) 사용이 맞습니다.
- Kakao/Google 모두 비동기인데, 현재는 콜백/Task 완료를 기다리지 않고 바로 UI 상태를 isLogoutCompleted=true 로 바꿉니다. 실패해도 성공처럼 보일 수 있습니다.
- UI Context 의존을 제거하고 @ApplicationContext를 주입하면 구조가 더 깔끔해집니다.
권장 수정 예시(diff):
@@
-import android.util.Log
+import android.util.Log
@@
-import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
@@
-import com.kakao.sdk.user.UserApiClient
+import com.kakao.sdk.user.UserApiClient
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.tasks.await
@@
class MyPageViewModel @Inject constructor(
- private val userRepository: UserRepository,
- private val tokenManager: TokenManager
+ private val userRepository: UserRepository,
+ @ApplicationContext private val appContext: Context,
+ private val tokenManager: TokenManager
) : ViewModel() {
@@
- fun confirmLogout(context: Context) {
+ fun confirmLogout() {
viewModelScope.launch {
- tokenManager.clearTokens()
- // 2. 카카오 SDK에서 로그아웃
- UserApiClient.instance.unlink { error ->
- if (error != null) {
- Log.e("MyPageViewModel", "카카오 로그아웃 실패", error)
- } else {
- Log.d("MyPageViewModel", "카카오 로그아웃 성공")
- }
- }
-
- // 3. 구글 SDK에서 로그아웃
- val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).build()
- GoogleSignIn.getClient(context, gso).signOut()
-
- _uiState.update { it.copy(showLogoutDialog = false, isLogoutCompleted = true) }
+ try {
+ // 1) 토큰 삭제 (실패해도 계속 진행)
+ runCatching { tokenManager.clearTokens() }
+ .onFailure { Log.e(TAG, "토큰 삭제 실패", it) }
+
+ // 2) 카카오 로그아웃 (unlink가 아닌 logout 사용)
+ runCatching { kakaoLogout() }
+ .onFailure { Log.e(TAG, "카카오 로그아웃 실패", it) }
+
+ // 3) 구글 로그아웃 (Task.await로 완료 보장)
+ val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).build()
+ runCatching { GoogleSignIn.getClient(appContext, gso).signOut().await() }
+ .onFailure { Log.e(TAG, "구글 로그아웃 실패", it) }
+ } finally {
+ _uiState.update { it.copy(showLogoutDialog = false, isLogoutCompleted = true) }
+ }
}
}
+
+ private companion object {
+ const val TAG = "MyPageViewModel"
+ }추가로 필요한 보조 함수(클래스 내부에 추가):
private suspend fun kakaoLogout() = kotlinx.coroutines.suspendCancellableCoroutine<Unit> { cont ->
UserApiClient.instance.logout { error ->
if (error != null) {
cont.cancel(error)
} else {
cont.resume(Unit, onCancellation = null)
}
}
}참고:
- 정말로 “연결 해제(회원탈퇴)”가 의도라면 unlink 유지가 맞습니다. 그 경우 UI 문구/효과도 “연결 해제”로 명확히 해 주세요.
- Task.await 사용 시 kotlinx-coroutines-play-services 의존성이 필요합니다(별도 코멘트 참고).
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt
around lines 70-85, replace the Kakao unlink call with logout (unless you truly
intend account unlink) and stop flipping isLogoutCompleted immediately; instead
await both asynchronous sign-outs (Kakao and Google) before updating UI state.
Implement a suspend wrapper for UserApiClient.instance.logout using
suspendCancellableCoroutine and suspend the Google signOut Task (either via
Tasks.await or a suspend wrapper using addOnCompleteListener) so failures
cancel/throw and success resumes; only set isLogoutCompleted = true after both
suspend calls complete successfully. Also remove direct Context usage by
injecting @ApplicationContext Context into the ViewModel constructor and use
that for GoogleSignIn client creation.
➕ 이슈 링크
🔎 작업 내용
액세스 토큰이 살아있는 동안은 재접속 시 자동로그인 되도록 처리했습니다.
📸 스크린샷
😢 해결하지 못한 과제
[] TASK
📢 리뷰어들에게
Summary by CodeRabbit