-
Notifications
You must be signed in to change notification settings - Fork 0
[feature/#23] 온보딩 기능 구현 #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthrough디자인 시스템에 TypingAnimatedSpeechBubble 컴포저블을 추가하고, 온보딩 모듈에 디자인시스템 의존성을 추가했습니다. OnBoardingScreen은 로그인 처리 후 내부 상태로 분기해 LoginContent와 12단계 StoryContent 흐름을 제공하며 콜백명이 onOnboardingFinished로 변경되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as 사용자
participant OBS as OnBoardingScreen
participant VM as OnBoardingViewModel
participant DS as TypingAnimatedSpeechBubble
U->>OBS: 앱 진입
OBS->>VM: 자동 로그인 시도 / 상태 구독
alt 로그인 로딩/오류
VM-->>OBS: loading / error
OBS-->>U: LoginContent 표시 (로딩/재시도)
else 로그인 성공
VM-->>OBS: success
OBS-->>U: StoryContent 시작
loop steps 1..12
OBS->>DS: fullText 전달 (타이핑 애니메이션 시작)
U-->>OBS: 화면 탭 -> 다음 단계로 진행
end
OBS->>U: 최종 단계 완료 및 onOnboardingFinished 호출
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Assessment against linked issues
Suggested reviewers
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 unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
feature/onboarding/build.gradle.kts (1)
13-13: Android 전용 라이브러리를 commonMain에서 제거하고 androidMain으로 이동 필요commonMain에 선언된 Android 전용 라이브러리(
androidx.security:security-crypto,slf4j-android)는 iOS 등 비-Android 타깃에서 메타데이터가 없어 KMP 빌드 시 오류를 발생시킬 수 있습니다. 아래와 같이 수정해 주세요.
- 파일:
feature/onboarding/build.gradle.kts
commonMain.dependencies에서 아래 라인 제거
- implementation("androidx.security:security-crypto:1.1.0-alpha06")
- implementation(libs.slf4j.android)
androidMain.dependencies블록 추가 및 의존성 선언
• 버전 카탈로그 alias(libs.androidx.security.crypto)가 없다면gradle/libs.versions.toml의[libraries]에 아래와 같이 추가• 이후 build.gradle.kts에선[libraries] androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" }implementation(libs.androidx.security.crypto)사용예시(diff):
kotlin { sourceSets { commonMain { dependencies { implementation(projects.core.domain.onboarding) implementation(projects.core.designsystem) implementation(libs.slf4j.api) - implementation(libs.slf4j.android) - implementation("androidx.security:security-crypto:1.1.0-alpha06") } } + androidMain { + dependencies { + implementation(libs.slf4j.android) + implementation(libs.androidx.security.crypto) + } + } } }
🧹 Nitpick comments (31)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt (4)
88-93: 배경 이미지는 장식적 요소이므로 contentDescription를 null로 설정 (접근성 개선)현재 "Speech bubble background"가 스크린리더에 불필요하게 읽힙니다. 장식용 이미지는 null로 처리하세요.
아래와 같이 수정:
- Image( - painter = painterResource(backgroundImage), // 선택된 배경 이미지 리소스 - contentDescription = "Speech bubble background", - modifier = Modifier.matchParentSize(), // Box 크기에 맞춤 - contentScale = ContentScale.FillBounds // 이미지가 잘리지 않고 꽉 차도록 설정 - ) + Image( + painter = painterResource(backgroundImage), + contentDescription = null, // 장식용 이미지: 스크린리더에서 제외 + modifier = Modifier.matchParentSize(), + contentScale = ContentScale.FillBounds + )
42-46: 중복 초기화 제거: remember(fullText) 대신 remember 사용LaunchedEffect(fullText)에서 이미 displayedText를 초기화하므로, remember에 key로 fullText를 주면 두 번 초기화됩니다. 불필요한 재설정을 줄이세요.
- var displayedText by remember(fullText) { mutableStateOf("") } + var displayedText by remember { mutableStateOf("") }
48-61: 여러 공백/줄바꿈 보존 이슈 — 단어 단위 애니메이션 시 원문 형식이 달라질 수 있음
fullText.split(' ')는 연속 공백이나 줄바꿈을 보존하지 못합니다. 원문 레이아웃을 유지하려면 토큰을 “단어 또는 공백”으로 분리해 누적하세요.예시(아이디어):
// 공백/줄바꿈 포함 토큰화: 단어(\\S+) 또는 공백(\\s+) val tokens = Regex("""\S+|\s+""").findAll(fullText).map { it.value }.toList() var currentText = "" tokens.forEach { token -> currentText += token displayedText = currentText // 단어일 때만(=공백이 아닐 때만) 지연 if (!token.isBlank()) delay(150L) }이 접근은 원문 내 연속 공백과 줄바꿈을 그대로 유지합니다.
71-79: 매직 넘버(고정 높이) 최소화 또는 파라미터화 제안
150.dp / 220.dp고정 높이는 다양한 텍스트 길이에서 오버플로를 유발할 수 있습니다. 높이를 파라미터화하거나, 최대 줄 수/overflow(TextOverflow)로 텍스트를 제어하는 방식을 권장합니다.원하시면 textStyle, maxLines, overflow 등을 파라미터로 노출하는 리팩터를 제안드리겠습니다.
core/network/build.gradle.kts (1)
40-41: 네트워크 모듈 → 도메인(Onboarding) 의존성 검토 결과 및 제안– 스크립트를 통해 core/domain/onboarding → core/network 간 순환 의존은 발생하지 않음을 확인했습니다.
– 하지만 core/network/build.gradle.kts(40~41)에서projects.core.domain.onboarding을 직접 참조하고 있어 계층 분리가 약화됩니다.제안:
• core/network 모듈에 TokenProvider 인터페이스(포트)를 정의 (e.g.core/network/src/main/kotlin/com/example/network/TokenProvider.kt)
• core/domain/onboarding 모듈에서 해당 인터페이스를 구현 및 DI
• build.gradle.kts 의존 설정은implementation(projects.core.network)만 두고, 도메인 의존은 제거위 방안을 통해 인프라 → 도메인 의존을 역전(Inversion of Control)하여 계층 결합도를 낮추는 것을 권장드립니다.
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/di/OnBoardingModule.kt (2)
3-4: DI 타입/이름 혼선 가능성 — TokenUseCase vs RefreshTokenUseCase 정리 필요현재 DI 모듈은
RefreshTokenUseCase를 바인딩하지만, PR 전반의 변경사항과 요약에 따르면 온보딩은 도메인의TokenUseCase(토큰 상태 관리/흐름 노출)를 사용하는 것으로 보입니다. ViewModel 파라미터명이tokenUseCase인 점도 혼선을 유발합니다.검증 포인트:
- OnBoardingViewModel의 생성자 두 번째 인자 실제 타입이
RefreshTokenUseCase인지TokenUseCase인지 확인 필요.- 네트워크 계층(Bearer Auth)과 온보딩의 토큰 상태 연동 목적이라면
TokenUseCase주입이 더 자연스럽습니다.정리가 필요하다면 아래 중 하나로 맞추는 것을 제안합니다.
옵션 A) 온보딩에서 도메인
TokenUseCase사용-import com.nexters.emotia.domain.login.usecase.RefreshTokenUseCase +import com.nexters.emotia.core.domain.onboarding.usecase.TokenUseCase @@ - // UseCase 주입 - factoryOf(::RefreshTokenUseCase) + // TokenUseCase는 DomainOnboardingModule에서 제공(singleOf)되므로 중복 바인딩 불필요 @@ - OnBoardingViewModel( - loginRepository = get(), - tokenUseCase = get() - ) + OnBoardingViewModel( + loginRepository = get(), + tokenUseCase = get<TokenUseCase>() + )옵션 B) 실제로
RefreshTokenUseCase를 쓰는 경우, 이름 일치로 가독성/의도 명확화- OnBoardingViewModel( - loginRepository = get(), - tokenUseCase = get() - ) + OnBoardingViewModel( + loginRepository = get(), + refreshTokenUseCase = get<RefreshTokenUseCase>() + )
11-12: 중복/경합 바인딩 방지
RefreshTokenUseCase를 여기서 factory로 바인딩할 경우, 다른 모듈(예: 로그인 도메인 DI)에서 동일 유스케이스를 바인딩하면 중복 또는 의도치 않은 스코프 차이(매번 새 인스턴스 vs single)로 이어질 수 있습니다. 단일 소스 모듈에서만 바인딩하고 여기서는 의존만 가져오는 구성을 권장합니다.core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/di/DomainOnboardingModule.kt (1)
10-10: 표현 통일성: singleOf 사용 권장(사소함)다른 바인딩들과의 일관성을 위해
TokenManager도singleOf(::TokenManager)로 표현을 통일하는 것을 제안드립니다.- single { TokenManager() } + singleOf(::TokenManager)core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/repository/TokenRepository.kt (1)
11-16: 단순 in-memory 조회에 대한 suspend 여부 재검토 권장현재 getAccessToken/getRefreshToken/getUsername/updateAccessToken/clearTokens/hasValidToken 모두 suspend입니다. 구현이 메모리 기반이라면 suspend는 불필요할 수 있고, 반대로 영속 저장소(예: DataStore)로 확장할 계획이라면 유지가 타당합니다. 의도에 따라 일관성 있게 결정해 주세요.
core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/manager/TokenManager.kt (3)
47-51: 중첩 ‘!!’ 제거로 가독성 및 안전성 개선현재 null 체크 후 value를 두 번 참조하고 있어 가독성이 떨어집니다. 로컬 변수로 받아 간결하게 표현하면 더 안전합니다.
아래처럼 변경을 제안합니다:
- fun hasValidToken(): Boolean { - return _currentToken.value != null && - !_currentToken.value!!.accessToken.isBlank() && - !_currentToken.value!!.refreshToken.isBlank() - } + fun hasValidToken(): Boolean { + val token = _currentToken.value + return token?.let { it.accessToken.isNotBlank() && it.refreshToken.isNotBlank() } == true + }
18-19: println 로깅은 라이브 환경에서 제거하거나 Logger로 대체하세요표준 출력은 플랫폼별 로그 수집과 레벨 제어가 어렵습니다. Kermit/napier/SLF4J 등 공통 Logger로 교체하거나 빌드 타입별로 비활성화하는 것을 권장합니다.
Also applies to: 37-38, 44-44
12-18: isLoggedIn 파생 상태 관리 전략 점검현재 isLoggedIn을 별도 StateFlow로 유지합니다. 토큰 Flow로부터 파생해 계산하면 불일치 가능성을 줄일 수 있습니다(예: clear/update 누락 케이스). 다만 Scope 필요성 때문에 현재 구조를 유지한다면, 모든 토큰 변경 지점에서 isLoggedIn 동기 업데이트가 계속 보장되는지 주기적으로 점검해 주세요.
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt (3)
37-37: println 로깅을 Logger로 대체 권장ViewModel에서도 println 대신 Logger를 사용해 로그 레벨/필터링/플랫폼별 집계를 지원하는 것이 좋습니다.
Also applies to: 44-44, 55-55, 66-66, 78-78
33-85: Result 처리 시 NPE 방지 및 오류 로깅 강화 제안isSuccess 분기에서 getOrNull()!! 대신 fold/onSuccess/onFailure를 쓰면 NPE 가능성을 제거하고 오류 흐름을 더 명확히 표현할 수 있습니다.
- if (loginResult.isSuccess) { - val loginEntity = loginResult.getOrNull()!! - ... - } else { - ... - } + loginResult.fold( + onSuccess = { loginEntity -> + // saveToken 및 UI 상태 갱신 + }, + onFailure = { t -> + // register 시도 또는 오류 처리 + } +)
88-104: ViewModel의 토큰 유틸 메서드 미사용 확인됨 — 제거 권장OnBoardingViewModel에 정의된 토큰 관련 메서드가 외부(Call Site)에서 호출되는 부분이 없습니다. UI 계층에서 직접 토큰을 조회·변경하지 않고 네트워크/레포지토리에서 캡슐화되고 있으므로, API Surface를 줄이기 위해 아래 메서드들을 삭제하는 것을 권장합니다.
검증 스크립트 결과
rg -nP \ -g 'feature/onboarding/src/commonMain/kotlin/**' \ -g '!feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt' \ -C2 '\b(getAccessToken|getRefreshToken|getUsername|updateAccessToken|clearTokens|hasValidToken)\s*\(' # → 호출 위치 없음제거 대상 (feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt)
- suspend fun getAccessToken()
- suspend fun getRefreshToken()
- suspend fun getUsername()
- suspend fun updateAccessToken(newAccessToken: String)
- suspend fun clearTokens()
- suspend fun hasValidToken(): Boolean
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (5)
168-173: 미사용 상태(secondImageOffsetY) 제거계산된 secondImageOffsetY가 실제로 사용되지 않습니다. 유지 필요가 없다면 제거해 재조합 비용과 혼란을 줄이세요.
- val secondImageOffsetY by animateDpAsState( - targetValue = if (currentStep == 2) -(screenHeight / 2) else 0.dp, - animationSpec = tween(600) - )
269-280: Step 5/6 이미지 리소스 중복 사용 확인 필요currentStep == 5와 currentStep == 6 모두 Res.drawable.six를 사용합니다. 의도된 반복인지, 하나는 다른 리소스(예: seven)여야 하는지 확인 부탁드립니다. 접근성 측면에서도 contentDescription이 동일하게 “Fifth screen”으로 표기되어 있어 혼동을 줄 수 있습니다.
Also applies to: 282-293
430-440: 상단 진행 텍스트의 포맷/세이프가드 점검currentStep + 1 / totalSteps 포맷은 직관적입니다. 단계 총합(totalSteps) 변경 시에도 마지막 단계 표기가 숨겨지도록 조건(currentStep < totalSteps - 1)이 잘 설정되어 있습니다. 접근성(스크린리더) 고려 시 semantics 추가를 권장합니다.
444-463: 미사용 컴포저블(SpeechBubble) 삭제 또는 실제 사용으로 전환TypingAnimatedSpeechBubble를 사용 중이라 SpeechBubble은 사용되지 않습니다. 제거해 빌드 경고 및 유지보수 오버헤드를 줄이는 것을 권장합니다.
-@Composable -private fun SpeechBubble( - text: String, - modifier: Modifier = Modifier -) { - Surface( - modifier = modifier.padding(start = 24.dp, end = 24.dp, bottom = 100.dp), - color = Color.Black.copy(alpha = 0.7f), - shape = RoundedCornerShape(16.dp) - ) { - Text( - text = text, - color = Color.White, - fontSize = 16.sp, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), - lineHeight = 24.sp - ) - } -}
199-206: contentDescription 개선여러 단계에서 동일하거나 부정확한 contentDescription이 사용됩니다. 스크린리더 사용자 경험을 위해 단계/이미지 의미가 드러나도록 구체화해 주세요.
Also applies to: 261-266, 275-279, 288-292, 302-306, 315-319, 328-332, 341-345, 354-358, 410-414
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (4)
49-76: 토큰 로드/리프레시 처리 방향성 좋습니다(스켈레톤 확인)Ktor Auth 플러그인으로 토큰 주입을 일원화한 점 매우 좋습니다. refreshTokens는 현재 스켈레톤이므로 401 응답 시 실제 리프레시 API 호출 및 TokenUseCase.updateAccessToken 저장까지 연결해주세요(중복 재시도/무한 루프 방지).
원하시면 refresh 로직의 공통 처리(401 감지→리프레시→요청 재시도) 스니펫을 제안드릴게요.
40-48: Logging 레벨/민감정보 마스킹 확인LogLevel.ALL은 헤더/바디까지 로깅될 수 있어 민감정보 노출 위험이 있습니다. 운영 빌드에서 레벨 하향 또는 Authorization 헤더 마스킹을 보장해 주세요.
80-84: 프로토콜 HTTP → HTTPS 전환 검토(환경 분기)기본 프로토콜을 HTTP로 고정하고 있습니다. 운영 환경에서는 HTTPS가 필요합니다. 빌드 타입/환경 변수로 분기하는 것을 권장합니다.
106-109: 미사용 상수(TEST_TOKEN) 제거사용되지 않는 상수는 혼란을 유발합니다. 제거를 권장합니다.
- companion object Companion { + companion object Companion { private const val TIMEOUT_MILLIS = 10_000L - const val TEST_TOKEN = - "" }core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/usecase/TokenUseCase.kt (3)
13-39: 표현식 본문(=)으로 간결화해 가독성을 높이는 것이 좋습니다.현재 모든 메서드가 단순 위임만 수행합니다. 표현식 본문으로 정리하면 중복이 줄고 의도가 더 분명해집니다.
아래처럼 변경을 제안합니다:
- suspend fun saveToken(loginEntity: LoginEntity) { - tokenRepository.saveToken(loginEntity) - } + suspend fun saveToken(loginEntity: LoginEntity) = + tokenRepository.saveToken(loginEntity) - suspend fun getAccessToken(): String? { - return tokenRepository.getAccessToken() - } + suspend fun getAccessToken(): String? = + tokenRepository.getAccessToken() - suspend fun getRefreshToken(): String? { - return tokenRepository.getRefreshToken() - } + suspend fun getRefreshToken(): String? = + tokenRepository.getRefreshToken() - suspend fun getUsername(): String? { - return tokenRepository.getUsername() - } + suspend fun getUsername(): String? = + tokenRepository.getUsername() - suspend fun updateAccessToken(newAccessToken: String) { - tokenRepository.updateAccessToken(newAccessToken) - } + suspend fun updateAccessToken(newAccessToken: String) = + tokenRepository.updateAccessToken(newAccessToken) - suspend fun clearTokens() { - tokenRepository.clearTokens() - } + suspend fun clearTokens() = + tokenRepository.clearTokens() - suspend fun hasValidToken(): Boolean { - return tokenRepository.hasValidToken() - } + suspend fun hasValidToken(): Boolean = + tokenRepository.hasValidToken()
7-9: 간단한 KDoc을 추가해 역할(읽기 전용 흐름 노출 + 토큰 CRUD 위임)을 명시해 주세요.도메인 경계에서의 책임이 분명해져 추후 유지보수에 도움이 됩니다.
+/** + * 토큰 관리를 위한 도메인 유스케이스. + * - 읽기 전용 StateFlow(currentToken, isLoggedIn) 재노출 + * - 토큰 저장/조회/갱신/삭제 및 유효성 검사 위임 + */ class TokenUseCase( private val tokenRepository: TokenRepository ) {
7-40: 단위 테스트 제안: 위임 보장 및 흐름 노출 검증이 필요합니다.
- save/update/clear가 Repository로 정확히 위임되는지
- currentToken/isLoggedIn이 Repository의 StateFlow를 동일 인스턴스로 재노출하는지
필요하시면 MockK/Fake Repository 기반의 간단한 테스트 템플릿을 제공하겠습니다.
core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/repository/TokenRepositoryImpl.kt (4)
15-41: 표현식 본문(=)으로 메서드 단순화 제안모든 메서드가 TokenManager로의 위임만 수행하므로, 표현식 본문을 사용하면 가독성이 좋아집니다.
- override suspend fun saveToken(loginEntity: LoginEntity) { - tokenManager.saveToken(loginEntity) - } + override suspend fun saveToken(loginEntity: LoginEntity) = + tokenManager.saveToken(loginEntity) - override suspend fun getAccessToken(): String? { - return tokenManager.getAccessToken() - } + override suspend fun getAccessToken(): String? = + tokenManager.getAccessToken() - override suspend fun getRefreshToken(): String? { - return tokenManager.getRefreshToken() - } + override suspend fun getRefreshToken(): String? = + tokenManager.getRefreshToken() - override suspend fun getUsername(): String? { - return tokenManager.getUsername() - } + override suspend fun getUsername(): String? = + tokenManager.getUsername() - override suspend fun updateAccessToken(newAccessToken: String) { - tokenManager.updateAccessToken(newAccessToken) - } + override suspend fun updateAccessToken(newAccessToken: String) = + tokenManager.updateAccessToken(newAccessToken) - override suspend fun clearTokens() { - tokenManager.clearTokens() - } + override suspend fun clearTokens() = + tokenManager.clearTokens() - override suspend fun hasValidToken(): Boolean { - return tokenManager.hasValidToken() - } + override suspend fun hasValidToken(): Boolean = + tokenManager.hasValidToken()
3-10: 레이어링 점검: Data 레이어가 Domain의 구현(Manager)에 의존하고 있습니다.Data 구현체(TokenRepositoryImpl)가 Domain 패키지의 TokenManager에 의존하는 구조는 클린 아키텍처 관점에서 경계가 모호합니다. 다음 중 한 가지로 재구성하는 것을 권장합니다.
- TokenManager를 data 모듈로 이동하고 LocalDataSource/TokenStore로 명명
- 혹은 Domain에 TokenStore 인터페이스만 두고(data에 구현), RepositoryImpl은 해당 인터페이스에 의존
현재 구조로도 동작에는 문제가 없으나, 성장 시 의존 방향/테스트 용이성/교체 가능성에서 제약을 줄 수 있습니다.
8-10: 가시성 internal 고려외부 모듈에 구현체를 노출할 필요가 없다면 internal로 축소해 모듈 경계를 명확히 할 수 있습니다. DI에서 구현체를 직접 참조하지 않고 인터페이스로 바인딩한다면 문제 없습니다.
-class TokenRepositoryImpl( +internal class TokenRepositoryImpl( private val tokenManager: TokenManager ) : TokenRepository {
8-42: 단위 테스트 제안: 위임/흐름 일치 검증
- 각 메서드가 TokenManager에 정확히 위임되는지
- currentToken/isLoggedIn이 동일한 StateFlow 인스턴스를 노출하는지
필요 시 Fake TokenManager로 스텁을 만들어 드릴 수 있습니다.
📜 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.
⛔ Files ignored due to path filters (12)
core/designsystem/src/commonMain/composeResources/drawable/eight.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/first.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/five.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/four.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/nine.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/npcTextFieldBackground.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/second.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/seven.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/six.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/ten.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/textFieldBackground.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/third.pngis excluded by!**/*.png
📒 Files selected for processing (18)
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/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.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/build.gradle.kts(1 hunks)feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt(1 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:
feature/onboarding/build.gradle.ktscore/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 (8)
📚 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:
feature/onboarding/build.gradle.ktscore/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:
feature/onboarding/build.gradle.ktscore/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 **/build.gradle* : Use libs.* references for dependency declarations
Applied to files:
feature/onboarding/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/src/commonMain/kotlin/**/*.kt : Keep business logic, data models, and network calls in commonMain
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/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 (2)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (1)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt (1)
TypingAnimatedSpeechBubble(36-118)
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-91)
🔇 Additional comments (18)
feature/onboarding/build.gradle.kts (1)
9-9: 디자인시스템(commonMain) 의존성 추가 적절온보딩 UI에서 디자인시스템 컴포넌트를 공용 소스셋에서 사용하는 목적에 부합합니다.
core/network/src/commonMain/kotlin/com/nexters/emotia/network/di/NetworkModule.kt (1)
8-12: EmotiaNetwork.get() 시그니처 직접 확인 필요
현재 스크립트 결과로get(...)메서드를 찾을 수 없어, 토큰 파라미터가 남아있는지 직접 열어 확인해 주세요.core/network/build.gradle.kts (1)
36-36: Ktor Auth 추가 적절Bearer 토큰 플로우를 위한
libs.ktor.client.auth의존성 추가는 합당합니다.core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/di/DataOnboardingModule.kt (1)
20-20: TokenRepository 바인딩 추가 적절도메인 인터페이스에 데이터 구현을 바인딩하는 구성이 명확합니다. 이후 확장(예: 다른 저장소 구현) 시에도 교체 용이합니다.
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/navigation/OnBoardingNavigation.kt (1)
12-15: 콜백 명 변경 반영이 정확합니다
onNavigateToChatting→onOnboardingFinished변경에 맞춰 네비게이션 연동이 올바르게 적용되었습니다. 공용(common) 모듈에서 네비게이션을 구성하는 것도 팀 가이드와 일치합니다.gradle/libs.versions.toml (1)
83-83: Ktor Auth 카탈로그 추가 OK
ktor-client-auth항목 추가가 네트워크 계층의 Bearer 토큰 플러그인 도입과 일치합니다. 버전도ktor버전 레퍼런스를 사용하고 있어 유지보수성 측면에서 좋습니다.core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/di/DomainOnboardingModule.kt (1)
10-13: 토큰 도메인 구성 바인딩 적절합니다
TokenManager와TokenUseCase를 도메인 레벨에서 제공하는 구성이 네트워크/온보딩 연동 목적에 부합합니다.core/network/src/commonMain/kotlin/com/nexters/emotia/network/service/ChatApiService.kt (3)
16-23: 토큰 인자 제거 및 Body 전송으로 전환 — 방향성 좋습니다
createChatRoom에서 토큰 파라미터 제거 및CreateRoomRequestbody 사용으로 변경된 점이 Ktor Auth(Bearer) 도입 방향과 일치합니다.
25-33: sendChat도 토큰 제거/Body 전송 일관성 확보 OK
sendChat역시 토큰을 제거하고SendChatRequest본문만 전송하도록 통일되어 있습니다. 네이밍/경로 문자열 인터폴레이션도 명확합니다.
35-43: EmotiaNetwork.get 시그니처 수동 확인 필요EmotiaNetwork에서
get메서드 정의부를 찾아token파라미터 포함 여부를 검증해 주세요. 자동으로 인증 토큰을 부착한다면,getFairies에서token인자를 제거하여 일관성을 유지하는 것이 좋습니다:-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}", + ) +}검증 포인트:
- EmotiaNetwork 내
get메서드 정의 위치token파라미터 자동 부착 여부core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/repository/TokenRepository.kt (1)
6-17: 토큰 저장/조회 API와 Flow 공개 범위가 명확합니다도메인 레벨에서 토큰 상태를 StateFlow로 노출하고 저장/조회/정리 기능을 일관되게 제공합니다. 상위 레이어에서 구독 및 호출이 용이합니다.
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingViewModel.kt (1)
21-24: 토큰 저장 시점 연동이 적절합니다로그인/회원가입 성공 시점에 TokenUseCase.saveToken을 호출해 인증 상태를 일관되게 유지합니다. 상위 UI/네트워크 계층과의 역할 분리가 명확합니다.
Also applies to: 46-49, 68-70
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (2)
60-99: 두 단계 플로우(로그인 → 스토리) 전환 구조가 명확합니다로그인 상태에 따라 LoginContent/StoryContent로 분기하는 구조가 직관적이며, 콜백(onOnboardingFinished)으로 내비게이션 경계를 깨끗하게 분리했습니다.
361-389: 말풍선 텍스트 단계와 이미지 단계의 매핑 검토말풍선이 표시되는 단계(0,2~10)가 이미지 전환 단계와 어긋나면 사용자 경험에 혼선이 생길 수 있습니다. 의도된 타이밍인지 확인해 주세요.
core/network/src/commonMain/kotlin/com/nexters/emotia/network/EmotiaNetwork.kt (1)
87-92: get 함수의 미사용 token 매개변수 제거 및 시그니처 정리 필요현재
token매개변수가 선언만 되어 있고 실제로 사용되지 않습니다. 아래와 같이 제거하고 호출부에 positional 혹은 named argument로token을 넘기는 곳이 없는지 최종 확인해주세요.- 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()• 호출부에
token인자를 named 또는 positional 방식으로 전달하는 곳이 없는지 검토 바랍니다.
• 불필요해진token관련 코드를 모두 제거해 API 일관성을 유지해주세요.core/domain/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/domain/onboarding/usecase/TokenUseCase.kt (2)
7-12: 얇은 위임과 StateFlow 재노출 구조는 명확하고 일관적입니다 (LGTM).UseCase가 Repository에 대한 얇은 파사드로 동작하고, 읽기 전용 StateFlow를 그대로 노출하는 선택은 UI/네트워크 양쪽에서 재활용하기에 적절합니다.
11-12: isLoggedIn와 hasValidToken의 의미/출처를 정합성 있게 유지하고 있는지 확인 바랍니다.두 값이 서로 다른 기준(예: 단순 존재 vs 만료 검증)을 의미한다면 네이밍/주석으로 구분을 명시하거나 하나로 통합을 고려해 주세요. 중복된 판단 로직은 호출부 혼란을 유발할 수 있습니다.
Also applies to: 37-39
core/data/onboarding/src/commonMain/kotlin/com/nexters/emotia/core/data/onboarding/repository/TokenRepositoryImpl.kt (1)
12-14: 간결한 StateFlow 위임 👍currentToken과 isLoggedIn을 그대로 위임하는 구현은 단순하고 명확합니다.
sxunea
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
코틀린하느라 수고했습니다 !
| Box( | ||
| modifier = modifier | ||
| .fillMaxWidth() | ||
| .height(boxHeight) // 동적으로 높이 적용 | ||
| .padding(start = 20.dp, end = 20.dp, bottom = 48.dp) | ||
| ) { | ||
| // useAlternativeBackground 값에 따라 배경 이미지 선택 | ||
| val backgroundImage = if (useAlternativeBackground) { | ||
| Res.drawable.textFieldBackground | ||
| } else { | ||
| Res.drawable.npcTextFieldBackground | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Step 4 | ||
| AnimatedVisibility( | ||
| visible = currentStep == 4, | ||
| enter = fadeIn(animationSpecFloat), | ||
| exit = fadeOut(animationSpecFloat) | ||
| ) { | ||
| Image( | ||
| painter = painterResource(Res.drawable.five), | ||
| contentDescription = "Fifth screen", | ||
| modifier = Modifier.fillMaxSize(), | ||
| contentScale = ContentScale.Crop | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Step 4 | |
| AnimatedVisibility( | |
| visible = currentStep == 4, | |
| enter = fadeIn(animationSpecFloat), | |
| exit = fadeOut(animationSpecFloat) | |
| ) { | |
| Image( | |
| painter = painterResource(Res.drawable.five), | |
| contentDescription = "Fifth screen", | |
| modifier = Modifier.fillMaxSize(), | |
| contentScale = ContentScale.Crop | |
| ) | |
| } | |
| @Composable | |
| fun StepImage( | |
| currentStep: Int, | |
| targetStep: Int, | |
| drawableRes: DrawableResource, | |
| description: String, | |
| animationSpec: FiniteAnimationSpec<Float> | |
| ) { | |
| AnimatedVisibility( | |
| // 이하 코드 ) |
스텝 12까지 중복된 코드가 많아요 이런식으로 묶어주세요 !
...rc/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
Show resolved
Hide resolved
| // 텍스트 오버레이 | ||
| Text( | ||
| "이제 당신의 감정을 기록해 보세요", | ||
| color = Color.White, | ||
| fontSize = 20.sp, | ||
| modifier = Modifier | ||
| .align(Alignment.BottomCenter) | ||
| .padding(bottom = 100.dp) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 텍스트랑, 우측 상단에 {현페이지}/12 부분 디자인에 없는데 구두로 상의된건가요오 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵넵
...rc/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
Show resolved
Hide resolved
haeti-dev
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혜선이 코드리뷰 반영한뒤에 머지 부탁드립니다~~ 저는 일단 이 브렌치에서 브렌치따서 작업하고 있을게용
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt (1)
88-93: 리소스 이미지를 배경으로 쓰는 접근에 대한 장기 대응 제안이전 리뷰 피드백과 동일하게, 말풍선 전체를 이미지로 구성하면 단말 해상도/비율 대응, 텍스트 영역 패딩/정렬 제어가 어려워집니다. Outline/Shape 기반 컨테이너 + SVG(요정/장식)만 이미지화하는 방향이 확장성/접근성 모두 유리합니다. 단기적으로는 본 PR 유지하되, 후속 작업으로 마이그레이션 이슈를 여는 것을 권장합니다.
원하시면 Shape 기반 말풍선 컴포저블 초안을 만들어 드릴게요(꼬리 위치/패딩 파라미터화 포함).
Also applies to: 95-106
🧹 Nitpick comments (6)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt (6)
71-86: 배경/정렬/높이의 의미가 prop 이름과 반대로 매핑되어 보입니다
useAlternativeBackground=true일 때 일반 텍스트필드 배경(150dp, 중앙 정렬),false일 때 NPC 배경(220dp, 좌측 정렬)로 동작합니다. prop 이름(“대체 배경”)과 주석 기대, 그리고 온보딩 맥락(NPC 말풍선) 관점에서 혼동 소지가 큽니다. 또한 텍스트 정렬이 모두Alignment.Center여서 상단 패딩 이슈(과거 리뷰 지적)와도 상충됩니다.다음처럼 매핑을 뒤집고 정렬을 상단 기준으로 맞추는 것을 권장합니다.
@@ - val boxHeight = if (useAlternativeBackground) 150.dp else 220.dp + val boxHeight = if (useAlternativeBackground) 220.dp else 150.dp @@ - val backgroundImage = if (useAlternativeBackground) { - Res.drawable.textFieldBackground - } else { - Res.drawable.npcTextFieldBackground - } + val backgroundImage = if (useAlternativeBackground) { + Res.drawable.npcTextFieldBackground + } else { + Res.drawable.textFieldBackground + } @@ - val (textAlign, textModifier) = if (useAlternativeBackground) { - // 일반 배경: 중앙 정렬, 수평 패딩 - TextAlign.Center to Modifier - .align(Alignment.Center) - .padding(horizontal = 24.dp, vertical = 20.dp) - } else { - // NPC 배경: 시작(좌측) 정렬, 시작 패딩 116dp 적용 - TextAlign.Start to Modifier - .align(Alignment.Center) - .padding(start = 116.dp, end = 24.dp, top = 20.dp, bottom = 16.dp) - } + val (textAlign, textModifier) = if (useAlternativeBackground) { + // NPC 배경: 시작(좌측) 정렬, 시작 패딩 116dp 적용 + TextAlign.Start to Modifier + .align(Alignment.TopStart) + .padding(start = 116.dp, end = 24.dp, top = 20.dp, bottom = 16.dp) + } else { + // 일반 배경: 중앙 정렬, 수평 패딩 + TextAlign.Center to Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 24.dp, vertical = 20.dp) + }추가로, prop 의미를 명확히 하려면
useNpcBackground: Boolean같은 이름으로 교체하는 것도 고려해주세요(호출부 영향 큼).Also applies to: 96-106
88-93: 접근성: 장식용 배경 이미지는 contentDescription=null로 설정배경은 정보 전달 목적이 아니므로 스크린리더가 중복 낭독하지 않도록
null을 권장합니다.- contentDescription = "Speech bubble background", + contentDescription = null,
36-41: 애니메이션 토큰화 개선 + 속도 파라미터화
- 현재는
' '(스페이스)만 기준이라 개행/탭/연속 공백이 있는 문장, 한국어+이모지 혼합 텍스트에서 토큰화가 어긋날 수 있습니다.- 딜레이 값을 하드코딩하기보다 파라미터로 노출하면 화면/상황별로 재사용성이 좋아집니다.
아래처럼 공백 보존 토큰화(정규식 split)와 딜레이 파라미터화를 제안합니다.
@@ -fun TypingAnimatedSpeechBubble( - fullText: String, - useAlternativeBackground: Boolean = false, - modifier: Modifier = Modifier -) { +fun TypingAnimatedSpeechBubble( + fullText: String, + useAlternativeBackground: Boolean = false, + modifier: Modifier = Modifier, + wordDelayMillis: Long = 150L, + charDelayMillis: Long = 100L, +) { @@ - LaunchedEffect(fullText) { + LaunchedEffect(fullText) { displayedText = "" // 텍스트 초기화 - // 띄어쓰기 유무에 따라 애니메이션 방식을 다르게 처리 - if (fullText.contains(" ")) { - // 단어 단위 애니메이션 - val words = fullText.split(' ') - var currentText = "" - words.forEachIndexed { index, word -> - // 이전 텍스트에 현재 단어를 추가 - currentText += word - // 마지막 단어가 아니면 띄어쓰기 추가 - if (index < words.size - 1) { - currentText += " " - } - displayedText = currentText - delay(150L) // 한 단어당 딜레이 - } - } else { - // 글자 단위 애니메이션 - fullText.forEach { char -> - displayedText += char - delay(100L) // 한 글자당 딜레이 (타이핑 속도) - } - } + // 공백(스페이스/개행/탭 등)을 보존하는 토큰화: 공백 토큰은 지연 0 + val hasWhitespace = fullText.any { it.isWhitespace() } + if (hasWhitespace) { + val tokens = Regex("(?<=\\s)|(?=\\s)") + .split(fullText) + .filter { it.isNotEmpty() } + var currentText = "" + for (token in tokens) { + currentText += token + displayedText = currentText + delay(if (token.isBlank()) 0L else wordDelayMillis) + } + } else { + for (ch in fullText) { + displayedText += ch + delay(charDelayMillis) + } + } }Also applies to: 44-69
42-43: 불필요한 remember 키 제거
LaunchedEffect(fullText)에서 직접 초기화하므로remember(fullText)는 중복입니다. 단순화하세요.-var displayedText by remember(fullText) { mutableStateOf("") } +var displayedText by remember { mutableStateOf("") }
3-27: 텍스트 오버플로 방지: maxLines/overflow 지정고정 높이(150/220dp)에서 긴 문장이 들어오면 오버플로될 수 있습니다. 줄 수 제한과 생략표시를 권장합니다.
@@ import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow @@ Text( text = displayedText, color = Color.White, style = textStyle, textAlign = textAlign, modifier = textModifier, - lineHeight = 20.sp + lineHeight = 20.sp, + maxLines = if (useAlternativeBackground) 5 else 4, + overflow = TextOverflow.Ellipsis, )Also applies to: 108-116
78-79: 매직 넘버 패딩 상수화
start=20, end=20, bottom=48은 DS 스페이싱 토큰으로 상수화하면 추후 다크모드/태블릿 대응 시 일관성 유지에 유리합니다.
📜 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.
📒 Files selected for processing (1)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt(1 hunks)
🧰 Additional context used
🪛 GitHub Actions: Firebase App Distribution CD
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
[error] 112-112: Unresolved reference 'typography'. Gradle task ':core:designsystem:compileDebugKotlinAndroid' failed.
🪛 GitHub Actions: Ace Client CI
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
[error] 112-112: Unresolved reference 'typography' at SpeechBubbleTextField.kt:112:21. This caused the Gradle task ':core:designsystem:compileDebugKotlinAndroid' to fail during './gradlew assembleDebug'.
| import androidx.compose.foundation.Image | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.height | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.LaunchedEffect | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.graphics.Color | ||
| import androidx.compose.ui.layout.ContentScale | ||
| import androidx.compose.ui.text.style.TextAlign | ||
| import androidx.compose.ui.unit.dp | ||
| import androidx.compose.ui.unit.sp | ||
| import emotia.core.designsystem.generated.resources.Res | ||
| import emotia.core.designsystem.generated.resources.npcTextFieldBackground | ||
| import emotia.core.designsystem.generated.resources.textFieldBackground | ||
| import kotlinx.coroutines.delay | ||
| import org.jetbrains.compose.resources.painterResource | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
컴파일 실패: 'typography' 미정의 — 스타일을 파라미터로 주입하도록 수정 제안
CI가 가리키는 그대로 Line 112에서 typography 식별자가 해석되지 않아 빌드가 중단됩니다. DS 타이포그래피 리소스를 직접 참조하지 않고, 기본값은 MaterialTheme를 사용하되 외부에서 스타일을 주입할 수 있게 하면 재사용성과 테스트 용이성이 올라가고, DS 의존을 느슨하게 만들 수 있습니다.
아래 최소 수정으로 컴파일을 복구하면서 API 유연성을 확보할 수 있습니다.
@@
-import androidx.compose.material3.Text
+import androidx.compose.material3.Text
+import androidx.compose.material3.MaterialTheme
@@
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.TextStyle
@@
fun TypingAnimatedSpeechBubble(
fullText: String,
useAlternativeBackground: Boolean = false,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ textStyle: TextStyle = MaterialTheme.typography.bodyMedium,
) {
@@
Text(
text = displayedText,
color = Color.White,
- style = typography.emotia14M,
+ style = textStyle,
textAlign = textAlign,
modifier = textModifier,
lineHeight = 20.sp
)- 만약 DS에
EmotiaTheme.typography.emotia14M같은 공식 스타일이 존재한다면, 위 기본값을 해당 스타일로 바꾸는 것도 좋습니다. - 시그니처가 바뀌므로 호출부에서 새로운 파라미터를 검토하세요(기본값이 있어 대개 변경 불필요).
아래 스크립트로 호출부를 점검할 수 있어요.
원하시면 DS의 공식 타이포그래피를 기본값으로 연결하는 패치까지 준비해드릴게요.
Also applies to: 36-41, 108-116
🏁 Script executed:
#!/bin/bash
# TypingAnimatedSpeechBubble 호출부 확인
rg -nP --glob '!**/build/**' '\bTypingAnimatedSpeechBubble\s*\(' -C2Length of output: 1506
typography 미정의 오류 수정 및 API 확장 제안
현재 TypingAnimatedSpeechBubble 내부에서 typography.emotia14M 식별자가 정의되지 않아 CI 빌드가 실패합니다. 최소한의 변경으로 컴파일을 복구하면서 외부에서 텍스트 스타일을 주입할 수 있도록 API를 확장하는 방안을 제안드립니다.
• 호출부 검토 결과
– feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt:384 외 1건 확인
– 기본 파라미터(textStyle = MaterialTheme.typography.bodyMedium) 덕분에 시그니처 변경에 따른 호출부 수정 불필요
• 수정 예시
diff --git a/core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt b/core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
index abcdefg..hijklmn 100644
--- a/core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
+++ b/core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
@@ -1,7 +1,9 @@
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Text
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color@@ -35,7 +37,9 @@ import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.painterResource
@Composable
-fun TypingAnimatedSpeechBubble(
+fun TypingAnimatedSpeechBubble(
fullText: String,
useAlternativeBackground: Boolean = false,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ textStyle: TextStyle = MaterialTheme.typography.bodyMedium,
) {
@@ -109,7 +113,7 @@ @Composable
Text(
text = displayedText,
color = Color.White,
- style = typography.emotia14M,
+ style = textStyle,
textAlign = textAlign,
modifier = textModifier,
lineHeight = 20.sp참고: DS 내에 EmotiaTheme.typography.emotia14M 같은 공식 스타일이 이미 존재한다면, 기본값을 해당 스타일로 바꾸시면 됩니다.
📝 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.
| import androidx.compose.foundation.Image | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.foundation.layout.height | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.layout.ContentScale | |
| import androidx.compose.ui.text.style.TextAlign | |
| import androidx.compose.ui.unit.dp | |
| import androidx.compose.ui.unit.sp | |
| import emotia.core.designsystem.generated.resources.Res | |
| import emotia.core.designsystem.generated.resources.npcTextFieldBackground | |
| import emotia.core.designsystem.generated.resources.textFieldBackground | |
| import kotlinx.coroutines.delay | |
| import org.jetbrains.compose.resources.painterResource | |
| ++ b/core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt | |
| @@ -1,7 +1,9 @@ | |
| import androidx.compose.foundation.Image | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.material3.MaterialTheme | |
| import androidx.compose.foundation.layout.height | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.material3.Text | |
| import androidx.compose.ui.text.TextStyle | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.getValue | |
| @@ -33,7 +35,9 @@ import kotlinx.coroutines.delay | |
| import org.jetbrains.compose.resources.painterResource | |
| @Composable | |
| fun TypingAnimatedSpeechBubble( | |
| fullText: String, | |
| useAlternativeBackground: Boolean = false, | |
| modifier: Modifier = Modifier, | |
| textStyle: TextStyle = MaterialTheme.typography.bodyMedium, | |
| ) { | |
| // … | |
| Box(modifier = modifier) { | |
| @@ -105,7 +109,7 @@ @Composable | |
| Image( | |
| painter = painterResource( | |
| id = if (useAlternativeBackground) Res.drawable.npcTextFieldBackground else Res.drawable.textFieldBackground | |
| ), | |
| style = textStyle, | |
| color = Color.White, | |
| textAlign = textAlign, | |
| modifier = textModifier, |
🤖 Prompt for AI Agents
In
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
around lines 3 to 27, the code references an undefined typography.emotia14M
inside TypingAnimatedSpeechBubble causing CI to fail; add a textStyle parameter
to the composable with a sensible default (e.g.,
MaterialTheme.typography.bodyMedium or EmotiaTheme.typography.emotia14M if that
style exists) and replace the direct typography.emotia14M reference with this
parameter so callers remain unchanged while compilation is restored and external
text style injection is supported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (1)
172-236: 스텝별 UI 중복을 더 줄일 수 있습니다.3
10단계를 리스트 기반으로 잘 묶으셨습니다. Step 13도 공통화 가능해 보입니다(예:StepImage(...)컴포저블로 오프셋/높이/알파/애니메이션 스펙을 파라미터화).원하시면
StepImage/StepStack초안 생성해 드리겠습니다.Also applies to: 238-251
🧹 Nitpick comments (9)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (9)
128-133: 스토리 진행 상태는rememberSaveable로 보존하거나 ViewModel로 승격을 고려하세요.프로세스 재생성/탭 전환 등에서 단계가 0으로 리셋될 수 있습니다. 적어도
rememberSaveable로 보존하는 것을 권장합니다. KMP 공통 모듈에서runtime-saveable사용 가능합니다.- var currentStep by remember { mutableStateOf(0) } + import androidx.compose.runtime.saveable.rememberSaveable + var currentStep by rememberSaveable { mutableStateOf(0) }장기적으로는 ViewModel(StateFlow)로 올려서 테스트성과 재사용성을 높이는 것도 좋습니다.
134-151: 스토리 이미지 매핑 중복(두 번의six) 및 주석/인덱스 불일치 가능성 확인 요청.
Res.drawable.six가 5단계와 6단계에 연속으로 배치되어 있습니다(라인 141, 142). 의도된 반복인지, 에셋 누락/오타인지 확인이 필요합니다. 또한 주석의 “3단계(인덱스 0)” 등 표기가 실제currentStep과 헷갈릴 수 있어 유지보수성이 떨어집니다.정합성 확인 후, 리스트를 “스텝 번호 → 리소스”로 명시적인 Map 또는 데이터 클래스 배열로 관리하면 가독성이 좋아집니다.
152-164: 전체 화면 클릭으로 다음 단계 진행은 접근성과 오조작 위험이 큽니다.TalkBack/스크린리더 환경에서는 설명을 읽는 도중 탭만으로 스킵될 수 있습니다. 또한 사용자가 텍스트 타이핑 애니메이션을 끝까지 보기 전에 다음 단계로 넘어갈 수 있습니다.
권장:
- 하단에 명시적 “다음” 버튼(또는 우측 탭 영역) 제공 +
semantics { onClick(...) }설명 추가TypingAnimatedSpeechBubble가 제공한다면 “타이핑 완료 콜백”과 연동해 완료 전에는 진행을 비활성화- 최소한 더블탭 방지(연타 디바운싱)와 멀티 클릭 차단
원하시면 접근성 점검용 스크립트/체크리스트 제공 가능합니다.
172-196: 장식 이미지의contentDescription는null로 설정해 스크린 리더가 불필요한 정보를 읽지 않도록 하세요.현재 “First screen”, “Second screen” 등 의미 없는 라벨이 읽히게 됩니다. 텍스트가 별도로 제공되므로 장식 이미지는
null권장입니다.- contentDescription = "First screen", + contentDescription = null, @@ - contentDescription = "Second screen", + contentDescription = null, @@ - contentDescription = "Second screen (top half)", + contentDescription = null, @@ - contentDescription = "Third screen (bottom half)", + contentDescription = null, @@ - contentDescription = "마지막 배경", + contentDescription = null,Also applies to: 200-213, 221-236, 244-251, 301-306
269-281:AnimatedVisibility(visible = true)는 효과가 없습니다 — 말풍선 전환은AnimatedContent로.현재
visible=true라서 스텝 변경 시 페이드 인/아웃이 동작하지 않습니다.currentStep을 타겟 상태로 하는AnimatedContent또는key(currentStep)를 사용해 전환을 애니메이션하세요. 또한 타이핑을 매 스텝 리셋하려면 key를 걸어주는 것이 안전합니다.- if (speechBubbleText != null) { - AnimatedVisibility( - visible = true, - enter = fadeIn(tween(300)), - exit = fadeOut(tween(300)), - modifier = Modifier.align(Alignment.BottomCenter) - ) { - TypingAnimatedSpeechBubble( - fullText = speechBubbleText, - useAlternativeBackground = useAlternativeBackground - ) - } - } + androidx.compose.animation.AnimatedContent( + targetState = currentStep, + transitionSpec = { fadeIn(tween(300)) togetherWith fadeOut(tween(300)) }, + modifier = Modifier.align(Alignment.BottomCenter) + ) { _ -> + if (speechBubbleText != null) { + TypingAnimatedSpeechBubble( + fullText = speechBubbleText, + useAlternativeBackground = useAlternativeBackground + ) + } + }필요 import:
+import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.togetherWith
97-125: 원문 오류 메시지 직접 노출은 UX/보안상 바람직하지 않습니다.사용자에게는 일반화된 메시지를 보여주고, 내부 로그/크래시 리포팅에 상세 원인을 남기는 방향을 권장합니다. 또한 문자열은 리소스로 분리하세요(다국어 대응).
예)
- UI: “문제가 발생했어요. 잠시 후 다시 시도해 주세요.”
- 버튼: “다시 시도”
- 로딩 문구: “로그인 중...”
원하시면 Compose Multiplatform 리소스(stringResource(Res.string.xxx)) 적용 패치 드릴게요.
322-331: 상단 진행 텍스트는 디자인시스템 타이포/컬러 토큰 사용을 권장합니다.현재
14.sp,Color.White하드코딩입니다. 디자인 토큰으로 치환하면 다크모드/가독성 정책을 일관되게 유지할 수 있습니다. 또한semantics { contentDescription = "진행률 ${currentStep+1}/$totalSteps" }추가를 고려하세요.
71-75:startStory는 파생 상태이므로 단순화 가능합니다.
startStory는 사실상uiState.isLoginSuccess파생입니다. 유지 이유(예: 로그인 성공 후 플래그 유지 필요)가 없다면 직접 분기해도 됩니다.- var startStory by remember { mutableStateOf(false) } - ... - LaunchedEffect(uiState.isLoginSuccess) { - if (uiState.isLoginSuccess) { - startStory = true - } - } + val startStory = uiState.isLoginSuccess
238-251: 애니메이션 상수/스펙을 상수화하여 재사용성/일관성 향상.
600ms가 여러 군데 하드코딩되어 있습니다. 파일 상단에private const val DURATION_MS = 600또는 공용 AnimSpec 홀더를 두고 재사용하면 유지보수에 유리합니다.
📜 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.
📒 Files selected for processing (1)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt(3 hunks)
🧰 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 : Use common state management patterns (ViewModel, StateFlow)
Applied to files:
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt
🔇 Additional comments (1)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (1)
57-63: 콜백 이름 변경 확인 완료
OnBoardingScreen 호출부(OnBoardingNavigation.kt)와 내부 람다 전달부가 모두onOnboardingFinished로 올바르게 갱신되었으며, 온보딩 기능 내에 이전 콜백명(onNavigateToChatting)의 잔여 참조가 없습니다. 결과 및 메인 네비게이션 코드에서 여전히 사용 중인onNavigateToChatting은 별도 기능의 콜백이므로 온보딩 변경과 무관합니다.
| import androidx.compose.foundation.layout.Arrangement | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.BoxWithConstraints | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.height | ||
| import androidx.compose.foundation.layout.offset | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.layout.size | ||
| import androidx.compose.foundation.shape.CircleShape | ||
| import androidx.compose.material3.Button |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Column scope의 weight 확장함수 import 누락으로 빌드 에러 발생 가능.
아래 블록(라인 221, 232)에서 Modifier.weight(1f)를 사용하지만 weight 확장함수 import가 없습니다.
다음 한 줄을 import에 추가하세요.
+import androidx.compose.foundation.layout.weight📝 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.
| import androidx.compose.foundation.layout.Arrangement | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.BoxWithConstraints | |
| import androidx.compose.foundation.layout.Column | |
| import androidx.compose.foundation.layout.fillMaxSize | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.foundation.layout.height | |
| import androidx.compose.foundation.layout.offset | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.foundation.layout.size | |
| import androidx.compose.foundation.shape.CircleShape | |
| import androidx.compose.material3.Button | |
| import androidx.compose.foundation.layout.Arrangement | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.BoxWithConstraints | |
| import androidx.compose.foundation.layout.Column | |
| import androidx.compose.foundation.layout.fillMaxSize | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.foundation.layout.height | |
| import androidx.compose.foundation.layout.offset | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.foundation.layout.size | |
| import androidx.compose.foundation.layout.weight | |
| import androidx.compose.foundation.shape.CircleShape | |
| import androidx.compose.material3.Button |
🤖 Prompt for AI Agents
In
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt
(import block around lines 13–24; usages at lines ~221 and ~232), the Column
scope weight extension is not imported causing a build error where
Modifier.weight(1f) is used; fix by adding the missing import for the weight
extension from androidx.compose.foundation.layout to the top import list so
Modifier.weight resolves.
| import emotia.core.designsystem.generated.resources.Res | ||
| import emotia.core.designsystem.generated.resources.eight | ||
| import emotia.core.designsystem.generated.resources.first | ||
| import emotia.core.designsystem.generated.resources.five | ||
| import emotia.core.designsystem.generated.resources.four | ||
| import emotia.core.designsystem.generated.resources.nine | ||
| import emotia.core.designsystem.generated.resources.second | ||
| import emotia.core.designsystem.generated.resources.seven | ||
| import emotia.core.designsystem.generated.resources.six | ||
| import emotia.core.designsystem.generated.resources.ten | ||
| import emotia.core.designsystem.generated.resources.third | ||
| import org.jetbrains.compose.resources.DrawableResource |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
리소스 import 경로 오타로 컴파일 실패합니다.
emotia.core.designsystem... 앞의 com.nexters. 네임스페이스가 누락됐습니다. 또한 Res.drawable.*를 이미 사용하므로 개별 리소스 심볼(eight, first 등) import는 불필요합니다.
아래처럼 수정해주세요.
-import emotia.core.designsystem.generated.resources.Res
-import emotia.core.designsystem.generated.resources.eight
-import emotia.core.designsystem.generated.resources.first
-import emotia.core.designsystem.generated.resources.five
-import emotia.core.designsystem.generated.resources.four
-import emotia.core.designsystem.generated.resources.nine
-import emotia.core.designsystem.generated.resources.second
-import emotia.core.designsystem.generated.resources.seven
-import emotia.core.designsystem.generated.resources.six
-import emotia.core.designsystem.generated.resources.ten
-import emotia.core.designsystem.generated.resources.third
+import com.nexters.emotia.core.designsystem.generated.resources.Res📝 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.
| import emotia.core.designsystem.generated.resources.Res | |
| import emotia.core.designsystem.generated.resources.eight | |
| import emotia.core.designsystem.generated.resources.first | |
| import emotia.core.designsystem.generated.resources.five | |
| import emotia.core.designsystem.generated.resources.four | |
| import emotia.core.designsystem.generated.resources.nine | |
| import emotia.core.designsystem.generated.resources.second | |
| import emotia.core.designsystem.generated.resources.seven | |
| import emotia.core.designsystem.generated.resources.six | |
| import emotia.core.designsystem.generated.resources.ten | |
| import emotia.core.designsystem.generated.resources.third | |
| import org.jetbrains.compose.resources.DrawableResource | |
| // Replace the erroneous and redundant imports... | |
| -import emotia.core.designsystem.generated.resources.Res | |
| -import emotia.core.designsystem.generated.resources.eight | |
| -import emotia.core.designsystem.generated.resources.first | |
| -import emotia.core.designsystem.generated.resources.five | |
| -import emotia.core.designsystem.generated.resources.four | |
| -import emotia.core.designsystem.generated.resources.nine | |
| -import emotia.core.designsystem.generated.resources.second | |
| -import emotia.core.designsystem.generated.resources.seven | |
| -import emotia.core.designsystem.generated.resources.six | |
| -import emotia.core.designsystem.generated.resources.ten | |
| import com.nexters.emotia.core.designsystem.generated.resources.Res | |
| import org.jetbrains.compose.resources.DrawableResource |
🤖 Prompt for AI Agents
In
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt
around lines 42 to 53, the import path is missing the leading com.nexters.
namespace and several unnecessary individual resource symbols are imported;
update the Res import to use
com.nexters.emotia.core.designsystem.generated.resources.Res (or the correct
full package) and remove the individual resource imports (eight, first, five,
etc.) since you reference resources via Res.drawable.*; keep only the necessary
org.jetbrains.compose.resources.DrawableResource import if still needed.
639a186 to
2509f0d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (2)
41-53: Res import 경로 오타 + 개별 리소스 import 불필요로 인한 컴파일 실패
Res의 패키지 경로가emotia...로 되어 있어 컴파일 오류가 납니다. 올바른 네임스페이스는com.nexters.emotia...입니다.Res.drawable.*로 접근하고 있으므로first,second,third등 개별 드로어블 심볼 import는 제거하세요.아래 diff로 수정 바랍니다.
-import emotia.core.designsystem.generated.resources.Res -import emotia.core.designsystem.generated.resources.eight -import emotia.core.designsystem.generated.resources.first -import emotia.core.designsystem.generated.resources.five -import emotia.core.designsystem.generated.resources.four -import emotia.core.designsystem.generated.resources.nine -import emotia.core.designsystem.generated.resources.second -import emotia.core.designsystem.generated.resources.seven -import emotia.core.designsystem.generated.resources.six -import emotia.core.designsystem.generated.resources.ten -import emotia.core.designsystem.generated.resources.third +import com.nexters.emotia.core.designsystem.generated.resources.Res또한, 아래 import가 없어
Modifier.weight(1f)가 컴파일되지 않습니다. 같은 import 블록에 추가하세요.+import androidx.compose.foundation.layout.weight
221-236: Column 범위의 weight 사용: import 누락이 블록은
Modifier.weight(1f)를 사용합니다. 상단에import androidx.compose.foundation.layout.weight가 누락되어 있으면 빌드가 실패합니다. 위 import 정리 코멘트에 포함한 대로 추가해 주세요.
🧹 Nitpick comments (6)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (6)
107-123: 하드코딩된 로그인 UI 문구 → Compose Resources로 이전 권장다국어/일관성 관점에서 하드코딩 문자열을
Res.string.*+stringResource로 이전하세요. (리소스 키는 예시이며 생성 필요)- Text( - text = "오류: $errorMessage", - color = Color.White, - modifier = Modifier.padding(bottom = 16.dp) - ) + Text( + text = stringResource(Res.string.onboarding_error_format, errorMessage), + color = Color.White, + modifier = Modifier.padding(bottom = 16.dp) + ) - Button(onClick = onRetry) { - Text("다시 시도") - } + Button(onClick = onRetry) { + Text(stringResource(Res.string.common_retry)) + } ... - Text( - text = "로그인 중...", - color = Color.White, - modifier = Modifier.padding(top = 16.dp) - ) + Text( + text = stringResource(Res.string.login_in_progress), + color = Color.White, + modifier = Modifier.padding(top = 16.dp) + )필요한 import:
+import org.jetbrains.compose.resources.stringResource원하시면 strings.xml/compose-resources 정의까지 함께 드립니다.
254-267: 온보딩 내러티브 텍스트 하드코딩 → 리소스화스토리 텍스트가 모두 문자열 리터럴로 하드코딩되어 있습니다. 변경/번역/디자인 카피 변경 대응을 위해 리소스 키로 분리하세요.
예시 (리소스 키 도입):
- val (speechBubbleText, useAlternativeBackground) = when (currentStep) { - 0 -> "깊은 숲 너머,\n오랜 시간 잊혀진 세계가 있어요." to true - 2 -> "이곳은 이모티아.\n감정을 품은 요정들과 생명들이 살아가는 숲." to true - 3 -> "...어라?\n저기, 나무 뒤에... 누군가 있는 것 같아요." to true - ... - 10 -> "자, 이제 이모티아의 숲으로 같이 걸어가 볼까?" to false - else -> null to false - } + val (speechBubbleText, useAlternativeBackground) = when (currentStep) { + 0 -> stringResource(Res.string.story_0) to true + 2 -> stringResource(Res.string.story_2) to true + 3 -> stringResource(Res.string.story_3) to true + ... + 10 -> stringResource(Res.string.story_10) to false + else -> null to false + }
269-280: 말풍선 전환 시 페이드가 적용되지 않음 (AnimatedVisibility가 항상 visible=true)현재 블록은
speechBubbleText가 null→non-null로 바뀔 때만 페이드가 동작합니다. 스텝 간 텍스트가 바뀌는 경우에도 자연스러운 전환을 원하면AnimatedContent로 교체하는 것을 권장합니다.- if (speechBubbleText != null) { - AnimatedVisibility( - visible = true, - enter = fadeIn(tween(300)), - exit = fadeOut(tween(300)), - modifier = Modifier.align(Alignment.BottomCenter) - ) { - TypingAnimatedSpeechBubble( - fullText = speechBubbleText, - useAlternativeBackground = useAlternativeBackground - ) - } - } + androidx.compose.animation.AnimatedContent( + targetState = speechBubbleText, + transitionSpec = { fadeIn(tween(300)) togetherWith fadeOut(tween(300)) }, + modifier = Modifier.align(Alignment.BottomCenter) + ) { text -> + if (text != null) { + TypingAnimatedSpeechBubble( + fullText = text, + useAlternativeBackground = useAlternativeBackground + ) + } + }필요한 import:
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.togetherWith
151-163: 전체 화면 탭 연타 방지 + 접근성 레이블 보강 제안
- 빠른 연타로 스텝이 여러 번 증가할 수 있습니다. 짧은 락으로 방지하세요.
- 스크린 전체 클릭은 보조기기에서 목적이 불명확합니다.
onClickLabel과Role.Button을 지정하세요.- .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { - if (currentStep < totalSteps - 1) { - currentStep++ - } else { - onOnboardingFinished() - } - } + .clickable( + interactionSource = interactionSource, + indication = null, + onClickLabel = "다음", + role = Role.Button + ) { + if (isAdvancing) return@clickable + isAdvancing = true + if (currentStep < totalSteps - 1) { + currentStep++ + } else { + onOnboardingFinished() + } + scope.launch { + delay(300) + isAdvancing = false + } + }해당 변경을 위해 동일 함수 상단(이 블록 바로 위)에 다음 상태/스코프 선언을 추가하세요:
+ val interactionSource = remember { MutableInteractionSource() } + val scope = rememberCoroutineScope() + var isAdvancing by remember { mutableStateOf(false) }필요한 import:
+import androidx.compose.ui.semantics.Role +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch
310-317: 마지막 안내 문구 리소스화 및 디자인시스템 폰트/컬러 적용 제안최종 카피도 하드코딩되어 있습니다.
Res.string.onboarding_final_cta로 이전하고, 가능하면 디자인시스템의 타이포/컬러 토큰을 적용하세요.- Text( - "이제 당신의 감정을 기록해 보세요", - color = Color.White, - fontSize = 20.sp, + Text( + stringResource(Res.string.onboarding_final_cta), + color = Color.White, // e.g. design system color token + fontSize = 20.sp, // e.g. design system typography modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 100.dp) )필요한 import:
+import org.jetbrains.compose.resources.stringResource
58-63: 네이밍 일관성 제안: OnBoarding → Onboarding파일/패키지 경로는
onboarding인데, 컴포저블 명은OnBoardingScreen입니다. 팀 컨벤션에 맞춰OnboardingScreen으로 통일하는 것을 권장합니다. (바이너리 API 안정성 이슈가 없다면)
📜 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.
⛔ Files ignored due to path filters (12)
core/designsystem/src/commonMain/composeResources/drawable/eight.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/first.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/five.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/four.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/nine.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/npcTextFieldBackground.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/second.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/seven.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/six.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/ten.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/textFieldBackground.pngis excluded by!**/*.pngcore/designsystem/src/commonMain/composeResources/drawable/third.pngis excluded by!**/*.png
📒 Files selected for processing (3)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt(1 hunks)feature/onboarding/build.gradle.kts(1 hunks)feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- feature/onboarding/build.gradle.kts
- core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
🧰 Additional context used
🧠 Learnings (3)
📚 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,androidMain,iosMain}/kotlin/**/*.kt : Leverage Compose resources via Res.drawable.* and Res.string.*
Applied to files:
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.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,androidMain,iosMain}/kotlin/**/*.kt : Use Compose Resources for strings, images, and other assets
Applied to files:
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.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/OnBoardingScreen.kt
🧬 Code graph analysis (1)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (1)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt (1)
TypingAnimatedSpeechBubble(37-119)
⏰ 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). (1)
- GitHub Check: Firebase App Distribution
🔇 Additional comments (3)
feature/onboarding/src/commonMain/kotlin/com/nexters/emotia/feature/onboarding/OnBoardingScreen.kt (3)
238-251: 3~10단계 이미지 전환 리팩토링 좋습니다
storyImages+simpleImageTransitionSteps로 전환 로직을 단일 블록으로 통합해 가독성과 유지보수성이 개선되었습니다.coerceIn(storyImages.indices)로 인덱스 보호한 것도 👍
57-63: 공개 API 콜백명 변경 확인 완료다음과 같이 검증했습니다.
- OnBoarding 관련 코드(
OnBoardingScreen.kt,OnBoardingNavigation.kt)에서 오직onOnboardingFinished만 사용 중- 기존 콜백명(
onNavigateToChatting)은 메인/결과 화면 네비게이션용으로, 온보딩 스크린 변경과 무관따라서 외부 호출부에서 새 콜백명으로의 일관된 교체가 완료되었으며, 추가 수정이 필요 없습니다.
200-203: slideInVertically 인자 순서 오류: 컴파일 불가
slideInVertically(tween(600)) { it / 2 }는 매개변수 순서가 뒤바뀌어 있습니다. 명시적 네이밍으로 교체하세요.- enter = slideInVertically(tween(600)) { it / 2 } + fadeIn(animationSpecFloat), + enter = slideInVertically( + initialOffsetY = { it / 2 }, + animationSpec = tween(durationMillis = 600) + ) + fadeIn(animationSpecFloat),Likely an incorrect or invalid review comment.

작업 내용
Summary by CodeRabbit