Skip to content

Conversation

@wjdrjs00
Copy link
Collaborator

@wjdrjs00 wjdrjs00 commented Nov 27, 2025

Related issue 🛠

Work Description ✏️

  • 단일 퀴즈 비즈니스 로직 구현(지남력 퀴즈)
  • 단일 퀴즤 목록 조회 api 연동
  • 현재 제공되지 않는 퀴즈 비활성화 로직 구현
  • 퀴즈 종료 로직 구현(다이얼로그)
  • 퀴즈 카테고리 도메인 로직 통합

Screenshot 📸

  • N/A

Uncompleted Tasks 😅

  • 퀴즈 결과 전송 로직 구현하기

Summary by CodeRabbit

  • 새로운 기능

    • 지속성(Persistence) 퀴즈로 기능 전환 — 서버 기반 퀴즈 불러오기 지원
    • 퀴즈 결과 표시 방식 간소화 및 즉시 피드백 제공
  • UI/UX 개선

    • 퀴즈 카테고리 화면에 활성/비활성 상태 반영 및 스크롤 가능 목록 적용
    • 비활성 카테고리 선택 시 토스트 안내 표시
    • 퀴즈 종료 전 확인 대화상자 추가 및 백 버튼 처리 개선

✏️ Tip: You can customize this high-level summary in your review settings.

@wjdrjs00 wjdrjs00 self-assigned this Nov 27, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 27, 2025

Walkthrough

OrientationQuiz 라우트와 화면/뷰모델을 PersistenceQuiz로 전환하고, 하드코딩된 퀴즈를 Retrofit → DataSource → Repository → UseCase → ViewModel 흐름으로 교체했습니다. 도메인 모델(Quiz→PersistenceQuiz), 카테고리 열거형, DI 바인딩, 디자인 시스템 소량 인터페이스 변경 및 UI 사이드이펙트/상태 관리를 추가했습니다.

Changes

Cohort / File(s) 변경 사항
네비게이션 변경
core/navigation/src/main/java/com/moa/app/navigation/AppRoute.kt, app/src/main/kotlin/com/moa/app/main/MainActivity.kt, feature/senior/src/main/kotlin/com/moa/app/feature/senior/home/SeniorHomeViewModel.kt
OrientationQuizPersistenceQuiz로 데이터 객체/라우트 이름과 네비게이션 경로 교체
도메인 모델 추가/삭제
domain/src/main/kotlin/com/moa/app/domain/quiz/model/PersistenceQuiz.kt, domain/src/main/kotlin/com/moa/app/domain/quiz/model/QuizCategory.kt, domain/src/main/kotlin/com/moa/app/domain/quiz/model/Quiz.kt, domain/src/main/kotlin/com/moa/app/domain/quiz/model/Quizzes.kt
PersistenceQuiz, QuizCategory 추가; 기존 Quiz, Quizzes 제거
유스케이스 변경
domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/FetchOrientationQuizUseCase.kt, domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/FetchPersistenceQuizUseCase.kt, domain/src/main/kotlin/com/moa/app/domain/quiz/usecase/CheckAnswerUseCase.kt
FetchOrientationQuizUseCase가 리포지토리 의존으로 변경(카테고리 파라미터로 호출) 및 CheckAnswerUseCase 제거; 새로운 FetchPersistenceQuizUseCase 추가
데이터 계층 — API/DTO/리포지토리/데이터소스
data/src/main/kotlin/com/moa/app/data/quiz/service/QuizService.kt, data/src/main/kotlin/com/moa/app/data/quiz/model/response/PersistenceQuizResponse.kt, data/src/main/kotlin/com/moa/app/data/quiz/datasource/QuizDataSource.kt, data/src/main/kotlin/com/moa/app/data/quiz/datasourceImpl/QuizDataSourceImpl.kt, data/src/main/kotlin/com/moa/app/data/quiz/repositoryImpl/QuizRepositoryImpl.kt
Retrofit 서비스, 응답 DTO + toDomain 매퍼, QuizDataSource 인터페이스 및 구현, QuizRepositoryImpl 추가
DI 및 의존성
data/src/main/kotlin/com/moa/app/data/di/ServiceModule.kt, data/src/main/kotlin/com/moa/app/data/di/DataSourceModule.kt, data/src/main/kotlin/com/moa/app/data/di/RepositoryModule.kt, data/build.gradle.kts
QuizService 제공자 추가, QuizDataSource/QuizRepository 바인딩 추가, kotlinx.collections.immutable 의존성 추가
UI: 지남력 퀴즈 전환(Orientation→Persistence)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt, feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizViewModel.kt, feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/orientation/OrientationQuizViewModel.kt
OrientationQuiz 삭제 및 PersistenceQuizScreen/PersistenceQuizViewModel 추가 — 백엔드 로드, 선택/검증/결과 대화상자, 종료 확인 흐름 구현
카테고리 UI 리팩토링
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/category/QuizCategoryScreen.kt, .../QuizCategoryViewModel.kt, .../QuizCategoryCard.kt, .../model/QuizCategoryExtension.kt, .../model/QuizCategoryUiState.kt, .../model/QuizCategorySideEffect.kt, .../QuizCategory.kt
QuizCategory 파일 제거(기존 기능형 열거) 및 확장프로퍼티로 UI 매핑 이동, ViewModel에 StateFlow/SideEffect 추가, 카드에 isEnabled 처리 및 토스트/백버튼 처리 등 상태·사이드이펙트 도입
디자인 시스템 소소 변경
core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/product/dialog/MaAlertDialog.kt
선택적 onDialogDismissRequest: (() -> Unit)? 파라미터 추가 및 우선 사용 로직 도입
결과 대화상자 리팩토링
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizResultDialog.kt
상태 기반 API 제거 → 명시적 파라미터(isCorrect, correctAnswer) 기반으로 단순화

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: 사용자 선택/정답확인/결과대화상자 표시 흐름
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

주의가 필요한 영역:

  • 데이터 계층(QuizService → DataSource → Repository → UseCase) 전체 연쇄 검증
  • PersistenceQuizViewModel의 로직(타이밍, dialog/exit 흐름)과 UI 바인딩
  • 기존 Quiz/Quizzes/Orientation 관련 삭제가 미치는 영향(참조 누락)
  • DI 바인딩(Service/DataSource/Repository) 및 gradle 의존성 추가 확인
  • QuizCategory 리팩토링으로 인한 리소스(iconRes) / 문자열 매핑 일관성

Possibly related PRs

Poem

🐰 햇살에 깃든 퀴즈 한 송이,
백엔드 샘에서 답이 흘러오네.
도메인 씨앗 뿌리고 리포지토리 길 닦아,
선택과 검증의 춤을 추며,
새 퀴즈 숲이 피어났네! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 지남력 퀴즈 비즈니스 로직 구현 및 API 연동이라는 주요 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 요구되는 템플릿 구조를 따르고 있으며, 관련 이슈, 작업 내용, 스크린샷, 미완료 작업이 모두 포함되어 있습니다.
Linked Issues check ✅ Passed PR의 모든 코드 변경사항이 #37의 지남력 퀴즈 비즈니스 로직 구현 요구사항을 충족합니다. 도메인 모델, 레포지토리, 유스케이스, 뷰모델, UI 화면 등 전체 계층이 일관되게 구현되었습니다.
Out of Scope Changes check ✅ Passed OrientationQuizScreen에서 PersistenceQuizScreen으로의 이름 변경과 기존 OrientationQuiz 관련 코드 제거는 #37의 지남력 퀴즈 구현을 위해 필요한 범위 내 변경사항입니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/37-quiz-logic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 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 기준 현재 그리기 순서는:

  1. Image (zIndex(1f))
  2. (비활성 시) 회색 Box 오버레이 (zIndex 기본 0)
  3. 제목/설명 Text (zIndex 기본 0)

Compose 에서는 더 높은 zIndex 를 가진 컴포저블이 마지막에 그려지므로, Image 가 텍스트와 오버레이 위에 올라와 텍스트가 가려질 수 있습니다.

의도하신 레이어링이 “이미지 → (옵션) 회색 오버레이 → 텍스트” 라면, ImagezIndex(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: 데이터 소스 구현은 단순·명확하며, 타입 안정성은 추후 확장 여지가 있습니다

QuizDataSourceImplQuizService 호출을 그대로 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 구조가 단순하고 의도가 잘 드러납니다

enabledCategoriesSet으로 둬서 중복을 막는 점이 적절합니다. 기본값이 항상 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 설계는 단순하지만 타입 안정성 측면에서 여지가 있습니다

현재는 백엔드 스펙에 맞춰 typeString으로 직접 받도록 정의되어 있어 구현이 직관적입니다. 다만 레이어 경계에서 도메인 타입(예: 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: Rename getCurrentAnswerIndex to better reflect its actual behavior.

The method at domain/src/main/kotlin/com/moa/app/domain/quiz/model/PersistenceQuiz.kt:13 returns Boolean, not an index. The name getCurrentAnswerIndex is misleading since it actually checks if the selected answer is correct. Consider renaming it to isAnswerCorrect or checkAnswerCorrect for clarity.

Current implementation:

fun getCurrentAnswerIndex(selectedAnswerIndex: Int): Boolean {
    return selectedAnswerIndex == answerOptions.indexOf(answer)
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65bc428 and c4b41fb.

📒 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 바인딩이 기존 패턴과 잘 맞습니다

ServiceModuleQuizService@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!

showResultDialogdialogResult를 함께 확인하여 결과 다이얼로그를 조건부로 표시하는 로직이 적절합니다. 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 로직을 깔끔하게 유지하는 좋은 방법입니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ 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

📥 Commits

Reviewing files that changed from the base of the PR and between c4b41fb and 4ae23d1.

📒 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: 정답 선택 시 결과 다이얼로그 노출 중에는 상태 변경을 막는 처리 좋습니다.

showResultDialogtrue 인 상태에서는 선택을 무시하여, 결과 다이얼로그가 떠 있는 동안 사용자가 보기를 다시 탭해도 상태가 꼬이지 않도록 잘 방어하고 있습니다. 실제 UI 에서 계속/확인 버튼 활성화 조건과도 일관되게 동작하는지만 한 번 더 확인해 주세요.


93-125: 다음 문제/마지막 문제 분기 및 다이얼로그 종료 타이밍이 명확합니다.

현재 상태 스냅샷으로 nextQuestionIndexisLastQuestion 를 계산한 뒤, 마지막 문제에서는 결과 다이얼로그를 숨기고 짧은 지연 후 exitQuiz() 를 호출하는 흐름, 그 외에는 다음 문제로 넘어가며 선택/다이얼로그 상태를 초기화하는 흐름이 깔끔합니다. 실제 단말에서 “마지막 문제 → 결과 다이얼로그 → 화면 종료” 동작이 의도한 타이밍으로 보이는지만 한 번 더 확인해 주세요.


127-147: 종료 다이얼로그 상태 토글과 네비게이션 분리가 잘 되어 있습니다.

exitDialog 플래그로 다이얼로그 노출 여부만 관리하고, 실제 화면 종료는 exitQuiz() 한 곳에서 navigator.navigateBack() 으로 처리해서 책임이 잘 분리되어 있습니다. 시스템 백 버튼과 UI 상의 뒤로 버튼이 모두 이 ViewModel 경로를 타는지, Activity/Fragment 레벨에서 별도 처리되는 경로는 없는지만 한 번 점검해 두면 좋겠습니다.

@wjdrjs00 wjdrjs00 merged commit d3f312d into develop Nov 27, 2025
4 checks passed
@wjdrjs00 wjdrjs00 deleted the feature/37-quiz-logic branch November 27, 2025 08:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 단일 퀴즈 비즈니스 로직을 구현합니다.

2 participants