Skip to content

Conversation

@haeti-dev
Copy link
Collaborator

@haeti-dev haeti-dev commented Aug 22, 2025

작업 내용

  • 통이미지로 된 컴포넌트를 직접구현했습니다.
  • 요정 등장 화면을 디자인에 맞게 수정했습니다.
  • 화면 너비에 상관없이 요정 선택 페이저가 중앙 정렬되게 하였습니다.

이건 꼭 봐주세요

  • 리펙토링할게 참 많네요..

Summary by CodeRabbit

  • New Features

    • 결과 화면에 단계별 탭 진행과 타이핑 말풍선 표시 추가, 마지막 단계에서 편지 화면으로 이동.
  • Style

    • 말풍선 UI를 단색 배경+테두리로 재디자인하고 NPC 라벨/장식 추가.
    • 채팅 화면 요정 카드 페이저를 화면 폭에 맞춰 중앙 정렬·고정 크기 제공.
    • 결과 화면 배경·이미지 배치 개선 및 페이드·그라데이션 시각 효과 강화.
    • 편지 화면 이미지 세로 위치 미세 조정.
  • Refactor

    • 텍스트 크기 렌더링을 밀도에 따라 일관되게 적용하는 유틸리티 추가.

@haeti-dev haeti-dev self-assigned this Aug 22, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 22, 2025

Walkthrough

대화 말풍선 컴포넌트를 배경 이미지 기반에서 테마 기반 박스+보더 구조로 재구성하고, Dp→Sp 변환 유틸을 추가했습니다. 요정 등장 화면은 단계별 인터랙션과 타이핑 말풍선을 포함하는 새로운 레이아웃으로 개편되었습니다. 채팅 페이저는 고정 카드 폭과 동적 패딩으로 가운데 정렬되며, 편지 화면 이미지는 y 오프셋이 소폭 조정되었습니다.

Changes

Cohort / File(s) Summary
DesignSystem • Speech bubble refactor
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt, core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/util/DpExt.kt
말풍선 배경 이미지를 제거하고 투명 블랙 배경+화이트 보더 박스로 교체. ic_polygon/img_npc는 painterResource로 레이어링. 높이 고정(150.dp). useAlternativeBackground 분기별 텍스트 배치/스타일 재구성. 테마 색상/타이포 사용. Dp.toTextPx()(@composable) 유틸 추가.
Result • Fairy screen flow
feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/fairy/FairyScreen.kt
multi-step(currentStep) 인터랙션 추가, stepTexts에 따른 TypingAnimatedSpeechBubble 표시(AnimatedVisibility). 배경·그라디언트·요정 이미지 레이아웃 재구성( SubcomposeAsyncImage 사용 ). 최종 단계 후 onNavigateToLetter 호출로 네비게이션.
Chat • Pager centering
feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt
BoxWithConstraints로 available width 계산, cardWidth=200.dp 고정, horizontalPadding 계산을 통한 HorizontalPager 가운데 정렬, PageSize.Fixed 사용. FairyCard에 modifier 파라미터 추가(기존 시그니처 확장).
Result • Letter tweak
feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/letter/LetterScreen.kt
요정 이미지 y 오프셋을 -56.dp에서 -49.dp로 조정.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as 사용자
  participant FairyScreen as FairyScreen
  participant TypingBubble as TypingAnimatedSpeechBubble
  participant Nav as Navigator

  User->>FairyScreen: 화면 진입
  FairyScreen->>FairyScreen: currentStep = 0, 애니메이션/표시 준비
  FairyScreen->>TypingBubble: stepTexts[currentStep]로 타이핑 시작
  User-->>FairyScreen: 화면 탭
  FairyScreen->>FairyScreen: currentStep 증가 (0→1→2)
  FairyScreen->>TypingBubble: 표시 텍스트 갱신/재시작
  User->>FairyScreen: 탭(최종 단계 이후)
  FairyScreen->>Nav: onNavigateToLetter()
  Note over TypingBubble: 타이핑: 공백 포함 시 단어 단위, 그 외 문자 단위
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
요정 등장 화면 수정 및 구현 (#40)
이미지로 되어 있는 칩을 텍스트 기반으로 변경 (#40) 변경 내역에 칩(이미지→텍스트) 전환 관련 코드/컴포넌트 수정이 포함되어 있지 않습니다.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
채팅 페이저 가운데 정렬 및 카드 폭 고정 (feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt) 이 변경은 요정 등장 화면과 칩 변경 요구와 직접 관련되지 않음 — 레이아웃/페이저 동작 개선으로 보임.
편지 화면 이미지 y 오프셋 조정 -56.dp → -49.dp (feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/letter/LetterScreen.kt) 미세한 레이아웃 튜닝이며, 이슈 #40의 명시적 목표(요정 등장 화면, 칩 변경)와 직접 연관되지 않습니다.

Possibly related PRs

Suggested reviewers

  • sxunea
  • jife-archive

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 feature/fairy-show

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

🧹 Nitpick comments (17)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/util/DpExt.kt (2)

8-9: 함수명과 반환 단위 불일치: toTextPx는 SP(TextUnit)를 반환합니다.

이름은 px를 연상시키지만 실제론 TextUnit.Sp를 반환합니다. 혼동을 줄이기 위해 새 이름으로 노출하거나, 기존 API는 Deprecated 처리 후 포워딩하는 걸 권장합니다.

아래와 같이 리네이밍 + Deprecated 포워딩을 제안드립니다.

-@Composable
-fun Dp.toTextPx(): TextUnit = with(LocalDensity.current) { [email protected]().toSp() }
+@Deprecated("Use toSpText()", ReplaceWith("this.toSpText()"))
+@Composable
+fun Dp.toTextPx(): TextUnit = toSpText()
+
+@Composable
+fun Dp.toSpText(): TextUnit = with(LocalDensity.current) { [email protected]().toSp() }

3-9: 타이포그래피 단위 일관성 재검토 제안

DP→SP 변환 유틸은 사용처에서 의미가 모호해질 수 있습니다. 가능하면 텍스트 사이즈는 sp(예: 14.sp) 또는 테마 타이포그래피 토큰을 직접 사용해 단위를 명확히 하세요. 유틸은 꼭 필요한 케이스(디자인 토큰 이관 등) 한정으로 사용 권장합니다.

feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt (2)

240-251: O(n) 인덱스 계산 제거: itemsIndexed 사용으로 비용 절감

현재 각 아이템에서 indexOf로 인덱스를 구해 O(n^2) 패턴이 됩니다. itemsIndexed로 치환하면 불필요한 탐색을 제거할 수 있습니다.

아래처럼 변경을 제안드립니다(상단에 import androidx.compose.foundation.lazy.itemsIndexed 추가 필요).

-                        items(
-                            items = uiState.messages,
-                            key = { message -> message.timestamp }
-                        ) { message ->
-                            val messageIndex = uiState.messages.indexOf(message)
+                        itemsIndexed(
+                            items = uiState.messages,
+                            key = { _, message -> message.timestamp }
+                        ) { index, message ->
                             ChatBubble(
                                 text = message.text,
                                 type = message.type,
                                 messageId = message.timestamp.toString(),
-                                skipTypewriterEffect = messageIndex <= 1
+                                skipTypewriterEffect = index <= 1
                             )
                         }

추가 import:

import androidx.compose.foundation.lazy.itemsIndexed

425-458: 오버레이 접근성/세만틱스 보완 제안

터치 차단을 위해 빈 clickable를 두신 점은 합리적이지만, 보조기기 탐색에서도 포커스가 가지 않도록 세만틱스 제외를 권장합니다.

예: Modifier.semantics { this.invisibleToUser() } 또는 clearAndSetSemantics { } 추가를 고려해주세요.

core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt (6)

93-99: 장식용 폴리곤 이미지는 contentDescription 제거

장식 목적의 아이콘은 스크린리더에 노출하지 않는 편이 접근성에 유리합니다. contentDescription = null로 설정을 권장합니다.

-        Image(
-            painter = painterResource(Res.drawable.ic_polygon),
-            contentDescription = "Polygon icon",
+        Image(
+            painter = painterResource(Res.drawable.ic_polygon),
+            contentDescription = null,
             modifier = Modifier
                 .align(Alignment.BottomEnd)
                 .padding(end = 36.dp, bottom = 64.dp)
         )

101-108: 텍스트 사이즈는 sp로 직접 지정 권장

14.dp.toTextPx() 대신 14.sp로 명시하면 단위가 분명해지고 유틸 의존이 줄어듭니다. 또한 테마 토큰이 이미 sp를 포함한다면 override 자체가 필요 없을 수 있습니다.

-                style = typography.emotia14M.copy(
-                    fontSize = 14.dp.toTextPx()
-                ),
+                style = typography.emotia14M.copy(
+                    fontSize = 14.sp
+                ),

116-123: NPC 이미지는 의미가 있다면 대체 텍스트 구체화, 없다면 숨김 처리

현재 "NPC character"는 정보 가치가 낮습니다. 의미가 없다면 null, 있다면 역할/상태를 담은 한국어 설명으로 갱신해주세요.


125-139: 하드코딩 문자열 리소스화

"요정여왕"은 리소스로 분리해 현지화와 일관된 관리가 가능하도록 해주세요. Compose Multiplatform의 stringResource 사용을 권장합니다.

예시:

-            Text(
-                text = "요정여왕",
+            Text(
+                text = stringResource(Res.string.npc_queen),
                 color = colors.white,
                 style = typography.emotia14M.copy(
-                    fontSize = 14.dp.toTextPx()
+                    fontSize = 14.sp
                 ),

추가 필요:

  • import org.jetbrains.compose.resources.stringResource
  • emotia.core.designsystem.generated.resourcesnpc_queen 문자열 추가
    원하시면 리소스 추가 PR 패치도 제공하겠습니다.

141-158: 본문 텍스트도 sp 직접 지정 및 패딩/오프셋 상수화 제안

14.dp.toTextPx()14.sp로 단위 명확화, 매직 넘버 패딩/오프셋(예: start = 136.dp)은 상수로 추출하면 유지보수성이 올라갑니다.

-                style = typography.emotia14M.copy(
-                    fontSize = 14.dp.toTextPx()
-                ),
+                style = typography.emotia14M.copy(
+                    fontSize = 14.sp
+                ),

50-69: 타이핑 애니메이션 토크나이징 개선 여지

split(' ')는 연속 공백/개행을 제대로 다루지 못합니다. Regex("\\s+") 기반 분할 또는 개행 처리 보완을 고려해주세요. 또한 딜레이(150/100ms)를 파라미터로 노출하면 테스트 및 접근성(감속 모드) 대응이 용이합니다.

feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/letter/LetterScreen.kt (2)

131-147: 콘텐츠 설명 보강: URL 대신 요정 이름 사용

contentDescription = "$fairyImage image"는 URL/경로가 들어갈 수 있어 비직관적입니다. 접근성을 위해 요정 이름 기반으로 바꾸는 걸 권장합니다.

-                contentDescription = "$fairyImage image",
+                contentDescription = "$fairyName 이미지",

또는 장식 목적이라면 null로 비노출 처리하세요.


170-185: 문구 문자열 리소스화 및 플레이스홀더 국제화

"${fairyName}에게 위로의 말을 건네보자." 등 UI 문구는 리소스로 분리해 현지화/일관성을 확보하세요. placeholder도 stringResource의 포맷 문자열을 활용하면 깔끔합니다.

원하시면 Res.string 항목 초안을 함께 제공하겠습니다.

feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/fairy/FairyScreen.kt (5)

90-96: 스텝 문구 하드코딩 → 문자열 리소스 전환

"앗...!", "저 아이는... ${fairyName}이야!", "${fairyName}에게 위로를 건네볼까?"는 리소스로 분리하고, 이름 치환은 포맷 문자열로 처리해 주세요.

예: Res.string.fairy_step_1, Res.string.fairy_step_2_with_name, Res.string.fairy_step_3_with_name


81-88: 매직 넘버 상수화: 반경 값 2000f/1800f

애니메이션 최대 반경(2000f) 및 완료 임계값(1800f)을 상수로 추출하면 가독성과 조정 용이성이 높아집니다.

아래 상수 예시(파일 상단 또는 companion object):

private const val EXPANSION_MAX_RADIUS = 2000f
private const val EXPANSION_DONE_THRESHOLD = 1800f

그리고 사용부:

targetValue = if (uiState.isExpanding) EXPANSION_MAX_RADIUS else 0f

97-112: 탭 진행 조건의 임계값도 상수 사용 권장

animatedRadius >= 1800f 반복 사용은 상수로 대체하세요. 상기 제안한 EXPANSION_DONE_THRESHOLD로 치환하면 일관성이 높아집니다.

-                if (animatedRadius >= 1800f) {
+                if (animatedRadius >= EXPANSION_DONE_THRESHOLD) {

122-137: 이미지 contentDescription 개선

"$fairyImage image"는 정보 전달력이 낮습니다. 사용자에게 의미 있는 "${uiState.fairyName} 이미지" 등으로 대체하거나, 장식이라면 null로 처리해주세요.


182-194: AnimatedVisibility의 불필요한 visible=true 제거

이미 if (stepTexts != null && animatedRadius >= 1800f)에 의해 표시 조건이 보장되므로 visible = true는 중복입니다. 제거로 간결성 향상.

-            AnimatedVisibility(
-                visible = true,
+            AnimatedVisibility(
                 enter = fadeIn(tween(300)),
                 exit = fadeOut(tween(300)),
                 modifier = Modifier.align(Alignment.BottomCenter)
             ) {

또는 if 제거 후 visible = stepTexts != null && animatedRadius >= EXPANSION_DONE_THRESHOLD로 일원화하는 방법도 있습니다.

📜 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 79b0c1a and 1d09d78.

⛔ Files ignored due to path filters (2)
  • core/designsystem/src/commonMain/composeResources/drawable/ic_polygon.png is excluded by !**/*.png
  • core/designsystem/src/commonMain/composeResources/drawable/img_npc.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt (3 hunks)
  • core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/util/DpExt.kt (1 hunks)
  • feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt (3 hunks)
  • feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/fairy/FairyScreen.kt (5 hunks)
  • feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/letter/LetterScreen.kt (1 hunks)
🧰 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/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/fairy/FairyScreen.kt
  • core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.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/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/fairy/FairyScreen.kt
  • core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.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/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/fairy/FairyScreen.kt
  • core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt
  • feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt
🧬 Code graph analysis (2)
feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/fairy/FairyScreen.kt (1)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt (1)
  • TypingAnimatedSpeechBubble (42-161)
feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt (1)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/FairyCard.kt (1)
  • FairyCard (38-138)
⏰ 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 (5)
feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt (3)

302-318: 페이저 가운데 정렬 로직 👍

BoxWithConstraintsmaxWidth 대비 cardWidth 기반의 동적 contentPadding을 계산해 가운데 정렬하는 접근이 깔끔합니다. 다양한 화면 폭에서 안정적으로 동작할 것으로 보입니다.


137-143: roomId의 Int 캐스팅 안전성 확인 필요

uiState.roomId?.toInt() ?: 0는 원본이 Long/BigInt일 경우 오버플로 우려가 있습니다. 네비게이션 파이프라인이 Int를 강제한다면 상한 검증 또는 타입 일치(가능하면 Long 유지)를 검토해주세요.

잠재적 대응:

  • onNavigateToFairy의 chatRoomIdLong으로 변경, 혹은
  • 안전 변환(범위 체크) 및 로깅 추가

319-367: 선택 카드 기준 스포트라이트 좌표 캡처 로직 적절

선택된 카드에서만 onGloballyPositioned로 중심 좌표/반경을 갱신하는 방식이 불필요한 계산을 줄여줍니다. y축 보정(-50.dp)도 시각적 중심을 잘 맞춥니다.

core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/SpeechBubbleTextField.kt (1)

71-92: 고정 높이(150.dp) 박스: 작은 화면/다국어 줄바꿈 대응 검토

고정 높이로 말풍선이 잘릴 수 있습니다. 최대 높이/스크롤 또는 적응형 높이 전략을 검토해주세요.

feature/result/src/commonMain/kotlin/com/nexters/emotia/feature/result/fairy/FairyScreen.kt (1)

113-153: 배경/그라데이션 레이아웃 구성 적절

레터 배경 + 하단 그라데이션 영역으로 단계별 인터랙션을 유도하는 구조가 직관적입니다. 이후 단계에서 CTA 노출을 고려하기에도 확장성이 좋아 보입니다.

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

🧹 Nitpick comments (6)
feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt (6)

302-368: 페이저 가운데 정렬 접근 훌륭합니다. 카드 치수 상수화 및 매직 넘버(50.dp) 정리 제안

BoxWithConstraints + PageSize.Fixed로 가로 중앙 정렬을 안정적으로 맞추신 점 좋습니다. 다만 카드 폭/높이가 여기저기 중복(200.dp, 280.dp)되고, 스포트라이트 중심 보정값 50.dp가 매직 넘버로 남아 있어 유지보수성이 떨어집니다. 아래처럼 cardHeight와 spotlightYOffset을 상수로 두고 재사용하면 가독성과 변경 용이성이 올라갑니다.

 BoxWithConstraints {
-    val cardWidth = 200.dp
+    val cardWidth = 200.dp
+    val cardHeight = 280.dp
+    val spotlightYOffset = 50.dp
     val horizontalPadding = maxOf(
         0.dp,
         (maxWidth - cardWidth) / 2
     )
 
     HorizontalPager(
         state = pagerState,
         modifier = Modifier
             .fillMaxWidth()
             .padding(vertical = 28.dp),
         pageSize = PageSize.Fixed(cardWidth),
         pageSpacing = 24.dp,
         contentPadding = PaddingValues(horizontal = horizontalPadding)
     ) { page ->
         val fairy = uiState.fairies[page]
         val isSelected =
             page == pagerState.currentPage
 
         val yOffset by animateFloatAsState(
             targetValue = if (isSelected) 0f else 28f,
             animationSpec = tween(
                 durationMillis = 300,
                 easing = FastOutSlowInEasing
             )
         )
 
         FairyCard(
             name = fairy.name,
             image = fairy.silhouetteImage,
             emotion = fairy.emotion,
             emotionDescription = fairy.description,
             isSelected = isSelected,
             modifier = Modifier
-                .size(
-                    width = 200.dp,
-                    height = 280.dp
-                )
+                .size(
+                    width = cardWidth,
+                    height = cardHeight
+                )
                 .offset(y = yOffset.dp)
                 .then(
                     if (isSelected) {
                         Modifier.onGloballyPositioned { coordinates ->
                             val position =
                                 coordinates.positionInRoot()
                             val size =
                                 coordinates.size
                             fairyCardCenter =
                                 Offset(
                                     x = position.x + size.width / 2,
-                                    y = position.y + size.height / 2 - with(
-                                        density
-                                    ) { 50.dp.toPx() }
+                                    y = position.y + size.height / 2 - with(
+                                        density
+                                    ) { spotlightYOffset.toPx() }
                                 )
                             fairyCardSize =
                                 minOf(
                                     size.width,
                                     size.height
                                 ) / 2f
                         }
                     } else {
                         Modifier
                     }
                 )
         )
     }
 }

292-299: Pager ↔ ViewModel 선택 상태 동기화(양방향) 보강 권장

현재는 Pager → VM(SelectFairy) 방향만 동기화됩니다. 외부 요인(복원, 딥링크, 서버 추천 등)으로 uiState.selectedFairyIndex가 바뀔 때 Pager가 그 페이지로 스크롤되지 않을 가능성이 있습니다. 아래 효과를 추가해 양방향 일관성을 확보하는 것을 권장합니다.

 LaunchedEffect(pagerState.currentPage) {
     viewModel.handleIntent(
         ChattingIntent.SelectFairy(
             pagerState.currentPage
         )
     )
 }
+
+// VM -> UI 동기화: 외부에서 selectedFairyIndex가 변경되면 Pager도 맞춰줍니다.
+LaunchedEffect(uiState.selectedFairyIndex) {
+    val target = uiState.selectedFairyIndex.coerceIn(0, pagerState.pageCount - 1)
+    if (pagerState.currentPage != target) {
+        pagerState.animateScrollToPage(target)
+    }
+}

160-168: 스크롤 인덱스 계산이 타이핑 인디케이터/페이저 동시 노출 시 빗나갈 수 있음

현재 showFairyPager가 true일 때 lastIndex를 messages.size로만 계산합니다. 동시에 isLoading이 true면 타이핑 인디케이터 아이템이 삽입되어 페이저까지 스크롤되지 않을 수 있습니다. 아래처럼 조건에 따른 추가 아이템 수를 반영해 정확한 마지막 인덱스로 스크롤하는 것을 권장합니다.

-LaunchedEffect(uiState.messages.size, uiState.showFairyPager) {
+LaunchedEffect(uiState.messages.size, uiState.showFairyPager, uiState.isLoading) {
     if (uiState.messages.isNotEmpty()) {
-        val lastIndex = if (uiState.showFairyPager) {
-            uiState.messages.size
-        } else {
-            uiState.messages.size - 1
-        }
+        val typingItemCount = if (uiState.isLoading && uiState.messages.isNotEmpty()) 1 else 0
+        val pagerItemCount = if (uiState.showFairyPager) 1 else 0
+        val lastIndex = uiState.messages.size + typingItemCount + pagerItemCount - 1
         lazyListState.animateScrollToItem(lastIndex)
     }
 }

111-123: Spotlight 시작 반경 2000f 상수 — 초대형 화면에서 불충분할 수 있음

애니메이션 시작 반경을 2000f로 고정하면 태블릿/데스크톱 초고해상도에서 화면 전체가 완전히 “비가려진 상태”로 시작하지 않을 수 있습니다. 최대 대각선 길이 기반의 동적 계산(예: hypot(width, height))으로 full-screen 반경을 산출해 시작값으로 쓰는 방식을 고려해 주세요. BoxWithConstraints 또는 Canvas의 size를 활용해 동적으로 반영하는 것이 안전합니다.


474-481: 하드코딩된 그라데이션 색상 → 디자인 토큰 사용 권장

CreateRoom의 배경 그라데이션이 Hex로 하드코딩되어 있습니다. 상단 채팅 화면과 동일하게 LocalEmotiaColors를 사용하면 테마 일관성과 유지보수성이 좋아집니다.

-    Column(
-        modifier = modifier
+    val colors = LocalEmotiaColors.current
+    Column(
+        modifier = modifier
             .fillMaxSize()
             .background(
                 Brush.verticalGradient(
-                    colors = listOf(
-                        Color(0xFF171E2D),
-                        Color(0xFF1A1A1B)
-                    )
+                    colors = listOf(
+                        colors.backgroundBlue,
+                        Color.Black
+                    )
                 )
             )

542-542: 입력 영역 고정 padding(24.dp) — IME/세이프에어리어와 합쳐 과다 여백 가능성 점검

상위 Column에 이미 imePadding이 적용되어 있어, 하단 Box의 vertical = 24.dp가 특정 기기에서 과도한 여백으로 보일 수 있습니다. 디자인 의도라면 OK지만, 필요 시 키보드 표시 여부에 따라 padding을 보정하거나 spacing을 Emotia 디자인 토큰으로 치환하는 것을 고려해 주세요.

📜 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 1d09d78 and 556a08c.

📒 Files selected for processing (1)
  • feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt (5 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/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt
🧬 Code graph analysis (1)
feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt (1)
core/designsystem/src/commonMain/kotlin/com/nexters/emotia/core/designsystem/component/FairyCard.kt (1)
  • FairyCard (38-138)
⏰ 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 (1)
feature/chatting/src/commonMain/kotlin/com/nexters/emotia/feature/chatting/ChattingScreen.kt (1)

487-487: 배경 이미지 컨테이너에서 fillMaxWidth + weight(1f) 조합 적절합니다

상단 영역이 남는 공간을 유연하게 차지하도록 처리되어 레이아웃 안정성이 좋습니다.

@haeti-dev haeti-dev merged commit dfa8537 into develop Aug 26, 2025
3 checks passed
@haeti-dev haeti-dev deleted the feature/fairy-show branch August 26, 2025 13:35
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.

[feature] 요정 등장 화면 구현

3 participants