Skip to content

Conversation

@wjdrjs00
Copy link
Collaborator

@wjdrjs00 wjdrjs00 commented Jan 12, 2026

Related issue 🛠

Work Description ✏️

  • Foldable 기기를 고려하여 Quiz Content UX를 개선했습니다.

Screenshot 📸

  • N/A

Uncompleted Tasks 😅

  • 현재는 Quiz Content 영역만 반영 -> 추후 디자인파트와 논의하여 앱 전역에 대응하기

Summary by CodeRabbit

  • 새로운 기능

    • 뒤로가기 버튼 더블 탭으로 앱 종료 기능 추가
    • 폴더블 기기(갤럭시 Z Fold 등) 전용 프리뷰 지원 추가
  • 버그 수정

    • 회원가입 페이지 성별 선택 버튼 순서 수정
  • 개선

    • 퀴즈 화면 레이아웃 재정비 및 반응형/정렬 개선
    • 클릭 리플 피드백 제거로 터치 경험 간소화
    • 미리보기 이미지 처리 개선 및 프리뷰 안정성 향상

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

@wjdrjs00 wjdrjs00 self-assigned this Jan 12, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 12, 2026

Walkthrough

새로운 core/ui 모듈이 추가되어 UI 유틸리티(더블 백 종료 컴포저블, clickableWithoutRipple·quizMaxWidth 모디파이어, FoldablePreviews)들이 도입되었고 여러 피처 모듈이 이를 의존하도록 업데이트되었습니다. 다수 퀴즈 화면이 레이아웃/컴포넌트로 리팩토링되었습니다.

Changes

Cohort / File(s) 변경 요약
Core UI 모듈 신규 생성
core/ui/build.gradle.kts, core/ui/.gitignore, core/ui/proguard-rules.pro, core/ui/src/main/AndroidManifest.xml, core/ui/src/main/java/com/moa/app/ui/TerminateOnDoubleBack.kt, core/ui/src/main/java/com/moa/app/ui/extension/Modifier.kt, core/ui/src/main/java/com/moa/app/ui/preview/FoldablePreviews.kt
새로운 모듈 추가: TerminateOnDoubleBack 컴포저블, clickableWithoutRipple, quizMaxWidth, FoldablePreviews 어노테이션 및 관련 빌드/매니페스트/프리뷰 파일
프로젝트 설정·의존성 업데이트
settings.gradle.kts, app/build.gradle.kts, core/designsystem/build.gradle.kts, feature/guardian/build.gradle.kts, feature/senior/build.gradle.kts
projects.core.ui 의존성 추가 및 모듈 포함 설정
백버튼 처리 중앙화
feature/guardian/.../GuardianHomeScreen.kt, feature/senior/.../SeniorHomeScreen.kt
기존 커스텀 BackPressHandler 제거 후 TerminateOnDoubleBack() 사용으로 통일
TopBar 및 클릭 동작 표준화
core/designsystem/.../MaHomeTopBar.kt, feature/senior/.../QuizDescription.kt
clickableclickableWithoutRipple 대체, TopQuizDescription에 alignImageEnd 파라미터 추가 및 레이아웃/클립·배경 처리 정리
퀴즈 화면 레이아웃·컴포넌트 리팩토링
여러 파일 under feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/... (e.g., AttentionQuizScreen.kt, DailyQuizScreen.kt, LinguisticQuizScreen.kt, MemoryQuizScreen.kt, PersistenceQuizScreen.kt, SpaceTimeQuizScreen.kt, 관련 quizform/컴포넌트들)
quizMaxWidth 적용, 중앙 정렬, FoldablePreviews 적용, Coil preview 핸들러 추가, 여러 화면에서 전용 QuizForm 컴포넌트로 위임 및 레이아웃 조정
온보딩 성별 선택 로직 변경
feature/onboarding/.../SignUpProfileScreen.kt
MaSelectButton 매핑 변경: 왼쪽 버튼이 남성(MALE), 오른쪽 버튼이 여성(FEMALE)으로 스왑

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Activity
    participant TerminateOnDoubleBack
    participant BackHandler
    participant Toast
    participant LaunchedEffect

    User->>Activity: 뒤로 버튼 누름 (1)
    Activity->>TerminateOnDoubleBack: Back event 전달
    TerminateOnDoubleBack->>BackHandler: 첫번째 처리 (상태 설정)
    BackHandler->>Toast: 토스트 표시 (메시지)
    TerminateOnDoubleBack->>LaunchedEffect: backPressedOnce 타이머 시작

    User->>Activity: 뒤로 버튼 누름 (2) (within delay)
    Activity->>TerminateOnDoubleBack: Back event 전달
    TerminateOnDoubleBack->>BackHandler: 두번째 처리 (finish 호출)
    BackHandler->>Activity: finish() 호출
    Activity->>User: 앱 종료

    Note over LaunchedEffect: delay 경과 시 backPressedOnce 초기화
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Poem

🐰
core/ui 깃털처럼 모아왔네,
클릭은 살짝 부드럽게, 리플은 숨기고,
뒤로 한 번은 경고, 두 번은 안녕이라네.
접는 화면도 미리 보고, 퀴즈는 가운데로—
당근 하나 들고 배포하러 뛰어갈게요! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경 사항을 명확하게 반영하고 있습니다: '[Feature/#77] Quiz Content UX를 개선합니다.' - Foldable 기기 대응 및 Quiz UX 개선이라는 핵심 내용을 잘 전달합니다.
Description check ✅ Passed PR 설명이 저장소의 필수 템플릿을 따르고 있으며, 관련 이슈(#77), 작업 내용, 적용 범위, 미완료 항목을 모두 포함하고 있습니다.
Linked Issues check ✅ Passed 이슈 #77의 목표인 Foldable 기기 대응 및 core:ui 모듈 생성을 완벽하게 충족하고 있습니다. 새로운 core/ui 모듈 생성, FoldablePreviews 어노테이션, 공통 Modifier 확장 함수 추가 등이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 이슈 #77의 범위 내에 있습니다. core:ui 모듈 생성, Quiz Content 관련 레이아웃 개선, Foldable 기기 대응, 공통 UI 코드 재사용화 등 모두 명시된 목표와 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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/QuizDescription.kt (1)

50-56: alignImageEnd = true일 때 패딩 방향 확인 필요

padding(start = 44.dp)가 항상 적용되어, alignImageEnd = true로 이미지가 오른쪽 정렬될 때도 왼쪽에서 44.dp 패딩이 유지됩니다. 의도된 동작인지 확인이 필요합니다.

대칭적인 디자인을 원한다면 아래와 같이 조건부 패딩을 고려해 주세요:

💡 조건부 패딩 적용 제안
         Image(
             painter = painterResource(R.drawable.img_quiz_character_top),
             contentDescription = null,
             modifier = Modifier
-                .padding(start = 44.dp)
+                .padding(
+                    start = if (alignImageEnd) 0.dp else 44.dp,
+                    end = if (alignImageEnd) 44.dp else 0.dp
+                )
                 .align(alignment = alignment)
         )
🤖 Fix all issues with AI agents
In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/daily/DailyQuizScreen.kt:
- Around line 187-202: The "Continue" button is shown when uiState.currentQuiz
is null because the condition uses "uiState.currentQuiz !is MemoryQuiz" which is
true for null; update the check to ensure currentQuiz is non-null and not a
MemoryQuiz before rendering MaButton (e.g., use "uiState.currentQuiz != null &&
uiState.currentQuiz !is MemoryQuiz" or "uiState.currentQuiz?.let { it !is
MemoryQuiz } == true") and keep the existing properties (onContinueClick,
enabled, modifiers, Text) unchanged so the button only appears when a quiz is
loaded and it's not a MemoryQuiz.

In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt:
- Around line 116-119: Add an isContinueButtonEnabled Boolean to
LinguisticQuizUiState and use it instead of the hardcoded enabled = true in
LinguisticQuizScreen's MaButton (replace enabled = true with enabled =
uiState.isContinueButtonEnabled). Update the state initialization and the
answer-selection logic (e.g., the handler that marks an option selected in your
ViewModel/Composables) to set isContinueButtonEnabled = true when a valid answer
is chosen and false otherwise, following the same pattern used by
AttentionQuizScreen/DailyQuizScreen/SpaceTimeQuizScreen/PersistenceQuizScreen.

In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizTextModeContent.kt:
- Line 36: The code defines a fixed-size val labels = listOf("첫번째", "두번째",
"세번째") but later indexes it using the answers list (labels[index]), risking
IndexOutOfBounds when answers.size > 3; update MemoryQuizTextModeContent so you
either (a) guard when accessing labels by using labels.getOrNull(index) ?:
fallbackLabel (or compute a label from index) wherever labels[index] is used, or
(b) enforce/validate that answers.size <= labels.size before mapping (throw or
truncate) so callers cannot pass more than three answers—locate the labels
declaration and all usages of labels[index] (the mapping logic around answers)
and apply one of these fixes consistently.

In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizScreen.kt:
- Around line 147-155: The preview data uses the wrong quiz category: change the
SpaceTimeQuiz instance's type property from QuizCategory.ATTENTION to
QuizCategory.SPACETIME so the preview reflects the correct category; locate the
SpaceTimeQuiz constructor call (id = 1, questionImageUrl = "", imageOptionsUrl =
persistentListOf("", ""), type = QuizCategory.ATTENTION, ...) and replace the
type value with QuizCategory.SPACETIME.
🧹 Nitpick comments (5)
core/ui/src/main/java/com/moa/app/ui/extension/Modifier.kt (1)

25-25: 매직 넘버 428.dp에 대한 문서화 권장

quizMaxWidth()의 최대 너비 값이 하드코딩되어 있습니다. 이 값의 의도(예: 특정 foldable 기기 기준 또는 디자인 가이드라인)를 명확히 하기 위해 상수로 추출하거나 KDoc 주석을 추가하는 것을 권장합니다.

♻️ 상수 추출 예시
+private val QuizMaxWidthDp = 428.dp
+
-fun Modifier.quizMaxWidth(): Modifier = this.then(Modifier.widthIn(max = 428.dp))
+/**
+ * Quiz 콘텐츠의 최대 너비를 제한합니다.
+ * Foldable 기기에서 콘텐츠가 과도하게 넓어지는 것을 방지합니다.
+ */
+fun Modifier.quizMaxWidth(): Modifier = this.then(Modifier.widthIn(max = QuizMaxWidthDp))
feature/guardian/src/main/kotlin/com/moa/app/feature/guardian/home/GuardianHomeScreen.kt (1)

105-110: clickableWithoutRipple 사용 고려

이 파일에서 ripple 없이 클릭 처리를 하는 곳이 여러 군데 있습니다. MaHomeTopBar에서 사용한 것처럼 clickableWithoutRipple 확장 함수를 사용하면 코드 일관성이 향상됩니다.

♻️ 리팩토링 예시
 Image(
     imageVector = ImageVector.vectorResource(R.drawable.ic_bell),
     contentDescription = null,
     modifier = Modifier
-        .clickable(
-            onClick = onAlertClick,
-            role = Role.Button,
-            interactionSource = null,
-            indication = null
-        )
+        .clickableWithoutRipple(onClick = onAlertClick, role = Role.Button)
         .padding(8.dp)
 )
feature/senior/src/main/kotlin/com/moa/app/feature/senior/home/SeniorHomeScreen.kt (1)

219-222: 클릭 처리 방식 불일치

Line 173-178의 "퀴즈" 박스는 ripple()을 명시적으로 사용하지만, "리포트" 박스는 기본 indication을 사용합니다. 두 박스의 시각적 피드백이 동일해야 한다면 일관된 방식으로 통일하는 것이 좋습니다.

♻️ 일관성 개선 예시
 Box(
     modifier = Modifier
         .weight(1f)
         .clip(RoundedCornerShape(24.dp))
         .clickable(
+            interactionSource = remember { MutableInteractionSource() },
+            indication = ripple(),
             onClick = onReportClick,
             role = Role.Button,
         )
         .background(MoaTheme.colors.orange500)
         .padding(start = 16.dp)
 )
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/SpaceTimeQuizForm.kt (1)

84-87: Preview 코드에서 non-null assertion(!!) 사용에 주의하세요.

ContextCompat.getDrawable()null을 반환할 수 있으나, 디자인 시스템의 리소스이므로 Preview 컨텍스트에서는 허용 가능합니다. 다만, 여러 Preview 파일에서 동일한 패턴이 반복되므로 공통 유틸리티로 추출하는 것을 고려해 보세요.

♻️ 공통 Preview Handler 유틸리티 제안

core/ui 모듈에 공통 Preview Handler를 추가하면 중복을 줄일 수 있습니다:

// core/ui/.../preview/DefaultAsyncImagePreviewHandler.kt
@OptIn(ExperimentalCoilApi::class)
fun createDefaultPreviewHandler(@DrawableRes drawableRes: Int): AsyncImagePreviewHandler {
    return AsyncImagePreviewHandler { request ->
        ContextCompat.getDrawable(request.context, drawableRes)?.asImage()
            ?: error("Drawable resource not found")
    }
}
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (1)

139-147: Preview 함수 네이밍 일관성 개선 제안

다른 Preview 함수들(PreviewTopQuizDescription, PreviewBottomQuizDescription)과 달리 이 함수만 Preview()로 명명되어 있습니다. 일관성을 위해 PreviewCenterQuizDescription으로 변경을 권장합니다.

♻️ 네이밍 변경 제안
 @Preview(showBackground = true)
 @PreviewFontScale
 @Composable
-private fun Preview() {
+private fun PreviewCenterQuizDescription() {
     CenterQuizDescription(
         quizDescription = "아래의 그림은\n무엇일까요?",
         onImageClick = {},
     )
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf677f and aebc8bc.

📒 Files selected for processing (30)
  • app/build.gradle.kts
  • core/designsystem/build.gradle.kts
  • core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/product/topbar/MaHomeTopBar.kt
  • core/ui/.gitignore
  • core/ui/build.gradle.kts
  • core/ui/consumer-rules.pro
  • core/ui/proguard-rules.pro
  • core/ui/src/main/AndroidManifest.xml
  • core/ui/src/main/java/com/moa/app/ui/TerminateOnDoubleBack.kt
  • core/ui/src/main/java/com/moa/app/ui/extension/Modifier.kt
  • core/ui/src/main/java/com/moa/app/ui/preview/FoldablePreviews.kt
  • feature/guardian/build.gradle.kts
  • feature/guardian/src/main/kotlin/com/moa/app/feature/guardian/home/GuardianHomeScreen.kt
  • feature/onboarding/src/main/kotlin/com/moa/app/feature/onboarding/signup/SignUpProfileScreen.kt
  • feature/senior/build.gradle.kts
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/home/SeniorHomeScreen.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/attention/AttentionQuizScreen.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizLoadContent.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/AttentionQuizForm.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/LinguisticQuizForm.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/PersistenceQuizForm.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/SpaceTimeQuizForm.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/daily/DailyQuizScreen.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/MemoryQuizScreen.kt
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizTextModeContent.kt
  • 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/spacetime/SpaceTimeQuizScreen.kt
  • settings.gradle.kts
💤 Files with no reviewable changes (1)
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/AttentionQuizForm.kt
🧰 Additional context used
🧬 Code graph analysis (7)
feature/onboarding/src/main/kotlin/com/moa/app/feature/onboarding/signup/SignUpProfileScreen.kt (1)
core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/core/button/MaSelectButton.kt (1)
  • MaSelectButton (30-64)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizScreen.kt (3)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizSlideAnimation.kt (1)
  • QuizSlideAnimation (18-43)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/SpaceTimeQuizForm.kt (1)
  • SpaceTimeQuizForm (29-78)
core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/core/button/MaButton.kt (1)
  • MaButton (30-68)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/attention/AttentionQuizScreen.kt (3)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizSlideAnimation.kt (1)
  • QuizSlideAnimation (18-43)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/AttentionQuizForm.kt (1)
  • AttentionQuizForm (14-40)
core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/core/button/MaButton.kt (1)
  • MaButton (30-68)
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/QuizSlideAnimation.kt (1)
  • QuizSlideAnimation (18-43)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/PersistenceQuizForm.kt (1)
  • PersistenceQuizForm (18-54)
core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/core/button/MaButton.kt (1)
  • MaButton (30-68)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/home/SeniorHomeScreen.kt (1)
core/ui/src/main/java/com/moa/app/ui/TerminateOnDoubleBack.kt (1)
  • TerminateOnDoubleBack (15-40)
feature/guardian/src/main/kotlin/com/moa/app/feature/guardian/home/GuardianHomeScreen.kt (1)
core/ui/src/main/java/com/moa/app/ui/TerminateOnDoubleBack.kt (1)
  • TerminateOnDoubleBack (15-40)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/PersistenceQuizForm.kt (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (1)
  • TopQuizDescription (26-58)
⏰ 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 ktlint
  • GitHub Check: Run Unit Tests
🔇 Additional comments (36)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizTextModeContent.kt (1)

39-39: LGTM!

Foldable 기기 지원을 위한 수평 패딩 추가가 적절합니다. AI 요약에 따르면 상위 컨테이너에서 quizMaxWidth()와 중앙 정렬이 적용되므로, 이 패딩이 내부 콘텐츠 여백으로 잘 작동할 것입니다.

core/ui/.gitignore (1)

1-1: LGTM!

새로운 core:ui 모듈에 대한 표준 .gitignore 항목입니다.

core/ui/src/main/AndroidManifest.xml (1)

1-4: LGTM!

Android 라이브러리 모듈에 필요한 최소한의 매니페스트 파일입니다. 네임스페이스는 build.gradle.kts에서 정의되므로 적절합니다.

settings.gradle.kts (1)

39-39: LGTM!

core:ui 모듈이 프로젝트 구조에 올바르게 포함되었습니다. 다른 core 모듈들과 일관된 위치에 배치되어 있습니다.

core/ui/build.gradle.kts (1)

1-13: LGTM!

core:ui 모듈의 Gradle 설정이 적절합니다. 커스텀 플러그인을 통한 라이브러리 및 Compose 설정, 명확한 네임스페이스 정의, 그리고 최소한의 필수 의존성만 포함하고 있습니다.

core/ui/proguard-rules.pro (1)

1-21: LGTM!

표준 ProGuard 템플릿 파일입니다. 현재는 활성화된 규칙이 없으며, 필요 시 추후 커스터마이징할 수 있습니다.

feature/onboarding/src/main/kotlin/com/moa/app/feature/onboarding/signup/SignUpProfileScreen.kt (1)

224-246: LGTM!

성별 버튼 순서 변경이 올바르게 구현되었습니다. 각 버튼의 onClick, selected 상태, 그리고 표시 텍스트가 일관되게 매핑되어 있습니다. 왼쪽에 남성, 오른쪽에 여성 버튼을 배치하는 것은 한국어 UI에서 일반적인 패턴입니다.

core/ui/src/main/java/com/moa/app/ui/preview/FoldablePreviews.kt (1)

5-15: Foldable 디바이스 프리뷰 어노테이션이 정확하게 구현되었습니다.

Multi-preview 어노테이션 패턴을 사용하여 Z Fold 6의 펼침/접힘 상태를 한 번에 프리뷰할 수 있어 개발 효율성이 향상됩니다. 디바이스 스펙이 Samsung Galaxy Z Fold 6의 공식 사양과 정확히 일치합니다:

  • 펼침 상태 (메인 디스플레이): 795dp × 925dp, 374 dpi
  • 접힘 상태 (커버 디스플레이): 378dp × 927dp, 410 dpi
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/attention/AttentionQuizScreen.kt (2)

97-111: currentQuiz가 null일 때 빈 공간만 남는 점 확인 필요

currentQuiz가 null일 경우 QuizSlideAnimationAttentionQuizForm이 렌더링되지 않아 빈 공간만 남게 됩니다. 이 상태에서 MaButton은 여전히 표시되지만, 사용자 입장에서 어떤 퀴즈도 없는 상태로 "계속" 버튼이 보이는 UX가 적절한지 확인해 주세요.


128-150: Foldable 기기 대응 Preview 추가 👍

@FoldablePreviews 어노테이션을 활용하여 접이식 기기에서의 레이아웃을 미리 확인할 수 있도록 개선되었습니다.

core/ui/src/main/java/com/moa/app/ui/extension/Modifier.kt (1)

10-23: clickableWithoutRipple 확장 함수가 잘 구현되었습니다.

ripple 효과 없이 클릭 가능한 modifier를 중앙화하여 코드 중복을 줄였습니다. indication = null과 함께 interactionSource를 nullable로 처리하는 방식은 Compose 1.6+ API와 호환됩니다.

core/designsystem/src/main/kotlin/com/moa/app/designsystem/component/product/topbar/MaHomeTopBar.kt (1)

53-54: clickableWithoutRipple 적용 완료

설정 아이콘의 클릭 처리가 새로운 공통 유틸리티 함수로 마이그레이션되었습니다. Role.Button이 적절히 전달되어 접근성이 유지됩니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/MemoryQuizScreen.kt (1)

97-143: quizMaxWidth() 적용으로 Foldable 기기 대응 완료

다른 퀴즈 화면(AttentionQuizScreen 등)과 일관된 패턴으로 quizMaxWidth()CenterHorizontally 정렬이 적용되었습니다. 콘텐츠 너비 제한으로 접이식 기기에서의 UX가 개선됩니다.

core/ui/src/main/java/com/moa/app/ui/TerminateOnDoubleBack.kt (2)

25-30: LaunchedEffect 키 사용 패턴 확인

backPressedOncetrue로 변경될 때마다 LaunchedEffect가 재시작됩니다. 사용자가 딜레이 내에 연속으로 백 버튼을 누르면 코루틴이 취소되고 재시작되어 의도한 대로 동작합니다.

다만, backPressedOncefalse로 리셋될 때도 LaunchedEffect가 재실행되는데, 조건문으로 인해 실제 동작은 없으므로 문제없습니다.


15-39: 공통 컴포저블로 뒤로가기 종료 로직 중앙화 👍

TerminateOnDoubleBack 컴포저블이 여러 화면에서 재사용될 수 있도록 잘 설계되었습니다. enabled, delayMillis, message 파라미터를 통해 유연성을 제공합니다.

향후 다국어 지원을 고려한다면 message 기본값을 string resource로 변경하는 것을 권장합니다.

feature/guardian/src/main/kotlin/com/moa/app/feature/guardian/home/GuardianHomeScreen.kt (1)

48-48: TerminateOnDoubleBack 적용 완료

기존의 커스텀 BackPressHandler 로직이 중앙화된 TerminateOnDoubleBack 컴포저블로 대체되어 코드 중복이 제거되었습니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/home/SeniorHomeScreen.kt (1)

55-55: TerminateOnDoubleBack 적용 완료

Senior 홈 화면에서도 중앙화된 뒤로가기 종료 로직이 적용되어 Guardian 홈 화면과 일관된 동작을 보장합니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/daily/DailyQuizScreen.kt (2)

110-116: Foldable 디바이스 지원을 위한 레이아웃 구조 개선이 잘 되었습니다.

quizMaxWidth() 확장 함수와 CenterHorizontally 정렬을 활용하여 Foldable 기기에서 콘텐츠가 중앙에 적절한 너비로 표시되도록 구조화되었습니다.


118-185: 이 리뷰 의견은 잘못되었습니다. Quiz는 sealed interface이므로 현재 when 분기는 이미 exhaustive하며, else 처리가 필요하지 않습니다.

sealed interface에서는 컴파일러가 모든 구현 타입을 추적하기 때문에, 5가지 퀴즈 타입(PersistenceQuiz, LinguisticQuiz, MemoryQuiz, AttentionQuiz, SpaceTimeQuiz)을 모두 처리한 현재 코드가 완전합니다.

Likely an incorrect or invalid review comment.

feature/guardian/build.gradle.kts (1)

14-14: core.ui 모듈 의존성 추가가 적절합니다.

Guardian 모듈에서 core.ui 유틸리티(TerminateOnDoubleBack, clickableWithoutRipple 등)를 활용할 수 있도록 의존성이 추가되었습니다.

app/build.gradle.kts (1)

82-82: core.ui 의존성 추가가 적절합니다.

앱 모듈에서 core.ui 유틸리티를 직접 사용할 수 있도록 명시적으로 의존성이 추가되었습니다. core.designsystem을 통해 전이적으로도 의존하지만, 명시적 선언은 의도를 명확히 합니다.

feature/senior/build.gradle.kts (1)

14-14: core.ui 의존성이 Senior 모듈 리팩토링에 필요합니다.

quizMaxWidth, clickableWithoutRipple, TerminateOnDoubleBack 등 core.ui 유틸리티가 Senior 퀴즈 화면에서 사용되므로 의존성 추가가 적절합니다.

core/designsystem/build.gradle.kts (1)

11-11: 순환 의존성 확인 완료 - 문제 없음

core.ui 모듈이 core.designsystem을 의존하지 않으므로 순환 의존성이 발생하지 않습니다. 의존성 방향이 한 방향(designsystemui)으로만 설정되어 있어 빌드 오류의 위험이 없습니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizLoadContent.kt (3)

48-55: 레이아웃 수정이 잘 적용되었습니다.

quizMaxWidth() 적용 및 Alignment.Center 정렬을 통해 Foldable 기기에서의 UX가 개선되었습니다. modifier 체인 순서(quizMaxWidth()fillMaxSize()align())도 적절합니다.


62-68: clip과 background 순서가 올바릅니다.

clip(RoundedCornerShape(32.dp)) 이후 background(MoaTheme.colors.white) 순서로 적용하여 라운드 코너가 정상적으로 적용됩니다.


89-96: Preview 함수에 @FoldablePreviews 추가 승인.

Foldable 기기 프리뷰 지원이 PR 목표에 부합합니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/SpaceTimeQuizForm.kt (1)

37-37: 레이아웃 수정 승인.

외부에서 전달받은 modifier를 직접 사용하도록 변경하여 재사용성이 향상되었습니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/PersistenceQuizForm.kt (1)

26-30: TopQuizDescription 컴포넌트로의 리팩토링 승인.

기존 BoxImage 조합을 재사용 가능한 TopQuizDescription 컴포넌트로 대체하여 코드 중복이 줄었습니다. alignImageEnd = true 설정으로 기존 동작을 유지합니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/quizform/LinguisticQuizForm.kt (2)

38-38: 외부 modifier 직접 사용으로 유연성 향상.

하드코딩된 horizontal padding을 제거하고 외부에서 전달받은 modifier를 직접 사용하여 호출처에서 레이아웃을 제어할 수 있게 되었습니다.


51-51: 유연한 레이아웃을 위한 weight(1f) 사용 승인.

고정 높이 대신 weight(1f)를 사용하여 다양한 화면 크기에서 적응형 레이아웃이 가능해졌습니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt (1)

95-127: 레이아웃 구조 개선 승인.

quizMaxWidth()Alignment.CenterHorizontally를 적용하여 Foldable 기기에서의 UX가 개선되었습니다. LinguisticQuizForm으로의 위임도 적절합니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizScreen.kt (1)

97-129: 레이아웃 및 SpaceTimeQuizForm 위임 승인.

quizMaxWidth()와 중앙 정렬 적용, SpaceTimeQuizForm으로의 위임이 올바르게 구현되었습니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/persistence/PersistenceQuizScreen.kt (2)

87-119: 레이아웃 구조 및 QuizSlideAnimation 적용 승인.

quizMaxWidth()와 중앙 정렬, QuizSlideAnimation 내부에서 PersistenceQuizForm 렌더링이 올바르게 구현되었습니다. MaButtonenabled 상태도 uiState.isContinueButtonEnabled를 사용하여 일관성이 있습니다.


123-143: Preview 개선 승인.

@FoldablePreviews 추가 및 간소화된 샘플 데이터로 Preview가 개선되었습니다.

feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/component/QuizDescription.kt (2)

66-93: LGTM!

변경 사항이 적절합니다:

  • verticalAlignment = Alignment.CenterVertically로 Row 내 수직 정렬 개선
  • clip()background() 순서로 올바른 모디파이어 체이닝
  • weight(1f)defaultMinSize로 유연한 레이아웃 구성

96-128: LGTM!

clip()background() 순서 적용 및 clickableWithoutRipple 사용이 다른 컴포넌트와 일관성 있게 적용되었습니다.

Comment on lines +187 to 202
if (uiState.currentQuiz !is MemoryQuiz) {
MaButton(
onClick = onContinueClick,
enabled = uiState.isContinueButtonEnabled,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.padding(bottom = 12.dp),
) {
Text(
text = "계속",
style = MoaTheme.typography.body1Bold,
modifier = Modifier.padding(vertical = 16.dp, horizontal = 20.dp),
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

currentQuiz가 null일 때 버튼 표시 여부를 확인하세요.

Line 118에서 currentQuiz?.let으로 null 체크를 하지만, Line 187의 조건문 uiState.currentQuiz !is MemoryQuizcurrentQuiz가 null일 때도 true를 반환합니다. 따라서 퀴즈가 로드되지 않은 상태에서도 "계속" 버튼이 표시될 수 있습니다.

제안된 수정
-            if (uiState.currentQuiz !is MemoryQuiz) {
+            if (uiState.currentQuiz != null && uiState.currentQuiz !is MemoryQuiz) {
📝 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.

Suggested change
if (uiState.currentQuiz !is MemoryQuiz) {
MaButton(
onClick = onContinueClick,
enabled = uiState.isContinueButtonEnabled,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.padding(bottom = 12.dp),
) {
Text(
text = "계속",
style = MoaTheme.typography.body1Bold,
modifier = Modifier.padding(vertical = 16.dp, horizontal = 20.dp),
)
}
}
if (uiState.currentQuiz != null && uiState.currentQuiz !is MemoryQuiz) {
MaButton(
onClick = onContinueClick,
enabled = uiState.isContinueButtonEnabled,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.padding(bottom = 12.dp),
) {
Text(
text = "계속",
style = MoaTheme.typography.body1Bold,
modifier = Modifier.padding(vertical = 16.dp, horizontal = 20.dp),
)
}
}
🤖 Prompt for AI Agents
In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/daily/DailyQuizScreen.kt
around lines 187 - 202, The "Continue" button is shown when uiState.currentQuiz
is null because the condition uses "uiState.currentQuiz !is MemoryQuiz" which is
true for null; update the check to ensure currentQuiz is non-null and not a
MemoryQuiz before rendering MaButton (e.g., use "uiState.currentQuiz != null &&
uiState.currentQuiz !is MemoryQuiz" or "uiState.currentQuiz?.let { it !is
MemoryQuiz } == true") and keep the existing properties (onContinueClick,
enabled, modifiers, Text) unchanged so the button only appears when a quiz is
loaded and it's not a MemoryQuiz.

Comment on lines +116 to +119
MaButton(
onClick = onContinueClick,
enabled = true,
modifier = Modifier.fillMaxWidth()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. Search for isContinueButtonEnabled in the linguistic module
echo "=== Searching for isContinueButtonEnabled in linguistic module ==="
rg -n "isContinueButtonEnabled" --type=kotlin -g "**/linguistic/**"

# 2. Check the LinguisticQuizUiState file
echo -e "\n=== Finding LinguisticQuizUiState file ==="
fd "LinguisticQuizUiState" --type=kotlin

# 3. Compare with other quiz screens
echo -e "\n=== Checking SpaceTimeQuizScreen for enabled property ==="
fd "SpaceTimeQuizScreen" --type=kotlin

echo -e "\n=== Checking PersistenceQuizScreen for enabled property ==="
fd "PersistenceQuizScreen" --type=kotlin

Repository: team-memory-care/MoA-Android

Length of output: 841


🏁 Script executed:

#!/bin/bash
# 1. Search for isContinueButtonEnabled across the codebase
echo "=== Searching for isContinueButtonEnabled ==="
rg "isContinueButtonEnabled" -A 2 -B 2

# 2. Find LinguisticQuizScreen.kt
echo -e "\n=== Finding LinguisticQuizScreen.kt ==="
find . -name "LinguisticQuizScreen.kt" -type f

# 3. Find LinguisticQuizUiState
echo -e "\n=== Finding LinguisticQuizUiState ==="
find . -name "*LinguisticQuiz*" -type f | grep -i uistate

# 4. Find all quiz screens
echo -e "\n=== Finding all quiz screens ==="
find . -name "*QuizScreen.kt" -type f | grep -E "(Linguistic|SpaceTime|Persistence)"

# 5. Check structure of quiz directory
echo -e "\n=== Quiz directory structure ==="
find . -path "*/quiz/*" -name "*.kt" | head -30

Repository: team-memory-care/MoA-Android

Length of output: 8560


🏁 Script executed:

#!/bin/bash
# Check if LinguisticQuizUiState has isContinueButtonEnabled
echo "=== LinguisticQuizUiState.kt content ==="
cat -n ./feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/model/LinguisticQuizUiState.kt

echo -e "\n=== LinguisticQuizScreen.kt lines 110-130 ==="
sed -n '110,130p' ./feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt | cat -n

Repository: team-memory-care/MoA-Android

Length of output: 2493


LinguisticQuizUiStateisContinueButtonEnabled 속성을 추가하고 하드코딩된 enabled = true 제거

LinguisticQuizUiState.kt에는 다른 퀴즈 화면들(AttentionQuizScreen, DailyQuizScreen, SpaceTimeQuizScreen, PersistenceQuizScreen)에서 사용하는 isContinueButtonEnabled 속성이 없습니다. LinguisticQuizScreen.kt 116-119줄에서 enabled = true로 하드코딩되어 있으므로, 사용자가 답을 선택하지 않았을 때에도 계속 버튼이 활성화됩니다. 다른 퀴즈들처럼 LinguisticQuizUiStateisContinueButtonEnabled 속성을 추가하고 이를 통해 선택된 답변 여부에 따라 버튼 활성화 상태를 제어해야 합니다.

🤖 Prompt for AI Agents
In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/linguistic/LinguisticQuizScreen.kt
around lines 116 - 119, Add an isContinueButtonEnabled Boolean to
LinguisticQuizUiState and use it instead of the hardcoded enabled = true in
LinguisticQuizScreen's MaButton (replace enabled = true with enabled =
uiState.isContinueButtonEnabled). Update the state initialization and the
answer-selection logic (e.g., the handler that marks an option selected in your
ViewModel/Composables) to set isContinueButtonEnabled = true when a valid answer
is chosen and false otherwise, following the same pattern used by
AttentionQuizScreen/DailyQuizScreen/SpaceTimeQuizScreen/PersistenceQuizScreen.

) {
val focusManager = LocalFocusManager.current
val focusRequesters = remember { List(answers.size) { FocusRequester() } }
val labels = listOf("첫번째", "두번째", "세번째")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

labels 리스트 접근 시 IndexOutOfBoundsException 위험

labels 리스트는 3개의 요소만 가지고 있으나, answers 리스트의 크기에 대한 제약이 없습니다. answers.size > 3인 경우 Line 66에서 labels[index] 접근 시 런타임 크래시가 발생합니다.

🐛 안전한 접근을 위한 수정 제안
-                        Text(
-                            text = "${labels[index]} 단어를 작성해주세요",
+                        Text(
+                            text = "${labels.getOrElse(index) { "${index + 1}번째" }} 단어를 작성해주세요",

또는 answers 크기를 3개로 제한하는 것이 비즈니스 로직상 맞다면, 호출부에서 명시적으로 제약을 두는 것을 권장합니다.

Also applies to: 64-70

🤖 Prompt for AI Agents
In
@feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/memory/component/MemoryQuizTextModeContent.kt
at line 36, The code defines a fixed-size val labels = listOf("첫번째", "두번째",
"세번째") but later indexes it using the answers list (labels[index]), risking
IndexOutOfBounds when answers.size > 3; update MemoryQuizTextModeContent so you
either (a) guard when accessing labels by using labels.getOrNull(index) ?:
fallbackLabel (or compute a label from index) wherever labels[index] is used, or
(b) enforce/validate that answers.size <= labels.size before mapping (throw or
truncate) so callers cannot pass more than three answers—locate the labels
declaration and all usages of labels[index] (the mapping logic around answers)
and apply one of these fixes consistently.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizScreen.kt (1)

138-141: !! 연산자 대신 null-safe 처리를 권장합니다.

getDrawable이 null을 반환할 경우 NPE가 발생할 수 있습니다. Preview 컨텍스트에서는 디자인 타임에 실패하지만, 명시적인 에러 메시지를 위해 requireNotNull 사용을 고려해 주세요.

♻️ 제안된 수정
     val previewHandler = AsyncImagePreviewHandler { request ->
-        val drawable = ContextCompat.getDrawable(request.context, R.drawable.img_default_card)!!
+        val drawable = requireNotNull(
+            ContextCompat.getDrawable(request.context, R.drawable.img_default_card)
+        ) { "Preview drawable not found: R.drawable.img_default_card" }
         drawable.asImage()
     }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aebc8bc and 4e472f3.

📒 Files selected for processing (1)
  • feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizScreen.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 (2)
feature/senior/src/main/kotlin/com/moa/app/feature/senior/quiz/spacetime/SpaceTimeQuizScreen.kt (2)

97-129: Foldable 기기 대응 레이아웃 개선이 잘 적용되었습니다.

quizMaxWidth() modifier와 Alignment.CenterHorizontally를 활용하여 Foldable 기기에서 콘텐츠가 중앙 정렬되도록 구현되었습니다. SpaceTimeQuizForm으로 퀴즈 렌더링 로직을 분리한 것도 재사용성 측면에서 좋습니다.


133-163: Coil 이미지 Preview 지원 및 FoldablePreviews 적용이 잘 구현되었습니다.

AsyncImagePreviewHandlerCompositionLocalProvider를 활용하여 Preview에서 Coil 이미지를 올바르게 표시하도록 구현했으며, @FoldablePreviews 어노테이션으로 Foldable 기기 프리뷰도 지원됩니다.

ExperimentalCoilApi는 현재 Coil 3에서도 여전히 실험적 단계이며, 실제로 3.2.0-rc01에서 함수형 생성자 동작이 변경된 사례가 있습니다. Coil 버전 업데이트 시 이 API의 변경 사항을 주의 깊게 모니터링해 주세요.

@wjdrjs00 wjdrjs00 merged commit 6af8d97 into develop Jan 12, 2026
4 checks passed
@wjdrjs00 wjdrjs00 deleted the feature/77-quiz-ux branch January 12, 2026 06:43
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] Quiz Content UX를 개선합니다.

2 participants