Conversation
Walkthrough구글/카카오 소셜 로그인 흐름과 네비게이션 진입 구조를 도입했습니다. Gradle/플러그인/리포지토리 설정을 추가하고 Firebase/Google Auth, Kakao SDK, DataStore 의존성을 구성했습니다. Auth API 모델/서비스/레포지토리와 Hilt 바인딩을 추가하고, 로그인 화면·스플래시·회원가입 닉네임 화면을 NavController 기반으로 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as LoginScreen
participant VM as LoginViewModel
participant Repo as AuthRepository
participant GAuth as FirebaseAuth/Google
participant API as AuthService
UI->>VM: 구글 로그인 버튼 클릭(idToken 획득)
VM->>Repo: loginWithGoogle(idToken)
Repo->>GAuth: signInWithCredential(idToken)
GAuth-->>Repo: Firebase User(UID)
Repo->>API: POST /oauth2/users { oauth2Id = "google_<UID>" }
API-->>Repo: BaseResponse<AuthResponse>
Repo-->>VM: Result<AuthResponse?>
VM-->>UI: uiState = Success/Error
UI->>UI: isNewUser 분기 네비게이션
sequenceDiagram
participant UI as LoginScreen
participant VM as LoginViewModel
participant Repo as AuthRepository
participant Kakao as Kakao SDK
participant API as AuthService
UI->>VM: 카카오 로그인 버튼 클릭
VM->>Repo: loginWithKakao(context)
Repo->>Kakao: 로그인 및 me()로 ID 조회
Kakao-->>Repo: user.id
Repo->>API: POST /oauth2/users { oauth2Id = "kakao_<id>" }
API-->>Repo: BaseResponse<AuthResponse>
Repo-->>VM: Result<AuthResponse?>
VM-->>UI: uiState = Success/Error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ 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: 16
🔭 Outside diff range comments (3)
app/build.gradle.kts (2)
13-15: local.properties 강제 로드로 인한 빌드 실패 및 "null" 문자열 누수 가능성
- CI/클린 환경에서 local.properties가 없으면 FileNotFoundException이 발생해 빌드가 실패합니다.
- 현재 문자열 보간으로 null이 들어오면 BuildConfig 값이 문자열 "null"이 되어 런타임에서 KakaoSdk.init 등에 잘못 전달될 수 있습니다.
다음과 같이 안전하게 파일 존재 여부를 체크하고 기본값을 제공해주세요.
-val properties = Properties().apply { - load(project.rootProject.file("local.properties").inputStream()) -} +val properties = Properties().apply { + val local = project.rootProject.file("local.properties") + if (local.exists()) { + local.inputStream().use { load(it) } + } +}또한 BuildConfig/Manifest placeholder 주입 시 getProperty의 기본값을 사용하여 "null" 문자열 누수를 방지하세요.
- buildConfigField("String", "BASE_URL", "\"${properties["BASE_URL"]}\"") - buildConfigField("String", "NATIVE_APP_KEY", "\"${properties["NATIVE_APP_KEY"]}\"") - manifestPlaceholders += mapOf( - "NATIVE_APP_KEY" to properties["NATIVE_APP_KEY"] as String - ) + buildConfigField("String", "BASE_URL", "\"${properties.getProperty("BASE_URL", "")}\"") + buildConfigField("String", "NATIVE_APP_KEY", "\"${properties.getProperty("NATIVE_APP_KEY", "")}\"") + manifestPlaceholders += mapOf( + "NATIVE_APP_KEY" to properties.getProperty("NATIVE_APP_KEY", "") + )Also applies to: 30-34
84-84: Compose Foundation 중복/버전 충돌 위험: BOM 사용 시 직접 버전 핀 제거 권장Compose BOM을 사용 중인데 별도로 foundation 1.5.0을 명시하면 버전 충돌/불일치가 발생할 수 있습니다. 상단에서 platform(libs.androidx.compose.bom)을 이미 사용 중이므로 직접 버전 지정 라인은 제거하는 것이 안전합니다.
다음 라인을 제거해 주세요.
- implementation("androidx.compose.foundation:foundation:1.5.0")또한 동일/유사 의존성인
libs.androidx.foundation와libs.foundation을 둘 다 추가하고 있습니다(라인 70, 75). 실제로 하나만 남기고 정리해 중복을 해소하는 것을 권장드립니다.app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt (1)
53-56: 로그 민감정보(토큰) 노출 위험: HttpLoggingInterceptor Level.BODY를 출시 빌드에서 비활성화하고 헤더 마스킹 필요현재 모든 빌드에서 BODY 로그가 활성화되어 있어 토큰/PII 유출 위험이 큽니다. 출시 빌드에서는 NONE으로, 디버그 빌드에서만 BODY를 사용하고 Authorization/Cookie 등 민감 헤더를 마스킹하세요.
다음과 같이 수정 제안드립니다.
@Provides @Singleton fun providesLoggingInterceptor(): HttpLoggingInterceptor = - HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } + HttpLoggingInterceptor().apply { + level = if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } + redactHeader("Authorization") + redactHeader("Cookie") + }
🧹 Nitpick comments (22)
.idea/gradle.xml (1)
9-9: IDE 전용 설정(commit 지양) 및 Gradle Toolchain으로 통일 권장gradleJvm을 IDE 파일에서 jbr-21로 고정하면 팀 환경마다 불일치가 생길 수 있습니다. Gradle(Java) Toolchain을 build 스크립트(plugins/AGP toolchain)로 명시해 일관성 있게 관리하고, .idea 변경은 커밋 대상에서 제외하는 것을 권장합니다.
가능한 대응:
- Gradle에서 java.toolchain.languageVersion = JavaLanguageVersion.of(21) 설정
- .gitignore로 .idea/gradle.xml 커밋 방지
gradle.properties (1)
23-24: BuildConfig 전역 활성화의 범위 명확화android.defaults.buildfeatures.buildconfig=true는 모든 모듈에 BuildConfig를 생성합니다. 전역 필요성이 없으면 모듈별 buildFeatures { buildConfig = true }로 명시하는 편이 의도 파악과 유지보수에 유리합니다. 전역 적용이 의도라면 그대로 유지해도 무방합니다.
app/google-services.json (2)
17-27: Web OAuth Client ID가 포함됨 — client_secret 노출 금지 및 사용처 확인JSON에 Web OAuth Client ID(client_type: 3)가 포함되어 있습니다. 이는 자체로 비밀값은 아니지만, 대응되는 client_secret 파일은 절대 커밋 금지입니다. 또한 서버 사이드 검증(flow)에 실제 사용 중인지 확인해 주세요. 사용하지 않는다면 구성 최소화가 바람직합니다.
Also applies to: 36-41
21-21: SHA-1 인증서 해시 확인 필요(디버그/릴리즈 모두 등록)Line 21의 certificate_hash가 디버그 서명만 반영된 값일 수 있습니다. Firebase/Google Sign-In은 릴리즈 서명 SHA-1/256도 반드시 등록해야 실제 배포 앱에서 정상 동작합니다.
app/src/main/java/com/texthip/thip/ui/navigator/routes/CommonRoutes.kt (1)
10-17: 라우트 직렬화 이름 고정 권장(@SerialName) — 난독화/리팩터 시 안정성 확보현재 객체명 기반 기본 이름을 사용 중입니다. 난독화(ProGuard/R8)나 클래스명 변경 시 직렬화 토큰이 바뀌면 복원/딥링크/상태 저장에 영향이 있을 수 있습니다. 각 오브젝트에 @SerialName으로 명시적 토큰을 두는 것을 권장합니다.
예:
@Serializable @SerialName("splash") data object Splash : CommonRoutes() @Serializable @SerialName("login") data object Login : CommonRoutes() @Serializable @SerialName("signup") data object Signup : CommonRoutes()app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (4)
34-36: 함수명 오타(Signin ↔ Signup) 정정파일명과 화면 역할이 “SignupNickname”인데 함수명이 “SigninNicknameScreen”입니다. 일관성 유지를 위해 함수명을 “SignupNicknameScreen”으로 변경 권장합니다.
-fun SigninNicknameScreen( +fun SignupNicknameScreen( navController: NavController, ) {- SigninNicknameScreen(navController) + SignupNicknameScreen(navController)Also applies to: 104-105
34-36: NavController 의존성 하향(콜백 주입)으로 화면을 순수 UI로 유지Composable에 NavController를 직접 주입하면 재사용성과 테스트성이 떨어집니다. 상위에서 onNext 콜백을 주입하는 방식으로 디커플링을 권장합니다.
-@Composable -fun SigninNicknameScreen( - navController: NavController, -) { +@Composable +fun SignupNicknameScreen( + onNext: (nickname: String) -> Unit, +) {외부(호스트)에서:
SignupNicknameScreen( onNext = { nickname -> navController.navigate(/* 다음 라우트 */) } )
23-24: 미사용 import 주의NavController/rememberNavController를 사용하지 않는 방향(콜백 주입)으로 가면 해당 import는 제거해 주세요.
40-40: 공백만 입력되는 케이스 방지 — trim 기반 활성화 검증스페이스만 입력해도 isNotBlank가 true가 될 수 있습니다. trim으로 사용자 의도치 않은 활성화를 방지하세요.
- val isRightButtonEnabled by remember { derivedStateOf { nickname.isNotBlank() } } + val isRightButtonEnabled by remember(nickname) { + derivedStateOf { nickname.trim().isNotEmpty() } + }app/build.gradle.kts (1)
100-109: 소셜 로그인 의존성 구성 적절 + 버전 카탈로그 일원화 제안
- Kakao, Firebase BoM, Firebase Auth, Play Services Auth 추가 구성은 목적에 부합합니다.
- 장기적으로 유지보수를 위해 버전/그룹을 libs.versions.toml로 일원화(버전 카탈로그)하여 문자열 하드코딩을 줄이는 것을 권장드립니다.
app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt (1)
66-68: 불필요한 requireNotNull 제거로 가독성 개선toMediaType()은 non-null을 반환하므로 requireNotNull은 불필요합니다. 간결하게 정리해 주세요.
- .addConverterFactory( - json.asConverterFactory(requireNotNull("application/json".toMediaType())) - ) + .addConverterFactory( + json.asConverterFactory("application/json".toMediaType()) + )app/src/main/java/com/texthip/thip/ThipApplication.kt (1)
3-19: Kakao SDK 초기화 시 빈/누락 키 처리 및 로깅 방식 개선
- 빌드 스크립트의 기본값 처리에 따라 키가 빈 문자열이 될 수 있으므로, 빈 값이면 초기화를 건너뛰고 경고 로그를 남기는 편이 안전합니다.
- e.printStackTrace() 대신 Log 사용을 권장합니다(또는 Timber).
다음과 같이 수정해 주세요.
import com.kakao.sdk.common.KakaoSdk import android.app.Application import dagger.hilt.android.HiltAndroidApp -import com.texthip.thip.BuildConfig +import com.texthip.thip.BuildConfig +import android.util.Log @@ override fun onCreate() { super.onCreate() - // 카카오 SDK 초기화 - try { - KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY) - }catch (e: Exception){ - e.printStackTrace() - } + // 카카오 SDK 초기화 + val kakaoKey = BuildConfig.NATIVE_APP_KEY + if (kakaoKey.isBlank()) { + Log.w("ThipApplication", "KAKAO NATIVE_APP_KEY is empty. KakaoSdk.init skipped.") + } else { + try { + KakaoSdk.init(this, kakaoKey) + } catch (e: Exception) { + Log.e("ThipApplication", "KakaoSdk.init failed", e) + } + } }app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (1)
35-39: AuthService DI 바인딩 추가 LGTM + 표현식 본문으로 일관성 개선 제안구현 자체는 적절합니다. 기존 RoomsService와 스타일을 맞추기 위해 표현식 본문으로 통일하면 가독성이 좋아집니다.
@Provides @Singleton - fun provideAuthService(retrofit: Retrofit): AuthService { - return retrofit.create(AuthService::class.java) - } + fun provideAuthService(retrofit: Retrofit): AuthService = + retrofit.create(AuthService::class.java)app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt (1)
33-46: 스플래시 딜레이를 파라미터화하여 테스트/프리뷰 안정성 개선고정된 3초 딜레이는 테스트/프리뷰에서 불필요한 대기와 불안정성을 유발할 수 있습니다. 딜레이를 파라미터로 분리(기본값 유지)하고, Preview에서는 0으로 전달하면 UX/테스트 모두 개선됩니다.
적용 제안:
-@Composable -fun SplashScreen( - navController: NavController, -) { - LaunchedEffect(key1 = Unit) { - //3초 delay - delay(3000L) +@Composable +fun SplashScreen( + navController: NavController, + splashDelayMillis: Long = 3000L, +) { + LaunchedEffect(key1 = Unit) { + // 스플래시 딜레이 + delay(splashDelayMillis) // 로그인 화면으로 이동 navController.navigate(CommonRoutes.Login) { popUpTo(CommonRoutes.Splash) { inclusive = true } } }- SplashScreen(navController) + SplashScreen(navController, splashDelayMillis = 0L)Also applies to: 76-77
app/src/main/java/com/texthip/thip/data/service/AuthService.kt (1)
12-14: 비정상 HTTP 응답 처리 전략 점검(2xx 외 응답 케이스)현재
suspend fun이BaseResponse<AuthResponse>를 직접 반환하여, 2xx 외 응답은 예외로 흐르게 됩니다. 호출부에서 에러 본문/HTTP 코드 기반 처리(예: 서버 메시지 표시)가 필요하다면Response<BaseResponse<AuthResponse>>로 감싸는 방식을 고려해 주세요. 반대로, 예외 기반 흐름이 팀 컨벤션이라면 현재 형태도 무방합니다.선택적 변경 예시:
- suspend fun checkNewUser( - @Body request: AuthRequest - ): BaseResponse<AuthResponse> + suspend fun checkNewUser( + @Body request: AuthRequest + ): retrofit2.Response<BaseResponse<AuthResponse>>해당 변경 시 Repository 계층의 에러 처리 로직도 함께 조정되어야 합니다.
app/src/main/java/com/texthip/thip/MainActivity.kt (1)
38-41: NavHost에 Modifier.fillMaxSize 추가 제안NavHost에 크기 지정이 없으면 특정 레이아웃 상황에서 의도치 않게 공간을 차지하지 못할 수 있습니다. 전체 화면을 차지하도록 명시해 두는 편이 안전합니다.
적용 제안:
- NavHost( - navController = navController, - startDestination = CommonRoutes.Splash - ) { + NavHost( + navController = navController, + startDestination = CommonRoutes.Splash, + modifier = androidx.compose.ui.Modifier.fillMaxSize() + ) {파일 상단에 필요한 import가 없다면 다음을 추가하세요:
import androidx.compose.ui.Modifier import androidx.compose.foundation.layout.fillMaxSizeapp/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt (2)
83-89: GoogleSignInClient remember 키에 Context 포함Context에 의존하는 객체는 remember 키에 Context를 포함시켜 안전하게 재생성되도록 하는 것이 좋습니다.
- val googleSignInClient = remember { + val googleSignInClient = remember(context) { val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(context.getString(R.string.default_web_client_id)) .requestEmail() .build() GoogleSignIn.getClient(context, gso) }
104-106: 네비게이션 중복 방지 및 상태 복원 옵션 추가중복 네비게이션을 방지하고(launchSingleTop), 메인 탭 복원을 고려한다면(restoreState) 다음 옵션을 권장합니다.
- navController.navigate(destination) { - popUpTo(CommonRoutes.Login) { inclusive = true } - } + navController.navigate(destination) { + popUpTo(CommonRoutes.Login) { inclusive = true } + launchSingleTop = true + restoreState = true + }app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt (1)
26-31: 인증 토큰은 평문 Preferences 대신 암호화 저장을 고려하세요.현재 Preferences DataStore는 암호화되지 않습니다. 세션/액세스 토큰을 저장할 경우 루팅 환경 등에서 평문 노출 위험이 있습니다. Jetpack Security Crypto를 이용한 EncryptedSharedPreferences 또는 암호화 Serializer가 적용된 Proto DataStore로 전환을 권장합니다.
예시: EncryptedSharedPreferences 사용
val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() val prefs = EncryptedSharedPreferences.create( context, "thip_secure_prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) // 저장 prefs.edit().putString("app_token", token).apply() // 읽기 val token = prefs.getString("app_token", null) // 삭제 prefs.edit().remove("app_token").apply()혹은 Proto DataStore + custom Serializer에 Tink/crypto를 적용해 암호화하는 방식도 가능합니다(장기적으로 권장).
Also applies to: 41-45
app/src/main/java/com/texthip/thip/data/repository/AuthRepository.kt (2)
78-85: 널 단정 연산자(!!) 제거로 안전성 향상.이미 null 체크가 끝났으므로
user.id!!대신 안전한user.id를 그대로 사용하세요.- } else if (user?.id != null) { - continuation.resume(user.id!!) + } else if (user?.id != null) { + continuation.resume(user.id)
37-49: FirebaseAuth 의존성 주입으로 테스트 용이성과 모듈성 개선 제안.
Firebase.auth를 직접 참조하면 유닛테스트/목킹이 어려워집니다.FirebaseAuth를 생성자 주입하거나, 인증 공급자를 래핑한 인터페이스를 도입하면 테스트와 대체 구현(예: emulator, fake) 적용이 쉬워집니다.app/src/main/java/com/texthip/thip/ui/signin/viewmodel/KakaoLoginViewModel.kt (1)
1-1: 파일명-클래스명 일치 제안.파일명이
KakaoLoginViewModel.kt인데 클래스는LoginViewModel이며 구글/카카오를 모두 다룹니다.LoginViewModel.kt로 파일명을 정리하면 검색성과 일관성이 좋아집니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (23)
.idea/gradle.xml(1 hunks)app/build.gradle.kts(4 hunks)app/google-services.json(1 hunks)app/src/main/AndroidManifest.xml(2 hunks)app/src/main/java/com/texthip/thip/MainActivity.kt(2 hunks)app/src/main/java/com/texthip/thip/ThipApplication.kt(1 hunks)app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt(1 hunks)app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt(2 hunks)app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/auth/request/AuthRequest.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/auth/response/AuthResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/repository/AuthRepository.kt(1 hunks)app/src/main/java/com/texthip/thip/data/service/AuthService.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/navigator/routes/CommonRoutes.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/signin/viewmodel/KakaoLoginViewModel.kt(1 hunks)app/src/main/res/values/strings.xml(1 hunks)build.gradle.kts(1 hunks)gradle.properties(1 hunks)gradle/libs.versions.toml(2 hunks)settings.gradle.kts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/src/main/java/com/texthip/thip/MainActivity.kt (7)
app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt (1)
SplashScreen(32-71)app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt (1)
LoginScreen(54-134)app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (1)
feedNavigation(10-23)app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (1)
groupNavigation(39-217)app/src/main/java/com/texthip/thip/ui/navigator/navigations/SearchNavigation.kt (1)
searchNavigation(9-13)app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt (1)
myPageNavigation(10-19)app/src/main/java/com/texthip/thip/ui/navigator/navigations/CommonNavigation.kt (1)
commonNavigation(14-29)
app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt (1)
app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
ThipTheme(41-69)
🪛 Gitleaks (8.27.2)
app/src/main/java/com/texthip/thip/ui/signin/viewmodel/KakaoLoginViewModel.kt
51-51: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
🔇 Additional comments (10)
build.gradle.kts (1)
7-7: Google Services 플러그인 버전 확인 결과
- 최신 안정 버전: 4.4.3 (2025-06-26), 현재 build.gradle.kts에 적용된 버전과 일치합니다.
- AGP 호환성: com.google.gms:google-services 4.4.3은 AGP 7.3.0 이상(8.x까지)에서 정상 동작합니다.
• 프로젝트가 사용 중인 AGP 버전(예:com.android.tools.build:gradle의 버전)이 7.3.0 이상인지 다시 한번 확인해주세요.- 권장: pluginManagement 블록 또는 versions catalog를 활용해 플러그인 버전 관리를 중앙집중화하는 방안을 검토해 보세요.
app/build.gradle.kts (2)
9-9: Google Services 플러그인 적용 위치 적절앱 모듈에 com.google.gms.google-services 플러그인을 적용한 구성이 적합합니다. 상위 빌드에서 classpath가 추가되어 있다면 Firebase 설정과 함께 정상 동작할 것입니다.
76-76: lifecycle-runtime-compose 알파 채택: Compose BOM과의 호환성 점검 권장알파 버전(2.10.0-alpha01)을 사용하고 있어 다른 Compose/AndroidX 버전과의 호환성 이슈가 생길 수 있습니다. 팀 기준에 따라 안정 채널로 맞추거나, 최소한 현재 BOM 및 Kotlin 버전과의 호환 여부를 확인해 주세요.
app/src/main/java/com/texthip/thip/data/model/auth/response/AuthResponse.kt (1)
6-10: 직렬화 모델 정의 적절필드 매핑과 null-비허용 설계가 현재 서버 응답(토큰 발급, 신규 여부 플래그)과 부합합니다. NetworkModule의 Json(ignoreUnknownKeys=true) 설정과도 호환됩니다.
app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt (1)
41-45: 스택 정리 방식은 적절합니다Splash → Login 전환 시 popUpTo(inclusive=true)로 스플래시를 백스택에서 제거하는 접근은 올바릅니다. 이후 Login → Main 전환에서도 동일한 원칙을 적용한 점과 일관됩니다.
app/src/main/java/com/texthip/thip/MainActivity.kt (1)
43-51: 인증 플로우 그래프 구성 LGTM타입 안전 네비게이션을 사용하여 Splash → Login → Signup 플로우를 구성했고, 각 화면에서 NavController를 주입받는 구조가 명확합니다. 시작 목적지 설정도 적절합니다.
app/src/main/AndroidManifest.xml (3)
19-35: Kakao 콜백 액티비티/메타데이터 설정 적절카카오 SDK AppKey 메타데이터 및
AuthCodeHandlerActivity의 인텐트 필터/스킴 설정이 적절합니다.android:exported="true"도 OAuth 콜백 처리에 필요합니다.
5-5: INTERNET 권한 추가 OK네트워크 호출이 있으므로 INTERNET 권한 추가는 필수이며 적절합니다.
19-22: NATIVE_APP_KEY manifestPlaceholders 매핑 확인 및 치환 보장
- app/build.gradle.kts (32–33):
manifestPlaceholders에"NATIVE_APP_KEY"→properties["NATIVE_APP_KEY"]매핑이 정상 설정되어 있습니다.- AndroidManifest.xml (19–22, 30–33):
${NATIVE_APP_KEY}가com.kakao.sdk.AppKey메타데이터 및kakao${NATIVE_APP_KEY}스킴에 올바르게 치환됩니다.- ThipApplication.kt:
BuildConfig.NATIVE_APP_KEY를 이용한KakaoSdk.init초기화에도 문제가 없습니다.위 설정으로 빌드 실패 우려가 해소되었으므로, 추가 검증이나 조치가 필요하지 않습니다.
app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt (1)
118-123: UI-로직 분리와 상태 기반 사이드이펙트 처리 적절UI는
LoginContent로 분리하고, 로그인 결과/에러에 따른 사이드 이펙트를LaunchedEffect(uiState)로 처리하는 구조는 명확하고 Compose 친화적입니다.
| "api_key": [ | ||
| { | ||
| "current_key": "AIzaSyCFzpbeT-8JQCGxiSKuVXMvcNNYFtl6Fuo" | ||
| } | ||
| ], |
There was a problem hiding this comment.
💡 Verification agent
❓ Verification inconclusive
공개 저장소에 Firebase API Key가 그대로 커밋됨 — 키 제한 및 회수(rotate) 필요
이 파일은 보통 커밋되기도 하지만, 레포가 공개(public)라면 남용 위험이 있습니다. 최소한 다음 조치가 필요합니다:
- GCP 콘솔에서 해당 API Key에 Android 앱 제한(패키지명 + SHA-1/256) 강제.
- 불필요한 API 사용권한 제거(키 제한).
- 이미 공개됐다면 키 회수(rotate) 후 새 키로 교체.
- release용 google-services.json은 CI에서만 주입하고, 레포에는 debug용만 커밋(예: src/debug/google-services.json, src/release는 CI 시크릿 사용).
원하시면 CI에서 flavor별 google-services.json을 주입하도록 스크립트/가이드를 드리겠습니다.
긴급: 공개 저장소에 노출된 Firebase API Key 제한 및 회수 필요
app/google-services.json (29-33행)에 아래와 같은 API Key가 그대로 커밋되어 있습니다. 노출된 키는 악용될 위험이 있으므로 즉시 조치해 주세요:
- GCP 콘솔에서 해당 API Key에 Android 앱 제한(패키지명 + SHA-1/256) 설정
- 사용하지 않는 API 권한 제거
- 이미 공개된 키는 회수(rotate) 후 신규 키로 교체
- release용 google-services.json은 CI에서만 주입하고, 저장소에는 debug용만 커밋 (예: src/debug/google-services.json, release는 CI 시크릿 사용)
필요하시면 CI에서 flavor별 json을 주입하는 스크립트/가이드를 제공해 드리겠습니다.
🤖 Prompt for AI Agents
In app/google-services.json around lines 29-33 an exposed Firebase/GCP API key
was committed; restrict and rotate it immediately: in GCP Console add Android
app restrictions (package name + SHA-1/256) and remove any unnecessary API
permissions, then revoke (rotate) the compromised key and replace it with the
new key in your local config; update the repo to remove the release
google-services.json (keep only a debug/test file, e.g., move release json out
of repo to src/debug/google-services.json if needed), and change CI to inject
the release google-services.json from secrets during build (ensure CI uses the
new rotated key and that no production keys remain in commits).
| class TokenManager @Inject constructor( | ||
| @ApplicationContext private val context: Context | ||
| ) { | ||
| private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "thip_auth_tokens") |
There was a problem hiding this comment.
🛠️ Refactor suggestion
DataStore 위임 프로퍼티는 파일 top-level로 옮기세요 (싱글 인스턴스 보장 및 메모리 누수 방지).
Jetpack DataStore 권장사항은 Context 확장 위임 프로퍼티를 파일 최상위(top-level)에 두는 것입니다. 현재 클래스 내부에 선언되어 있어도 싱글톤일 때는 동작하지만, 유지보수성과 안전성 측면에서 top-level로 이동하는 것이 좋습니다.
아래 diff로 클래스 내부 사용처를 context.authDataStore로 변경하고, 내부 위임 프로퍼티를 제거해 주세요:
- private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "thip_auth_tokens")
+ // moved to top-level: use `context.authDataStore` instead
@@
- context.dataStore.edit { prefs ->
+ context.authDataStore.edit { prefs ->
prefs[APP_TOKEN_KEY] = token
}
@@
- return context.dataStore.data.map { prefs ->
+ return context.authDataStore.data.map { prefs ->
prefs[APP_TOKEN_KEY]
}
@@
- context.dataStore.edit { prefs ->
+ context.authDataStore.edit { prefs ->
prefs.remove(APP_TOKEN_KEY)
}클래스 외부(파일 상단)에 아래 top-level 확장 프로퍼티를 추가하세요:
// 파일 상단 (클래스 외부)
val Context.authDataStore: DataStore<Preferences> by preferencesDataStore(name = "thip_auth_tokens")Also applies to: 28-31, 35-37, 42-44
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt around lines
19 (and also apply same change for usages at 28-31, 35-37, 42-44), the
Context.dataStore delegate is declared inside the class; move this DataStore
extension delegate to the file top-level to guarantee a single instance and
avoid leaks. Remove the in-class delegated property and add a top-level
extension val Context.authDataStore: DataStore<Preferences> by
preferencesDataStore(name = "thip_auth_tokens") at the file head, then update
all in-class usages to reference context.authDataStore instead of the removed
property.
| //저장된 토큰을 Flow 형태로 불러옴 | ||
| fun getToken(): Flow<String?> { | ||
| return context.dataStore.data.map { prefs -> | ||
| prefs[APP_TOKEN_KEY] | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
DataStore 읽기 시 IOException 처리를 추가해 앱 크래시를 방지하세요.
DataStore의 data Flow는 I/O 오류 시 예외를 내보낼 수 있습니다. catch로 IOException을 처리해 빈 Preferences를 emit하도록 하는 것이 권장됩니다.
- fun getToken(): Flow<String?> {
- return context.dataStore.data.map { prefs ->
- prefs[APP_TOKEN_KEY]
- }
- }
+ fun getToken(): Flow<String?> {
+ return context.authDataStore.data
+ .catch { e ->
+ if (e is IOException) emit(emptyPreferences()) else throw e
+ }
+ .map { prefs -> prefs[APP_TOKEN_KEY] }
+ }추가 import가 필요합니다(파일 import 섹션에 추가):
import androidx.datastore.preferences.core.emptyPreferences
import kotlinx.coroutines.flow.catch
import java.io.IOException🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt around lines
33 to 38, the DataStore data Flow isn't handling IOExceptions which can crash
the app; wrap the data Flow with catch to intercept IOException and emit
emptyPreferences(), then continue mapping to APP_TOKEN_KEY; also add the
suggested imports: androidx.datastore.preferences.core.emptyPreferences,
kotlinx.coroutines.flow.catch, and java.io.IOException.
| @Serializable | ||
| data class AuthRequest( | ||
| @SerialName("oauth2Id") val oauth2Id: String | ||
| ) No newline at end of file |
There was a problem hiding this comment.
UID 단독 전송은 위변조 위험 — 서버 검증용 토큰(idToken/serverAuthCode) 포함 권장
현재 요청 스키마가 oauth2Id(예: “google_”)만 담습니다. 클라이언트가 임의 UID를 조작해 전송할 수 있어, 서버는 사용자 신원을 검증할 수 없습니다. 최소 한 가지 검증 가능한 증거를 추가하세요:
- Google: ID Token(JWT) 또는 Server Auth Code 전달 → 서버에서 구글/파이어베이스로 검증
- Kakao: Access Token 또는 ID Token 전달 → 서버 검증
예시 스키마 제안:
@Serializable
data class AuthRequest(
- @SerialName("oauth2Id") val oauth2Id: String
+ @SerialName("oauth2Id") val oauth2Id: String,
+ @SerialName("provider") val provider: String, // "google" | "kakao" 등
+ @SerialName("idToken") val idToken: String? = null,
+ @SerialName("serverAuthCode") val serverAuthCode: String? = null
)서버가 검증을 수행하지 않으면 인증 우회 취약점이 됩니다.
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/model/auth/request/AuthRequest.kt
around lines 5-8, the DTO currently only contains oauth2Id which allows
client-side UID spoofing; add one or more verifiable token fields (e.g.,
idToken: String? and/or serverAuthCode: String? and/or accessToken: String?) to
the data class and annotate them for serialization so the server can receive
provider-issued proof, make at least one token required for authentication flows
(or validate presence at deserialization/validation layer), and update any
calling code/serialization tests to send and handle the new token field(s) so
the backend can verify the token with the provider before trusting oauth2Id.
| //카카오 SDK 로그인 수행 -> 유저ID를 반환하는 suspend 메서드 | ||
| private suspend fun getKakaoUserId(context: Context): Long = suspendCancellableCoroutine { continuation -> | ||
| // 카카오톡 설치 여부에 따라 로그인 방식 결정 | ||
| if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { | ||
| // 카카오톡으로 로그인 | ||
| UserApiClient.instance.loginWithKakaoTalk(context) { token, error -> | ||
| if (error != null) { | ||
| continuation.resumeWithException(error) | ||
| } else if (token != null) { | ||
| // 로그인 성공 시 사용자 정보 요청 | ||
| fetchUserInfo(continuation) | ||
| } | ||
| } | ||
| } else { | ||
| // 카카오계정으로 로그인 | ||
| UserApiClient.instance.loginWithKakaoAccount(context) { token, error -> | ||
| if (error != null) { | ||
| continuation.resumeWithException(error) | ||
| } else if (token != null) { | ||
| // 로그인 성공 시 사용자 정보 요청 | ||
| fetchUserInfo(continuation) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
카카오 로그인: KakaoTalk 실패 시 계정 로그인으로 폴백, ‘사용자 취소’는 조용히 취소로 처리하세요.
현재 KakaoTalk 로그인 실패 시 즉시 예외로 종료합니다. 권장 패턴은 Talk 실패(취소 제외) 시 계정 로그인으로 자동 폴백하고, 사용자가 취소한 경우에는 에러가 아닌 취소로 처리하는 것입니다.
- if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
- // 카카오톡으로 로그인
- UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
- if (error != null) {
- continuation.resumeWithException(error)
- } else if (token != null) {
- // 로그인 성공 시 사용자 정보 요청
- fetchUserInfo(continuation)
- }
- }
- } else {
- // 카카오계정으로 로그인
- UserApiClient.instance.loginWithKakaoAccount(context) { token, error ->
- if (error != null) {
- continuation.resumeWithException(error)
- } else if (token != null) {
- // 로그인 성공 시 사용자 정보 요청
- fetchUserInfo(continuation)
- }
- }
- }
+ if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
+ // 카카오톡으로 로그인
+ UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
+ when {
+ error != null -> {
+ // 사용자가 취소한 경우: 조용히 취소로 처리
+ if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
+ continuation.cancel(CancellationException("KakaoTalk login cancelled"))
+ } else {
+ // 기타 오류: 카카오계정 로그인으로 폴백
+ UserApiClient.instance.loginWithKakaoAccount(context) { token2, error2 ->
+ when {
+ error2 != null -> continuation.resumeWithException(error2)
+ token2 != null -> fetchUserInfo(continuation)
+ else -> continuation.resumeWithException(
+ IllegalStateException("KakaoAccount login returned neither token nor error")
+ )
+ }
+ }
+ }
+ }
+ token != null -> fetchUserInfo(continuation)
+ else -> continuation.resumeWithException(
+ IllegalStateException("KakaoTalk login returned neither token nor error")
+ )
+ }
+ }
+ } else {
+ // 카카오계정으로 로그인
+ UserApiClient.instance.loginWithKakaoAccount(context) { token, error ->
+ when {
+ error != null -> continuation.resumeWithException(error)
+ token != null -> fetchUserInfo(continuation)
+ else -> continuation.resumeWithException(
+ IllegalStateException("KakaoAccount login returned neither token nor error")
+ )
+ }
+ }
+ }필요한 import를 추가하세요:
import com.kakao.sdk.common.model.ClientError
import com.kakao.sdk.common.model.ClientErrorCause
import kotlinx.coroutines.CancellationException🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/repository/AuthRepository.kt around
lines 52 to 76, the KakaoTalk login branch currently treats any error as a hard
failure; change it so that if the error is a ClientError with cause
ClientErrorCause.Cancelled (user cancelled) you treat it as a silent
cancellation (cancel the continuation or resume with a CancellationException),
otherwise on non-cancel errors you fall back to calling loginWithKakaoAccount
and only resumeWithException if that also fails; add the suggested imports
(com.kakao.sdk.common.model.ClientError,
com.kakao.sdk.common.model.ClientErrorCause,
kotlinx.coroutines.CancellationException) and apply the same cancellation vs
fallback logic symmetrically in both the KakaoTalk and KakaoAccount callbacks so
user cancel is not surfaced as an exception while other errors trigger fallback
or final exception.
| /* //신규유저 아닌 경우 피드화면으로 테스트 | ||
| val fakeToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjcsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.5CrcGkff5rcwF25qSsw8BY_GZ4W9w7QMJ6kXlwW4Ub0" | ||
| val fakeResponse = AuthResponse(token = fakeToken, isNewUser = false) | ||
| _uiState.update { LoginUiState.Success(fakeResponse) } | ||
| */ |
There was a problem hiding this comment.
주석에 하드코딩된 JWT 제거 (Gitleaks 경고).
정적 분석(Gitleaks)에서 JWT가 노출된 것으로 탐지되었습니다. 비록 주석이지만 레포지토리 히스토리에 남아 스캐너에 지속적으로 걸릴 수 있습니다. 즉시 제거하고 필요 시 시뮬레이션은 테스트/로컬 리소스로 분리하세요.
- /* //신규유저 아닌 경우 피드화면으로 테스트
- val fakeToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjcsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.5CrcGkff5rcwF25qSsw8BY_GZ4W9w7QMJ6kXlwW4Ub0"
- val fakeResponse = AuthResponse(token = fakeToken, isNewUser = false)
- _uiState.update { LoginUiState.Success(fakeResponse) }
-*/📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /* //신규유저 아닌 경우 피드화면으로 테스트 | |
| val fakeToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjcsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.5CrcGkff5rcwF25qSsw8BY_GZ4W9w7QMJ6kXlwW4Ub0" | |
| val fakeResponse = AuthResponse(token = fakeToken, isNewUser = false) | |
| _uiState.update { LoginUiState.Success(fakeResponse) } | |
| */ |
🧰 Tools
🪛 Gitleaks (8.27.2)
51-51: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/signin/viewmodel/KakaoLoginViewModel.kt
around lines 50-54, remove the commented-out hardcoded JWT and any related fake
token/response lines to eliminate secrets from the codebase; if you need to
simulate login for local testing, move the fake token/response into test
resources or mock objects (unit/instrumentation tests) or generate tokens from a
test-only helper, and ensure no secret values are left in comments or committed
files.
| .onFailure { throwable -> | ||
| Log.e("LoginViewModel", "Login failed: ${throwable.message}", throwable) | ||
| _uiState.update { | ||
| LoginUiState.Error(throwable.message ?: "알 수 없는 통신 오류가 발생했습니다.") | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
사용자 ‘취소’와 실제 실패를 구분해 UX 개선 (구글).
구글 로그인 흐름에서도 동일하게 취소 예외를 분기 처리하세요.
- .onFailure { throwable ->
- Log.e("LoginViewModel", "Login failed: ${throwable.message}", throwable)
- _uiState.update {
- LoginUiState.Error(throwable.message ?: "알 수 없는 통신 오류가 발생했습니다.")
- }
- }
+ .onFailure { throwable ->
+ if (throwable is CancellationException) {
+ _uiState.update { LoginUiState.Idle }
+ return@onFailure
+ }
+ Log.e("LoginViewModel", "Login failed: ${throwable.message}", throwable)
+ _uiState.update {
+ LoginUiState.Error(throwable.message ?: "알 수 없는 통신 오류가 발생했습니다.")
+ }
+ }🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/signin/viewmodel/KakaoLoginViewModel.kt
around lines 71-76, the failure block treats all exceptions as errors; update it
to distinguish user-cancellation from real failures: check the throwable for
cancellation (e.g., specific cancellation exception/type or status code used by
the login SDK — for Google that would be ApiException with a cancelled status —
and for Kakao the SDK's cancellation indicator), and if it represents a user
cancel set _uiState to a dedicated cancellation state (or a neutral state)
instead of LoginUiState.Error; otherwise keep logging and setting the Error
state. Ensure the Log still records the throwable in both branches.
| <!-- Google login constants --> | ||
| <string name="default_web_client_id" translatable="false">353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com</string> | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
default_web_client_id는 수동 정의 지양(google-services 플러그인 자동 주입 권장)
google-services 플러그인이 google-services.json을 기반으로 default_web_client_id를 strings.xml에 자동 생성합니다. 수동으로 추가하면 실제 json과 불일치하여 Google Sign-In/Firebase Auth 실패가 발생할 수 있습니다. 자동 주입에 맡기거나, 최소한 현재 값이 google-services.json과 일치하는지 확인하세요.
권장 수정:
- <!-- Google login constants -->
- <string name="default_web_client_id" translatable="false">353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com</string>동일성 검증 스크립트:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) strings.xml에서 default_web_client_id 추출
STRINGS_FILE=$(fd -a 'strings.xml' app/src/main/res/values | head -n1)
if [[ -z "${STRINGS_FILE:-}" ]]; then
echo "strings.xml not found" >&2
exit 1
fi
DECLARED_ID=$(rg -n '<string name="default_web_client_id"[^>]*>([^<]+)</string>' "$STRINGS_FILE" -or '$1' | head -n1)
if [[ -z "${DECLARED_ID:-}" ]]; then
echo "No default_web_client_id declared in strings.xml" >&2
# 수동 선언이 없다면 자동 주입 사용으로 간주
exit 0
fi
# 2) google-services.json에서 oauth_client.client_id 목록 추출
GS_FILE=$(fd -a 'google-services.json' app | head -n1)
if [[ -z "${GS_FILE:-}" ]]; then
echo "google-services.json not found under app/" >&2
exit 2
fi
IDS=$(jq -r '..|.oauth_client? // empty | .[]?.client_id? // empty' "$GS_FILE" | sort -u)
if [[ -z "${IDS:-}" ]]; then
echo "No oauth_client.client_id entries found in google-services.json" >&2
exit 3
fi
echo "Declared in strings.xml: $DECLARED_ID"
echo "client_ids in google-services.json:"
echo "$IDS" | sed 's/^/ - /'
# 3) 일치 여부 확인
if echo "$IDS" | grep -qx "$DECLARED_ID"; then
echo "OK: strings.xml default_web_client_id matches google-services.json"
exit 0
else
echo "MISMATCH: strings.xml default_web_client_id does not match any client_id in google-services.json" >&2
exit 4
fiLength of output: 2196
default_web_client_id 수동 정의 제거 및 Google-services 플러그인 자동 주입 사용
스크립트 검증 결과, strings.xml에 수동으로 선언된 default_web_client_id 값 앞에 378: 라인 번호가 포함되어 있어
google-services.json의 oauth_client.client_id 목록과 일치하지 않습니다. 이 불일치는 Google Sign-In/Firebase Auth 실패로 이어질 수 있습니다.
수정이 필요한 위치:
- 파일:
app/src/main/res/values/strings.xml - 라인: 377–379
수정 제안:
- <!-- Google login constants -->
- <string name="default_web_client_id" translatable="false">353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com</string>위 수동 선언을 삭제하고, 빌드 시 google-services 플러그인이 google-services.json 기반으로 자동 생성하는 default_web_client_id를 사용하세요.
정상 동작 여부는 플러그인 주입 후 생성된 strings.xml 내용을 재확인해 주세요.
📝 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.
| <!-- Google login constants --> | |
| <string name="default_web_client_id" translatable="false">353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com</string> |
🤖 Prompt for AI Agents
In app/src/main/res/values/strings.xml around lines 377–379, remove the manually
defined <string name="default_web_client_id"> entry (the entire line currently
containing the client ID) so the value is not hard-coded; ensure the Google
Services Gradle plugin is applied in the app build
(com.google.gms.google-services) and let the plugin inject default_web_client_id
from google-services.json during build, then rebuild and verify the generated
strings.xml contains the correct default_web_client_id.
| retrofit = "3.0.0" | ||
| retrofitKotlinSerializationConverter = "1.0.0" | ||
| androidxComposeNavigation = "2.8.2" | ||
| lifecycleRuntimeCompose = "2.10.0-alpha01" |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
alpha 버전 도입 주의 — 생태계 버전 정렬(안정 버전 2.9.1 권장)
lifecycle-runtime-compose를 2.10.0-alpha01로 올리면 나머지 Lifecycle/Compose 의존성과 호환성 이슈가 생길 수 있습니다(현재 viewmodel-compose=2.9.1, runtime-ktx=2.8.7, Compose BOM=2024.09.00). 우선 안정 버전으로 정렬하는 것을 권장합니다.
-lifecycleRuntimeCompose = "2.10.0-alpha01"
+lifecycleRuntimeCompose = "2.9.1"필요 시 최신 매트릭스 확인 후 올리는 것이 안전합니다.
다음 쿼리로 최신 호환성 정보를 확인해 주세요:
Also applies to: 62-62
🌐 Web query:
"androidx.lifecycle:lifecycle-runtime-compose 2.10.0-alpha01"가 Compose BOM 2024.09.00 및 lifecycle-viewmodel-compose 2.9.1과 호환되는가?
💡 Result:
간단히 답변하면: 아니요 — 호환성이 보장되지 않습니다.
이유(요약)
- androidx.lifecycle:lifecycle-runtime-compose:2.10.0-alpha01은 Compose runtime 1.9.0-beta03에 의존합니다. (mvnrepository.com)
- Compose BOM 2024.09.00은 Compose 라이브러들에 대해 주로 1.7.x (예: runtime 1.7.0)을 관리합니다. 즉 BOM이 제공하는 Compose 버전은 lifecycle-runtime-compose 2.10.0-alpha01이 기대하는 1.9.x 베타보다 낮습니다. (mvnrepository.com)
- lifecycle-viewmodel-compose:2.9.1은 Compose runtime 1.7.8에 의존합니다(2.9.1의 런타임 의존성). 따라서 2.10.0-alpha01(=compose-runtime 1.9.x 기대)과 2.9.1(=compose-runtime 1.7.x 기대)을 섞으면 Compose 런타임 버전 충돌이 발생할 가능성이 큽니다. (mvnrepository.com)
권장사항
- 안정적으로 가려면 같은 연쇄(페어) 버전을 맞추세요: lifecycle-runtime-compose도 2.9.x (예: 2.9.1 또는 2.9.2)로 맞추거나, Compose BOM을 lifecycle-runtime-compose가 요구하는 Compose 1.9.x 베타를 포함하는 BOM(또는 Compose 라이브러리 직접 버전 지정)으로 올리세요.
- 2.10.0-alpha01은 “alpha”이므로 프로덕션에서는 주의가 필요합니다(호환성/버그 가능성).
- 실제로는 gradle의 의존성 해결 결과(resolved version)를 gradle dependencyInsight 또는 ./gradlew app:dependencies로 확인하고, 런타임 테스트해 보세요.
원문 소스(참고)
- lifecycle-runtime-compose:2.10.0-alpha01 (런타임 의존성: compose-runtime 1.9.0-beta03). (mvnrepository.com)
- compose-bom:2024.09.00 (관리되는 Compose 버전 목록 — runtime 1.7.x 등). (mvnrepository.com)
- lifecycle-viewmodel-compose:2.9.1 (런타임 의존성: compose-runtime 1.7.8). (mvnrepository.com)
원하시면 현재 프로젝트의 Gradle 의존성 출력(resolved versions) 결과를 붙여주시면, 어떤 버전을 고정/오버라이드해야 할지 구체적으로 추천해 드리겠습니다.
Citations:
- 1: https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-runtime-compose/2.10.0-alpha01
- 2: https://mvnrepository.com/artifact/androidx.compose/compose-bom/2024.09.00
- 3: https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-viewmodel-compose/2.9.1
- 4: https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-runtime-compose/2.10.0-alpha01
- 5: https://mvnrepository.com/artifact/androidx.compose/compose-bom/2024.09.00
- 6: https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-viewmodel-compose/2.9.1
라이프사이클 Compose 의존성 버전 동기화 필요
2.10.0-alpha01은 Compose BOM 2024.09.00 및 lifecycle-viewmodel-compose:2.9.1이 기대하는 Compose 런타임(1.7.x 계열)과 호환되지 않습니다. 프로덕션 안정성을 위해 동일한 2.9.x 계열로 맞추거나, BOM을 1.9.x 베타를 포함하는 최신 매트릭스로 올려야 합니다.
해결 방안:
- gradle/libs.versions.toml의 lifecycleRuntimeCompose를 2.9.1(또는 2.9.2)로 내리기
- 또는 Compose BOM을 lifecycle-runtime-compose 2.10.0-alpha01이 요구하는 Compose-runtime 1.9.x 베타를 포함하는 버전으로 업그레이드
수정 위치:
- File: gradle/libs.versions.toml
Line: 28
Diff 제안:
-lifecycleRuntimeCompose = "2.10.0-alpha01"
+lifecycleRuntimeCompose = "2.9.1"추가 검증:
./gradlew :app:dependencies또는dependencyInsight로 실제 해석된 Compose 런타임 버전을 확인하세요.
📝 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.
| lifecycleRuntimeCompose = "2.10.0-alpha01" | |
| lifecycleRuntimeCompose = "2.9.1" |
🤖 Prompt for AI Agents
In gradle/libs.versions.toml around line 28, lifecycleRuntimeCompose is set to
2.10.0-alpha01 which is incompatible with the Compose BOM and
lifecycle-viewmodel-compose (expects Compose runtime 1.7.x/1.8.x); change
lifecycleRuntimeCompose to a stable 2.9.x version (e.g., 2.9.1 or 2.9.2) OR
upgrade the Compose BOM to a version that includes Compose-runtime 1.9.x beta to
match 2.10.0-alpha01, then run ./gradlew :app:dependencies or use
dependencyInsight to verify the resolved Compose runtime version.
| maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Kakao 전용 저장소에 content filter 적용 권장(공급망 리스크 축소)
제3자 Maven 저장소 추가는 공급망 위험(의도치 않은 그룹/좌표 해석)을 증가시킵니다. 해당 저장소를 Kakao 그룹으로 제한해 해석 범위를 축소하세요.
권장 수정:
- maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/")
+ maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") {
+ content {
+ includeGroupByRegex("com\\.kakao.*")
+ }
+ }🤖 Prompt for AI Agents
In settings.gradle.kts around lines 19-20, the maven repository declaration for
"https://devrepo.kakao.com/nexus/content/groups/public/" lacks a content filter
which increases supply-chain risk; restrict artifact resolution to Kakao
packages by adding a content block (e.g., includeGroup or includeGroupByRegex
for the Kakao groupId like "com.kakao" or the appropriate org prefix) to the
repository definition so only artifacts from the Kakao group are resolved from
this repo.
There was a problem hiding this comment.
Actionable comments posted: 11
🔭 Outside diff range comments (4)
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (1)
55-67: 중복 탭/다중 실행 방지: 로딩 상태 플래그 추가 권장현재 onRightClick이 비동기 실행 전에 버튼을 비활성화하지 않아, 빠른 연타 시 중복 요청(및 중복 네비게이션) 가능성이 있습니다. 간단한 로딩 상태를 추가해 방지하세요.
@@ - val coroutineScope = rememberCoroutineScope() + val coroutineScope = rememberCoroutineScope() + var isSubmitting by rememberSaveable { mutableStateOf(false) } @@ - isRightButtonEnabled = isRightButtonEnabled, + isRightButtonEnabled = isRightButtonEnabled && !isSubmitting, @@ - onRightClick = { + onRightClick = { + if (isSubmitting) return@InputTopAppBar + isSubmitting = true //TODO 서버 연동시 로직 변경 필요 - coroutineScope.launch { - delay(500) // 서버 응답 시뮬레이션 - if (nickname == "test") { - showWarning = true - warningMessageResId = R.string.nickname_warning - } else { - showWarning = false - warningMessageResId = null - // 다음 페이지로 이동 - } - } + coroutineScope.launch { + try { + delay(500) // 서버 응답 시뮬레이션 + if (nickname == "test") { + showWarning = true + warningMessageResId = R.string.nickname_warning + } else { + showWarning = false + warningMessageResId = null + // TODO: 실제 목적지로 네비게이션 연결 + // navController.navigate(CommonRoutes.XXX) + } + } finally { + isSubmitting = false + } + } }app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt (1)
54-56: 로그 레벨을 빌드 타입에 따라 분기하여 민감정보 노출 방지현재 BODY 로그는 토큰/헤더/페이로드를 모두 출력합니다. 릴리스 빌드에선 끄는 것이 안전합니다.
fun providesLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY + level = if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } }app/build.gradle.kts (2)
13-15: local.properties 의존성 완화 및 안전한 주입 처리CI/리뷰 환경에서 local.properties 부재 혹은 키 누락 시 NPE/캐스팅 예외 발생 위험이 있습니다. 파일 존재 여부를 확인하고, 환경변수로 폴백하며, InputStream을 안전하게 close하도록 변경을 권장합니다.
아래와 같이 수정해 주세요:
-val properties = Properties().apply { - load(project.rootProject.file("local.properties").inputStream()) -} +val properties = Properties().also { props -> + val f = project.rootProject.file("local.properties") + if (f.exists()) { + f.inputStream().use { props.load(it) } + } +} +val nativeAppKey = properties.getProperty("NATIVE_APP_KEY") + ?: System.getenv("NATIVE_APP_KEY") + ?: "" @@ - buildConfigField("String", "NATIVE_APP_KEY", "\"${properties["NATIVE_APP_KEY"]}\"") - manifestPlaceholders += mapOf( - "NATIVE_APP_KEY" to properties["NATIVE_APP_KEY"] as String - ) + buildConfigField("String", "NATIVE_APP_KEY", "\"$nativeAppKey\"") + manifestPlaceholders["NATIVE_APP_KEY"] = nativeAppKeyAlso applies to: 31-34
84-84: Compose Foundation 중복/버전 드리프트 가능성 제거Compose BOM을 사용 중이며 이미 foundation 의존성을 선언했습니다. 명시 버전 의존성은 제거하여 버전 불일치/충돌을 방지하세요.
- implementation("androidx.compose.foundation:foundation:1.5.0")
🧹 Nitpick comments (18)
.idea/gradle.xml (1)
9-9: IDE 전용 gradleJvm 설정 커밋 지양 + Gradle Toolchain으로 통일 권장로컬 환경 종속적인 JBR 설정은 팀/CI 간 불일치와 불필요한 변경 노이즈를 유발합니다. .idea 변경은 PR에서 제외하고, Gradle Toolchain으로 JDK 버전을 통일해 주세요.
적용 제안(diff):
- <option name="gradleJvm" value="jbr-21" />Gradle(KTS)에서 Toolchain으로 관리 예:
// 루트 build.gradle.kts 혹은 각 모듈 java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) // 팀 합의 버전(17/21 등) } }CI/로컬 모두 Gradle Wrapper + Toolchain을 사용하도록 문서화(README) 해두면 혼선이 줄어듭니다.
app/google-services.json (1)
1-47: google-services.json 커밋은 일반적이나, API 키 제한 및 SHA-1 등록 상태를 반드시 확인이 파일 커밋 자체는 Firebase/Google 권장 플로우에 부합합니다. 다만 운영 보안을 위해 다음을 확인해 주세요.
체크리스트:
- API Key(
current_key)가 GCP 콘솔에서 “Android 앱 제한(패키지명 + SHA-1)”으로 강하게 제한되어 있는지 확인. 불필요한 API는 비활성화.- debug/release(및 flavor별) SHA-1/256을 모두 등록했는지 확인. 미등록 시 릴리즈 빌드에서 Google Sign-In 실패.
package_name(com.texthip.thip)과 앱의 실제 applicationId(빌드 변형 포함)가 일치하는지 확인. 변형이 있다면 client 항목 추가 필요.- app 모듈에
com.google.gms.google-services플러그인이 정확히 1회 적용되어 있는지(중복 적용 금지) 확인.default_web_client_id(strings.xml) 값이 oauth_client 설정과 일치하는지 확인.민감정보/PII는 포함되어 있지 않으며, 본 파일만으로 비공개 리소스 접근은 불가하나 키 제한이 느슨하면 오용 가능성이 있으므로 꼭 제한을 적용하세요.
app/src/main/java/com/texthip/thip/data/model/auth/request/AuthRequest.kt (1)
5-8: 요청 바디 포맷을 KDoc으로 명시해 주세요.서버에서 기대하는 값의 형태(예: "google_", "kakao_")를 클래스 주석에 남겨두면 사용처에서 혼동이 줄어듭니다.
@Serializable -data class AuthRequest( +/** + * 서버로 전달하는 OAuth2 사용자 식별자. + * 예: "google_<uid>", "kakao_<id>" + */ +data class AuthRequest( @SerialName("oauth2Id") val oauth2Id: String )app/src/main/java/com/texthip/thip/ui/navigator/routes/CommonRoutes.kt (1)
10-17: 네이밍 일관성 정리 제안: Signin vs Signup 혼재현재 화면 파일과 패키지는 signin을, 라우트는 Signup을 사용하고 있어 혼동 여지가 있습니다. 아래 중 하나로 정리하는 것을 권장합니다.
- 화면/패키지명을 signup으로 변경하여 라우트와 맞추기
- 또는 CommonRoutes에 Signin 라우트를 두고, Signin/Signup 용어를 통일
필요 시 Auth 전용 Routes(예: AuthRoutes)로 분리해 확장성(이메일 로그인, 약관 동의 등)도 확보할 수 있습니다.
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (1)
34-36: NavController 주입만 되고 미사용 — onRightClick에서 실제 네비게이션 연결 필요닉네임 검증 성공 시 다음 화면으로 실제 이동을 연결해 주세요. 라우트가 확정되면
navController.navigate(...)호출과 popUpTo 전략(스택 정리)을 함께 설계하는 것을 권장합니다.app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt (2)
19-20: preferencesDataStore는 파일 최상위에서 선언 권장공식 권장 패턴은 파일 최상위에
Context.dataStore확장 프로퍼티를 선언하는 것입니다. 현재도 @singleton 환경에서 문제는 없겠지만, 가독성과 재사용성을 위해 이동을 권장합니다.- private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "thip_auth_tokens") + // (파일 최상위로 이동)파일 최상위에 아래 코드를 추가해 주세요:
// 파일 최상위(클래스 바깥) private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "thip_auth_tokens")
26-45: 토큰 저장소 보안 강화 제안민감 정보(토큰)를 Preferences DataStore에 평문 저장하면 디바이스 루팅/백업 노출 시 위험합니다. 다음 중 하나로 보안을 보완해 주세요.
- AndroidX Security Crypto(EncryptedSharedPreferences) 사용
- DataStore에 암호화 레이어 적용(예: 변환기 적용 또는 자체 암복호 처리)
- 토큰 수명 단축/재발급 전략 병행
요건에 따라 구현 방식을 결정하면 설계에 맞는 샘플 코드 제공 가능합니다.
app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt (1)
5-5: 미사용 import 제거
AuthService는 본 파일에서 사용되지 않습니다. 불필요한 import는 정리해 주세요.-import com.texthip.thip.data.service.AuthServiceapp/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (1)
32-33: 함수명 일관성: providesRoomsService → provideRoomsService 권장동일 모듈 내 다른 프로바이더들과 네이밍 패턴을 맞추면 가독성이 좋아집니다.
다음처럼 함수명을 정리하는 것을 제안합니다:
- fun providesRoomsService(retrofit: Retrofit): RoomsService = + fun provideRoomsService(retrofit: Retrofit): RoomsService = retrofit.create(RoomsService::class.java)app/src/main/java/com/texthip/thip/data/service/AuthService.kt (1)
9-15: Retrofit AuthService 정의 적절 — 오류 처리 방식만 상위 레이어에서 확인suspend 반환을 사용 중이므로 비-2xx 응답은 HttpException으로 흐르며 BaseResponse 파싱 전 예외가 발생합니다. 레포지토리에서 예외 맵핑/토스트/네비게이션 처리 전략이 있는지 확인해 주세요. 상태코드별 분기 필요 시 Response<BaseResponse<…>>로 받아 처리하는 옵션도 있습니다.
선택적 대안(예외 대신 응답 코드 분기 필요할 때):
+import retrofit2.Response @@ - suspend fun checkNewUser( + suspend fun checkNewUser( @Body request: AuthRequest - ): BaseResponse<AuthResponse> + ): Response<BaseResponse<AuthResponse>>app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt (1)
36-46: 고정 3초 대기보다 “자동 로그인 분기 + 최소 지연”으로 개선 제안UX 관점에서 토큰(또는 로그인 상태) 존재 시 곧바로 다음 화면으로 분기하고, 필요 시에만 짧은 딜레이를 주는 편이 좋습니다. 또한 매직 넘버(3000L)는 상수로 추출하는 것을 권장합니다.
적용 예시(핵심 변경만 발췌):
- LaunchedEffect(key1 = Unit) { - //3초 delay - delay(3000L) - // 로그인 화면으로 이동 - navController.navigate(CommonRoutes.Login) { + LaunchedEffect(Unit) { + val shouldAutoLogin = /* 토큰/세션 존재 여부 확인 */ + if (shouldAutoLogin) { + // 메인 탭 등으로 바로 이동 + navController.navigate(/* MainTabRoutes.Feed 등 */) { + popUpTo(CommonRoutes.Splash) { inclusive = true } + launchSingleTop = true + } + return@LaunchedEffect + } + delay(SPLASH_DELAY_MS) + navController.navigate(CommonRoutes.Login) { popUpTo(CommonRoutes.Splash) { inclusive = true } } }파일 상단(또는 companion object)에 상수 추가:
private const val SPLASH_DELAY_MS = 1500Lapp/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt (3)
95-97: 토큰 저장은 LaunchedEffect 내부에서 직접 호출이미
LaunchedEffect는 suspend 컨텍스트입니다. 별도 스코프 사용 없이 직접 호출해 불필요한 중첩을 제거하세요.- coroutineScope.launch { //토큰 저장 - tokenManager.saveToken(state.response.token) - } + // 토큰 저장 + tokenManager.saveToken(state.response.token)
83-90: GoogleSignInClient는 Context 변화에 반응하도록remember(context)로 키 설정구성 변경(다크모드, 회전 등) 시 새로운 Context로 재생성되도록 키를 지정하는 것이 안전합니다.
- val googleSignInClient = remember { + val googleSignInClient = remember(context) { val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(context.getString(R.string.default_web_client_id)) .requestEmail() .build() GoogleSignIn.getClient(context, gso) }
91-107: 네비게이션 사이드이펙트는 안전하지만, 토큰 저장/상태 초기화 위치 일관성 개선 제안현 구현은 작동엔 문제 없으나, 토큰 저장과 상태 초기화를 ViewModel로 옮기면 UI가 더 “덜 상태풀”해지고 테스트가 쉬워집니다. 성공 시점에 ViewModel에서 토큰을 저장하고, 저장 성공 후 라우팅 이벤트(One-off Event)를 UI로 전달하는 패턴(이벤트 채널/SharedFlow)로 리팩터링을 고려해 주세요.
app/src/main/java/com/texthip/thip/data/repository/AuthRepository.kt (2)
31-33: 사소한 포매팅: 들여쓰기 불균일
val request앞 공백이 다른 라인 대비 과합니다. 가독성을 위해 들여쓰기 정렬을 권장합니다.- val request = AuthRequest(oauth2Id = "kakao_$kakaoUserId") + val request = AuthRequest(oauth2Id = "kakao_$kakaoUserId")
25-36: 서버 응답 null 처리 흐름은 ViewModel과 일관적이나, Repository에서 도메인 규약을 강화하는 방안 고려지금은
Result<AuthResponse?>로 null 가능성을 노출하고 ViewModel이 null을 에러로 변환합니다. 레이어 간 계약을 명확히 하기 위해 Repository에서 이미 null이면 실패(Result.failure(...))로 치환해 UI 레이어에선 성공=항상 유효 응답이라는 가정을 둘 수도 있습니다.app/src/main/java/com/texthip/thip/ui/signin/viewmodel/KakaoLoginViewModel.kt (2)
31-49: 카카오/구글 공통 흐름(Domain Result → UI State) 중복 제거 제안두 메서드가 거의 동일한 상태 전이 로직을 가집니다. 테스트 용이성과 유지보수성을 위해 공통 헬퍼로 추출을 권장합니다.
- fun kakaoLogin(context: Context) { - viewModelScope.launch { - _uiState.update { LoginUiState.Loading } - - //카카오 로그인부터 서버 통신까지 - authRepository.loginWithKakao(context) - .onSuccess { response -> - if (response != null) { - _uiState.update { LoginUiState.Success(response) } - } else { - _uiState.update { LoginUiState.Error("서버로부터 응답을 받지 못했습니다.") } - } - } - .onFailure { throwable -> - Log.e("LoginViewModel", "Login failed: ${throwable.message}", throwable) - _uiState.update { - LoginUiState.Error(throwable.message ?: "알 수 없는 통신 오류가 발생했습니다.") - } - } - } - } + private fun performLogin(request: suspend () -> Result<AuthResponse?>) { + viewModelScope.launch { + _uiState.update { LoginUiState.Loading } + request() + .onSuccess { response -> + if (response != null) { + _uiState.update { LoginUiState.Success(response) } + } else { + _uiState.update { LoginUiState.Error("서버로부터 응답을 받지 못했습니다.") } + } + } + .onFailure { throwable -> + Log.e("LoginViewModel", "Login failed: ${throwable.message}", throwable) + _uiState.update { + LoginUiState.Error(throwable.message ?: "알 수 없는 통신 오류가 발생했습니다.") + } + } + } + } + + fun kakaoLogin(context: Context) = performLogin { authRepository.loginWithKakao(context) }
58-77: 구글 로그인도 공통 헬퍼 사용으로 단순화위 공통화와 동일한 맥락입니다.
- fun googleLogin(idToken: String){ - viewModelScope.launch { - _uiState.update { LoginUiState.Loading } - - //구글 로그인부터 서버 통신까지 - authRepository.loginWithGoogle(idToken) - .onSuccess { response -> - if (response != null) { - _uiState.update { LoginUiState.Success(response) } - } else { - _uiState.update { LoginUiState.Error("서버로부터 응답을 받지 못했습니다.") } - } - } - .onFailure { throwable -> - Log.e("LoginViewModel", "Login failed: ${throwable.message}", throwable) - _uiState.update { - LoginUiState.Error(throwable.message ?: "알 수 없는 통신 오류가 발생했습니다.") - } - } - } - } + fun googleLogin(idToken: String) = performLogin { authRepository.loginWithGoogle(idToken) }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (23)
.idea/gradle.xml(1 hunks)app/build.gradle.kts(4 hunks)app/google-services.json(1 hunks)app/src/main/AndroidManifest.xml(2 hunks)app/src/main/java/com/texthip/thip/MainActivity.kt(2 hunks)app/src/main/java/com/texthip/thip/ThipApplication.kt(1 hunks)app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt(1 hunks)app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt(2 hunks)app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/auth/request/AuthRequest.kt(1 hunks)app/src/main/java/com/texthip/thip/data/model/auth/response/AuthResponse.kt(1 hunks)app/src/main/java/com/texthip/thip/data/repository/AuthRepository.kt(1 hunks)app/src/main/java/com/texthip/thip/data/service/AuthService.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/navigator/routes/CommonRoutes.kt(1 hunks)app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt(3 hunks)app/src/main/java/com/texthip/thip/ui/signin/viewmodel/KakaoLoginViewModel.kt(1 hunks)app/src/main/res/values/strings.xml(1 hunks)build.gradle.kts(1 hunks)gradle.properties(1 hunks)gradle/libs.versions.toml(2 hunks)settings.gradle.kts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt (1)
app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
ThipTheme(41-69)
app/src/main/java/com/texthip/thip/MainActivity.kt (7)
app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt (1)
SplashScreen(32-71)app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt (1)
LoginScreen(54-134)app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (1)
feedNavigation(10-23)app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (1)
groupNavigation(39-217)app/src/main/java/com/texthip/thip/ui/navigator/navigations/SearchNavigation.kt (1)
searchNavigation(9-13)app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt (1)
myPageNavigation(10-19)app/src/main/java/com/texthip/thip/ui/navigator/navigations/CommonNavigation.kt (1)
commonNavigation(14-29)
🔇 Additional comments (21)
gradle.properties (1)
23-24: android.defaults.buildfeatures.buildconfig은 소문자 “buildconfig”가 맞습니다
- 기존
android.defaults.buildfeatures.buildconfig=true설정은 올바르므로 변경 불필요합니다.- 참고: 이 글로벌 플래그는 AGP 9.0에서 제거될 예정이므로, 장기적으로는 각 모듈의
build.gradle내에서와 같이 모듈별로 설정하는 방식으로 전환하는 것을 권장드립니다.android { buildFeatures { buildConfig = true } }- 또한
android.nonTransitiveRClass=true를 사용하면 빌드 및 메모리 최적화에 도움이 되지만, 종속 라이브러리의 리소스를 직접 참조하던 코드가 있다면 컴파일 에러가 발생할 수 있으므로 전체 빌드 검증을 꼭 진행해 주세요.Likely an incorrect or invalid review comment.
app/src/main/java/com/texthip/thip/data/model/auth/request/AuthRequest.kt (1)
5-8: 요청 모델 정의 적절합니다 (서버 계약과 일치).kotlinx.serialization 기반 직렬화와 필드 이름 매핑이 명확합니다. 최소 단위로 잘 설계되었습니다.
app/src/main/java/com/texthip/thip/ui/navigator/routes/CommonRoutes.kt (1)
10-17: 새 라우트(Splash/Login/Signup) 추가 방향 좋습니다.NavGraph 확장에 필요한 최소 라우트가 깔끔히 추가되었습니다.
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (3)
23-25: Preview 전용 NavController import 사용은 적절합니다.실행 코드엔 영향이 없고, 미리보기 구성도 깔끔합니다.
40-40: derivedStateOf 사용 적절입력 값에 따른 우측 버튼 활성화 계산이 간결하며 recomposition 비용도 최소화됩니다.
104-105: Preview 구성 OK
rememberNavController()로 미리보기 주입이 정상 동작합니다.app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (1)
35-39: AuthService DI 바인딩 추가 적절Retrofit 인스턴스 주입만 정상이라면 문제 없습니다. 다른 Service들과 동일한 스코프로 잘 구성되어 있습니다.
app/src/main/java/com/texthip/thip/data/model/auth/response/AuthResponse.kt (2)
6-10: API 스키마/널 처리 확인 필요서버 응답 스키마가 항상 token, isNewUser를 포함하는지 확인해 주세요. 신규 유저일 때 토큰이 빈 문자열이거나 필드가 누락될 수 있다면 역직렬화 실패/런타임 NPE 가능성이 있습니다. Retrofit에서 kotlinx.serialization 컨버터를 우선 등록했는지도 함께 점검 부탁드립니다.
6-10: 간결하고 직관적인 모델 정의 LGTM직렬화 어노테이션과 필드 네이밍이 명확하며, 불필요한 보일러플레이트 없이 목적에 부합합니다.
app/src/main/java/com/texthip/thip/ui/signin/screen/SplashScreen.kt (1)
41-45: popUpTo + inclusive 사용으로 백스택 정리 적절스플래시를 백스택에서 제거하는 탐색 옵션이 올바르게 설정되어 있습니다.
app/src/main/AndroidManifest.xml (2)
5-5: INTERNET 권한 추가 적절소셜 로그인/서버 통신에 필수 권한으로 합리적입니다.
19-35: 카카오 AppKey/스킴 자리표시자 해석 확인 및 빌드 안전장치 필요android:value와 스킴에서 사용 중인 ${NATIVE_APP_KEY}가 항상 주입되는지 확인해 주세요. app/build.gradle.kts에서 local.properties 미존재/키 누락 시 manifestPlaceholders 캐스팅에서 빌드 타임 예외가 날 수 있습니다(해당 파일 코멘트에 방어 코드 제안 포함).
app/build.gradle.kts (2)
91-95: Retrofit 컨버터 중복 사용 주의(직렬화 컨버터 우선순위 확인)gson 컨버터와 kotlinx.serialization 컨버터를 동시에 등록하면 등록 순서에 따라 역직렬화 컨버터가 달라집니다. 본 PR의 모델은 @serializable 기반이므로 serialization 컨버터가 우선 적용되는지 확인하시거나, 불필요하다면 gson 의존성을 제거하세요.
9-9: Google Services 플러그인 적용 적절Firebase Auth/Google Sign-In 연동을 위한 플러그인 추가가 적절합니다.
app/src/main/java/com/texthip/thip/MainActivity.kt (2)
36-65: 네비게이션 루트 전환 구조 깔끔합니다RootNavHost에서 인증/메인 그래프를 모듈화하여 구성한 점 좋습니다. startDestination이 Splash로 일관되며, 각 네비게이션 섹션 함수로 분리된 구조가 유지보수에 유리합니다.
43-51: Typed Navigation 사용 시 Route 타입/직렬화 설정 재확인composable<CommonRoutes.*> 제네릭 사용은 Typed Navigation 전제를 필요로 합니다. CommonRoutes의 각 엔트리가 @serializable 등 요구사항을 충족하는지와 Navigation 버전 호환성을 한번 더 점검해 주세요.
app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt (2)
124-132: 로딩 오버레이 처리 LGTMUI State 기반의 전면 로딩 인디케이터 처리 적절합니다. 동시 클릭 등으로 인한 중복 트리거를 잘 가려줍니다.
85-86: 설정 확인 완료:default_web_client_id및google-services.json모두 존재함
app/google-services.json파일과
app/src/main/res/values/strings.xml의<string name="default_web_client_id">…</string>이 정상 확인되었습니다.
추가 조치가 필요하지 않습니다.app/src/main/java/com/texthip/thip/data/repository/AuthRepository.kt (1)
37-50: Firebase 기반 구글 로그인 플로우 LGTM
GoogleAuthProvider+await()사용으로 깔끔하게 구성되어 있고, UID 추출 후 서버 확인까지 일관적입니다.app/src/main/java/com/texthip/thip/ui/signin/viewmodel/KakaoLoginViewModel.kt (2)
16-21: UI 상태 모델링 LGTMIdle/Loading/Success/Error로 단순·명확하게 표현되어 있어 UI에서 처리하기 좋습니다.
81-84: 상태 초기화 메서드 명확하고 적절탭 이동/BackStack 변경 시 중복 실행 방지에 유용합니다.
| implementation(platform("com.google.firebase:firebase-bom:34.1.0")) | ||
| implementation("com.google.firebase:firebase-auth") | ||
| implementation("com.google.android.gms:play-services-auth:21.2.0") |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Firebase BOM / Play Services Auth 최신화 여부 확인
보안/호환성 차원에서 최신 안정 버전 사용을 권장합니다. 현재 지정된 버전이 최신인지 확인해 주세요.
🌐 Web query:
What is the latest stable version of "com.google.firebase:firebase-bom" and "com.google.android.gms:play-services-auth"?
💡 Result:
- Firebase BoM latest stable: 34.1.0.[3][6][7]
- Google Play services Auth latest stable: 21.4.0.[1][3]
Details and sources:
- Google’s Firebase Auth docs updated Aug 8, 2025 show the Gradle snippet using implementation(platform("com.google.firebase:firebase-bom:34.1.0")).[3][6] The Firebase release notes confirm the major BoM bump to 34.0.0 on July 21, 2025, with the docs reflecting the current 34.1.0 point update.[7][3]
- Google Play services release notes list play-services-auth version 21.4.0 released July 23, 2025.[1] The same version is referenced in current Firebase Auth integration docs.[3]
Citations:
- 1: https://developers.google.com/android/guides/releases
- 2: https://developer.android.com/games/pgs/migration_overview
- 3: https://firebase.google.com/docs/auth/android/play-games
- 4: https://mvnrepository.com/artifact/com.google.firebase/firebase-bom
- 5: https://mvnrepository.com/artifact/com.google.android.gms/play-services-auth
- 6: https://firebase.google.com/docs/auth/android/email-link-auth
- 7: https://firebase.google.com/support/releases
- 8: https://developer.android.com/identity/sign-in/legacy-gsi-migration
- 9: Updating to Firebase BOM 33.8.0 firebase/firebase-android-sdk#6629
- 10: https://firebase.google.com/support/release-notes/android
Play Services Auth 라이브러리 버전 업데이트 필요
- 파일:
app/build.gradle.kts(라인 107–109) - Firebase BoM(
34.1.0)은 최신 안정 버전이지만,
com.google.android.gms:play-services-auth는 현재21.2.0으로 설정되어 있습니다. - 최신 안정 버전인 21.4.0으로 올려주세요.
수정 예시:
- implementation("com.google.android.gms:play-services-auth:21.2.0")
+ implementation("com.google.android.gms:play-services-auth:21.4.0")📝 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.
| implementation(platform("com.google.firebase:firebase-bom:34.1.0")) | |
| implementation("com.google.firebase:firebase-auth") | |
| implementation("com.google.android.gms:play-services-auth:21.2.0") | |
| implementation(platform("com.google.firebase:firebase-bom:34.1.0")) | |
| implementation("com.google.firebase:firebase-auth") | |
| implementation("com.google.android.gms:play-services-auth:21.4.0") |
🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 107 to 109, the dependency
com.google.android.gms:play-services-auth is pinned to 21.2.0; update that
artifact to the latest stable 21.4.0 so it matches the current Play Services
stable release and works with the Firebase BOM; modify the implementation line
to reference version 21.4.0.
| //저장된 토큰을 Flow 형태로 불러옴 | ||
| fun getToken(): Flow<String?> { | ||
| return context.dataStore.data.map { prefs -> | ||
| prefs[APP_TOKEN_KEY] | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
DataStore 접근 시 예외 처리(catch) 추가 권장
Preferences DataStore는 I/O 예외로 플로우가 취소될 수 있습니다. 예외를 처리해 기본값을 반환하도록 하면 안정성이 올라갑니다.
- fun getToken(): Flow<String?> {
- return context.dataStore.data.map { prefs ->
- prefs[APP_TOKEN_KEY]
- }
- }
+ fun getToken(): Flow<String?> {
+ return context.dataStore.data
+ .catch { e ->
+ if (e is IOException) emit(emptyPreferences()) else throw e
+ }
+ .map { prefs -> prefs[APP_TOKEN_KEY] }
+ }추가로 필요한 import:
import java.io.IOExceptionimport androidx.datastore.preferences.core.emptyPreferencesimport kotlinx.coroutines.flow.catch
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt around lines
33 to 38, wrap the DataStore flow with a catch that handles IOException by
emitting emptyPreferences and rethrow other exceptions, then continue mapping
prefs[APP_TOKEN_KEY] so a default value is returned on I/O errors; add the
necessary imports: java.io.IOException,
androidx.datastore.preferences.core.emptyPreferences, and
kotlinx.coroutines.flow.catch.
| private suspend fun getKakaoUserId(context: Context): Long = suspendCancellableCoroutine { continuation -> | ||
| // 카카오톡 설치 여부에 따라 로그인 방식 결정 | ||
| if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { | ||
| // 카카오톡으로 로그인 | ||
| UserApiClient.instance.loginWithKakaoTalk(context) { token, error -> | ||
| if (error != null) { | ||
| continuation.resumeWithException(error) | ||
| } else if (token != null) { | ||
| // 로그인 성공 시 사용자 정보 요청 | ||
| fetchUserInfo(continuation) | ||
| } | ||
| } | ||
| } else { | ||
| // 카카오계정으로 로그인 | ||
| UserApiClient.instance.loginWithKakaoAccount(context) { token, error -> | ||
| if (error != null) { | ||
| continuation.resumeWithException(error) | ||
| } else if (token != null) { | ||
| // 로그인 성공 시 사용자 정보 요청 | ||
| fetchUserInfo(continuation) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
KakaoTalk 로그인 실패 시 계정 로그인 폴백 및 중복 resume 방지 필요
- 현재
loginWithKakaoTalk실패 시 바로 예외를 재개합니다. 공식 가이드처럼 사용자 취소를 제외하고는loginWithKakaoAccount로 폴백하는 것이 UX/성공률 모두에 유리합니다. suspendCancellableCoroutine사용 시 취소/중복 resume에 주의해야 합니다.continuation.isActive체크로 중복 resume을 방지하고, 토큰/에러 모두 null인 경우도 처리해야 합니다.
아래와 같이 보완을 권장합니다. (외부 변경: 상단에 import com.kakao.sdk.common.model.ClientError 및 ClientErrorCause 추가 필요)
- private suspend fun getKakaoUserId(context: Context): Long = suspendCancellableCoroutine { continuation ->
- // 카카오톡 설치 여부에 따라 로그인 방식 결정
- if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
- // 카카오톡으로 로그인
- UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
- if (error != null) {
- continuation.resumeWithException(error)
- } else if (token != null) {
- // 로그인 성공 시 사용자 정보 요청
- fetchUserInfo(continuation)
- }
- }
- } else {
- // 카카오계정으로 로그인
- UserApiClient.instance.loginWithKakaoAccount(context) { token, error ->
- if (error != null) {
- continuation.resumeWithException(error)
- } else if (token != null) {
- // 로그인 성공 시 사용자 정보 요청
- fetchUserInfo(continuation)
- }
- }
- }
- }
+ private suspend fun getKakaoUserId(context: Context): Long = suspendCancellableCoroutine { continuation ->
+ val loginWithAccount: () -> Unit = {
+ UserApiClient.instance.loginWithKakaoAccount(context) { token, error ->
+ when {
+ !continuation.isActive -> Unit
+ error != null -> continuation.resumeWithException(error)
+ token != null -> fetchUserInfo(continuation)
+ else -> continuation.resumeWithException(IllegalStateException("Kakao login returned neither token nor error"))
+ }
+ }
+ }
+
+ if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
+ UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
+ when {
+ !continuation.isActive -> Unit
+ error != null -> {
+ // 사용자 취소는 그대로 취소, 그 외 에러는 계정 로그인으로 폴백
+ val isCancelled = (error is com.kakao.sdk.common.model.ClientError) &&
+ (error.reason == com.kakao.sdk.common.model.ClientErrorCause.Cancelled)
+ if (isCancelled) {
+ continuation.resumeWithException(error)
+ } else {
+ loginWithAccount()
+ }
+ }
+ token != null -> fetchUserInfo(continuation)
+ else -> continuation.resumeWithException(IllegalStateException("Kakao login returned neither token nor error"))
+ }
+ }
+ } else {
+ loginWithAccount()
+ }
+ }📝 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.
| private suspend fun getKakaoUserId(context: Context): Long = suspendCancellableCoroutine { continuation -> | |
| // 카카오톡 설치 여부에 따라 로그인 방식 결정 | |
| if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { | |
| // 카카오톡으로 로그인 | |
| UserApiClient.instance.loginWithKakaoTalk(context) { token, error -> | |
| if (error != null) { | |
| continuation.resumeWithException(error) | |
| } else if (token != null) { | |
| // 로그인 성공 시 사용자 정보 요청 | |
| fetchUserInfo(continuation) | |
| } | |
| } | |
| } else { | |
| // 카카오계정으로 로그인 | |
| UserApiClient.instance.loginWithKakaoAccount(context) { token, error -> | |
| if (error != null) { | |
| continuation.resumeWithException(error) | |
| } else if (token != null) { | |
| // 로그인 성공 시 사용자 정보 요청 | |
| fetchUserInfo(continuation) | |
| } | |
| } | |
| } | |
| } | |
| private suspend fun getKakaoUserId(context: Context): Long = suspendCancellableCoroutine { continuation -> | |
| // 계정 로그인 로직 재사용을 위한 람다 | |
| val loginWithAccount: () -> Unit = { | |
| UserApiClient.instance.loginWithKakaoAccount(context) { token, error -> | |
| when { | |
| !continuation.isActive -> Unit | |
| error != null -> continuation.resumeWithException(error) | |
| token != null -> fetchUserInfo(continuation) | |
| else -> continuation.resumeWithException( | |
| IllegalStateException("Kakao login returned neither token nor error") | |
| ) | |
| } | |
| } | |
| } | |
| if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { | |
| // 카톡 설치 시 먼저 카톡 로그인 시도 | |
| UserApiClient.instance.loginWithKakaoTalk(context) { token, error -> | |
| when { | |
| !continuation.isActive -> Unit | |
| error != null -> { | |
| // 사용자 취소는 곧바로 예외 전달, 그 외 에러는 계정 로그인으로 폴백 | |
| val isCancelled = (error is com.kakao.sdk.common.model.ClientError) && | |
| (error.reason == com.kakao.sdk.common.model.ClientErrorCause.Cancelled) | |
| if (isCancelled) { | |
| continuation.resumeWithException(error) | |
| } else { | |
| loginWithAccount() | |
| } | |
| } | |
| token != null -> fetchUserInfo(continuation) | |
| else -> continuation.resumeWithException( | |
| IllegalStateException("Kakao login returned neither token nor error") | |
| ) | |
| } | |
| } | |
| } else { | |
| // 카톡 미설치 시 바로 계정 로그인 | |
| loginWithAccount() | |
| } | |
| } |
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/repository/AuthRepository.kt around
lines 53 to 76, the KakaoTalk login path currently resumes with an exception
immediately on any error and risks multiple resumes or null token/error
handling; update the flow to: on loginWithKakaoTalk error, if the error is a
user-cancel (ClientError with cause CANCELLED) then resumeWithException only if
continuation.isActive, otherwise fall back to calling loginWithKakaoAccount and
delegate to the same success/error handler; before any continuation.resume or
resumeWithException always check continuation.isActive to avoid duplicate
resumes and add a guard branch that handles the case where both token and error
are null by resuming with a clear exception; add imports for
com.kakao.sdk.common.model.ClientError and ClientErrorCause at the top.
| UserApiClient.instance.me { user, error -> | ||
| if (error != null) { | ||
| continuation.resumeWithException(error) | ||
| } else if (user?.id != null) { | ||
| continuation.resume(user.id!!) | ||
| } else { | ||
| continuation.resumeWithException(IllegalStateException("Kakao User ID is null")) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
me 콜백에서 취소/중복 resume 방지
Compose 화면 전환 등으로 코루틴이 취소된 뒤 콜백이 도착하면 중복 resume 예외가 날 수 있습니다. continuation.isActive 체크를 추가하세요.
private fun fetchUserInfo(continuation: CancellableContinuation<Long>) {
UserApiClient.instance.me { user, error ->
- if (error != null) {
- continuation.resumeWithException(error)
- } else if (user?.id != null) {
- continuation.resume(user.id!!)
- } else {
- continuation.resumeWithException(IllegalStateException("Kakao User ID is null"))
- }
+ if (!continuation.isActive) return@me
+ when {
+ error != null -> continuation.resumeWithException(error)
+ user?.id != null -> continuation.resume(user.id!!)
+ else -> continuation.resumeWithException(IllegalStateException("Kakao User ID is null"))
+ }
}
}🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/repository/AuthRepository.kt around
lines 80 to 88, the callback passed to UserApiClient.instance.me can attempt to
resume the coroutine after it was cancelled, causing duplicate-resume
exceptions; guard all continuation.resume and continuation.resumeWithException
calls with a continuation.isActive check (e.g., only call
resume/resumeWithException when continuation.isActive) so that callbacks
received after coroutine cancellation are ignored.
| class ThipApplication : Application(){ | ||
| override fun onCreate() { | ||
| super.onCreate() | ||
|
|
||
| // 카카오 SDK 초기화 | ||
| try { | ||
| KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY) | ||
| }catch (e: Exception){ | ||
| e.printStackTrace() | ||
| } | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Kakao SDK 초기화: 광범위한 예외 삼키기 대신 키 검증 + 명확한 로깅
현재 try/catch(Exception)으로 초기화 실패를 삼키면 설정 오류(예: 빈 NATIVE_APP_KEY)가 런타임에서 조용히 무시됩니다. 초기화 전 키를 검증하고, 문제 시 명확히 로깅한 뒤 초기화를 건너뛰는 쪽이 디버깅과 운영 안정성에 유리합니다.
적용 제안(diff):
- // 카카오 SDK 초기화
- try {
- KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY)
- }catch (e: Exception){
- e.printStackTrace()
- }
+ // 카카오 SDK 초기화
+ if (BuildConfig.NATIVE_APP_KEY.isBlank()) {
+ android.util.Log.e("ThipApplication", "KAKAO NATIVE_APP_KEY is missing. Kakao SDK initialization skipped.")
+ } else {
+ KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY)
+ }검증 포인트:
- CI/릴리즈 빌드에서도
BuildConfig.NATIVE_APP_KEY가 빈 문자열이 아닌지 확인(로컬 프로퍼티 미설정 시 대체 값 주입 여부). - Manifest의
com.kakao.sdk.AppKey메타데이터와 프로그램적 초기화 키가 동일하게 유지되는지 확인.
아래 스크립트를 실행해 NATIVE_APP_KEY 설정 위치와 메타데이터를 확인해 주세요.
이 정보를 바탕으로 검증 후 최종 리뷰 코멘트를 작성하겠습니다.
🏁 Script executed:
#!/bin/bash
# 1. build.gradle에서 BuildConfig 필드로 선언된 NATIVE_APP_KEY 위치 확인
rg "buildConfigField.*NATIVE_APP_KEY" -n
# 2. 프로젝트 전반에서 NATIVE_APP_KEY 사용 위치 확인 (예: KakaoSdk.init 호출 외)
rg "NATIVE_APP_KEY" -n
# 3. AndroidManifest.xml에서 com.kakao.sdk.AppKey 메타데이터 선언 확인
rg "com.kakao.sdk.AppKey" -nLength of output: 806
카카오 SDK 초기화: 키 검증 후 명확한 로깅으로 예외 처리 개선
BuildConfig.NATIVE_APP_KEY가 app/build.gradle.kts에서 properties["NATIVE_APP_KEY"]로 정의되고, AndroidManifest.xml의 com.kakao.sdk.AppKey 메타데이터 및 앱 스킴(kakao${NATIVE_APP_KEY})에도 동일한 값으로 주입되는 것을 확인했습니다. 따라서 초기화 전 빈 값 검증 + 오류 로깅 후 초기화를 건너뛰는 방식으로 변경하는 것이 적절합니다.
대상 파일
- app/src/main/java/com/texthip/thip/ThipApplication.kt
제안하는 diff:
@@ -13,9 +13,12 @@ class ThipApplication : Application(){
- // 카카오 SDK 초기화
- try {
- KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY)
- } catch (e: Exception) {
- e.printStackTrace()
- }
+ // 카카오 SDK 초기화: 키 검증 후 초기화
+ val kakaoKey = BuildConfig.NATIVE_APP_KEY
+ if (kakaoKey.isBlank()) {
+ Log.e("ThipApplication", "KAKAO NATIVE_APP_KEY가 설정되지 않았습니다. SDK 초기화를 건너뜁니다.")
+ } else {
+ KakaoSdk.init(this, kakaoKey)
+ }이렇게 변경하면 설정 누락 시 Silent Fail을 방지하고, 운영 환경에서 문제 원인을 빠르게 파악할 수 있습니다.
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ThipApplication.kt around lines 10 to 21,
the KakaoSdk.init call should guard against an empty or missing
BuildConfig.NATIVE_APP_KEY and log a clear error instead of silently failing;
change the onCreate to check if BuildConfig.NATIVE_APP_KEY.isNullOrBlank() and
if so log an error (including the key state and guidance to check
app/build.gradle.kts and AndroidManifest meta-data) and skip initialization,
otherwise proceed to call KakaoSdk.init inside a try/catch that logs the
exception with context (use Log.e or your app logger) rather than only printing
the stack trace.
| if (result.resultCode == Activity.RESULT_OK) { | ||
| val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) | ||
| try { | ||
| // 성공 시, ID 토큰을 ViewModel으로 전달 | ||
| val account = task.getResult(ApiException::class.java)!! | ||
| val idToken = account.idToken!! | ||
| viewModel.googleLogin(idToken) | ||
| } catch (e: ApiException) { | ||
| // 실패 시 에러 처리 | ||
| Toast.makeText(context, "구글 로그인에 실패했습니다.", Toast.LENGTH_SHORT).show() | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
구글 로그인 결과 처리 시 NPE 위험 제거 및 취소 케이스 대응 필요
현재 !!를 사용해 account와 idToken을 강제 언래핑하고 있어 설정/빌드 문제(예: default_web_client_id 미설정)나 드문 케이스에서 NPE가 발생할 수 있습니다. 또한 RESULT_CANCELED일 때 사용자 피드백이 없습니다. 아래처럼 null-safe 처리와 취소/예외 메시지를 추가해주세요.
- if (result.resultCode == Activity.RESULT_OK) {
- val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
- try {
- // 성공 시, ID 토큰을 ViewModel으로 전달
- val account = task.getResult(ApiException::class.java)!!
- val idToken = account.idToken!!
- viewModel.googleLogin(idToken)
- } catch (e: ApiException) {
- // 실패 시 에러 처리
- Toast.makeText(context, "구글 로그인에 실패했습니다.", Toast.LENGTH_SHORT).show()
- }
- }
+ if (result.resultCode == Activity.RESULT_OK) {
+ val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
+ try {
+ val account = task.getResult(ApiException::class.java)
+ val idToken = account?.idToken
+ if (!idToken.isNullOrBlank()) {
+ viewModel.googleLogin(idToken)
+ } else {
+ Toast.makeText(context, "구글 로그인 토큰을 가져오지 못했습니다.", Toast.LENGTH_SHORT).show()
+ }
+ } catch (e: ApiException) {
+ Toast.makeText(context, "구글 로그인에 실패했습니다. (${e.statusCode})", Toast.LENGTH_SHORT).show()
+ } catch (e: Exception) {
+ Toast.makeText(context, "구글 로그인 처리 중 오류가 발생했습니다.", Toast.LENGTH_SHORT).show()
+ }
+ } else {
+ Toast.makeText(context, "구글 로그인이 취소되었습니다.", Toast.LENGTH_SHORT).show()
+ }📝 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.
| if (result.resultCode == Activity.RESULT_OK) { | |
| val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) | |
| try { | |
| // 성공 시, ID 토큰을 ViewModel으로 전달 | |
| val account = task.getResult(ApiException::class.java)!! | |
| val idToken = account.idToken!! | |
| viewModel.googleLogin(idToken) | |
| } catch (e: ApiException) { | |
| // 실패 시 에러 처리 | |
| Toast.makeText(context, "구글 로그인에 실패했습니다.", Toast.LENGTH_SHORT).show() | |
| } | |
| } | |
| } | |
| if (result.resultCode == Activity.RESULT_OK) { | |
| val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) | |
| try { | |
| val account = task.getResult(ApiException::class.java) | |
| val idToken = account?.idToken | |
| if (!idToken.isNullOrBlank()) { | |
| viewModel.googleLogin(idToken) | |
| } else { | |
| Toast.makeText(context, "구글 로그인 토큰을 가져오지 못했습니다.", Toast.LENGTH_SHORT).show() | |
| } | |
| } catch (e: ApiException) { | |
| Toast.makeText(context, "구글 로그인에 실패했습니다. (${e.statusCode})", Toast.LENGTH_SHORT).show() | |
| } catch (e: Exception) { | |
| Toast.makeText(context, "구글 로그인 처리 중 오류가 발생했습니다.", Toast.LENGTH_SHORT).show() | |
| } | |
| } else { | |
| Toast.makeText(context, "구글 로그인이 취소되었습니다.", Toast.LENGTH_SHORT).show() | |
| } |
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/signin/screen/LoginScreen.kt around
lines 67 to 79, remove the unsafe use of `!!` on `account` and `idToken` and add
handling for RESULT_CANCELED: safely obtain the GoogleSignIn account using
null-safe calls, check that `account` and `account.idToken` are non-null before
calling `viewModel.googleLogin(idToken)`, and show a Toast (or other user
feedback) when the user cancels (resultCode == Activity.RESULT_CANCELED) or when
the idToken is null; in the catch block include the exception message in logs
and show a user-friendly Toast indicating the error.
| <!-- Google login constants --> | ||
| <string name="default_web_client_id" translatable="false">353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com</string> | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
default_web_client_id 수동 정의는 플러그인 생성 리소스와 충돌/불일치 위험
google-services 플러그인은 빌드 변형에 맞는 default_web_client_id를 자동 생성합니다. 수동 정의는 빌드 변형 간 불일치나 값 불일치로 로그인 실패를 유발할 수 있습니다. 수동 정의 제거를 권장드립니다. 유지가 필요하다면 google-services.json의 값과 정확히 일치하는지 반드시 검증해 주세요.
제거 제안:
- <!-- Google login constants -->
- <string name="default_web_client_id" translatable="false">353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com</string>값 일치 여부는 다음 스크립트로 검증할 수 있습니다:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) strings.xml에서 수동 정의된 default_web_client_id 추출"
XML_FILE=$(fd -a 'strings.xml' app/src/main/res | head -n 1 || true)
if [[ -z "${XML_FILE:-}" ]]; then
echo "strings.xml을 찾지 못했습니다."; exit 0
fi
PY_XML=$(python - << 'PY'
import sys, xml.etree.ElementTree as ET
import os
path = os.environ.get("XML_FILE")
tree = ET.parse(path)
root = tree.getroot()
val = None
for s in root.findall("string"):
if s.get("name") == "default_web_client_id":
val = (s.text or "").strip()
break
print(val or "")
PY
)
echo "strings.xml client_id: ${PY_XML}"
echo
echo "2) google-services.json에서 oauth client_id 추출"
JSON_FILE=$(fd -a 'google-services.json' | head -n 1 || true)
if [[ -z "${JSON_FILE:-}" ]]; then
echo "google-services.json을 찾지 못했습니다. 프로젝트에 파일이 포함되어 있는지 확인하세요."
exit 0
fi
# 여러 oauth_client 중 web client를 우선 탐색
JSON_CLIENT_IDS=$(jq -r '
.client[]?.oauth_client[]? | .client_type as $t | .client_id as $id |
"\($t):\($id)"
' "$JSON_FILE" || true)
echo "google-services.json client_ids:"
echo "$JSON_CLIENT_IDS"
# web client 우선 비교
WEB_ID=$(echo "$JSON_CLIENT_IDS" | awk -F: '$1=="3"{print $2; exit}')
[[ -z "$WEB_ID" ]] && WEB_ID=$(echo "$JSON_CLIENT_IDS" | head -n1 | cut -d: -f2)
echo
echo "비교 결과:"
if [[ -n "$PY_XML" && -n "$WEB_ID" ]]; then
if [[ "$PY_XML" == "$WEB_ID" ]]; then
echo "OK: 수동 정의와 google-services.json의 client_id가 일치합니다."
else
echo "경고: 값 불일치. 수동 정의 제거 또는 값 동기화 필요."
echo "strings.xml: $PY_XML"
echo "google-services.json: $WEB_ID"
fi
else
echo "참고: 비교할 값이 충분하지 않습니다. 파일 존재/형식을 확인하세요."
fiLength of output: 715
스크립트 수정: XML 파일 경로를 Python 스크립트 인자로 전달해 값을 올바르게 추출하고, google-services.json의 web client_id와 비교합니다. 아래 명령을 실행해 재검증해 주세요.
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# 1) strings.xml에서 수동 정의된 default_web_client_id 추출
XML_FILE=$(fd -a 'strings.xml' app/src/main/res | head -n1 || true)
if [[ -z "$XML_FILE" ]]; then
echo "ERROR: strings.xml을 찾지 못했습니다." >&2
exit 1
fi
MANUAL_ID=$(python3 - "$XML_FILE" << 'PY'
import sys, xml.etree.ElementTree as ET
path = sys.argv[1]
tree = ET.parse(path)
root = tree.getroot()
el = root.find("string[@name='default_web_client_id']")
print(el.text.strip() if el is not None and el.text else "")
PY
)
echo "strings.xml default_web_client_id: $MANUAL_ID"
# 2) google-services.json에서 web client_id 추출
JSON_FILE=$(fd -a 'google-services.json' . | head -n1 || true)
if [[ -z "$JSON_FILE" ]]; then
echo "ERROR: google-services.json을 찾지 못했습니다." >&2
exit 1
fi
WEB_ID=$(jq -r '
.client[]?.oauth_client[]?
| select(.client_type == 3)
| .client_id
' "$JSON_FILE" | head -n1)
echo "google-services.json web client_id: $WEB_ID"
# 3) 비교
if [[ -z "$MANUAL_ID" || -z "$WEB_ID" ]]; then
echo "참고: 비교할 값이 충분하지 않습니다. 파일 형식을 확인하세요."
elif [[ "$MANUAL_ID" == "$WEB_ID" ]]; then
echo "OK: 수동 정의와 google-services.json의 client_id가 일치합니다."
else
echo "WARNING: 수동 정의와 client_id 불일치. 제거 또는 값 동기화 필요."
echo " strings.xml: $MANUAL_ID"
echo " google-services.json: $WEB_ID"
exit 2
fiLength of output: 1694
default_web_client_id 값 일치 확인 및 수동 정의 제거 권장
현재 strings.xml의 default_web_client_id 값이 google-services.json의 web client_id와 일치함을 확인했습니다.
하지만 google-services 플러그인은 빌드 변형별로 자동 생성하므로, 유지보수성과 잠재적 불일치 위험을 줄이기 위해 수동 정의를 제거하시길 권장드립니다.
- strings.xml default_web_client_id: 353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com
- google-services.json web client_id: 353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com
제거 제안:
- <!-- Google login constants -->
- <string name="default_web_client_id" translatable="false">353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com</string>📝 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.
| <!-- Google login constants --> | |
| <string name="default_web_client_id" translatable="false">353417813537-ck9g1v0qprlb5nf4dvasinim403eng0f.apps.googleusercontent.com</string> |
🤖 Prompt for AI Agents
In app/src/main/res/values/strings.xml around lines 377-379, there's a manually
defined default_web_client_id that duplicates the value generated from
google-services.json; remove this manual <string name="default_web_client_id">
entry so the google-services plugin can generate the per-variant value at build
time, and ensure google-services.json is present in the project root and the
google-services plugin is applied in the module build.gradle so the generated
resource is used for all build variants.
| alias(libs.plugins.kotlin.android) apply false | ||
| alias(libs.plugins.kotlin.compose) apply false | ||
| id("com.google.dagger.hilt.android") version "2.57" apply false | ||
| id("com.google.gms.google-services") version "4.4.3" apply false |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Google Services 플러그인 루트 선언 OK — app 모듈 적용/JSON 존재 확인 필요
루트에 apply false로 선언한 점 적절합니다. 앱 모듈에서 실제 적용 여부와 google-services.json 존재를 확인해 주세요. 또한 default_web_client_id 리소스가 수동 정의와 중복되지 않는지 점검이 필요합니다.
다음 스크립트로 확인할 수 있습니다:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) app 모듈에서 플러그인 적용 여부 확인"
fd -a 'build.gradle.kts' -x sh -c 'echo "--- {} ---"; rg -n $'"'"'plugins\s*\{[^}]*id\("com\.google\.gms\.google-services"\)'{} || true
echo
echo "2) google-services.json 존재 확인 (모듈 루트 혹은 관련 디렉토리에서 검색)"
fd -a 'google-services.json' || true
echo
echo "3) default_web_client_id 리소스 수동 정의 중복 여부 확인"
rg -n 'name="default_web_client_id"' app || trueLength of output: 840
앱 모듈에서 플러그인 적용 여부를 제대로 확인하기 위해 아래 스크립트를 실행해 주세요:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) app 모듈에서 Google Services 플러그인 적용 여부 확인"
rg -n 'id\("com\.google\.gms\.google-services"\)' app || true
echo
echo "2) google-services.json 존재 위치 재확인"
rg -l '.*' -g 'google-services.json' .
echo
echo "3) default_web_client_id 리소스 중복 정의 확인"
rg -n 'name="default_web_client_id"' app/src/main/res || trueLength of output: 716
Google Services 설정 검토 필요 — 자동 생성 리소스 중복 제거
- 루트 build.gradle.kts에
id("com.google.gms.google-services") version "4.4.3" apply false선언은 적절합니다. - app/build.gradle.kts(9)에서 플러그인 적용이 확인되었습니다.
- app/google-services.json 파일이 존재합니다.
- app/src/main/res/values/strings.xml(378)에
default_web_client_id가 수동 정의되어 있어, Google Services 플러그인이 자동 생성하는 동일 리소스와 중복될 수 있습니다.
→ default_web_client_id 수동 정의를 제거하거나 자동 생성 리소스를 사용하도록 조치해 주세요.
🤖 Prompt for AI Agents
In build.gradle.kts around line 7 the Google Services plugin is declared
(`com.google.gms.google-services`), but app/src/main/res/values/strings.xml
contains a manually defined default_web_client_id at line ~378 that will collide
with the resource auto-generated by the Google Services plugin; remove the
manual default_web_client_id entry from app/src/main/res/values/strings.xml (or
conditionally guard it) so the auto-generated resource from google-services.json
is used, then rebuild to verify no duplicate resource errors.
| retrofit = "3.0.0" | ||
| retrofitKotlinSerializationConverter = "1.0.0" | ||
| androidxComposeNavigation = "2.8.2" | ||
| lifecycleRuntimeCompose = "2.10.0-alpha01" |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
alpha(2.10.0-alpha01) 채택: 안정성/호환성 리스크 — 안정 버전으로 정렬 권장
다른 lifecycle 의존성(예: viewmodel-compose 2.9.1, runtime-ktx 2.8.7)과 버전대가 상이합니다. 신규 API가 필요한 것이 아니라면 안정 릴리스(예: 2.9.1)로 맞추는 편이 충돌/회귀 리스크를 줄입니다.
다음과 같이 정렬을 제안합니다(예: viewmodel-compose와 동일 버전):
-lifecycleRuntimeCompose = "2.10.0-alpha01"
+lifecycleRuntimeCompose = "2.9.1"의존성 버전 정합성은 아래 스크립트로 점검 가능합니다:
Also applies to: 62-62
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) 버전 카탈로그의 lifecycle 관련 버전들"
rg -n 'lifecycle.*=' gradle/libs.versions.toml
echo
echo "2) 앱에서 lifecycle runtime compose 사용 여부"
rg -n 'lifecycle[-.]runtime[-.]compose|libs\.lifecycle\.runtime\.compose' -A2 app
echo
echo "3) lifecycle 계열 버전 불일치 징후 확인(뷰모델/런타임/컴포즈)"
rg -n 'androidx\.lifecycle.*(runtime|viewmodel).*' -A1 app gradle/libs.versions.toml || trueLength of output: 3714
lifecycle-runtime-compose 버전을 안정 릴리스(2.9.1)로 정렬하세요
아래 파일에서 lifecycle-runtime-compose 버전(2.10.0-alpha01)이 다른 Compose 관련 lifecycle 의존성들과 불일치하여 안정성·호환성 리스크가 있습니다:
- 파일:
gradle/libs.versions.toml- 라인 28, 62
제안하는 수정 내용:
- lifecycleRuntimeCompose = "2.10.0-alpha01"
+ lifecycleRuntimeCompose = "2.9.1"이렇게 변경하면 lifecycleViewmodelCompose(2.9.1) 및 lifecycleRuntimeKtx(2.8.7)와 버전 정합성을 맞춰 의존성 충돌 및 회귀 위험을 줄일 수 있습니다.
📝 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.
| lifecycleRuntimeCompose = "2.10.0-alpha01" | |
| lifecycleRuntimeCompose = "2.9.1" |
🤖 Prompt for AI Agents
In gradle/libs.versions.toml around lines 28 and 62, lifecycle-runtime-compose
is set to 2.10.0-alpha01 which is inconsistent with other lifecycle Compose
dependencies; change lifecycleRuntimeCompose to the stable 2.9.1 (and update any
duplicate entries on line 62) so the Compose lifecycle versions align with
lifecycleViewmodelCompose(2.9.1) and lifecycleRuntimeKtx(2.8.7), avoiding
alpha/stable mismatch and reducing compatibility risk.
| repositories { | ||
| google() | ||
| mavenCentral() | ||
| maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Kakao 저장소를 제한적으로 포함해 공급망 리스크/충돌 최소화
전체 의존성 해석 범위에 제3자 저장소를 추가하면 아티팩트 섀도잉/스푸핑 리스크가 커집니다. Kakao 그룹만 해석하도록 exclusiveContent(or content filter)로 범위를 제한해 주세요.
적용 제안(diff):
- maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/")
+ exclusiveContent {
+ forRepository {
+ maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/")
+ }
+ filter {
+ includeGroupByRegex("com\\.kakao(\\..*)?")
+ }
+ }참고: 이미 repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)가 설정되어 있는 점은 좋습니다. 외부 저장소는 꼭 필요한 그룹만 포함하도록 유지해 주세요.
📝 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.
| maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") | |
| exclusiveContent { | |
| forRepository { | |
| maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") | |
| } | |
| filter { | |
| includeGroupByRegex("com\\.kakao(\\..*)?") | |
| } | |
| } |
🤖 Prompt for AI Agents
In settings.gradle.kts around line 19, the project adds the Kakao Maven URL
directly which expands dependency resolution to that entire repository; wrap
this repository in an exclusiveContent block that restricts resolution to only
Kakao package groups (e.g., includeGroup("com.kakao") or a regex for Kakao group
prefixes) so only artifacts from the intended Kakao group(s) are fetched from
that URL; update the repositories block to use exclusiveContent { forRepository
{ maven(url = "...") } filter { includeGroup("com.kakao") /* or
includeGroupByRegex("^(com|kr)\\.kakao") */ } } to minimize supply-chain risk
and artifact shadowing.
➕ 이슈 링크
🔎 작업 내용
-구글 소셜 로그인 구현했습니다.
📸 스크린샷
bandicam.2025-08-13.01-26-08-300.2.mp4
신규 사용자인 경우, isNewUser = true , 임시 토큰 발급 확인 했습니다.
😢 해결하지 못한 과제
[] TASK
📢 리뷰어들에게
Summary by CodeRabbit
New Features
Refactor
Chores