-
Notifications
You must be signed in to change notification settings - Fork 0
[Feature/#47] 언어능력 퀴즈를 구현합니다. #48
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
- QuizResult -> QuizScore
- 퀴즈 활성화 및 ux 라이팅 수정
Walkthrough언어능력 퀴즈(LinguisticQuiz)를 도입하고 Quiz 도메인/응답 계층을 일반화했으며, 점수 업로드 엔드포인트·DTO·뷰모델·화면·네비게이션 등록과 관련된 데이터/리포지토리/서비스/UI를 추가·수정했습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Screen as LinguisticQuizScreen
participant VM as LinguisticQuizViewModel
participant FetchUC as FetchQuizUseCase
participant Repo as QuizRepositoryImpl
participant DS as QuizDataSourceImpl
participant API as QuizService
participant UploadUC as UploadQuizScoreUseCase
User->>Screen: 진입
Screen->>VM: 초기화 / 이벤트 처리
VM->>FetchUC: invoke(LINGUISTIC)
FetchUC->>Repo: fetchQuizzes(LINGUISTIC)
Repo->>DS: fetchQuizzes(LINGUISTIC)
DS->>API: GET /api/v1/quiz?type=LINGUISTIC
API-->>DS: List<QuizResponse>
DS-->>Repo: 응답을 도메인으로 변환
Repo-->>FetchUC: List<Quiz>
FetchUC-->>VM: 퀴즈목록 반환
VM->>Screen: uiState 업데이트 (로딩→콘텐츠)
loop 사용자 반복
User->>Screen: 답안 선택 / 계속
Screen->>VM: selectAnswer / checkAnswer
VM->>VM: 정답 판정(isAnswerCorrect), 상태 변경
end
alt 마지막 문제 완료
VM->>UploadUC: invoke(QuizScore)
UploadUC->>Repo: uploadQuizScore(QuizScore)
Repo->>DS: uploadQuizScore(QuizScore.toDto())
DS->>API: POST /api/v1/quiz/result
API-->>DS: 성공
DS-->>Repo: 성공 응답
Repo-->>UploadUC: 결과 전파
UploadUC-->>VM: 완료 알림
VM->>Screen: 결과/네비게이션 수행
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. 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.
Actionable comments posted: 6
🧹 Nitpick comments (11)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/model/QuizResult.kt (1)
3-3: 선택사항: KDoc 문서화 추가 고려API 계약(contract)을 명확하게 하기 위해 각 속성에 대한 KDoc 주석을 추가하면 좋습니다:
/** * 퀴즈 결과를 나타냅니다. * * @property isCorrect 사용자의 답변이 정답인지 여부 * @property correctAnswer 정답 텍스트 */ data class QuizResult(val isCorrect: Boolean, val correctAnswer: String)core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/product/topbar/MaStepProgressTopAppBar.kt (2)
20-20: 사용하지 않는 import를 제거하세요.
StepIndicator를 더 이상 사용하지 않으므로 이 import 문을 제거해야 합니다.다음 diff를 적용하여 사용하지 않는 import를 제거하세요:
-import com.moa.app.designsystem.component.core.indicator.StepIndicator
64-68: LGTM! 선택적 개선 사항을 고려하세요.단계 표시를 단순한 텍스트 형식으로 변경한 것은 깔끔하고 명확합니다. 스타일링도 제목과 일관성 있게 적용되어 있습니다.
선택적으로 다음 개선 사항을 고려할 수 있습니다:
- 간격 조정: 제목과 단계 표시 사이의 시각적 간격을 위해
Modifier.padding(top = 4.dp)추가를 고려하세요.- 접근성: 스크린 리더를 위한 의미론적 정보 추가를 고려하세요 (예:
semantics { contentDescription = "$totalSteps 단계 중 $currentStep 단계" }).- 지역화: 구분자 "/"가 하드코딩되어 있습니다. 다국어 지원이 필요한 경우 문자열 리소스로 추출하는 것을 고려하세요.
선택적으로 다음 diff를 적용하여 간격과 접근성을 개선할 수 있습니다:
Text( text = "$currentStep/$totalSteps", color = MoaTheme.colors.black, style = MoaTheme.typography.headLine2Bold, + modifier = Modifier.padding(top = 4.dp) )gradle/libs.versions.toml (1)
49-50: 주석 스타일 일관성 개선 제안다른 라이브러리 섹션의 주석은 대문자로 시작합니다(예: "# Network", "# Logging"). 일관성을 위해 "# Coil"로 변경하는 것을 권장합니다.
다음 diff를 적용하세요:
-# coil +# Coil coil = "3.3.0"domain/src/main/kotlin/com/moa/app/domain/quiz/model/QuizScore.kt (1)
3-7: 유효성 검증 로직 추가를 고려하세요.QuizScore 데이터 클래스에 무효한 상태를 방지하는 검증 로직이 없습니다. 예를 들어,
correctNumber가totalNumber보다 큰 경우나 음수 값이 들어오는 경우를 방지할 수 없습니다.다음과 같이
init블록을 추가하여 검증할 수 있습니다:data class QuizScore( val totalNumber: Int, val correctNumber: Int, val type: QuizCategory, -) +) { + init { + require(totalNumber >= 0) { "totalNumber must be non-negative" } + require(correctNumber >= 0) { "correctNumber must be non-negative" } + require(correctNumber <= totalNumber) { "correctNumber cannot exceed totalNumber" } + } +}data/src/main/kotlin/com/moa/app/data/quiz/service/QuizService.kt (1)
14-15: API 인터페이스 타입 안정성이 개선되었습니다.쿼리 파라미터 타입을
String에서QuizCategory로 변경하여 타입 안정성이 향상되었습니다. Retrofit은 @query 파라미터로 전달된 enum을 기본적으로 enum의 name을 사용하여 직렬화하며, QuizCategory 열거형의 값(PERSISTENCE, LINGUISTIC, MEMORY, ATTENTION, SPACETIME)이 대문자로 정의되어 있어 백엔드 API와 호환됩니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt (1)
44-49: 빈 리스트 엣지 케이스 처리 고려 필요
filterIsInstance<PersistenceQuiz>()가 빈 리스트를 반환할 경우에도Success상태로 처리됩니다. API에서 퀴즈를 반환했지만PersistenceQuiz타입이 없는 경우, 사용자에게 빈 화면이 표시될 수 있습니다.quizzesDeferred.await().fold( onSuccess = { quizzes -> val persistenceQuizzes = quizzes.filterIsInstance<PersistenceQuiz>() + if (persistenceQuizzes.isEmpty()) { + _uiState.update { + PersistenceQuizUiState.Error(message = "퀴즈가 존재하지 않습니다.") + } + return@fold + } _uiState.update { PersistenceQuizUiState.Success(quizzes = persistenceQuizzes.toImmutableList()) } },feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt (2)
19-19: deprecated된 import 경로 수정 필요
androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel은 deprecated되었습니다.androidx.hilt.navigation.compose.hiltViewModel로 변경해주세요.-import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.hilt.navigation.compose.hiltViewModel
160-173: "계속" 버튼 활성화 조건 검토 필요버튼이 항상
enabled = true로 설정되어 있어, 답변을 선택하지 않아도 클릭할 수 있습니다.PersistenceQuizViewModel의isContinueButtonEnabled패턴처럼 답변 선택 여부에 따라 버튼을 비활성화하는 것이 좋겠습니다.MaButton( onClick = onContinueClick, - enabled = true, + enabled = uiState.selectedAnswerIndex != null && !uiState.showResultDialog, modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp) .padding(bottom = 12.dp), ) {data/src/main/kotlin/com/moa/app/data/quiz/datasourceImpl/QuizDataSourceImpl.kt (1)
16-18: 불필요한 중복 괄호가 있습니다.
(type)주위에 불필요한 괄호가 있습니다. 가독성을 위해 제거하는 것이 좋습니다.override suspend fun fetchQuizzes(type: QuizCategory): Result<List<QuizResponse>> { - return quizService.fetchQuizzes((type)).toResult { it } + return quizService.fetchQuizzes(type).toResult { it } }feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt (1)
70-88:checkAnswer()중복 호출 시 경쟁 상태(race condition) 가능성
_uiState.update블록 이후에 별도의 코루틴이 실행되므로, 빠르게 중복 호출되면 여러goToNextQuestion()코루틴이 동시에 실행될 수 있습니다.상태 업데이트와 코루틴 실행을 원자적으로 처리하거나, 진행 중인 작업을 추적하는 것을 고려해 주세요:
+ private var advanceJob: kotlinx.coroutines.Job? = null + fun checkAnswer() { + if (advanceJob?.isActive == true) return + _uiState.update { state -> val selectedAnswerIndex = state.selectedAnswerIndex ?: return@update state val quiz = state.currentQuiz ?: return@update state // ... } - viewModelScope.launch { + advanceJob = viewModelScope.launch { delay(2000L) goToNextQuestion() } }
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (32)
app/src/main/kotlin/com/moa/app/main/MainActivity.kt(2 hunks)core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/product/topbar/MaStepProgressTopAppBar.kt(1 hunks)core/navigation/src/main/java/com/moa/app/navigation/AppRoute.kt(1 hunks)data/src/main/kotlin/com/moa/app/data/quiz/datasource/QuizDataSource.kt(1 hunks)data/src/main/kotlin/com/moa/app/data/quiz/datasourceImpl/QuizDataSourceImpl.kt(1 hunks)data/src/main/kotlin/com/moa/app/data/quiz/model/request/QuizScoreRequest.kt(1 hunks)data/src/main/kotlin/com/moa/app/data/quiz/model/response/LinguisticQuizResponse.kt(1 hunks)data/src/main/kotlin/com/moa/app/data/quiz/model/response/PersistenceQuizResponse.kt(1 hunks)data/src/main/kotlin/com/moa/app/data/quiz/model/response/QuizResponse.kt(1 hunks)data/src/main/kotlin/com/moa/app/data/quiz/repositoryImpl/QuizRepositoryImpl.kt(1 hunks)data/src/main/kotlin/com/moa/app/data/quiz/service/QuizService.kt(1 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/model/LinguisticQuiz.kt(1 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/model/PersistenceQuiz.kt(1 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/model/Quiz.kt(1 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/model/QuizScore.kt(1 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/repository/QuizRepository.kt(1 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/FetchQuizUseCase.kt(1 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/UploadQuizScoreUseCase.kt(1 hunks)feature/senior/build.gradle.kts(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryScreen.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryViewModel.kt(2 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategoryExtension.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizLoadContent.kt(2 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizSlideAnimation.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/model/LinguisticQuizUiState.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/model/QuizResult.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt(4 hunks)gradle/libs.versions.toml(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/kotlin/com/moa/app/main/MainActivity.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt (1)
LinguisticQuizScreen(36-71)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizLoadContent.kt (1)
QuizLoadContent(29-81)
🔇 Additional comments (35)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/model/QuizResult.kt (1)
1-3: 직렬화(Serialization) 지원 필요 여부 확인 필요합니다.이
QuizResult데이터 클래스는 백엔드 API 연동 시 직렬화가 필요할 수 있습니다. 현재 코드에는 직렬화 관련 주석(annotation)이 없습니다. 프로젝트에서 사용 중인 직렬화 라이브러리(Kotlinx Serialization, Jackson, Gson, Moshi 등)에 따라 적절한 주석을 추가해야 할 수 있습니다.// 예: Kotlinx Serialization 사용 시 @Serializable data class QuizResult(val isCorrect: Boolean, val correctAnswer: String) // 예: Jackson/Gson 사용 시 (추가 설정 필요할 수 있음) @SerializedName("isCorrect")프로젝트의 다른 데이터 모델들(예: 도메인/데이터 레이어의 Quiz 모델)과 일관성 있게 직렬화 방식을 적용하고 있는지 확인해주세요.
feature/senior/build.gradle.kts (1)
17-17: 변경 사항이 올바릅니다.Coil 이미지 로딩 라이브러리가 버전 카탈로그 번들을 통해 올바르게 추가되었습니다. 언어능력 퀴즈 기능에서 이미지를 표시하는 데 적절한 선택입니다.
gradle/libs.versions.toml (2)
149-149: 번들 정의가 올바릅니다.Coil 번들이
coil-compose와coil-network를 올바르게 그룹화하고 있습니다. 이는 Compose에서 이미지 로딩에 필요한 핵심 라이브러리들이며, 프로젝트의 다른 번들 정의 패턴과 일관성 있게 구현되었습니다.
111-113: Coil 3.3.0 라이브러리 정의 올바름 확인됨라이브러리 정의가 올바르게 구성되어 있습니다. io.coil-kt.coil3은 Coil 3.x의 올바른 Maven 좌표입니다. coil-network-okhttp 3.3.0은 38개 컴포넌트에서 사용되고 있으며, coil-compose 3.3.0은 268개 컴포넌트에서 사용되고 있습니다. 두 라이브러리 모두 3.3.0 버전으로 공식 문서에 나타난 예제와 동일합니다.
app/src/main/kotlin/com/moa/app/main/MainActivity.kt (1)
34-34: LGTM!LinguisticQuiz 라우트가 적절하게 등록되었습니다. 다른 라우트들과 일관된 패턴을 따르고 있습니다.
Also applies to: 80-80
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryScreen.kt (1)
76-76: LGTM!사용자 대면 텍스트가 더 일반적인 표현으로 개선되었습니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt (1)
52-52: LGTM!QuizLoadContent에 category 파라미터가 적절하게 전달되었습니다. 카테고리별 로딩 UI를 지원하기 위한 리팩토링과 일관성 있게 구현되었습니다.
core/navigation/src/main/java/com/moa/app/navigation/AppRoute.kt (1)
40-41: LGTM!LinguisticQuiz 라우트가 적절하게 추가되었습니다. 직렬화 어노테이션과 네이밍이 다른 라우트들과 일관성 있게 구현되었습니다.
domain/src/main/kotlin/com/moa/app/domain/quiz/model/Quiz.kt (1)
3-9: LGTM!Quiz sealed interface가 잘 설계되었습니다. 모든 퀴즈 타입에 공통적으로 필요한 프로퍼티들을 추상화하여 타입 안전성과 확장성을 제공합니다.
data/src/main/kotlin/com/moa/app/data/quiz/model/request/QuizScoreRequest.kt (1)
8-21: LGTM!QuizScoreRequest DTO와 매핑 함수가 적절하게 구현되었습니다. @SerialName 어노테이션으로 JSON 필드명을 명시적으로 지정하고, 확장 함수를 사용한 도메인-DTO 변환 패턴이 깔끔합니다.
domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/UploadQuizScoreUseCase.kt (1)
7-13: 깔끔한 구현입니다.유스케이스가 리포지토리에 명확하게 위임하고 있으며, 의존성 주입과 Result 타입을 올바르게 사용하고 있습니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryViewModel.kt (1)
58-60: 네비게이션 라우트 구현을 확인해주세요.새로 추가된 LINGUISTIC, ATTENTION, SPACETIME 카테고리에 대한 네비게이션 로직이 추가되었습니다. 로직 자체는 문제없으나, 해당 라우트들이 실제로 구현되어 있는지 확인이 필요합니다.
data/src/main/kotlin/com/moa/app/data/quiz/model/response/LinguisticQuizResponse.kt (2)
9-18: 데이터 모델 구현이 적절합니다.Kotlinx Serialization 어노테이션이 올바르게 적용되어 있으며, 부모 클래스 QuizResponse의 프로퍼티를 적절히 오버라이드하고 있습니다.
20-30: 도메인 매핑이 올바르게 구현되어 있습니다.데이터 레이어에서 도메인 레이어로의 변환이 명확하며, toImmutableList()를 사용하여 불변성을 보장하고 있습니다.
domain/src/main/kotlin/com/moa/app/domain/quiz/repository/QuizRepository.kt (1)
8-9: 리포지토리 인터페이스 개선이 적절합니다.메서드명을
fetchQuizzes로 일반화하고 반환 타입을Quiz로 변경하여 여러 퀴즈 타입을 지원할 수 있게 되었습니다.uploadQuizScore메서드 추가도 적절합니다.domain/src/main/kotlin/com/moa/app/domain/quiz/model/LinguisticQuiz.kt (1)
5-13: 도메인 모델 구조가 적절합니다.Quiz 인터페이스를 올바르게 구현하고 있으며, ImmutableList를 사용하여 불변성을 보장하고 있습니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/model/LinguisticQuizUiState.kt (1)
9-43: UI 상태 모델링이 잘 되어 있습니다.@immutable 어노테이션을 사용하여 Compose와의 호환성을 보장하고, 불변 컬렉션을 사용하며, computed 프로퍼티로 파생 상태를 제공하는 등 모범 사례를 따르고 있습니다. INIT 상태도 적절하게 정의되어 있습니다.
data/src/main/kotlin/com/moa/app/data/quiz/service/QuizService.kt (1)
17-18: 새로운 업로드 API가 올바르게 정의되어 있습니다.POST 엔드포인트와 요청 본문 어노테이션이 적절하게 설정되어 있습니다.
data/src/main/kotlin/com/moa/app/data/quiz/repositoryImpl/QuizRepositoryImpl.kt (2)
15-20: 리포지토리 구현이 적절합니다.데이터 소스에서 받은 응답을 도메인 모델로 올바르게 매핑하고 있으며,
mapCatching을 사용하여 에러 처리를 보존하고 있습니다.
22-24: 업로드 로직이 올바르게 구현되어 있습니다.데이터 소스로의 명확한 위임 패턴을 따르고 있습니다.
data/src/main/kotlin/com/moa/app/data/quiz/datasource/QuizDataSource.kt (1)
3-9: LGTM!인터페이스가 깔끔하게 정의되어 있습니다.
QuizCategory도메인 타입을 사용하여 타입 안전성을 확보하고,fetchQuizzes와uploadQuizScore두 메서드로 퀴즈 데이터 조회 및 점수 업로드 기능을 명확히 분리했습니다.domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/FetchQuizUseCase.kt (1)
8-13: LGTM!
PersistenceQuiz특화 유즈케이스에서 범용Quiz타입을 반환하는 유즈케이스로 리팩토링되었습니다. 단일 책임 원칙을 잘 따르고 있으며, 리포지토리 호출을 깔끔하게 위임하고 있습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategoryExtension.kt (1)
57-85: LGTM!로딩 상태를 위한 색상 속성들이 일관된 패턴으로 잘 구현되어 있습니다.
@Composable어노테이션을 통해 테마 컬러에 접근하는 방식이 적절합니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt (1)
71-85: LGTM!
isAnswerCorrect(selectedAnswerIndex)메서드 사용으로 정답 검증 로직이 도메인 모델에 캡슐화되어 더 깔끔해졌습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizLoadContent.kt (1)
29-81: LGTM!
QuizCategory파라미터를 추가하여 재사용 가능한 컴포넌트로 잘 리팩토링되었습니다. 카테고리별 색상과 설명이 적절히 적용되며,Box레이아웃을 통해 배경 이미지와 콘텐츠가 깔끔하게 구성되어 있습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizSlideAnimation.kt (1)
15-40: LGTM!제네릭 타입
T : Quiz를 활용한 재사용 가능한 슬라이드 애니메이션 컴포넌트입니다.quiz.id를contentKey로 사용하여 퀴즈 전환 시 애니메이션이 정확히 트리거되며,spring애니메이션으로 자연스러운 전환 효과를 제공합니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt (2)
115-151: LGTM!
step(2)와repeat(2)를 활용한 2열 그리드 구현이 잘 되어 있습니다. 홀수 개의 옵션일 때Spacer로 레이아웃 균형을 맞추는 처리도 적절합니다.
36-71: LGTM!
LinguisticQuizScreen구조가 잘 설계되어 있습니다.BackHandler를 통한 뒤로가기 처리, 로딩/콘텐츠 상태 분기, 결과 다이얼로그 및 종료 확인 다이얼로그 표시 로직이 명확합니다.data/src/main/kotlin/com/moa/app/data/quiz/datasourceImpl/QuizDataSourceImpl.kt (1)
20-22: LGTM!
uploadQuizScore메서드가 올바르게 구현되었습니다.toDto()변환과toResult()처리가 기존 패턴을 잘 따르고 있습니다.data/src/main/kotlin/com/moa/app/data/quiz/model/response/QuizResponse.kt (2)
9-17: LGTM!
QuizResponsesealed class가 잘 설계되었습니다.@JsonClassDiscriminator를 통한 다형성 직렬화 설정과 공통 속성 정의가 적절합니다.
19-24: LGTM!sealed class의
when표현식을 통해 모든 하위 타입을 명시적으로 처리하고 있어 새로운 타입 추가 시 컴파일 타임에 누락을 감지할 수 있습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt (2)
39-61: LGTM!최소 로딩 시간 보장을 위한
async/awaitAll패턴이 적절하게 구현되었습니다. 퀴즈 로딩 실패 시 에러 로깅과 상태 업데이트도 잘 처리되어 있습니다.
108-125: LGTM!
submitQuizResult에서 성공 시exitQuiz()를 호출하여 화면을 벗어나므로 로딩 상태 초기화가 불필요합니다. 실패 시 에러 메시지와 로딩 상태 처리가 적절합니다.data/src/main/kotlin/com/moa/app/data/quiz/model/response/PersistenceQuizResponse.kt (2)
9-17: LGTM!
QuizResponsesealed class 상속 구조가 잘 구현되었습니다.@SerialName("PERSISTENCE")를 통해 다형성 역직렬화 시 올바른 타입으로 매핑됩니다.
19-28: LGTM!
toDomain()함수에서QuizCategory.PERSISTENCE를 직접 사용하여 문자열 파싱 오류 가능성을 제거한 점이 좋습니다.
...nior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategoryExtension.kt
Show resolved
Hide resolved
...ure/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryViewModel.kt
Show resolved
Hide resolved
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt
Show resolved
Hide resolved
| Image( | ||
| painter = painterResource(R.drawable.img_quiz_character_center), | ||
| contentDescription = null, | ||
| modifier = modifier.clickable( | ||
| onClick = onImageClick, | ||
| role = Role.Button, | ||
| interactionSource = remember { MutableInteractionSource() }, | ||
| indication = null | ||
| ), | ||
| ) |
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.
접근성 개선을 위해 contentDescription을 추가하세요.
Image가 클릭 가능한 버튼 역할을 하고 있지만 contentDescription이 null로 설정되어 있습니다. 스크린 리더 사용자가 이 버튼의 목적을 이해할 수 없습니다.
다음 diff를 적용하여 적절한 설명을 추가하세요:
Image(
painter = painterResource(R.drawable.img_quiz_character_center),
- contentDescription = null,
+ contentDescription = "퀴즈 설명 듣기",
modifier = modifier.clickable(📝 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.
| Image( | |
| painter = painterResource(R.drawable.img_quiz_character_center), | |
| contentDescription = null, | |
| modifier = modifier.clickable( | |
| onClick = onImageClick, | |
| role = Role.Button, | |
| interactionSource = remember { MutableInteractionSource() }, | |
| indication = null | |
| ), | |
| ) | |
| Image( | |
| painter = painterResource(R.drawable.img_quiz_character_center), | |
| contentDescription = "퀴즈 설명 듣기", | |
| modifier = modifier.clickable( | |
| onClick = onImageClick, | |
| role = Role.Button, | |
| interactionSource = remember { MutableInteractionSource() }, | |
| indication = null | |
| ), | |
| ) |
🤖 Prompt for AI Agents
In
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt
around lines 34 to 43, the Image is clickable but contentDescription is null;
add a meaningful, localized description by creating a string resource (e.g., add
an entry like quiz_character_image_description in res/values/strings.xml) and
set contentDescription =
stringResource(R.string.quiz_character_image_description) on the Image so screen
readers can convey the button's purpose while keeping the existing clickable
behavior.
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 (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (1)
36-36: 접근성을 위해 contentDescription을 추가하세요.이 Image는 버튼 역할(Role.Button)을 하지만 contentDescription이 여전히 null입니다. 스크린 리더 사용자가 이 버튼의 용도를 파악할 수 없어 접근성에 문제가 됩니다. 이전 리뷰에서 지적된 사항이 아직 수정되지 않았습니다.
다음과 같이 수정하세요:
Image( painter = painterResource(R.drawable.img_quiz_character_center), - contentDescription = null, + contentDescription = "퀴즈 설명 듣기", modifier = Modifier.clickable(더 나은 방법으로는 문자열 리소스를 사용하는 것이 좋습니다:
contentDescription = stringResource(R.string.quiz_description_button)
🧹 Nitpick comments (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategoryExtension.kt (1)
48-86: 새로운load*속성들의 사용 목적을 문서화하는 것을 권장합니다.기존 속성(
description,backgroundColor)과 새로운load*접두사 속성들(loadDescription,loadBackgroundColor등)의 차이와 각각의 사용 시점이 명확하지 않습니다. KDoc 주석을 추가하여 다음을 설명하면 코드 가독성과 유지보수성이 향상됩니다:
- 각 속성 세트의 사용 목적 (예: 퀴즈 목록 화면 vs 로딩/준비 화면)
- 언제
load*속성을 사용하고 언제 기본 속성을 사용하는지예시:
+/** + * 퀴즈 로딩/준비 화면에서 사용되는 설명 텍스트 + * 퀴즈 목록 화면의 [description]과 구분하여 사용 + */ val QuizCategory.loadDescription: String get() = when (this) { QuizCategory.PERSISTENCE -> "지남력은\n시간과 장소를 확인해요" ... }
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
domain/src/main/kotlin/com/moa/app/domain/quiz/model/LinguisticQuiz.kt(1 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/model/PersistenceQuiz.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryViewModel.kt(2 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategoryExtension.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryViewModel.kt
⏰ 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: Run Unit Tests
🔇 Additional comments (9)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategoryExtension.kt (2)
48-55: 과거 리뷰의 오타가 수정되었습니다.Line 50의 "시관과" → "시간과" 오타가 올바르게 수정되었습니다. 코드가 정상적으로 작동합니다.
57-86: Composable 색상 속성들이 올바르게 구현되었습니다.새로운 색상 속성들(
loadBackgroundColor,loadTitleColor,loadReadyColor)이 Compose 패턴을 올바르게 따르고 있으며,MoaTheme.colors를 통해 일관된 테마 색상을 사용하고 있습니다. 각 카테고리별로 적절한 색상 shade가 선택되었습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (2)
45-61: 레이아웃과 스타일링이 적절합니다.Box와 Text의 구조가 잘 구성되어 있습니다.
fillMaxSize()를 사용하여 Row의 나머지 공간을 채우는 방식과 테마 시스템 활용이 적절합니다.
65-73: Preview 구현이 베스트 프랙티스를 따르고 있습니다.Preview 함수가 private으로 선언되어 있고 적절한 샘플 데이터를 제공하고 있어 좋습니다.
domain/src/main/kotlin/com/moa/app/domain/quiz/model/LinguisticQuiz.kt (3)
1-3: 패키지 구조와 임포트가 적절합니다.도메인 레이어의 패키지 구조가 올바르며, ImmutableList 사용은 불변성을 보장하는 좋은 선택입니다.
5-13: 데이터 클래스 구조가 올바르게 설계되었습니다.Quiz 인터페이스를 올바르게 구현하고 있으며, ImmutableList를 사용하여 스레드 안전성과 불변성을 보장합니다. questionImage 필드는 언어능력 퀴즈의 특성을 잘 반영하고 있습니다.
14-17: 이전 리뷰의 우려사항이 해결되었습니다.인덱스 범위 검증이 올바르게 구현되어 IndexOutOfBoundsException이 발생할 가능성이 제거되었습니다. 잘못된 인덱스에 대해 false를 반환하는 방어적 프로그래밍 접근 방식이 적절합니다.
domain/src/main/kotlin/com/moa/app/domain/quiz/model/PersistenceQuiz.kt (2)
5-12: Quiz 인터페이스 구현이 올바릅니다.인터페이스 속성에 override 키워드가 적절하게 적용되었으며, 통합 Quiz 인터페이스로의 리팩토링 목표와 일치합니다. answerOptions는 구현별 속성으로 적절하게 유지되고 있습니다.
13-16: 이전 리뷰의 우려사항이 해결되었습니다.인덱스 범위 검증이 추가되어 IndexOutOfBoundsException 발생 가능성이 제거되었습니다. LinguisticQuiz와 일관된 구현으로 잘못된 인덱스를 안전하게 처리합니다.
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: 3
🧹 Nitpick comments (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt (1)
48-48: 백엔드 데이터 불일치를 감지할 수 있도록 로깅을 추가하는 것을 고려하세요.
filterIsInstance<LinguisticQuiz>()를 사용하면 LINGUISTIC 카테고리로 요청했지만 다른 타입의 퀴즈가 반환될 경우 조용히 필터링되어 데이터 불일치를 놓칠 수 있습니다.필터링 전후의 크기를 비교하여 불일치가 있을 경우 로그를 남기는 것을 권장합니다:
onSuccess = { quizzes -> val linguisticQuizzes = quizzes.filterIsInstance<LinguisticQuiz>() + if (quizzes.size != linguisticQuizzes.size) { + Timber.w("Expected all LINGUISTIC quizzes, but got ${quizzes.size - linguisticQuizzes.size} non-linguistic items") + } _uiState.update {
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt(1 hunks)
⏰ 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: Run Unit Tests
🔇 Additional comments (8)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt (8)
1-24: LGTM! 임포트 구성이 적절합니다.필요한 의존성들이 모두 올바르게 임포트되어 있습니다.
26-31: LGTM! ViewModel 구성이 올바릅니다.Hilt DI를 통한 의존성 주입이 적절하게 구현되어 있습니다.
33-34: LGTM! StateFlow 패턴이 올바르게 구현되었습니다.상태 관리가 표준 패턴을 따르고 있으며, 캡슐화가 적절합니다.
36-38: LGTM! 초기화 로직이 적절합니다.ViewModel 생성 시 퀴즈를 로드하는 것은 적절한 패턴입니다.
64-69: LGTM! 답변 선택 로직이 적절합니다.로딩 중이거나 결과 다이얼로그 표시 중일 때 답변 선택을 방지하는 가드 조건이 잘 구현되어 있습니다.
91-107: LGTM! 다음 문제로 이동하는 로직이 올바릅니다.퀴즈 완료 여부를 확인하고 적절하게 다음 문제로 이동하거나 결과를 제출하는 로직이 잘 구현되어 있습니다.
109-126: LGTM! 퀴즈 결과 제출 로직이 적절합니다.결과 업로드 및 에러 처리가 올바르게 구현되어 있습니다.
128-136: LGTM! 네비게이션 로직이 올바릅니다.뒤로가기 및 종료 다이얼로그 처리가 적절하게 구현되어 있습니다.
| val minLoadingTime = async { delay(2000L) } | ||
| val quizzesDeferred = async { fetchQuizUseCase(QuizCategory.LINGUISTIC) } | ||
| awaitAll(minLoadingTime, quizzesDeferred) | ||
| quizzesDeferred.await().fold( |
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.
🛠️ Refactor suggestion | 🟠 Major
awaitAll 이후 중복된 await() 호출을 제거하세요.
Line 45에서 awaitAll을 호출한 후 Line 46에서 다시 quizzesDeferred.await()를 호출하는 것은 불필요합니다. awaitAll이 완료되면 이미 결과를 사용할 수 있으므로, .getCompleted() 또는 직접 quizzesDeferred의 결과를 사용해야 합니다.
다음 diff를 적용하여 중복 호출을 제거하세요:
- quizzesDeferred.await().fold(
+ quizzesDeferred.getCompleted().fold(📝 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.
| quizzesDeferred.await().fold( | |
| quizzesDeferred.getCompleted().fold( |
🤖 Prompt for AI Agents
In
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt
around line 45–46, you call awaitAll() and then immediately call
quizzesDeferred.await() again which is redundant; replace the second await()
with the already-available results from awaitAll (store awaitAll() result in a
variable and use it) or call quizzesDeferred.getCompleted() instead of await(),
and remove the duplicate suspend call so you only use the completed results.
| onFailure = { t -> | ||
| Timber.e(t, "fetchLinguisticQuizzes failed") | ||
| _uiState.update { it.copy(isLoading = false) } | ||
| }, |
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.
에러 처리 일관성을 개선하세요.
Line 58에서는 에러 메시지를 설정하지 않지만, Line 123에서는 errorMessage를 설정합니다. 사용자에게 일관된 에러 피드백을 제공하기 위해 여기서도 에러 메시지를 설정하는 것이 좋습니다.
다음 diff를 적용하세요:
onFailure = { t ->
Timber.e(t, "fetchLinguisticQuizzes failed")
- _uiState.update { it.copy(isLoading = false) }
+ _uiState.update { it.copy(isLoading = false, errorMessage = "퀴즈 로드 실패") }
},📝 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.
| onFailure = { t -> | |
| Timber.e(t, "fetchLinguisticQuizzes failed") | |
| _uiState.update { it.copy(isLoading = false) } | |
| }, | |
| onFailure = { t -> | |
| Timber.e(t, "fetchLinguisticQuizzes failed") | |
| _uiState.update { it.copy(isLoading = false, errorMessage = "퀴즈 로드 실패") } | |
| }, |
🤖 Prompt for AI Agents
In
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt
around lines 56–59, the onFailure block logs the throwable but does not set the
UI errorMessage like other failure paths; update the _uiState.update call to
also set errorMessage = t.message ?: "Unknown error" (or a localized generic
message) while keeping isLoading = false and retaining Timber.e(t, ...).
| viewModelScope.launch { | ||
| delay(2000L) | ||
| goToNextQuestion() | ||
| } |
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.
코루틴 취소 처리를 추가하여 메모리 누수를 방지하세요.
viewModelScope.launch로 시작된 코루틴이 2초 지연 후 goToNextQuestion()을 호출하는데, 사용자가 이 시간 동안 화면을 나가거나 ViewModel이 제거되면 문제가 발생할 수 있습니다. 또한, 사용자가 빠르게 여러 답변을 제출하면 여러 개의 지연된 코루틴이 동시에 실행될 수 있습니다.
Job을 추적하고 새로운 답변 체크 시 이전 Job을 취소하도록 수정하세요:
+ private var nextQuestionJob: Job? = null
+
fun checkAnswer() {
_uiState.update { state ->
val selectedAnswerIndex = state.selectedAnswerIndex ?: return@update state
val quiz = state.currentQuiz ?: return@update state
val isCorrect = quiz.isAnswerCorrect(selectedAnswerIndex)
val correctAnswer = if (isCorrect) "" else quiz.answer
state.copy(
showResultDialog = true,
quizResult = QuizResult(isCorrect = isCorrect, correctAnswer = correctAnswer),
correctCount = if (isCorrect) state.correctCount + 1 else state.correctCount,
)
}
- viewModelScope.launch {
+ nextQuestionJob?.cancel()
+ nextQuestionJob = viewModelScope.launch {
delay(2000L)
goToNextQuestion()
}
}🤖 Prompt for AI Agents
In
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizViewModel.kt
around lines 85-88, the coroutine started with viewModelScope.launch that delays
2000ms and calls goToNextQuestion() can leak or run multiple times if the
ViewModel is cleared or user answers quickly; fix by introducing a nullable Job
property (e.g., nextQuestionJob), before launching cancel the existing job if
active, assign the new launch to nextQuestionJob, and ensure it is cancelled in
onCleared (or when a new answer is submitted) so only one delayed action runs
and no coroutine continues after ViewModel is destroyed.
Related issue 🛠
Work Description ✏️
Screenshot 📸
Uncompleted Tasks 😅
Summary by CodeRabbit
새로운 기능
사용자 인터페이스 개선
✏️ Tip: You can customize this high-level summary in your review settings.