[FEAT/#42] 채팅 대화문 평가 API 연동 - #43
Conversation
Walkthrough평가 기능(평가 요청/응답 모델, 매퍼, 리포지토리/DI, 유즈케이스, ViewModel·UI·컴포넌트, 리소스)과 관련한 대규모 추가·수정이 이루어졌고, 메시지 엔터티·도메인·Room 스키마가 재구성되며 마이그레이션(1→2, 2→3)과 DB 버전(1→3) 업데이트가 포함됐다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant UI as ChatScreen / ChatBubble
participant VM as ChatViewModel
participant UC as EvaluationUseCase
participant Repo as EvaluationRepository
participant DS as EvaluationRemoteDataSource
participant API as Evaluation API
participant DB as Room/MessageDao
User->>UI: 메시지 전송
UI->>VM: postChat()
VM->>VM: 봇 응답 처리
VM->>VM: fetchEvaluation(conversationId)
VM->>DB: 마지막 사용자 메시지 상태=Loading로 업데이트
VM->>UC: EvaluationRequestData 호출
UC->>Repo: postEvaluation(request)
Repo->>DS: toDto() 및 원격 호출
DS->>API: HTTP POST
API-->>DS: EvaluationResponseDto
DS-->>Repo: DTO 반환
Repo-->>UC: Domain 결과 반환
UC-->>VM: Result< EvaluationResponseData >
alt 성공
VM->>VM: evaluationState 계산 (PASS/NOT_PASS)
VM->>DB: updateMessage(평가 결과 JSON 포함)
VM->>UI: UI 업데이트 (아이콘/데이터)
else 실패
VM->>VM: evaluationState=NOT_PASS, 에러 처리
VM->>UI: 에러 반영
end
Note over UI: 사용자 클릭 시
User->>UI: 평가 아이콘 클릭
UI->>UI: EvaluationDetailDialog 표시 (evaluationData)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt (1)
1-35: 긴급: MIGRATION_1_2에서 ALTER TABLE ... DROP COLUMN 사용 — 마이그레이션 실패 위험
- 확인: AppDatabase 버전=2, MIGRATION_1_2는 message 테이블에서 passed / commentContextuality / commentLexicalVariety 컬럼을 DROP하고 contextualityPassed(INTEGER), contextualityComment(TEXT), grammarPassed(INTEGER), grammarComment(TEXT), grammarErrors(TEXT)를 ADD합니다. MessageEntity/MessageData와 매퍼는 새 필드를 반영하고 있습니다.
- 문제: ALTER TABLE ... DROP COLUMN은 런타임 SQLite 버전에 따라 지원되지 않을 수 있어(구형 Android 환경에서 실패 가능) 현재 구현은 일부 디바이스에서 마이그레이션 실패를 초래할 위험이 큽니다.
- 조치(권장): MIGRATION_1_2를 안전한 패턴(새 테이블 생성 → 기존 데이터 복사 → 기존 테이블 삭제 → 새 테이블 rename → 인덱스·외래키·제약 재생성)으로 교체하세요. 수정 위치: app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt. 새 컬럼은 NULL 허용으로 엔티티·도메인 모델과 일치하므로 NOT NULL/기본값 문제는 없습니다.
🧹 Nitpick comments (23)
app/src/main/res/drawable/ic_check_22.xml (1)
2-8: 아이콘 색상 하드코딩 → 테마/틴트로 제어 권장, 크기 비율 확인
- path의 fillColor가 고정(#6E7FFF)이라 다크/라이트 테마에서 부자연스러울 수 있습니다. 사용처(Icon/Image)에서 tint로 제어하거나 vector 레벨의 android:tint 적용을 검토해주세요.
- width=22dp, height=23dp(뷰포트 22x23)는 다른 22dp 아이콘과 베이스라인이 어긋날 수 있습니다. 상하 여백/정렬을 한번 확인 부탁드립니다.
app/src/main/res/drawable/ic_caution_14.xml (1)
6-9: NOT_PASS 아이콘 색상/접근성 보완 제안
- 색상 #FF9AA7가 하드코딩되어 있습니다. 사용처에서 tint로 제어해 Color.kt의 SecondaryRed와 일관성을 유지하는 방식을 권장합니다.
- 상태 전달을 색상에만 의존하지 않도록 Icon(..., contentDescription=...) 등 대체 텍스트를 함께 제공해주세요.
.github/PULL_REQUEST_TEMPLATE.md (2)
1-3: 연관 이슈 표기 형식 보완GitHub 자동 링크를 위해 키워드 사용을 권장합니다. 예시:
-## #️⃣연관된 이슈 -- closed +## #️⃣ 연관된 이슈 +- Closes #42
11-12: 이미지/비디오 대체 텍스트 및 속성 보완 (markdownlint MD045 대응)
- 이미지 alt 텍스트 및 width 속성 따옴표, 비디오 controls 속성을 추가해주세요.
-<img src="" width=300 /> -<video src="" width=300 /> +<img src="" width="300" alt="채팅 평가 UI 스크린샷" /> +<video src="" width="300" controls aria-label="채팅 평가 UI 동영상 미리보기" />app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt (1)
58-60: 메서드 명명 컨벤션(nitpick)
ExportSentenceDao()처럼 UpperCamelCase 메서드명은 Kotlin/Room 관례와 다릅니다. 추후 API 변경 시exportSentenceDao()로 정리하는 것을 제안합니다.app/src/main/java/com/malharang/app/data/repositoryimpl/MessageRepositoryImpl.kt (1)
24-26: updateMessage가 insertMessage(insert with REPLACE)를 호출 — '업데이트' vs '업서트' 의도를 명확히 하세요.MessageDao.insertMessage는 @insert(onConflict = OnConflictStrategy.REPLACE)이므로 제약 위반은 발생하지 않지만, 메서드명(updateMessage)과 반환형(Long)이 실제 동작(업서트)과 불일치해 혼동을 유발합니다. 의도에 따라 하나를 선택해 정리하세요:
순수 업데이트를 원하면: DAO에
@Update suspend fun updateMessage(message: MessageEntity): Int추가하고 repository/domain/usecase의updateMessage반환형을 Int로 변경. (파일: app/src/main/java/com/malharang/app/data/local/dao/MessageDao.kt, app/src/main/java/com/malharang/app/data/repositoryimpl/MessageRepositoryImpl.kt, app/src/main/java/com/malharang/app/domain/repository/MessageRepository.kt, app/src/main/java/com/malharang/app/domain/usecase/MessageUseCase.kt)업서트(현재 동작)를 원하면: 메서드명을
upsertMessage로 명확화하고 Long 반환을 유지. Room 2.5+이면 DAO에@Upsert사용, 아니면 기존@Insert(onConflict = REPLACE)를 유지하되 DAO/Repository/Domain/UseCase에서 이름을 통일하세요. (동일 파일들)당장 변경하지 않으려면: 현재 구현도 문제를 일으키지 않음(REPLACE가 충돌을 방지). 다만 혼동 방지를 위해 메서드명 또는 주석으로 의도를 문서화하세요.
app/src/main/java/com/malharang/app/presentation/screen/chat/ChatScreen.kt (2)
241-244: 평가 아이콘 탭 시 데이터가 null이면 무반응입니다 — 최소한의 사용자 피드백 또는 클릭 비활성화가 필요합니다.현재
chat.evaluationData가 없으면 람다가 no-op로 끝납니다. 사용자는 아이콘을 탭해도 아무 일도 일어나지 않아 혼란스러울 수 있습니다. 간단히 토스트를 보여주거나 클릭 자체를 비활성화하세요.권장(간단): 토스트 추가
- onEvaluationClick = { - chat.evaluationData?.let { evaluationData -> - selectedEvaluationData = evaluationData - } - } + onEvaluationClick = { + chat.evaluationData?.let { evaluationData -> + selectedEvaluationData = evaluationData + } ?: run { + // TODO: string 리소스로 이동 + androidx.compose.ui.platform.LocalContext.current + .toast("평가 세부 정보가 아직 없어요") + } + }참고: 위 변경을 사용하려면
ChatScreen스코프에서LocalContext접근이 필요합니다(파일 상단에 이미 import 있음). 원한다면 클릭을 완전히 비활성화하는 방향(아이콘을 비클리커블로 렌더링)도 제안 가능합니다.
154-154: UI 계층에 도메인 모델을 직접 보관 중입니다.
selectedEvaluationData타입이com.malharang.app.domain.model.EvaluationResponseData로 UI 계층에 누수되고 있습니다. 프레젠테이션 전용 모델(또는 최소typealias)로 치환해 의존 역전을 유지하세요.예:
app/presentation/model/EvaluationUiData.kt를 도입하고 매퍼에서 변환.app/src/main/java/com/malharang/app/presentation/model/ChatMessageModel.kt (1)
13-15: 프레젠테이션 모델에 도메인 타입(EvaluationResponseData) 직접 포함 — 계층 분리를 권장합니다.UI는 도메인 변경의 파급을 피하기 위해 자체 모델을 갖는 편이 안전합니다. 변환은 매퍼에서 수행하세요.
추가로
evaluationState가 null 가능인데, 호출부에서?: EMPTY로 보정하므로 OK입니다.app/src/main/java/com/malharang/app/presentation/screen/chat/component/ChatBubble.kt (2)
201-210: 평가 아이콘 접근성: 터치 타깃/설명 부족.
- 터치 타깃 24dp는 권장 최소(48dp) 미만.
contentDescription = null로 스크린 리더 접근 불가.권장 수정:
- Icon( + Icon( imageVector = ImageVector.vectorResource(evaluationState.icon), - contentDescription = null, + // TODO: string 리소스로 이동 + contentDescription = "평가 결과 보기", tint = Color.Unspecified, modifier = modifier - .size(24.dp) + .size(36.dp) // 터치 타깃 확장 + .padding(6.dp) // 실제 아이콘은 24dp .clip(CircleShape) .clickable(onClick = onClick) )
193-199: 로딩 인디케이터 접근성 라벨 추가 권장.로딩 상태를 스크린 리더가 알 수 있도록 semantics 라벨을 추가하세요.
- CircularProgressIndicator( + CircularProgressIndicator( strokeWidth = loadingStrokeWidthDp.dp, color = colors.greenBasic, - modifier = modifier + modifier = modifier .size(16.dp) + .semantics { contentDescription = "평가 중" } )필요 import:
import androidx.compose.ui.semantics.semanticsimport androidx.compose.ui.semantics.contentDescriptionapp/src/main/java/com/malharang/app/domain/mapper/ChatMessageDataMapper.kt (1)
19-23: 아이콘 클릭-데이터 불일치 가능성(UX).
evaluationState는contextualityPassed만으로 결정되지만,evaluationData는 grammar 필드까지 필요합니다. 그 결과 PASS/NOT_PASS 아이콘이 보이는데 다이얼로그 데이터가 없어 탭 시 무반응이 될 수 있습니다(상위 코멘트 참조).옵션:
- 상태 노출을
evaluationData가 생성된 경우에만 하거나,evaluationData를 contextuality만으로도 생성해(Grammar는 empty) 상세 보기 가능하게 하세요.Also applies to: 25-52
app/src/main/java/com/malharang/app/domain/model/ConversationData.kt (1)
15-19: 마이그레이션 확인 — MIGRATION_1_2가 추가되어 있음; DROP COLUMN 호환성 검증 필요
확인된 파일:
- app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt (MIGRATION_1_2: ALTER TABLE ... DROP/ADD COLUMN, addMigrations 호출)
- app/src/main/java/com/malharang/app/data/local/AppDatabase.kt (version = 2)
- app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt (새 필드: contextualityPassed, contextualityComment, grammarPassed, grammarComment, grammarErrors)
- app/src/main/java/com/malharang/app/domain/model/ConversationData.kt (MessageData에 동일 필드)
- 매퍼: app/src/main/java/com/malharang/app/data/mapper/todomain/MessageEntityMapper.kt, app/src/main/java/com/malharang/app/data/mapper/toentity/MessageDataMapper.kt (필드 매핑 반영)
결론/조치(권고): 마이그레이션 누락 문제는 해결되어 있음. 다만 ALTER TABLE ... DROP COLUMN은 일부 Android/SQLite 환경에서 지원되지 않아 마이그레이션 실패 가능성이 있으므로 구버전 디바이스에서 마이그레이션을 테스트하거나 DROP COLUMN 대신 안전한 새 테이블 생성→데이터 복사 방식으로 변경할 것.
app/src/main/java/com/malharang/app/presentation/screen/chat/component/EvaluationDetailDialog.kt (4)
75-86: 헤더 아이콘이 맥락성만 반영됨 — 전체 평가 결과를 반영하도록 수정 필요grammar가 실패해도 contextuality만 PASS이면 상단 아이콘이 체크로 표시됩니다. 두 지표를 모두 반영하도록 조건을 보완하세요.
- imageVector = ImageVector.vectorResource( - if (evaluationData.data.contextuality.pass) { + imageVector = ImageVector.vectorResource( + if (evaluationData.data.contextuality.pass && evaluationData.data.grammar.pass) { R.drawable.ic_check_22 } else { R.drawable.ic_caution_14 } ),
69-74: 하드코딩된 문자열을 stringResource로 치환해 i18n/l10n 지원영문 하드코딩이 다수 존재합니다. stringResource로 교체하고 strings.xml에 추가하세요. PASS/FAIL도 지역화 대상입니다.
+import androidx.compose.ui.res.stringResource @@ - text = "Evaluation Result", + text = stringResource(R.string.evaluation_result), @@ - title = "Contextuality", + title = stringResource(R.string.evaluation_contextuality), @@ - title = "Grammar", + title = stringResource(R.string.evaluation_grammar), @@ - Text( - text = "Grammar Errors", + Text( + text = stringResource(R.string.evaluation_grammar_errors), @@ - text = if (passed) "PASS" else "FAIL", + text = if (passed) stringResource(R.string.pass) else stringResource(R.string.fail), @@ - text = "Original", + text = stringResource(R.string.evaluation_original), @@ - text = grammarError.originalSentence, + text = grammarError.originalSentence, @@ - text = "Correction", + text = stringResource(R.string.evaluation_correction), @@ - text = grammarError.correctedSentence, + text = grammarError.correctedSentence, @@ - text = "Close", + text = stringResource(R.string.common_close),strings.xml 예시(별도 파일):
<resources> <string name="evaluation_result">Evaluation Result</string> <string name="evaluation_contextuality">Contextuality</string> <string name="evaluation_grammar">Grammar</string> <string name="evaluation_grammar_errors">Grammar Errors</string> <string name="evaluation_original">Original</string> <string name="evaluation_correction">Correction</string> <string name="pass">PASS</string> <string name="fail">FAIL</string> <string name="common_close">Close</string> </resources>Also applies to: 100-105, 108-113, 118-124, 191-201, 238-241, 243-249, 252-256, 258-264, 141-145, 153-161
126-128: LazyColumn items에 key 미지정 — 스크롤/재조합 안정성 개선항목 key를 지정해 불필요한 재조합과 스크롤 위치 흔들림을 줄이세요.
- items(evaluationData.data.grammar.grammar) { grammarError -> + items( + items = evaluationData.data.grammar.grammar, + key = { it.originalSentence.hashCode() xor it.correctedSentence.hashCode() } + ) { grammarError -> GrammarErrorCard(grammarError = grammarError) }
91-97: 고정 maxHeight(480.dp)는 기기/글꼴 크기에 취약화면 높이 비율 기반으로 동적으로 제한하는 방식을 고려하세요. 예: LocalConfiguration.screenHeightDp.dp * 0.6f 등.
app/src/main/java/com/malharang/app/data/mapper/todomain/EvaluationResponseMapper.kt (1)
37-43: Grammar 리스트 null‑safe 매핑 권장서버가 빈/누락 리스트를 반환해도 안전하게 동작하도록 가드하세요.
fun GrammarDto.toDomain(): GrammarData { return GrammarData( comment = this.comment, - grammar = this.grammar.map { it.toDomain() }, + grammar = this.grammar?.map { it.toDomain() }.orEmpty(), pass = this.pass ) }app/src/main/java/com/malharang/app/presentation/screen/chat/ChatViewModel.kt (2)
483-489: 에러와 평가 실패를 동일한 NOT_PASS로 표시네트워크/서버 오류를 FAIL과 동일하게 표기하면 UX 혼선이 있습니다. Error 전용 상태 도입을 고려하세요(예: EvaluationState.Error(message)).
466-478: JSON 직렬화가 Presentation 레이어에 위치직렬화/영속 책임은 data 레이어(Mapper/Repository)로 이동하는 것이 적절합니다. ViewModel에서 Json.encodeToString을 호출하지 않도록 리팩터링하세요.
app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt (1)
29-35: grammarErrors를 문자열로 저장 — 컨버터 또는 별도 엔티티로 타입 안전성 확보 권장문자열 JSON은 타입 안전성과 쿼리 확장성에 한계가 있습니다. Room TypeConverter나 별도 테이블로 정규화하는 방식을 고려하세요.
TypeConverter 예시(별도 파일):
// app/src/main/java/com/malharang/app/data/local/converter/GrammarErrorConverters.kt package com.malharang.app.data.local.converter import androidx.room.TypeConverter import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import com.malharang.app.domain.model.GrammarErrorData object GrammarErrorConverters { @TypeConverter @JvmStatic fun fromList(list: List<GrammarErrorData>?): String? = list?.let { Json.encodeToString(it) } @TypeConverter @JvmStatic fun toList(json: String?): List<GrammarErrorData>? = json?.let { Json.decodeFromString(it) } }AppDatabase에 @TypeConverters 등록 후:
- Entity 필드 타입을 List? 로 변경
- ViewModel의 수동 직렬화 코드를 제거
app/src/main/java/com/malharang/app/domain/model/EvaluationData.kt (2)
25-29: ContextualityData.contextuality 필드 사용처 불명확현재 UI/매퍼에서 미사용이며 Mapper는 빈 리스트로 투영 중입니다. 불필요하면 제거해 도메인 단순화하거나, 사용할 계획이라면 매핑을 완성하세요.
25-35: Boolean 필드 명명 개선 제안(nitpick)pass 보다 passed가 의미가 더 명확합니다(과거분사 형태). 외부 API 스키마와 다르면 유지해도 무방하나, 내부 도메인에서는 passed로 통일하는 방안을 검토하세요.
Also applies to: 37-43
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (27)
.github/PULL_REQUEST_TEMPLATE.md(1 hunks)app/src/main/java/com/malharang/app/core/designsystem/theme/Color.kt(3 hunks)app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt(3 hunks)app/src/main/java/com/malharang/app/data/local/AppDatabase.kt(1 hunks)app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt(1 hunks)app/src/main/java/com/malharang/app/data/mapper/todomain/EvaluationResponseMapper.kt(1 hunks)app/src/main/java/com/malharang/app/data/mapper/todomain/MessageEntityMapper.kt(1 hunks)app/src/main/java/com/malharang/app/data/mapper/todto/EvaluationRequestMapper.kt(1 hunks)app/src/main/java/com/malharang/app/data/mapper/toentity/MessageDataMapper.kt(1 hunks)app/src/main/java/com/malharang/app/data/repositoryimpl/EvaluationRepositoryImpl.kt(1 hunks)app/src/main/java/com/malharang/app/data/repositoryimpl/MessageRepositoryImpl.kt(1 hunks)app/src/main/java/com/malharang/app/di/RepositoryModule.kt(3 hunks)app/src/main/java/com/malharang/app/domain/mapper/ChatMessageDataMapper.kt(2 hunks)app/src/main/java/com/malharang/app/domain/model/ConversationData.kt(1 hunks)app/src/main/java/com/malharang/app/domain/model/EvaluationData.kt(1 hunks)app/src/main/java/com/malharang/app/domain/repository/EvaluationRepository.kt(1 hunks)app/src/main/java/com/malharang/app/domain/repository/MessageRepository.kt(1 hunks)app/src/main/java/com/malharang/app/domain/usecase/EvaluationUseCase.kt(1 hunks)app/src/main/java/com/malharang/app/domain/usecase/MessageUseCase.kt(1 hunks)app/src/main/java/com/malharang/app/presentation/model/ChatMessageModel.kt(1 hunks)app/src/main/java/com/malharang/app/presentation/screen/chat/ChatScreen.kt(5 hunks)app/src/main/java/com/malharang/app/presentation/screen/chat/ChatViewModel.kt(6 hunks)app/src/main/java/com/malharang/app/presentation/screen/chat/component/ChatBubble.kt(4 hunks)app/src/main/java/com/malharang/app/presentation/screen/chat/component/EvaluationDetailDialog.kt(1 hunks)app/src/main/java/com/malharang/app/presentation/screen/chat/type/EvaluationState.kt(1 hunks)app/src/main/res/drawable/ic_caution_14.xml(1 hunks)app/src/main/res/drawable/ic_check_22.xml(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/malharang/app/presentation/screen/chat/ChatScreen.kt (1)
app/src/main/java/com/malharang/app/presentation/screen/chat/component/EvaluationDetailDialog.kt (1)
EvaluationDetailDialog(44-150)
app/src/main/java/com/malharang/app/presentation/screen/chat/component/EvaluationDetailDialog.kt (1)
app/src/main/java/com/malharang/app/core/designsystem/theme/Theme.kt (1)
MalHaRangTheme(37-58)
🪛 markdownlint-cli2 (0.17.2)
.github/PULL_REQUEST_TEMPLATE.md
8-8: Images should have alternate text (alt text)
(MD045, no-alt-text)
⏰ 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: CI
🔇 Additional comments (20)
app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt (1)
44-45: 마이그레이션 등록 OK — 테스트 추가 권장
addMigrations(MIGRATION_1_2)등록은 적절합니다. 1→2 마이그레이션이 실제 단말에서도 안전한지MigrationTestHelper기반 테스트 추가를 권장합니다.app/src/main/java/com/malharang/app/core/designsystem/theme/Color.kt (3)
31-32: SecondaryRed 추가 LGTM새 색상 상수 추가는 명확합니다.
88-90: 기본 팔레트에 포함 OK
defaultMalHaRangColors에의 반영 문제 없습니다.
64-66: MalHaRangColors 생성자 변경 — 직접 생성 호출부 존재 여부 확인 필요데이터 클래스 생성자에 파라미터가 추가되어 MalHaRangColors(...)로 직접 생성하는 호출부가 있으면 컴파일 오류가 납니다. 전역 기본 인스턴스만 사용하는지 확인하세요.
로컬에서 사용처 검색(환경에 따라 하나씩 실행):
rg -n --hidden --no-ignore -S 'MalHaRangColors\s*\(' -g '!**/build/**' git grep -n -E 'MalHaRangColors\s*\(' || true grep -RIn --line-number -P 'MalHaRangColors\s*\(' . || trueapp/src/main/java/com/malharang/app/data/local/AppDatabase.kt (1)
14-15: Room 버전 2 반영 OK — 런타임 빌더에서도 적용 확인DB 버전 업은 적절합니다. 모든 빌드 변형에서
DatabaseModule의addMigrations(MIGRATION_1_2)가 누락되지 않았는지 확인 부탁드립니다.app/src/main/java/com/malharang/app/domain/usecase/MessageUseCase.kt (1)
33-39: 메시지 업데이트 UseCase 구현이 적절합니다새로 추가된 UpdateMessageUseCase는 평가 기능을 위한 메시지 업데이트를 위해 필요한 구현입니다. Repository 패턴과 Clean Architecture를 잘 따르고 있으며, 기존 UseCase들과 일관성 있는 구조를 유지하고 있습니다.
app/src/main/java/com/malharang/app/presentation/screen/chat/type/EvaluationState.kt (1)
7-14: 평가 상태 enum 정의가 적절하게 구현되었습니다각 평가 상태에 맞는 아이콘 리소스가 적절하게 매핑되어 있습니다. EMPTY와 Loading 상태에 대해 0을 사용한 것도 적절한 선택입니다. 네이밍 컨벤션도 명확하고 직관적입니다.
app/src/main/java/com/malharang/app/domain/repository/EvaluationRepository.kt (1)
6-8: 평가 Repository 인터페이스 정의가 적절합니다Clean Architecture의 Domain Layer에서 Repository 인터페이스를 정의하고, 적절한 Result 타입을 사용하여 에러 핸들링을 고려한 설계입니다. suspend 함수로 정의하여 비동기 처리도 고려되었습니다.
app/src/main/java/com/malharang/app/data/mapper/todomain/MessageEntityMapper.kt (1)
11-15: 새로운 평가 필드로의 매핑 변경이 적절합니다기존의 passed, commentContextuality, commentLexicalVariety 필드에서 새로운 평가 필드 구조(contextualityPassed, contextualityComment, grammarPassed, grammarComment, grammarErrors)로 변경된 매핑이 올바르게 구현되었습니다. 데이터베이스 스키마 변경과 일치하는 구조입니다.
app/src/main/java/com/malharang/app/data/mapper/todto/EvaluationRequestMapper.kt (2)
8-12: 평가 요청 DTO 매핑 함수가 올바르게 구현되었습니다EvaluationRequestData를 EvaluationRequestDto로 변환하는 매핑 로직이 간결하고 명확하게 구현되었습니다. messages 필드의 각 MessageData를 toDto()로 변환하는 방식도 적절합니다.
14-19: 메시지 DTO 매핑 함수 구현이 적절합니다MessageData에서 MessageDto로의 변환에서 필요한 필드(role, content)만 추출하여 API 요청에 맞는 구조로 매핑한 것이 올바른 접근입니다.
app/src/main/java/com/malharang/app/domain/usecase/EvaluationUseCase.kt (1)
8-16: 평가 UseCase 구현이 Clean Architecture 원칙을 잘 따릅니다단일 책임 원칙을 준수하며 Repository를 통해 평가 API를 호출하는 구조가 적절합니다. Result 타입을 사용하여 성공/실패 케이스를 명확하게 처리할 수 있도록 설계되었습니다.
app/src/main/java/com/malharang/app/data/mapper/toentity/MessageDataMapper.kt (1)
11-15: 엔티티 매핑 업데이트가 데이터베이스 스키마와 일치합니다새로운 평가 필드 구조로 업데이트된 매핑이 올바르게 구현되었습니다. 대응하는 todomain 매퍼와 일관성을 유지하고 있어 데이터 변환 과정에서 문제가 없을 것으로 예상됩니다.
app/src/main/java/com/malharang/app/presentation/screen/chat/ChatScreen.kt (1)
239-244: 평가 다이얼로그 연동 흐름은 자연스럽고 일관적입니다.빈 상태 기본값 처리(
?: EvaluationState.EMPTY)와 다이얼로그 표시/해제 상태 관리가 깔끔합니다.Also applies to: 276-282
app/src/main/java/com/malharang/app/presentation/screen/chat/component/ChatBubble.kt (1)
50-58: 새 파라미터/아이콘 통합은 자연스럽습니다.USER 메시지에만 평가 아이콘을 배치한 결정과 기본값 처리 흐름이 명확합니다. 미세한 UI/UX 조정은 별도 코멘트 참고.
Also applies to: 81-93, 230-232
app/src/main/java/com/malharang/app/data/repositoryimpl/EvaluationRepositoryImpl.kt (1)
14-16: DI 바인딩 확인됨 — 조치 불필요RepositoryModule에서 EvaluationRepository → EvaluationRepositoryImpl 바인딩 확인: app/src/main/java/com/malharang/app/di/RepositoryModule.kt (bindEvaluationRepository, ≈lines 78–80).
DataSourceModule에서 EvaluationRemoteDataSource → EvaluationRemoteDataSourceImpl 바인딩 확인: app/src/main/java/com/malharang/app/di/DataSourceModule.kt (bindsEvaluationDataSource, line 42).
EvaluationRepositoryImpl.postEvaluation은 runCatching 패턴으로 적절합니다.app/src/main/java/com/malharang/app/di/RepositoryModule.kt (4)
5-5: 올바른 import 추가EvaluationRepositoryImpl 구현체에 대한 import가 정확히 추가되었습니다.
15-15: 올바른 domain interface import 추가EvaluationRepository 도메인 인터페이스에 대한 import가 정확히 추가되었습니다.
76-80: Dependency Injection 바인딩 설정 완료EvaluationRepository와 그 구현체 간의 DI 바인딩이 올바르게 설정되었습니다. 다른 repository 바인딩들과 일관된 패턴을 따르고 있어 좋습니다.
76-80: 확인: EvaluationRepository·EvaluationRepositoryImpl 존재 및 바인딩 일치EvaluationRepository(도메인)와 EvaluationRepositoryImpl(데이터), EvaluationUseCase에서의 사용이 확인되었고, 구현체는 @Inject 생성자를 갖추어 RepositoryModule의 @BINDS와 일치합니다.
| private val MIGRATION_1_2 = object : Migration(1, 2) { | ||
| override fun migrate(database: SupportSQLiteDatabase) { | ||
| database.execSQL("ALTER TABLE message DROP COLUMN passed") | ||
| database.execSQL("ALTER TABLE message DROP COLUMN commentContextuality") | ||
| database.execSQL("ALTER TABLE message DROP COLUMN commentLexicalVariety") | ||
| database.execSQL("ALTER TABLE message ADD COLUMN contextualityPassed INTEGER") | ||
| database.execSQL("ALTER TABLE message ADD COLUMN contextualityComment TEXT") | ||
| database.execSQL("ALTER TABLE message ADD COLUMN grammarPassed INTEGER") | ||
| database.execSQL("ALTER TABLE message ADD COLUMN grammarComment TEXT") | ||
| database.execSQL("ALTER TABLE message ADD COLUMN grammarErrors TEXT") | ||
| } | ||
| } |
There was a problem hiding this comment.
SQLite DROP COLUMN 사용은 다수 기기에서 실패 위험 — 재생성(copy) 방식으로 마이그레이션 필요
Android 내장 SQLite 3.35 미만에서는 ALTER TABLE ... DROP COLUMN을 지원하지 않아 마이그레이션 시 크래시가 발생할 수 있습니다. 새로운 테이블 생성 → 데이터 복사 → 교체 패턴으로 전환을 권장합니다. 스켈레톤 예시:
- database.execSQL("ALTER TABLE message DROP COLUMN passed")
- database.execSQL("ALTER TABLE message DROP COLUMN commentContextuality")
- database.execSQL("ALTER TABLE message DROP COLUMN commentLexicalVariety")
- database.execSQL("ALTER TABLE message ADD COLUMN contextualityPassed INTEGER")
- database.execSQL("ALTER TABLE message ADD COLUMN contextualityComment TEXT")
- database.execSQL("ALTER TABLE message ADD COLUMN grammarPassed INTEGER")
- database.execSQL("ALTER TABLE message ADD COLUMN grammarComment TEXT")
- database.execSQL("ALTER TABLE message ADD COLUMN grammarErrors TEXT")
+ // 1) 새 테이블 생성 (MessageEntity 실제 스키마로 대체 필요)
+ database.execSQL(
+ """
+ CREATE TABLE message_new (
+ /* TODO: 실제 컬럼 정의로 대체 */
+ id INTEGER PRIMARY KEY NOT NULL,
+ conversationId INTEGER NOT NULL,
+ content TEXT,
+ createdAt INTEGER,
+ contextualityPassed INTEGER,
+ contextualityComment TEXT,
+ grammarPassed INTEGER,
+ grammarComment TEXT,
+ grammarErrors TEXT
+ )
+ """.trimIndent()
+ )
+ // 2) 기존 데이터 복사 (삭제되는 컬럼 제외)
+ database.execSQL(
+ """
+ INSERT INTO message_new (
+ id, conversationId, content, createdAt,
+ contextualityPassed, contextualityComment, grammarPassed, grammarComment, grammarErrors
+ )
+ SELECT
+ id, conversationId, content, createdAt,
+ NULL, NULL, NULL, NULL, NULL
+ FROM message
+ """.trimIndent()
+ )
+ // 3) 교체
+ database.execSQL("DROP TABLE message")
+ database.execSQL("ALTER TABLE message_new RENAME TO message")필요 시 인덱스/제약조건 재생성도 포함해주세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private val MIGRATION_1_2 = object : Migration(1, 2) { | |
| override fun migrate(database: SupportSQLiteDatabase) { | |
| database.execSQL("ALTER TABLE message DROP COLUMN passed") | |
| database.execSQL("ALTER TABLE message DROP COLUMN commentContextuality") | |
| database.execSQL("ALTER TABLE message DROP COLUMN commentLexicalVariety") | |
| database.execSQL("ALTER TABLE message ADD COLUMN contextualityPassed INTEGER") | |
| database.execSQL("ALTER TABLE message ADD COLUMN contextualityComment TEXT") | |
| database.execSQL("ALTER TABLE message ADD COLUMN grammarPassed INTEGER") | |
| database.execSQL("ALTER TABLE message ADD COLUMN grammarComment TEXT") | |
| database.execSQL("ALTER TABLE message ADD COLUMN grammarErrors TEXT") | |
| } | |
| } | |
| private val MIGRATION_1_2 = object : Migration(1, 2) { | |
| override fun migrate(database: SupportSQLiteDatabase) { | |
| // 1) 새 테이블 생성 (MessageEntity 실제 스키마로 대체 필요) | |
| database.execSQL( | |
| """ | |
| CREATE TABLE message_new ( | |
| /* TODO: 실제 컬럼 정의로 대체 */ | |
| id INTEGER PRIMARY KEY NOT NULL, | |
| conversationId INTEGER NOT NULL, | |
| content TEXT, | |
| createdAt INTEGER, | |
| contextualityPassed INTEGER, | |
| contextualityComment TEXT, | |
| grammarPassed INTEGER, | |
| grammarComment TEXT, | |
| grammarErrors TEXT | |
| ) | |
| """.trimIndent() | |
| ) | |
| // 2) 기존 데이터 복사 (삭제되는 컬럼 제외) | |
| database.execSQL( | |
| """ | |
| INSERT INTO message_new ( | |
| id, conversationId, content, createdAt, | |
| contextualityPassed, contextualityComment, grammarPassed, grammarComment, grammarErrors | |
| ) | |
| SELECT | |
| id, conversationId, content, createdAt, | |
| NULL, NULL, NULL, NULL, NULL | |
| FROM message | |
| """.trimIndent() | |
| ) | |
| // 3) 교체 | |
| database.execSQL("DROP TABLE message") | |
| database.execSQL("ALTER TABLE message_new RENAME TO message") | |
| } | |
| } |
🤖 Prompt for AI Agents
In app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt around
lines 22–33, the migration currently uses "ALTER TABLE ... DROP COLUMN" which
can crash on older SQLite versions; replace it with the safe copy-recreate
pattern: create a new table with the intended final schema (include all new
columns contextualityPassed INTEGER, contextualityComment TEXT, grammarPassed
INTEGER, grammarComment TEXT, grammarErrors TEXT and any original columns you
want to keep), copy data from the old message table into the new table using
INSERT INTO new_table(columns...) SELECT mapped_columns... FROM message (map or
set sensible defaults/nulls for new columns and map any old columns like
passed/commentContextuality/commentLexicalVariety to the new columns if
appropriate), drop the old table, then rename the new table to message, and
finally recreate any indexes/constraints that existed; implement these SQL
execSQL calls inside the migrate() body instead of DROP COLUMN.
| database.execSQL("ALTER TABLE message ADD COLUMN contextualityPassed INTEGER") | ||
| database.execSQL("ALTER TABLE message ADD COLUMN contextualityComment TEXT") | ||
| database.execSQL("ALTER TABLE message ADD COLUMN grammarPassed INTEGER") | ||
| database.execSQL("ALTER TABLE message ADD COLUMN grammarComment TEXT") | ||
| database.execSQL("ALTER TABLE message ADD COLUMN grammarErrors TEXT") |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
새 컬럼의 NULL/DEFAULT 제약 확인 필요
엔티티에서 해당 필드가 non-null 타입이면 마이그레이션 시 NOT NULL DEFAULT가 필요합니다. 예: INTEGER NOT NULL DEFAULT 0, TEXT NOT NULL DEFAULT '' 등. 엔티티 nullability와 EXACT 매핑 여부를 확인해주세요.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== 1) Find ALTER TABLE migration lines ==="
rg -n --hidden --no-ignore -S "ALTER TABLE message ADD COLUMN" || true
echo
echo "=== 2) Search for the new field names in repo ==="
rg -n --hidden --no-ignore -S "contextualityPassed|contextualityComment|grammarPassed|grammarComment|grammarErrors" -g '!**/build/**' || true
echo
echo "=== 3) Show DatabaseModule.kt if present ==="
if [ -f app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt ]; then
sed -n '1,240p' app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt || true
else
rg -n --hidden --no-ignore -S "DatabaseModule.kt" || true
fi
echo
echo "=== 4) Find Message entity / class definitions ==="
mapfile -t msgs < <(rg -n --hidden --no-ignore -S "@Entity|data class Message|class Message" -l || true)
if [ "${#msgs[@]}" -eq 0 ]; then
echo "No files with @Entity/data class Message/class Message found."
else
for f in "${msgs[@]}"; do
echo "---- $f ----"
sed -n '1,240p' "$f" || true
done
fi
echo
echo "=== 5) Show files that contain the specific new fields ==="
mapfile -t files < <(rg -n --hidden --no-ignore -S "contextualityPassed|contextualityComment|grammarPassed|grammarComment|grammarErrors" -g '!**/build/**' -l || true)
if [ "${#files[@]}" -eq 0 ]; then
echo "No files contain those field names."
else
for f in "${files[@]}"; do
echo "---- $f ----"
sed -n '1,240p' "$f" || true
done
fiLength of output: 3363
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== 1) Locate files mentioning Message entity or @Database ==="
rg -n --hidden --no-ignore -S 'data class Message|class Message\b|@Entity\b|@Database\b|abstract class AppDatabase|interface AppDatabase' || true
echo
echo "=== 2) List files that match ==="
rg --hidden --no-ignore -l -S 'data class Message|class Message\b|@Entity\b|@Database\b|abstract class AppDatabase|interface AppDatabase' || true
echo
echo "=== 3) Print matched files (first 300 lines each) ==="
for f in $(rg --hidden --no-ignore -l -S 'data class Message|class Message\b|@Entity\b|@Database\b|abstract class AppDatabase|interface AppDatabase' || true); do
echo "---- $f ----"
sed -n '1,300p' "$f" || true
done
echo
echo "=== 4) Search specifically for the new fields ==="
rg -n --hidden --no-ignore -S 'contextualityPassed|contextualityComment|grammarPassed|grammarComment|grammarErrors' || true
echo
echo "=== 5) If Message entity file found, show full file (safe fallback) ==="
MSG_FILE=$(rg --hidden --no-ignore -l -S 'data class Message|class Message\b|@Entity\b' | head -n1 || true)
if [ -n "$MSG_FILE" ]; then
echo "Showing $MSG_FILE"
sed -n '1,500p' "$MSG_FILE" || true
else
echo "No Message entity file found by patterns."
fiLength of output: 11275
새 컬럼 NULL/DEFAULT 확인 완료 — DROP COLUMN은 재검토 필요
- 확인: app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt 에서 contextualityPassed/ contextualityComment/ grammarPassed/ grammarComment/ grammarErrors 가 모두 nullable(Boolean? / String?)로 선언되어 있어 ALTER TABLE ... ADD COLUMN에 NOT NULL DEFAULT를 추가할 필요 없음.
- 중요: app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt 의 MIGRATION_1_2 블록에서 ALTER TABLE ... DROP COLUMN을 사용하고 있음. 일부 Android/SQLite 환경에서 DROP COLUMN이 지원되지 않아 마이그레이션 실패 가능 — 테이블 재생성(copy → insert → rename) 방식으로 대체하거나 타겟 런타임의 SQLite 버전에서 DROP COLUMN 지원을 확실히 보장할 것.
🤖 Prompt for AI Agents
In app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt around
lines 27 to 31, the migration adds nullable columns which is fine (no NOT NULL
DEFAULT needed because the entity fields are nullable), but the MIGRATION_1_2
block currently uses ALTER TABLE ... DROP COLUMN which may not be supported on
all Android/SQLite runtimes; replace the DROP COLUMN approach with a safe
table-recreation migration: create a new temporary table with the desired schema
(including the new nullable columns), copy data from the old table into the temp
table mapping columns, drop the old table, and rename the temp table to the
original name (ensuring indices and constraints are re-created), or
alternatively guard the migration to run only on SQLite versions that guarantee
DROP COLUMN support.
| val grammarErrors = msg.grammarErrors?.let { json -> | ||
| kotlinx.serialization.json.Json.decodeFromString<List<com.malharang.app.domain.model.GrammarErrorData>>(json) | ||
| } ?: emptyList() | ||
|
|
||
| com.malharang.app.domain.model.EvaluationResponseData( |
There was a problem hiding this comment.
kotlinx.serialization 사용 시 import 누락으로 컴파일 실패 가능.
Json.decodeFromString 확장함수는 별도 import가 필요합니다. 또한 기본 Json은 ignoreUnknownKeys=false라 파싱 실패가 빈번할 수 있습니다.
최소 수정(컴파일러 에러 방지):
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import com.malharang.app.domain.model.GrammarErrorData파싱 내구성 개선(권장):
- val grammarErrors = msg.grammarErrors?.let { json ->
- kotlinx.serialization.json.Json.decodeFromString<List<com.malharang.app.domain.model.GrammarErrorData>>(json)
+ val grammarErrors = msg.grammarErrors?.let { raw ->
+ JSON.decodeFromString<List<GrammarErrorData>>(raw)
} ?: emptyList()파일 상단(또는 companion)에 재사용 가능한 인스턴스 추가:
// 파일 상단
private val JSON = Json { ignoreUnknownKeys = true }🤖 Prompt for AI Agents
In app/src/main/java/com/malharang/app/domain/mapper/ChatMessageDataMapper.kt
around lines 27 to 31, the call to
kotlinx.serialization.json.Json.decodeFromString may not compile because the
extension function import is missing and using the default Json instance is
brittle; add the import kotlinx.serialization.decodeFromString at the top (or
fully-qualify the call) and create a reusable Json instance with
ignoreUnknownKeys = true (e.g., private val JSON = Json { ignoreUnknownKeys =
true }) then replace Json.decodeFromString(...) with JSON.decodeFromString(...)
so parsing is resilient and the code compiles.
| suspend fun insertMessage(message: MessageEntity): Long | ||
| suspend fun getMessagesByConversationId(conversationId: Long): List<MessageEntity> | ||
| suspend fun deleteMessagesByConversationId(conversationId: Long) | ||
| suspend fun updateMessage(message: MessageEntity): Long |
There was a problem hiding this comment.
updateMessage의 의미/시그니처 불일치
업데이트 연산은 보통 영향 행 수(Int)를 반환합니다. 현재 Long 반환은 insert 계열 의미에 가깝습니다. 다음 중 하나로 정렬을 제안합니다.
- suspend fun updateMessage(message: MessageEntity): Long
+ suspend fun updateMessage(message: MessageEntity): Int또는 업서트 의도라면 메서드명을 upsertMessage로 변경하고 DAO도 @Upsert/@insert(REPLACE)와 일치시키세요.
📝 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.
| suspend fun updateMessage(message: MessageEntity): Long | |
| suspend fun updateMessage(message: MessageEntity): Int |
🤖 Prompt for AI Agents
In app/src/main/java/com/malharang/app/domain/repository/MessageRepository.kt
around line 9, the method signature suspend fun updateMessage(message:
MessageEntity): Long is inconsistent: update operations should return
affected-row count (Int) while Long suggests insert/upsert behavior. Fix by
either (A) changing the return type to Int and keeping the name updateMessage,
and update any callers/DAO implementation to return Int from the update
operation; or (B) if the intent is upsert, rename the method to upsertMessage
and ensure the DAO uses @Upsert or @Insert(onConflict = REPLACE) and returns
Long (or appropriate id), updating callers accordingly.
| val lastUserMessageIndex = _state.value.chatList.indexOfLast { it.sender == SenderType.USER } | ||
| if (lastUserMessageIndex == -1) return@launch | ||
|
|
||
| // 마지막 사용자 메시지에 Loading 상태 설정 | ||
| updateChatMessageAt(lastUserMessageIndex) { | ||
| it.copy(evaluationState = EvaluationState.Loading) | ||
| } |
There was a problem hiding this comment.
UI 업데이트가 고정 인덱스에 의존 — 동시 입력 시 잘못된 메시지를 갱신할 수 있음
평가 요청 중에 사용자가 메시지를 더 보내면 인덱스가 밀려 타 메시지를 갱신할 수 있습니다. 타겟 텍스트를 캡처해 재검색하거나(임시), 궁극적으로는 메시지 ID 기반으로 갱신하세요.
// 마지막 사용자 메시지의 인덱스 찾기
val lastUserMessageIndex = _state.value.chatList.indexOfLast { it.sender == SenderType.USER }
if (lastUserMessageIndex == -1) return@launch
+ // 인덱스 변동 대비: 타겟 텍스트 캡처
+ val targetUserText = _state.value.chatList[lastUserMessageIndex].text
@@
- // 마지막 사용자 메시지에 평가 결과와 데이터 설정
- updateChatMessageAt(lastUserMessageIndex) {
+ // 인덱스 변동 시 재탐색
+ val idx = _state.value.chatList.indexOfLast { it.sender == SenderType.USER && it.text == targetUserText }
+ if (idx != -1) updateChatMessageAt(idx) {
it.copy(
evaluationState = evaluationState,
evaluationData = response
)
- }
+ }
@@
- // 마지막 사용자 메시지에 실패 상태 설정
- updateChatMessageAt(lastUserMessageIndex) {
+ val idxFail = _state.value.chatList.indexOfLast { it.sender == SenderType.USER && it.text == targetUserText }
+ if (idxFail != -1) updateChatMessageAt(idxFail) {
it.copy(evaluationState = EvaluationState.NOT_PASS)
- }
+ }권장: ChatMessageModel에 messageId를 포함하고 ID로 갱신하는 형태로의 이관을 추후 진행하세요.
Also applies to: 452-457, 485-487
🤖 Prompt for AI Agents
In app/src/main/java/com/malharang/app/presentation/screen/chat/ChatViewModel.kt
around lines 433-439 (also applies to 452-457 and 485-487): the code currently
finds the last user message by list index then updates it, which races when new
messages arrive and shifts indexes; change the logic to locate and update
messages by a stable identifier instead of an index — add a messageId field to
ChatMessageModel (if not already present), ensure each message is created with a
unique id, and replace updateChatMessageAt(index) calls with an
updateChatMessageById(messageId) that finds the current index by id and applies
the mutation; as a short-term mitigation (until messageId is available), capture
the target message object (or its unique text) before launching the coroutine
and use that captured identifier to re-find the correct message in the latest
state before updating.
| val userMessageFromDb = allMessages.findLast { it.role == "user" && it.content == lastUserMessage.text } | ||
|
|
||
| userMessageFromDb?.let { dbMessage -> | ||
| val grammarErrorsJson = Json.encodeToString(response.data.grammar.grammar) | ||
|
|
||
| val updatedMessage = MessageData( | ||
| id = dbMessage.id, | ||
| conversationId = dbMessage.conversationId, | ||
| role = dbMessage.role, | ||
| content = dbMessage.content, | ||
| contextualityPassed = response.data.contextuality.pass, |
There was a problem hiding this comment.
DB 업데이트 타겟팅이 텍스트 일치에 의존 — 동일 텍스트 다건 시 오갱신 위험
DB 측에서는 최신 사용자 메시지를 기준으로 ID를 사용해 갱신하는 편이 안전합니다. 최소한 텍스트 비교는 제거하세요.
- val userMessageFromDb = allMessages.findLast { it.role == "user" && it.content == lastUserMessage.text }
+ val userMessageFromDb = allMessages.lastOrNull { it.role == "user" }
@@
- updateMessageUseCase(updatedMessage)
+ updateMessageUseCase(updatedMessage)추가 권장: insert 시점의 반환 ID를 UI 상태에 보관하고 그 ID로 직접 매핑하는 구조로 개선.
Also applies to: 480-481
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt (1)
23-24: 마이그레이션에 CREATE INDEX 추가 필요 — @ColumnInfo(index=true)는 기존 DB에 자동 반영되지 않음@ColumnInfo(index = true)는 스키마 재생성 시에만 반영됩니다. 현재 app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt의 MIGRATION_1_2·MIGRATION_2_3에서는 CREATE INDEX가 없고 ALTER TABLE만 수행되어 기존 사용자 DB에 인덱스가 생성되지 않습니다.
조치(권장): 적절한 Migration의 migrate(...)에 다음을 추가하세요.
database.execSQL("CREATE INDEX IF NOT EXISTS index_message_conversation_id ON message(conversation_id)");위치 참조: app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt (conversation_id에 @ColumnInfo(name = "conversation_id", index = true) 설정).
♻️ Duplicate comments (2)
app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt (1)
22-33: SQLite DROP COLUMN은 안드로이드 다수 기기에서 실패 — 안전한 재생성(copy) 마이그레이션으로 교체 필요Android 내장 SQLite(3.35 미만)에서
ALTER TABLE ... DROP COLUMN미지원으로 런타임 크래시 위험이 큽니다. 메시지 테이블을 새로 생성→데이터 복사→교체 패턴으로 바꾸세요. 아래는 패턴 예시이며, 스키마는 반드시 MessageEntity와 “완전히 동일”해야 합니다(컬럼, 타입, NOT NULL, PK/Index 포함):- database.execSQL("ALTER TABLE message DROP COLUMN passed") - database.execSQL("ALTER TABLE message DROP COLUMN commentContextuality") - database.execSQL("ALTER TABLE message DROP COLUMN commentLexicalVariety") - database.execSQL("ALTER TABLE message ADD COLUMN contextualityPassed INTEGER") - database.execSQL("ALTER TABLE message ADD COLUMN contextualityComment TEXT") - database.execSQL("ALTER TABLE message ADD COLUMN grammarPassed INTEGER") - database.execSQL("ALTER TABLE message ADD COLUMN grammarComment TEXT") - database.execSQL("ALTER TABLE message ADD COLUMN grammarErrors TEXT") + // 1) MessageEntity와 동일한 최종 스키마로 신규 테이블 생성 + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS message_new ( + -- TODO: MessageEntity.kt와 동일하게 교체 (예시) + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + conversationId INTEGER NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + contextualityPassed INTEGER, + contextualityComment TEXT, + grammarPassed INTEGER, + grammarComment TEXT, + grammarErrors TEXT + ) + """.trimIndent() + ) + // 2) 기존 데이터 복사 (삭제되는 컬럼 제외, 신규 컬럼은 NULL/기본값) + database.execSQL( + """ + INSERT INTO message_new ( + id, conversationId, role, content, + contextualityPassed, contextualityComment, grammarPassed, grammarComment, grammarErrors + ) + SELECT + id, conversationId, role, content, + NULL, NULL, NULL, NULL, NULL + FROM message + """.trimIndent() + ) + // 3) 교체 + database.execSQL("DROP TABLE message") + database.execSQL("ALTER TABLE message_new RENAME TO message") + // 4) 인덱스/제약조건 재생성 (필요 시) + // database.execSQL("CREATE INDEX IF NOT EXISTS idx_message_conversationId ON message(conversationId)")추가로, 새로 추가한 컬럼이 엔티티에서 nullable이라면 DEFAULT/NOT NULL 지정은 불필요합니다. non-null이라면
NOT NULL DEFAULT를 지정하세요.다음 스크립트로 엔티티 스키마와 마이그레이션 사용 컬럼을 빠르게 확인하세요:
#!/bin/bash set -euo pipefail echo "=== MessageEntity 확인 ===" fd -a "MessageEntity.kt" | xargs -I{} sh -c 'echo "--- {} ---"; sed -n "1,240p" {}' echo echo "=== DROP COLUMN 사용 여부 ===" rg -n 'ALTER TABLE\s+message\s+DROP COLUMN' app/src/main/java || trueapp/src/main/java/com/malharang/app/presentation/screen/chat/ChatViewModel.kt (1)
430-493: 인덱스 기반 UI 갱신은 레이스 위험 — 타겟 캡처 후 재탐색(임시) + DB 갱신은 텍스트 일치 제거동시 입력/도착 시 리스트 인덱스가 변해 다른 메시지를 갱신할 수 있습니다. 또한 DB 갱신 대상을 텍스트로 찾으면 동문장 중복 시 오갱신됩니다. 단기 대응으로 “타겟 사용자 텍스트”를 캡처해 재탐색하고, DB는 최신 USER 1건으로 한정하세요. 장기적으로는 ChatMessageModel에 messageId를 넣어 ID 기반으로 갱신하는 구조로 이관하세요.
// 마지막 사용자 메시지의 인덱스 찾기 val lastUserMessageIndex = _state.value.chatList.indexOfLast { it.sender == SenderType.USER } if (lastUserMessageIndex == -1) return@launch + // 인덱스 변동 대비: 타겟 텍스트 캡처 + val targetUserText = _state.value.chatList[lastUserMessageIndex].text @@ - // 마지막 사용자 메시지에 Loading 상태 설정 - updateChatMessageAt(lastUserMessageIndex) { + // 마지막 사용자 메시지에 Loading 상태 설정 (재탐색) + val idxLoading = _state.value.chatList.indexOfLast { it.sender == SenderType.USER && it.text == targetUserText } + if (idxLoading != -1) updateChatMessageAt(idxLoading) { it.copy(evaluationState = EvaluationState.Loading) } @@ - ).onSuccess { response -> + ).onSuccess { response -> val evaluationState = if (response.data.contextuality.pass) { EvaluationState.PASS } else { EvaluationState.NOT_PASS } - // 마지막 사용자 메시지에 평가 결과와 데이터 설정 - updateChatMessageAt(lastUserMessageIndex) { + // 마지막 사용자 메시지에 평가 결과와 데이터 설정 (재탐색) + val idx = _state.value.chatList.indexOfLast { it.sender == SenderType.USER && it.text == targetUserText } + if (idx != -1) updateChatMessageAt(idx) { it.copy( evaluationState = evaluationState, evaluationData = response ) - } + } // DB에 평가 데이터 저장 (USER 메시지만) - val lastUserMessage = _state.value.chatList[lastUserMessageIndex] + val lastUserMessage = _state.value.chatList.lastOrNull { it.sender == SenderType.USER } ?: return@onSuccess if (lastUserMessage.sender == SenderType.USER) { val allMessages = getMessageByConversationByIdUseCase(conversationId) - val userMessageFromDb = allMessages.findLast { it.role == "user" && it.content == lastUserMessage.text } + // 텍스트 일치 제거: 최신 USER 메시지 1건 + val userMessageFromDb = allMessages.lastOrNull { it.role == "user" } @@ userMessageFromDb?.let { dbMessage -> val contextualityErrorsJson = Json.encodeToString(response.data.contextuality.contextuality) val grammarErrorsJson = Json.encodeToString(response.data.grammar.grammar) @@ updateMessageUseCase(updatedMessage) } } }.onFailure { error -> - // 마지막 사용자 메시지에 실패 상태 설정 - updateChatMessageAt(lastUserMessageIndex) { + // 실패 시에도 대상 재탐색 + val idxFail = _state.value.chatList.indexOfLast { it.sender == SenderType.USER && it.text == targetUserText } + if (idxFail != -1) updateChatMessageAt(idxFail) { it.copy(evaluationState = EvaluationState.NOT_PASS) - } + } _errorMessage.value = error.message }참고: 실패를 NOT_PASS로 표시하면 “평가 실패”와 “평가 결과 불합격”이 구분되지 않습니다. 필요 시 EvaluationState.Error 추가를 검토하세요.
최소 재현: 채팅 중 빠르게 2개 이상 메시지 전송 → 평가 응답 수신 시 올바른 메시지에 상태가 붙는지 확인해 주세요.
🧹 Nitpick comments (6)
app/src/main/java/com/malharang/app/domain/model/ConversationData.kt (1)
15-20: 도메인에 JSON 문자열 필드가 직접 노출됨 — 혼동 방지용 네이밍/타이핑 개선 제안contextualityErrors/grammarErrors가 JSON 문자열임을 명시적으로 드러내거나(Entity 전용으로 이동) 도메인에서는 리스트 타입으로 유지하고 매핑 계층에서만 직렬화/역직렬화하세요. 최소한 필드명을 contextualityErrorsJson/grammarErrorsJson 등으로 바꾸면 가독성이 좋아집니다.
app/src/main/java/com/malharang/app/presentation/screen/chat/component/EvaluationDetailDialog.kt (2)
70-87: 하드코딩 문자열/접근성 개선UI 텍스트를 string 리소스로 이동하고 아이콘에 contentDescription을 제공하세요.
아래와 같이 시작해 주세요(다른 텍스트도 동일 패턴 적용):
- Text( - text = "Evaluation Result", + Text( + text = stringResource(R.string.evaluation_result), ... ) @@ - Icon( + Icon( imageVector = ImageVector.vectorResource( ... ), - contentDescription = null, + contentDescription = stringResource( + if (evaluationData.data.contextuality.pass) + R.string.evaluation_status_pass_icon + else + R.string.evaluation_status_fail_icon + ), tint = Color.Unspecified, modifier = Modifier.size(28.dp) )추가로 "Close", "Contextuality", "Grammar", "Contextuality Suggestions", "Grammar Errors", "Original", "Suggestions", "Correction" 등도 strings.xml로 이동 권장.
119-121: LazyColumn 항목에 key 지정으로 재구성 안정성 개선동일 내용 추가/삭제 시 불필요한 재조합을 줄이려면 key를 부여하세요.
- items(evaluationData.data.contextuality.contextuality) { contextualityError -> + items( + items = evaluationData.data.contextuality.contextuality, + key = { it.originalSentence } + ) { contextualityError -> ContextualityErrorCard(contextualityError = contextualityError) } @@ - items(evaluationData.data.grammar.grammar) { grammarError -> + items( + items = evaluationData.data.grammar.grammar, + key = { it.originalSentence + "->" + it.correctedSentence } + ) { grammarError -> GrammarErrorCard(grammarError = grammarError) }Also applies to: 143-145
app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt (2)
29-35: JSON String 보관 대신 타입 안전 List + TypeConverter 권장
- 문자열(JSON) 보관은 스키마/파싱 오류를 컴파일 타임에 잡지 못하고, 필드 구조 변경 시 런타임 실패로 이어질 수 있습니다.
- 아래처럼 List/List로 저장하고, TypeConverter로 직렬화/역직렬화하는 쪽을 추천합니다.
적용 diff(엔티티 필드 타입 및 컨버터 어노테이션 변경):
- val contextualityErrors: String? = null, // JSON string of contextuality errors + @androidx.room.TypeConverters(EvaluationConverters::class) + val contextualityErrors: List<com.malharang.app.domain.model.ContextualityErrorData>? = null, @@ - val grammarErrors: String? = null // JSON string of grammar errors + @androidx.room.TypeConverters(EvaluationConverters::class) + val grammarErrors: List<com.malharang.app.domain.model.GrammarErrorData>? = null컨버터 예시(새 파일, 패키지 예시는 data/local/converter):
package com.malharang.app.data.local.converter import androidx.room.TypeConverter import com.malharang.app.domain.model.ContextualityErrorData import com.malharang.app.domain.model.GrammarErrorData import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json object EvaluationConverters { private val json = Json { ignoreUnknownKeys = true } @TypeConverter @JvmStatic fun contextualityErrorsToJson(value: List<ContextualityErrorData>?): String? = value?.let { json.encodeToString(ListSerializer(ContextualityErrorData.serializer()), it) } @TypeConverter @JvmStatic fun jsonToContextualityErrors(value: String?): List<ContextualityErrorData>? = value?.let { json.decodeFromString(ListSerializer(ContextualityErrorData.serializer()), it) } @TypeConverter @JvmStatic fun grammarErrorsToJson(value: List<GrammarErrorData>?): String? = value?.let { json.encodeToString(ListSerializer(GrammarErrorData.serializer()), it) } @TypeConverter @JvmStatic fun jsonToGrammarErrors(value: String?): List<GrammarErrorData>? = value?.let { json.decodeFromString(ListSerializer(GrammarErrorData.serializer()), it) } }Room 빌더에 TypeConverters 등록을 잊지 마세요(엔티티/DB 레벨 모두 가능).
29-35: 컬럼 네이밍 일관성 제안(snake_case 통일)
- 기존 컬럼에 conversation_id(snake_case)가 존재하는 반면, 신규 평가지표는 camelCase입니다. 쿼리/마이그레이션 가독성을 위해 snake_case로 통일하는 방안을 제안합니다.
적용 diff(컬럼명만 명시):
- val contextualityPassed: Boolean? = null, + @androidx.room.ColumnInfo(name = "contextuality_passed") + val contextualityPassed: Boolean? = null, - val contextualityComment: String? = null, + @androidx.room.ColumnInfo(name = "contextuality_comment") + val contextualityComment: String? = null, - val contextualityErrors: String? = null, // JSON string of contextuality errors + @androidx.room.ColumnInfo(name = "contextuality_errors") + val contextualityErrors: String? = null, // JSON string of contextuality errors - val grammarPassed: Boolean? = null, + @androidx.room.ColumnInfo(name = "grammar_passed") + val grammarPassed: Boolean? = null, - val grammarComment: String? = null, + @androidx.room.ColumnInfo(name = "grammar_comment") + val grammarComment: String? = null, - val grammarErrors: String? = null // JSON string of grammar errors + @androidx.room.ColumnInfo(name = "grammar_errors") + val grammarErrors: String? = null // JSON string of grammar errorsapp/src/main/java/com/malharang/app/domain/model/EvaluationData.kt (1)
25-35: 불린 필드 명칭 'pass' → 'passed'로 통일(+ @SerialName으로 API 키 유지)
- 엔티티에서는 contextualityPassed/grammarPassed를 쓰고, 도메인 모델은 pass를 씁니다. 일관성 및 가독성을 위해 passed로 통일하고, 서버 키가 pass라면 @SerialName으로 호환성을 유지하세요.
적용 diff:
+import kotlinx.serialization.SerialName @@ -@Serializable -data class ContextualityData( +@Serializable +data class ContextualityData( val comment: String, val contextuality: List<ContextualityErrorData>, - val pass: Boolean + @SerialName("pass") + val passed: Boolean ) @@ -@Serializable -data class GrammarData( +@Serializable +data class GrammarData( val comment: String, val grammar: List<GrammarErrorData>, - val pass: Boolean + @SerialName("pass") + val passed: Boolean )참고: 리스트 필드 명도
contextuality/grammar→errors로 바꾸면 의미가 선명해집니다. 이 경우에도@SerialName("contextuality"),@SerialName("grammar")로 서버 키를 유지하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt(3 hunks)app/src/main/java/com/malharang/app/data/local/AppDatabase.kt(1 hunks)app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt(1 hunks)app/src/main/java/com/malharang/app/data/mapper/todomain/EvaluationResponseMapper.kt(1 hunks)app/src/main/java/com/malharang/app/data/mapper/todomain/MessageEntityMapper.kt(1 hunks)app/src/main/java/com/malharang/app/data/mapper/toentity/MessageDataMapper.kt(1 hunks)app/src/main/java/com/malharang/app/domain/mapper/ChatMessageDataMapper.kt(2 hunks)app/src/main/java/com/malharang/app/domain/model/ConversationData.kt(1 hunks)app/src/main/java/com/malharang/app/domain/model/EvaluationData.kt(1 hunks)app/src/main/java/com/malharang/app/presentation/screen/chat/ChatViewModel.kt(6 hunks)app/src/main/java/com/malharang/app/presentation/screen/chat/component/EvaluationDetailDialog.kt(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- app/src/main/java/com/malharang/app/data/mapper/todomain/EvaluationResponseMapper.kt
- app/src/main/java/com/malharang/app/data/local/AppDatabase.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/malharang/app/presentation/screen/chat/component/EvaluationDetailDialog.kt (1)
app/src/main/java/com/malharang/app/core/designsystem/theme/Theme.kt (1)
MalHaRangTheme(37-58)
🪛 detekt (1.23.8)
app/src/main/java/com/malharang/app/domain/mapper/ChatMessageDataMapper.kt
[warning] 52-52: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
⏰ 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: CI
🔇 Additional comments (10)
app/src/main/java/com/malharang/app/core/local/room/DatabaseModule.kt (2)
35-39: MIGRATION_2_3 추가는 타당1→2 마이그레이션이 안전해지면 2→3의 TEXT 컬럼 추가는 문제 없습니다(엔티티에서 nullable이면 OK).
가능하면 실제 스키마 검증을 위해 Room의 Schema export를 켜 두세요(Room Gradle 옵션).
50-51: 마이그레이션 연결 OKbuilder에 마이그레이션이 추가되어 있어 업그레이드 경로가 보장됩니다.
app/src/main/java/com/malharang/app/data/mapper/todomain/MessageEntityMapper.kt (1)
11-16: 매핑 정확 — 새 평가 필드 반영 완료엔티티→도메인 매핑이 새 필드와 nullability에 일치합니다.
app/src/main/java/com/malharang/app/data/mapper/toentity/MessageDataMapper.kt (1)
11-16: 매핑 정확 — 도메인→엔티티 필드 정합성 양호새 평가 필드가 누락 없이 매핑됩니다.
app/src/main/java/com/malharang/app/presentation/screen/chat/ChatViewModel.kt (1)
233-235: 응답 직후 평가 트리거는 합리적대화 흐름상 응답 도착 시 평가를 시작하는 위치 선정이 적절합니다.
app/src/main/java/com/malharang/app/domain/mapper/ChatMessageDataMapper.kt (1)
7-9: decodeFromString 미임포트 및 Json 인스턴스·예외 로깅 추가 필요kotlinx.serialization의 decodeFromString 확장이 누락되어 컴파일 에러가 발생하며, 기본 Json 사용은 스키마 변경에 취약하고 예외를 삼키고 있으므로 아래 변경을 적용하세요.
파일: app/src/main/java/com/malharang/app/domain/mapper/ChatMessageDataMapper.kt (7-9, 영향 범위 28-35, 52-55)
import com.malharang.app.presentation.screen.chat.type.EvaluationState -import kotlinx.serialization.json.Json +import kotlinx.serialization.json.Json +import kotlinx.serialization.decodeFromString +import com.malharang.app.domain.model.ContextualityErrorData +import com.malharang.app.domain.model.GrammarErrorData +import timber.log.Timber @@ - val contextualityErrors = msg.contextualityErrors?.let { json -> - Json.decodeFromString<List<com.malharang.app.domain.model.ContextualityErrorData>>(json) + val contextualityErrors = msg.contextualityErrors?.let { raw -> + JSON.decodeFromString<List<ContextualityErrorData>>(raw) } ?: emptyList() @@ - val grammarErrors = msg.grammarErrors?.let { json -> - Json.decodeFromString<List<com.malharang.app.domain.model.GrammarErrorData>>(json) + val grammarErrors = msg.grammarErrors?.let { raw -> + JSON.decodeFromString<List<GrammarErrorData>>(raw) } ?: emptyList() @@ - } catch (e: Exception) { - null + } catch (e: Exception) { + Timber.w(e, "Failed to decode evaluation JSON") + null }파일 상단(임포트 바로 아래)에 재사용 가능한 JSON 인스턴스 추가:
// 파일 상단(임포트 아래) private val JSON = Json { ignoreUnknownKeys = true }컴파일러/정적분석 오류 및 로깅이 기대대로 개선되는지 확인하세요.
app/src/main/java/com/malharang/app/data/local/entity/MessageEntity.kt (1)
29-35: 평가 필드 구조(6분할) 도입 좋습니다
- 도메인/엔티티 정렬이 명확해지고, nullable 기본값으로 마이그레이션 리스크가 줄었습니다. 사용자 메시지에만 값이 채워진다는 계약이 ViewModel/DAO 단에서도 유지되는지만 확인해 주세요.
app/src/main/java/com/malharang/app/domain/model/EvaluationData.kt (3)
37-41: 에러 아이템에 @serializable 지정된 점 좋습니다
- Room 보관(JSON) 및 UI 표현에서 안전하게 재사용 가능해집니다. 상위 컨테이너에도 직렬화가 필요한지 여부만 위 코멘트대로 확인해 주세요.
Also applies to: 43-49
9-12: EvaluationMessageData — 선언부 외 참조 없음, 제거 권장app/src/main/java/com/malharang/app/domain/model/EvaluationData.kt에 선언만 존재합니다. 사용처가 없다면 삭제하세요.
5-7: 검증 결과: 컨테이너 클래스에 @serializable 를 일괄 추가할 필요 없음네트워크 직렬화는 DTO(EvaluationRequestDto / EvaluationResponseDto 등)에 kotlinx.serialization을 사용하고 있으며(domain ↔ dto 매퍼로 변환), Json.encodeToString 호출은 response.data.contextuality.contextuality (List)만 직렬화하므로 요소 타입(ContextualityErrorData/GrammarErrorData)에만 @serializable이 있으면 충분합니다.
참고 위치: app/src/main/java/com/malharang/app/domain/model/EvaluationData.kt (ContextualityErrorData/GrammarErrorData에 @serializable 선언됨), app/src/main/java/com/malharang/app/presentation/screen/chat/ChatViewModel.kt (Json.encodeToString 사용: 466–467), app/src/main/java/com/malharang/app/data/remote/dto/ (네트워크용 DTO들이 @serializable).
조치: 현재 PR에서는 제안한 @serializable 추가 불필요. 단, 향후 domain 모델을 직접 Json로 직렬화하려는 경우 컨테이너 클래스(EvaluationResponseData 등)에 @serializable를 추가하세요.
Likely an incorrect or invalid review comment.
#️⃣연관된 이슈
closed #42
📝작업 내용
스크린샷
Screen_recording_20250916_015504.mp4
💬리뷰 요구사항
Summary by CodeRabbit
신기능
UI
데이터/저장소
잡무