diff --git a/lib/function/browser_notification.dart b/lib/function/browser_notification.dart new file mode 100644 index 0000000..e1860bc --- /dev/null +++ b/lib/function/browser_notification.dart @@ -0,0 +1,3 @@ +// ๐ŸŒ ํ”Œ๋žซํผ์— ๋”ฐ๋ผ ์›น ์ „์šฉ ๊ตฌํ˜„/๋ฌด๋™์ž‘ ๊ตฌํ˜„ ์ค‘ ํ•˜๋‚˜๋กœ ์—ฐ๊ฒฐ๋˜๋Š” ์ง„์ž…์ . +export 'browser_notification_stub.dart' + if (dart.library.html) 'browser_notification_web.dart'; diff --git a/lib/function/browser_notification_stub.dart b/lib/function/browser_notification_stub.dart new file mode 100644 index 0000000..e583158 --- /dev/null +++ b/lib/function/browser_notification_stub.dart @@ -0,0 +1,4 @@ +// ๐ŸŒ ์›น์ด ์•„๋‹Œ ๋นŒ๋“œ(Android ๋“ฑ)์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ API ์ž์ฒด๊ฐ€ ์—†์œผ๋‹ˆ ์•„๋ฌด ๋™์ž‘๋„ ์•ˆ ํ•œ๋‹ค. +Future requestNotificationPermission() async => false; + +void showBrowserNotification(String title, String body) {} diff --git a/lib/function/browser_notification_web.dart b/lib/function/browser_notification_web.dart new file mode 100644 index 0000000..50e647e --- /dev/null +++ b/lib/function/browser_notification_web.dart @@ -0,0 +1,24 @@ +// ๐ŸŒ ์›น ์ „์šฉ - ๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ(Notification) API๋กœ "ํƒ€์ด๋จธ ์ผœ๋†“๊ณ  ํƒญ์„ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ" ๊ฐ™์€ +// ์ฆ‰๊ฐ์ ์ธ ๋กœ์ปฌ ์•Œ๋ฆผ์„ ๋„์šด๋‹ค. FCM ํ‘ธ์‹œ์™€ ๋‹ฌ๋ฆฌ ์„œ๋ฒ„ ์™•๋ณต ์—†์ด ๋ฐ”๋กœ ๋œฌ๋‹ค. +// ์•Œ๋ฆผ API๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์กฐ์šฉํžˆ ๋ฌด์‹œํ•œ๋‹ค. +import 'dart:js_interop'; +import 'package:web/web.dart' as web; + +Future requestNotificationPermission() async { + try { + if (web.Notification.permission == 'granted') return true; + final permission = await web.Notification.requestPermission().toDart; + return permission.toDart == 'granted'; + } catch (_) { + return false; + } +} + +void showBrowserNotification(String title, String body) { + try { + if (web.Notification.permission != 'granted') return; + web.Notification(title, web.NotificationOptions(body: body)); + } catch (_) { + // ์กฐ์šฉํžˆ ๋ฌด์‹œ + } +} diff --git a/lib/ui/study_timer_screen.dart b/lib/ui/study_timer_screen.dart index ca6923a..b2438d9 100644 --- a/lib/ui/study_timer_screen.dart +++ b/lib/ui/study_timer_screen.dart @@ -1,6 +1,8 @@ // โฑ๏ธ ์šฐ๋ฆฌ ํ•™๊ต ์ „์šฉ "๋ฐฑํ’ˆํƒ€" ๊ณต๋ถ€ ํƒ€์ด๋จธ ํ™”๋ฉด (UI ์ „์šฉ). // ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/study_timer_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. +import 'dart:async'; import 'package:flutter/material.dart'; +import '../function/browser_notification.dart'; import '../function/study_timer_controller.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; @@ -28,6 +30,7 @@ class StudyTimerScreen extends StatefulWidget { class _StudyTimerScreenState extends State with WidgetsBindingObserver { late final StudyTimerController _controller; + Timer? _awayTimer; @override void initState() { @@ -48,16 +51,34 @@ class _StudyTimerScreenState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { - // ๐Ÿ› [์›น ํƒญ ๋”œ๋ ˆ์ด ๋ฒ„๊ทธ ์ˆ˜์ •] ๋‹ค๋ฅธ ์ฐฝ/ํƒญ์— ๊ฐ”๋‹ค๊ฐ€ ์ด ํƒญ์œผ๋กœ ๋Œ์•„์™”์„ ๋•Œ(resumed) ๋ฐ€๋ฆฐ - // ์‹œ๊ฐ„์„ ๋ฐ”๋กœ ๋”ฐ๋ผ์žก๋Š”๋‹ค - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒญ์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํƒ€์ด๋จธ๋ฅผ ๋А๋ฆฌ๊ฒŒ ๋Œ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ. if (state == AppLifecycleState.resumed) { + // ๐Ÿ› [์›น ํƒญ ๋”œ๋ ˆ์ด ๋ฒ„๊ทธ ์ˆ˜์ •] ๋‹ค๋ฅธ ์ฐฝ/ํƒญ์— ๊ฐ”๋‹ค๊ฐ€ ์ด ํƒญ์œผ๋กœ ๋Œ์•„์™”์„ ๋•Œ(resumed) ๋ฐ€๋ฆฐ + // ์‹œ๊ฐ„์„ ๋ฐ”๋กœ ๋”ฐ๋ผ์žก๋Š”๋‹ค - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒญ์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํƒ€์ด๋จธ๋ฅผ ๋А๋ฆฌ๊ฒŒ ๋Œ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ. + _awayTimer?.cancel(); _controller.onAppResumed(); + return; + } + // ๐Ÿ”” ํƒ€์ด๋จธ๊ฐ€ ์ผœ์ ธ ์žˆ๋Š”๋ฐ ํƒญ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๊ฐ€๋ฉด(๋‹ค๋ฅธ ํƒญ/์ฐฝ/์•ฑ์œผ๋กœ ์ด๋™), 20์ดˆ ๋’ค์—๋„ + // ๊ณ„์† ๋”ด ๋ฐ ๊ฐ€์žˆ์œผ๋ฉด ๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ์œผ๋กœ ๋‹ค์‹œ ๊ณต๋ถ€ํ•˜๋ผ๊ณ  ์•Œ๋ ค์ค€๋‹ค. + final bool isAway = + state == AppLifecycleState.hidden || + state == AppLifecycleState.inactive || + state == AppLifecycleState.paused; + if (isAway && _controller.isRunning) { + _awayTimer?.cancel(); + _awayTimer = Timer(const Duration(seconds: 20), () { + showBrowserNotification( + '๋ฐฑํ’ˆํƒ€ - ๊ณต๋ถ€ ์ค‘์ด์—ˆ์ž–์•„์š”!', + 'ํƒ€์ด๋จธ๊ฐ€ ์ผœ์ ธ ์žˆ์–ด์š”. ๋‹ค์‹œ ๋Œ์•„์™€์„œ ์ง‘์ค‘ํ•ด๋ณผ๊นŒ์š”?', + ); + }); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); + _awayTimer?.cancel(); _controller.dispose(); super.dispose(); } @@ -398,7 +419,10 @@ class _StudyTimerScreenState extends State icon: Icons.play_arrow_rounded, label: _controller.pomodoroEnabled ? '๋ฝ€๋ชจ๋„๋กœ ์‹œ์ž‘' : '๊ณต๋ถ€ ์‹œ์ž‘', color: AppPalette.ink, - onTap: () => _runAction(_controller.start), + onTap: () { + requestNotificationPermission(); + _runAction(_controller.start); + }, ); } if (_controller.pomodoroEnabled && _controller.phase != null) {