feat(app): add manual entry and sharing flows

This commit is contained in:
JiWoong Sul
2025-11-19 16:36:39 +09:00
parent 5ade584370
commit 947fe59486
110 changed files with 5937 additions and 3781 deletions

View File

@@ -12,13 +12,14 @@ class NotificationService {
NotificationService._internal();
// Flutter Local Notifications 플러그인
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
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;
@@ -26,10 +27,12 @@ class NotificationService {
Future<bool> initialize() async {
// 시간대 초기화
tz.initializeTimeZones();
// Android 초기화 설정
const androidInitSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const androidInitSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);
// iOS 초기화 설정
final iosInitSettings = DarwinInitializationSettings(
requestAlertPermission: true,
@@ -39,33 +42,33 @@ class NotificationService {
// 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;
}
@@ -79,9 +82,11 @@ class NotificationService {
playSound: true,
enableVibration: true,
);
await _notifications
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.createNotificationChannel(androidChannel);
}
@@ -89,23 +94,32 @@ class NotificationService {
Future<bool> requestPermission() async {
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
final androidImplementation = _notifications
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidImplementation != null) {
// Android 13 (API 33) 이상에서는 권한 요청이 필요
if (!kIsWeb && (true /* 웹에서는 버전 체크 불가 */)) {
final granted = await androidImplementation.requestNotificationsPermission();
if (!kIsWeb && (true /* 웹에서는 버전 체크 불가 */ )) {
final granted = await androidImplementation
.requestNotificationsPermission();
return granted ?? false;
}
// Android 12 이하는 자동 허용
return true;
}
} else if (!kIsWeb && (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS)) {
} else if (!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS)) {
final iosImplementation = _notifications
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>();
final macosImplementation = _notifications
.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>();
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin
>();
if (iosImplementation != null) {
final granted = await iosImplementation.requestPermissions(
alert: true,
@@ -114,7 +128,7 @@ class NotificationService {
);
return granted ?? false;
}
if (macosImplementation != null) {
final granted = await macosImplementation.requestPermissions(
alert: true,
@@ -124,7 +138,7 @@ class NotificationService {
return granted ?? false;
}
}
return false;
}
@@ -132,11 +146,13 @@ class NotificationService {
Future<bool> checkPermission() async {
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
final androidImplementation = _notifications
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidImplementation != null) {
// Android 13 이상에서만 권한 확인
if (!kIsWeb && (true /* 웹에서는 버전 체크 불가 */)) {
if (!kIsWeb && (true /* 웹에서는 버전 체크 불가 */ )) {
final granted = await androidImplementation.areNotificationsEnabled();
return granted ?? false;
}
@@ -144,27 +160,27 @@ class NotificationService {
return true;
}
}
// iOS/macOS는 설정에서 확인
return true;
}
// 알림 탭 콜백
static void Function(NotificationResponse)? onNotificationTap;
/// 방문 확인 알림 예약
Future<void> scheduleVisitReminder({
required String restaurantId,
required String restaurantName,
required DateTime recommendationTime,
int? delayMinutes,
}) 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 minutesToWait = delayMinutes ?? 90 + Random().nextInt(31);
final scheduledTime = tz.TZDateTime.now(
tz.local,
).add(Duration(minutes: minutesToWait));
// 알림 상세 설정
final androidDetails = AndroidNotificationDetails(
_channelId,
@@ -178,20 +194,20 @@ class NotificationService {
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,
@@ -202,11 +218,11 @@ class NotificationService {
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
payload: 'visit_reminder|$restaurantId|$restaurantName|${recommendationTime.toIso8601String()}',
payload:
'visit_reminder|$restaurantId|$restaurantName|${recommendationTime.toIso8601String()}',
);
if (kDebugMode) {
print('알림 예약됨: ${scheduledTime.toLocal()} ($randomMinutes 후)');
print('알림 예약됨: ${scheduledTime.toLocal()} ($minutesToWait 후)');
}
} catch (e) {
if (kDebugMode) {
@@ -265,20 +281,15 @@ class NotificationService {
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,
);
await _notifications.show(0, title, body, notificationDetails);
}
}
}