import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:timezone/timezone.dart' as tz; import 'package:timezone/data/latest_all.dart' as tz; /// 알림 서비스 싱글톤 클래스 class NotificationService { // 싱글톤 인스턴스 static final NotificationService _instance = NotificationService._internal(); factory NotificationService() => _instance; NotificationService._internal(); // Flutter Local Notifications 플러그인 final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); // 알림 채널 정보 static const String _channelId = 'lunchpick_visit_reminder'; static const String _channelName = '방문 확인 알림'; static const String _channelDescription = '점심 식사 후 방문을 확인하는 알림입니다.'; // 알림 ID (방문 확인용) static const int _visitReminderNotificationId = 1; /// 알림 서비스 초기화 Future initialize() async { // 시간대 초기화 tz.initializeTimeZones(); // Android 초기화 설정 const androidInitSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); // iOS 초기화 설정 final iosInitSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, onDidReceiveLocalNotification: (id, title, body, payload) async { // iOS 9 이하 버전 대응 }, ); // macOS 초기화 설정 final macOSInitSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); // 플랫폼별 초기화 설정 통합 final initSettings = InitializationSettings( android: androidInitSettings, iOS: iosInitSettings, macOS: macOSInitSettings, ); // 알림 플러그인 초기화 final initialized = await _notifications.initialize( initSettings, onDidReceiveNotificationResponse: _onNotificationTap, onDidReceiveBackgroundNotificationResponse: _onBackgroundNotificationTap, ); // Android 알림 채널 생성 (웹이 아닌 경우에만) if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { await _createNotificationChannel(); } return initialized ?? false; } /// Android 알림 채널 생성 Future _createNotificationChannel() async { const androidChannel = AndroidNotificationChannel( _channelId, _channelName, description: _channelDescription, importance: Importance.high, playSound: true, enableVibration: true, ); await _notifications .resolvePlatformSpecificImplementation() ?.createNotificationChannel(androidChannel); } /// 알림 권한 요청 Future requestPermission() async { if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { final androidImplementation = _notifications .resolvePlatformSpecificImplementation(); if (androidImplementation != null) { // Android 13 (API 33) 이상에서는 권한 요청이 필요 if (!kIsWeb && (true /* 웹에서는 버전 체크 불가 */)) { final granted = await androidImplementation.requestNotificationsPermission(); return granted ?? false; } // Android 12 이하는 자동 허용 return true; } } else if (!kIsWeb && (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS)) { final iosImplementation = _notifications .resolvePlatformSpecificImplementation(); final macosImplementation = _notifications .resolvePlatformSpecificImplementation(); if (iosImplementation != null) { final granted = await iosImplementation.requestPermissions( alert: true, badge: true, sound: true, ); return granted ?? false; } if (macosImplementation != null) { final granted = await macosImplementation.requestPermissions( alert: true, badge: true, sound: true, ); return granted ?? false; } } return false; } /// 권한 상태 확인 Future checkPermission() async { if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { final androidImplementation = _notifications .resolvePlatformSpecificImplementation(); if (androidImplementation != null) { // Android 13 이상에서만 권한 확인 if (!kIsWeb && (true /* 웹에서는 버전 체크 불가 */)) { final granted = await androidImplementation.areNotificationsEnabled(); return granted ?? false; } // Android 12 이하는 자동 허용 return true; } } // iOS/macOS는 설정에서 확인 return true; } // 알림 탭 콜백 static void Function(NotificationResponse)? onNotificationTap; /// 방문 확인 알림 예약 Future scheduleVisitReminder({ required String restaurantId, required String restaurantName, required DateTime recommendationTime, }) async { try { // 1.5~2시간 사이의 랜덤 시간 계산 (90~120분) final randomMinutes = 90 + Random().nextInt(31); // 90 + 0~30분 final scheduledTime = tz.TZDateTime.now(tz.local).add( Duration(minutes: randomMinutes), ); // 알림 상세 설정 final androidDetails = AndroidNotificationDetails( _channelId, _channelName, channelDescription: _channelDescription, importance: Importance.high, priority: Priority.high, ticker: '방문 확인', icon: '@mipmap/ic_launcher', autoCancel: true, enableVibration: true, playSound: true, ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, sound: 'default', ); final notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, macOS: iosDetails, ); // 알림 예약 await _notifications.zonedSchedule( _visitReminderNotificationId, '다녀왔음? 🍴', '$restaurantName 어땠어요? 방문 기록을 남겨주세요!', scheduledTime, notificationDetails, androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, payload: 'visit_reminder|$restaurantId|$restaurantName|${recommendationTime.toIso8601String()}', ); if (kDebugMode) { print('알림 예약됨: ${scheduledTime.toLocal()} ($randomMinutes분 후)'); } } catch (e) { if (kDebugMode) { print('알림 예약 실패: $e'); } } } /// 예약된 방문 확인 알림 취소 Future cancelVisitReminder() async { await _notifications.cancel(_visitReminderNotificationId); } /// 모든 알림 취소 Future cancelAllNotifications() async { await _notifications.cancelAll(); } /// 예약된 알림 목록 조회 Future> getPendingNotifications() async { return await _notifications.pendingNotificationRequests(); } /// 알림 탭 이벤트 처리 void _onNotificationTap(NotificationResponse response) { if (onNotificationTap != null) { onNotificationTap!(response); } else if (response.payload != null) { if (kDebugMode) { print('알림 탭: ${response.payload}'); } } } /// 백그라운드 알림 탭 이벤트 처리 @pragma('vm:entry-point') static void _onBackgroundNotificationTap(NotificationResponse response) { if (onNotificationTap != null) { onNotificationTap!(response); } else if (response.payload != null) { if (kDebugMode) { print('백그라운드 알림 탭: ${response.payload}'); } } } /// 즉시 알림 표시 (테스트용) Future showImmediateNotification({ required String title, required String body, }) async { const androidDetails = AndroidNotificationDetails( _channelId, _channelName, channelDescription: _channelDescription, importance: Importance.high, priority: Priority.high, ); const iosDetails = DarwinNotificationDetails(); const notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, macOS: iosDetails, ); await _notifications.show( 0, title, body, notificationDetails, ); } }