Skip to content

Conversation

@jife-archive
Copy link
Member

@jife-archive jife-archive commented Aug 16, 2025

[feat/#23]

  • 싱글톤 객체를 추가하였습니다
  • closed #이슈번호

Summary by CodeRabbit

  • 신규 기능

    • 토큰 관리 기능 추가: 토큰 저장/조회/갱신 상태를 노출하는 도메인·데이터 계층과 사용사례 도입
  • 개선

    • 네트워크 인증 자동화: 클라이언트 수준의 Bearer 인증 통합으로 토큰 사용이 중앙화되고 요청별 헤더 처리 제거
    • 로그인 시 토큰 영구화: 로그인 성공 시 토큰이 저장되어 자동 로그인·세션 안정성 향상
  • UI

    • 온보딩 흐름 변경: 로그인 후 6단계 스토리형 온보딩으로 전환하고 완료 콜백으로 다음 화면 이동; 화면 디자인·전환 및 오류 처리 개선

@coderabbitai
Copy link

coderabbitai bot commented Aug 16, 2025

Walkthrough

네트워크에 Ktor Bearer 인증을 도입해 EmotiaNetwork가 TokenUseCase를 주입받도록 변경하고, 채팅 API/데이터소스에서 토큰 파라미터를 제거했습니다. 온보딩 계층에 TokenManager/Repository/UseCase 및 DI 바인딩을 추가하고 온보딩 화면과 뷰모델에 토큰 저장·스토리 흐름을 도입했습니다.

Changes

Cohort / File(s) Change summary
Network auth integration
core/network/src/.../EmotiaNetwork.kt, core/network/src/.../di/NetworkModule.kt, core/network/build.gradle.kts, gradle/libs.versions.toml
EmotiaNetwork가 TokenUseCase를 주입받도록 변경. Ktor client-auth 추가 및 Bearer 인증(loadTokens/refreshTokens)으로 토큰 중앙화. per-request token 인자 및 수동 Authorization 헤더 로직 제거. DI 생성자 호출을 EmotiaNetwork(get())로 변경.
Chat API token removal
core/network/src/.../service/ChatApiService.kt, core/data/chatting/src/.../ChattingRemoteDataSourceImpl.kt
createChatRoom/sendChat 시그니처에서 token 인자 제거 및 네트워크 호출에서 token 전달 삭제. 관련 import/호출 정리.
Onboarding token domain
core/domain/onboarding/src/.../manager/TokenManager.kt, core/domain/onboarding/src/.../repository/TokenRepository.kt, core/domain/onboarding/src/.../usecase/TokenUseCase.kt, core/domain/onboarding/src/.../di/DomainOnboardingModule.kt
TokenManager, TokenRepository 인터페이스, TokenUseCase 추가. StateFlow로 currentToken/isLoggedIn 노출. DI에 TokenManager 및 TokenUseCase 등록.
Onboarding token data + DI
core/data/onboarding/src/.../repository/TokenRepositoryImpl.kt, core/data/onboarding/src/.../di/DataOnboardingModule.kt
TokenRepositoryImpl 추가(토큰 동작을 TokenManager에 위임). DataOnboardingModule에 singleOf(::TokenRepositoryImpl).bind<TokenRepository>() 바인딩 추가.
Feature onboarding flow update
feature/onboarding/src/.../OnBoardingScreen.kt, feature/onboarding/src/.../OnBoardingViewModel.kt, feature/onboarding/src/.../di/OnBoardingModule.kt, feature/onboarding/src/.../navigation/OnBoardingNavigation.kt
로그인 성공 시 채팅으로 즉시 이동하지 않고 스토리(startStory) 진행으로 변경. OnBoardingScreen 콜백명 onOnboardingFinished로 변경. ViewModel에 TokenUseCase 주입 및 토큰 저장/조회 API 추가. DI에서 ViewModel 바인딩에 tokenUseCase 주입 추가.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant OnBoardingScreen
  participant OnBoardingViewModel
  participant TokenUseCase
  participant TokenRepository
  participant TokenManager

  User->>OnBoardingScreen: 로그인 시도
  OnBoardingScreen->>OnBoardingViewModel: login()/register()
  OnBoardingViewModel->>TokenUseCase: saveToken(LoginEntity)
  TokenUseCase->>TokenRepository: saveToken(LoginEntity)
  TokenRepository->>TokenManager: saveToken(LoginEntity)
  TokenManager-->>TokenUseCase: currentToken/isLoggedIn 업데이트
  OnBoardingViewModel-->>OnBoardingScreen: 로그인 성공 알림
  OnBoardingScreen->>OnBoardingScreen: 스토리 진행 시작
  OnBoardingScreen-->>User: onOnboardingFinished 호출
Loading
sequenceDiagram
  participant Client as Ktor HttpClient
  participant EmotiaNetwork
  participant TokenUseCase
  participant API as Server

  Client->>EmotiaNetwork: POST(path, body)
  EmotiaNetwork->>TokenUseCase: getAccessToken()/getRefreshToken()
  TokenUseCase-->>EmotiaNetwork: tokens
  EmotiaNetwork->>Client: Auth(Bearer) 구성(loadTokens/refreshTokens)
  Client->>API: 요청 (Authorization: Bearer <access>)
  API-->>Client: 응답
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • sxunea
  • haeti-dev

Poem

( )_( )
/(•ᴥ• )/ 토큰 한 알 품은 토끼가 달려,
헤더는 맡기고 발은 가볍게 폴짝.
여섯 장 이야기 끝에 문을 밀면,
🥕 스토리 끝, 온보딩 끝, 채팅 길로 훌쩍.

Tip

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

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

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/LoginUI

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 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: 4

🔭 Outside diff range comments (4)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (2)

40-43: 로그에 Authorization 헤더 마스킹 필요.

LogLevel.ALL은 헤더까지 노출될 수 있어 액세스 토큰 유출 위험이 있습니다. Authorization 헤더는 반드시 마스킹하세요.

아래 패치를 적용하세요.

         install(Logging) {
             logger = Logger.DEFAULT
             level = LogLevel.ALL
+            sanitizeHeader { header -> header.equals(HttpHeaders.Authorization, ignoreCase = true) }
         }

추가로 다음 임포트가 필요합니다(파일 상단):

import io.ktor.http.HttpHeaders

80-83: HTTPS 필수 적용 (Production 환경)
Ktor HttpClient 초기화에서 protocol = URLProtocol.HTTP로 설정돼 있어, 배포용 빌드에서는 반드시 HTTPS를 사용해야 합니다.

  • 파일: core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (라인 79–83)
  • 변경 전:
    protocol = URLProtocol.HTTP
  • 변경 제안:
    • 환경별 분기 처리로 프로덕션에서는 URLProtocol.HTTPS 강제
    • 또는 baseUrl에 스킴(https://) 포함

배포 빌드에서 HTTP 설정이 남지 않도록 빌드 설정(예: Gradle flavor) 또는 코드 분기 로직을 추가하세요.

feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt (2)

37-49: 네트워크 예외 안전성 보강 + PII 로그 축소.

  • 네트워크 호출이 Result를 반환하더라도 예외가 던져질 수 있습니다. try-catch로 예외를 Result.failure로 전환해 크래시를 방지하세요.
  • println에 디바이스 UUID가 그대로 노출됩니다. 운영 로그에서 PII 노출 위험이 있으니 로거나 마스킹을 권장합니다.

적용 제안:

-            println("[OnBoardingViewModel] 자동 로그인 시작 - UUID: $deviceUuid")
+            // TODO: replace with proper logger (e.g., Napier) and avoid printing raw UUID
+            println("[OnBoardingViewModel] 자동 로그인 시작")

-            val loginResult = loginRepository.login(deviceUuid)
+            val loginResult = try {
+                loginRepository.login(deviceUuid)
+            } catch (t: Throwable) {
+                Result.failure(t)
+            }

55-76: 회원가입 경로도 동일하게 예외 안전성 보강.

회원가입 호출도 try-catch로 감싸 예외를 Result.failure로 변환하는 편이 안전합니다. 또한 에러 메시지 로그에 민감 정보가 섞이지 않도록 주의하세요.

-                val registerResult = loginRepository.register(registerRequest)
+                val registerResult = try {
+                    loginRepository.register(registerRequest)
+                } catch (t: Throwable) {
+                    Result.failure(t)
+                }
🧹 Nitpick comments (23)
core/network/build.gradle.kts (1)

40-40: 네트워크 모듈의 도메인(use-case) 직접 의존 — 경계 침범 가능성, TokenProvider 추상화 제안

core/networkprojects.core.domain.onboarding(TokenUseCase)에 직접 의존하면 레이어 간 결합이 커지고 순환 의존 리스크가 생길 수 있습니다. 네트워크는 “토큰을 공급받는” 좁은 계약만 필요하므로, 다음처럼 얇은 추상화(예: TokenProvider)에 의존하는 구조를 고려해 주세요.

예시:

// core/network (commonMain) - 새로 도입
interface TokenProvider {
    suspend fun loadAccessToken(): String?
    suspend fun refreshAccessToken(): String?
}
// EmotiaNetwork 생성자 변경 (예시)
class EmotiaNetwork(private val tokenProvider: TokenProvider) { /* ... */ }
// domain/data 쪽 어댑터 - TokenUseCase를 위임
class TokenProviderImpl(
    private val tokenUseCase: TokenUseCase
) : TokenProvider {
    override suspend fun loadAccessToken() = tokenUseCase.getAccessToken()
    override suspend fun refreshAccessToken() = tokenUseCase.getRefreshToken()
}
// DI
single<TokenProvider> { TokenProviderImpl(get()) }
single { EmotiaNetwork(get<TokenProvider>()) }

단기적으로는 현재 구조도 동작엔 문제 없지만, 중장기 유지보수성과 테스트 용이성을 위해 위 분리를 권장합니다.

core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/repository/TokenRepository.kt (2)

7-8: StateFlow 대신 Flow로 추상화해 구현 세부를 감추는 것을 권장

도메인 인터페이스는 구현 세부(상태 보유 형태)에 중립적인 Flow가 더 느슨한 결합을 제공합니다. UI/콜러 입장에서는 읽기 전용 스트림이면 충분한 경우가 많습니다.

다음처럼 타입을 좁혀보는 것을 제안합니다:

-    val currentToken: StateFlow<LoginEntity?>
-    val isLoggedIn: StateFlow<Boolean>
+    val currentToken: Flow<LoginEntity?>
+    val isLoggedIn: Flow<Boolean>

추가로 import도 다음과 같이 조정이 필요합니다(파일 상단):

// 변경 전
import kotlinx.coroutines.flow.StateFlow
// 변경 후
import kotlinx.coroutines.flow.Flow

10-16: KDoc으로 의미 명확화 제안(특히 hasValidToken, updateAccessToken)

  • hasValidToken: “유효”의 정의(만료 시간 검증 포함 여부, clock skew 처리 등) 명시
  • updateAccessToken: 갱신 시점에 currentToken 동기화 보장 여부, 스레드-세이프티 등

간단한 KDoc만 있어도 호출 측 오해가 줄어듭니다.

core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/di/DomainOnboardingModule.kt (1)

10-10: singleOf(::TokenManager) 사용으로 DI 선언 일관성 제안

아래처럼 TokenManagersingleOf를 사용하면 같은 파일 내 선언 스타일이 통일됩니다.

-    single { TokenManager() }
+    singleOf(::TokenManager)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/di/OnBoardingModule.kt (1)

11-11: RefreshTokenUseCase 바인딩의 모듈 위치 재고 권장.

Feature 모듈의 DI에서 도메인 UseCase를 직접 factory로 등록하면 모듈 경계가 흐려질 수 있습니다. Domain 측 DI 모듈(예: DomainLoginModule)에서 일괄 등록하고 Feature는 get()만 사용하는 편이 계층/의존성 관리에 유리합니다.

core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/di/DataOnboardingModule.kt (1)

6-9: 데이터 레이어가 도메인 구현(TokenManager)에 직접 의존하는 구조 재고 권장.

현재 TokenRepositoryImpl이 core.domain.onboarding.manager.TokenManager에 의존하는 것으로 보입니다. Clean Architecture 관점에서 data -> domain 의존은 인터페이스 수준에 국한하는 것이 일반적입니다. 대안:

  • TokenManager를 data/common 모듈(저장소/상태 관리)로 이동하고, domain에서는 인터페이스만 참조
  • 또는 domain에 TokenProvider 인터페이스를 두고, data에서 구현해 주입(의존성 역전)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (3)

29-29: httpClient 가시성 축소.

외부에서 직접 httpClient에 접근 가능하면 인증/기본설정 우회 호출이 생길 수 있습니다. 캡슐화를 위해 private 권장.

-    val httpClient = createHttpClient(NetworkConfig.baseUrl)
+    private val httpClient = createHttpClient(NetworkConfig.baseUrl)

31-31: expectSuccess 활성화로 실패 응답을 예외로 취급 권장.

명시적으로 expectSuccess = true 설정 시 4xx/5xx가 즉시 예외로 전파되어 에러 처리가 단순해집니다.

-    private fun createHttpClient(hostName: String): HttpClient = HttpClient {
+    private fun createHttpClient(hostName: String): HttpClient = HttpClient {
+        expectSuccess = true

98-102: 사용되지 않는 TEST_TOKEN 제거.

미사용 상수는 혼동을 줄 수 있으니 정리 권장.

     companion object Companion {
         private const val TIMEOUT_MILLIS = 10_000L
-        const val TEST_TOKEN =
-            ""
     }
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt (3)

29-31: 중복 상태 관리 최소화 검토(단일 SoT).

uiState.isLoginSuccess와 tokenUseCase.isLoggedIn/currentToken 두 축이 동시에 존재합니다. 장기적으로는 isLoggedIn(StateFlow)을 화면 전환의 단일 근거로 사용하고, isLoginSuccess는 일회성 이벤트(예: Snackbar)로만 쓰는 방향을 제안합니다. 이중 상태는 드물게 불일치가 생길 수 있습니다.


33-41: 중복 호출/경쟁 상태 방지 가드.

onRetry 등에서 performAutoLogin이 중복 호출될 수 있습니다. isLoading 상태 가드 혹은 Job 취소/스위칭(launchIn/collectLatest 등)로 경쟁 상태를 완화하세요.

-    fun performAutoLogin(deviceUuid: String) {
+    fun performAutoLogin(deviceUuid: String) {
         viewModelScope.launch {
-            _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
+            if (_uiState.value.isLoading) return@launch
+            _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)

88-104: 뷰모델에서 유틸 메서드 직접 노출은 최소화 권장.

현재 ViewModel이 TokenUseCase의 API를 그대로 재노출하고 있습니다. 화면단에서 직접 UseCase를 주입받아 쓰거나, 필요한 케이스만 노출해 ViewModel의 공용 API를 슬림하게 유지하는 편이 유지보수에 유리합니다.

feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (4)

41-57: 기보유 토큰 시 자동 로그인 스킵 → 즉시 스토리 진입.

이미 유효 토큰이 있는 경우 불필요한 로그인 호출을 생략하고 startStory를 true로 전환하는 편이 UX/성능 모두에 유리합니다. isLoggedIn(StateFlow)을 활용해 초기 분기하세요.

-    val uiState by viewModel.uiState.collectAsState()
+    val uiState by viewModel.uiState.collectAsState()
+    val isLoggedIn by viewModel.isLoggedIn.collectAsState()

-    LaunchedEffect(Unit) {
-        println("[OnBoardingScreen] 화면 진입 - 자동 로그인 시작, UUID: $deviceUuid")
-        viewModel.performAutoLogin(deviceUuid)
-    }
+    LaunchedEffect(Unit) {
+        // 이미 로그인되어 있으면 스토리 즉시 시작
+        if (isLoggedIn) {
+            startStory = true
+        } else {
+            // TODO: replace with proper logger and avoid printing raw UUID
+            println("[OnBoardingScreen] 자동 로그인 시도")
+            viewModel.performAutoLogin(deviceUuid)
+        }
+    }

-    LaunchedEffect(uiState.isLoginSuccess) {
-        if (uiState.isLoginSuccess) {
-            println("[OnBoardingScreen] 로그인 성공, 스토리 온보딩 시작")
-            startStory = true
-        }
-    }
+    LaunchedEffect(isLoggedIn, uiState.isLoginSuccess) {
+        if (isLoggedIn || uiState.isLoginSuccess) {
+            println("[OnBoardingScreen] 온보딩 스토리 시작")
+            startStory = true
+        }
+    }

94-109: 에러 메시지 하드코딩/현지화 대응.

사용자 노출 문자열은 문자열 리소스로 분리하는 것을 권장합니다(다국어/번역 추후 대비). Compose MPP 환경에서는 expect/actual 또는 공유 리소스 전략을 검토해 주세요.


138-152: 전체 클릭 오버레이 UX 확인 요청.

화면 전체가 클릭 가능하면 접근성/오작동(의도치 않은 터치) 가능성이 있습니다. 최소한 마지막 슬라이드에만 CTA 버튼을 두는 등 개선 여지가 있습니다. 현재 구현 자체는 문제 없어 보입니다.


170-181: 반복되는 동일 색상 분기 정리(가독성).

모든 인덱스가 동일 색을 사용하므로 when 절을 상수로 치환해 간결화할 수 있습니다.

-        modifier = modifier.background(
-            color = when (imageIndex) {
-                0 -> Color(0xFF1A1A2E)
-                1 -> Color(0xFF1A1A2E)
-                2 -> Color(0xFF1A1A2E)
-                3 -> Color(0xFF1A1A2E)
-                4 -> Color(0xFF1A1A2E)
-                5 -> Color(0xFF1A1A2E)
-                else -> Color(0xFF1A1A2E)
-            }
-        ),
+        modifier = modifier.background(Color(0xFF1A1A2E)),
core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/usecase/TokenUseCase.kt (1)

7-40: 단순 위임형 UseCase 구성은 합리적입니다.

StateFlow 노출과 저장/조회 메서드 위임이 명료합니다. 현재는 메모리 기반 구현이지만, 나중에 영속화로 바뀌어도 UseCase 인터페이스는 유지됩니다.

향후 데이터 소스가 IO를 수반하지 않는 한, getAccessToken/getRefreshToken/getUsername/hasValidToken 등은 suspend 제거를 고려해도 됩니다(호출부 간결화). 다만 기존 공개 API 변경 영향도를 먼저 확인해 주세요.

core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/manager/TokenManager.kt (5)

33-39: 토큰 미존재 시 업데이트 분기 로깅도 PII 없이 제한.

없을 때의 브랜치를 명시하거나, 로그를 제거하는 편이 더 안전합니다.

     fun updateAccessToken(newAccessToken: String) {
         val currentLogin = _currentToken.value
         if (currentLogin != null) {
             _currentToken.value = currentLogin.copy(accessToken = newAccessToken)
-            println("[TokenManager] 액세스 토큰 업데이트 완료")
         }
     }

41-45: 토큰 제거 로그도 삭제 권장.

운영 로그를 최소화하여 토큰 라이프사이클 정보가 추론되지 않게 하는 것이 좋습니다.

     fun clearTokens() {
         _currentToken.value = null
         _isLoggedIn.value = false
-        println("[TokenManager] 토큰 클리어 완료")
     }

47-51: 널 처리 단순화 및 !! 제거.

안전 호출로 가독성과 안정성을 개선할 수 있습니다.

-    fun hasValidToken(): Boolean {
-        return _currentToken.value != null &&
-                !_currentToken.value!!.accessToken.isBlank() &&
-                !_currentToken.value!!.refreshToken.isBlank()
-    }
+    fun hasValidToken(): Boolean {
+        val t = _currentToken.value
+        return t?.accessToken?.isNotBlank() == true && t.refreshToken.isNotBlank()
+    }

8-14: isLoggedIn를 파생 상태로 계산하는 접근 고려.

현재는 별도 플래그를 유지하고 있지만, currentToken로부터 map/transformLatest로 파생시키면 상태 일관성이 더 높아집니다(동시 업데이트 실수 방지).


8-20: 영속화 부재 확인 요청(앱 재시작 시 토큰 소실).

TokenManager가 메모리 기반이므로 프로세스 재시작/크래시에 토큰이 소실됩니다. KMP 환경에서 Settings/Datastore(e.g., Multiplatform Settings)와의 연계를 검토해 주세요.

앱 재기동 후에도 세션을 유지해야 한다면, 다음과 같은 단계로 검증/대응 가능합니다:

  • Data 모듈에 PersistentTokenStore를 추가하고 TokenRepositoryImpl에서 초기 로딩.
  • 부팅 시점에 영속 저장소 → TokenManager로 로드.
core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/repository/TokenRepositoryImpl.kt (1)

15-42: 레이어링 개선 제안: Data → Domain Manager 의존 제거.

현재 Data 레이어가 domain의 TokenManager에 직접 의존합니다. 일반적으로는

  • Domain: TokenRepository 인터페이스만 선언
  • Data: TokenRepositoryImpl에서 Persistence + InMemory 캐시(TokenManager 역할)를 구현
  • Manager/캐시는 Data에 두거나 별도 core/common(storage) 모듈로 분리
    가 이상적입니다. 변경이 크다면 추후 리팩토링 과제로 분리하셔도 됩니다.
📜 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 0431b94 and 55ab8dd.

⛔ Files ignored due to path filters (6)
  • core/designsystem/src/commonMain/composeResources/drawable/01.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/02.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/03.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/04.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/05.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/06.png is excluded by !**/*.png
📒 Files selected for processing (16)
  • core/data/chatting/src/commonMain/kotlin/com/nexters/emotia/core/data/chatting/datasource/ChattingRemoteDataSourceImpl.kt (0 hunks)
  • core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/di/DataOnboardingModule.kt (2 hunks)
  • core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/repository/TokenRepositoryImpl.kt (1 hunks)
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/di/DomainOnboardingModule.kt (1 hunks)
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/manager/TokenManager.kt (1 hunks)
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/repository/TokenRepository.kt (1 hunks)
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/usecase/TokenUseCase.kt (1 hunks)
  • core/network/build.gradle.kts (1 hunks)
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (3 hunks)
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/di/NetworkModule.kt (1 hunks)
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt (1 hunks)
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (2 hunks)
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt (5 hunks)
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/di/OnBoardingModule.kt (1 hunks)
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/navigation/OnBoardingNavigation.kt (1 hunks)
  • gradle/libs.versions.toml (1 hunks)
💤 Files with no reviewable changes (1)
  • core/data/chatting/src/commonMain/kotlin/com/nexters/emotia/core/data/chatting/datasource/ChattingRemoteDataSourceImpl.kt
🧰 Additional context used
📓 Path-based instructions (2)
**/build.gradle*

📄 CodeRabbit Inference Engine (CLAUDE.md)

Use libs.* references for dependency declarations

Files:

  • core/network/build.gradle.kts
gradle/libs.versions.toml

📄 CodeRabbit Inference Engine (CLAUDE.md)

Version catalog managed in gradle/libs.versions.toml

Files:

  • gradle/libs.versions.toml
🧠 Learnings (7)
📚 Learning: 2025-07-27T10:25:59.389Z
Learnt from: CR
PR: Nexters/team-ace-client#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-27T10:25:59.389Z
Learning: Applies to composeApp/src/commonMain/kotlin/**/*.kt : Keep business logic, data models, and network calls in commonMain

Applied to files:

  • core/network/build.gradle.kts
📚 Learning: 2025-07-27T10:25:59.389Z
Learnt from: CR
PR: Nexters/team-ace-client#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-27T10:25:59.389Z
Learning: Applies to composeApp/build.gradle* : Common dependencies in commonMain.dependencies

Applied to files:

  • core/network/build.gradle.kts
📚 Learning: 2025-07-27T10:25:59.389Z
Learnt from: CR
PR: Nexters/team-ace-client#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-27T10:25:59.389Z
Learning: Applies to composeApp/build.gradle* : Platform-specific dependencies in respective androidMain/iosMain blocks

Applied to files:

  • core/network/build.gradle.kts
📚 Learning: 2025-08-10T08:22:45.334Z
Learnt from: sxunea
PR: Nexters/team-ace-client#22
File: feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt:26-31
Timestamp: 2025-08-10T08:22:45.334Z
Learning: In Kotlin Multiplatform (KMP) projects, `org.koin.compose.viewmodel.koinViewModel()` from Koin is now compatible with commonMain source sets and can be used across all platforms, not just Android.

Applied to files:

  • core/network/build.gradle.kts
📚 Learning: 2025-07-27T10:25:59.389Z
Learnt from: CR
PR: Nexters/team-ace-client#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-27T10:25:59.389Z
Learning: Applies to gradle/libs.versions.toml : Version catalog managed in gradle/libs.versions.toml

Applied to files:

  • gradle/libs.versions.toml
📚 Learning: 2025-07-27T10:25:59.389Z
Learnt from: CR
PR: Nexters/team-ace-client#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-27T10:25:59.389Z
Learning: Applies to composeApp/src/commonMain/kotlin/**/*.kt : Implement navigation logic in common code when possible

Applied to files:

  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/navigation/OnBoardingNavigation.kt
📚 Learning: 2025-07-27T10:25:59.389Z
Learnt from: CR
PR: Nexters/team-ace-client#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-27T10:25:59.389Z
Learning: Applies to composeApp/src/commonMain/kotlin/**/*.kt : Use common state management patterns (ViewModel, StateFlow)

Applied to files:

  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt
🧬 Code Graph Analysis (1)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/di/NetworkModule.kt (1)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (1)
  • get (87-87)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: Firebase App Distribution
🔇 Additional comments (13)
core/network/build.gradle.kts (1)

36-36: Ktor Bearer Auth 의존성 추가 적절합니다

commonMain에 libs.ktor.client.auth 추가로 멀티플랫폼 전역에서 Bearer 인증 플러그인을 활용할 수 있게 되어 방향성 좋습니다. 버전 카탈로그 사용도 가이드에 부합합니다.

gradle/libs.versions.toml (1)

83-83: ktor-client-auth 라이브러리 선언 LGTM

좌표(io.ktor:ktor-client-auth)와 버전 참조(version.ref = "ktor") 모두 일관적이며, 기존 Ktor 3.x 선언들과도 잘 맞습니다.

core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/repository/TokenRepository.kt (1)

6-17: 도메인 레이어 계약이 명확합니다

토큰의 상태 흐름(State)과 CRUD·검증 API가 깔끔히 정의되어 있어 네트워크/온보딩 플로우 요구사항을 잘 커버합니다.

core/network/src/commonMain/kotlin/com/nexters/emotia/network/di/NetworkModule.kt (1)

9-9: 모듈 조립 확인 완료
initKoin(KoinInitializer.kt)에서 appModule에 정의된 coreNetworkModule, dataOnboardingModule, domainOnboardingModule이 모두 includes(…)로 포함되어 있고, 실제로 modules(appModule) 호출을 통해 로드되는 것을 확인했습니다.

feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/di/OnBoardingModule.kt (1)

13-17: OnBoardingViewModel DI 파라미터 주입 변경 정상 적용 (loginRepository + tokenUseCase).

토큰 관리 책임을 ViewModel로부터 TokenUseCase로 위임하는 방향성과 Koin 바인딩이 일치합니다. 도메인/데이터 모듈에서 각 바인딩이 제공되는 전제하에 문제 없어 보입니다.

core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/di/DataOnboardingModule.kt (1)

20-21: TokenRepository -> TokenRepositoryImpl 바인딩 추가 적절.

Koin single 바인딩으로 레포지토리 주입이 일관되게 해결됩니다. 의도대로 TokenUseCase에서 의존성 해소가 가능해집니다.

core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt (2)

15-22: 토큰 파라미터 제거로 네트워크 계층 단순화 잘 되었습니다.

Bearer 인증이 EmotiaNetwork로 집약되면서 API 시그니처가 간결해졌습니다. 요청 바디/응답 타입 유지도 적절합니다.


24-31: 호출부/데이터소스 시그니처 동기화 완료 확인

  • 모든 .kt 파일에서 createChatRoom/sendChat 호출에 구 토큰 인자 전달 또는 Authorization 헤더 수동 설정 흔적이 없습니다.
  • 상위 호출부가 모두 갱신된 것으로 확인되었습니다.
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt (2)

21-24: TokenUseCase DI 주입 추가는 적절합니다.

뷰모델이 토큰 상태와 저장을 직접 다룰 수 있게 되어 책임이 명확해졌습니다. DI 모듈만 일관되게 업데이트되어 있다면 문제 없어 보입니다.


46-49: 토큰 저장 타이밍 LGTM.

로그인 성공 직후 토큰을 저장하고 UI 상태를 갱신하는 순서가 자연스럽습니다. 이후 네트워크 계층의 Bearer 인증과도 일관됩니다.

feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (2)

60-81: 흑색 배경 및 단계 전환 UI 구성은 명확합니다.

로딩/에러와 스토리 온보딩 UI를 상태 기반으로 분리한 점이 깔끔합니다.


36-40: 공용 API 호출부 모두 onOnboardingFinished로 업데이트됨

OnBoardingScreen 호출부를 전수 점검한 결과, OnBoardingNavigation.kt(라인 12)에서만 호출되며, 모든 곳이 onNavigateToChatting이 아닌 onOnboardingFinished로 정상 갱신된 것을 확인했습니다. 추가 조치 불필요합니다.

core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/repository/TokenRepositoryImpl.kt (1)

8-14: 간결한 위임 구현, 상태 노출 일관성 좋습니다.

TokenManager의 StateFlow를 그대로 노출하여 구독 비용을 최소화했습니다.

Comment on lines +15 to +20
fun saveToken(loginEntity: LoginEntity) {
_currentToken.value = loginEntity
_isLoggedIn.value = true
println("[TokenManager] 토큰 저장 완료: ${loginEntity.username}")
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

PII(개인정보) 로그 제거 필요.

사용자 이름 등 PII를 println으로 남기는 것은 보안/프라이버시 리스크입니다. 운영 빌드에서 노출되지 않도록 제거하거나 마스킹/레벨 제한된 로거로 교체하세요.

     fun saveToken(loginEntity: LoginEntity) {
         _currentToken.value = loginEntity
         _isLoggedIn.value = true
-        println("[TokenManager] 토큰 저장 완료: ${loginEntity.username}")
     }
📝 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
fun saveToken(loginEntity: LoginEntity) {
_currentToken.value = loginEntity
_isLoggedIn.value = true
println("[TokenManager] 토큰 저장 완료: ${loginEntity.username}")
}
fun saveToken(loginEntity: LoginEntity) {
_currentToken.value = loginEntity
_isLoggedIn.value = true
}
🤖 Prompt for AI Agents
In
core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/manager/TokenManager.kt
around lines 15 to 20, the println call logs PII (loginEntity.username); remove
the plaintext PII log and replace it with either no log or a non-PII,
level-controlled logger (e.g., debug/info) that omits or masks the username
(e.g., show only first char + mask) or log only a generic success message;
ensure the new logging uses the project's logger so it can be disabled in
production.

) {
val httpClient = createHttpClient(NetworkConfig.baseUrl)

private fun createHttpClient(hostName: String): HttpClient = HttpClient {
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

파라미터 명(baseUrl vs hostName) 정합성.

createHttpClient(hostName: String)에 baseUrl을 전달하면서 내부에서는 host에 그대로 할당하고 있습니다. baseUrl이 스킴/포트/패스를 포함한다면 오동작 가능합니다. host만 넘기거나 URL 파싱으로 host/port/https를 분리하세요.

다음 스크립트로 NetworkConfig.baseUrl 값의 사용처를 확인하고 스킴 포함 여부를 점검하세요.


🏁 Script executed:

#!/bin/bash
# baseUrl 사용처와 주변 맥락 확인
rg -nP -C3 --type=kt '\bNetworkConfig\.baseUrl\b'

Length of output: 78


기존 스크립트가 --type=kt 필터 오류로 인해 검색되지 않았습니다. 타입 필터를 제거하고 파일 확장자로만 검색해 다시 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# baseUrl 사용처와 주변 맥락 확인 (타입 필터 제거, 확장자만 glob)
rg -nP -C3 '\bNetworkConfig\.baseUrl\b' --glob '*.kt'

Length of output: 875


NetworkConfig의 정의부에서 baseUrl이 어떻게 선언됐는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# NetworkConfig 정의와 baseUrl 선언 확인
rg -nP -C5 '\b(object|class)\s+NetworkConfig\b' --glob '*.kt'

Length of output: 1909


createHttpClient 구현 부분을 확인해 보겠습니다.


🏁 Script executed:

#!/bin/bash
# createHttpClient 사용부와 정의부 함께 출력
rg -nC10 'createHttpClient' --glob 'core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt'

Length of output: 2683


createHttpClient 파라미터 정합성 및 기본 요청 설정 추가

NetworkConfig.baseUrl(스킴/호스트/포트/경로 전체)를 hostName 매개변수에 전달하고 있으나 함수 내부에서 이를 전혀 사용하지 않고 있습니다. 의도에 맞게 아래 중 하나를 선택해 반영하세요.

  1. 매개변수명을 baseUrl로 변경하고, defaultRequest { url { takeFrom(baseUrl) } } 등으로 전체 URL을 적용
  2. 매개변수를 host: String 정도로 간소화하고, 스킴/포트 등은 고정하거나 별도 설정

– 파일: core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt
createHttpClient 함수 시그니처 및 빌더 내부 수정 필요

예시 diff:

--- a/core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt
+++ b/core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt
@@ -29,8 +29,15 @@ class EmotiaNetwork(
     val httpClient = createHttpClient(NetworkConfig.baseUrl)

-    private fun createHttpClient(hostName: String): HttpClient = HttpClient {
+    private fun createHttpClient(baseUrl: String): HttpClient = HttpClient {
         install(ContentNegotiation) { /* ... */ }
         install(Logging) { logger = Logger.DEFAULT }
+        defaultRequest {
+            url {
+                // 전체 URL에서 스킴/호스트/포트/경로를 파싱해 적용
+                takeFrom(baseUrl)
+            }
+        }
     }
🤖 Prompt for AI Agents
In
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt
around line 31, createHttpClient currently accepts hostName but never uses it;
change the parameter to baseUrl:String and apply it in the client builder by
adding a defaultRequest block that sets url { takeFrom(baseUrl) } so the
provided full NetworkConfig.baseUrl is used for all requests; update any call
sites to pass NetworkConfig.baseUrl and adjust the function signature
accordingly.

Comment on lines +49 to +76
install(Auth) {
bearer {
loadTokens {
val accessToken = tokenUseCase.getAccessToken()
val refreshToken = tokenUseCase.getRefreshToken()
if (!accessToken.isNullOrBlank()) {
BearerTokens(accessToken, refreshToken)
} else {
null
}
}
refreshTokens {
val refreshToken = tokenUseCase.getRefreshToken()
if (!refreshToken.isNullOrBlank()) {
// TODO: 리프레시 토큰으로 새 액세스 토큰 받아오는 로직
// 현재는 기존 토큰 반환
val accessToken = tokenUseCase.getAccessToken()
if (!accessToken.isNullOrBlank()) {
BearerTokens(accessToken, refreshToken)
} else {
null
}
} else {
null
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Bearer 인증: 최초 요청에도 토큰 전송 활성화 필요.

기본 설정에선 401 챌린지 이후에야 토큰을 붙입니다. 서버가 선제 토큰을 기대하거나 왕복을 줄이려면 sendWithoutRequest 활성화를 권장합니다.

아래 패치를 적용하세요.

         install(Auth) {
             bearer {
+                sendWithoutRequest = true
                 loadTokens {
                     val accessToken = tokenUseCase.getAccessToken()
                     val refreshToken = tokenUseCase.getRefreshToken()
                     if (!accessToken.isNullOrBlank()) {
                         BearerTokens(accessToken, refreshToken)
                     } else {
                         null
                     }
                 }
                 refreshTokens {
-                    val refreshToken = tokenUseCase.getRefreshToken()
-                    if (!refreshToken.isNullOrBlank()) {
-                        // TODO: 리프레시 토큰으로 새 액세스 토큰 받아오는 로직
-                        // 현재는 기존 토큰 반환
-                        val accessToken = tokenUseCase.getAccessToken()
-                        if (!accessToken.isNullOrBlank()) {
-                            BearerTokens(accessToken, refreshToken)
-                        } else {
-                            null
-                        }
-                    } else {
-                        null
-                    }
+                    val refreshToken = tokenUseCase.getRefreshToken()
+                    if (refreshToken.isNullOrBlank()) return@refreshTokens null
+                    // TODO: 실제 갱신 API 호출 후 토큰 저장(tokenUseCase.updateAccessToken 등) 구현
+                    // 임시로 null 반환하여 실패를 상위에서 처리하도록 함(재시도 루프/구토큰 재사용 방지)
+                    null
                 }
             }
         }
📝 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
install(Auth) {
bearer {
loadTokens {
val accessToken = tokenUseCase.getAccessToken()
val refreshToken = tokenUseCase.getRefreshToken()
if (!accessToken.isNullOrBlank()) {
BearerTokens(accessToken, refreshToken)
} else {
null
}
}
refreshTokens {
val refreshToken = tokenUseCase.getRefreshToken()
if (!refreshToken.isNullOrBlank()) {
// TODO: 리프레시 토큰으로 새 액세스 토큰 받아오는 로직
// 현재는 기존 토큰 반환
val accessToken = tokenUseCase.getAccessToken()
if (!accessToken.isNullOrBlank()) {
BearerTokens(accessToken, refreshToken)
} else {
null
}
} else {
null
}
}
}
}
install(Auth) {
bearer {
sendWithoutRequest = true
loadTokens {
val accessToken = tokenUseCase.getAccessToken()
val refreshToken = tokenUseCase.getRefreshToken()
if (!accessToken.isNullOrBlank()) {
BearerTokens(accessToken, refreshToken)
} else {
null
}
}
refreshTokens {
val refreshToken = tokenUseCase.getRefreshToken()
if (refreshToken.isNullOrBlank()) return@refreshTokens null
// TODO: 실제 갱신 API 호출 후 토큰 저장(tokenUseCase.updateAccessToken 등) 구현
// 임시로 null 반환하여 실패를 상위에서 처리하도록 함(재시도 루프/구토큰 재사용 방지)
null
}
}
}
🤖 Prompt for AI Agents
In
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt
around lines 49 to 76, the Auth bearer config currently only sends tokens after
a 401 challenge; enable sending tokens on the initial request by setting
sendWithoutRequest = true inside the bearer { } configuration. Update the bearer
block to include sendWithoutRequest = true so access/refresh tokens returned by
loadTokens/refreshTokens are attached to outgoing requests immediately.

Comment on lines 12 to 15
OnBoardingScreen(
onNavigateToChatting = onNavigateNext,
onOnboardingFinished = onNavigateNext,
deviceUuid = deviceUuid
)
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

파라미터명 변경(onOnboardingFinished) 반영 OK.

호출부에서 콜백을 동일하게 전달하면서 명확한 의미로 이름만 갱신된 점 좋습니다. 공용 API 변경이므로 다른 호출부에 잔존 참조 없는지만 확인하면 됩니다.

다음 스크립트로 구 코드명(onNavigateToChatting) 사용 흔적을 확인하세요. 결과가 비어 있으면 정상입니다.


🏁 Script executed:

#!/bin/bash
# 레거시 파라미터명 사용 탐지
rg -nP -C2 --type=kt '\bonNavigateToChatting\s*='

Length of output: 78


아래 스크립트로 다시 확인해주세요.


🏁 Script executed:

#!/bin/bash
# 레거시 파라미터명 사용 탐지 (확장자별 검색)
rg -nP '\bonNavigateToChatting\s*=' -g '*.kt' -C2

Length of output: 1321


onNavigateToChatting 레거시 호출부 갱신 필요

onOnboardingFinished(onNavigateNext)로 파라미터명이 변경되었으므로, 다음 호출부에서도 함께 수정해야 합니다:

  • feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/navigation/ResultNavigation.kt:
    onNavigateToChatting = onNavigateToChattingonOnboardingFinished = onNavigateNext
  • feature/main/src/commonMain/kotlin/com/nexters/emotia/feature/main/MainNavHost.kt:
    onNavigateToChatting = { navigator.navigateToChatting() }
    onOnboardingFinished = onNavigateNext (또는 적절한 람다명)

위 두 군데를 모두 업데이트한 뒤 다시 한번 확인 부탁드립니다.

🤖 Prompt for AI Agents
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/navigation/OnBoardingNavigation.kt
lines 12-15: the onboarding composable now expects onOnboardingFinished named
parameter instead of the old onNavigateToChatting; update the two call sites
listed in the review to match the new parameter name: in
feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/navigation/ResultNavigation.kt
replace the old onNavigateToChatting = onNavigateToChatting with
onOnboardingFinished = onNavigateNext, and in
feature/main/src/commonMain/kotlin/com/nexters/emotia/feature/main/MainNavHost.kt
replace the old onNavigateToChatting lambda assignment with onOnboardingFinished
= onNavigateNext (or an appropriate lambda) so both call sites use the new
parameter name.

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: 2

🔭 Outside diff range comments (3)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (1)

88-95: get()에서 수동 Authorization 헤더 제거 및 시그니처 정리

Bearer 플러그인을 도입했으므로 per-call 토큰 전달은 제거하는 것이 일관되고 안전합니다. 또한 현재 코드로 인해 header()/HttpHeaders 미정의 오류도 발생 중입니다.

적용 패치:

-    suspend inline fun <reified T : Any> get(
-        path: String,
-        token: String? = null,
-    ): T = httpClient.get(path) {
-        token?.let {
-            header(HttpHeaders.Authorization, "Bearer $it")
-        }
-    }.body()
+    suspend inline fun <reified T : Any> get(
+        path: String,
+    ): T = httpClient.get(path).body()
core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt (2)

16-23: createChatRoom에서 미사용 token 파라미터 제거

Bearer 인증으로 전환했으므로 token 인자는 더 이상 필요하지 않습니다. 현재는 미사용 파라미터로 남아 있고, API 표면을 간결하게 유지하기 위해 제거를 권장합니다.

적용 패치:

-    suspend fun createChatRoom(
-        username: String,
-        token: String,
-    ): CreateRoomResponse {
+    suspend fun createChatRoom(
+        username: String,
+    ): CreateRoomResponse {
         return network.post(
             path = "api/v1/chat-rooms",
             body = CreateRoomRequest(username = username)
         )
     }

36-44: getFairies에서도 token 파라미터 제거 및 호출부 정리

네트워크 계층에서 토큰을 자동 부착하므로 token 인자를 제거하고 호출부를 단순화하세요. 현재 CI의 “MatchGroup?” 관련 오류도 EmotiaNetwork 컴파일 이슈의 연쇄 오류로 보이며, 본 정리와 네트워크 수정을 반영하면 해소될 것입니다.

적용 패치:

-    suspend fun getFairies(
-        chatRoomId: String,
-        token: String,
-    ): GetFairiesResponse {
-        return network.get(
-            path = "api/v1/fairies?chatRoomId=${chatRoomId}",
-            token = token
-        )
-    }
+    suspend fun getFairies(
+        chatRoomId: String,
+    ): GetFairiesResponse {
+        return network.get(
+            path = "api/v1/fairies?chatRoomId=$chatRoomId",
+        )
+    }
♻️ Duplicate comments (2)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (2)

32-32: baseUrl/hostName 혼동 및 기본 URL 미적용 — takeFrom(baseUrl)로 정합성 확보

createHttpClient 파라미터명을 hostName으로 두고 NetworkConfig.baseUrl(스킴/호스트/경로 포함 가능)을 그대로 host에 넣고 있어 오동작 위험이 큽니다. 기본 요청 URL은 takeFrom(baseUrl)로 전체를 적용하세요. (이 내용은 이전 리뷰에서도 지적되었습니다.)

적용 패치:

-    private fun createHttpClient(hostName: String): HttpClient = HttpClient {
+    private fun createHttpClient(baseUrl: String): HttpClient = HttpClient {
@@
         defaultRequest {
             contentType(ContentType.Application.Json)
-            url {
-                // 우리 서버 나오고 수정 필요
-                protocol = URLProtocol.HTTP
-                host = hostName
-            }
+            url {
+                // baseUrl 전체(스킴/호스트/포트/패스) 적용
+                takeFrom(baseUrl)
+            }
         }

Also applies to: 80-84


50-77: Bearer 인증: 최초 요청에도 토큰 전송 + refreshTokens 동작 보정

  • 최초 요청에서부터 토큰을 전송하려면 sendWithoutRequest = true 설정이 필요합니다. (이전 리뷰와 동일 제안)
  • refreshTokens에서 기존 토큰을 그대로 재반환하면 401 루프/의도치 않은 재시도 가능성이 높습니다. 실제 갱신 API가 준비될 때까지는 null을 반환해 상위에서 처리(로그아웃/재인증)하도록 하세요.

적용 패치:

         install(Auth) {
             bearer {
+                sendWithoutRequest = true
                 loadTokens {
                     val accessToken = tokenUseCase.getAccessToken()
                     val refreshToken = tokenUseCase.getRefreshToken()
                     if (!accessToken.isNullOrBlank()) {
                         BearerTokens(accessToken, refreshToken)
                     } else {
                         null
                     }
                 }
                 refreshTokens {
-                    val refreshToken = tokenUseCase.getRefreshToken()
-                    if (!refreshToken.isNullOrBlank()) {
-                        // TODO: 리프레시 토큰으로 새 액세스 토큰 받아오는 로직
-                        // 현재는 기존 토큰 반환
-                        val accessToken = tokenUseCase.getAccessToken()
-                        if (!accessToken.isNullOrBlank()) {
-                            BearerTokens(accessToken, refreshToken)
-                        } else {
-                            null
-                        }
-                    } else {
-                        null
-                    }
+                    val refreshToken = tokenUseCase.getRefreshToken()
+                    if (refreshToken.isNullOrBlank()) return@refreshTokens null
+                    // TODO: 실제 갱신 API 호출 후 토큰 저장(tokenUseCase.updateAccessToken 등)
+                    // 임시: null 반환하여 상위에서 재인증/로그아웃 등 처리
+                    null
                 }
             }
         }
🧹 Nitpick comments (1)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (1)

41-44: 로깅 레벨 운영 적용 시 완화 권장

LogLevel.ALL은 개발 단계에 적합합니다. 운영/릴리스 빌드에서는 INFO(또는 NONE/HEADERS 등)로 낮추는 것을 권장합니다. 멀티플랫폼이라 BuildConfig 사용이 힘들다면, DI로 Logger를 주입하거나 환경 플래그를 통해 제어하세요.

📜 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 55ab8dd and 97ce0b7.

📒 Files selected for processing (3)
  • core/data/chatting/src/commonMain/kotlin/com/nexters/emotia/core/data/chatting/datasource/ChattingRemoteDataSourceImpl.kt (0 hunks)
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (3 hunks)
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt (1 hunks)
💤 Files with no reviewable changes (1)
  • core/data/chatting/src/commonMain/kotlin/com/nexters/emotia/core/data/chatting/datasource/ChattingRemoteDataSourceImpl.kt
🧰 Additional context used
🪛 GitHub Actions: Firebase App Distribution CD
core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt

[error] 20-20: Unresolved reference 'post'.


[error] 30-30: Unresolved reference 'post'.


[error] 40-40: Return type mismatch: expected 'GetFairiesResponse', actual 'MatchGroup?'.


[error] 40-40: Unresolved reference: MatchGroupCollection.get(name: String) due to receiver type mismatch.

core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt

[error] 53-53: Unresolved reference 'tokenUseCase'.


[error] 54-54: Unresolved reference 'tokenUseCase'.


[error] 56-56: Argument type mismatch: actual type is 'Any', but 'String' was expected.


[error] 62-62: Unresolved reference 'tokenUseCase'.


[error] 66-66: Unresolved reference 'tokenUseCase'.


[error] 68-68: Argument type mismatch: actual type is 'Any', but 'String' was expected.


[error] 68-68: Argument type mismatch: actual type is 'Any', but 'String?' was expected.


[error] 93-93: Unresolved reference 'header'.


[error] 93-93: Unresolved reference 'HttpHeaders'.


[error] 110-110: Syntax error: Missing '***.

🪛 GitHub Actions: Ace Client CI
core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt

[error] 20-20: Unresolved reference 'post'.


[error] 30-30: Unresolved reference 'post'.


[error] 40-40: Return type mismatch: expected 'GetFairiesResponse', actual 'MatchGroup?'.


[error] 40-40: Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch: fun MatchGroupCollection.get(name: String): MatchGroup?

core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt

[error] 53-53: Unresolved reference 'tokenUseCase'.


[error] 54-54: Unresolved reference 'tokenUseCase'.


[error] 56-56: Argument type mismatch: actual type is 'Any', but 'String' was expected.


[error] 62-62: Unresolved reference 'tokenUseCase'.


[error] 66-66: Unresolved reference 'tokenUseCase'.


[error] 68-68: Argument type mismatch: actual type is 'Any', but 'String' was expected.


[error] 68-68: Argument type mismatch: actual type is 'Any', but 'String?' was expected.


[error] 93-93: Unresolved reference 'header'.


[error] 93-93: Unresolved reference 'HttpHeaders'.


[error] 110-110: Syntax error: Missing '***'.

🔇 Additional comments (1)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt (1)

26-33: Compile 오류 원인 확인 필요 및 EmotiaNetwork 정의 검토 요청

ChatApiServicenetwork.post(…) 호출은 EmotiaNetwork에 정의된 post 확장/멤버 함수가 컴파일 시점에 반드시 존재해야 합니다. 현재 unresolved reference 오류는 EmotiaNetwork에 해당 시그니처가 아직 추가되지 않았기 때문으로 보입니다. EmotiaNetwork 쪽 수정 후 정상 호출이 보장된다는 전제는 올바르나, 아래 사항을 직접 확인해 주세요.

• EmotiaNetwork 정의 파일(예: core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt)에서 post(path: String, body: T): R 시그니처가 정확히 추가되었는지
post 함수가 suspend 키워드와 제네릭 반환 타입을 올바르게 갖추고 있는지
ChatApiService의 다른 network.post 호출부(createRoom)에서도 동일 시그니처로 컴파일 오류 없이 동작하는지

Comment on lines +52 to +56
loadTokens {
val accessToken = tokenUseCase.getAccessToken()
val refreshToken = tokenUseCase.getRefreshToken()
if (!accessToken.isNullOrBlank()) {
BearerTokens(accessToken, refreshToken)
} else {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

TokenUseCase 반환 타입 불일치로 BearerTokens 인자 타입 오류 발생

CI 로그의 “actual type is 'Any', but 'String' was expected”는 getAccessToken()/getRefreshToken()의 반환 타입이 String?이 아님을 시사합니다. 가장 바람직한 해법은 TokenUseCase 인터페이스를 String? 반환으로 정정하는 것입니다. 임시로는 안전 캐스팅을 적용해 컴파일을 풀 수 있습니다.

적용 패치(임시 안전 캐스팅; 근본 해결은 TokenUseCase 시그니처 수정):

-                loadTokens {
-                    val accessToken = tokenUseCase.getAccessToken()
-                    val refreshToken = tokenUseCase.getRefreshToken()
-                    if (!accessToken.isNullOrBlank()) {
-                        BearerTokens(accessToken, refreshToken)
-                    } else {
-                        null
-                    }
-                }
+                loadTokens {
+                    val accessToken = tokenUseCase.getAccessToken() as? String
+                    val refreshToken = tokenUseCase.getRefreshToken() as? String
+                    if (!accessToken.isNullOrBlank()) {
+                        BearerTokens(accessToken, refreshToken.orEmpty())
+                    } else {
+                        null
+                    }
+                }

추가로 TokenUseCase의 반환 타입을 String?로 정규화하는 변경이 가능하면 그 방향을 권장합니다. 원하시면 도메인 계층 시그니처 정리 패치도 제안드릴게요.

🧰 Tools
🪛 GitHub Actions: Firebase App Distribution CD

[error] 53-53: Unresolved reference 'tokenUseCase'.


[error] 54-54: Unresolved reference 'tokenUseCase'.


[error] 56-56: Argument type mismatch: actual type is 'Any', but 'String' was expected.

🪛 GitHub Actions: Ace Client CI

[error] 53-53: Unresolved reference 'tokenUseCase'.


[error] 54-54: Unresolved reference 'tokenUseCase'.


[error] 56-56: Argument type mismatch: actual type is 'Any', but 'String' was expected.

🤖 Prompt for AI Agents
In
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt
around lines 52-57, the CI error indicates getAccessToken()/getRefreshToken()
return type mismatch (actual Any) causing BearerTokens to expect String; fix by
updating the TokenUseCase interface methods to return String? (preferred, update
signatures in the domain layer and adjust implementations), or as a temporary
compile-fix safely cast the results to String? before constructing BearerTokens
(e.g., use safe cast or toString? handling) and ensure null/blank checks remain
valid.

# Conflicts:
#	core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt
#	core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt
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: 0

♻️ Duplicate comments (4)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (3)

31-31: 파라미터 명/역할 정합성: hostName → baseUrl로 명시

인자로 baseUrl을 전달하고 있으므로 시그니처를 baseUrl로 변경하는 편이 혼동을 줄입니다.

적용 diff:

-    private fun createHttpClient(hostName: String): HttpClient = HttpClient {
+    private fun createHttpClient(baseUrl: String): HttpClient = HttpClient {

77-84: defaultRequest에서 baseUrl 전체를 적용

현재는 protocol/host만 지정하여 경로/포트/스킴 손실 위험이 있습니다. takeFrom(baseUrl)로 일괄 적용하세요.

적용 diff:

         defaultRequest {
             contentType(ContentType.Application.Json)
             url {
-                // 우리 서버 나오고 수정 필요
-                protocol = URLProtocol.HTTP
-                host = hostName
+                // baseUrl의 스킴/호스트/포트/경로를 그대로 사용
+                takeFrom(baseUrl)
             }
         }

49-76: Bearer 설정: 토큰 널/타입 안전성 보장 + 최초 요청에도 전송

  • BearerTokens에 String?을 넘겨 컴파일 에러가 발생할 수 있습니다. 널/타입 안전 처리를 하세요.
  • 최초 요청부터 토큰을 붙이려면 sendWithoutRequest = true를 권장합니다.

적용 diff:

         install(Auth) {
             bearer {
+                // 최초 요청에도 토큰 전송
+                sendWithoutRequest = true
                 loadTokens {
-                    val accessToken = tokenUseCase.getAccessToken()
-                    val refreshToken = tokenUseCase.getRefreshToken()
-                    if (!accessToken.isNullOrBlank()) {
-                        BearerTokens(accessToken, refreshToken)
+                    val accessToken = (tokenUseCase.getAccessToken() ?: "").trim()
+                    val refreshToken = (tokenUseCase.getRefreshToken() ?: "").trim()
+                    if (accessToken.isNotBlank()) {
+                        BearerTokens(accessToken, refreshToken)
                     } else {
                         null
                     }
                 }
                 refreshTokens {
-                    val refreshToken = tokenUseCase.getRefreshToken()
-                    if (!refreshToken.isNullOrBlank()) {
-                        // TODO: 리프레시 토큰으로 새 액세스 토큰 받아오는 로직
-                        // 현재는 기존 토큰 반환
-                        val accessToken = tokenUseCase.getAccessToken()
-                        if (!accessToken.isNullOrBlank()) {
-                            BearerTokens(accessToken, refreshToken)
-                        } else {
-                            null
-                        }
-                    } else {
-                        null
-                    }
+                    val refreshToken = tokenUseCase.getRefreshToken()
+                    if (refreshToken.isNullOrBlank()) return@refreshTokens null
+                    // TODO: 실제 갱신 API 호출 후 tokenUseCase.updateAccessToken(...) 수행
+                    // 임시: null 반환하여 상위에서 401 처리/재로그인 플로우로 위임
+                    null
                 }
             }
         }

참고: 만약 TokenUseCase의 반환 타입이 String?가 아니라면(예: Any), 도메인 시그니처를 String?로 정규화하는 것이 근본 해결책입니다. 임시로는 안전 캐스팅(as?)과 orEmpty() 조합으로 컴파일을 풀 수 있습니다.

core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/manager/TokenManager.kt (1)

15-19: PII(개인정보) 로그 제거 필요

사용자 이름을 println으로 직접 남기면 운영 환경에 PII가 노출됩니다. 제거하거나 비식별화된 로거로 교체하세요.

다음 최소 수정안을 적용해 주세요:

     fun saveToken(loginEntity: LoginEntity) {
         _currentToken.value = loginEntity
         _isLoggedIn.value = true
-        println("[TokenManager] 토큰 저장 완료: ${loginEntity.username}")
+        // 로그에는 PII를 남기지 않습니다.
     }

추가로 프로젝트 전반에서 println 대신 레벨 제어 가능한 로거(Napier 등 KMM 호환 로거)를 사용해 운영 빌드에서 비활성화될 수 있도록 하는 것을 권장합니다.

🧹 Nitpick comments (14)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (5)

43-46: startStory는 rememberSaveable로 보존하세요

온보딩 중 프로세스 재생성/구성 변경 시 상태를 유지하려면 remember → rememberSaveable로 바꾸는 편이 안전합니다.

적용 diff:

-    var startStory by remember { mutableStateOf(false) }
+    var startStory by rememberSaveable { mutableStateOf(false) }

추가 import(선택):

import androidx.compose.runtime.saveable.rememberSaveable

93-101: 사용자 노출 에러 메시지는 도메인 메시지로 매핑 권장

서버/네트워크의 원문 메시지를 그대로 노출하면 UX/보안(내부 정보 노출) 측면에서 부적절할 수 있습니다. 코드/사유 기반으로 사용자 친화적 문구로 매핑하는 레이어를 두는 것을 권장합니다.


117-152: 온보딩 완료 콜백 중복 호출 방지(멀티탭 가드 추가)

마지막 페이지에서 연속 탭 시 onOnboardingFinished가 중복 호출될 수 있습니다. 간단한 가드 플래그로 방지해 주세요.

적용 diff:

     val totalImages = 6
-    var currentImageIndex by remember { mutableStateOf(0) }
+    var currentImageIndex by remember { mutableStateOf(0) }
+    var isFinishing by remember { mutableStateOf(false) }

@@
-                ) {
-                    if (currentImageIndex < totalImages - 1) {
-                        currentImageIndex++
-                    } else {
-                        onOnboardingFinished()
-                    }
-                }
+                ) {
+                    if (isFinishing) return@clickable
+                    if (currentImageIndex < totalImages - 1) {
+                        currentImageIndex++
+                    } else {
+                        isFinishing = true
+                        onOnboardingFinished()
+                    }
+                }

124-135: AnimatedVisibility 반복 대신 Crossfade/AnimatedContent 고려

동시에 6개의 레이어를 구성하고 가시성만 토글하는 방식은 오버헤드가 있습니다. Crossfade(currentImageIndex) { ... } 또는 AnimatedContent를 사용하면 단일 레이어로 전환 효과를 구현할 수 있어 더 간결하고 효율적입니다.


170-181: 동일 색상 분기 제거로 단순화 가능

모든 분기에서 동일 색상(0xFF1A1A2E)을 반환하므로 when 블록 없이 바로 지정할 수 있습니다.

적용 diff:

-    Box(
-        modifier = modifier.background(
-            color = when (imageIndex) {
-                0 -> Color(0xFF1A1A2E)
-                1 -> Color(0xFF1A1A2E)
-                2 -> Color(0xFF1A1A2E)
-                3 -> Color(0xFF1A1A2E)
-                4 -> Color(0xFF1A1A2E)
-                5 -> Color(0xFF1A1A2E)
-                else -> Color(0xFF1A1A2E)
-            }
-        ),
+    Box(
+        modifier = modifier.background(Color(0xFF1A1A2E)),
core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/di/DomainOnboardingModule.kt (1)

10-10: 코드 스타일 일관성: single → singleOf로 통일 가능

이미 singleOf를 사용하고 있으므로 TokenManager도 singleOf(::TokenManager)로 통일하면 가독성이 좋아집니다.

적용 diff:

-    single { TokenManager() }
+    singleOf(::TokenManager)
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (1)

41-43: 로그 레벨 운영 분기 권장

LogLevel.ALL은 민감 정보(헤더 등) 노출 가능성이 있어 운영 빌드에서는 하향 조정/비활성화가 안전합니다.

예: 빌드 플래그로 분기하여 DEBUG에서만 ALL, 릴리스에선 INFO/ERROR.

core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/manager/TokenManager.kt (7)

8-14: DI에서 싱글톤 보장 여부 확인 (class vs object)

PR 설명상 “싱글톤 객체”를 도입했다고 되어 있지만, 이 파일은 class TokenManager로 선언되어 있습니다. DI에서 single 스코프로 바인딩되어 한 인스턴스만 생성되는지 확인 부탁드립니다. 전역적으로 토큰을 관리하므로 다중 인스턴스가 생기면 상태가 분산됩니다.

필요하다면 object TokenManager로 전환하거나, DI 모듈에서 single { TokenManager() }로 명시해 싱글턴을 강제하세요. 원하시면 DI 바인딩 예시도 드리겠습니다.


12-13: isLoggedIn은 파생 상태라 중복 관리 비용 존재

isLoggedIn_currentToken의 존재/유효성에서 파생되는 값이라 별도의 MutableStateFlow로 관리하면 업데이트 누락이나 불일치가 생길 여지가 있습니다. 가능하면 단일 소스(currentToken)만 유지하고, UI/호출부에서 map { it != null && ... }로 파생시키는 패턴을 권장합니다.

원하시면 코루틴 스코프를 주입해 isLoggedIn: StateFlow<Boolean>currentToken.map { ... }.stateIn(...)으로 만드는 예시를 제공할게요.

Also applies to: 17-17, 43-43


33-39: 액세스 토큰 갱신은 원자적으로 처리 (StateFlow.update 사용 권장)

현재는 읽고-수정-할당 패턴이라 동시성 시나리오에서 드물게 갱신 손실 가능성이 있습니다. MutableStateFlow.update { ... }를 사용해 원자적으로 갱신하세요. 또한 불필요한 println은 제거합니다.

아래처럼 변경을 제안합니다:

-    fun updateAccessToken(newAccessToken: String) {
-        val currentLogin = _currentToken.value
-        if (currentLogin != null) {
-            _currentToken.value = currentLogin.copy(accessToken = newAccessToken)
-            println("[TokenManager] 액세스 토큰 업데이트 완료")
-        }
-    }
+    fun updateAccessToken(newAccessToken: String) {
+        _currentToken.update { current ->
+            current?.copy(accessToken = newAccessToken) ?: current
+        }
+        // 액세스 토큰 업데이트 완료 (PII/민감정보 미포함)
+    }

참고: 아래 코멘트의 import 추가도 함께 적용해야 컴파일됩니다.


3-7: update() 사용 시 import 추가 필요

위 리팩터를 적용한다면 update 익스텐션 import가 필요합니다.

 import com.nexters.emotia.core.domain.onboarding.entity.LoginEntity
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update

41-45: 운영 빌드에서 println 제거 또는 로거로 교체

명시적인 표준출력 로그는 플랫폼/빌드에 따라 제어가 어렵습니다. 필요 시 레벨 제어 가능한 로거로 교체하거나 단순히 제거하세요.

     fun clearTokens() {
         _currentToken.value = null
         _isLoggedIn.value = false
-        println("[TokenManager] 토큰 클리어 완료")
+        // 토큰 클리어 완료
     }

47-51: NPE 가능성 제거 및 중복 접근 최소화

동일한 함수 내에서 _currentToken.value를 여러 번 읽고 !!를 사용하는 대신 로컬 변수에 담아 안전하게 처리하세요. 이 방식이 가독성과 안전성 모두에 유리합니다.

-    fun hasValidToken(): Boolean {
-        return _currentToken.value != null &&
-                !_currentToken.value!!.accessToken.isBlank() &&
-                !_currentToken.value!!.refreshToken.isBlank()
-    }
+    fun hasValidToken(): Boolean {
+        val token = _currentToken.value ?: return false
+        return token.accessToken.isNotBlank() && token.refreshToken.isNotBlank()
+    }

8-52: 메모리 기반만으로는 토큰 영속성/보안 미흡 — 보안 저장소 연계 검토 권장

현재 구현은 프로세스가 종료되면 토큰이 유실됩니다. 실제 로그인 플로우에서는 다음을 권장합니다.

  • 데이터 계층에서 안전한 영속 저장소 연계: KMM 환경에서는 Encrypted Settings/Keychain/Keystore 연동 레이어를 두고, Domain은 Repository 인터페이스만 의존.
  • 앱 시작 시 저장소의 토큰을 로드해 _currentToken 초기화.
  • 토큰 갱신/클리어 시 저장소와 메모리를 함께 갱신하는 트랜잭션적 API 제공.

원하시면 KMM 공용 인터페이스/플랫폼별 actual 구현 스캐폴딩까지 초안 코드 제공해 드릴 수 있습니다.

📜 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 97ce0b7 and c4edf5f.

⛔ Files ignored due to path filters (6)
  • core/designsystem/src/commonMain/composeResources/drawable/01.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/02.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/03.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/04.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/05.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/06.png is excluded by !**/*.png
📒 Files selected for processing (16)
  • core/data/chatting/src/commonMain/kotlin/com/nexters/emotia/core/data/chatting/datasource/ChattingRemoteDataSourceImpl.kt (0 hunks)
  • core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/di/DataOnboardingModule.kt (2 hunks)
  • core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/repository/TokenRepositoryImpl.kt (1 hunks)
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/di/DomainOnboardingModule.kt (1 hunks)
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/manager/TokenManager.kt (1 hunks)
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/repository/TokenRepository.kt (1 hunks)
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/usecase/TokenUseCase.kt (1 hunks)
  • core/network/build.gradle.kts (1 hunks)
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (3 hunks)
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/di/NetworkModule.kt (1 hunks)
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt (1 hunks)
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (2 hunks)
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt (5 hunks)
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/di/OnBoardingModule.kt (1 hunks)
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/navigation/OnBoardingNavigation.kt (1 hunks)
  • gradle/libs.versions.toml (1 hunks)
💤 Files with no reviewable changes (1)
  • core/data/chatting/src/commonMain/kotlin/com/nexters/emotia/core/data/chatting/datasource/ChattingRemoteDataSourceImpl.kt
🚧 Files skipped from review as they are similar to previous changes (9)
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/navigation/OnBoardingNavigation.kt
  • core/network/build.gradle.kts
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/usecase/TokenUseCase.kt
  • gradle/libs.versions.toml
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/di/OnBoardingModule.kt
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt
  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt
  • core/network/src/commonMain/kotlin/com/nexters/emotia/network/di/NetworkModule.kt
  • core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/repository/TokenRepository.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-27T10:25:59.389Z
Learnt from: CR
PR: Nexters/team-ace-client#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-27T10:25:59.389Z
Learning: Applies to composeApp/src/commonMain/kotlin/**/*.kt : Implement navigation logic in common code when possible

Applied to files:

  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt
🔇 Additional comments (6)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (1)

36-40: 공개 API 변경에 따른 콜백 교체 완료 확인

OnBoardingScreen 호출부를 확인한 결과, 기존 onNavigateToChatting 파라미터 사용은 모두 onOnboardingFinished으로 교체되었습니다:

  • feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/navigation/OnBoardingNavigation.kt:13 (OnBoardingScreen(onOnboardingFinished = onNavigateNext, …))

OnBoardingScreen 호출부에 더 이상 onNavigateToChatting 사용이 없으므로 추가 작업은 필요 없습니다.

core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/di/DomainOnboardingModule.kt (1)

10-12: 토큰 DI 바인딩 구성 LGTM

TokenManager/TokenUseCase 싱글톤 바인딩 구성이 일관되고 간결합니다. Data 모듈의 TokenRepository 바인딩과도 잘 맞물립니다.

core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/di/DataOnboardingModule.kt (1)

20-20: TokenRepository 바인딩 추가 LGTM

TokenRepositoryImpl 바인딩이 적절하며 Domain의 TokenManager 싱글톤과 의존성 그래프가 자연스럽게 연결됩니다.

core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (1)

87-91: token 파라미터 제거 및 시그니처 정리 완료

  • 코드베이스 전반에서 EmotiaNetwork.get(path, token) 호출부가 없는 것 확인
  • Bearer 플러그인으로 인증 헤더가 처리되므로 token 파라미터는 불필요

적용 diff:

-    suspend inline fun <reified T : Any> get(
-        path: String,
-        token: String? = null,
-    ): T = httpClient.get(path) {
-    }.body()
+    suspend inline fun <reified T : Any> get(
+        path: String,
+    ): T = httpClient.get(path) {}.body()
core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/repository/TokenRepositoryImpl.kt (1)

8-42: 위임 레이어 구성 적절 — 단순/명확합니다

TokenManager로의 위임이 일관되며, StateFlow 노출과 토큰 수명주기 API가 명확합니다.

core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/manager/TokenManager.kt (1)

21-31: 간결한 getter 구성 👍

불필요한 처리 없이 현재 상태를 그대로 노출하는 getter 구성은 명확하고 좋습니다.

@jife-archive jife-archive merged commit d005e87 into develop Aug 16, 2025
1 of 3 checks passed
@jife-archive jife-archive deleted the feat/LoginUI branch August 16, 2025 15:21
@jife-archive jife-archive changed the title Feat/login UI Feat/Singleton Token Aug 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants