Skip to content

[feat/#145] 카카오 로그인 구현#161

Merged
znayeonzn merged 65 commits intodevelopfrom
feat/#145-kakao-login
Aug 31, 2025
Merged

[feat/#145] 카카오 로그인 구현#161
znayeonzn merged 65 commits intodevelopfrom
feat/#145-kakao-login

Conversation

@znayeonzn
Copy link
Collaborator

@znayeonzn znayeonzn commented Aug 26, 2025

Related issue 🛠

Work Description 📝

  • 카카오 회원가입 및 로그인 API 연동
  • reissueAccessToken(토큰 재발급) API 연동
  • 로컬에 토큰 저장

Screenshot 📸

ByeBoo_signin.mp4
ByeBoo_signup.mp4
스크린샷 2025-08-26 191040

Uncompleted Tasks 😅

  • 로그인 실패 시, 에러 처리(스낵바)

PR Point 📌

기능 구현과 변경 사항에 대해 간단히 설명 남기겠습니다!

기능 구현
[회원가입 및 로그인 API 연동]

  1. 카카오 앱/웹에서 응답 받은 카카오 accessToken을 서버 쪽에 넘겨줍니다.
  2. 응답 받은 accessTokenplatform을 요청한 후, 토큰과 회원가입 여부를 응답 받습니다.
  3. 전달 받은 토큰은 DataStore에 저장합니다.

[토큰 재발급 API 연동]

  1. 토큰이 만료되기 직전이거나 만료되면 refreshToken을 서버 쪽에 넘겨줍니다.
  2. accessToken, refreshToken을 응답 받습니다.
  3. 기존 헤더 Authorization을 지우고 새로운 토큰으로 넣어줍니다.
  4. 전달 받은 토큰을 DataStore에 저장합니다.

변경 사항

  1. SplashViewModel & LoginUseCase
  • 뷰모델에서는 loginUseCase 호출하여 카카오에서 전달받은 카카오토큰과 platform 값을 넘겨주고 응답에 대한 분기 처리로 사이드이펙트 처리만 해줍니다.
  • 유즈케이스에서는 토큰을 저장하고 삭제하는 로직을 구성했습니다. Repository 호출!
  1. AuthRepository, AuthRepositoryImpl
  • 회원가입 및 로그인 , 토큰 재발급 함수 작성
  1. AuthInterceptor
  • 일반 API 요청 전에 DataStore에 저장된 AccessToken 을 헤더에 붙여줍니다
  1. TokenAuthenticator
  • 서버가 401 응답값을 보냈을 때 동작하고, 저장된 RefreshToken으로 토큰을 재발급 받습니다.
  • 뷰모델에서 직접 토큰 갱신 로직을 호출하지 않고 네트워크 계층에서 토큰을 관리합니다.

더 자세한 거 원하면....아티클 적어서....개인적으로 보내드리겠음ㅋ.ㅋ
로컬프로퍼티에 카카오네이티브앱키 추가해주세요~ 따옴표 빼고 적어야함!!!!
로그에 키 해시 값 카카오 디벨로퍼에 등록해주세요!!

너무 길어졌네요..! 기다려주셔서 감삼둥...틀린 거 있으면 알려주세요!! 제발

트러블 슈팅 💥

  • 로그인 성공한 후에 바로 홈으로 넘어가지 않고 잠시 스플래시 화면에 있다가 홈으로 이동하는데 이거... 왜이런 건가요?? 상태 개선이 필요한 것인지 알려주시면 감사하겠습니다!

Summary by CodeRabbit

  • 신기능

    • 카카오 로그인 지원: 카톡/웹 로그인 흐름 추가 및 앱 초기화와 OAuth 리다이렉트 처리
    • 이용약관 화면 흐름 추가: 스플래시→약관→완료 시 사용자 정보 화면으로 이동
  • 개선

    • 토큰 기반 인증 전환 및 자동 토큰 재발급으로 로그인 유지 및 보안성 향상
    • 네트워크 인증 헤더·클라이언트 분리로 요청 안정성 개선
    • 액세스 토큰 캐시로 성능 최적화 및 빠른 인증 처리

@znayeonzn znayeonzn self-assigned this Aug 26, 2025
@znayeonzn znayeonzn added 🍒 [FEAT] 새로운 기능 구현 🐰 아연 아연 labels Aug 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 26, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

카카오 로그인 및 토큰 기반 인증을 도입했다. Kakao SDK 초기화 및 OAuth 액티비티/딥링크를 추가하고, 스플래시에서 카카오 로그인 흐름을 연결했다. 액세스/리프레시 토큰 저장·재발급용 데이터·네트워크·DI 계층(Interceptor/Authenticator 포함)과 도메인/유스케이스를 추가/조정했다.

Changes

Cohort / File(s) Summary
Build config & Manifest
app/build.gradle.kts, app/src/main/AndroidManifest.xml
KAKAO_NATIVE_APP_KEY를 local.properties에서 읽어 BuildConfig·manifestPlaceholders로 주입. Kakao OAuth 핸들러 com.kakao.sdk.auth.AuthCodeHandlerActivity 추가 및 딥링크 스킴 등록.
App init
app/src/main/java/com/byeboo/app/ByeBoo.kt, app/src/main/java/com/byeboo/app/presentation/main/MainActivity.kt
Application에서 KakaoSdk.init 호출 추가. MainActivity에서 Kakao key hash 로그 출력 추가.
Core network auth
app/src/main/java/com/byeboo/app/core/network/AuthInterceptor.kt, .../TokenAuthenticator.kt, .../qualifier/NetworkQualifier.kt
AuthInterceptor를 MasterKey에서 Bearer 토큰 방식으로 변경. OkHttp Authenticator로 TokenAuthenticator 추가. DI용 @Auth qualifier 추가.
Local token storage
.../data/datasource/local/TokenDataSource.kt, .../datasourceimpl/local/TokenDataSourceImpl.kt, .../data/di/TokenDataStoreModule.kt
DataStore 기반 TokenDataSource 인터페이스와 구현, TokenDataStore DI 모듈 추가(토큰 저장/조회/삭제).
Remote auth API & DTOs
.../data/service/auth/AuthService.kt, .../data/datasource/remote/auth/AuthRemoteDataSource.kt, .../datasourceimpl/remote/auth/AuthRemoteDataSourceImpl.kt, .../data/dto/**/auth/*Dto.kt
카카오 로그인 및 토큰 재발급용 Retrofit 서비스와 원격 데이터 소스, 요청/응답 DTO 추가.
Data mapper & repos
.../data/mapper/auth/AuthMapper.kt, .../data/repositoryimpl/auth/AuthRepositoryImpl.kt, .../data/repositoryimpl/auth/TokenRepositoryImpl.kt
DTO→도메인 매핑, AuthRepositoryImpl 추가. TokenRepositoryImpl을 TokenDataSource 기반으로 재작성하고 메모리 캐시(접근 토큰) 추가.
DI: Network/Data/Service/Repository modules
.../data/di/NetworkModule.kt, .../data/di/ServiceModule.kt, .../data/di/DataSourceModule.kt, .../data/di/RepositoryModule.kt
Auth 전용 OkHttp/Retrofit(@Auth) 제공자 추가 및 AuthService 바인딩. AuthInterceptor/TokenAuthenticator 제공자와 모듈 바인딩 조정.
Domain & UseCase
.../core/model/auth/TokenEntity.kt, .../domain/model/auth/AuthResult.kt, .../domain/repository/auth/*, .../domain/usecase/LoginUseCase.kt
TokenEntity 및 AuthResult 추가. AuthRepository/TokenRepository 계약 변경(토큰 중심, Flow 제공). 로그인 유스케이스(LoginUseCase) 추가.
Presentation: Splash / Terms / Navigation
.../presentation/splash/*, .../presentation/splash/termsofservice/*, .../presentation/main/*
Splash 쪽 사이드이펙트 타입을 확장하여 카카오 로그인 트리거(StartKakaoTalkLogin/StartKakaoWebLogin 등) 추가. Terms 화면에서 완료 시 UserInfo 네비게이션 연결. Nav graph 및 MainNavigator 시그니처 확장.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant App as ByeBoo App
    participant Kakao as Kakao SDK
    participant BE as Backend API

    User->>App: 스플래시에서 카카오 로그인 버튼 탭
    App->>Kakao: UserApiClient.loginWithKakaoTalk / loginWithKakaoAccount
    Kakao-->>App: OAuthToken (kakaoToken)
    App->>BE: POST /api/v1/auth/login (Authorization: Bearer {kakaoToken}, body: platform)
    BE-->>App: {accessToken, refreshToken, isRegistered}
    App->>App: TokenDataStore 저장 및 TokenRepository 캐시 업데이트
    alt isRegistered == true
        App->>App: NavigateToHome
    else
        App->>App: NavigateToTermsOfService
    end
Loading
sequenceDiagram
    autonumber
    participant Client as OkHttp (AuthInterceptor)
    participant Repo as TokenRepository
    participant BE as Backend API
    participant Authn as TokenAuthenticator

    Client->>Repo: getCachedAccessToken()
    alt accessToken 존재
        Client->>BE: 요청 (Authorization: Bearer {access})
        BE-->>Client: 401 Unauthorized
        Client->>Authn: authenticate(response)
        Authn->>Repo: getRefreshToken()
        Authn->>BE: POST /api/v1/auth/reissue (Authorization: Bearer {refresh})
        BE-->>Authn: {newAccess, newRefresh}
        Authn->>Repo: saveTokens(newTokens)
        Authn-->>Client: 재시도 요청(Authorization: Bearer {newAccess})
        Client->>BE: 재시도 요청
        BE-->>Client: 200 OK
    else accessToken 없음
        Client->>BE: 요청 (헤더 없음)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Assessment against linked issues

Objective Addressed Explanation
카카오 로그인 구현 (토큰 요청/응답, SDK 초기화, OAuth 액티비티) [#145]
스플래시에서 카카오 로그인 트리거 및 콜백 처리 [#145]
로그인 응답 토큰 저장(로컬) 및 도메인 연동 [#145]
네트워크에 토큰 전송 및 재발급 처리 연동 [#145]

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
OkHttp 전역 재구성 및 Auth 전용 OkHttp/Retrofit 분리 (app/src/main/java/com/byeboo/app/data/di/NetworkModule.kt) 카카오 로그인 기능 구현 범위에 비해 네트워크 계층 구조 재편(전용 클라이언트/컨버터 배치 등)은 기능 요구서에 명시되지 않은 설계 변경임.
TokenAuthenticator의 전역 재시도/재발급 로직 추가 (app/src/main/java/com/byeboo/app/core/network/TokenAuthenticator.kt) 단순한 카카오 로그인 구현 과제에서 벗어나 전역 인증 재시도 전략(상태/반복 카운트 등)을 도입함.
TokenDataStoreModule의 모듈화 및 DI 노출 (app/src/main/java/com/byeboo/app/data/di/TokenDataStoreModule.kt) 로그인 결과 저장은 요구사항이지만 별도의 모듈화 및 토큰 전용 DataSource/DI는 원 이슈의 최소 요구 범위보다 확장된 작업으로 보임.

Possibly related PRs

Suggested labels

🌎 서버 통신

Suggested reviewers

  • fredleeJH
  • sohee6989

Poem

토끼가 폴짝, 키 해시를 남기네 🥕
카카오 바람에 토큰이 춤추고
액세스는 주머니에, 리프레시는 안전하게
약관 넘으면 집으로, 네비게이션 뿅
토끼는 휘파람 불며 배포를 기다리네

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0dd75e1 and 1e500d5.

📒 Files selected for processing (9)
  • app/src/main/java/com/byeboo/app/data/datasource/local/TokenDataSource.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/datasourceimpl/local/TokenDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/repositoryimpl/auth/TokenRepositoryImpl.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/domain/repository/auth/TokenRepository.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/domain/usecase/LoginUseCase.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/presentation/main/MainNavigator.kt (2 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/SplashScreen.kt (2 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/SplashState.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/SplashViewModel.kt (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#145-kakao-login

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/byeboo/app/data/di/NetworkModule.kt (1)

30-38: 디버그 로그에 Authorization 헤더가 그대로 노출됩니다. 토큰 유출 위험이 큽니다.

HttpLoggingInterceptor 기본 설정으로는 헤더/바디가 평문으로 출력됩니다. 최소한 Authorization 헤더는 마스킹해야 합니다.

권장 패치:

 fun providesLoggingInterceptor(): HttpLoggingInterceptor {
-    return HttpLoggingInterceptor().apply {
-        level = if (BuildConfig.DEBUG) {
-            HttpLoggingInterceptor.Level.BODY
-        } else {
-            HttpLoggingInterceptor.Level.NONE
-        }
-    }
+    return HttpLoggingInterceptor().apply {
+        // 보안: 민감 헤더 마스킹
+        redactHeader("Authorization")
+        level = if (BuildConfig.DEBUG) {
+            HttpLoggingInterceptor.Level.BODY
+        } else {
+            HttpLoggingInterceptor.Level.NONE
+        }
+    }
 }
🧹 Nitpick comments (58)
app/src/main/java/com/byeboo/app/ByeBoo.kt (1)

27-29: KAKAO_NATIVE_APP_KEY 누락/공백 가드 추가 제안

로컬 설정 누락 시 런타임에서만 문제를 인지하게 됩니다. 디버그 빌드에서 즉시 실패시키고, 릴리스에서는 경고 로그만 남기도록 가드를 두면 트러블슈팅이 빨라집니다.

적용 예시:

-    private fun initKakaoSdk() {
-        KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY)
-    }
+    private fun initKakaoSdk() {
+        val appKey = BuildConfig.KAKAO_NATIVE_APP_KEY
+        if (appKey.isBlank()) {
+            // 디버그에서는 빠르게 실패해 설정 누락을 조기 발견
+            if (BuildConfig.DEBUG) error("Missing KAKAO_NATIVE_APP_KEY. local.properties에 kakao.native.app.key를 따옴표 없이 설정하세요.")
+            Timber.w("KAKAO_NATIVE_APP_KEY is blank; KakaoSdk.init skipped.")
+            return
+        }
+        KakaoSdk.init(this, appKey)
+    }

릴리스 서명된 빌드에서도 BuildConfig.KAKAO_NATIVE_APP_KEY가 정상 주입되는지 한 번 확인 부탁드립니다.

app/src/main/java/com/byeboo/app/presentation/main/MainActivity.kt (1)

5-5: 키 해시 로그는 개발용으로 DEBUG 가드 + Timber 일원화 권장

배포 빌드에서 불필요한 로그를 남기지 않도록 DEBUG 가드, 로깅 라이브러리는 Timber로 통일하는 것이 좋습니다.

아래처럼 변경을 제안드립니다.

-import android.util.Log
+import com.byeboo.app.BuildConfig
+import timber.log.Timber
@@
-        val keyHash = Utility.getKeyHash(this)
-        Log.d("KAKAO_KEY_HASH", "keyHash = $keyHash")
+        if (BuildConfig.DEBUG) {
+            val keyHash = Utility.getKeyHash(this)
+            Timber.d("KAKAO_KEY_HASH = %s", keyHash)
+        }

키 해시 값은 디버그/릴리스 서명 키에 따라 달라집니다. Kakao Developers에 두 키 모두 등록이 필요한지 운영 전략을 확인해 주세요.

Also applies to: 11-11, 19-21

app/src/main/java/com/byeboo/app/data/dto/request/auth/KakaoLoginRequestDto.kt (2)

8-10: 플랫폼 문자열의 타입 안정성 강화 제안

platform: String은 오타/허용값 이탈 위험이 있습니다. 백엔드 계약이 고정된 열거 값이라면 enum으로 직렬화 매핑을 권장합니다.

다음과 같이 enum을 도입해 컴파일 타임 안전성을 높일 수 있습니다.

 @Serializable
 data class KakaoLoginRequestDto(
-    @SerialName("platform")
-    val platform: String
+    @SerialName("platform")
+    val platform: AuthPlatformDto
 )
+
+@Serializable
+enum class AuthPlatformDto {
+    @SerialName("KAKAO")
+    KAKAO,
+    @SerialName("APPLE")
+    APPLE,
+    @SerialName("GOOGLE")
+    GOOGLE
+}

참고: 기존 호출부에서 "KAKAO" 리터럴 대신 AuthPlatformDto.KAKAO를 전달하도록 변경이 필요합니다.


8-10: 불필요한 @SerialName 제거 고려

필드명이 서버 스키마와 동일하면 @SerialName("platform")는 생략 가능합니다. 유지 의도가 “서버 스키마 고정”을 명시하는 것이라면 현 상태 유지도 OK입니다.

대안:

-    @SerialName("platform")
-    val platform: String
+    val platform: String
app/src/main/java/com/byeboo/app/data/datasource/local/TokenDataSource.kt (1)

6-9: 널 처리 정책 및 명명 일관성 재검토

  • 빈 문자열로 부재를 표현하면 호출부에서 의미 모호성이 생깁니다. Flow<String?> 또는 Flow<Option<String>>(커스텀) 채택을 고려해 주세요.
  • 스트림이라는 점을 드러내려면 메서드명을 getAccessTokenFlow() 등으로 정리하는 것도 선택지입니다.

가능한 대안:

-interface TokenDataSource {
-    fun getAccessToken(): Flow<String>
-    fun getRefreshToken(): Flow<String>
+interface TokenDataSource {
+    fun getAccessToken(): Flow<String?>
+    fun getRefreshToken(): Flow<String?>
     suspend fun updateTokens(accessToken: String, refreshToken: String)
     suspend fun clearTokens()
 }

변경 시 도메인/리포지토리 레이어의 매핑·null 처리 로직 동반 정리가 필요합니다.

app/src/main/java/com/byeboo/app/presentation/splash/SplashState.kt (2)

7-8: Kakao 로그인 트리거 2가지를 단일 이벤트+파라미터로 축약 제안

StartKakaoTalkLogin / StartKakaoWebLogin의 중복을 줄이기 위해 단일 이벤트에 로그인 방법을 파라미터로 받는 형태를 고려해보세요. 이벤트 개수가 늘어나는 것을 방지하고, when 처리도 간결해집니다.

해당 위치에 대한 최소 변경 diff:

 sealed interface SplashStateSideEffect {
     data object NavigateToHome : SplashStateSideEffect
     data object NavigateToUserInfo : SplashStateSideEffect
     data object NavigateToTermsOfService : SplashStateSideEffect
-    data object StartKakaoTalkLogin : SplashStateSideEffect
-    data object StartKakaoWebLogin : SplashStateSideEffect
+    data class StartKakaoLogin(val method: KakaoLoginMethod) : SplashStateSideEffect
 }

추가 코드(동일 파일 내부 혹은 별도 파일에 정의):

enum class KakaoLoginMethod { TALK, WEB }

3-3: 네이밍 간소화 제안: SplashStateSideEffect → SplashSideEffect

State와 SideEffect가 혼재된 듯한 인상을 줄 수 있어, 보다 직관적인 SplashSideEffect로의 네이밍 단순화를 제안합니다. 리스너/핸들러 측 코드 가독성이 좋아집니다. (대규모 참조 변경이므로 팀 컨벤션 합의 후 고려)

참고 diff:

-sealed interface SplashStateSideEffect {
+sealed interface SplashSideEffect {
-    data object NavigateToHome : SplashStateSideEffect
-    data object NavigateToUserInfo : SplashStateSideEffect
-    data object NavigateToTermsOfService : SplashStateSideEffect
-    data object StartKakaoTalkLogin : SplashStateSideEffect
-    data object StartKakaoWebLogin : SplashStateSideEffect
+    data object NavigateToHome : SplashSideEffect
+    data object NavigateToUserInfo : SplashSideEffect
+    data object NavigateToTermsOfService : SplashSideEffect
+    data object StartKakaoTalkLogin : SplashSideEffect
+    data object StartKakaoWebLogin : SplashSideEffect
 }
app/src/main/java/com/byeboo/app/presentation/main/MainNavigator.kt (1)

136-139: NavOptions 사용 일관성/편의성 개선 제안

현재 일부 메서드는 NavOptions를 nullable/기본값 허용, 일부는 non-null로 강제합니다. navigateToTerms에도 기본 옵션을 내부에서 생성하는 오버로드를 추가하면 호출부 단순화 및 API 일관성이 좋아집니다.

예시(추가 메서드, 참고용):

fun navigateToTerms() {
    val options = navOptions {
        launchSingleTop = true
    }
    navController.navigateToTerms(options)
}
app/src/main/java/com/byeboo/app/core/model/auth/TokenEntity.kt (1)

3-6: 레이어 네이밍/위치에 대한 가벼운 코멘트

Entity 네이밍은 일반적으로 데이터/영속 계층 모델에 쓰는 경우가 많습니다. 도메인/코어 공용 모델이라면 Token 또는 TokenPair 같은 중립적 네이밍을 고려해보세요. (선택사항)

app/src/main/java/com/byeboo/app/domain/model/auth/AuthResult.kt (1)

7-7: 불린 의미 명확화에 대한 가벼운 제안

isRegistered가 “기존 회원 여부”인지 “회원가입 완료 여부(프로필 입력 포함)”인지 혼동 여지가 있습니다. 호출부에서 의미가 모호하다면 hasCompletedOnboarding 등 보다 구체적인 네이밍/코멘트를 고려하세요.

app/src/main/java/com/byeboo/app/presentation/splash/termsofservice/TermsOfServiceViewModel.kt (1)

53-58: 중복 클릭 시 서스펜션 방지: tryEmit + 버퍼 설정 권장

TermsOfServiceViewModel_sideEffect는 현재 기본값(MutableSharedFlow<TermsOfServiceSideEffect>())으로 선언돼 있고, 버튼 클릭 시 emit은 콜렉터가 바쁜 경우 서스펜드됩니다.
TermsOfServiceScreen에서는 LaunchedEffect(Unit) 내에서 viewModel.sideEffect.collect로 제대로 수집되고 있어(콜렉터 부재 이슈 없음), 아래와 같이 변경을 권장드립니다.

• MutableSharedFlow 선언부에 버퍼 추가

- private val _sideEffect = MutableSharedFlow<TermsOfServiceSideEffect>()
+ private val _sideEffect = MutableSharedFlow<TermsOfServiceSideEffect>(
+     replay = 0,
+     extraBufferCapacity = 1
+ )

emittryEmit 교체 (비서스펜딩)

- viewModelScope.launch {
-     _sideEffect.emit(TermsOfServiceSideEffect.NavigateToUserInfo)
- }
+ viewModelScope.launch {
+     _sideEffect.tryEmit(TermsOfServiceSideEffect.NavigateToUserInfo)
+ }

위 조합을 적용하면, 콜렉터가 순간적으로 바쁘거나 일시적으로 없는 경우에도 이벤트가 버퍼에 쌓여 불필요한 서스펜션과 중복 네비게이션을 예방할 수 있습니다.

app/src/main/java/com/byeboo/app/data/di/RepositoryModule.kt (1)

86-88: AuthRepository 바인딩 중복 없음 확인 및 네이밍 컨벤션 통일 제안

  • @Provides로 별도 제공된 AuthRepository 없음 → 중복 충돌 위험 없음
  • RepositoryModule.kt 내 유일한 @Binds 바인딩 확인
  • AuthRepositoryImpl 생성자에 @Inject 어노테이션 적용되어 있음

메서드명 컨벤션을 모두 bindsXxx 형태로 통일하면 모듈 간 가독성이 더욱 좋아집니다.

간단 제안:

-    abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository
+    abstract fun bindsAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository
app/src/main/java/com/byeboo/app/presentation/splash/navigation/SplashNavigation.kt (1)

13-21: 시그니처 확장에 기본값 추가 제안

navigateToTermsOfService 파라미터에 기본값 ={}을 추가하여 바이너리/소스 호환성을 확보할 것을 권장합니다.

  • MainNavHost.kt의 호출부는 이미 navigateToTermsOfService = { … }로 업데이트되어 있어, 누락된 호출부는 없습니다.

적용 예시:

@@ app/src/main/java/com/byeboo/app/presentation/splash/navigation/SplashNavigation.kt
-    navigateToTermsOfService: () -> Unit,
+    navigateToTermsOfService: () -> Unit = {},
app/src/main/java/com/byeboo/app/presentation/splash/termsofservice/navigation/TermsNavigation.kt (3)

3-3: 불필요/착오 import 제거: android.R.attr.padding

Android 프레임워크 attr import는 본 파일에서 사용되지 않으며, padding: Dp 파라미터와 혼동을 유발할 수 있습니다. 제거를 권장합니다.

-import android.R.attr.padding

10-10: 불필요 import 제거: navigateToUserInfo 확장함수

현재 파일에서는 NavController 확장함수를 사용하지 않고 동일한 이름의 람다 파라미터를 전달하고 있습니다. 혼선을 줄이기 위해 import 제거를 권장합니다.

-import com.byeboo.app.presentation.auth.navigation.navigateToUserInfo

19-25: 파라미터 명 변경 시 호출부 동기화 필요

termsGraph 내부 파라미터를 onNavigateToUserInfo로 리네임할 경우, 호출부에서도 동일하게 업데이트해야 합니다. 아래 위치에서 수정이 필요합니다.

  • 파일: app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.kt
  • 라인: 104–106
-        termsGraph(
-            navigateToUserInfo = { navigator.navigateToUserInfo(clearStackNavOptions) },
+        termsGraph(
+            onNavigateToUserInfo = { navigator.navigateToUserInfo(clearStackNavOptions) },
             padding = padding
         )

이렇게 변경하면 콜백 의도가 명확해지고, 확장함수와 람다의 네이밍 충돌도 방지됩니다.

app/src/main/java/com/byeboo/app/data/datasource/remote/auth/AuthRemoteDataSource.kt (3)

8-8: 토큰 파라미터 명을 명확하게 변경 제안 (의미 충돌 방지).

현재 token은 카카오 액세스 토큰을 의미하지만, 앱 내에서도 "액세스 토큰"을 다루기 때문에 혼동 소지가 큽니다. 의미를 분명히 하기 위해 kakaoAccessToken으로 변경을 권장합니다.

아래 diff 제안:

-    suspend fun loginWithKakao(token: String, platform: String): BaseResponse<KakaoLoginResponseDto>
+    suspend fun loginWithKakao(kakaoAccessToken: String, platform: String): BaseResponse<KakaoLoginResponseDto>

8-8: platform String → 타입 세이프한 Enum/값 객체로 변경 고려.

platform: String은 오타/허용되지 않은 값을 컴파일 타임에 잡을 수 없습니다. 서버 스펙이 고정된 값 집합을 요구한다면 enum class Platform { ANDROID, IOS } 등으로 승격해 도메인까지 일관되게 사용하는 것을 권장합니다.

필요 시, KakaoLoginRequestDtoplatform 필드와 매칭되는 허용값 목록(백엔드 스웨거/문서)을 확인해주시면 Enum 정의까지 도와드리겠습니다.


9-9: 리이슈 요청의 인증 스킴 일관성 확인 요청.

reissueAccessToken(refreshToken: String)은 구현부에서 Authorization: Bearer <refreshToken> 형태로 헤더를 구성할 가능성이 높습니다. AuthService에서도 동일 스킴을 전제하므로, 백엔드가 요구하는 정확한 스킴(예: Bearer, Refresh, Token 등)을 재확인해 주세요. 스킴이 다르면 401 루프가 발생할 수 있습니다.

원하시면 DI 모듈/인터셉터 구성을 점검하는 스크립트를 제공해 드립니다.

app/src/main/java/com/byeboo/app/domain/repository/auth/AuthRepository.kt (2)

7-7: 파라미터 명 변경으로 의미 명확화.

데이터소스와 동일하게 tokenkakaoAccessToken으로 변경하면 호출부 가독성과 오용 가능성이 낮아집니다.

아래 diff 제안:

-    suspend fun loginWithKakao(token: String, platform: String): Result<AuthResult>
+    suspend fun loginWithKakao(kakaoAccessToken: String, platform: String): Result<AuthResult>

7-7: platform을 도메인 레벨에서 Enum으로 고정 권장.

도메인 인터페이스 시그니처 단계에서부터 String 대신 Platform 같은 Enum/값 객체를 사용하면 레이어 전반의 타입 안전성이 올라갑니다. 매핑은 DTO에서 처리하세요.

원하시면 Platform Enum과 매퍼 보일러플레이트까지 PR에 맞춰 생성해 드립니다.

app/src/main/java/com/byeboo/app/data/mapper/auth/AuthMapper.kt (1)

13-16: 코드 스타일 일관성(접두 this. 제거) 제안.

같은 파일 내 첫 매퍼와 스타일을 맞추면 가독성이 좋아집니다.

아래 diff 제안:

-fun TokenReissueResponseDto.toDomain(): TokenEntity = TokenEntity(
-    accessToken = this.accessToken,
-    refreshToken = this.refreshToken
-)
+fun TokenReissueResponseDto.toDomain(): TokenEntity = TokenEntity(
+    accessToken = accessToken,
+    refreshToken = refreshToken
+)
app/src/main/java/com/byeboo/app/data/service/auth/AuthService.kt (2)

12-21: 엔드포인트 경로의 선행 슬래시 제거 제안.

Retrofit에서는 일반적으로 상대 경로에 선행 /를 두지 않습니다. 스타일 일관성과 예상치 못한 URL 결합 문제를 피하기 위해 제거를 권장합니다.

아래 diff 제안:

-    @POST("/api/v1/auth/login")
+    @POST("api/v1/auth/login")
 ...
-    @POST("/api/v1/auth/reissue")
+    @POST("api/v1/auth/reissue")

13-16: 인증 스킴 상수화 및 재사용 포인트 제공.

Authorization 값(예: "Bearer ")을 여러 곳에서 문자열로 만들면 실수 여지가 있습니다. 상수화를 통해 단일 출처로 관리하세요. (실 사용은 DataSource/Interceptor에서)

아래와 같이 상수를 둔 뒤, 구현부에서 "$AUTH_SCHEME_BEARER $token" 형태로 사용을 권장합니다.

 interface AuthService {
+    companion object {
+        const val AUTH_SCHEME_BEARER = "Bearer"
+    }
app/src/main/java/com/byeboo/app/domain/usecase/LoginUseCase.kt (1)

13-15: platform은 String 대신 타입 세이프한 Enum/Sealed 클래스로 받는 것을 권장합니다.

문자열 오탈자나 서버 스펙 변경 시 컴파일 타임 검증이 되지 않습니다. AuthPlatform { KAKAO, ... } 등으로 교체를 고려해 주세요. 도메인 모델과 요청 DTO 모두에 동일 타입을 사용하면 일관성이 좋아집니다.

원하시면 Enum 도입과 전체 호출부/DTO 치환에 대한 최소 변경 diff를 준비해 드리겠습니다.

app/src/main/java/com/byeboo/app/presentation/splash/termsofservice/TermsOfServiceScreen.kt (3)

27-27: 불필요한 import 제거 제안

navigateToUserInfo를 직접 호출하지 않으므로 해당 import는 사용되지 않습니다. 정리해 주세요.

-import com.byeboo.app.presentation.auth.navigation.navigateToUserInfo

41-46: sideEffect 수집은 collectLatest로 전환해 중복 트리거를 방지하세요.

빠르게 연속 방출되는 경우 마지막 항목만 처리하는 편이 안전합니다. 또한 LaunchedEffect(Unit) 내 수집은 적절하나, collectLatest가 의도에 더 부합합니다.

-    LaunchedEffect(Unit) {
-        viewModel.sideEffect.collect { effect ->
+    LaunchedEffect(Unit) {
+        viewModel.sideEffect.collectLatest { effect ->
             when (effect) {
                 is TermsOfServiceSideEffect.OpenUrl -> openUrl(context = context, effect.url)
                 is TermsOfServiceSideEffect.NavigateToUserInfo -> navigateToUserInfo()
             }
         }
     }

위 변경을 위해 다음 import가 필요합니다(선택 라인 범위 외 보조 변경):

import kotlinx.coroutines.flow.collectLatest

82-87: 약관 목록이 긴 기기/폰트 환경에서 내용이 하단에서 잘릴 수 있습니다. 스크롤 가능하도록 처리하세요.

현재 Column만 사용하고 있어 컨텐츠 오버플로우 시 접근성이 떨어질 수 있습니다. verticalScroll(rememberScrollState())를 적용해 주세요.

-        Column(
-            modifier = Modifier
-                .padding(horizontal = 24.dp)
-                .padding(top = 67.dp, bottom = padding)
-                .fillMaxSize()
-        ) {
+        Column(
+            modifier = Modifier
+                .padding(horizontal = 24.dp)
+                .padding(top = 67.dp, bottom = padding)
+                .fillMaxSize()
+                .verticalScroll(rememberScrollState())
+        ) {

보조 import:

import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
app/src/main/java/com/byeboo/app/data/di/TokenDataStoreModule.kt (1)

29-34: 바인딩 명확화를 위해 @BINDS + @qualifier 도입을 고려해 주세요.

향후 다른 종류의 DataStore(예: 설정, 프로필 등)가 추가될 때 모호성을 줄이기 위해 토큰 DataStore에 전용 Qualifier(@TokenPreferences 등)를 부여하면 DI가 더 견고해집니다. 또한 인터페이스-구현 매핑은 @Binds가 더 경량입니다(선택).

원하시면 @Qualifier 선언과 모듈 분리를 포함한 최소 변경 diff를 제공하겠습니다.

app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.kt (2)

12-12: 불필요한 import 제거 제안

navigateToUserInfo 심볼은 이 파일에서 직접 참조되지 않습니다(메서드는 navigator.navigateToUserInfo로 호출). 사용되지 않는 import를 정리해 주세요.

-import com.byeboo.app.presentation.auth.navigation.navigateToUserInfo

55-55: 사소한 포매팅

괄호 앞 공백 하나가 있습니다. 팀 포매터 규칙에 맞춰 정리해 주세요.

-            navigateToTermsOfService = { navigator.navigateToTerms(clearStackNavOptions )},
+            navigateToTermsOfService = { navigator.navigateToTerms(clearStackNavOptions) },
app/src/main/java/com/byeboo/app/data/datasourceimpl/local/TokenDataSourceImpl.kt (4)

17-23: Flow를 반환하는 접근자는 suspend가 불필요합니다. 전체 인터페이스에서 제거를 검토해 주세요.

Flow는 콜드 스트림으로, 반환자에 suspend가 붙으면 호출 측에 불필요한 제약을 만들고 API 사용성이 떨어집니다. 가능하다면 인터페이스/구현 모두에서 suspend를 제거하세요.

-    override suspend fun getAccessToken(): Flow<String> = datastore.data.map {
+    override fun getAccessToken(): Flow<String> = datastore.data.map {
         preferences -> preferences[ACCESS_TOKEN] ?: ""
     }

-    override suspend fun getRefreshToken(): Flow<String> = datastore.data.map {
+    override fun getRefreshToken(): Flow<String> = datastore.data.map {
         preferences -> preferences[REFRESH_TOKEN] ?: ""
     }

참고: 인터페이스(TokenDataSource, TokenRepository) 시그니처 변경이 수반됩니다.


9-9: 사용되지 않는 import 제거

firstOrNull는 이 파일에서 사용되지 않습니다.

-import kotlinx.coroutines.flow.firstOrNull

25-37: 코드 스타일 및 원자적 갱신 확인

  • edit{edit { 등 간격 일관성을 맞춰 주세요.
  • 두 토큰을 하나의 edit {} 블록에서 갱신하고 있어 원자성이 확보되어 좋습니다. 이 패턴을 유지해 주세요.
-        datastore.edit{ preferences ->
+        datastore.edit { preferences ->
             preferences[ACCESS_TOKEN] = accessToken
             preferences[REFRESH_TOKEN] = refreshToken
         }
...
-        datastore.edit { preferences->
+        datastore.edit { preferences ->
             preferences.remove(ACCESS_TOKEN)
             preferences.remove(REFRESH_TOKEN)
         }

39-42: 토큰 보안 저장소 고려

현재 Preferences DataStore는 평문 저장입니다. 리스크 프로파일에 따라 다음을 검토해 주세요.

  • Jetpack Security Crypto + EncryptedSharedPreferences
  • Proto DataStore + 커스텀 암복호화 Serializer(Tink)
  • 최소한 스냅샷/백업 시 제외(백업 정책 확인)

원하시면 Tink 기반 Proto DataStore 예제를 이 구조에 맞춰 제공하겠습니다.

app/src/main/java/com/byeboo/app/core/network/AuthInterceptor.kt (2)

16-18: Interceptor에서 runBlocking 사용은 불가피하나, IO 회피를 위한 메모리 캐시 경로 추가 권장

OkHttp Interceptor는 동기 컨텍스트이므로 runBlocking 사용 자체는 일반적입니다. 다만 매 요청 시 DataStore 등 영속 저장소 접근이 발생하면 지연이 커질 수 있으니, TokenRepository에 “메모리 캐시 조회(peek)” API를 추가하고 우선 사용하도록 고려해 주세요.

예) TokenRepository에 아래 시그니처 추가 후, 우선 캐시 조회:

  • fun peekAccessToken(): String?
  • suspend fun getAccessToken(): String // 캐시 미스 시에만 suspend 경로

28-31: AUTHORIZATION/BEARER 상수 중복 정의 정리

동일 상수가 TokenAuthenticator에도 존재합니다. 상수 충돌 및 오타 리스크를 줄이기 위해 공용 모듈(예: core/network/AuthHeaders.kt)로 이동해 단일 출처로 관리해 주세요.

app/src/main/java/com/byeboo/app/data/repositoryimpl/auth/AuthRepositoryImpl.kt (1)

22-25: 재발급 응답의 data null 가능성 대비(선택)

현재 .data.toDomain()에서 data가 null이면 예외가 납니다(runCatching으로 Failure 처리되긴 함). 필요하다면 서버 규약을 반영해 명시적으로 null 가드 또는 도메인화된 에러로 매핑해 디버깅 가독성을 올릴 수 있습니다.

app/src/main/java/com/byeboo/app/presentation/splash/SplashScreen.kt (2)

48-70: sideEffect collect 범위 안정화 고려

LaunchedEffect(Unit) + collect 패턴은 동작하나, 수명 주기를 의식한 collect(예: lifecycleOwner.repeatOnLifecycle)로 교체하면 화면 회전/백그라운드 복귀 시 중복 수집 가능성을 더 확실히 다룹니다. 현재 구조로도 큰 문제는 없으므로 선택 사항입니다.


131-135: 하드코딩 문자열/콘텐츠 설명 → 리소스화

사용자 노출 문자열과 contentDescription을 string 리소스로 이전해 현지화/접근성 품질을 높여 주세요.

예)

  • text = stringResource(R.string.splash_kakao_login)
  • contentDescription = stringResource(R.string.cd_kakao_logo)
app/src/main/java/com/byeboo/app/data/datasourceimpl/remote/auth/AuthRemoteDataSourceImpl.kt (1)

26-28: BEARER 상수의 중복 정의 제거

여기서도 BEARER를 별도로 가집니다. Interceptor/Authenticator와 동일 상수를 공용 위치로 이동해 단일화해 주세요.

app/src/main/java/com/byeboo/app/core/network/TokenAuthenticator.kt (4)

18-38: 동시 401 대응 시 토큰 재발급 단일화(Mutex)로 중복 호출/토큰 경합 방지

여러 요청이 동시에 401을 받으면 재발급 API가 다중 호출되어 마지막 저장 토큰 경합이 생길 수 있습니다. 전역 Mutex로 authenticate 블록을 직렬화하는 방식을 권장합니다. 또한 공백 refreshToken도 무시하도록 보강해 주세요.

아래와 같이 적용 가능합니다:

@@
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
@@
 class TokenAuthenticator @Inject constructor(
@@
 ) : Authenticator {
+    private val refreshMutex = Mutex()
@@
-    override fun authenticate(route: Route?, response: Response): Request? = runBlocking {
-        if (responseCount(response) >= 2) return@runBlocking null
-
-        val refreshToken = tokenRepository.getRefreshToken().firstOrNull() ?: return@runBlocking null
-
-        val result = authRepository.reissueAccessToken(refreshToken)
-
-        val newAuthenticatedToken = result.getOrNull()
-
-        if (newAuthenticatedToken == null) {
-            tokenRepository.clearTokens()
-            return@runBlocking null
-        }
-
-        tokenRepository.saveTokens(newAuthenticatedToken)
-
-        response.request.newBuilder()
-            .removeHeader(AUTHORIZATION)
-            .addHeader(AUTHORIZATION, "$BEARER ${newAuthenticatedToken.accessToken}")
-            .build()
-    }
+    override fun authenticate(route: Route?, response: Response): Request? = runBlocking {
+        refreshMutex.withLock {
+            if (responseCount(response) >= 2) return@withLock null
+
+            val refreshToken = tokenRepository.getRefreshToken()
+                .firstOrNull()
+                ?.takeIf { it.isNotBlank() }
+                ?: return@withLock null
+
+            val result = authRepository.reissueAccessToken(refreshToken)
+            val newAuthenticatedToken = result.getOrNull() ?: run {
+                tokenRepository.clearTokens()
+                return@withLock null
+            }
+
+            tokenRepository.saveTokens(newAuthenticatedToken)
+
+            response.request.newBuilder()
+                .removeHeader(AUTHORIZATION)
+                .addHeader(AUTHORIZATION, "$BEARER ${newAuthenticatedToken.accessToken}")
+                .build()
+        }
+    }

40-49: 지역 변수명이 매개변수와 동일하여 가독성 저하

var response: Response? = response는 섀도잉으로 혼동됩니다. 지역 변수를 다른 이름으로 변경해 주세요.

-    private fun responseCount(response: Response): Int {
-        var response: Response? = response
+    private fun responseCount(response: Response): Int {
+        var current: Response? = response
         var count = 1
-
-        while (response?.priorResponse != null) {
-            count++
-            response = response.priorResponse
+        while (current?.priorResponse != null) {
+            count++
+            current = current.priorResponse
         }
         return count
     }

18-38: 재발급 요청의 자기-참조 루프 회피 전략 제안

동일 OkHttp 클라이언트를 통해 재발급 API를 호출하면, 재발급 요청이 또 401일 때 Authenticator가 다시 개입하여 루프가 생길 수 있습니다. 다음 중 하나를 고려해 주세요.

  • 재발급 전용 OkHttp/Retrofit 클라이언트(Authenticator/Interceptor 미부착) 사용
  • 재발급 엔드포인트에 대해 authenticate에서 early-return 처리

51-54: AUTHORIZATION/BEARER 상수 공용화

AuthInterceptor와 동일 제안: 공용 상수로 통합해 관리하세요.

app/src/main/java/com/byeboo/app/domain/repository/auth/TokenRepository.kt (2)

7-8: Flow를 반환하는 API에 suspend 불필요 — 호출 측에 불필요한 제약을 줍니다.

Flow 자체가 지연(cold) 스트림이라 수집 시점에만 일시중단이 필요합니다. 시그니처에서 suspend를 제거해 주세요. 구현체도 함께 정리 필요.

적용 diff:

-    suspend fun getAccessToken(): Flow<String>
-    suspend fun getRefreshToken(): Flow<String>
+    fun getAccessToken(): Flow<String>
+    fun getRefreshToken(): Flow<String>

11-13: 캐시 제어 API가 도메인 레이어 인터페이스에 노출되어 있습니다.

initCachedAccessToken, updateCachedAccessToken, getCachedAccessToken는 기술적 관점(캐시 전략)에 가까워 도메인 계약에 포함되면 결합도가 커집니다. 네트워크 계층에서만 필요하다면 별도 TokenProvider(core/network 쪽)로 역할을 분리하거나, 최소한 updateCachedAccessToken는 구현체 내부로 숨기는 것을 권장합니다.

app/src/main/java/com/byeboo/app/data/di/NetworkModule.kt (3)

40-45: kotlinx.serialization Json 설정 누락 — 서버 필드 변경에 취약합니다.

ignoreUnknownKeys 등 안전한 디폴트를 켜두면 호환성이 좋아집니다.

간단 적용:

-@OptIn(ExperimentalSerializationApi::class)
-@Provides
-@Singleton
-fun providesConverterFactory(): Converter.Factory = Json.asConverterFactory(
-    "application/json".toMediaType()
-)
+@OptIn(ExperimentalSerializationApi::class)
+@Provides
+@Singleton
+fun providesConverterFactory(): Converter.Factory =
+    Json {
+        ignoreUnknownKeys = true
+        explicitNulls = false
+        coerceInputValues = true
+    }.asConverterFactory("application/json".toMediaType())

58-67: DI 바인딩 중복 가능성 — 타입을 상위 인터페이스로 맞추는 편이 깔끔합니다.

이미 provideTokenAuthenticatorAuthenticator 바인딩을 제공합니다. providesOkHttpClient 파라미터 타입도 Authenticator로 올리면 중복 생성/바인딩을 줄일 수 있습니다.

적용:

 fun providesOkHttpClient(
-    authInterceptor: Interceptor,
-    tokenAuthenticator: TokenAuthenticator,
+    authInterceptor: Interceptor,
+    authenticator: Authenticator,
     loggingInterceptor: HttpLoggingInterceptor
 ): OkHttpClient =
     OkHttpClient.Builder()
         .addInterceptor(loggingInterceptor)
         .addInterceptor(authInterceptor)
-        .authenticator(tokenAuthenticator)
+        .authenticator(authenticator)
         .build()

63-67: 네트워크 타임아웃 기본값 명시 권장.

기본 타임아웃은 환경별로 달라질 수 있어 장애 시 디버깅이 어렵습니다. 적정 값 명시를 권장합니다.

예시:

 OkHttpClient.Builder()
+    .connectTimeout(10, TimeUnit.SECONDS)
+    .readTimeout(10, TimeUnit.SECONDS)
+    .writeTimeout(10, TimeUnit.SECONDS)
app/src/main/java/com/byeboo/app/presentation/splash/SplashViewModel.kt (3)

35-43: 단순 side-effect 전파에 launch 사용: tryEmit로 간소화 가능.

UI 수집자가 활성 상태라면 tryEmit이 더 단순하고 불필요한 코루틴 생성을 줄입니다. 실패 시 로그만 남기고 무시해도 되는 성격이라면 고려해보세요.

예시:

 fun startKakaoLogin(isLoginAvailable: Boolean) {
-    viewModelScope.launch {
-        if (isLoginAvailable) {
-            _sideEffect.emit(SplashStateSideEffect.StartKakaoTalkLogin)
-        } else {
-            _sideEffect.emit(SplashStateSideEffect.StartKakaoWebLogin)
-        }
-    }
+    if (isLoginAvailable) {
+        _sideEffect.tryEmit(SplashStateSideEffect.StartKakaoTalkLogin)
+    } else {
+        _sideEffect.tryEmit(SplashStateSideEffect.StartKakaoWebLogin)
+    }
 }

71-74: 실패 분기에서 사용자 피드백이 없습니다.

else 분기(카카오 로그인 실패)에 대한 스낵바/토스트 등 UX 피드백 또는 재시도 유도 액션을 추가해 주세요.

필요하시면 공통 에러 핸들러/SideEffect 설계를 제안해드릴게요.


78-80: 플랫폼 문자열 상수 대신 타입 세이프한 모델 사용 권장.

문자열 오타에 취약합니다. enum class Platform { KAKAO, ... } 또는 @StringDef/sealed class로 교체를 권장합니다.

app/src/main/java/com/byeboo/app/data/repositoryimpl/auth/TokenRepositoryImpl.kt (5)

16-19: Flow 반환 함수에 suspend 불필요 — 인터페이스와 함께 정리하세요.

인터페이스 수정과 일관되게 구현체도 suspend 제거가 필요합니다.

적용:

-    override suspend fun getAccessToken(): Flow<String> = tokenDataSource.getAccessToken()
+    override fun getAccessToken(): Flow<String> = tokenDataSource.getAccessToken()
 
-    override suspend fun getRefreshToken(): Flow<String> = tokenDataSource.getRefreshToken()
+    override fun getRefreshToken(): Flow<String> = tokenDataSource.getRefreshToken()

14-14: 가벼운 캐시에는 @Volatile로 충분하나, AtomicReference 고려 여지.

현재 쓰기/읽기 경쟁에서 원자성은 충분합니다. 다만 향후 토큰 회전 시 동시 갱신/읽기 로직이 복잡해지면 AtomicReference<String>로 전환하면 의도를 더 명확히 표현할 수 있습니다.


30-33: 초기 캐시 로드 시 firstOrNull() 사용 적절. 단, 호출 스레드 주의.

UI 스레드 블로킹은 아니지만, initCachedAccessToken() 호출이 메인 디스패처에서 실행될 수 있으므로 I/O 디스패처로 전환해두면 안전합니다(특히 DataStore가 느린 디바이스에서 초기 Read 비용이 있을 수 있음).

예시(구현 내부):

withContext(Dispatchers.IO) {
    cachedAccessToken = tokenDataSource.getAccessToken().firstOrNull().orEmpty()
}

20-28: 토큰 영속화 순서 OK. 예외 처리 보완 여지.

영속화 실패 시 캐시 업데이트가 스킵되도록 현재 순서가 맞습니다. 다만 저장/삭제 실패를 로깅하거나 도메인 예외로 변환해 상위 레이어(Authenticator/UseCase)에 신호를 주면 복구 시나리오 설계가 수월합니다.


34-38: 캐시 갱신 API의 가시성 검토.

updateCachedAccessToken는 구현 디테일입니다. 인터페이스에서 제거하고 구현체 내부로 한정하면 오남용을 줄일 수 있습니다. 상위 레이어는 saveTokens만 호출해도 일관 상태가 유지되도록 두는 편이 안전합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 692c52d and 0dd75e1.

📒 Files selected for processing (38)
  • app/build.gradle.kts (2 hunks)
  • app/src/main/AndroidManifest.xml (1 hunks)
  • app/src/main/java/com/byeboo/app/ByeBoo.kt (3 hunks)
  • app/src/main/java/com/byeboo/app/core/model/auth/TokenEntity.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/core/network/AuthInterceptor.kt (2 hunks)
  • app/src/main/java/com/byeboo/app/core/network/TokenAuthenticator.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/core/network/qualifier/NetworkQualifier.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/datasource/local/TokenDataSource.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/datasource/remote/auth/AuthRemoteDataSource.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/datasourceimpl/local/TokenDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/datasourceimpl/remote/auth/AuthRemoteDataSourceImpl.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/di/DataSourceModule.kt (3 hunks)
  • app/src/main/java/com/byeboo/app/data/di/NetworkModule.kt (3 hunks)
  • app/src/main/java/com/byeboo/app/data/di/RepositoryModule.kt (3 hunks)
  • app/src/main/java/com/byeboo/app/data/di/ServiceModule.kt (2 hunks)
  • app/src/main/java/com/byeboo/app/data/di/TokenDataStoreModule.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/dto/request/auth/KakaoLoginRequestDto.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/dto/response/auth/KakaoLoginResponseDto.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/dto/response/auth/TokenReissueResponseDto.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/mapper/auth/AuthMapper.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/repositoryimpl/auth/AuthRepositoryImpl.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/repositoryimpl/auth/TokenRepositoryImpl.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/data/service/auth/AuthService.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/domain/model/auth/AuthResult.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/domain/repository/auth/AuthRepository.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/domain/repository/auth/TokenRepository.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/domain/usecase/LoginUseCase.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/presentation/main/MainActivity.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.kt (3 hunks)
  • app/src/main/java/com/byeboo/app/presentation/main/MainNavigator.kt (2 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/SplashScreen.kt (3 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/SplashState.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/SplashViewModel.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/navigation/SplashNavigation.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/termsofservice/TermsOfServiceScreen.kt (3 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/termsofservice/TermsOfServiceState.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/termsofservice/TermsOfServiceViewModel.kt (1 hunks)
  • app/src/main/java/com/byeboo/app/presentation/splash/termsofservice/navigation/TermsNavigation.kt (2 hunks)
🔇 Additional comments (25)
app/src/main/java/com/byeboo/app/ByeBoo.kt (1)

16-16: 앱 시작 시 Kakao SDK 초기화 위치 적절함

앱 전역(Application)에서 초기화하는 방향 맞습니다. 기존 초기화 순서(로그 → 다크모드)와 충돌도 없습니다.

app/build.gradle.kts (1)

39-42: BuildConfig/Manifest 연동 방식 적절

  • BuildConfig에 문자열 리터럴로 주입하는 방식과
  • 동일 값을 manifestPlaceholders로 전달해 딥링크 스킴 구성

둘 다 Kakao 가이드에 부합합니다.

manifestPlaceholders로 주입된 스킴이 실제 설치 APK의 AndroidManifest에 kakao<앱키>로 반영되는지 한 번 추출/검증해 주세요(./gradlew :app:processDebugMainManifest 후 빌드 아웃풋 확인).

app/src/main/AndroidManifest.xml (1)

30-43: AuthCodeHandlerActivity 선언 OK + 최소 검증 체크리스트

Kakao OAuth 리다이렉트 처리를 위한 Activity 선언이 가이드에 맞습니다(exported="true", BROWSABLE 카테고리, 스킴 kakao${KAKAO_NATIVE_APP_KEY}).

통합 테스트 시 아래를 확인해 주세요.

  • KakaoTalk 설치 O: 인앱 → 카카오앱 → 복귀까지 정상 리다이렉트
  • KakaoTalk 설치 X: Chrome Custom Tabs/브라우저 → 복귀 정상
  • 잘못된/미등록 키해시: SDK에서 에러 콜백 수신 및 UI 오류 처리(스낵바 TODO 반영 필요)
  • 스킴 충돌 여부: 다른 딥링크 인텐트와 충돌 없는지
  • 다중 프로세스/다중 태스크 환경에서 재진입 시 이 액티비티가 히스토리에 남지 않는지(현재 설정은 기본이므로 문제 없을 가능성이 높습니다)
app/src/main/java/com/byeboo/app/presentation/splash/termsofservice/TermsOfServiceState.kt (2)

19-22: 사이드이펙트 모델링 방향성 좋습니다

네비게이션과 외부 열기 액션을 sealed interface로 분리해 타입 안정성과 가독성이 좋아졌습니다. 화면-네비게이션 모듈 간 결합도도 낮아집니다.


21-21: Kotlin/AGP 버전 확인 필요
위 스크립트 실행 결과가 없어 자동 검출이 불가합니다. 아래 항목을 수동으로 확인해 주세요.

  • 루트 build.gradle 또는 build.gradle.kts 파일에서
    • org.jetbrains.kotlin:kotlin-gradle-plugin 버전(1.9 이상 여부)
    • com.android.tools.build:gradle(Android Gradle Plugin) 버전
  • 필요 시 호환성 유지를 위해 data object를 일반 object로 변경 권장
app/src/main/java/com/byeboo/app/core/network/qualifier/NetworkQualifier.kt (2)

1-7: Qualifier 분리 👍

인증 전용 네트워크 객체 분리를 위한 @Auth Qualifier 추가는 DI 그래프 가독성과 오배선 방지에 효과적입니다. 네이밍도 직관적입니다.


5-7: Retention 설정 일관성 확인 및 KDoc 추가 권장

  • 현재 전체 코드베이스에서 @Qualifier 애노테이션은 NetworkQualifier.ktAuth에만 사용되고 있으며, 모두 AnnotationRetention.BINARY로 설정되어 있습니다.
    따라서 팀 내 다른 Qualifier들과의 Retention 정책은 일관적이며, Hilt/Dagger 환경에서도 BINARY Retention은 정상 동작합니다.

  • 선택 사항으로, 아래와 같이 KDoc을 추가하면 Auth Qualifier의 목적이 더 명확해집니다:

 @Qualifier
 @Retention(AnnotationRetention.BINARY)
-annotation class Auth
+/**
+ * 인증 전용 네트워크 객체 식별자.
+ * - 예: Auth용 OkHttpClient/Retrofit 바인딩 구분
+ */
+annotation class Auth
app/src/main/java/com/byeboo/app/data/dto/response/auth/TokenReissueResponseDto.kt (1)

7-12: 토큰 재발급 DTO 구성 적절

백엔드 응답 스키마에 맞춰 access/refresh 토큰만을 보유하도록 단순화된 모델이 명확합니다. 도메인 매퍼를 통한 변환과 결합해 유지보수가 용이합니다.

app/src/main/java/com/byeboo/app/presentation/splash/SplashState.kt (1)

3-8: 사이드이펙트 타입 분리 깔끔합니다

네비게이션/로그인 트리거를 상태와 분리한 sealed interface 설계가 명확합니다. UI에서 일회성 이벤트로 안전하게 처리하기 좋습니다.

app/src/main/java/com/byeboo/app/presentation/main/MainNavigator.kt (2)

136-139: Terms 네비게이션 엔트리 추가 적절

MainNavigator에서 Terms로의 진입점을 명시적으로 노출한 점 좋습니다. 호출부가 NavController 확장에 직접 의존하지 않아 테스트/교체 용이합니다.


30-30: 확인 완료: navigateToTerms 확장 함수 및 Terms 목적지 정상 등록됨

  • app/src/main/java/com/byeboo/app/presentation/splash/termsofservice/navigation/TermsNavigation.kt(15–17행)에서 fun NavController.navigateToTerms 정의 확인
  • app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.kt(104–107행)에서 termsGraph 호출로 Terms 목적지 등록 확인

런타임 네비게이션 예외 없이 정상 동작이 보장됩니다.

app/src/main/java/com/byeboo/app/data/di/ServiceModule.kt (2)

66-70: @Auth 전용 Retrofit으로 AuthService 분리 제공한 점 좋습니다

로그인/재발급 경로를 일반 API 클라이언트와 분리해 사이드 이펙트(인터셉터/인증기 충돌)를 줄일 수 있습니다.


66-70: Auth 전용 클라이언트에 인증 인터셉터·Authenticator 미부착 확인

  • NetworkModule.ktprovidesOkHttpAuthClient(@Auth) 빌더는 오직 HttpLoggingInterceptor만 추가하며, AuthInterceptorTokenAuthenticator를 전혀 붙이지 않습니다.
  • 이어서 provideAuthRetrofit(@Auth client) 역시 별도 인터셉터나 Authenticator 없이 Retrofit을 구성합니다.

따라서 AuthService 호출 시 액세스 토큰 인터셉터를 타거나 TokenAuthenticator가 재발급을 무한 반복하는 문제는 발생하지 않습니다.

app/src/main/java/com/byeboo/app/domain/model/auth/AuthResult.kt (1)

5-8: 도메인 결과 타입 구성 적절

토큰 정보 + 가입여부 플래그로 호출부 의사결정이 명확합니다. TokenEntity의 toString 마스킹이 반영되면, 본 타입의 로깅 안전성도 확보됩니다.

app/src/main/java/com/byeboo/app/data/di/DataSourceModule.kt (1)

62-64: 검증 완료: AuthRemoteDataSource DI 구성 적절
아래 사항을 확인했으며, 의존성 그래프가 올바르게 구성되어 있습니다.

  • AuthRemoteDataSourceImpl 클래스에 @Inject constructor(authService: AuthService) 생성자 주입 확인
  • AuthRemoteDataSource 인터페이스 정의 존재 확인
  • ServiceModule.kt에서 @Provides @Singleton fun providesAuthService(@Auth retrofit: Retrofit): AuthService 바인딩 확인
  • DataSourceModule.kt에서 @Binds @Singleton abstract fun bindAuthRemoteDataSource(impl: AuthRemoteDataSourceImpl): AuthRemoteDataSource 바인딩 확인

이상으로 추가 조치 없이 머지 가능합니다.

app/src/main/java/com/byeboo/app/data/dto/response/auth/KakaoLoginResponseDto.kt (2)

6-14: DTO 구성 깔끔합니다.

직렬화 어노테이션과 필드 구성 모두 명확하며, 도메인 매핑과도 일치합니다.


6-14: 서버 응답 키 이름(케이스) 재확인 요청.

현재 @SerialName("accessToken" | "refreshToken" | "isRegistered")로 카멜 케이스를 사용합니다. 백엔드가 스네이크(access_token)를 반환하면 역직렬화가 실패합니다. 스웨거/샘플 응답으로 키 이름을 확인해 주세요.

확인 후 필요 시 다음과 같이 조정합니다:

-    @SerialName("accessToken")
+    @SerialName("access_token")
     val accessToken: String,
-    @SerialName("refreshToken")
+    @SerialName("refresh_token")
     val refreshToken: String,
-    @SerialName("isRegistered")
+    @SerialName("is_registered")
     val isRegistered: Boolean,
app/src/main/java/com/byeboo/app/data/mapper/auth/AuthMapper.kt (1)

8-11: 도메인 매핑 LGTM.

AuthResult로의 변환이 직관적이고 중복 로직이 없습니다.

app/src/main/java/com/byeboo/app/data/service/auth/AuthService.kt (1)

11-22: AuthService용 OkHttpClient/Retrofit 분리 상태 확인 완료

검토 결과, AuthService는 @Auth qualifier가 붙은 전용 OkHttpClient 및 Retrofit 인스턴스를 사용하도록 DI가 설정되어 있습니다.

  • @Auth OkHttpClient(providesOkHttpAuthClient)에는 AuthInterceptor가 적용되지 않고, 오직 loggingInterceptor만 등록되어 있습니다.
  • @Auth Retrofit(providerAuthRetrofit)도 이 전용 클라이언트를 사용하며, ServiceModule에서 AuthService를 주입할 때 @Auth Retrofit을 참조합니다.

따라서 loginWithKakaoreissueAccessToken 호출 시에는 기본 AuthInterceptor가 작동하지 않아, 수동으로 넘기는 @Header("Authorization") 값과 중복되거나 덮어쓰이는 일이 없습니다. 문제 없음을 확인했습니다.

app/src/main/java/com/byeboo/app/data/di/TokenDataStoreModule.kt (1)

17-19: preferencesDataStore 중복 정의 없음

두 개의 선언은 서로 다른 파일(TokenDataStoreModule.kt, HeartzDataStoreModule.kt)에 각각 한 번씩만 존재하며, name 값("token_preferences" vs "heartz_datastore")도 다르므로 동일 이름으로 인한 런타임 이슈가 발생하지 않습니다. 이대로 머지 진행해 주세요.

app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.kt (1)

27-31: popUpTo(0)으로 전체 백스택 초기화: Navigation Compose 2.8.9 동작 확인 필요

현재 프로젝트에서는 libs.versions.toml에 정의된 대로 Navigation Compose 버전이 2.8.9입니다.
이 버전에서 popUpTo(0) { inclusive = true }가 전체 백스택을 안전하게 초기화하는지 공식 문서를 통해 반드시 검증해 주세요.

 val clearStackNavOptions = navOptions {
-    popUpTo(0) { inclusive = true }
+    // Navigation Compose 2.8.9에서 ID 0 동작 확인 후 필요 시 교체
+    popUpTo(0) { inclusive = true }
     launchSingleTop = true
     restoreState = true
 }

확인 완료 후 동작이 불명확하거나 변경이 필요하다면, 위 제안된 대체 방안으로 수정해 주세요.

app/src/main/java/com/byeboo/app/data/datasourceimpl/local/TokenDataSourceImpl.kt (1)

17-23: AuthInterceptor의 빈 문자열 필터링 로직이 올바르게 동작함이 확인되었습니다
AuthInterceptor에서는 accessToken.isNotBlank() 조건을 사용하여 빈 문자열 또는 공백만으로 구성된 토큰의 경우 헤더를 추가하지 않도록 구현되어 있습니다. 따라서 기본값인 ""(빈 문자열)이 반환되더라도 Authorization: Bearer 가 전송되지 않습니다.

  • app/src/main/java/com/byeboo/app/core/network/AuthInterceptor.kt
    if (accessToken.isNotBlank()) {
        header(AUTHORIZATION, "$BEARER $accessToken")
    }

위와 같이 isNotBlank()를 사용해 공백·빈 문자열을 엄격히 필터링하므로, 해당 우려는 해소되었습니다.

app/src/main/java/com/byeboo/app/data/repositoryimpl/auth/AuthRepositoryImpl.kt (1)

14-21: 카카오 로그인 매핑 경로 깔끔합니다

Remote → DTO → Domain 매핑 흐름이 간결하고 runCatching으로 예외 경로를 Result로 수렴한 점 좋습니다.

app/src/main/java/com/byeboo/app/presentation/splash/SplashScreen.kt (1)

56-60: 카카오 로그인 콜백 서명 확인 완료

SplashViewModel.ktupdateLoginResult 함수 시그니처가

fun updateLoginResult(token: OAuthToken?, error: Throwable?)

로 정의되어 있어, Kakao SDK의 (OAuthToken?, Throwable?) -> Unit 콜백 서명과 정확히 일치합니다. 런타임 오류 발생 우려가 없으므로 해당 검토 사항은 해소되었습니다.

app/src/main/java/com/byeboo/app/data/datasourceimpl/remote/auth/AuthRemoteDataSourceImpl.kt (1)

18-21: 로그인/재발급 시 Authorization 수동 지정은 OK — 단, Interceptor의 덮어쓰기 가드와 함께 가야 안전

여기서 Retrofit 메서드로 설정한 Authorization 헤더가 Interceptor에서 다시 접근 토큰으로 교체되면 요청이 실패합니다. AuthInterceptor에 “기존 헤더가 없을 때만 주입” 가드를 반드시 적용해 주세요(별도 코멘트 참고).

Also applies to: 23-24

Comment on lines +34 to +35
val kakaoNativeAppKey = properties["kakao.native.app.key"].toString()

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

local.properties 누락 시 NPE 가능성 → 명시적 검증으로 빌드 타임 실패 처리

properties["kakao.native.app.key"].toString()는 키가 없으면 NPE가 납니다. 빌드 타임에 명확한 메시지로 실패하도록 바꾸는 것을 권장합니다. PR 설명에도 “따옴표 없이”라고 되어 있으니 메시지에 함께 명시합니다.

아래처럼 변경을 제안드립니다.

-        val kakaoNativeAppKey = properties["kakao.native.app.key"].toString()
+        val kakaoNativeAppKey = properties.getProperty("kakao.native.app.key")
+            ?: throw GradleException(
+                "Missing 'kakao.native.app.key' in local.properties. 값은 따옴표 없이 입력하세요. 예) kakao.native.app.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+            )

추가로, 설정 캐시 친화적으로 가려면 providers.gradleProperty(...)를 쓰는 방식으로의 전환도 고려해볼 수 있습니다(선택).

📝 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
val kakaoNativeAppKey = properties["kakao.native.app.key"].toString()
val kakaoNativeAppKey = properties.getProperty("kakao.native.app.key")
?: throw GradleException(
"Missing 'kakao.native.app.key' in local.properties. 값은 따옴표 없이 입력하세요. 예) kakao.native.app.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
)
🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 34-35,
properties["kakao.native.app.key"].toString() will NPE if the key is missing;
change to explicitly validate and fail the build with a clear message (e.g.
throw GradleException or use requireNotNull) that states the property must be
present in local.properties without quotes (e.g. kakao.native.app.key=VALUE),
and optionally consider switching to
providers.gradleProperty("kakao.native.app.key").forUseAtConfigurationTime() to
be settings-cache friendly.

Comment on lines +3 to +6
data class TokenEntity(
val accessToken: String,
val refreshToken: String
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

액세스/리프레시 토큰의 로그 유출 가능성 차단 필요

data class는 기본 toString/복사에서 민감값이 그대로 노출됩니다. 토큰이 로그/크래시리포팅에 유출되면 보안 사고로 직결됩니다. toString을 명시적으로 마스킹하세요.

권장 최소 변경 diff:

 data class TokenEntity(
     val accessToken: String,
     val refreshToken: String
-)
+) {
+    override fun toString(): String = "TokenEntity(accessToken=**REDACTED**, refreshToken=**REDACTED**)"
+}

추가로:

  • 로깅 정책에서 AuthResult/TokenEntity 객체를 직접 로그로 남기지 않도록 주의.
  • 데이터 저장소(DataStore 등)에는 가능하면 암호화 레이어 사용 고려.
📝 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
data class TokenEntity(
val accessToken: String,
val refreshToken: String
)
data class TokenEntity(
val accessToken: String,
val refreshToken: String
) {
override fun toString(): String =
"TokenEntity(accessToken=**REDACTED**, refreshToken=**REDACTED**)"
}
🤖 Prompt for AI Agents
In app/src/main/java/com/byeboo/app/core/model/auth/TokenEntity.kt around lines
3-6, the data class exposes sensitive token values via auto-generated
toString/copy; override toString to mask accessToken and refreshToken (e.g. show
only last 4 chars or a fixed placeholder) so logs/crash reports do not contain
raw tokens, and consider replacing the data class with a regular class if you
need to prevent copy/componentN leakage; additionally ensure
logging/serialization never logs the whole object and consider adding encryption
for persistent storage.

Comment on lines +6 to +7
suspend fun getAccessToken(): Flow<String>
suspend fun getRefreshToken(): Flow<String>
Copy link

@coderabbitai coderabbitai bot Aug 26, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Flow 반환 메서드의 suspend 제거

Flow는 지연 연산으로, 선언 자체에 suspend가 필요 없습니다. 현재 시그니처는 호출부에 불필요한 suspend 전파를 유도합니다.

다음과 같이 변경을 제안합니다(구현/호출부 동반 수정 필요):

 interface TokenDataSource {
-    suspend fun getAccessToken(): Flow<String>
-    suspend fun getRefreshToken(): Flow<String>
+    fun getAccessToken(): Flow<String>
+    fun getRefreshToken(): Flow<String>
     suspend fun updateTokens(accessToken: String, refreshToken: String)
     suspend fun clearTokens()
 }
📝 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
suspend fun getAccessToken(): Flow<String>
suspend fun getRefreshToken(): Flow<String>
interface TokenDataSource {
fun getAccessToken(): Flow<String>
fun getRefreshToken(): Flow<String>
suspend fun updateTokens(accessToken: String, refreshToken: String)
suspend fun clearTokens()
}
🤖 Prompt for AI Agents
In app/src/main/java/com/byeboo/app/data/datasource/local/TokenDataSource.kt
around lines 6-7, the two method signatures declare suspend fun
getAccessToken(): Flow<String> and suspend fun getRefreshToken(): Flow<String>;
remove the suspend modifier from both declarations so they are fun
getAccessToken(): Flow<String> and fun getRefreshToken(): Flow<String>, then
update the implementing class(es) to match and refactor all call sites to stop
expecting these methods to be suspend (collect/consume the Flow normally without
requiring coroutine call just to obtain the Flow).

Copy link
Collaborator

Choose a reason for hiding this comment

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

이거 아래도 코드리뷰 달았는데 이유가 있으신지 궁금해요~

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@fredleeJH 네, 좋은 지적이십니다!

suspend fun getAccessToken(): Flow<String> 패턴에 대한 제안 이유는 다음과 같습니다:

기술적 근거:

  • Flow는 차가운 스트림(cold stream)으로 지연 평가되어, 선언 자체에 suspend가 불필요합니다
  • Flow를 반환하는 함수는 단순히 Flow 객체를 생성/반환하는 것이므로 비동기 작업이 아닙니다
  • 실제 비동기 작업은 Flowcollect할 때 발생합니다

현재 패턴의 문제점:

// 현재: 불필요한 suspend 전파
suspend fun getTokens() {
    val accessTokenFlow = tokenDataSource.getAccessToken() // suspend 필요
    accessTokenFlow.collect { ... } // 실제 suspend 지점
}

// 개선 후: 더 자연스러운 사용
fun getTokens() {
    val accessTokenFlow = tokenDataSource.getAccessToken() // suspend 불필요
    accessTokenFlow.collect { ... } // 여기서만 suspend
}

하지만 다른 곳에서도 같은 패턴이 있다면, 일관성 측면에서 전체적인 접근 방식을 검토해보는 게 좋겠습니다. 혹시 특별한 설계 의도가 있으시다면 알려주세요!


🏁 Script executed:

#!/bin/bash
# Search for other Flow-returning methods with suspend modifier
echo "=== Searching for suspend fun methods returning Flow ==="
rg -n "suspend fun.*Flow<" --type kotlin

echo -e "\n=== Searching for Flow usage patterns in implementations ==="
# Look for implementations of TokenDataSource
rg -n "class.*TokenDataSource" --type kotlin

echo -e "\n=== Check TokenDataSourceImpl implementation ==="
cat app/src/main/java/com/byeboo/app/data/datasourceimpl/local/TokenDataSourceImpl.kt

echo -e "\n=== Check TokenRepositoryImpl usage ==="
rg -n -A5 -B5 "getAccessToken\|getRefreshToken" app/src/main/java/com/byeboo/app/data/repositoryimpl/auth/TokenRepositoryImpl.kt

Length of output: 3559


@fredleeJH 스크립트 결과를 보니 말씀하신 대로 이 패턴이 전체 토큰 관리 아키텍처에 일관되게 적용되어 있네요!

현재 상황:

// 4곳 모두 동일한 패턴
- TokenDataSource.kt (interface)
- TokenRepository.kt (interface) 
- TokenDataSourceImpl.kt (implementation)
- TokenRepositoryImpl.kt (implementation)

구현 살펴보면:

// 모두 단순한 Flow 생성/위임
override suspend fun getAccessToken(): Flow<String> = datastore.data.map { ... }
override suspend fun getAccessToken(): Flow<String> = tokenDataSource.getAccessToken()

이런 경우 선택지가 두 가지입니다:

  1. 일관성 우선: 현재 패턴 유지 (모든 곳에서 suspend fun)
  2. 기술적 정확성 우선: 전체 수정 (모든 곳에서 suspend 제거)

개인적으로는 2번을 추천하지만, 팀의 코딩 스타일이나 기존 컨벤션이 있다면 1번도 합리적입니다.

혹시 팀에서 Flow 반환 메서드에 대한 일관된 가이드라인이 있으신가요? 아니면 이 기회에 정리해보시는 것도 좋을 것 같습니다! 🤔


✏️ Learnings added
Learnt from: fredleeJH
PR: 36-APPJAM-HEARTZ/ByeBoo-ANDROID#161
File: app/src/main/java/com/byeboo/app/data/datasource/local/TokenDataSource.kt:6-7
Timestamp: 2025-08-28T02:47:57.174Z
Learning: TokenDataSource와 TokenRepository의 Flow 반환 메서드들(`getAccessToken()`, `getRefreshToken()`)에 일관되게 suspend 키워드가 사용되고 있으나, Flow 반환 메서드는 기술적으로 suspend가 불필요하다. 이는 전체 토큰 관리 아키텍처에 걸친 패턴이므로 수정 시 interface와 implementation 모두 함께 변경이 필요하다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines 69 to +77
@Provides
@Singleton
fun providesConverterFactory(): Converter.Factory = Json.asConverterFactory(
"application/json".toMediaType()
)
@Auth
fun providesOkHttpAuthClient(
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Auth 전용 클라이언트의 바디 로깅은 토큰(응답 바디) 노출로 직결됩니다.

로그인/재발급 응답 바디에 access/refresh 토큰이 포함되므로 BODY 레벨 로깅은 피해야 합니다. Auth 전용 인터셉터를 분리해 HEADERS 수준으로 제한하세요. 헤더 마스킹도 필수입니다.

변경 제안(새 프로바이더 추가 + 주입 변경):

+    @Provides
+    @Singleton
+    @Auth
+    fun providesAuthLoggingInterceptor(): HttpLoggingInterceptor =
+        HttpLoggingInterceptor().apply {
+            // 헤더만 로깅 + 민감 헤더 마스킹
+            redactHeader("Authorization")
+            level = if (BuildConfig.DEBUG) {
+                HttpLoggingInterceptor.Level.HEADERS
+            } else {
+                HttpLoggingInterceptor.Level.NONE
+            }
+        }
+
 @Auth
-fun providesOkHttpAuthClient(
-    loggingInterceptor: HttpLoggingInterceptor
+fun providesOkHttpAuthClient(
+    @Auth loggingInterceptor: HttpLoggingInterceptor
 ): OkHttpClient = OkHttpClient.Builder()
     .addInterceptor(loggingInterceptor)
     .build()
📝 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
@Provides
@Singleton
fun providesConverterFactory(): Converter.Factory = Json.asConverterFactory(
"application/json".toMediaType()
)
@Auth
fun providesOkHttpAuthClient(
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
@Provides
@Singleton
@Auth
fun providesAuthLoggingInterceptor(): HttpLoggingInterceptor =
HttpLoggingInterceptor().apply {
// Only log headers and mask the Authorization header
redactHeader("Authorization")
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.HEADERS
} else {
HttpLoggingInterceptor.Level.NONE
}
}
@Provides
@Singleton
@Auth
fun providesOkHttpAuthClient(
@Auth loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
🤖 Prompt for AI Agents
In app/src/main/java/com/byeboo/app/data/di/NetworkModule.kt around lines 69 to
77, the Auth-specific OkHttpClient currently uses BODY-level logging which can
expose access/refresh tokens in response bodies; create a new provider for an
auth-specific HttpLoggingInterceptor configured to log only HEADERS, add
header-masking (e.g., mask Authorization, Set-Cookie, access/refresh token
header names) either via a custom interceptor or using the logging interceptor's
redaction API, register a new @Auth OkHttpClient provider that uses this
sanitized interceptor (and does not add any body-logging), and update DI
injection points to use this Auth client for login/token endpoints while keeping
the existing client unchanged for other requests.

Comment on lines +7 to +12
data class TokenReissueResponseDto(
@SerialName("accessToken")
val accessToken: String,
@SerialName("refreshToken")
val refreshToken: String
)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

보안 점검: 토큰 로깅/헤더 관리 확인

민감정보인 토큰이 로그에 노출되지 않도록 다음 항목을 확인해 주세요.

  • OkHttp 로깅 인터셉터에서 Authorization 헤더 redaction 적용 여부
  • 토큰 저장 시 암호화(예: AndroidX Security Crypto) 적용 여부
  • 서버로 전송 시 Bearer <token> 포맷 일관성

점검 스크립트:

선택사항: DataStore에 저장하는 구현부에서 Crypto 레이어로 감싸는 것을 권장드립니다. 필요하시면 예시 구현을 드리겠습니다.


🏁 Script executed:

#!/bin/bash
# Authorization 헤더 redaction 확인
rg -nP 'HttpLoggingInterceptor' -C3
rg -nP '\.redactHeader\(\s*"Authorization"\s*\)' -n -C1

# Authorization 세팅 위치 확인
rg -nP '"Authorization"\s*[:=]\s*' -n -C2

# DataStore/Encrypted 저장 확인(간접적): Security Crypto 사용 여부 검색
rg -nP 'androidx\.security|MasterKeys|EncryptedSharedPreferences|Tink' -n -C2

Length of output: 3284


추가 검증을 위해 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# HttpLoggingInterceptor.redactHeader 사용 여부 재검증
rg -n 'redactHeader'

# Authorization 헤더가 addHeader 또는 header 메서드를 통해 설정되는지 확인
rg -nP '\baddHeader\(\s*"Authorization"'
rg -nP '\bheader\(\s*"Authorization"'

# DataStore 사용 위치 확인
rg -nP 'dataStore\(' -n -C3

# 암호화·Crypto 관련 라이브러리 사용 여부 확인
rg -nP 'Crypto|encrypt|Tink' -n -C3

Length of output: 171


보안 점검: 민감 정보 노출 방지 위해 다음 이슈 반드시 반영 필요

검증 결과,

  • HttpLoggingInterceptorredactHeader("Authorization") 호출이 없어 로그에 토큰이 그대로 노출될 수 있습니다.
  • 토큰 저장부에서 AndroidX Security Crypto(예: EncryptedSharedPreferences·EncryptedDataStore) 등 암호화 라이브러리 사용이 확인되지 않았습니다.
  • 인증 인터셉터(AuthInterceptor 등)에서 Authorization 헤더를 Bearer <token> 형식으로 일관되게 설정하는지 확인이 필요합니다.

수정해야 할 위치

  • NetworkModule.kt (31–37행):
    @Provides
    @Singleton
    fun providesLoggingInterceptor(): HttpLoggingInterceptor =
        HttpLoggingInterceptor().apply {
            level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
                    else HttpLoggingInterceptor.Level.NONE
  •     // Authorization 헤더 redaction
    
  •     redactHeader("Authorization")
    }
    
  • 토큰 저장 구현부:
    • SharedPreferences나 DataStore로 토큰을 저장하는 부분에 AndroidX Security Crypto 적용
    • 예:
      val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
      val encryptedPrefs = EncryptedSharedPreferences.create(
          "token_prefs",
          masterKeyAlias,
          context,
          EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
          EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
      )
      encryptedPrefs.edit().putString("accessToken", token).apply()
  • AuthInterceptor 구현부:
    override fun intercept(chain: Interceptor.Chain): Response {
        val token = tokenProvider.getAccessToken()
        return chain.proceed(
            chain.request()
                .newBuilder()
                .addHeader("Authorization", "Bearer $token") // Bearer 포맷 일관성
                .build()
        )
    }

필요 시 추가 예시 구현이나 도움 드리겠습니다.

Comment on lines +3 to 38
import com.byeboo.app.core.model.auth.TokenEntity
import com.byeboo.app.data.datasource.local.TokenDataSource
import com.byeboo.app.domain.repository.auth.TokenRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject

class TokenRepositoryImpl @Inject constructor(
private val userLocalDataSource: UserLocalDataSource
private val tokenDataSource: TokenDataSource
) : TokenRepository {

override suspend fun getUserId(): Long? {
return userLocalDataSource.getUserId()
@Volatile private var cachedAccessToken: String = ""

override suspend fun getAccessToken(): Flow<String> = tokenDataSource.getAccessToken()

override suspend fun getRefreshToken(): Flow<String> = tokenDataSource.getRefreshToken()

override suspend fun saveTokens(tokens: TokenEntity) {
tokenDataSource.updateTokens(tokens.accessToken, tokens.refreshToken)
updateCachedAccessToken(tokens.accessToken)
}

override suspend fun clearTokens() {
tokenDataSource.clearTokens()
updateCachedAccessToken("")
}

override suspend fun initCachedAccessToken() {
cachedAccessToken = tokenDataSource.getAccessToken().firstOrNull().orEmpty()
}

override fun getCachedAccessToken(): String = cachedAccessToken

override fun updateCachedAccessToken(token: String) {
cachedAccessToken = token
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

토큰 저장소 암호화 보장 여부 확인 필요.

DataStore를 그대로 쓰면 디스크 평문 저장입니다. androidx.security.crypto(EncryptedFile/SharedPreferences), Tink, 또는 자체 암복호화 래퍼 적용 여부를 확인해 주세요. 없으면 보안 리스크입니다.

다음 스크립트로 암호화 관련 사용 여부를 확인해 주세요(리포지토리 루트에서 실행):


🏁 Script executed:

#!/bin/bash
# 암호화/보안 저장소 사용 흔적 검색
rg -n \
  -e 'androidx\.security\.crypto' \
  -e 'MasterKey' \
  -e 'Encrypted(SharedPreferences|File|DataStore)' \
  -e 'javax\.crypto|Cipher|SecretKey' \
  -e 'Tink|Aead' \
  -g '!**/build/**' -S -C2

Length of output: 177


시큐어 스토리지 미적용 확인 – 토큰 암호화 레이어 필수 적용 필요

rg 검색 결과 암호화 관련(androidx.security.crypto, Tink, Cipher 등) 참조가 전혀 발견되지 않아, 현 상태로는 디스크에 평문 토큰이 저장됩니다. 이는 심각한 보안 리스크이므로 즉시 암호화 저장소를 도입해 주세요.

점검 및 조치 사항:

  • 대상 파일
    • app/src/main/java/com/byeboo/app/data/repositoryimpl/auth/TokenRepositoryImpl.kt
    • 실제 토큰을 쓰고 읽는 데이터소스: app/src/main/java/com/byeboo/app/data/datasource/local/TokenDataSource.kt
  • 암호화 스토리지 옵션
    • AndroidX Security Crypto (EncryptedSharedPreferences 또는 EncryptedFile)
    • Tink Aead 래퍼
    • 자체 javax.crypto.Cipher 기반 래퍼
  • 예시 수정(diff):
    // 기존: 평문 DataStore
    - private val dataStore: DataStore<Preferences> = context.createDataStore(name = "token_store")
    + // 암호화된 SharedPreferences 사용 예시
    + private val dataStore: EncryptedSharedPreferences = EncryptedSharedPreferences.create(
    +   "token_store",
    +   MasterKey.Builder(context)
    +     .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    +     .build(),
    +   context,
    +   EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    +   EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    + )
  • 구현 후 반드시 실제 디스크에 평문이 남지 않는지 검증해 주세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
app/src/main/java/com/byeboo/app/data/repositoryimpl/auth/TokenRepositoryImpl.kt
lines 3-38: current flow returns/saves tokens via TokenDataSource but there is
no encryption layer so tokens are stored plaintext; update the data layer to use
an encrypted storage implementation (prefer AndroidX Security Crypto
EncryptedSharedPreferences or Tink AEAD) inside
app/src/main/java/com/byeboo/app/data/datasource/local/TokenDataSource.kt so
that all reads return decrypted values and all writes persist encrypted bytes;
change TokenDataSource to perform encryption/decryption (or wrap its existing
SharedPreferences/file access with EncryptedSharedPreferences/Tink), keep
TokenRepositoryImpl API the same but ensure initCachedAccessToken reads the
decrypted token, and after implementation verify on-device storage contains only
encrypted data (no plaintext tokens).

@sohee6989 sohee6989 changed the title [feat/#145 kakao login] 카카오 로그인 구현 [feat/#145] 카카오 로그인 구현 Aug 26, 2025
Copy link
Collaborator

@fredleeJH fredleeJH left a comment

Choose a reason for hiding this comment

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

간단한 리뷰 몇개만 먼저 달겠습니다! 금요일 안으로 추가적인 리뷰 달고 어푸 드리겠습니다!


interface TokenRepository {
suspend fun getUserId(): Long?
suspend fun getAccessToken(): Flow<String>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Flow 자체가 콜드 스트림으로 알고 있는데 suspend가 굳이 필요 있을까요?

Suggested change
suspend fun getAccessToken(): Flow<String>
fun getAccessToken(): Flow<String>

밑에도 동일하고요! 아니면 suspend fun + Flow를 쓰신 특별한 이유가 있는지 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

원래 String으로 받았었는데 리이슈 api 오류 때문에 Flow로 바꾸고 나서 suspend 를 못 지웠습니다... suspend 삭제할게요 감사합니다!

) : Authenticator {

override fun authenticate(route: Route?, response: Response): Request? = runBlocking {
if (responseCount(response) >= 2) return@runBlocking null
Copy link
Collaborator

Choose a reason for hiding this comment

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

궁금한게 있습니다! 동기적으로 동작하는 authenticatorrunBlocking을 쓴 이유는 토큰 재 발급등의 suspend api 호출을 위해서인가요? 이게 메인쓰레드를 블락시키지는 않나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

suspend api를 호출하기 위해 runBlocking을 사용했습니다.
Authenticator 가 네트워크 스레드에서 실행되어 메인 스레드에서 호출되지 않기 때문에 메인 UI를 블락시키지는 않는다는 것으로 알고 있습니다.

.build()
}

private fun responseCount(response: Response): Int {
Copy link
Collaborator

Choose a reason for hiding this comment

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

어떤 함수인가용?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

토큰 만료 시 재발급을 실패하거나 계속 401이 발생할 때 무한 루프를 방지하기 위한 함수입니다.

Comment on lines +6 to +7
suspend fun getAccessToken(): Flow<String>
suspend fun getRefreshToken(): Flow<String>
Copy link
Collaborator

Choose a reason for hiding this comment

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

이거 아래도 코드리뷰 달았는데 이유가 있으신지 궁금해요~

val result = loginUseCase(token.accessToken, platform = KAKAO)

result.onSuccess { auth ->
if (auth.isRegistered) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

이게 자동로그인 분기처리 하는 코드가 맞을까요? 만약 앱에서 로그아웃해도 isRegistered 여부는 true로 남아있을텐데 그럼 로그아웃을 해도 홈으로 가는건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

제가 놓친 부분이네요..!! 감사해요 분기 처리 다시 바꿔놓을게요!

@sohee6989 sohee6989 moved this to Todo in HEARTZ-ANDROID Aug 29, 2025
"String",
"KAKAO_NATIVE_APP_KEY",
properties["kakao.native.app.key"].toString()
"\"$kakaoNativeAppKey\""
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3: 이건 왜 변수를 따로 빼는 방식으로 바꾼거예요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이거 원래 방식대로 하면 카카오네이티브 값을 못 읽어와서 빌드 오류 떠요... 그래서 일단 이렇게 구현했음...!!

Comment on lines 47 to 76
when {
token != null -> {
val result = loginUseCase(token.accessToken, platform = KAKAO)

result.onSuccess { auth ->
if (auth.isRegistered) {
delay(1000)
_sideEffect.emit(SplashStateSideEffect.NavigateToHome)
} else {
_sideEffect.emit(SplashStateSideEffect.NavigateToTermsOfService)
}
}.onFailure { e ->
//Todo: 스낵바
}
}

error is ClientError && error.reason == ClientErrorCause.Cancelled -> {
//Todo: 스낵바
}

error is AuthError -> {
_sideEffect.emit(SplashStateSideEffect.StartKakaoWebLogin)
}

else -> {
// 카카오 로그인 실패
}
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3: 이거


if (token != null) {
            loginUseCase(token.accessToken, platform = KAKAO)
                .onSuccess { auth ->
                    if (auth.isRegistered) {
                        delay(1000)
                        _sideEffect.emit(SplashStateSideEffect.NavigateToHome)
                    } else {
                        _sideEffect.emit(SplashStateSideEffect.NavigateToTermsOfService)
                    }
                }
                .onFailure { e ->
                   
              }
            return@launch
        }

        when (error) {
            null -> {
                ㅣ
            }
            is ClientError -> {
                when (error.reason) {
                    ClientErrorCause.Cancelled -> {
                      
                    }
                    else -> {
                       
                    }
                }
            }
            is AuthError -> {
              
                _sideEffect.emit(SplashStateSideEffect.StartKakaoWebLogin)
            }
            else -> {
               
            }
        }

이런 식으로 쓰는거 어떠세요???

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

에러를 나중에 집중 분기 처리할 수 도 있군뇨!! 제안해주신 걸로 수정해볼게요!

text = "Kakao로 시작하기",
style = ByeBooTheme.typography.body2,
color = ByeBooTheme.colors.black
color = ByeBooTheme.colors.black,
Copy link
Collaborator

Choose a reason for hiding this comment

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

P2: , 지우자요~

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵!!

super.onCreate(savedInstanceState)

val keyHash = Utility.getKeyHash(this)
Log.d("KAKAO_KEY_HASH", "keyHash = $keyHash")
Copy link
Collaborator

Choose a reason for hiding this comment

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

P2: 로그 지워주세요!

Copy link
Collaborator

Choose a reason for hiding this comment

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

이거 @sohee6989 소희님 브랜치 이걸로 옮기고 해시키 받아서 카카오에 등록하면 @znayeonzn 아연님이 지우는걸로하죠!

Copy link
Collaborator

Choose a reason for hiding this comment

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

아 그거땜에 둔거였군요..!
좋습니다!!


@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Auth No newline at end of file
Copy link
Collaborator

Choose a reason for hiding this comment

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

@sohee6989
Copy link
Collaborator

왕 너무 수고하셨습니다!!
역시 CM!!!
완전 자세하게는 못 봤는데 아티클 써주시면 한 번 더 자세하게 뜯어볼게요!!

@fredleeJH
Copy link
Collaborator

고생많으셨어요! 놓친 부분은 추후에 같이 확인해봐요!

@znayeonzn znayeonzn merged commit b4299fc into develop Aug 31, 2025
1 check was pending
@github-project-automation github-project-automation bot moved this from Todo to Done in HEARTZ-ANDROID Aug 31, 2025
@znayeonzn znayeonzn added the 🌎 서버 통신 서버 통신 작업 label Sep 1, 2025
@sohee6989 sohee6989 deleted the feat/#145-kakao-login branch November 11, 2025 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🌎 서버 통신 서버 통신 작업 🍒 [FEAT] 새로운 기능 구현 🐰 아연 아연

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[FEAT] 카카오 로그인 구현

3 participants