Skip to content

20260217 #27 타이머 할 일 연동 UI 전반 개선#28

Merged
EM-H20 merged 33 commits intomainfrom
20260217_#27_타이머_할_일_연동_UI_전반_개선
Feb 19, 2026

Hidden character warning

The head ref may contain hidden characters: "20260217_#27_\ud0c0\uc774\uba38_\ud560_\uc77c_\uc5f0\ub3d9_UI_\uc804\ubc18_\uac1c\uc120"
Merged

20260217 #27 타이머 할 일 연동 UI 전반 개선#28
EM-H20 merged 33 commits intomainfrom
20260217_#27_타이머_할_일_연동_UI_전반_개선

Conversation

@EM-H20
Copy link
Contributor

@EM-H20 EM-H20 commented Feb 19, 2026

✨ 변경 사항


✅ 테스트


  • 수동 테스트 완료
  • 테스트 코드 완료

Summary by CodeRabbit

  • New Features

    • 타이머 기록 화면 추가(날짜별 세션 그룹, 페이징 및 통계)
    • 캘린더: 날짜 하단 체크박스/플래닛 스타일 마커 및 제목 탭으로 월↔주 토글
    • 타이머 종료 시 세션 요약 대화 표시 및 누적 시간 반영
  • Bug Fixes

    • 텍스트 필드 배경 변경으로 테두리 겹침 해결
    • 게스트 로그인/로그아웃 시 히스토리·캐시 완전 초기화
    • 활성 타이머에 연결된 할일 삭제 차단
    • 로그아웃 확인 대화 통일(파괴적 확인 버튼)
  • UI/Style

    • 캘린더 셀 비주얼(행성/궤도 스타일), 간격·패딩·스낵바 시각 조정

 
- DragHandle 공통 위젯 추출 (4곳 중복 제거)
- CalendarHeader 공통 위젯 추출 (2곳 중복 제거)
- 타이머 통계 Consumer 격리 (매초 불필요한 리빌드 방지)
- AppPadding.bottomSheetTitlePadding 프리셋 추가 (3곳 하드코딩 제거)
- clearSessions 등 미사용 데드코드 삭제
- _formatMinutes() 중복 제거 → timer_format_utils.dart 추출
- timer_history_screen ref.watch → .select((g) => g.isEmpty) 최적화
- timer_session_provider 도달 불가 조건문 제거
- category_add_bottom_sheet 인라인 드래그 핸들 → DragHandle 위젯
- AppPadding.bottomSheetTitlePadding / horizontal20 상수 적용
- spacing_and_radius.dart Legacy 호환 상수 5개 삭제 + app_button.dart 반영
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Walkthrough

타이머 세션 영속화(모델/로컬 저장소/리포지토리/프로바이더), 통계 제공자 분리, TimerHistory 화면(페이지네이션) 추가, SpaceCalendar 헤더·셀 리디자인 및 마커 변경, 게스트 세션 데이터 클리어 강화, 여러 UI 위젯(DragHandle, CalendarHeader, AppTextField 등) 및 레이아웃 상수 조정이 도입되었습니다.

Changes

Cohort / File(s) Summary
타이머 도메인 & 모델
lib/features/timer/domain/entities/timer_session_entity.dart, .../timer_session_entity.freezed.dart, lib/features/timer/data/models/timer_session_model.dart, .../timer_session_model.freezed.dart, .../timer_session_model.g.dart
TimerSessionEntity/TimerSessionModel(Freezed) 및 JSON 직렬화/변환 추가.
로컬 저장소 & 리포지토리
lib/features/timer/data/datasources/timer_session_local_datasource.dart, lib/features/timer/data/repositories/timer_session_repository_impl.dart, lib/features/timer/domain/repositories/timer_session_repository.dart
SharedPreferences 기반 로컬 데이터소스와 RepositoryImpl 추가( get/save/add/clearAll ).
타이머 프로바이더 계층
lib/features/timer/presentation/providers/timer_session_provider.dart, .../timer_session_provider.g.dart, lib/features/timer/presentation/providers/study_stats_provider.dart, .../study_stats_provider.g.dart, lib/features/timer/presentation/providers/timer_provider.dart, .../timer_provider.g.dart
세션 리스트 노티파이어·sorted DateGroup 제공자 추가, 통계 제공자 분리. timer_provider.stop() 시그니처 변경 및 세션 저장/실제분 누적 로직 추가.
타이머 UI & 히스토리 화면
lib/features/timer/presentation/screens/timer_screen.dart, lib/features/timer/presentation/screens/timer_history_screen.dart, lib/features/timer/presentation/utils/timer_format_utils.dart
TimerHistoryScreen(페이징) 추가, timer 화면의 정지 흐름 결과 반환·다이얼로그 표시, formatMinutes/normalizeDate 유틸 추가.
캘린더/헤더 및 관련 UI
lib/core/widgets/atoms/calendar_header.dart, lib/features/home/presentation/widgets/space_calendar.dart, lib/features/todo/presentation/widgets/todo_add_bottom_sheet.dart
CalendarHeader 위젯 추가, SpaceCalendar에서 헤더 분리 및 셀 디자인(planet/orbit)·마커 로직 변경, bottom sheet 캘린더 통합 변경.
코어 UI 위젯 및 상수
lib/core/widgets/atoms/drag_handle.dart, lib/core/widgets/inputs/app_text_field.dart, lib/core/widgets/feedback/app_snackbar.dart, lib/core/widgets/buttons/app_button.dart, lib/core/constants/spacing_and_radius.dart
DragHandle 추가, AppTextField에 showBorder 옵션·배경 색상 일관화, SnackBar 레이아웃 조정, 반응형 간격 사용, bottomSheetTitlePadding 추가 및 일부 레거시 spacing 제거.
할일 삭제 보호 및 선택 UI
lib/features/todo/presentation/widgets/dismissible_todo_item.dart, lib/features/todo/presentation/screens/todo_list_screen.dart, lib/features/todo/presentation/widgets/todo_select_bottom_sheet.dart, lib/features/todo/presentation/widgets/category_add_bottom_sheet.dart
타이머 연동된 할일의 스와이프/배치 삭제 차단 로직 추가, TodoSelectBottomSheet에 카테고리·누적시간 표시 및 DragHandle/패딩 적용, AppTextField showBorder 사용.
인증/게스트 흐름 및 프로필/소셜
lib/features/auth/presentation/providers/auth_provider.dart, lib/features/profile/presentation/screens/profile_screen.dart, lib/features/social/presentation/screens/social_screen.dart
게스트 로그인/로그아웃 시 todo/timer/category clearAll 및 관련 프로바이더 invalidate 추가. 로그아웃 다이얼로그에 isDestructive 및 게스트 안내 적용(호출부 변경).
라우터 및 앱 진입
lib/routes/app_router.dart, lib/main.dart
TimerHistoryScreen으로 라우트 연결, main.dart에서 SharedPreferences 준비 시 TimerSessionLocalDataSource 프로바이더 오버라이드 추가 및 일부 사용되지 않는 위젯 제거(HomePage).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as Client(UI)
participant Timer as TimerNotifier
participant TodoRepo as TodoRepository
participant SessionRepo as TimerSessionRepository
participant LocalPrefs as SharedPreferences
UI->>Timer: stop() 호출
Note right of Timer: startedAt/endedAt 캡처, sessionDuration 계산
Timer->>TodoRepo: _updateTodoActualMinutes(todoId, minutes)
TodoRepo-->>Timer: 새로운 totalMinutes (또는 null)
Timer->>SessionRepo: addSession(TimerSessionEntity)
SessionRepo->>LocalPrefs: saveSessions(json)
LocalPrefs-->>SessionRepo: 저장 완료
SessionRepo-->>Timer: 완료
Timer-->>UI: 결과 반환 (sessionDuration, todoTitle?, totalMinutes?)
UI->>UI: 결과 다이얼로그 표시

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 반영합니다. 타이머와 할 일 연동 UI 개선이라는 핵심 목표가 잘 드러나 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 20260217_#27_타이머_할_일_연동_UI_전반_개선

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

🧹 Nitpick comments (14)
lib/features/todo/data/datasources/local_todo_datasource.dart (1)

64-68: 릴리즈 빌드에서 불필요한 역직렬화 실행 — kDebugMode 가드 추가 권장

debugPrint는 Flutter 릴리즈 빌드에서 자동으로 제거되지 않습니다(출력 속도만 제한). 따라서 64–65번 줄의 getTodos() / getCategories() 호출(JSON 전체 역직렬화)은 로그 출력 여부와 무관하게 릴리즈 모드에서도 매번 실행됩니다.

kDebugMode const 가드로 계수 계산과 로그를 묶으면, 프로덕션 빌드에서 컴파일 타임에 완전히 제거됩니다.

♻️ 개선 제안
 Future<void> clearAll() async {
+    if (kDebugMode) {
+      final todoCount = getTodos().length;
+      final categoryCount = getCategories().length;
+      debugPrint('🧹 Todo 캐시 삭제 완료 (할일: $todoCount개, 카테고리: $categoryCount개)');
+    }
     await _prefs.remove(_todosKey);
     await _prefs.remove(_categoriesKey);
 }

또는 카운트 정보가 불필요하면 더 간단하게:

 Future<void> clearAll() async {
+    if (kDebugMode) {
+      debugPrint('🧹 Todo 캐시 삭제 완료');
+    }
     await _prefs.remove(_todosKey);
     await _prefs.remove(_categoriesKey);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/todo/data/datasources/local_todo_datasource.dart` around lines
64 - 68, The calls to getTodos() and getCategories() are being executed even in
release builds because debugPrint doesn't remove their deserialization cost;
wrap the count computation and debugPrint call in a kDebugMode guard (import
foundation.dart if needed) so that getTodos(), getCategories(), and the
debugPrint are only executed in debug builds, or alternatively remove the count
computation entirely and just call _prefs.remove(_todosKey) and
_prefs.remove(_categoriesKey) if the counts are not needed; update the block
that currently calls getTodos().length, getCategories().length and debugPrint
accordingly.
lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart (1)

138-138: 매직 넘버 8.h 대신 AppSpacing 상수를 사용하세요.

파일 전체에서 AppSpacing.s12, AppSpacing.s4 등 상수를 일관되게 사용하고 있지만 이 위치만 하드코딩된 값입니다.

✨ 수정 제안
-     padding: EdgeInsets.only(bottom: 8.h),
+     padding: EdgeInsets.only(bottom: AppSpacing.s8),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart` at
line 138, Replace the hardcoded EdgeInsets.only(bottom: 8.h) in the
TodoSelectBottomSheet widget with the existing spacing constant (e.g.,
AppSpacing.s8) to keep spacing consistent with other uses like AppSpacing.s12
and AppSpacing.s4; locate the padding expression (EdgeInsets.only(bottom: 8.h))
and swap it to use AppSpacing.s8 so the file uses the shared spacing constants
throughout.
lib/features/todo/presentation/widgets/todo_add_bottom_sheet.dart (1)

374-377: _calendarFocusedDay 할당이 setState 블록 밖에서 이루어집니다.

_toggleDate가 내부적으로 setState를 호출하므로 실제로 동작하지만, 상태 변경을 setState 밖에서 하는 것은 의도를 모호하게 만듭니다. _toggleDate 내에서 focusedDay도 함께 업데이트하거나, 여기서 별도의 setState로 감싸는 것이 더 명확합니다.

♻️ 제안: setState 내에서 상태 변경 통합
 onDaySelected: (selectedDay, focusedDay) {
-  _toggleDate(selectedDay);
-  _calendarFocusedDay = focusedDay;
+  setState(() {
+    final normalized = DateTime(selectedDay.year, selectedDay.month, selectedDay.day);
+    final index = _selectedScheduledDates.indexWhere((d) => d == normalized);
+    if (index >= 0) {
+      _selectedScheduledDates.removeAt(index);
+    } else {
+      _selectedScheduledDates.add(normalized);
+    }
+    _calendarFocusedDay = focusedDay;
+  });
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/todo/presentation/widgets/todo_add_bottom_sheet.dart` around
lines 374 - 377, onDaySelected currently calls _toggleDate(selectedDay) which
itself calls setState, but assigns _calendarFocusedDay = focusedDay outside of
any setState; move the focused-day update into the same state update to make
intent explicit: either modify _toggleDate to accept focusedDay and update
_calendarFocusedDay inside its setState, or wrap both calls in a single setState
inside onDaySelected (update _calendarFocusedDay and call the logic that
_toggleDate performs without duplicating state changes). Reference:
onDaySelected, _toggleDate, and _calendarFocusedDay.
lib/core/widgets/buttons/app_button.dart (1)

256-263: 로딩 인디케이터의 width/height에 서로 다른 단위(.w / .h)를 사용하고 있습니다.

CircularProgressIndicator는 정사각형 컨테이너가 필요합니다. .w.h는 화면 비율에 따라 다른 값을 반환하므로, 비표준 화면 비율에서 타원형으로 렌더링될 수 있습니다. 둘 다 20.w로 통일하는 것이 안전합니다.

♻️ 제안: 동일한 단위로 통일
   return SizedBox(
     width: 20.w,
-    height: 20.h,
+    height: 20.w,
     child: CircularProgressIndicator(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/core/widgets/buttons/app_button.dart` around lines 256 - 263, SizedBox
currently sets width with 20.w and height with 20.h which can produce a
non-square (oval) loader on some screen ratios; change the height to use the
same unit as width so the CircularProgressIndicator is rendered square (set
height to 20.w), keeping the existing strokeWidth and valueColor usage around
CircularProgressIndicator and _effectiveForegroundColor.
lib/core/widgets/atoms/calendar_header.dart (1)

74-98: 화살표 버튼의 터치 영역이 다소 작을 수 있습니다.

showArrowBackground: false 경로에서 패딩 8.w + 아이콘 20.w ≈ 36dp 정도입니다. Material 가이드라인 권장 최소 터치 영역은 48dp입니다. 접근성을 고려하면 SizedBox로 최소 영역을 확보하거나, GestureDetector 대신 IconButton(기본 48dp 터치 영역)을 사용하는 것을 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/core/widgets/atoms/calendar_header.dart` around lines 74 - 98, The touch
target in _buildArrowButton when showArrowBackground is false is too small
(~36dp); update the widget to ensure a minimum 48dp tappable area by replacing
or wrapping the current GestureDetector/Padding+Icon with either an IconButton
(which provides a 48dp default touch target) or wrap the child in a SizedBox
with minWidth/minHeight 48.w (or use constraints) while keeping onTap wired to
the existing onTap; ensure the change is applied only in the showArrowBackground
== false branch so visual layout for the backgrounded variant remains unchanged.
lib/core/widgets/inputs/app_text_field.dart (1)

315-315: AppPadding.all16은 모든 방향에 16.w를 적용합니다.

기존 코드에서 vertical: 16.h를 사용했다면, all16EdgeInsets.all(16.w)이므로 수직 방향도 .w 기반으로 변경됩니다. 실질적 차이는 미미하지만 의도된 변경인지 확인해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/core/widgets/inputs/app_text_field.dart` at line 315, The contentPadding
currently uses AppPadding.all16 which applies 16.w to all sides (including
vertical), but the original intent used vertical: 16.h; confirm whether vertical
padding should use .h; if not intentional, replace AppPadding.all16 with a
symmetric padding that preserves vertical: 16.h and horizontal: 16.w (or use the
existing AppPadding.vertical16/AppPadding.symmetric helper) so contentPadding
uses the correct units; check the code around contentPadding and
AppPadding.all16 to make the change.
lib/features/home/presentation/widgets/space_calendar.dart (2)

175-211: 중첩 삼항 연산자를 별도 메서드나 switch expression으로 분리 고려

isPlanet ? ... : isOrbit ? ... : ... 패턴이 30줄 이상에 걸쳐 있어 가독성이 떨어집니다. Dart 3의 switch expression이나 별도의 헬퍼 메서드로 분리하면 각 스타일의 경계가 더 명확해집니다.

♻️ switch expression 활용 예시
-    final decoration = isPlanet
-        ? BoxDecoration(
-            shape: BoxShape.circle,
-            gradient: RadialGradient(
-              center: const Alignment(-0.3, -0.3),
-              radius: 0.9,
-              colors: [
-                AppColors.primary.withValues(alpha: 0.6),
-                AppColors.primaryDark.withValues(alpha: 0.3),
-              ],
-            ),
-            border: Border.all(
-              color: AppColors.primaryLight.withValues(alpha: 0.5),
-              width: 1.5,
-            ),
-            boxShadow: [
-              BoxShadow(
-                color: AppColors.primary.withValues(alpha: 0.4),
-                blurRadius: 12,
-                spreadRadius: 3,
-              ),
-            ],
-          )
-        : isOrbit
-        ? BoxDecoration(
-            shape: BoxShape.circle,
-            gradient: RadialGradient(
-              center: Alignment.center,
-              radius: 0.8,
-              colors: [
-                AppColors.secondary.withValues(alpha: 0.15),
-                Colors.transparent,
-              ],
-            ),
-            border: Border.all(color: AppColors.secondaryLight, width: 1.5),
-          )
-        : const BoxDecoration(shape: BoxShape.circle);
+    final decoration = switch ((isPlanet, isOrbit)) {
+      (true, _) => _planetDecoration(),
+      (_, true) => _orbitDecoration(),
+      _         => const BoxDecoration(shape: BoxShape.circle),
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/home/presentation/widgets/space_calendar.dart` around lines 175
- 211, The nested ternary that assigns decoration (isPlanet ? ... : isOrbit ?
... : ...) is hard to read; refactor it into a separate helper like
buildDecoration(...) or use a Dart 3 switch expression on a style enum/flag to
return the appropriate BoxDecoration for the planet, orbit, and default cases.
Locate the current decoration assignment (variable name decoration) and replace
the ternary with a call to the new function (e.g., BoxDecoration
buildDecoration({bool isPlanet, bool isOrbit}) or switch on a DecorationType)
that constructs each BoxDecoration (the planet gradient/border/shadow block, the
orbit gradient/border block, and the default circle) so each branch is isolated
and clearer.

49-56: CalendarHeader 탐색과 TableCalendar의 onPageChanged 이중 호출 가능성

CalendarHeader의 화살표 버튼이 onPageChanged를 직접 호출하면 부모가 focusedDay를 업데이트하고, 이로 인해 TableCalendar가 페이지를 전환하면서 자체 onPageChanged(Line 77)를 다시 발생시킬 수 있습니다. 부모의 핸들러가 멱등(idempotent)하면 기능상 문제가 없지만, 불필요한 리빌드가 발생할 수 있습니다.

또한 TableCalendar의 페이지 전환 애니메이션 없이 즉시 점프되므로, 사용자 경험에 미세한 차이가 생길 수 있습니다.

#!/bin/bash
# CalendarHeader의 onPageChanged 콜백이 부모에서 어떻게 처리되는지 확인
rg -n --type=dart 'onPageChanged' -C5 -g '!*.freezed.dart' -g '!*.g.dart'
lib/features/auth/presentation/providers/auth_provider.dart (1)

336-348: 로그아웃 실패 시 이전 상태로 복원 — 사용자 피드백 부재 가능성

에러 상태 대신 이전 상태로 복원하는 것은 UX 관점에서 좋은 선택이나, 로그아웃 실패 시 사용자에게 어떤 피드백도 전달되지 않습니다. debugPrint는 개발자만 확인 가능합니다.

호출부에서 signOut()의 성공/실패를 구분할 수 있도록 Future<bool>을 반환하거나, 별도의 에러 콜백 메커니즘을 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/auth/presentation/providers/auth_provider.dart` around lines 336
- 348, The sign-out flow in the auth provider silently restores the previous
state on failure and only calls debugPrint, so callers can’t detect failures;
change the sign-out contract and handling so signOut reports success/failure:
update the use case behind signOutUseCaseProvider (the useCase.execute call) to
return a Future<bool> (true on success, false or throw on failure) and update
the provider method that calls useCase.execute to await that boolean and return
it to the caller (or propagate an error), while keeping state transitions (set
AsyncValue.loading(), then AsyncValue.data(null) on success, previous on
failure) and replacing debugPrint with a user-visible error path (returning
false or rethrowing) so callers can show feedback. Ensure you reference
signOutUseCaseProvider and the provider method that currently calls
useCase.execute when applying the change.
docs/plans/2026-02-18-guest-timer-history-clear-bug.md (1)

17-23: 코드 블록에 언어 지정 누락.

정적 분석 도구(markdownlint MD040)에서 경고한 대로, 펜스 코드 블록에 언어가 지정되지 않았습니다.

제안 수정
-```
+```text
 AuthNotifier.signOut() (게스트)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-02-18-guest-timer-history-clear-bug.md` around lines 17 - 23,
The fenced code block in the markdown contains the snippet starting with
"AuthNotifier.signOut() (게스트)" and lacks a language identifier, causing
markdownlint MD040; update the triple-backtick fence to include a language
(e.g., use "text") so the block becomes a labeled code fence and the linter
warning is resolved, ensuring the block that lists todoRepo.clearAll(),
timerSessionRepo.clearAll(), and the 'guest_timer_sessions' key is annotated.
docs/plans/2026-02-17-timer-integration-ui-improvement.md (1)

426-433: 계획 문서의 _formatMinutes와 실제 구현이 다릅니다.

이 계획에서는 _formatMinutes_TimerScreenState의 로컬 메서드로 정의하지만, 실제 timer_screen.dart 구현에서는 timer_format_utils.dart에서 formatMinutes를 import하여 사용하고 있습니다. 유틸리티 함수로 추출한 것이 더 좋은 접근이므로 문제는 아니지만, 계획 문서가 실제 구현과 달라 혼동될 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-02-17-timer-integration-ui-improvement.md` around lines 426 -
433, The plan document defines a local method named _formatMinutes under
_TimerScreenState but the real implementation uses a utility function
formatMinutes from timer_format_utils.dart and imports it in timer_screen.dart;
update the plan to match the code by replacing the local-method description with
a note that formatMinutes in timer_format_utils.dart is used (or alternatively
describe why you would keep a local _formatMinutes), referencing _formatMinutes,
_TimerScreenState, formatMinutes, timer_format_utils.dart and timer_screen.dart
so readers understand the actual source and location of the formatting logic.
lib/features/timer/data/datasources/timer_session_local_datasource.dart (1)

19-28: _prefs.remove() 호출이 await 없이 실행됩니다.

catch 블록에서 _prefs.remove(_sessionsKey)Future<bool>을 반환하지만 await되지 않습니다. getSessions()가 동기 메서드(List 반환)이므로 async로 바꾸기는 어렵지만, 손상된 데이터가 제거되지 않은 채 다음 호출에서 다시 파싱 실패할 수 있습니다. 또한 오류 로깅이 없어 데이터 손상 디버깅이 어렵습니다.

제안: 디버그 로그 추가 및 unawaited 명시
     } catch (_) {
-      _prefs.remove(_sessionsKey);
+      debugPrint('⚠️ TimerSession JSON 파싱 실패 — 캐시 삭제');
+      unawaited(_prefs.remove(_sessionsKey));
       return [];
     }

unawaited를 사용하려면 dart:async에서 import하거나, 메서드 시그니처를 Future<List<TimerSessionModel>>로 변경하여 await하는 방법도 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/timer/data/datasources/timer_session_local_datasource.dart`
around lines 19 - 28, In getSessions() catch block, await is missing for
_prefs.remove(_sessionsKey) and there is no error logging; import dart:async and
call unawaited(_prefs.remove(_sessionsKey)) (or change getSessions() to async
and await the removal), and add a debug/error log that includes the caught
exception and stack trace so corrupted data removal is observable; reference the
symbols _prefs.remove, _sessionsKey, getSessions(), and
TimerSessionModel.fromJson when adding the log and removal call.
docs/plans/2026-02-18-guest-mode-history-clear-bug.md (1)

13-15: 마크다운 제목 레벨 건너뛰기.

markdownlint(MD001)에 따르면, h2(##) 없이 바로 h3(###)으로 점프하고 있습니다. ## Root Cause Analysis로 변경하면 문서 구조가 더 일관됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-02-18-guest-mode-history-clear-bug.md` around lines 13 - 15,
문서에서 제목 레벨이 건너뛰여 markdownlint(MD001)을 유발하니 현재의 "### 현재 흐름" 헤더를 "## 현재 흐름"으로 변경해
문서 내 "## Root Cause Analysis"와 같은 h2 레벨로 맞추고, 다른 섹션("## Root Cause Analysis",
"### 현재 흐름")의 헤더 레벨이 연속성이 있도록 검토하세요.
lib/features/timer/presentation/providers/timer_session_provider.dart (1)

38-42: addSession에 오류 처리가 없습니다.

await repository.addSession(session) 실패 시 예외가 호출자로 전파되고 state는 갱신되지 않아 세션이 유실됩니다. 최소한 예외를 catch하여 실패 여부를 로깅하거나 호출자가 대응할 수 있도록 처리를 추가하세요.

♻️ 오류 처리 추가 제안
  Future<void> addSession(TimerSessionEntity session) async {
    final repository = ref.read(timerSessionRepositoryProvider);
-   await repository.addSession(session);
-   state = repository.getSessions();
+   try {
+     await repository.addSession(session);
+     state = repository.getSessions();
+   } catch (e, st) {
+     // 저장 실패 시 기존 세션 목록 유지
+     debugPrint('세션 저장 실패: $e\n$st');
+   }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/timer/presentation/providers/timer_session_provider.dart` around
lines 38 - 42, Wrap the call to repository.addSession(session) inside a
try/catch in addSession so failures are handled: call
ref.read(timerSessionRepositoryProvider) as before, then try awaiting
repository.addSession(session) and on success set state =
repository.getSessions(); in the catch (catch Exception e, StackTrace st) log
the error (include e and st) with your project's logger and either rethrow or
return a failure result so callers can respond; ensure you reference addSession,
timerSessionRepositoryProvider, repository.addSession, and state =
repository.getSessions() when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/plans/2026-02-17-textfield-border-overlap-fix.md`:
- Around line 15-29: The markdown code fences in the document use plain ```
without a language tag (MD040); update both diagram blocks by changing their
opening fences from ``` to ```text so the static analyzer recognizes them as
text blocks—locate the two blocks that start with ``` above the diagrams showing
"바텀시트 Container ..." and replace each opening fence with ```text.

In `@lib/features/home/presentation/widgets/space_calendar.dart`:
- Around line 131-149: The star marker in markerBuilder uses a fixed Icon size
(18.w) and Positioned(bottom: 1.h) which can overlap the date text because the
cell size is 36.w; change the layout to size the icon relative to the cell and
move it lower to avoid overlap—for example compute iconSize as a fraction of the
cell (e.g., cellSize * 0.25 or min(availableWidth * 0.25, 18.w)) instead of
hardcoding 18.w, and replace the Positioned(bottom: 1.h) with a layout that
reserves space (use Align/Column with SizedBox spacer or a Container with fixed
height matching the cell) so the icon is placed beneath the date text; update
references in the markerBuilder/Positioned/Icon usage to use the new computed
iconSize and reserved spacing.

In `@lib/features/timer/presentation/providers/timer_provider.dart`:
- Around line 86-111: You capture sessionDuration but call DateTime.now() after
awaiting _updateTodoActualMinutes, which makes startedAt/endedAt slightly late;
fix by capturing a single timestamp (e.g., final now = DateTime.now())
immediately before any awaits and use that when constructing TimerSessionEntity
(use now.subtract(sessionDuration) and now for startedAt/endedAt) and then call
_updateTodoActualMinutes(todoId, elapsedMinutes) and await addSession(session)
in any order that preserves using the captured now; update references inside the
block where TimerSessionEntity is created and where addSession on
timerSessionListNotifierProvider.notifier is invoked so both use the preserved
timestamp.

In `@lib/features/timer/presentation/utils/timer_format_utils.dart`:
- Around line 7-11: formatMinutes currently returns "X시간 0분" for exact hours;
change it so when minutes == 0 it returns only "$hours시간" (omit the minutes
part). In the formatMinutes function, after computing hours and minutes, branch
so that if hours > 0 and minutes == 0 you return only the hours string, if hours
> 0 and minutes > 0 return "$hours시간 $minutes분", otherwise return "$minutes분".
Update uses of formatMinutes if any assumptions expect the trailing "0분".

In `@lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart`:
- Around line 213-216: The displayed string for accumulated actual minutes lacks
a label; update the items.add call in todo_select_bottom_sheet.dart (where
todo.actualMinutes is handled) to include a clear label (e.g., "실제
${todo.actualMinutes}분" or "누적 ${todo.actualMinutes}분") instead of just
'${todo.actualMinutes}분'; locate the block around the if (todo.actualMinutes !=
null && todo.actualMinutes! > 0) check and change items.add to prepend the
chosen label so it’s unambiguous when shown alongside the expected time label.
- Around line 41-42: 현재 코드에서 categoryListNotifierProvider를 todosAsync.when(...)
내부에서 조건부로 ref.watch하고 있어 Riverpod 의존성 그래프가 재빌드마다 변경됩니다; 해결하려면 todosAsync.when을
호출하기 전에 최상단에서 항상 ref.watch(categoryListNotifierProvider)를 호출해 categoriesAsync
(또는 categories) 값을 미리 얻고, 이후 todosAsync.when(data: ...) 내부에서는 이 미리 획득한
categories/ categoriesAsync 변수를 사용하도록 변경하세요 — 참조 식별자: ref.watch,
categoryListNotifierProvider, todosAsync.when.

---

Nitpick comments:
In `@docs/plans/2026-02-17-timer-integration-ui-improvement.md`:
- Around line 426-433: The plan document defines a local method named
_formatMinutes under _TimerScreenState but the real implementation uses a
utility function formatMinutes from timer_format_utils.dart and imports it in
timer_screen.dart; update the plan to match the code by replacing the
local-method description with a note that formatMinutes in
timer_format_utils.dart is used (or alternatively describe why you would keep a
local _formatMinutes), referencing _formatMinutes, _TimerScreenState,
formatMinutes, timer_format_utils.dart and timer_screen.dart so readers
understand the actual source and location of the formatting logic.

In `@docs/plans/2026-02-18-guest-mode-history-clear-bug.md`:
- Around line 13-15: 문서에서 제목 레벨이 건너뛰여 markdownlint(MD001)을 유발하니 현재의 "### 현재 흐름"
헤더를 "## 현재 흐름"으로 변경해 문서 내 "## Root Cause Analysis"와 같은 h2 레벨로 맞추고, 다른 섹션("##
Root Cause Analysis", "### 현재 흐름")의 헤더 레벨이 연속성이 있도록 검토하세요.

In `@docs/plans/2026-02-18-guest-timer-history-clear-bug.md`:
- Around line 17-23: The fenced code block in the markdown contains the snippet
starting with "AuthNotifier.signOut() (게스트)" and lacks a language identifier,
causing markdownlint MD040; update the triple-backtick fence to include a
language (e.g., use "text") so the block becomes a labeled code fence and the
linter warning is resolved, ensuring the block that lists todoRepo.clearAll(),
timerSessionRepo.clearAll(), and the 'guest_timer_sessions' key is annotated.

In `@lib/core/widgets/atoms/calendar_header.dart`:
- Around line 74-98: The touch target in _buildArrowButton when
showArrowBackground is false is too small (~36dp); update the widget to ensure a
minimum 48dp tappable area by replacing or wrapping the current
GestureDetector/Padding+Icon with either an IconButton (which provides a 48dp
default touch target) or wrap the child in a SizedBox with minWidth/minHeight
48.w (or use constraints) while keeping onTap wired to the existing onTap;
ensure the change is applied only in the showArrowBackground == false branch so
visual layout for the backgrounded variant remains unchanged.

In `@lib/core/widgets/buttons/app_button.dart`:
- Around line 256-263: SizedBox currently sets width with 20.w and height with
20.h which can produce a non-square (oval) loader on some screen ratios; change
the height to use the same unit as width so the CircularProgressIndicator is
rendered square (set height to 20.w), keeping the existing strokeWidth and
valueColor usage around CircularProgressIndicator and _effectiveForegroundColor.

In `@lib/core/widgets/inputs/app_text_field.dart`:
- Line 315: The contentPadding currently uses AppPadding.all16 which applies
16.w to all sides (including vertical), but the original intent used vertical:
16.h; confirm whether vertical padding should use .h; if not intentional,
replace AppPadding.all16 with a symmetric padding that preserves vertical: 16.h
and horizontal: 16.w (or use the existing
AppPadding.vertical16/AppPadding.symmetric helper) so contentPadding uses the
correct units; check the code around contentPadding and AppPadding.all16 to make
the change.

In `@lib/features/auth/presentation/providers/auth_provider.dart`:
- Around line 336-348: The sign-out flow in the auth provider silently restores
the previous state on failure and only calls debugPrint, so callers can’t detect
failures; change the sign-out contract and handling so signOut reports
success/failure: update the use case behind signOutUseCaseProvider (the
useCase.execute call) to return a Future<bool> (true on success, false or throw
on failure) and update the provider method that calls useCase.execute to await
that boolean and return it to the caller (or propagate an error), while keeping
state transitions (set AsyncValue.loading(), then AsyncValue.data(null) on
success, previous on failure) and replacing debugPrint with a user-visible error
path (returning false or rethrowing) so callers can show feedback. Ensure you
reference signOutUseCaseProvider and the provider method that currently calls
useCase.execute when applying the change.

In `@lib/features/home/presentation/widgets/space_calendar.dart`:
- Around line 175-211: The nested ternary that assigns decoration (isPlanet ?
... : isOrbit ? ... : ...) is hard to read; refactor it into a separate helper
like buildDecoration(...) or use a Dart 3 switch expression on a style enum/flag
to return the appropriate BoxDecoration for the planet, orbit, and default
cases. Locate the current decoration assignment (variable name decoration) and
replace the ternary with a call to the new function (e.g., BoxDecoration
buildDecoration({bool isPlanet, bool isOrbit}) or switch on a DecorationType)
that constructs each BoxDecoration (the planet gradient/border/shadow block, the
orbit gradient/border block, and the default circle) so each branch is isolated
and clearer.

In `@lib/features/timer/data/datasources/timer_session_local_datasource.dart`:
- Around line 19-28: In getSessions() catch block, await is missing for
_prefs.remove(_sessionsKey) and there is no error logging; import dart:async and
call unawaited(_prefs.remove(_sessionsKey)) (or change getSessions() to async
and await the removal), and add a debug/error log that includes the caught
exception and stack trace so corrupted data removal is observable; reference the
symbols _prefs.remove, _sessionsKey, getSessions(), and
TimerSessionModel.fromJson when adding the log and removal call.

In `@lib/features/timer/presentation/providers/timer_session_provider.dart`:
- Around line 38-42: Wrap the call to repository.addSession(session) inside a
try/catch in addSession so failures are handled: call
ref.read(timerSessionRepositoryProvider) as before, then try awaiting
repository.addSession(session) and on success set state =
repository.getSessions(); in the catch (catch Exception e, StackTrace st) log
the error (include e and st) with your project's logger and either rethrow or
return a failure result so callers can respond; ensure you reference addSession,
timerSessionRepositoryProvider, repository.addSession, and state =
repository.getSessions() when making the change.

In `@lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart`:
- Line 138: Replace the hardcoded EdgeInsets.only(bottom: 8.h) in the
TodoSelectBottomSheet widget with the existing spacing constant (e.g.,
AppSpacing.s8) to keep spacing consistent with other uses like AppSpacing.s12
and AppSpacing.s4; locate the padding expression (EdgeInsets.only(bottom: 8.h))
and swap it to use AppSpacing.s8 so the file uses the shared spacing constants
throughout.

In `@lib/features/todo/data/datasources/local_todo_datasource.dart`:
- Around line 64-68: The calls to getTodos() and getCategories() are being
executed even in release builds because debugPrint doesn't remove their
deserialization cost; wrap the count computation and debugPrint call in a
kDebugMode guard (import foundation.dart if needed) so that getTodos(),
getCategories(), and the debugPrint are only executed in debug builds, or
alternatively remove the count computation entirely and just call
_prefs.remove(_todosKey) and _prefs.remove(_categoriesKey) if the counts are not
needed; update the block that currently calls getTodos().length,
getCategories().length and debugPrint accordingly.

In `@lib/features/todo/presentation/widgets/todo_add_bottom_sheet.dart`:
- Around line 374-377: onDaySelected currently calls _toggleDate(selectedDay)
which itself calls setState, but assigns _calendarFocusedDay = focusedDay
outside of any setState; move the focused-day update into the same state update
to make intent explicit: either modify _toggleDate to accept focusedDay and
update _calendarFocusedDay inside its setState, or wrap both calls in a single
setState inside onDaySelected (update _calendarFocusedDay and call the logic
that _toggleDate performs without duplicating state changes). Reference:
onDaySelected, _toggleDate, and _calendarFocusedDay.

Comment on lines +15 to +29
```
바텀시트 Container (spaceSurface: #1A1F3A)
└─ AppTextField AnimatedContainer (spaceSurface: #1A1F3A + spaceDivider 테두리)
└─ TextField (InputBorder.none)
```

배경색이 동일 → 테두리선만 보이면서 중첩/어색한 시각 효과

## 수정

```
바텀시트 Container (spaceSurface: #1A1F3A)
└─ AppTextField AnimatedContainer (spaceBackground: #0A0E27 + spaceDivider 테두리)
└─ TextField (InputBorder.none)
```
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

마크다운 코드 블록에 언어 지정 누락 (MD040)

정적 분석 도구가 라인 16의 코드 펜스에 언어 지정이 없다고 경고합니다. 다이어그램 블록 두 곳 모두 ` 뒤에 text를 추가하면 해결됩니다.

📝 제안 수정
-```
+```text
 바텀시트 Container (spaceSurface: `#1A1F3A`)
   └─ AppTextField AnimatedContainer (spaceSurface: `#1A1F3A` + spaceDivider 테두리)
        └─ TextField (InputBorder.none)

...

- +text
바텀시트 Container (spaceSurface: #1A1F3A)
└─ AppTextField AnimatedContainer (spaceBackground: #0A0E27 + spaceDivider 테두리)
└─ TextField (InputBorder.none)

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 16-16: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-02-17-textfield-border-overlap-fix.md` around lines 15 - 29,
The markdown code fences in the document use plain ``` without a language tag
(MD040); update both diagram blocks by changing their opening fences from ``` to
```text so the static analyzer recognizes them as text blocks—locate the two
blocks that start with ``` above the diagrams showing "바텀시트 Container ..." and
replace each opening fence with ```text.

Comment on lines +131 to +149
// 별 마커 — 미완료: 빈 별, 완료: 금색 별
markerBuilder: (context, day, events) {
if (events.isEmpty) return const SizedBox.shrink();
final allCompleted = events.every(
(t) => t.isCompletedForDate(day),
);
return Positioned(
bottom: 1.h,
child: Icon(
allCompleted
? Icons.star_rounded
: Icons.star_outline_rounded,
size: 18.w,
color: allCompleted
? AppColors.accentGold
: AppColors.textTertiary,
),
);
},
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

마커 Icon 사이즈 18.w가 셀 크기 36.w 대비 상대적으로 크고, Positioned(bottom: 1.h) 사용 시 텍스트와 겹칠 수 있습니다

별 마커가 날짜 텍스트 바로 아래에 위치하는데, 아이콘 사이즈가 셀의 절반이므로 소형 기기에서 텍스트와 겹칠 수 있습니다. 다양한 화면 크기에서 오버랩 여부를 확인해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/home/presentation/widgets/space_calendar.dart` around lines 131
- 149, The star marker in markerBuilder uses a fixed Icon size (18.w) and
Positioned(bottom: 1.h) which can overlap the date text because the cell size is
36.w; change the layout to size the icon relative to the cell and move it lower
to avoid overlap—for example compute iconSize as a fraction of the cell (e.g.,
cellSize * 0.25 or min(availableWidth * 0.25, 18.w)) instead of hardcoding 18.w,
and replace the Positioned(bottom: 1.h) with a layout that reserves space (use
Align/Column with SizedBox spacer or a Container with fixed height matching the
cell) so the icon is placed beneath the date text; update references in the
markerBuilder/Positioned/Icon usage to use the new computed iconSize and
reserved spacing.

Comment on lines +14 to +20
@riverpod
TimerSessionLocalDataSource timerSessionLocalDataSource(Ref ref) {
throw StateError(
'TimerSessionLocalDataSource가 초기화되지 않았습니다. '
'SharedPreferences 초기화를 확인하세요.',
);
}
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:

fd "main.dart" --type f --max-depth 5

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 89


🏁 Script executed:

rg -n "timerSessionLocalDataSource" --type dart

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 1274


🏁 Script executed:

rg -n "SharedPreferences" --type dart -A 3 -B 3

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 10135


🏁 Script executed:

cat -n lib/features/timer/presentation/providers/timer_session_provider.dart | head -50

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 1924


🏁 Script executed:

sed -n '185,195p' lib/main.dart

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 409


SharedPreferences 초기화 실패 시 TimerSessionLocalDataSource 오버라이드가 등록되지 않아 앱이 크래시됩니다.

main.dart의 라인 186-194에서 두 provider 오버라이드가 모두 if (prefs != null) 조건부로 등록됩니다. SharedPreferences.getInstance() 호출이 실패하면 prefs가 null이 되고, 이 경우 timerSessionLocalDataSourceProvider 오버라이드가 등록되지 않습니다. 타이머 세션 기능이 이 provider에 접근할 때 라인 16-19의 StateError가 발생하며 앱이 비정상 종료됩니다.

SharedPreferences 초기화 실패에 대한 폴백(예: 읽기 전용 no-op 구현) 또는 사용자 대면 에러 처리를 추가하세요.

- timer_provider: DateTime.now()를 비동기 작업 전에 캡처하여 세션 타임스탬프 정확도 개선
- timer_format_utils: 정각 시간 표시 개선 (1시간 0분 → 1시간)
- todo_select_bottom_sheet: ref.watch를 when() 콜백 밖으로 이동하여 Riverpod 의존성 안정화
- todo_select_bottom_sheet: 누적 시간 레이블 추가로 예상/실제 시간 구분 명확화
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart (1)

112-113: ⚠️ Potential issue | 🟡 Minor

에러 상태에서 사용자 피드백 누락.

error 콜백이 SizedBox.shrink()만 반환하여, 할 일 목록 로드 실패 시 사용자에게 아무런 안내가 없습니다. 빈 화면만 표시되어 사용자가 오류 원인이나 재시도 방법을 알 수 없습니다.

🛡️ 에러 상태 UI 제안
             loading: () => const Center(child: CircularProgressIndicator()),
-            error: (_, _) => const SizedBox.shrink(),
+            error: (_, _) => const Center(
+              child: SpaceEmptyState(
+                icon: Icons.error_outline,
+                title: '할 일을 불러오지 못했어요',
+                subtitle: '잠시 후 다시 시도해주세요',
+                iconSize: 32,
+                animated: false,
+              ),
+            ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart` around
lines 112 - 113, The error branch in the AsyncValue handling inside
todo_select_bottom_sheet.dart currently returns SizedBox.shrink(), leaving users
without feedback; replace the error callback (error: (_, _) => ...) with a
visible error UI such as Center with a Text describing the failure and an
ElevatedButton to retry, using the provided error and stackTrace parameters to
optionally log or show brief details; wire the retry button to the appropriate
retry/refetch method (e.g., call the provider's refresh/refetch or invoke an
onRetry callback) so tapping it attempts to reload the todo list and gives users
clear feedback and recovery.
🧹 Nitpick comments (2)
lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart (1)

207-215: formatMinutes 유틸리티를 사용하여 시간 표시 개선하기.

프로젝트의 lib/features/timer/presentation/utils/timer_format_utils.dart에 분 단위를 읽기 쉬운 형식으로 변환하는 formatMinutes 유틸리티가 이미 존재합니다 (예: 90 → "1시간 30분", 60 → "1시간"). 이 유틸리티는 timer_screen.dart, timer_history_screen.dart 등에서 활용되고 있습니다.

현재 코드에서는 estimatedMinutesactualMinutes를 원시 분 단위로만 표시하고 있으므로, 일관성과 가독성 향상을 위해 이 유틸리티를 적용하는 것을 권장합니다:

items.add('예상 ${formatMinutes(todo.estimatedMinutes!)}');
items.add('누적 ${formatMinutes(todo.actualMinutes!)}');

formatMinutes를 import한 후 적용하면 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart` around
lines 207 - 215, Use the existing formatMinutes utility to display readable
durations instead of raw minutes: import formatMinutes from timer_format_utils
and replace the string constructions that reference todo.estimatedMinutes and
todo.actualMinutes with items.add('예상 ${formatMinutes(todo.estimatedMinutes!)}')
and items.add('누적 ${formatMinutes(todo.actualMinutes!)}'), making sure to
null-check (using !) only after confirming the previous null and >0 checks
remain in place (references: todo.estimatedMinutes, todo.actualMinutes,
formatMinutes).
lib/features/timer/presentation/providers/timer_provider.dart (1)

95-100: 조건식 표현을 통일하는 것이 좋습니다.

Line 95의 elapsedMinutes > 0과 Line 100의 sessionDuration.inMinutes >= 1elapsedMinutes = sessionDuration.inMinutes이므로 동일한 조건입니다. 두 곳 중 하나의 표현으로 통일하면 가독성이 향상됩니다.

♻️ 제안 수정: 조건 표현 통일 (>= 1으로 통일하는 예시)
-   if (todoId != null && elapsedMinutes > 0) {
+   if (todoId != null && elapsedMinutes >= 1) {
      totalMinutes = await _updateTodoActualMinutes(todoId, elapsedMinutes);
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/timer/presentation/providers/timer_provider.dart` around lines
95 - 100, The two equivalent checks (elapsedMinutes > 0 and
sessionDuration.inMinutes >= 1) should be unified for consistency; replace the
condition in the branch that calls _updateTodoActualMinutes (where
elapsedMinutes is used alongside todoId) to use the same expression as the
session-duration check (sessionDuration.inMinutes >= 1) so both branches use the
identical condition (and keep using elapsedMinutes as the value passed to
_updateTodoActualMinutes).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/timer/presentation/providers/timer_provider.dart`:
- Around line 83-125: The stop() method can throw in _updateTodoActualMinutes or
addSession and currently state = const TimerState() won't run; wrap the awaited
operations (the block that calls _updateTodoActualMinutes(todoId,
elapsedMinutes) and
ref.read(timerSessionListNotifierProvider.notifier).addSession(session)) in a
try/finally so the finally always sets state = const TimerState() (and
cancels/cleans up if needed), and optionally catch/log the error inside the try
before rethrowing or returning null; ensure the unique symbols referenced are
stop(), _updateTodoActualMinutes,
timerSessionListNotifierProvider.notifier.addSession, and the state = const
TimerState() assignment so the UI is always reset even on exceptions.

---

Outside diff comments:
In `@lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart`:
- Around line 112-113: The error branch in the AsyncValue handling inside
todo_select_bottom_sheet.dart currently returns SizedBox.shrink(), leaving users
without feedback; replace the error callback (error: (_, _) => ...) with a
visible error UI such as Center with a Text describing the failure and an
ElevatedButton to retry, using the provided error and stackTrace parameters to
optionally log or show brief details; wire the retry button to the appropriate
retry/refetch method (e.g., call the provider's refresh/refetch or invoke an
onRetry callback) so tapping it attempts to reload the todo list and gives users
clear feedback and recovery.

---

Duplicate comments:
In `@lib/features/timer/presentation/providers/timer_provider.dart`:
- Around line 86-112: The code correctly captures endedAt before awaiting async
work and uses that captured DateTime consistently when constructing
TimerSessionEntity; ensure you keep endedAt captured prior to any await (see the
endedAt variable and the call to _updateTodoActualMinutes), and continue to pass
that same endedAt into TimerSessionEntity and into the call to
ref.read(timerSessionListNotifierProvider.notifier).addSession(session) rather
than calling DateTime.now() at session construction time.

In `@lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart`:
- Around line 19-22: The ref.watch calls have been correctly moved outside
conditional callbacks to keep Riverpod's dependency graph stable; ensure you
retain this pattern in the build method by keeping the
ref.watch(todoListNotifierProvider) and ref.watch(categoryListNotifierProvider)
calls (variables todosAsync and categoriesAsync) executed unconditionally and
continue using categoriesAsync.valueOrNull ?? [] (categories) as the safe
fallback — do not move these watches into any when()/map() callbacks or
conditionals.

---

Nitpick comments:
In `@lib/features/timer/presentation/providers/timer_provider.dart`:
- Around line 95-100: The two equivalent checks (elapsedMinutes > 0 and
sessionDuration.inMinutes >= 1) should be unified for consistency; replace the
condition in the branch that calls _updateTodoActualMinutes (where
elapsedMinutes is used alongside todoId) to use the same expression as the
session-duration check (sessionDuration.inMinutes >= 1) so both branches use the
identical condition (and keep using elapsedMinutes as the value passed to
_updateTodoActualMinutes).

In `@lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart`:
- Around line 207-215: Use the existing formatMinutes utility to display
readable durations instead of raw minutes: import formatMinutes from
timer_format_utils and replace the string constructions that reference
todo.estimatedMinutes and todo.actualMinutes with items.add('예상
${formatMinutes(todo.estimatedMinutes!)}') and items.add('누적
${formatMinutes(todo.actualMinutes!)}'), making sure to null-check (using !)
only after confirming the previous null and >0 checks remain in place
(references: todo.estimatedMinutes, todo.actualMinutes, formatMinutes).

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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/timer/presentation/providers/timer_provider.dart`:
- Around line 94-116: The current flow calls _updateTodoActualMinutes(...)
before adding the session via
ref.read(timerSessionListNotifierProvider.notifier).addSession(...), which can
leave the todo's actualMinutes updated without a corresponding
TimerSessionEntity if addSession throws; change the order so you create the
TimerSessionEntity and call addSession(session) first, then call
_updateTodoActualMinutes(todoId, elapsedMinutes), and ensure errors from the
second step are handled (e.g., log and allow retry) while still leaving the
saved session intact; keep the final state reset (state = const TimerState()) in
the finally block.
- Around line 119-125: _timer_screen.dart의 _onStop()에서 stop() 호출에 try/catch가 없어
stop() 내부의 예외가 상위로 전파되어 비동기 미처리 예외가 발생할 수 있습니다; _onStop() 안에서 await stop() 호출을
try { await stop(...); } catch (e, st) { /* 적절히 로그 또는 UI로 에러 처리 */ }로 감싸 예외를
처리하고 필요하면 사용자 알림이나 실패 복구(예: 상태 롤백)를 수행하도록 수정하세요; 참조할 심볼: _onStop(), stop().

Comment on lines +94 to 116
try {
// 연동된 할일이 있고 1분 이상 측정 시 actualMinutes 누적
if (todoId != null && elapsedMinutes > 0) {
totalMinutes = await _updateTodoActualMinutes(todoId, elapsedMinutes);
}

// 1분 이상 세션이면 기록 저장
if (sessionDuration.inMinutes >= 1) {
final session = TimerSessionEntity(
id: endedAt.millisecondsSinceEpoch.toString(),
todoId: todoId,
todoTitle: todoTitle,
startedAt: endedAt.subtract(sessionDuration),
endedAt: endedAt,
durationMinutes: elapsedMinutes,
);
await ref
.read(timerSessionListNotifierProvider.notifier)
.addSession(session);
}
} finally {
state = const TimerState();
}
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

addSession 실패 시 할일 시간만 증가하는 데이터 불일치

Line 97에서 _updateTodoActualMinutes가 성공(할일 actualMinutes 증가)한 후 Line 112의 addSession이 예외를 던지면, finally로 state는 올바르게 초기화되지만 할일에는 누적 시간이 반영되었음에도 대응하는 세션 기록이 없는 불일치 상태가 됩니다. TimerHistory 화면에서 세션은 보이지 않는데 할일의 누적 시간은 증가한 것으로 표시될 수 있습니다.

두 작업의 순서를 바꾸면(세션 저장 → 할일 업데이트) 실패 시나리오를 개선할 수 있습니다. 세션이 먼저 저장된 상태에서 할일 업데이트가 실패하더라도, 더 상세한 세션 데이터가 보존되고 할일 업데이트는 재시도 여지가 있습니다.

🛡️ 작업 순서 변경 제안
     try {
-      // 연동된 할일이 있고 1분 이상 측정 시 actualMinutes 누적
-      if (todoId != null && elapsedMinutes > 0) {
-        totalMinutes = await _updateTodoActualMinutes(todoId, elapsedMinutes);
-      }
-
       // 1분 이상 세션이면 기록 저장
       if (sessionDuration.inMinutes >= 1) {
         final session = TimerSessionEntity(
           id: endedAt.millisecondsSinceEpoch.toString(),
           todoId: todoId,
           todoTitle: todoTitle,
           startedAt: endedAt.subtract(sessionDuration),
           endedAt: endedAt,
           durationMinutes: elapsedMinutes,
         );
         await ref
             .read(timerSessionListNotifierProvider.notifier)
             .addSession(session);
       }
+
+      // 연동된 할일이 있고 1분 이상 측정 시 actualMinutes 누적
+      if (todoId != null && elapsedMinutes > 0) {
+        totalMinutes = await _updateTodoActualMinutes(todoId, elapsedMinutes);
+      }
     } finally {
       state = const TimerState();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/timer/presentation/providers/timer_provider.dart` around lines
94 - 116, The current flow calls _updateTodoActualMinutes(...) before adding the
session via ref.read(timerSessionListNotifierProvider.notifier).addSession(...),
which can leave the todo's actualMinutes updated without a corresponding
TimerSessionEntity if addSession throws; change the order so you create the
TimerSessionEntity and call addSession(session) first, then call
_updateTodoActualMinutes(todoId, elapsedMinutes), and ensure errors from the
second step are handled (e.g., log and allow retry) while still leaving the
saved session intact; keep the final state reset (state = const TimerState()) in
the finally block.

Comment on lines +119 to +125
return sessionDuration.inMinutes >= 1
? (
sessionDuration: sessionDuration,
todoTitle: todoTitle,
totalMinutes: totalMinutes,
)
: null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the timer_provider.dart file to understand the stop() method and the context around lines 119-125
cat -n lib/features/timer/presentation/providers/timer_provider.dart | head -150

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 5343


🏁 Script executed:

# Find .stop() calls in timer_screen.dart with context
rg -n --type dart -A 6 -B 2 '\.stop\(\)' --glob 'timer_screen.dart'

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 926


🏁 Script executed:

# Search for all .stop() calls in the entire dart codebase
rg -n --type dart '\.stop\(\)' --glob '!*_test.dart'

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 393


🏁 Script executed:

# Search for _onStop() calls
rg -n --type dart '_onStop\(' lib/features/timer/presentation/screens/timer_screen.dart -A 2 -B 2

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 251


🏁 Script executed:

# Search for where _onStop() is called (not the definition)
rg -n --type dart '_onStop\s*\(' lib/features/timer/presentation/screens/timer_screen.dart | grep -v 'Future<void> _onStop'

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 58


🏁 Script executed:

# Search for _onStop in the timer_screen.dart file with different patterns
rg -n --type dart '_onStop' lib/features/timer/presentation/screens/timer_screen.dart

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 150


🏁 Script executed:

# Check the context around line 194 where _onStop is used as a callback
cat -n lib/features/timer/presentation/screens/timer_screen.dart | sed -n '185,205p'

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 815


🏁 Script executed:

# Check the full timer_screen.dart to see if there's any global error handling or if it's wrapped in a try-catch
cat -n lib/features/timer/presentation/screens/timer_screen.dart | head -50

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 2317


🏁 Script executed:

# Check the entire _onStop method and surrounding context more thoroughly
cat -n lib/features/timer/presentation/screens/timer_screen.dart | sed -n '220,226p'

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 315


stop()에서 발생한 예외가 호출부로 전파됩니다

_onStop() 메서드(timer_screen.dart 222-226줄)에서 stop()을 호출할 때 try/catch 처리가 없습니다. stop() 메서드의 try 블록에서 예외가 발생하면 finally가 실행된 후 예외가 상위로 전파되며, 호출부에서 이를 처리하지 않으면 미처리 비동기 예외가 발생합니다. 버튼 콜백인 _onStop()try-catch 블록을 추가하여 예외를 처리해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/timer/presentation/providers/timer_provider.dart` around lines
119 - 125, _timer_screen.dart의 _onStop()에서 stop() 호출에 try/catch가 없어 stop() 내부의
예외가 상위로 전파되어 비동기 미처리 예외가 발생할 수 있습니다; _onStop() 안에서 await stop() 호출을 try { await
stop(...); } catch (e, st) { /* 적절히 로그 또는 UI로 에러 처리 */ }로 감싸 예외를 처리하고 필요하면 사용자
알림이나 실패 복구(예: 상태 롤백)를 수행하도록 수정하세요; 참조할 심볼: _onStop(), stop().

@EM-H20 EM-H20 merged commit ffa243c into main Feb 19, 2026
2 checks passed
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.

1 participant

Comments