Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.foundation)
//implementation(libs.androidx.foundation)

Copy link
Copy Markdown

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) 카탈로그 항목 복귀(필요 시):

-    //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.

implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.runtime.android)
implementation(libs.kotlinx.serialization.json)
Expand All @@ -91,7 +91,7 @@ dependencies {

// Retrofit
implementation(libs.retrofit)
implementation(libs.converter.gson)
//implementation(libs.converter.gson)
implementation(libs.retrofit.kotlin.serialization.converter)

// OkHttp
Expand All @@ -102,7 +102,7 @@ dependencies {
implementation("com.kakao.sdk:v2-all:2.21.6")

// 토큰 저장을 위한 DataStore
implementation ("androidx.datastore:datastore-preferences:1.1.1")
//implementation ("androidx.datastore:datastore-preferences:1.1.1")

// 구글 로그인
implementation(platform("com.google.firebase:firebase-bom:34.1.0"))
Expand Down
25 changes: 23 additions & 2 deletions app/src/main/java/com/texthip/thip/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,36 @@ fun RootNavHost() {
startDestination = CommonRoutes.Splash
) {
// --- 인증 관련 화면들 ---
authNavigation(navController)
authNavigation(
onNavigateToLogin = {
navController.navigate(CommonRoutes.Login) {
popUpTo(CommonRoutes.Splash) { inclusive = true }
}
},
onNavigateToHome = {
navController.navigate(CommonRoutes.Main) {
popUpTo(CommonRoutes.Splash) { inclusive = true }
}
},
onNavigateToSignup = {
navController.navigate(CommonRoutes.SignupFlow)
},
onNavigateToMainAfterSignup = {
navController.navigate(CommonRoutes.Main) { // 혹은 MainGraph
popUpTo(CommonRoutes.Splash) { inclusive = true }
}
},
navController = navController
)


// --- 메인 관련 화면들 ---
composable<CommonRoutes.Main> { // MainScreen으로 가는 경로 추가
MainScreen(
onNavigateToLogin = {
navController.navigate(CommonRoutes.Login) {
// 메인 화면으로 돌아올 수 없도록 모든 화면 기록 삭제
popUpTo(navController.graph.id) {
popUpTo(CommonRoutes.Main) {
inclusive = true
}
}
Expand Down
8 changes: 6 additions & 2 deletions app/src/main/java/com/texthip/thip/ThipApplication.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.texthip.thip

import com.kakao.sdk.common.KakaoSdk
import android.app.Application
import com.kakao.sdk.common.KakaoSdk
import com.texthip.thip.data.manager.TokenManager
import dagger.hilt.android.HiltAndroidApp
import com.texthip.thip.BuildConfig
import javax.inject.Inject


@HiltAndroidApp
class ThipApplication : Application(){
@Inject
lateinit var tokenManager: TokenManager

override fun onCreate() {
super.onCreate()

Expand Down
25 changes: 25 additions & 0 deletions app/src/main/java/com/texthip/thip/data/di/DataStoreModule.kt
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
}
}
73 changes: 24 additions & 49 deletions app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

민감 토큰을 평문 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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/java

Length 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/java

Length of output: 20592


전역 userId 타입 정합성 확보 필요

전역 검토 결과 UserSearchResponse 에서만 userIdInt 로 선언하고, 나머지 모든 모델 및 네비게이션, ViewModel, 서비스 레이어에서 Long 을 사용하고 있습니다. 서버가 Long 범위의 값을 반환할 경우 파싱 오류 또는 타입 불일치 문제가 발생할 수 있으므로, 아래와 같이 UserSearchResponseuserId 타입을 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("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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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/java

Length of output: 99


아래 스크립트로 .kt 파일 전체를 확장자 기반으로 다시 스캔해 보겠습니다.


🏁 Script executed:

#!/bin/bash
# userId 타입 정의/사용 현황 점검 (확장자 기반 검색)
rg -nC2 '\b(userId)\s*:\s*(Int|Long)\b' -g '*.kt' app/src/main/java

Length of output: 20589


UserSearchResponse의 userId 타입을 Long으로 통일하세요.

검증 결과, 대부분의 도메인 모델 및 API 응답에서 userIdLong으로 정의된 반면, UserSearchResponse.UserItemInt를 사용하고 있습니다. 이로 인해 64비트 ID 처리 중 오버플로우 또는 역직렬화 오류가 발생할 수 있으므로 반드시 수정해야 합니다.

• 수정 대상

  • 파일: app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt
    • 12행: @SerialName("userId") val userId: Int,

• 권장 수정 예시

-    @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.

Suggested change
@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).

)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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/java

Length 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.

Suggested change
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()}")

Expand Down
Loading