Skip to content

[Feature/#192] quiz api & workmanger#194

Merged
chanubc merged 28 commits into
developfrom
feature/#192-quiz-api
Oct 26, 2025
Merged

[Feature/#192] quiz api & workmanger#194
chanubc merged 28 commits into
developfrom
feature/#192-quiz-api

Conversation

@chanubc
Copy link
Copy Markdown
Member

@chanubc chanubc commented Oct 25, 2025

✅ 𝗖𝗵𝗲𝗰𝗸-𝗟𝗶𝘀𝘁

  • merge할 브랜치의 위치를 확인해 주세요(main❌/develop⭕)
  • 리뷰가 필요한 경우 리뷰어를 지정해 주세요
  • P1 단계의 리뷰는 필수로 반영합니다.
  • Approve된 PR은 assigner가 머지하고, 수정 요청이 온 경우 수정 후 다시 push를 합니다.

📌 𝗜𝘀𝘀𝘂𝗲𝘀

📎𝗪𝗼𝗿𝗸 𝗗𝗲𝘀𝗰𝗿𝗶𝗽𝘁𝗶𝗼𝗻

  • workmanager를 활용한 자정에 quiz 여부 백그라운드 스케줄링
  • quiz api 연결
  • quiz main 화면 타이머 구현

📷 𝗦𝗰𝗿𝗲𝗲𝗻𝘀𝗵𝗼𝘁

KakaoTalk_20251026_031546946.mp4
image

💬 𝗧𝗼 𝗥𝗲𝘃𝗶𝗲𝘄𝗲𝗿𝘀

Summary by CodeRabbit

  • 새로운 기능
    • 앱에 퀴즈 기능이 추가되어 매일 퀴즈를 풀고 결과(획득 XP, 정답률, 소요시간)를 확인할 수 있습니다.
    • 자정까지 남은 시간이 실시간으로 표시됩니다.
    • 퀴즈 제출 후 결과 화면과 오류 스낵바가 개선되었습니다.
    • 매일 자동으로 퀴즈 완료 상태가 초기화되어 다음 날 다시 참여할 수 있습니다.

# Conflicts:
#	core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt
#	core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt
#	core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt
#	core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Oct 25, 2025

Walkthrough

WorkManager 기반 일일 리셋 스케줄러와 퀴즈 네트워크/데이터스텁 계층, 관련 DI·뷰모델·UI를 추가하고 일부 디자인시스템 파라미터명과 매니페스트 provider 선언을 정리했습니다.

Changes

코호트 / 파일(s) 변경 요약
빌드 및 버전 카탈로그
gradle/libs.versions.toml, app/build.gradle.kts, core/data/build.gradle.kts, feature/quiz/build.gradle.kts
WorkManager 및 Hilt-Work 의존성 추가(버전 키 추가). 모듈 의존성 일부 변경(:core:common:core:domain).
앱 매니페스트
app/src/main/AndroidManifest.xml
androidx.startup.InitializationProvider 선언을 tools:node="remove"로 제거.
앱 초기화 / WorkManager 통합
app/src/main/java/com/teamwable/wable/WableApp.kt
HiltWorkerFactory 주입 및 WorkManager 초기화 호출, DailyTaskScheduler 스케줄링 호출 추가.
스케줄러 인터페이스
core/common/src/main/java/com/teamwable/common/intentprovider/DailyTaskScheduler.kt
DailyTaskScheduler 인터페이스 추가 (scheduleDailyReset()).
스케줄러 구현 및 워커
core/data/src/main/java/com/teamwable/data/worker/DailyTaskSchedulerImpl.kt, core/data/src/main/java/com/teamwable/data/worker/ResetQuizStatusWorker.kt
자정 기준 초기 딜레이 계산 후 24시간 주기 PeriodicWork 등록, ResetQuizStatusWorker로 퀴즈 완료 상태 초기화(재시도 처리 포함).
DI 모듈
core/data/src/main/java/com/teamwable/data/di/SchedulerModule.kt, core/data/src/main/java/com/teamwable/data/di/RepositoryModule.kt
DailyTaskScheduler 바인딩 추가 및 QuizRepository 바인딩 추가.
네트워크 계층 & DTO
core/network/.../QuizService.kt, .../dto/request/RequestQuizScoreDto.kt, .../dto/response/quiz/ResponseQuizDto.kt, .../ResponseQuizScoreDto.kt, core/network/src/main/java/com/teamwable/network/di/ServiceModule.kt
Retrofit 서비스(GET /api/v1/quiz, PATCH /api/v1/quiz/grade) 추가 및 DTO(요청/응답) 추가, DI 제공자 등록.
데이터 레이어: 리포지토리/매퍼/구현
core/data/src/main/java/com/teamwable/data/repository/QuizRepository.kt, .../repositoryimpl/DefaultQuizRepository.kt, .../mapper/toModel/QuizMapper.kt
QuizRepository 인터페이스 및 구현체 추가, DTO↔도메인 매핑 함수 추가.
DataStore 및 사용자 상태
core/datastore/.../WablePreferencesDataSource.kt, .../DefaultWablePreferenceDatasource.kt, core/data/.../repository/UserInfoRepository.kt, .../DefaultUserInfoRepository.kt
isQuizCompleted Flow 및 updateIsQuizCompleted() 추가, UserInfoRepository에 get/save 퀴즈 완료 상태 추가.
도메인 유스케이스
core/domain/src/main/java/com/teamwable/domain/usecase/GetRemainingTimeUseCase.kt
자정까지 남은 시간(시간, 분)을 매분 갱신하는 Flow 유스케이스 추가.
모델
core/model/src/main/java/com/teamwable/model/quiz/QuizModel.kt, QuizResultModel.kt, QuizScoreModel.kt
퀴즈 관련 도메인 모델(QuizModel, QuizResultModel, QuizScoreModel) 추가.
퀴즈 피처: ViewModel / 화면 / 네비게이션
feature/quiz/src/main/java/com/teamwable/quiz/** (여러 파일, 예: QuizStartViewModel.kt, QuizStartScreen.kt, QuizMainViewModel.kt, QuizResultViewModel.kt, fragments, state/intent/sideeffect 모델 등), feature/quiz/src/main/res/navigation/graph_quiz.xml
퀴즈 로드/제출 흐름 구현, 인텐트/사이드이펙트 아키텍처 적용, 결과 모델 전달용 네비게이션 인자(quizResultModel) 추가, UI 컴포저블 시그니처 및 상태 기반 리팩터링 등.
디자인 시스템
core/designsystem/src/main/java/.../WableGlideImage.kt, .../WableCustomCardWithStroke.kt, feature/onboarding/.../LckTeamItem.kt, feature/quiz/.../QuizButton.kt
Glide 실패 시 회색 박스 플레이스홀더 추가. WableCustomCardWithStroke 파라미터명 connerRadiuscornerRadius, backGroundColorbackgroundColor로 변경 및 호출부 대응 수정.

Sequence Diagram(s)

sequenceDiagram
    participant App as WableApp
    participant Scheduler as DailyTaskSchedulerImpl
    participant WM as WorkManager
    participant Worker as ResetQuizStatusWorker
    participant Repo as UserInfoRepository

    App->>Scheduler: initWorkManager() / scheduleDailyReset()
    Scheduler->>Scheduler: compute delay until midnight
    Scheduler->>WM: enqueueUniquePeriodicWork(ResetQuizStatusWorker, KEEP, 24h)
    Note over WM: 자정 도달 시 워커 실행
    WM->>Worker: doWork()
    Worker->>Repo: saveQuizCompleted(false)
    Repo-->>Worker: 완료
    Worker-->>WM: Result.success()
Loading
sequenceDiagram
    participant User as 사용자
    participant Start as QuizStartFragment
    participant VM as QuizStartViewModel
    participant Repo as QuizRepository
    participant API as QuizService
    participant Result as QuizResultFragment

    User->>Start: 화면 진입
    Start->>VM: LoadInitialData
    VM->>Repo: getQuiz()
    Repo->>API: GET /api/v1/quiz
    API-->>Repo: ResponseQuizDto
    Repo-->>VM: QuizModel
    VM-->>Start: 상태 업데이트 (quizModel)
    User->>Start: O/X 선택 및 제출
    Start->>VM: ClickSubmitBtn
    VM->>Repo: patchQuizScore(QuizScoreModel)
    Repo->>API: PATCH /api/v1/quiz/grade
    API-->>Repo: ResponseQuizScoreDto
    Repo-->>VM: QuizResultModel
    VM-->>Start: NavigateToResult(model)
    Start->>Result: 네비게이션 (quizResultModel 전달)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

추가 검토 포인트:

  • WorkManager 초기 딜레이(자정 계산) 및 중복 등록(KEEP) 동작 확인: DailyTaskSchedulerImpl.kt
  • Hilt + WorkManager 주입 설정과 HiltWorkerFactory 초기화: WableApp.ktResetQuizStatusWorker.kt
  • 퀴즈 제출/응답 흐름의 예외 처리와 시간 계산(경과 시간): QuizStartViewModel.kt, DefaultQuizRepository.kt
  • Safe Args로 전달되는 QuizResultModel 직렬화/패스 확인: graph_quiz.xml, fragment 인자 처리
  • 상태 기반 UI로의 전환에서 nullable 처리 및 초기값 일관성: 여러 composable/ViewModel 파일

Possibly related PRs

Suggested reviewers

  • sohyun127

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 The pull request title "[Feature/#192] quiz api & workmanger"는 변경 사항의 주요 두 가지 기능인 Quiz API 통합과 WorkManager를 명확하게 전달하고 있습니다. 제목은 간결하고 명확하며 변경 사항과 충분히 관련이 있습니다. 다만 "workmanger"는 "workmanager"의 오타이지만, 의도는 명확하게 전달됩니다.
Linked Issues Check ✅ Passed Issue #192의 모든 핵심 요구사항이 충족되었습니다. 서버 통신 요구사항은 QuizService, DefaultQuizRepository, 그리고 관련 DTO/Model 클래스들을 통해 구현되었습니다 [#192]. WorkManager 요구사항은 ResetQuizStatusWorker, DailyTaskSchedulerImpl, DailyTaskScheduler 인터페이스 및 WableApp의 초기화를 통해 완료되었습니다 [#192]. DataStore 저장 요구사항은 IsQuizCompleted 선호도 키, WablePreferencesDataSource 인터페이스 업데이트 및 DefaultWablePreferenceDatasource 구현을 통해 달성되었습니다 [#192]. 추가로 GetRemainingTimeUseCase와 QuizMainViewModel을 통해 타이머 기능도 구현되었습니다.
Out of Scope Changes Check ✅ Passed 대부분의 변경사항은 Issue #192의 범위 내에 있지만, WableCustomCardWithStroke와 WableGlideImage의 설계 시스템 변경은 약간의 고려가 필요합니다. 이러한 변경사항들은 QuizButton 등 Quiz 기능을 구현하기 위해 필요한 지원 인프라 변경이며 직접적으로 Quiz 기능을 지원합니다. LckTeamItem (Onboarding 기능)의 매개변수명 수정은 설계 시스템 변경에 대응하는 것으로, 다른 모듈에 있지만 필요한 업데이트입니다.
Description Check ✅ Passed PR 설명이 저장소의 템플릿을 잘 따르고 있습니다. ✅ Check-List, 📌 Issues (closed #192), 📎 Work Description (WorkManager 백그라운드 스케줄링, Quiz API 연결, 타이머 구현)이 모두 포함되어 있으며, 📷 Screenshots도 제공되었습니다. 💬 To Reviewers 섹션만 비어있지만 이는 선택사항입니다.
✨ 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/#192-quiz-api

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ffcd3c and d932860.

📒 Files selected for processing (3)
  • core/designsystem/src/main/java/com/teamwable/designsystem/component/card/WableCustomCardWithStroke.kt (1 hunks)
  • feature/onboarding/src/main/java/com/teamwable/onboarding/selectlckteam/component/LckTeamItem.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/component/QuizButton.kt (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • feature/onboarding/src/main/java/com/teamwable/onboarding/selectlckteam/component/LckTeamItem.kt
🔇 Additional comments (2)
feature/quiz/src/main/java/com/teamwable/quiz/component/QuizButton.kt (1)

36-38: LGTM! API 변경사항에 맞춰 올바르게 업데이트되었습니다.

WableCustomCardWithStroke의 새로운 파라미터명(cornerRadius, backgroundColor)을 정확하게 사용하고 있습니다.

core/designsystem/src/main/java/com/teamwable/designsystem/component/card/WableCustomCardWithStroke.kt (1)

18-31: QuizButton.kt에서 매개변수명 업데이트가 누락되었습니다.

검증 결과 feature/quiz/src/main/java/com/teamwable/quiz/component/QuizButton.kt에서 다음 항목을 확인했습니다:

  • Line 64: val backGroundColor: Color — 여전히 오래된 명명규칙 사용
  • Lines 37-38: type.backGroundColor 참조 — 새로운 backgroundColor 매개변수 전달 시 여전히 오래된 내부 속성명 사용

WableCustomCardWithStroke 매개변수명 변경이 완전하려면 이 파일도 일관되게 업데이트되어야 합니다. 이 파일이 현재 PR에 포함되는지, 그리고 리팩토링 범위가 모든 호출처까지 포함하는지 확인해주세요.


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.

@chanubc chanubc added ⭐ [FEAT] 새로운 기능 구현 🐻 찬우 차은우 ☕ [COMPOSE] 컴포즈 ui labels Oct 25, 2025
Copy link
Copy Markdown

@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: 5

🧹 Nitpick comments (16)
core/model/src/main/java/com/teamwable/model/quiz/QuizModel.kt (1)

3-8: 기본값 사용에 대한 고려사항

모든 속성에 기본값이 설정되어 있어 데이터 누락 시에도 객체 생성이 가능합니다. 특히 quizId = -1과 같은 센티널 값은 유효한 데이터인지 구별하기 어려울 수 있습니다. 필수 데이터가 누락된 경우 빠르게 실패하도록 nullable 타입을 고려해보는 것도 좋습니다.

core/data/src/main/java/com/teamwable/data/worker/ResetQuizStatusWorker.kt (1)

18-26: 예외 처리 범위를 구체화하는 것을 고려해보세요.

현재 모든 Exception을 catch하고 있어서 재시도가 불가능한 에러(예: IllegalStateException, SecurityException 등)에 대해서도 무한정 재시도를 시도할 수 있습니다. 특정 예외 타입에 대해서는 Result.failure()를 반환하는 것이 더 적절할 수 있습니다.

다음과 같이 재시도 가능한 예외와 그렇지 않은 예외를 구분하는 것을 고려해보세요:

 override suspend fun doWork(): Result {
     return try {
         userInfoRepository.saveQuizCompleted(false)
         Result.success()
-    } catch (e: Exception) {
+    } catch (e: IOException) {
+        Timber.e(e, "Network error during quiz reset")
+        Result.retry()
+    } catch (e: Exception) {
         Timber.e(e)
-        Result.retry()
+        Result.failure()
     }
 }
core/data/src/main/java/com/teamwable/data/worker/DailyTaskSchedulerImpl.kt (2)

23-28: 자정 정렬 필요 시 Periodic(24h)만으로는 서머타임(DST)에서 드리프트가 발생합니다.

PeriodicWorkRequest는 “이전 실행 시점 + 24h”로 반복되므로 DST 전후에 자정에서 ±1h씩 어긋날 수 있습니다. 자정 정렬이 중요하면 다음 패턴을 권장합니다:

  • OneTimeWorkRequest로 “다음 자정”만 예약
  • 워커 실행 끝에서 다시 “다음 자정” OneTimeWork를 예약

이렇게 하면 매일 자정(로컬 타임존)에 재정렬됩니다. 기존 주기를 유지해도 되지만 “자정 정확도” 요구가 있으면 전환을 고려해 주세요.


19-22: now를 두 번 호출하여 경계 시점에 미세한 오차 가능. 동일 기준으로 계산하세요.

동일한 now 기준을 사용하면 안전합니다. Zone 처리까지 포함하면 더 견고합니다.

-        val now = LocalDateTime.now()
-        val nextMidnight = LocalDateTime.now().plusDays(1).with(LocalTime.MIDNIGHT)
-        val initialDelay = Duration.between(now, nextMidnight)
+        val now = java.time.ZonedDateTime.now()
+        val nextMidnight = now.toLocalDate().plusDays(1).atStartOfDay(now.zone)
+        val initialDelay = java.time.Duration.between(now, nextMidnight)
core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt (1)

41-43: 퀴즈 완료 플래그 연동은 적절합니다. 네이밍/기본값만 확인해 주세요.

  • 구현은 DataSource에 잘 위임되었습니다. LGTM.
  • 메서드 네이밍은 기존 getIsPushAlarmAllowed()와의 일관성을 위해 getIsQuizCompleted()로 맞추는 것도 고려해 보세요. 선택 사항입니다.
  • 신규 키의 기본값(false 등)과 마이그레이션(기존 사용자)에 문제가 없는지 한 번만 점검 부탁드립니다.

Also applies to: 92-94

core/model/src/main/java/com/teamwable/model/quiz/QuizScoreModel.kt (1)

3-7: 필드 단위와 범위를 명시해 모델 계약을 명확히 해주세요.

  • quizTime: Int의 단위(초/밀리초)와 허용 범위를 문서화하거나 @IntRange로 제한하면 사용처 오류를 줄일 수 있습니다.
  • 이름도 elapsedSeconds 등 단위가 드러나게 하는 것을 권장합니다. 선택 사항입니다.
core/model/src/main/java/com/teamwable/model/quiz/QuizResultModel.kt (1)

6-12: 프로퍼티 의미를 더 분명히: 범위 어노테이션과 네이밍 개선 제안.

  • userPercent[0..100]가 예상됩니다. @IntRange(from = 0, to = 100)로 명시해 주세요.
  • continueNumber는 의미가 모호합니다. streakCount/consecutiveDays 등 도메인이 드러나는 이름을 고려해 주세요.
feature/quiz/src/main/java/com/teamwable/quiz/start/model/QuizStartSideEffect.kt (1)

11-11: UI에 Throwable을 직접 전달하지 마세요. 사용자 메시지/리소스로 변환하세요.

  • ShowSnackBar(val message: Throwable)는 내부 구현/스택 메시지를 노출할 수 있습니다.
  • String 혹은 @StringRes/에러코드로 전달하고, ViewModel에서 매핑하는 패턴을 권장합니다.
-    data class ShowSnackBar(val message: Throwable) : QuizStartSideEffect
+    data class ShowSnackBar(val message: String) : QuizStartSideEffect
+    // 또는
+    // data class ShowSnackBar(@StringRes val messageRes: Int) : QuizStartSideEffect
feature/quiz/src/main/java/com/teamwable/quiz/result/model/QuizResultSideEffect.kt (1)

8-8: UI로 Throwable 전달 지양 + 명명 통일 제안(ShowSnackbar).
UI 계층으로 예외 객체를 넘기면 직렬화/복원, 테스트, 메시지 결정 책임이 뒤섞입니다. 사용자 메시지(String 또는 UiText 계열)만 전달하고 이름을 Material 표기인 Snackbar로 통일을 권장합니다.

적용 예시:

-    data class ShowSnackBar(val message: Throwable) : QuizResultSideEffect
+    data class ShowSnackbar(val message: String) : QuizResultSideEffect

적용 시 호출부/when 분기에서 클래스명과 파라미터 타입 업데이트 필요.

feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultViewModel.kt (1)

33-37: 타이머 포맷 안전성 보강(음수 방지, Locale).
예기치 않은 음수 입력 방지와 숫자 포맷의 지역 의존성 제거를 권장합니다.

-    private fun formatTime(seconds: Int): String {
-        val minutes = seconds / 60
-        val remainingSeconds = seconds % 60
-        return String.format(Locale.KOREA, "%d:%02d", minutes, remainingSeconds)
-    }
+    private fun formatTime(totalSeconds: Int): String {
+        val s = totalSeconds.coerceAtLeast(0)
+        val minutes = s / 60
+        val remainingSeconds = s % 60
+        return "%d:%02d".format(Locale.ROOT, minutes, remainingSeconds)
+    }
core/network/src/main/java/com/teamwable/network/dto/request/RequestQuizScoreDto.kt (1)

6-11: 필드 단위 명시 및 어노테이션 간소화 제안.

  • quizTime의 단위를 KDoc으로 명시해 혼동 방지.
  • JSON 키가 프로퍼티명과 동일하면 @SerialName은 생략 가능합니다(팀 컨벤션에 따라 유지 가능).

KDoc 추가 예시:

+/**
+ * 퀴즈 점수 전송 요청.
+ * @param quizTime 단위: 초(second)
+ */
 @Serializable
 data class RequestQuizScoreDto(
-    @SerialName("quizId") val quizId: Long,
-    @SerialName("userAnswer") val userAnswer: Boolean,
-    @SerialName("quizTime") val quizTime: Int,
+    @SerialName("quizId") val quizId: Long,
+    @SerialName("userAnswer") val userAnswer: Boolean,
+    @SerialName("quizTime") val quizTime: Int,
 )
feature/quiz/src/main/java/com/teamwable/quiz/QuizMainFragment.kt (1)

46-51: 중복 내비게이션 방지 옵션 검토.
연속 클릭/중복 이벤트에 대비해 setLaunchSingleTop(true) 추가를 고려해 주세요.

 val navOptions = NavOptions.Builder()
-    .setPopUpTo(R.id.graph_quiz, true)
+    .setPopUpTo(R.id.graph_quiz, true)
+    .setLaunchSingleTop(true)
     .build()
feature/quiz/src/main/java/com/teamwable/quiz/result/model/QuizResultState.kt (1)

15-16: XP 값을 상수로 추출하는 것을 고려하세요.

성공/실패에 대한 XP 값이 하드코딩되어 있습니다. 향후 XP 보상 체계가 변경될 경우를 대비하여 companion object의 상수로 추출하는 것이 유지보수에 유리합니다.

다음과 같이 리팩토링할 수 있습니다:

 @Stable
 data class QuizResultState(
     val scoreTime: String = "0:00",
     val quizResult: Boolean = false,
     val userPercent: Int = 0,
 ) : BaseState {
+    companion object {
+        private const val XP_SUCCESS = 8
+        private const val XP_FAIL = 3
+    }
+
     val resultType: QuizResultType
         get() = if (quizResult) QuizResultType.SUCCESS else QuizResultType.FAIL
     val xp: Int
-        get() = if (quizResult) 8 else 3
+        get() = if (quizResult) XP_SUCCESS else XP_FAIL
 }
feature/quiz/src/main/java/com/teamwable/quiz/QuizMainViewModel.kt (1)

19-32: 숫자 리터럴 포맷의 일관성을 개선하세요.

Line 22에서는 5000을, Line 30에서는 5_000을 사용하고 있습니다. 가독성을 위해 일관된 포맷을 사용하는 것이 좋습니다.

다음과 같이 통일할 수 있습니다:

     val isQuizCompleted: StateFlow<Boolean?> = userInfoRepository.getQuizCompleted()
         .stateIn(
             scope = viewModelScope,
-            started = SharingStarted.WhileSubscribed(5000),
+            started = SharingStarted.WhileSubscribed(5_000),
             initialValue = null,
         )
feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt (1)

69-73: 경과 시간 계산에 방어 코드를 추가하는 것을 고려하세요.

시스템 시계가 뒤로 조정되는 경우(드문 경우이지만) 음수 값이 발생할 수 있습니다.

방어 코드를 추가할 수 있습니다:

 private fun getElapsedTime(): Int {
     val endTimeMillis = System.currentTimeMillis()
     val elapsedSeconds = (endTimeMillis - startTimeMillis) / 1000
-    return elapsedSeconds.toInt()
+    return elapsedSeconds.coerceAtLeast(0).toInt()
 }
feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartScreen.kt (1)

96-103: 이미지 레이아웃 제약을 확인하세요.

fillMaxWidth()와 고정 높이 168.dp를 함께 사용하고 있습니다. ContentScale.Fit이 종횡비를 유지하므로 이미지 비율에 따라 빈 공간이 생길 수 있습니다. UI 디자인에서 의도한 동작이 맞는지 확인해 주세요.

이미지가 다양한 종횡비를 가질 수 있다면, 고정 높이 대신 aspectRatio modifier 사용을 고려할 수 있습니다.

예시:

 WableGlideImage(
     imageUrl = state.quizModel.quizImage,
     contentScale = ContentScale.Fit,
     modifier = Modifier
         .fillMaxWidth()
         .padding(horizontal = 20.dp)
-        .height(168.dp),
+        .aspectRatio(16f / 9f),  // 원하는 비율로 조정
 )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 855580b and c56d87f.

📒 Files selected for processing (44)
  • app/build.gradle.kts (1 hunks)
  • app/src/main/AndroidManifest.xml (1 hunks)
  • app/src/main/java/com/teamwable/wable/WableApp.kt (3 hunks)
  • core/common/src/main/java/com/teamwable/common/intentprovider/DailyTaskScheduler.kt (1 hunks)
  • core/data/build.gradle.kts (1 hunks)
  • core/data/src/main/java/com/teamwable/data/di/RepositoryModule.kt (3 hunks)
  • core/data/src/main/java/com/teamwable/data/di/SchedulerModule.kt (1 hunks)
  • core/data/src/main/java/com/teamwable/data/mapper/toModel/QuizMapper.kt (1 hunks)
  • core/data/src/main/java/com/teamwable/data/repository/QuizRepository.kt (1 hunks)
  • core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt (2 hunks)
  • core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultQuizRepository.kt (1 hunks)
  • core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt (2 hunks)
  • core/data/src/main/java/com/teamwable/data/worker/DailyTaskSchedulerImpl.kt (1 hunks)
  • core/data/src/main/java/com/teamwable/data/worker/ResetQuizStatusWorker.kt (1 hunks)
  • core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt (3 hunks)
  • core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt (2 hunks)
  • core/designsystem/src/main/java/com/teamwable/designsystem/component/image/WableImage.kt (3 hunks)
  • core/domain/src/main/java/com/teamwable/domain/usecase/GetRemainingTimeUseCase.kt (1 hunks)
  • core/model/src/main/java/com/teamwable/model/quiz/QuizModel.kt (1 hunks)
  • core/model/src/main/java/com/teamwable/model/quiz/QuizResultModel.kt (1 hunks)
  • core/model/src/main/java/com/teamwable/model/quiz/QuizScoreModel.kt (1 hunks)
  • core/network/src/main/java/com/teamwable/network/datasource/QuizService.kt (1 hunks)
  • core/network/src/main/java/com/teamwable/network/di/ServiceModule.kt (2 hunks)
  • core/network/src/main/java/com/teamwable/network/dto/request/RequestQuizScoreDto.kt (1 hunks)
  • core/network/src/main/java/com/teamwable/network/dto/response/quiz/ResponseQuizDto.kt (1 hunks)
  • core/network/src/main/java/com/teamwable/network/dto/response/quiz/ResponseQuizScoreDto.kt (1 hunks)
  • feature/quiz/build.gradle.kts (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/QuizMainFragment.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/QuizMainScreen.kt (3 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/QuizMainViewModel.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultFragment.kt (3 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultScreen.kt (4 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultViewModel.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/result/model/QuizResultIntent.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/result/model/QuizResultSideEffect.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/result/model/QuizResultState.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartFragment.kt (2 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartScreen.kt (6 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/start/model/QuizStartIntent.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/start/model/QuizStartSideEffect.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/start/model/QuizStartState.kt (1 hunks)
  • feature/quiz/src/main/res/navigation/graph_quiz.xml (2 hunks)
  • gradle/libs.versions.toml (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt (1)
core/common/src/main/java/com/teamwable/common/base/BaseViewModel.kt (2)
  • postSideEffect (41-47)
  • intent (37-39)
feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultViewModel.kt (1)
core/common/src/main/java/com/teamwable/common/base/BaseViewModel.kt (2)
  • postSideEffect (41-47)
  • intent (37-39)
feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultScreen.kt (2)
feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultFragment.kt (1)
  • navigateToMain (37-40)
feature/quiz/src/main/java/com/teamwable/quiz/component/QuizComponent.kt (1)
  • QuizStatBox (21-53)
feature/quiz/src/main/java/com/teamwable/quiz/QuizMainFragment.kt (2)
feature/quiz/src/main/java/com/teamwable/quiz/QuizMainScreen.kt (1)
  • QuizMainRoute (31-42)
core/ui/src/main/java/com/teamwable/ui/extensions/FragmentExt.kt (1)
  • openUri (49-54)
core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultQuizRepository.kt (1)
core/data/src/main/java/com/teamwable/data/util/runSuspendCatching.kt (1)
  • runHandledCatching (19-24)
feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartScreen.kt (2)
feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartFragment.kt (1)
  • navigateToResult (35-38)
feature/quiz/src/main/java/com/teamwable/quiz/component/QuizButton.kt (1)
  • QuizOXButton (27-50)
🔇 Additional comments (36)
core/designsystem/src/main/java/com/teamwable/designsystem/component/image/WableImage.kt (1)

3-4: 임포트 추가가 적절합니다.

실패 UI 구현에 필요한 임포트들이 올바르게 추가되었습니다.

Also applies to: 16-16

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

13-13: 도메인 레이어로의 의존성 변경이 적절합니다.

:core:common에서 :core:domain으로 변경하여 도메인 주도 아키텍처를 따르고 있습니다. 새로 추가된 GetRemainingTimeUseCase 같은 도메인 유즈케이스를 사용하기 위한 올바른 접근입니다.

core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt (1)

16-16: 퀴즈 완료 상태 저장 기능이 올바르게 추가되었습니다.

기존 인터페이스 패턴과 일관성 있게 isQuizCompleted Flow와 updateIsQuizCompleted 메서드가 추가되었습니다.

Also applies to: 38-38

app/src/main/AndroidManifest.xml (1)

66-69: InitializationProvider 제거가 적절합니다.

Hilt 기반 WorkManager 초기화로 전환하면서 androidx.startup의 InitializationProvider를 제거한 것이 올바른 접근입니다. 이는 커스텀 초기화 로직과의 충돌을 방지합니다.

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

15-15: WorkManager 통합을 위한 의존성이 올바르게 추가되었습니다.

WorkManager 런타임, Hilt Work 통합, 그리고 필요한 컴파일러가 모두 포함되어 DI 기반 스케줄링 구현을 지원합니다.

Also applies to: 22-27

gradle/libs.versions.toml (1)

88-90: WorkManager와 Hilt Work 버전이 적절하게 추가되었습니다.

WorkManager 2.11.0과 Hilt Work 1.3.0은 안정적인 버전이며, 라이브러리 선언도 올바르게 구성되었습니다.

Also applies to: 122-124

app/build.gradle.kts (1)

107-107: Hilt Work 의존성이 올바르게 추가되었습니다.

앱 모듈에 Hilt Work 통합을 위한 의존성이 추가되어 HiltWorkerFactory를 사용한 WorkManager 초기화를 지원합니다.

core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt (1)

26-26: LGTM!

퀴즈 완료 상태를 관리하는 메서드들이 기존 repository 패턴과 일관되게 추가되었습니다. Flow를 사용한 읽기와 suspend를 사용한 쓰기 구조가 적절합니다.

Also applies to: 52-52

feature/quiz/src/main/res/navigation/graph_quiz.xml (1)

32-34: LGTM!

Navigation argument가 올바르게 선언되었습니다. QuizResultModel 타입이 적절하게 지정되어 있고, Safe Args를 통해 타입 안전성이 보장됩니다.

core/common/src/main/java/com/teamwable/common/intentprovider/DailyTaskScheduler.kt (1)

1-5: LGTM!

일일 작업 스케줄링을 위한 간결한 인터페이스입니다. 단일 책임 원칙을 잘 따르고 있으며, WorkManager 구현 세부사항을 추상화하고 있습니다.

feature/quiz/src/main/java/com/teamwable/quiz/result/model/QuizResultIntent.kt (1)

1-10: LGTM!

MVI 패턴에 맞는 Intent 정의입니다. data objectdata class를 적절하게 사용하여 상태가 없는 액션과 데이터를 포함하는 액션을 구분하고 있습니다.

core/network/src/main/java/com/teamwable/network/di/ServiceModule.kt (1)

11-11: LGTM!

QuizService가 기존 서비스들과 동일한 패턴으로 DI 모듈에 추가되었습니다. @WableRetrofit 한정자와 싱글톤 스코프가 적절하게 사용되었습니다.

Also applies to: 77-81

core/data/src/main/java/com/teamwable/data/mapper/toModel/QuizMapper.kt (1)

10-27: LGTM!

DTO와 도메인 모델 간의 매핑 함수들이 명확하게 구현되었습니다. 단순한 속성 매핑으로 충분하며, internal 가시성으로 적절하게 캡슐화되어 있습니다.

feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultFragment.kt (1)

16-16: LGTM!

Navigation Safe Args를 통한 인자 전달과 에러 처리를 위한 Snackbar 콜백이 올바르게 구현되었습니다. Fragment와 Composable 간의 통신 패턴이 명확하고 일관성이 있습니다.

Also applies to: 29-30, 42-44

app/src/main/java/com/teamwable/wable/WableApp.kt (1)

25-25: 원본 리뷰 댓글의 우려사항이 코드베이스 상황과 맞지 않습니다.

AndroidManifest.xml에서 androidx.startup.InitializationProvidertools:node="remove"로 명시적으로 제거되어 있습니다. 이는 WorkManagerInitializer를 포함한 모든 자동 초기화를 비활성화합니다.

따라서:

  • 기본 WorkManagerInitializer ContentProvider가 실행되지 않음
  • WableApp.onCreate()의 수동 WorkManager.initialize() 호출은 안전함
  • "WorkManager is already initialized" 충돌 위험이 없음
  • 현재 구현은 정상 작동함

Configuration.Provider 패턴은 유효한 대안이지만, 여기서는 필수 리팩토링이 아닙니다.

Likely an incorrect or invalid review comment.

core/data/src/main/java/com/teamwable/data/worker/DailyTaskSchedulerImpl.kt (1)

23-34: Companion.getInstance() 스타일만 개선하면 됩니다. 나머지 사항은 확인 불필요합니다.

코드를 검토한 결과:

  1. Duration 호환성: minSdk가 28이고, setInitialDelay(Duration)은 API 26+ 지원하므로 완전히 호환됩니다. 버전 검토나 TimeUnit 변환이 불필요합니다.

  2. 네트워크 제약은 불필요합니다: ResetQuizStatusWorker는 로컬 DB만 수정합니다 (userInfoRepository.saveQuizCompleted(false)). 서버 통신이 없으므로 RequiredNetworkType.CONNECTED 제약을 추가할 이유가 없습니다.

  3. 개선 가능한 부분: 다만 스타일상 WorkManager.getInstance(context)로 단순화하면 더 명확합니다.

-        WorkManager.Companion.getInstance(context).enqueueUniquePeriodicWork(
+        WorkManager.getInstance(context).enqueueUniquePeriodicWork(

Likely an incorrect or invalid review comment.

feature/quiz/src/main/java/com/teamwable/quiz/result/model/QuizResultSideEffect.kt (1)

6-6: 네비게이션 사이드이펙트 추가, 좋습니다.
단순·명확한 이벤트로 적절합니다.

feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultViewModel.kt (1)

16-21: 인텐트 처리와 사이드이펙트 포스트 구조, 적절합니다.
BaseViewModel 패턴과 일관됩니다.

core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt (1)

32-33: 퀴즈 완료 상태 플래그 추가, 패턴 일관·안전(default=false).
Datastore 접근/에러 처리 컨벤션과 일치합니다.

로그아웃/계정 전환 시 플래그 유지 여부 정책 확인 부탁드립니다. 필요 시 clear()/clearForRefreshToken 범위에 포함 여부를 결정해야 합니다.

Also applies to: 88-93, 160-164

core/data/src/main/java/com/teamwable/data/di/SchedulerModule.kt (1)

11-19: LGTM! 잘 구현된 DI 모듈입니다.

DailyTaskScheduler를 위한 Hilt 모듈이 올바르게 구현되었습니다. Singleton 스코프와 @BINDS를 사용하여 효율적으로 바인딩하고 있습니다.

feature/quiz/src/main/java/com/teamwable/quiz/start/model/QuizStartIntent.kt (1)

6-14: LGTM! MVI 패턴을 올바르게 따르고 있습니다.

Sealed interface를 사용한 Intent 정의가 명확하고, data object를 활용하여 메모리 효율적으로 구현되었습니다.

core/data/src/main/java/com/teamwable/data/repository/QuizRepository.kt (1)

7-11: LGTM! 깔끔한 Repository 인터페이스입니다.

Result 타입을 사용하여 에러 핸들링을 명시적으로 처리하고 있으며, suspend 함수로 비동기 작업을 지원합니다.

core/data/src/main/java/com/teamwable/data/di/RepositoryModule.kt (1)

106-110: LGTM! DI 바인딩이 올바르게 추가되었습니다.

QuizRepository 바인딩이 모듈의 기존 패턴을 일관되게 따르고 있으며, Singleton 스코프가 적절하게 적용되었습니다.

feature/quiz/src/main/java/com/teamwable/quiz/QuizMainScreen.kt (3)

32-42: LGTM! 잘 구조화된 Composable 아키텍처입니다.

Route와 Screen을 분리하여 ViewModel 의존성을 격리하고, collectAsStateWithLifecycle을 사용하여 생명주기를 안전하게 관리하고 있습니다. Hilt 통합도 올바르게 구현되었습니다.


45-48: 파라미터 기본값 제공으로 프리뷰 지원이 우수합니다.

기본 파라미터를 제공하여 프리뷰와 테스트를 용이하게 하면서도, 실제 사용 시 동적 데이터를 주입할 수 있도록 유연하게 설계되었습니다.


82-82: 동적 타이머 표시가 올바르게 구현되었습니다.

하드코딩된 값에서 ViewModel의 상태를 반영하도록 변경되어 실시간 타이머 기능이 정상적으로 작동할 것입니다.

core/network/src/main/java/com/teamwable/network/dto/response/quiz/ResponseQuizScoreDto.kt (1)

6-14: LGTM! DTO가 올바르게 정의되었습니다.

Kotlinx Serialization을 사용한 DTO 정의가 적절하며, 기본값 제공으로 역직렬화 안정성이 확보되었습니다. SerialName 어노테이션을 통해 API 계약과의 매핑도 명확합니다.

core/network/src/main/java/com/teamwable/network/datasource/QuizService.kt (1)

11-19: OkHttp 타임아웃 설정이 적절하게 구성되었습니다.

core/network/src/main/java/com/teamwable/network/di/NetworkModule.kt에서 OkHttpClient 빌더에 connectTimeout과 readTimeout이 각각 5초로 설정되어 있으며, 토큰 인터셉터와 로깅 인터셉터가 올바르게 통합되어 있습니다.

다만 명시적인 재시도 로직(retry policy) 구성이 검색 결과에서 확인되지 않았으므로, 재시도 메커니즘이 인터셉터 내부에 구현되어 있거나 별도로 설정되어 있는지 수동으로 검증해 주시기 바랍니다.

feature/quiz/src/main/java/com/teamwable/quiz/start/model/QuizStartState.kt (1)

13-16: Now let me get the actual context from the codebase around those usage points:

계산된 속성의 로직 검증 필요

userAnswer 속성이 oxType == OXType.O로 구현되어 있어, oxType이 null일 때와 OXType.X일 때 모두 false를 반환하므로 "아직 선택하지 않음"과 "X를 선택함"을 구분할 수 없다는 점이 확인되었습니다.

코드베이스 검색 결과에서 userAnswer는 다음 5개 위치에서 사용됩니다:

  • RequestQuizScoreDto.kt:9 (DTO)
  • QuizScoreModel.kt:5 (Model)
  • QuizStartState.kt:15 (현재 검토 대상)
  • QuizMapper.kt:25 (매핑)
  • QuizStartViewModel.kt:53 (ViewModel)

하지만 정확한 컨텍스트 없이는 userAnswer가 실제로 enabled == true 조건에서만 접근되는지 확인할 수 없습니다. 다음 사항들을 수동으로 검증하세요:

  • QuizStartViewModel.kt의 line 53 주변 코드에서 currentState.userAnswer 접근 시 enabled 가드 확인
  • QuizMapper.kt의 line 25 주변에서 userAnswer 매핑 로직 검증
  • 필요한 경우 userAnswer를 nullable Boolean으로 변경하거나 사용 시점에서 enabled == true 체크 추가 검토
core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultQuizRepository.kt (1)

14-26: LGTM!

리포지토리 구현이 깔끔하고 일관된 에러 처리 패턴을 사용하고 있습니다. mapper를 통한 DTO-Model 변환도 적절하게 분리되어 있습니다.

feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartFragment.kt (1)

28-42: LGTM!

에러 처리를 위한 Snackbar 통합과 QuizResultModel을 사용한 네비게이션 업데이트가 적절하게 구현되었습니다.

feature/quiz/src/main/java/com/teamwable/quiz/QuizMainViewModel.kt (1)

26-27: 시간 포맷이 의도한 대로인지 확인하세요.

현재 포맷 문자열 "%d:%02d"는 시간이 한 자릿수일 때 앞에 0을 붙이지 않습니다 (예: "9:59"). 일반적인 시계 표시는 "09:59"와 같이 두 자릿수를 사용합니다. UI 디자인에서 의도한 포맷이 맞는지 확인해 주세요.

두 자릿수 시간 포맷이 필요한 경우:

-    val remainingTime: StateFlow<String> = getRemainingTimeUseCase.invoke()
-        .map { (hours, minutes) -> "%d:%02d".format(hours, minutes) }
+    val remainingTime: StateFlow<String> = getRemainingTimeUseCase.invoke()
+        .map { (hours, minutes) -> "%02d:%02d".format(hours, minutes) }
         .stateIn(
core/network/src/main/java/com/teamwable/network/dto/response/quiz/ResponseQuizDto.kt (1)

6-16: LGTM!

DTO 구현이 표준 패턴을 따르고 있으며, 모든 필드에 적절한 기본값과 직렬화 어노테이션이 설정되어 있습니다.

feature/quiz/src/main/java/com/teamwable/quiz/result/QuizResultScreen.kt (1)

39-67: LGTM!

ViewModel 통합이 적절하게 구현되었으며, lifecycle-aware한 상태 수집과 side effect 처리가 올바르게 되어 있습니다.

feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartScreen.kt (1)

49-58: LGTM!

Side effect 처리가 lifecycle-aware하게 올바르게 구현되어 있습니다.

feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt (1)

50-55: 리뷰 코멘트 부정확성 확인 - 코드는 올바릅니다

검증 결과, 리뷰 코멘트의 설명이 실제 코드 상황과 맞지 않습니다. 코드는 이미 quizId.toLong()으로 올바르게 변환하고 있습니다(toInt()가 아님).

타입 차이는 설계상 의도된 것으로 확인됩니다:

  • QuizModel.quizId: Int
  • QuizScoreModel.quizId: Long (네트워크 DTO RequestQuizScoreDto도 동일하게 Long)

현재 구현은 타입 불일치를 적절히 처리하고 있으므로, 추가 수정이 필요하지 않습니다.

Likely an incorrect or invalid review comment.

Comment thread feature/quiz/src/main/java/com/teamwable/quiz/QuizMainFragment.kt Outdated
Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (8)
core/domain/src/main/java/com/teamwable/domain/usecase/GetRemainingTimeUseCase.kt (1)

16-26: 이전 리뷰의 밀리초 계산 문제가 수정되었습니다.

Line 18에서 now.nano / 1_000_000L을 포함하여 정확한 초기 지연 시간을 계산하고 있습니다. 타이머 정확도 문제가 해결되었습니다.

선택적으로, 가독성을 위해 매직 넘버 60_000L을 상수로 추출할 수 있습니다:

companion object {
    private const val MINUTE_IN_MILLIS = 60_000L
}
core/data/src/main/java/com/teamwable/data/worker/DailyTaskSchedulerImpl.kt (2)

20-22: LocalDateTime.now() 중복 호출을 제거하세요.

Line 21에서 LocalDateTime.now()를 다시 호출하는 대신 Line 20의 now 변수를 재사용해야 합니다. 현재 코드는 두 호출 사이에 시간이 흐르면 이론적으로 불일치가 발생할 수 있습니다.

다음과 같이 수정하세요:

 val now = LocalDateTime.now()
-val nextMidnight = LocalDateTime.now().plusDays(1).with(LocalTime.MIDNIGHT)
+val nextMidnight = now.plusDays(1).with(LocalTime.MIDNIGHT)
 val initialDelay = Duration.between(now, nextMidnight)

32-36: 불필요한 .Companion 명시를 제거하세요.

Kotlin에서는 companion object의 멤버에 접근할 때 .Companion을 명시적으로 작성할 필요가 없습니다.

다음과 같이 간소화하세요:

-WorkManager.Companion.getInstance(context).enqueueUniquePeriodicWork(
-    ResetQuizStatusWorker.Companion.WORK_NAME,
+WorkManager.getInstance(context).enqueueUniquePeriodicWork(
+    ResetQuizStatusWorker.WORK_NAME,
     ExistingPeriodicWorkPolicy.KEEP,
     dailyResetRequest,
 )
feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt (2)

38-47: 타이머 재시작 동작을 재고하세요.

Line 42에서 퀴즈 로드 성공 시마다 startTimeMillis가 재설정됩니다. 이는 다음 시나리오에서 타이머가 재시작되어 부정확한 측정이 발생할 수 있습니다:

  • 네트워크 실패 후 재시도
  • 화면 재구성으로 인한 재로드

타이머가 첫 로드 시에만 시작되도록 보장하려면 다음과 같이 가드를 추가하는 것을 고려하세요:

 .onSuccess { quizModel ->
-    startTimeMillis = System.currentTimeMillis()
+    if (startTimeMillis == 0L) {
+        startTimeMillis = System.currentTimeMillis()
+    }
     intent { copy(quizModel = quizModel) }
 }

의도된 동작이라면 무시하셔도 됩니다.


59-59: 시간 복사가 중복될 수 있습니다.

Line 59에서 model.copy(time = time)을 수행하는데, 이미 계산된 time 값을 모델에 덮어쓰고 있습니다. 서버 응답의 model에 이미 올바른 시간이 포함되어 있다면 이 작업은 불필요할 수 있습니다.

서버 응답에 시간이 포함되지 않거나 클라이언트 측 시간을 사용해야 하는 명확한 이유가 있다면 무시하셔도 됩니다.

feature/quiz/src/main/java/com/teamwable/quiz/QuizMainFragment.kt (3)

24-32: 중복 네비게이션/중복 setContent 방지: distinctUntilChanged() 추가 권장

동일 값 재방출(또는 STARTED 재진입) 시 현재 로직은 매번 initComposeView()/navigateToStart()를 다시 호출할 수 있습니다. flicker나 불필요 재네비게이션을 줄이기 위해 중복 상태를 필터링하세요. 필요 시 최초 1회 처리라면 take(1)도 고려 가능합니다.

다음 최소 변경을 제안합니다:

-                viewModel.isQuizCompleted
-                    .filterNotNull()
-                    .collect { isCompleted ->
+                viewModel.isQuizCompleted
+                    .filterNotNull()
+                    .distinctUntilChanged()
+                    .collect { isCompleted ->

40-42: Fragment의 ViewModel과 Compose의 hiltViewModel 이중화 가능성 — 명시 전달 권장

Fragment에서 by viewModels()로 보유한 인스턴스와 Composable의 hiltViewModel()가 동일 오너를 보지 않으면 서로 다른 인스턴스가 생성될 수 있습니다. 명시적으로 전달해 오해와 디버깅 비용을 줄이세요. (QuizMainRoute 시그니처가 viewModel 파라미터를 허용함 — 제공된 스니펫 기준)

-                    QuizMainRoute(
-                        onBtnClick = ::navigateToGoogleForm,
-                    )
+                    QuizMainRoute(
+                        viewModel = viewModel,
+                        onBtnClick = ::navigateToGoogleForm,
+                    )

검증: 실제로 동일 인스턴스인지 확인 부탁드립니다(네비게이션/Compose 오너 변경 시 이슈 가능).


49-54: popUpTo(graph, inclusive=true) 반복 호출 시 깜빡임 가능 — 현재 목적지 가드 및 DSL 제안

이미 navigation_quiz_start에 있을 때도 재내비게이션합니다. 가드를 두면 불필요 작업과 잠재적 예외를 줄입니다. 또한 navOptions DSL로 간결화 가능합니다.

     private fun navigateToStart() {
-        val navOptions = NavOptions.Builder()
-            .setPopUpTo(R.id.graph_quiz, true)
-            .build()
-
-        findNavController().navigate(R.id.navigation_quiz_start, null, navOptions)
+        if (findNavController().currentDestination?.id == R.id.navigation_quiz_start) return
+        val options = NavOptions.Builder()
+            .setPopUpTo(R.id.graph_quiz, true)
+            .build()
+        findNavController().navigate(R.id.navigation_quiz_start, null, options)
     }

대안(선호, KTX 사용 시):

+import androidx.navigation.navOptions
...
-        val options = NavOptions.Builder()
-            .setPopUpTo(R.id.graph_quiz, true)
-            .build()
+        val options = navOptions { popUpTo(R.id.graph_quiz) { inclusive = true } }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c56d87f and 2044dba.

📒 Files selected for processing (5)
  • core/data/src/main/java/com/teamwable/data/worker/DailyTaskSchedulerImpl.kt (1 hunks)
  • core/designsystem/src/main/java/com/teamwable/designsystem/component/image/WableImage.kt (3 hunks)
  • core/domain/src/main/java/com/teamwable/domain/usecase/GetRemainingTimeUseCase.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/QuizMainFragment.kt (1 hunks)
  • feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/designsystem/src/main/java/com/teamwable/designsystem/component/image/WableImage.kt
🧰 Additional context used
🧬 Code graph analysis (2)
feature/quiz/src/main/java/com/teamwable/quiz/QuizMainFragment.kt (2)
feature/quiz/src/main/java/com/teamwable/quiz/QuizMainScreen.kt (1)
  • QuizMainRoute (31-42)
core/ui/src/main/java/com/teamwable/ui/extensions/FragmentExt.kt (1)
  • openUri (49-54)
feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt (1)
core/common/src/main/java/com/teamwable/common/base/BaseViewModel.kt (2)
  • postSideEffect (41-47)
  • intent (37-39)
🔇 Additional comments (12)
core/domain/src/main/java/com/teamwable/domain/usecase/GetRemainingTimeUseCase.kt (3)

1-11: 필요한 import가 모두 포함되어 있고 적절합니다.

kotlinx.coroutines Flow API와 java.time을 사용한 시간 계산이 올바르게 구성되었습니다.


12-14: Use case 구현이 적절합니다.

operator fun을 사용하여 호출 가능한 인터페이스를 제공하고, onStart로 초기값을 즉시 emit하는 것이 올바른 패턴입니다.


28-37: 자정까지 남은 시간 계산이 정확합니다.

Duration.between()을 사용한 시간 계산과 시/분 추출 로직이 올바르게 구현되었습니다.

core/data/src/main/java/com/teamwable/data/worker/DailyTaskSchedulerImpl.kt (3)

24-30: WorkRequest 제약 조건이 의도적으로 생략되었는지 확인하세요.

현재 WorkRequest에 제약 조건(네트워크, 배터리, 충전 상태 등)이 설정되어 있지 않습니다. 이는 자정 리셋 작업이 모든 조건에서 실행되어야 한다는 의도일 수 있지만, 배터리 최적화나 Doze 모드에서의 동작을 고려해야 합니다.

필요에 따라 제약 조건 추가를 고려하세요:

val dailyResetRequest = PeriodicWorkRequestBuilder<ResetQuizStatusWorker>(
    repeatInterval = 24,
    repeatIntervalTimeUnit = TimeUnit.HOURS,
)
    .setInitialDelay(initialDelay)
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
    .setConstraints(
        Constraints.Builder()
            .setRequiresBatteryNotLow(true) // 필요시
            .build()
    )
    .build()

32-36: KEEP 정책의 영향을 검토하세요.

ExistingPeriodicWorkPolicy.KEEP 정책은 이미 작업이 스케줄되어 있으면 새 요청을 무시합니다. 이는 다음을 의미합니다:

  • 앱이 재시작되어도 기존 작업이 유지됩니다 (좋음)
  • 하지만 스케줄링 로직을 변경해도 (예: 다른 시간, 다른 제약 조건) 기존 작업이 취소되기 전까지는 변경사항이 적용되지 않습니다
  • Line 22의 initialDelay 계산은 KEEP 정책으로 인해 대부분의 경우 사용되지 않습니다

일일 퀴즈 리셋의 경우 KEEP 정책이 적절할 수 있지만, 다음을 확인하세요:

  1. 앱 업데이트 시 스케줄링 로직 변경이 필요한 경우 REPLACE 또는 UPDATE 정책을 고려하세요
  2. 또는 버전 변경 시 기존 작업을 명시적으로 취소하는 마이그레이션 로직을 추가하세요

10-12: 검토 의견을 무시하세요.

프로젝트의 minSdk는 gradle/libs.versions.toml에서 28로 설정되어 있습니다. java.time.* API는 API 26부터 지원되므로, minSdk 28에서는 완전히 네이티브로 지원되며 desugaring이 불필요합니다. 코드에 API 호환성 문제가 없습니다.

Likely an incorrect or invalid review comment.

feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt (5)

1-22: 클래스 구조와 의존성 주입이 올바르게 구성되었습니다.

Hilt와 BaseViewModel을 활용한 구조가 적절하며, 필요한 리포지토리들이 constructor injection을 통해 주입되고 있습니다.


25-27: 초기 데이터 로딩 구조가 개선되었습니다.

이전 리뷰 코멘트에서 지적된 타이머 시작 시점 문제가 해결되었습니다. 타이머가 초기화 시점이 아닌 퀴즈 데이터 로드 성공 후(Line 42)에 시작되도록 변경되었습니다.


64-69: 퀴즈 완료 처리가 올바르게 구현되었습니다.

DataStore에 완료 상태를 저장하고 결과 화면으로 네비게이션하는 로직이 적절하게 구현되었습니다.


71-75: 경과 시간 계산이 올바르게 구현되었습니다.

밀리초를 초 단위로 변환하는 로직이 정확하며, 일반적인 퀴즈 시나리오에서 안전하게 동작합니다.


29-36: 원본 리뷰 댓글은 근거가 없습니다.

userAnswer는 별도의 속성이 아니라, oxType에서 자동으로 파생되는 계산된 속성입니다 (QuizStartState.kt 15-16행):

val userAnswer: Boolean
    get() = oxType == OXType.O

Line 34에서 oxType을 업데이트하면 userAnswer는 자동으로 올바른 값을 반환하므로, 별도의 수정이나 검증이 필요하지 않습니다. 코드는 의도대로 올바르게 작동합니다.

Likely an incorrect or invalid review comment.

feature/quiz/src/main/java/com/teamwable/quiz/QuizMainFragment.kt (1)

56-57: LGTM — 외부 링크 열기 예외 처리 적절

openUri로 예외 토스트 처리되어 UX 안전합니다.

Comment on lines +49 to +62
private fun submitQuiz() {
val time = getElapsedTime()
viewModelScope.launch {
quizRepository.patchQuizScore(
QuizScoreModel(
quizId = currentState.quizModel.quizId.toLong(),
userAnswer = currentState.userAnswer,
quizTime = time,
),
)
.onSuccess { model -> setQuizCompleted(model.copy(time = time)) }
.onFailure { postSideEffect(QuizStartSideEffect.ShowSnackBar(it)) }
}
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Oct 25, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

퀴즈 제출 전 상태 검증이 필요합니다.

다음과 같은 중대한 문제들이 있습니다:

  1. Line 54-55: currentState.quizModelcurrentState.userAnswer에 대한 null/초기화 검증이 없습니다. 퀴즈가 로드되기 전에 제출 버튼을 클릭하면 크래시가 발생할 수 있습니다.

  2. 사용자 답변 검증 누락: 사용자가 답변을 선택하지 않고 제출할 수 있습니다.

다음과 같이 검증 로직을 추가하세요:

 private fun submitQuiz() {
+    val quizModel = currentState.quizModel ?: run {
+        postSideEffect(QuizStartSideEffect.ShowSnackBar(Throwable("퀴즈를 로딩하는 중입니다.")))
+        return
+    }
+    
+    if (currentState.oxType == null) {
+        postSideEffect(QuizStartSideEffect.ShowSnackBar(Throwable("답변을 선택해주세요.")))
+        return
+    }
+    
     val time = getElapsedTime()
     viewModelScope.launch {
         quizRepository.patchQuizScore(
             QuizScoreModel(
-                quizId = currentState.quizModel.quizId.toLong(),
+                quizId = quizModel.quizId.toLong(),
                 userAnswer = currentState.userAnswer,
                 quizTime = time,
             ),
         )
             .onSuccess { model -> setQuizCompleted(model.copy(time = time)) }
             .onFailure { postSideEffect(QuizStartSideEffect.ShowSnackBar(it)) }
     }
 }
📝 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
private fun submitQuiz() {
val time = getElapsedTime()
viewModelScope.launch {
quizRepository.patchQuizScore(
QuizScoreModel(
quizId = currentState.quizModel.quizId.toLong(),
userAnswer = currentState.userAnswer,
quizTime = time,
),
)
.onSuccess { model -> setQuizCompleted(model.copy(time = time)) }
.onFailure { postSideEffect(QuizStartSideEffect.ShowSnackBar(it)) }
}
}
private fun submitQuiz() {
val quizModel = currentState.quizModel ?: run {
postSideEffect(QuizStartSideEffect.ShowSnackBar(Throwable("퀴즈를 로딩하는 중입니다.")))
return
}
if (currentState.oxType == null) {
postSideEffect(QuizStartSideEffect.ShowSnackBar(Throwable("답변을 선택해주세요.")))
return
}
val time = getElapsedTime()
viewModelScope.launch {
quizRepository.patchQuizScore(
QuizScoreModel(
quizId = quizModel.quizId.toLong(),
userAnswer = currentState.userAnswer,
quizTime = time,
),
)
.onSuccess { model -> setQuizCompleted(model.copy(time = time)) }
.onFailure { postSideEffect(QuizStartSideEffect.ShowSnackBar(it)) }
}
}
🤖 Prompt for AI Agents
In feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt
around lines 49 to 62, add pre-submit validation to avoid crashes and empty
submissions: check that currentState.quizModel is non-null and initialized and
that currentState.userAnswer is non-null/non-empty before calling
quizRepository.patchQuizScore; if validation fails, return early and invoke
postSideEffect(QuizStartSideEffect.ShowSnackBar(...)) with an appropriate
message (e.g., "Quiz not loaded" or "Please select an answer"); also guard the
quizId conversion (toLong()) by ensuring the quizId is present/parsable before
using it to build QuizScoreModel.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

submit quiz는 quiz가 선택 되고 enabled 버튼을 통해서 선택이 되는 구조야 퀴즈가 선택되지 않으면 저 함수 자체가 실행 되지 않아

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how the submit button is wired in QuizStartScreen
ast-grep --pattern $'BoardRequestButton(
  $$$
  enabled = $ENABLED
  $$$
)'

# Also check the enabled property in QuizStartState
ast-grep --pattern $'val enabled$$$get() = $$$'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Read QuizStartScreen to see button wiring
cat feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartScreen.kt | grep -A5 -B5 "enabled"

# Read QuizStartState to confirm enabled property
cat feature/quiz/src/main/java/com/teamwable/quiz/start/model/QuizStartState.kt

Length of output: 796


@chanubc, 확인했습니다! 말씀하신 대로 QuizStartScreen에서 버튼이 enabled = state.enabled로 설정되어 있고, QuizStartStateenabled 속성이 oxType != null을 반환하므로, 사용자가 O/X를 선택하기 전까지는 제출 버튼이 비활성화되어 있네요. UI 레벨에서 이미 검증이 되고 있어서 제가 제안한 추가 검증은 불필요합니다. 👍

(ㅇㅅㅇ)♪


✏️ Learnings added
Learnt from: chanubc
PR: Team-Wable/WABLE-ANDROID#194
File: feature/quiz/src/main/java/com/teamwable/quiz/start/QuizStartViewModel.kt:49-62
Timestamp: 2025-10-25T19:46:32.144Z
Learning: In feature/quiz QuizStartViewModel.kt, the submitQuiz() function does not need null-check validation for oxType or quizModel because the submit button in QuizStartScreen is only enabled when state.enabled is true (which means oxType != null). The UI enforces this constraint, preventing submitQuiz() from being called when no answer is selected.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown

@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)
core/designsystem/src/main/java/com/teamwable/designsystem/component/image/WableGlideImage.kt (1)

35-41: 실패 상태에서 modifier 재사용을 피하세요.

이미지 로드 실패 시 회색 배경을 표시하는 것은 좋은 UX 개선입니다. 하지만 failure 블록 내에서 부모 modifier를 재사용하는 것은 다음과 같은 문제를 일으킬 수 있습니다:

  • failure 컴포저블은 이미 GlideImage의 bounds 내에서 렌더링되므로, 부모의 크기 제약을 상속받습니다
  • 부모 modifier에는 이미 noRippleThrottleClickable과 크기 제약이 포함되어 있어 불필요하게 중복됩니다
  • fillMaxSize()는 부모의 크기 제약과 충돌할 수 있습니다

다음과 같이 수정하여 새로운 Modifier를 사용하는 것을 권장합니다:

         failure = {
             Box(
-                modifier = modifier
+                modifier = Modifier
                     .fillMaxSize()
                     .background(WableTheme.colors.gray100),
             )
         },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2044dba and 4ffcd3c.

📒 Files selected for processing (1)
  • core/designsystem/src/main/java/com/teamwable/designsystem/component/image/WableGlideImage.kt (3 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: PR Checker
🔇 Additional comments (1)
core/designsystem/src/main/java/com/teamwable/designsystem/component/image/WableGlideImage.kt (1)

3-5: LGTM!

실패 상태 UI를 위한 필요한 import가 적절하게 추가되었습니다.

Also applies to: 17-17

Copy link
Copy Markdown
Collaborator

@sohyun127 sohyun127 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!!

@chanubc chanubc merged commit 95e5479 into develop Oct 26, 2025
2 checks passed
@chanubc chanubc deleted the feature/#192-quiz-api branch October 26, 2025 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

☕ [COMPOSE] 컴포즈 ui ⭐ [FEAT] 새로운 기능 구현 🐻 찬우 차은우

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] : 퀴즈 화면 서버통신 / workmanager도입

2 participants