-
Notifications
You must be signed in to change notification settings - Fork 3
[Fix] 액세스 토큰 만료되기 전, 앱 재실행 시 자동로그인 되도록 수정 #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
53aeb1a
3ee77f5
ac987c1
9ac0f3d
fac69fb
19bfc2e
c0af2d1
6c98866
a204a99
2857493
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.texthip.thip.data.di | ||
|
|
||
| import android.content.Context | ||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.preferences.preferencesDataStore | ||
| import dagger.Module | ||
| import dagger.Provides | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import dagger.hilt.components.SingletonComponent | ||
| import androidx.datastore.preferences.core.Preferences | ||
| import javax.inject.Singleton | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| object DataStoreModule { | ||
|
|
||
| private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "thip_tokens") | ||
|
|
||
| @Provides | ||
| @Singleton | ||
| fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> { | ||
| return context.dataStore | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,94 +1,69 @@ | ||
| package com.texthip.thip.data.manager | ||
|
|
||
| import android.content.Context | ||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.preferences.core.Preferences | ||
| import androidx.datastore.preferences.core.edit | ||
| import androidx.datastore.preferences.core.stringPreferencesKey | ||
| import androidx.datastore.preferences.preferencesDataStore | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.first | ||
| import kotlinx.coroutines.flow.map | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "thip_tokens") | ||
|
|
||
| @Singleton | ||
| class TokenManager @Inject constructor( | ||
| @ApplicationContext private val context: Context | ||
| private val dataStore: DataStore<Preferences> | ||
| ) { | ||
| companion object { | ||
| //토큰저장에 사용되는 키 | ||
| private val APP_TOKEN_KEY = stringPreferencesKey("app_token") | ||
| private val TEMP_TOKEN_KEY = stringPreferencesKey("temp_token") | ||
| private val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token") | ||
| private val APP_TOKEN_KEY = stringPreferencesKey("app_token") // 정식 액세스토큰 | ||
| private val TEMP_TOKEN_KEY = stringPreferencesKey("temp_token") // 임시 토큰 | ||
| private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token") | ||
|
Comment on lines
+19
to
21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ 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 |
||
| } | ||
|
|
||
| //토큰 저장 | ||
| // ====== 정식 토큰 ====== | ||
| suspend fun saveToken(token: String) { | ||
| context.dataStore.edit { prefs -> | ||
| dataStore.edit { prefs -> | ||
| prefs[APP_TOKEN_KEY] = token | ||
| } | ||
| } | ||
|
|
||
| //저장된 토큰을 Flow 형태로 불러옴 | ||
| fun getToken(): Flow<String?> { | ||
| return context.dataStore.data.map { prefs -> | ||
| prefs[APP_TOKEN_KEY] | ||
| } | ||
| return dataStore.data.map { prefs -> prefs[APP_TOKEN_KEY] } | ||
| } | ||
|
|
||
| suspend fun getTokenOnce(): String? { | ||
| return dataStore.data.map { prefs -> prefs[APP_TOKEN_KEY] }.first() | ||
| } | ||
|
|
||
| //저장된 토큰 삭제 (로그아웃 시?) | ||
| suspend fun deleteToken() { | ||
| context.dataStore.edit { prefs -> | ||
| prefs.remove(APP_TOKEN_KEY) | ||
| } | ||
| dataStore.edit { prefs -> prefs.remove(APP_TOKEN_KEY) } | ||
| } | ||
|
|
||
| // 임시 토큰 저장 | ||
| // ====== 임시 토큰 ====== | ||
| suspend fun saveTempToken(token: String) { | ||
| context.dataStore.edit { prefs -> | ||
| prefs[TEMP_TOKEN_KEY] = token | ||
| } | ||
| dataStore.edit { prefs -> prefs[TEMP_TOKEN_KEY] = token } | ||
| } | ||
|
|
||
| // 임시 토큰 읽기 | ||
| suspend fun getTempToken(): String? { | ||
| return context.dataStore.data.map { prefs -> | ||
| prefs[TEMP_TOKEN_KEY] | ||
| }.first() // Flow에서 첫 번째 값을 한번만 읽어옴 | ||
| suspend fun getTempTokenOnce(): String? { | ||
| return dataStore.data.map { prefs -> prefs[TEMP_TOKEN_KEY] }.first() | ||
| } | ||
|
|
||
| // 임시 토큰 삭제 | ||
| suspend fun deleteTempToken() { | ||
| context.dataStore.edit { prefs -> | ||
| prefs.remove(TEMP_TOKEN_KEY) | ||
| } | ||
| dataStore.edit { prefs -> prefs.remove(TEMP_TOKEN_KEY) } | ||
| } | ||
|
|
||
|
|
||
| // 정식 토큰들(Access, Refresh) 저장 | ||
| suspend fun saveAccessTokens(accessToken: String, refreshToken: String) { | ||
| context.dataStore.edit { prefs -> | ||
| prefs[ACCESS_TOKEN_KEY] = accessToken | ||
| prefs[REFRESH_TOKEN_KEY] = refreshToken | ||
| } | ||
| // ====== Refresh 토큰 (추후 확장용) ====== | ||
| suspend fun saveRefreshToken(token: String) { | ||
| dataStore.edit { prefs -> prefs[REFRESH_TOKEN_KEY] = token } | ||
| } | ||
|
|
||
| // Access Token 읽기 (Flow로 제공하여 토큰 변화를 감지할 수 있게 함) | ||
| fun getAccessToken(): kotlinx.coroutines.flow.Flow<String?> { | ||
| return context.dataStore.data.map { prefs -> | ||
| prefs[ACCESS_TOKEN_KEY] | ||
| } | ||
| suspend fun getRefreshTokenOnce(): String? { | ||
| return dataStore.data.map { prefs -> prefs[REFRESH_TOKEN_KEY] }.first() | ||
| } | ||
|
|
||
| // 모든 토큰 삭제 (로그아웃 시) | ||
| suspend fun clearTokens() { | ||
| context.dataStore.edit { prefs -> | ||
| prefs.clear() | ||
| } | ||
| dataStore.edit { prefs -> prefs.clear() } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,17 @@ | ||
| package com.texthip.thip.data.model.users.response | ||
|
|
||
| import com.google.gson.annotations.SerializedName | ||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class AliasChoiceResponse( | ||
| @SerializedName("aliasChoices") val aliasChoices: List<AliasChoice> | ||
| @SerialName("aliasChoices") val aliasChoices: List<AliasChoice> | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class AliasChoice( | ||
| @SerializedName("aliasName") val aliasName: String, | ||
| @SerializedName("categoryName") val categoryName: String, | ||
| @SerializedName("imageUrl") val imageUrl: String, | ||
| @SerializedName("aliasColor") val aliasColor: String | ||
| @SerialName("aliasName") val aliasName: String, | ||
| @SerialName("categoryName") val categoryName: String, | ||
| @SerialName("imageUrl") val imageUrl: String, | ||
| @SerialName("aliasColor") val aliasColor: String | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,22 @@ | ||
| package com.texthip.thip.data.model.users.response | ||
|
|
||
| import com.google.gson.annotations.SerializedName | ||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class MyFollowingsResponse( | ||
| @SerializedName("followings") val followings: List<FollowingList>, | ||
| @SerializedName("totalFollowingCount") val totalFollowingCount: Int, | ||
| @SerializedName("nextCursor") val nextCursor: String?, | ||
| @SerializedName("isLast") val isLast: Boolean | ||
| @SerialName("followings") val followings: List<FollowingList>, | ||
| @SerialName("totalFollowingCount") val totalFollowingCount: Int, | ||
| @SerialName("nextCursor") val nextCursor: String?, | ||
| @SerialName("isLast") val isLast: Boolean | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class FollowingList( | ||
| @SerializedName("userId") val userId: Long, | ||
| @SerializedName("nickname") val nickname: String, | ||
| @SerializedName("profileImageUrl") val profileImageUrl: String?, | ||
| @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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainuserId(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 타입 불일치 재검증 스크립트 아래 명령으로 🏁 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 타입 정합성 확보 필요 전역 검토 결과 수정 대상:
제안하는 변경(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?,
)이외에도 |
||
| @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 | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,23 @@ | ||
| package com.texthip.thip.data.model.users.response | ||
|
|
||
| import com.google.gson.annotations.SerializedName | ||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class OthersFollowersResponse( | ||
| @SerializedName("followers") val followers: List<FollowerList>, | ||
| @SerializedName("totalFollowerCount") val totalFollowerCount: Int, | ||
| @SerializedName("nextCursor") val nextCursor: String?, | ||
| @SerializedName("isLast") val isLast: Boolean | ||
| @SerialName("followers") val followers: List<FollowerList>, | ||
| @SerialName("totalFollowerCount") val totalFollowerCount: Int, | ||
| @SerialName("nextCursor") val nextCursor: String?, | ||
| @SerialName("isLast") val isLast: Boolean | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class FollowerList( | ||
| @SerializedName("userId") val userId: Long, | ||
| @SerializedName("nickname") val nickname: String, | ||
| @SerializedName("profileImageUrl") val profileImageUrl: String?, | ||
| @SerializedName("aliasName") val aliasName: String, | ||
| @SerializedName("aliasColor") val aliasColor: String, | ||
| @SerializedName("followerCount") val followerCount: Int, | ||
| @SerializedName("isMyself") val isMyself: 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, | ||
| @SerialName("isMyself") val isMyself: Boolean | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,10 @@ | ||
| package com.texthip.thip.data.model.users.response | ||
|
|
||
| import com.google.gson.annotations.SerializedName | ||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class SignupResponse( | ||
| @SerializedName("accessToken") val accessToken: String, | ||
| @SerializedName("userId") val userId: Long | ||
| @SerialName("accessToken") val accessToken: String, | ||
| @SerialName("userId") val userId: Long | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,18 +1,18 @@ | ||||||||||||||||||||||||||
| package com.texthip.thip.data.model.users.response | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import com.google.gson.annotations.SerializedName | ||||||||||||||||||||||||||
| import kotlinx.serialization.Serializable | ||||||||||||||||||||||||||
| import kotlinx.serialization.SerialName | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @Serializable | ||||||||||||||||||||||||||
| data class UserSearchResponse( | ||||||||||||||||||||||||||
| val userList: List<UserItem> | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| @Serializable | ||||||||||||||||||||||||||
| data class UserItem( | ||||||||||||||||||||||||||
| @SerializedName("userId") val userId: Int, | ||||||||||||||||||||||||||
| @SerializedName("nickname") val nickname: String, | ||||||||||||||||||||||||||
| @SerializedName("profileImageUrl") val profileImageUrl: String?, | ||||||||||||||||||||||||||
| @SerializedName("aliasName") val aliasName: String, | ||||||||||||||||||||||||||
| @SerializedName("aliasColor") val aliasColor: String, | ||||||||||||||||||||||||||
| @SerializedName("followerCount") val followerCount: Int | ||||||||||||||||||||||||||
| @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 | ||||||||||||||||||||||||||
|
Comment on lines
+12
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainuserId 타입이 다른 모델과 불일치합니다(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 아래 스크립트로 🏁 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 응답에서 • 수정 대상
• 권장 수정 예시 - @SerialName("userId") val userId: Int,
+ @SerialName("userId") val userId: Long,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -115,7 +115,7 @@ class UserRepository @Inject constructor( | |||||||||||||||||||||||||
| suspend fun signup(request: SignupRequest): Result<SignupResponse?> { | ||||||||||||||||||||||||||
| Log.d("SignupDebug", "UserRepository.signup() 호출됨. 요청 닉네임: ${request.nickname}") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| val tempToken = tokenManager.getTempToken() | ||||||||||||||||||||||||||
| val tempToken = tokenManager.getTempTokenOnce() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Log.d("SignupDebug", "가져온 임시 토큰: $tempToken") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
Comment on lines
116
to
121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 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에서 평문 토큰이 로그에 그대로 노출되고 있어 보안 이슈가 발생합니다. 아래 위치를 확인 후, 민감 정보가 유출되지 않도록 즉시 수정해주세요.
제안된 수정(diff): - Log.d("SignupDebug", "가져온 임시 토큰: $tempToken")
+ Log.d("SignupDebug", "임시 토큰 존재 여부: ${!tempToken.isNullOrBlank()}")이후에도 토큰·Authorization·Bearer 등 민감 정보가 로그에 남지 않도록 전역 검색( 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
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) 카탈로그 항목 복귀(필요 시):
추가로 libs.foundation과 libs.androidx.foundation이 서로 다른 별칭으로 같은 모듈을 가리킨다면 중복 하나를 정리해 단일 소스만 유지하세요.
Also applies to: 85-85
🤖 Prompt for AI Agents