-
Notifications
You must be signed in to change notification settings - Fork 0
[Feature/#37] 단일 퀴즈 비즈니스 로직 구현 및 api를 연동합니다. #42
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
WalkthroughOrientationQuiz 라우트와 화면/뷰모델을 PersistenceQuiz로 전환하고, 하드코딩된 퀴즈를 Retrofit → DataSource → Repository → UseCase → ViewModel 흐름으로 교체했습니다. 도메인 모델(Quiz→PersistenceQuiz), 카테고리 열거형, DI 바인딩, 디자인 시스템 소량 인터페이스 변경 및 UI 사이드이펙트/상태 관리를 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant PersistenceScreen as PersistenceQuizScreen
participant VM as PersistenceQuizViewModel
participant UseCase as FetchPersistenceQuizUseCase
participant Repo as QuizRepository
participant DS as QuizDataSource
participant API as QuizService
participant Backend
User->>PersistenceScreen: 화면 진입
PersistenceScreen->>VM: loadQuizzes()
VM->>UseCase: invoke(QuizCategory.PERSISTENCE)
UseCase->>Repo: fetchPersistenceQuizzes(PERSISTENCE)
Repo->>DS: fetchPersistenceQuizzes("PERSISTENCE")
DS->>API: fetchPersistenceQuizzes(type="PERSISTENCE")
API->>Backend: GET /api/v1/quiz/set?type=PERSISTENCE
Backend-->>API: PersistenceQuizResponse[]
API-->>DS: NetworkResult<List<PersistenceQuizResponse>>
DS-->>Repo: Result<List<PersistenceQuizResponse>> -> map to domain
Repo-->>UseCase: Result<List<PersistenceQuiz>>
UseCase-->>VM: Result<List<PersistenceQuiz>>
VM-->>PersistenceScreen: uiState = Success(...)
Note over VM,PersistenceScreen: 사용자 선택/정답확인/결과대화상자 표시 흐름
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizCategoryCard.kt (1)
35-69:zIndex때문에 텍스트가 이미지 뒤로 가려질 수 있습니다Line 56–69 기준 현재 그리기 순서는:
Image(zIndex(1f))- (비활성 시) 회색
Box오버레이 (zIndex기본 0)- 제목/설명
Text(zIndex기본 0)Compose 에서는 더 높은
zIndex를 가진 컴포저블이 마지막에 그려지므로,Image가 텍스트와 오버레이 위에 올라와 텍스트가 가려질 수 있습니다.의도하신 레이어링이 “이미지 → (옵션) 회색 오버레이 → 텍스트” 라면,
Image의zIndex(1f)를 제거하는 쪽이 자연스럽습니다.Image( painter = painterResource(backgroundImage), contentDescription = null, contentScale = ContentScale.Crop, alpha = if (isEnabled) 1f else 0.5f, - modifier = Modifier - .fillMaxSize() - .zIndex(1f) + modifier = Modifier.fillMaxSize() )이렇게 하면 구성 순서대로 이미지 → (옵션) 오버레이 → 텍스트가 그려져 가독성이 보장됩니다.
🧹 Nitpick comments (14)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizResultDialog.kt (2)
21-40: 다이얼로그 닫기 메커니즘 누락현재 다이얼로그는
onDismissRequest = {},dismissOnBackPress = false,dismissOnClickOutside = false로 설정되어 있어 사용자가 닫을 수 없습니다. 퀴즈 결과 확인 후 다음 단계로 진행할 수 있도록onDismiss또는onConfirm콜백 파라미터 추가를 권장합니다.@Composable fun QuizResultDialog( isCorrect: Boolean, correctAnswer: String, modifier: Modifier = Modifier, + onDismiss: () -> Unit = {}, ) { Dialog( - onDismissRequest = {}, + onDismissRequest = onDismiss, properties = DialogProperties( dismissOnBackPress = false, dismissOnClickOutside = false, ), ) {타이머 기반 자동 닫기가 외부에서 처리되는 경우라면 현재 구현이 의도된 것일 수 있습니다.
52-52: 한국어 조사 문법 개선 고려
"정답은 ${correctAnswer}이에요"에서 "이에요/예요"는 앞 글자의 받침 유무에 따라 달라집니다. 현재는 모든 경우에 "이에요"를 사용하고 있어, 받침이 없는 단어(예: "사과")의 경우 어색할 수 있습니다.core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/product/dialog/MaAlertDialog.kt (1)
28-42: 기존 API 유지하면서 dismiss 경로 분리한 설계가 좋습니다
onDialogDismissRequest를 추가해서 바깥 터치/백 버튼 dismiss 와 명시적인onDismiss버튼 클릭을 분리한 점이 좋습니다. 한 가지 제안이라면, 두 콜백의 역할이 헷갈릴 수 있어서 KDoc 으로 “시스템/백/바깥 터치용” vs “취소 버튼용” 정도만 짚어 두면 사용하는 쪽에서 더 직관적일 것 같습니다.data/src/main/kotlin/com/moa/app/data/quiz/datasourceImpl/QuizDataSourceImpl.kt (1)
11-16: 데이터 소스 구현은 단순·명확하며, 타입 안정성은 추후 확장 여지가 있습니다
QuizDataSourceImpl이QuizService호출을 그대로toResult { it }로 감싸는 구조라, 네트워크 레이어와의 역할 분리가 명확해서 좋습니다.추가로, 서버에서 허용하는 퀴즈 타입이 고정 enum 값이라면
type: String대신 enum/sealed class 로 한 번 감싸 두면 오타를 컴파일 타임에 막을 수 있으니, 이후 확장 시에 한 번 고려해볼 만합니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategoryUiState.kt (1)
1-13: QuizCategoryUiState 구조가 단순하고 의도가 잘 드러납니다
enabledCategories를Set으로 둬서 중복을 막는 점이 적절합니다. 기본값이 항상emptySet()라면:data class QuizCategoryUiState( val enabledCategories: Set<QuizCategory> = emptySet(), )처럼 생성자 기본값으로 처리하고
INIT를 생략하는 방식도 고려해 볼 수 있습니다(필요하다면 그대로 두셔도 무방합니다).data/src/main/kotlin/com/moa/app/data/quiz/datasource/QuizDataSource.kt (1)
1-7: QuizDataSource 설계는 단순하지만 타입 안정성 측면에서 여지가 있습니다현재는 백엔드 스펙에 맞춰
type을String으로 직접 받도록 정의되어 있어 구현이 직관적입니다. 다만 레이어 경계에서 도메인 타입(예:QuizCategory) ↔︎String매핑을QuizDataSourceImpl쪽에서만 담당하게 하고, 인터페이스는 도메인 친화적인 시그니처로 두는 것도 장기적으로는 안전할 수 있습니다. 기존AuthDataSource/UserDataSource패턴과 맞춰 볼지 한 번 정도 검토해 보셔도 좋겠습니다.domain/src/main/kotlin/com/moa/app/domain/quiz/model/PersistenceQuiz.kt (1)
1-16:getCurrentAnswerIndex함수 이름과 반환 타입이 어울리지 않습니다현재 구현은
selectedAnswerIndex가 정답 인덱스와 같은지 여부를 반환하고 있어 로직 자체는 문제없지만, 함수 이름만 보면Int(현재 정답 인덱스)를 돌려줄 것처럼 보입니다.예를 들어 다음과 같이 이름을 바꾸는 쪽이 더 이해하기 쉬워 보입니다.
fun isCorrectAnswer(selectedAnswerIndex: Int): Boolean = selectedAnswerIndex == answerOptions.indexOf(answer)또는 실제 정답 인덱스를 돌려주는 함수와, 정답 여부를 판단하는 함수를 분리하는 것도 고려해 볼 수 있습니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryScreen.kt (1)
43-53: LaunchedEffect 내부에서 flowWithLifecycle 사용은 중복될 수 있습니다.
collectAsStateWithLifecycle은 이미 lifecycle-aware하게 동작합니다.LaunchedEffect(Unit)내부에서flowWithLifecycle을 사용하면, LaunchedEffect는 컴포지션이 떠날 때까지 유지되므로 lifecycle 중복 처리가 발생할 수 있습니다.더 간단한 패턴으로 리팩토링을 고려해보세요:
- LaunchedEffect(Unit) { - viewModel.sideEffect - .flowWithLifecycle(lifecycleOwner.lifecycle) - .collect { sideEffect -> + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.sideEffect.collect { sideEffect -> when (sideEffect) { is QuizCategorySideEffect.ShowToast -> { Toast.makeText(context, sideEffect.message, Toast.LENGTH_LONG).show() } } } + } }domain/src/main/kotlin/com/moa/app/domain/quiz/model/QuizCategory.kt (1)
21-31:fromString함수를 Kotlin 내장 기능으로 단순화할 수 있습니다.수동으로 문자열을 매핑하는 대신 Kotlin enum의 내장 함수를 활용하면 새로운 enum 값이 추가될 때 동기화 문제를 방지할 수 있습니다.
companion object { fun fromString(value: String): QuizCategory { - return when (value) { - "PERSISTENCE" -> PERSISTENCE - "LINGUISTIC" -> LINGUISTIC - "MEMORY" -> MEMORY - "ATTENTION" -> ATTENTION - "SPACETIME" -> SPACETIME - else -> throw IllegalArgumentException("Invalid QuizCategory value: $value") - } + return entries.find { it.name == value } + ?: throw IllegalArgumentException("Invalid QuizCategory value: $value") } }또는 단순히
valueOf(value)를 사용할 수도 있습니다 (동일한 예외를 던짐).feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt (2)
29-29: 사용되지 않는 import가 있습니다.
DialogProperties가 import되었지만 이 파일에서 사용되지 않습니다.MaAlertDialog가 내부적으로 처리하는 것으로 보입니다.-import androidx.compose.ui.window.DialogProperties
196-228: Preview 데이터의 모든 퀴즈가 동일한 id를 가지고 있습니다.Preview 목적으로는 문제가 없지만, 실제 데이터 구조를 더 정확히 반영하려면 고유한 id 값을 사용하는 것이 좋습니다.
PersistenceQuiz( - id = 1, + id = 1, ... ), PersistenceQuiz( - id = 1, + id = 2, ... ), PersistenceQuiz( - id = 1, + id = 3, ... ), PersistenceQuiz( - id = 1, + id = 4, ... ),feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryViewModel.kt (1)
38-54: 문자열 리소스 추출을 고려해보세요.비활성화된 카테고리에 대한 가드 로직은 잘 구현되어 있습니다. 다만 토스트 메시지가 하드코딩되어 있어 향후 다국어 지원 시 수정이 필요할 수 있습니다.
나중에 문자열 리소스로 추출하는 것을 고려해보세요:
// strings.xml에 추가 // <string name="quiz_not_ready">%s 퀴즈는 아직 준비중이에요</string> _sideEffect.emit( QuizCategorySideEffect.ShowToast( context.getString(R.string.quiz_not_ready, quizCategory.name) ) )feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt (2)
3-3: 구조화된 로깅 사용을 고려해보세요.
android.util.Log는 개발 중에는 괜찮지만, 프로덕션 환경에서는 Timber나 다른 구조화된 로깅 라이브러리 사용을 권장합니다. 이를 통해 로그 레벨 관리와 프로덕션 모니터링이 더 용이해집니다.만약 프로젝트에서 Timber를 사용한다면:
-import android.util.Log +import timber.log.Timber // ... - Log.e("PersistenceQuizViewModel", "loadQuizzes: $t") + Timber.e(t, "Failed to load quizzes")Also applies to: 44-44
60-80: RenamegetCurrentAnswerIndexto better reflect its actual behavior.The method at
domain/src/main/kotlin/com/moa/app/domain/quiz/model/PersistenceQuiz.kt:13returnsBoolean, not an index. The namegetCurrentAnswerIndexis misleading since it actually checks if the selected answer is correct. Consider renaming it toisAnswerCorrectorcheckAnswerCorrectfor clarity.Current implementation:
fun getCurrentAnswerIndex(selectedAnswerIndex: Int): Boolean { return selectedAnswerIndex == answerOptions.indexOf(answer) }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (29)
app/src/main/kotlin/com/moa/app/main/MainActivity.kt(2 hunks)core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/product/dialog/MaAlertDialog.kt(1 hunks)core/navigation/src/main/java/com/moa/app/navigation/AppRoute.kt(1 hunks)data/build.gradle.kts(1 hunks)data/src/main/kotlin/com/moa/app/data/di/DataSourceModule.kt(2 hunks)data/src/main/kotlin/com/moa/app/data/di/RepositoryModule.kt(2 hunks)data/src/main/kotlin/com/moa/app/data/di/ServiceModule.kt(2 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/response/PersistenceQuizResponse.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/PersistenceQuiz.kt(1 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/model/QuizCategory.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/CheckAnswerUseCase.kt(0 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/FetchOrientationQuizUseCase.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/home/SeniorHomeViewModel.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategory.kt(0 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryScreen.kt(2 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryViewModel.kt(1 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/category/model/QuizCategorySideEffect.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategoryUiState.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizCategoryCard.kt(5 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizResultDialog.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/orientation/OrientationQuizViewModel.kt(0 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt(5 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt(1 hunks)
💤 Files with no reviewable changes (3)
- domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/CheckAnswerUseCase.kt
- feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategory.kt
- feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/orientation/OrientationQuizViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (3)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryScreen.kt (2)
core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/product/topbar/MaTopAppBar.kt (1)
MaTopAppBar(20-52)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizCategoryCard.kt (1)
QuizCategoryCard(29-88)
app/src/main/kotlin/com/moa/app/main/MainActivity.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt (1)
PersistenceQuizScreen(45-83)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt (3)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizLoadContent.kt (1)
QuizLoadContent(22-64)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizResultDialog.kt (1)
QuizResultDialog(21-40)core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/product/dialog/MaAlertDialog.kt (1)
MaAlertDialog(26-115)
⏰ 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: Run Unit Tests
- GitHub Check: Run ktlint
🔇 Additional comments (18)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizResultDialog.kt (1)
84-94: LGTM!정답/오답 두 가지 상태에 대한 Preview가 적절하게 구현되어 있습니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategorySideEffect.kt (1)
3-5: 퀴즈 카테고리 사이드이펙트 모델링 적절합니다
sealed interface+ShowToast구조로 UI 사이드이펙트를 타입 세이프하게 분리한 점 좋습니다. 이후 네비게이션/다이얼로그 등 다른 사이드이펙트도 확장하기 쉽겠습니다.app/src/main/kotlin/com/moa/app/main/MainActivity.kt (1)
32-79: Persistence 퀴즈 플로우로의 네비게이션 정리가 일관적입니다
AppRoute.PersistenceQuiz로 라우트와PersistenceQuizScreen연결을 맞춰 준 부분이 SeniorHomeViewModel 의 변경과도 일관되고, 기존 Orientation 퀴즈에서 새 플로우로 자연스럽게 마이그레이션된 것 같습니다.data/build.gradle.kts (1)
11-20: immutable 컬렉션 의존성 추가 적절합니다
kotlinx.collections.immutable를 data 모듈에 추가해서 응답 → 도메인 매핑 시 불변 리스트를 사용할 수 있게 한 선택이 전체 아키텍처 방향(불변 상태)과 잘 맞습니다. 현재 diff 범위 내에서는 다른 영향도 없어 보입니다.data/src/main/kotlin/com/moa/app/data/di/ServiceModule.kt (1)
5-36: QuizService DI 바인딩이 기존 패턴과 잘 맞습니다
ServiceModule에QuizService를 @singleton 으로 추가해 Auth/User 서비스와 동일한 패턴으로 구성한 점 좋습니다. Retrofit 기반 DataSource/Repository 계층에서 바로 주입해서 사용할 수 있겠습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/home/SeniorHomeViewModel.kt (1)
19-21: 홈 → 데일리 퀴즈 네비게이션이 새 라우트와 정합합니다
navigateToDailyQuiz에서AppRoute.PersistenceQuiz로 이동하도록 바꾼 부분이 MainActivity 의 NavHost 설정과 맞춰져 있어서, 홈 진입 플로우와 실제 화면 구성이 잘 정리되었습니다.data/src/main/kotlin/com/moa/app/data/di/RepositoryModule.kt (1)
5-29: QuizRepository DI 바인딩이 기존 패턴과 일관적입니다Auth/User 리포지토리와 동일하게
@Binds+@Singleton패턴으로QuizRepositoryImpl을 묶어 두어 Hilt 구성상 문제 없어 보입니다.core/navigation/src/main/java/com/moa/app/navigation/AppRoute.kt (1)
37-38: PersistenceQuiz 라우트 네이밍 변경이 자연스럽습니다OrientationQuiz 대신 PersistenceQuiz 로 네이밍을 통일하려는 의도에 잘 맞는 변경입니다. 다른 화면 및 ViewModel, 네비게이션 호출부에서
AppRoute.PersistenceQuiz로 모두 교체되었는지만 한 번 더 확인해 주세요.data/src/main/kotlin/com/moa/app/data/di/DataSourceModule.kt (1)
7-29: QuizDataSource DI 바인딩도 기존 구조와 잘 맞습니다Auth/User DataSource 바인딩과 동일한 구조(
@Binds,@Singleton)로 추가되어 Hilt 그래프 상 문제 없어 보입니다.domain/src/main/kotlin/com/moa/app/domain/quiz/repository/QuizRepository.kt (1)
1-8: 도메인 QuizRepository 인터페이스 정의가 명확합니다입력으로
QuizCategory, 결과로Result<List<PersistenceQuiz>>를 노출해 도메인 레이어에서 사용하기 좋은 추상화입니다. 다른 도메인 유스케이스들도 동일한Result패턴을 사용한다면 일관성 측면에서도 좋겠습니다.data/src/main/kotlin/com/moa/app/data/quiz/service/QuizService.kt (1)
1-15: QuizService Retrofit 정의가 간결하고 기존 패턴과 잘 맞습니다
/api/v1/quiz/set엔드포인트에type쿼리로 요청하고, 결과를NetworkResult<List<PersistenceQuizResponse>>로 감싸는 구조가 네트워크 모듈 패턴과 자연스럽게 어울립니다. 실제 백엔드 API 경로와 쿼리 파라미터 키가 최신 스펙과 일치하는지만 한 번 더 확인해 주세요.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryScreen.kt (1)
91-103: LGTM!
QuizCategory.entries를 순회하며uiState.enabledCategories를 기반으로isEnabled상태를 계산하는 로직이 적절합니다. 카드 클릭 시 해당 카테고리를 전달하는 구조도 명확합니다.data/src/main/kotlin/com/moa/app/data/quiz/repositoryImpl/QuizRepositoryImpl.kt (1)
10-18: LGTM!Repository 패턴이 깔끔하게 구현되어 있습니다.
mapCatching을 사용하여 도메인 모델 변환 중 발생할 수 있는 예외를 적절히 처리하고 있습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/model/QuizCategoryExtension.kt (1)
11-47: LGTM!도메인 모델(
QuizCategory)을 깔끔하게 유지하면서 UI 표현 로직을 extension property로 분리한 좋은 패턴입니다.@Composable및@DrawableRes어노테이션도 적절히 사용되었습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt (2)
63-68: LGTM!
showResultDialog와dialogResult를 함께 확인하여 결과 다이얼로그를 조건부로 표시하는 로직이 적절합니다. null 체크를 통해 안전하게 처리하고 있습니다.
70-80: LGTM!종료 확인 다이얼로그가 적절하게 구현되었습니다. 확인/취소 버튼에 대한 콜백이 명확하게 분리되어 있고,
onDialogDismissRequest를 통해 다이얼로그 외부 클릭 시에도 적절히 처리됩니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryViewModel.kt (1)
27-35: 깔끔한 상태 관리 구현입니다!StateFlow와 SharedFlow를 사용한 상태 관리가 잘 구현되어 있습니다. 초기 상태에서 PERSISTENCE 카테고리만 활성화하는 것도 PR 요구사항과 일치합니다.
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt (1)
144-165: 우수한 UI 상태 모델링입니다!Sealed interface를 사용한 상태 관리와 computed properties 디자인이 훌륭합니다.
isContinueButtonEnabled와 같은 파생 상태를 프로퍼티로 관리하는 것은 UI 로직을 깔끔하게 유지하는 좋은 방법입니다.
data/src/main/kotlin/com/moa/app/data/quiz/model/response/PersistenceQuizResponse.kt
Show resolved
Hide resolved
domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/FetchOrientationQuizUseCase.kt
Outdated
Show resolved
Hide resolved
.../senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt
Show resolved
Hide resolved
...nior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt
Show resolved
Hide resolved
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/persistence/PersistenceQuizViewModel.kt (1)
38-57: 최소 2초 로딩 지연은 여전히 프로덕션 UX에 부담이 큽니다.Line 40의
delay(2000L)를 async 로 감싸 최소 로딩 시간을 유지하도록 바꾸셨지만, 네트워크가 충분히 빨라져도 사용자는 항상 2초를 기다리게 된다는 점에서 이전 리뷰에서 지적된 것과 동일한 문제가 남아 있습니다. 실제 배포용이라면 이 최소 지연을 제거하거나, 적어도BuildConfig.DEBUG에서만 동작하도록 제한하는 편이 좋겠습니다. 예를 들어 다음처럼 단순화할 수 있습니다:- val minLoadingTime = async { delay(2000L) } - val quizzesDeferred = async { - fetchPersistenceQuizUseCase(QuizCategory.PERSISTENCE) - } - awaitAll(minLoadingTime, quizzesDeferred) - quizzesDeferred.await().fold( + fetchPersistenceQuizUseCase(QuizCategory.PERSISTENCE).fold( onSuccess = { quizzes -> _uiState.update { PersistenceQuizUiState.Success(quizzes = quizzes.toImmutableList()) } }, onFailure = { t -> _uiState.update { PersistenceQuizUiState.Error(message = "퀴즈가 존재하지 않습니다.") } Log.e("PersistenceQuizViewModel", "loadQuizzes: $t") }, )로딩 스켈레톤을 일부러 보여주고 싶다면, QA/디버그 빌드에서만 짧은 지연(예: 300~500ms) 을 거는 방식도 고려해 보시면 좋겠습니다.
🧹 Nitpick comments (3)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt (3)
45-54: 빈 퀴즈 리스트에 대한 처리 방식을 한 번 더 고민해 볼 여지가 있습니다.현재는
onSuccess에서 리스트가 비어 있어도 그대로Success상태로 넘기고 있습니다. 이 경우 실제로 퀴즈가 하나도 없을 때currentStep/totalSteps계산이나 화면 표현이 애매해지거나, 사용자가 “퀴즈가 존재하지 않습니다.” 같은 메시지를 보지 못할 수 있습니다. API 계약에 따라 다르겠지만, 빈 리스트일 때는Error또는 별도의Empty상태로 구분하는 것도 고려해 보시면 좋겠습니다.
71-91: 정답 검증 로직은 방어적으로 잘 짜여 있지만 도메인 메서드 네이밍이 살짝 모호합니다.선택되지 않은 경우, 인덱스 범위 밖인 경우, 이미 결과 다이얼로그가 떠 있는 경우를 모두
return@update로 빠르게 빠져나가도록 해서 NPE나 범위 오류 없이 안전하게 동작할 것으로 보입니다. 다만currentQuiz.getCurrentAnswerIndex(selectedAnswerIndex)가Boolean을 반환한다면,isCorrectAnswer(selectedIndex)처럼 의미가 더 드러나는 이름으로 바꾸는 것도 도메인 쪽 리팩토링 시 검토해 보시면 가독성이 좋아질 것 같습니다. 또한 정답인 경우correctAnswer를 빈 문자열로 설정하고 있어, UI 쪽에서isCorrect조건을 반드시 확인해야 하는데 이 부분이 실제 화면에서 잘 처리되는지만 한 번 더 점검해 주세요.
155-178: PersistenceQuizUiState 설계가 명확하고, 에러 메시지는 리소스로 분리해 두면 더 좋겠습니다.
sealed interface+Success안의currentStep,totalSteps,isContinueButtonEnabled계산 프로퍼티 구성은 UI 에서 사용하기 좋고 상태 전이도 잘 드러납니다. 다만Error.message에 하드코딩된 한국어 문자열을 넣기보다는, 에러 타입(enum 등)만 노출하고 실제 문구는 Compose 쪽에서 string 리소스로 매핑하는 패턴을 사용하면 다국어 지원과 재사용성 측면에서 더 유연해질 것 같습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
domain/src/main/kotlin/com/moa/app/domain/quiz/model/Quiz.kt(0 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/model/Quizzes.kt(0 hunks)domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/FetchPersistenceQuizUseCase.kt(1 hunks)feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt(1 hunks)
💤 Files with no reviewable changes (2)
- domain/src/main/kotlin/com/moa/app/domain/quiz/model/Quiz.kt
- domain/src/main/kotlin/com/moa/app/domain/quiz/model/Quizzes.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 (5)
domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/FetchPersistenceQuizUseCase.kt (1)
8-13: 도메인 UseCase 래핑 구조 괜찮습니다.
QuizRepository.fetchPersistenceQuizzes를 단일invoke메서드로 그대로 위임하고Result<List<PersistenceQuiz>>를 전달하는 구조라 상위 계층에서 사용하기 명확해 보입니다. 실제 단말에서 성공/실패 플로우 한 번씩만 확인해 주시면 될 것 같습니다.feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt (4)
31-36: 초기 UI 상태와 자동 로딩 트리거 구성이 자연스럽습니다.초기 상태를
Loading으로 두고init블록에서 한 번만loadQuizzes()를 호출하는 패턴이라 ViewModel 수명주기와 잘 맞고, 중복 요청 위험도 낮아 보입니다. 실제 화면 진입 시 한 번만 호출되는지 로그 등으로 한 번만 더 확인해 주세요.
61-69: 정답 선택 시 결과 다이얼로그 노출 중에는 상태 변경을 막는 처리 좋습니다.
showResultDialog가true인 상태에서는 선택을 무시하여, 결과 다이얼로그가 떠 있는 동안 사용자가 보기를 다시 탭해도 상태가 꼬이지 않도록 잘 방어하고 있습니다. 실제 UI 에서 계속/확인 버튼 활성화 조건과도 일관되게 동작하는지만 한 번 더 확인해 주세요.
93-125: 다음 문제/마지막 문제 분기 및 다이얼로그 종료 타이밍이 명확합니다.현재 상태 스냅샷으로
nextQuestionIndex와isLastQuestion를 계산한 뒤, 마지막 문제에서는 결과 다이얼로그를 숨기고 짧은 지연 후exitQuiz()를 호출하는 흐름, 그 외에는 다음 문제로 넘어가며 선택/다이얼로그 상태를 초기화하는 흐름이 깔끔합니다. 실제 단말에서 “마지막 문제 → 결과 다이얼로그 → 화면 종료” 동작이 의도한 타이밍으로 보이는지만 한 번 더 확인해 주세요.
127-147: 종료 다이얼로그 상태 토글과 네비게이션 분리가 잘 되어 있습니다.
exitDialog플래그로 다이얼로그 노출 여부만 관리하고, 실제 화면 종료는exitQuiz()한 곳에서navigator.navigateBack()으로 처리해서 책임이 잘 분리되어 있습니다. 시스템 백 버튼과 UI 상의 뒤로 버튼이 모두 이 ViewModel 경로를 타는지, Activity/Fragment 레벨에서 별도 처리되는 경로는 없는지만 한 번 점검해 두면 좋겠습니다.
Related issue 🛠
Work Description ✏️
Screenshot 📸
Uncompleted Tasks 😅
Summary by CodeRabbit
새로운 기능
UI/UX 개선
✏️ Tip: You can customize this high-level summary in your review settings.