20260217 #27 타이머 할 일 연동 UI 전반 개선#28
Conversation
- 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 반영
Walkthrough타이머 세션 영속화(모델/로컬 저장소/리포지토리/프로바이더), 통계 제공자 분리, TimerHistory 화면(페이지네이션) 추가, SpaceCalendar 헤더·셀 리디자인 및 마커 변경, 게스트 세션 데이터 클리어 강화, 여러 UI 위젯(DragHandle, CalendarHeader, AppTextField 등) 및 레이아웃 상수 조정이 도입되었습니다. Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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 전체 역직렬화)은 로그 출력 여부와 무관하게 릴리즈 모드에서도 매번 실행됩니다.
kDebugModeconst 가드로 계수 계산과 로그를 묶으면, 프로덕션 빌드에서 컴파일 타임에 완전히 제거됩니다.♻️ 개선 제안
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를 사용했다면,all16은EdgeInsets.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.
| ``` | ||
| 바텀시트 Container (spaceSurface: #1A1F3A) | ||
| └─ AppTextField AnimatedContainer (spaceSurface: #1A1F3A + spaceDivider 테두리) | ||
| └─ TextField (InputBorder.none) | ||
| ``` | ||
|
|
||
| 배경색이 동일 → 테두리선만 보이면서 중첩/어색한 시각 효과 | ||
|
|
||
| ## 수정 | ||
|
|
||
| ``` | ||
| 바텀시트 Container (spaceSurface: #1A1F3A) | ||
| └─ AppTextField AnimatedContainer (spaceBackground: #0A0E27 + spaceDivider 테두리) | ||
| └─ TextField (InputBorder.none) | ||
| ``` |
There was a problem hiding this comment.
마크다운 코드 블록에 언어 지정 누락 (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.
| // 별 마커 — 미완료: 빈 별, 완료: 금색 별 | ||
| 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, | ||
| ), | ||
| ); | ||
| }, |
There was a problem hiding this comment.
마커 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.
| @riverpod | ||
| TimerSessionLocalDataSource timerSessionLocalDataSource(Ref ref) { | ||
| throw StateError( | ||
| 'TimerSessionLocalDataSource가 초기화되지 않았습니다. ' | ||
| 'SharedPreferences 초기화를 확인하세요.', | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "main.dart" --type f --max-depth 5Repository: SpaceStudyShip/SpaceStudyShip-FE
Length of output: 89
🏁 Script executed:
rg -n "timerSessionLocalDataSource" --type dartRepository: SpaceStudyShip/SpaceStudyShip-FE
Length of output: 1274
🏁 Script executed:
rg -n "SharedPreferences" --type dart -A 3 -B 3Repository: SpaceStudyShip/SpaceStudyShip-FE
Length of output: 10135
🏁 Script executed:
cat -n lib/features/timer/presentation/providers/timer_session_provider.dart | head -50Repository: SpaceStudyShip/SpaceStudyShip-FE
Length of output: 1924
🏁 Script executed:
sed -n '185,195p' lib/main.dartRepository: 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 구현) 또는 사용자 대면 에러 처리를 추가하세요.
lib/features/timer/presentation/widgets/todo_select_bottom_sheet.dart
Outdated
Show resolved
Hide resolved
- timer_provider: DateTime.now()를 비동기 작업 전에 캡처하여 세션 타임스탬프 정확도 개선 - timer_format_utils: 정각 시간 표시 개선 (1시간 0분 → 1시간) - todo_select_bottom_sheet: ref.watch를 when() 콜백 밖으로 이동하여 Riverpod 의존성 안정화 - todo_select_bottom_sheet: 누적 시간 레이블 추가로 예상/실제 시간 구분 명확화
There was a problem hiding this comment.
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등에서 활용되고 있습니다.현재 코드에서는
estimatedMinutes와actualMinutes를 원시 분 단위로만 표시하고 있으므로, 일관성과 가독성 향상을 위해 이 유틸리티를 적용하는 것을 권장합니다: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 >= 1은elapsedMinutes = 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).
There was a problem hiding this comment.
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().
| 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(); | ||
| } |
There was a problem hiding this comment.
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.
| return sessionDuration.inMinutes >= 1 | ||
| ? ( | ||
| sessionDuration: sessionDuration, | ||
| todoTitle: todoTitle, | ||
| totalMinutes: totalMinutes, | ||
| ) | ||
| : null; |
There was a problem hiding this comment.
🧩 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 -150Repository: 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 2Repository: 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.dartRepository: 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 -50Repository: 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().
✨ 변경 사항
✅ 테스트
Summary by CodeRabbit
New Features
Bug Fixes
UI/Style