diff --git a/lib/function/study_timer_controller.dart b/lib/function/study_timer_controller.dart index 6e4fcbc..6b1ac56 100644 --- a/lib/function/study_timer_controller.dart +++ b/lib/function/study_timer_controller.dart @@ -36,7 +36,11 @@ class StudyTimerController extends ChangeNotifier { int studyMinutes = 50; int breakMinutes = 10; PomodoroPhase? _phase; - int _phaseSecondsLeft = 0; + // ๐Ÿ› [์›น ํƒญ ๋”œ๋ ˆ์ด ๋ฒ„๊ทธ ์ˆ˜์ •] ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒญ์˜ setInterval์„ ๋А๋ฆฌ๊ฒŒ(์‹ฌํ•˜๋ฉด ๋ถ„๋‹น + // 1๋ฒˆ) ๋Œ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์—, ๋งค tick๋งˆ๋‹ค ์ดˆ๋ฅผ 1์”ฉ ๋นผ๋Š” ์นด์šดํŠธ๋‹ค์šด์€ ๋‹ค๋ฅธ ์ฐฝ์„ ์˜ค๋ž˜ ๋ณด๊ณ  ์žˆ์œผ๋ฉด + // ์‹ค์ œ ์‹œ๊ฐ„๋ณด๋‹ค ๋งŽ์ด ๋ฐ€๋ฆฐ๋‹ค. ๊ทธ๋ž˜์„œ "์–ธ์ œ ๋๋‚˜์•ผ ํ•˜๋Š”์ง€"(_phaseEndsAt)๋ฅผ ์ ˆ๋Œ€ ์‹œ๊ฐ์œผ๋กœ + // ์žก์•„๋‘๊ณ , tick์ด ๋Šฆ๊ฒŒ ์™€๋„ ๊ทธ ์‹œ๊ฐ์„ ๊ธฐ์ค€์œผ๋กœ ์ •ํ™•ํžˆ ๊ณ„์‚ฐ/์ „ํ™˜ํ•œ๋‹ค. + DateTime? _phaseEndsAt; int completedCycles = 0; bool _phaseTransitioning = false; Timer? _phaseTicker; @@ -51,7 +55,12 @@ class StudyTimerController extends ChangeNotifier { bool get isPaused => _state == StudyTimerState.paused; bool get isIdle => _state == StudyTimerState.idle; PomodoroPhase? get phase => _phase; - int get phaseSecondsLeft => _phaseSecondsLeft; + + int get phaseSecondsLeft { + if (_phaseEndsAt == null) return 0; + final diff = _phaseEndsAt!.difference(DateTime.now()).inSeconds; + return diff > 0 ? diff : 0; + } /// ์ง€๊ธˆ ์„ธ์…˜ ํ•˜๋‚˜๋งŒ์˜ ๊ฒฝ๊ณผ ์‹œ๊ฐ„(ํƒ€์ด๋จธ ํ™”๋ฉด ํฐ ์ˆซ์ž์šฉ). ์ผ์‹œ์ •์ง€ ์ค‘์—” ๋ฉˆ์ถฐ ์žˆ๋‹ค. int get liveElapsedSeconds { @@ -78,6 +87,15 @@ class StudyTimerController extends ChangeNotifier { await _fetchStatus(); } + /// ๐Ÿ› [์›น ํƒญ ๋”œ๋ ˆ์ด ๋ฒ„๊ทธ ์ˆ˜์ •] ๋‹ค๋ฅธ ์ฐฝ/ํƒญ์— ๊ฐ”๋‹ค๊ฐ€ ์ด ํ™”๋ฉด์œผ๋กœ ๋Œ์•„์™”์„ ๋•Œ ํ˜ธ์ถœํ•œ๋‹ค. + /// ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒญ์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํƒ€์ด๋จธ๋ฅผ ๋А๋ฆฌ๊ฒŒ ๋Œ๋ ค์„œ ํ™”๋ฉด ์ˆซ์ž์™€ ๋ฝ€๋ชจ๋„๋กœ ์ „ํ™˜์ด + /// ๋ฐ€๋ฆด ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ค๊ณ (์ง„์งœ ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ์žฌ๋™๊ธฐํ™”) ๋ฝ€๋ชจ๋„๋กœ ๊ตฌ๊ฐ„์ด + /// ์ด๋ฏธ ๋๋‚ฌ์–ด์•ผ ํ•œ๋‹ค๋ฉด ๋ฐ”๋กœ ๋”ฐ๋ผ์žก๋Š”๋‹ค. + Future onAppResumed() async { + await _fetchStatus(); + checkPhaseDeadlineNow(); + } + @override void dispose() { _disposed = true; @@ -176,7 +194,7 @@ class StudyTimerController extends ChangeNotifier { if (result.$1 && pomodoroEnabled) { completedCycles = 0; _phase = PomodoroPhase.study; - _phaseSecondsLeft = studyMinutes * 60; + _phaseEndsAt = DateTime.now().add(Duration(minutes: studyMinutes)); _startPhaseTicker(); _safeNotify(); } @@ -221,22 +239,32 @@ class StudyTimerController extends ChangeNotifier { _phaseTicker?.cancel(); _phaseTicker = null; _phase = null; - _phaseSecondsLeft = 0; + _phaseEndsAt = null; } - /// ๐Ÿ… 1์ดˆ๋งˆ๋‹ค ๋ฝ€๋ชจ๋„๋กœ ๋‚จ์€ ์‹œ๊ฐ„์„ ์ค„์ด๊ณ , 0์ด ๋˜๋ฉด ๊ณต๋ถ€โ†”ํœด์‹์„ ์ž๋™์œผ๋กœ ์ „ํ™˜ํ•œ๋‹ค. - /// ํœด์‹ ๊ตฌ๊ฐ„์—์„œ๋„ ๊ณ„์† ํ˜๋Ÿฌ์•ผ ํ•ด์„œ, running์ผ ๋•Œ๋งŒ ๋„๋Š” _ticker์™€๋Š” ๋ณ„๋„๋กœ ๊ด€๋ฆฌํ•œ๋‹ค. + /// ๐Ÿ… 1์ดˆ๋งˆ๋‹ค ์ง€๊ธˆ์ด ๋๋‚˜์•ผ ํ•  ์‹œ๊ฐ(_phaseEndsAt)์„ ์ง€๋‚ฌ๋Š”์ง€ ํ™•์ธํ•ด์„œ, ์ง€๋‚ฌ์œผ๋ฉด + /// ๊ณต๋ถ€โ†”ํœด์‹์„ ์ž๋™์œผ๋กœ ์ „ํ™˜ํ•œ๋‹ค. ํœด์‹ ๊ตฌ๊ฐ„์—์„œ๋„ ๊ณ„์† ํ˜๋Ÿฌ์•ผ ํ•ด์„œ, running์ผ ๋•Œ๋งŒ + /// ๋„๋Š” _ticker์™€๋Š” ๋ณ„๋„๋กœ ๊ด€๋ฆฌํ•œ๋‹ค. void _startPhaseTicker() { _phaseTicker?.cancel(); - _phaseTicker = Timer.periodic(const Duration(seconds: 1), (_) { - if (_phase == null || _phaseTransitioning) return; - _phaseSecondsLeft -= 1; - if (_phaseSecondsLeft <= 0) { - _advancePhase(); - } else { - _safeNotify(); - } - }); + _phaseTicker = Timer.periodic( + const Duration(seconds: 1), + (_) => _checkPhaseDeadline(), + ); + } + + /// ํƒญ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์— ์žˆ๋Š” ๋™์•ˆ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ setInterval์„ ๋А๋ฆฌ๊ฒŒ ๋Œ๋ฆฌ๋ฉด ์œ„ 1์ดˆ tick๋„ + /// ๋Šฆ๊ฒŒ ์˜จ๋‹ค. ํƒญ์ด ๋‹ค์‹œ ํ™œ์„ฑํ™”๋  ๋•Œ(์•ฑ์ด resumed ๋  ๋•Œ) ํ™”๋ฉด์—์„œ ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์ง์ ‘ + /// ํ˜ธ์ถœํ•ด์„œ, ๋ฐ€๋ฆฐ tick์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ์ง€๋‚œ ์‹œ๊ฐ์„ ๋”ฐ๋ผ์žก๋Š”๋‹ค. + void checkPhaseDeadlineNow() => _checkPhaseDeadline(); + + void _checkPhaseDeadline() { + if (_phase == null || _phaseTransitioning || _phaseEndsAt == null) return; + if (DateTime.now().isAfter(_phaseEndsAt!)) { + _advancePhase(); + } else { + _safeNotify(); + } } Future _advancePhase() async { @@ -246,19 +274,21 @@ class StudyTimerController extends ChangeNotifier { if (success) { completedCycles += 1; _phase = PomodoroPhase.breakTime; - _phaseSecondsLeft = breakMinutes * 60; + _phaseEndsAt = DateTime.now().add(Duration(minutes: breakMinutes)); onPhaseChanged?.call('๊ณต๋ถ€ ๋! $breakMinutes๋ถ„๊ฐ„ ์‰ฌ์–ด๊ฐ€์š”.'); } else { - _phaseSecondsLeft = 1; // ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ ๋“ฑ์œผ๋กœ ์‹คํŒจํ–ˆ์œผ๋ฉด ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„. + _phaseEndsAt = DateTime.now().add( + const Duration(seconds: 1), + ); // ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ ๋“ฑ์œผ๋กœ ์‹คํŒจํ–ˆ์œผ๋ฉด ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„. } } else if (_phase == PomodoroPhase.breakTime) { final (success, _) = await resume(); if (success) { _phase = PomodoroPhase.study; - _phaseSecondsLeft = studyMinutes * 60; + _phaseEndsAt = DateTime.now().add(Duration(minutes: studyMinutes)); onPhaseChanged?.call('ํœด์‹ ๋! ๋‹ค์‹œ ๊ณต๋ถ€๋ฅผ ์‹œ์ž‘ํ•ด์š”.'); } else { - _phaseSecondsLeft = 1; + _phaseEndsAt = DateTime.now().add(const Duration(seconds: 1)); } } _phaseTransitioning = false; diff --git a/lib/ui/study_timer_screen.dart b/lib/ui/study_timer_screen.dart index 0a83c0f..ca6923a 100644 --- a/lib/ui/study_timer_screen.dart +++ b/lib/ui/study_timer_screen.dart @@ -25,7 +25,8 @@ class StudyTimerScreen extends StatefulWidget { State createState() => _StudyTimerScreenState(); } -class _StudyTimerScreenState extends State { +class _StudyTimerScreenState extends State + with WidgetsBindingObserver { late final StudyTimerController _controller; @override @@ -37,6 +38,7 @@ class _StudyTimerScreenState extends State { ); _controller.onPhaseChanged = _handlePhaseChanged; _controller.init(); + WidgetsBinding.instance.addObserver(this); } void _handlePhaseChanged(String message) { @@ -44,8 +46,18 @@ class _StudyTimerScreenState extends State { AppNotice.show(context, message, icon: Icons.timer_rounded); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + // ๐Ÿ› [์›น ํƒญ ๋”œ๋ ˆ์ด ๋ฒ„๊ทธ ์ˆ˜์ •] ๋‹ค๋ฅธ ์ฐฝ/ํƒญ์— ๊ฐ”๋‹ค๊ฐ€ ์ด ํƒญ์œผ๋กœ ๋Œ์•„์™”์„ ๋•Œ(resumed) ๋ฐ€๋ฆฐ + // ์‹œ๊ฐ„์„ ๋ฐ”๋กœ ๋”ฐ๋ผ์žก๋Š”๋‹ค - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒญ์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํƒ€์ด๋จธ๋ฅผ ๋А๋ฆฌ๊ฒŒ ๋Œ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ. + if (state == AppLifecycleState.resumed) { + _controller.onAppResumed(); + } + } + @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _controller.dispose(); super.dispose(); }