From e8b375a18c0757df6009c06569031ef1034f64fb Mon Sep 17 00:00:00 2001 From: sihoo Date: Mon, 21 Sep 2026 04:17:48 +0000 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=ED=92=88=ED=83=80=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=EB=A8=B8=20=EC=BC=9C=EB=86=93=EA=B3=A0=20=ED=83=AD=20?= =?UTF-8?q?=EC=9D=B4=ED=83=88=20=EC=8B=9C=20=EB=B8=8C=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EC=A0=80=20=EC=95=8C=EB=A6=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공부 타이머가 진행 중일 때 브라우저 탭이 백그라운드로 가면(다른 탭/창/앱으로 이동) 20초 뒤에도 계속 그 상태면 "다시 돌아와서 집중해볼까요?" 브라우저 알림을 띄운다. dart:html 대신 package:web + js_interop로 구현하고, 웹이 아닌 빌드에서는 조건부 임포트로 아무 동작 안 하는 스텁을 쓴다. Co-Authored-By: Claude Sonnet 5 --- lib/function/browser_notification.dart | 3 +++ lib/function/browser_notification_stub.dart | 4 +++ lib/function/browser_notification_web.dart | 24 +++++++++++++++++ lib/ui/study_timer_screen.dart | 30 ++++++++++++++++++--- 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 lib/function/browser_notification.dart create mode 100644 lib/function/browser_notification_stub.dart create mode 100644 lib/function/browser_notification_web.dart 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) {