feat: 알림 재예약 개선과 패키지 업그레이드
This commit is contained in:
@@ -114,6 +114,23 @@ class NotificationProvider extends ChangeNotifier {
|
||||
|
||||
// 알림이 활성화된 경우에만 알림 재예약 (비활성화 시에는 필요 없음)
|
||||
if (value) {
|
||||
final hasPermission = await NotificationService.checkPermission();
|
||||
if (!hasPermission) {
|
||||
final granted = await NotificationService.requestPermission();
|
||||
if (!granted) {
|
||||
debugPrint('알림 권한이 부여되지 않았습니다. 일부 알림이 제한될 수 있습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
final canExact = await NotificationService.canScheduleExactAlarms();
|
||||
if (!canExact) {
|
||||
final exactGranted =
|
||||
await NotificationService.requestExactAlarmsPermission();
|
||||
if (!exactGranted) {
|
||||
debugPrint('정확 알람 권한이 없어 근사 알림으로 예약됩니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 알림 설정 변경 시 모든 구독의 알림 재예약
|
||||
// 지연 실행으로 UI 응답성 향상
|
||||
Future.microtask(() => _rescheduleNotificationsIfNeeded());
|
||||
|
||||
@@ -103,6 +103,14 @@ class SubscriptionProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reschedulePaymentNotifications() async {
|
||||
try {
|
||||
await NotificationService.reschedulAllNotifications(_subscriptions);
|
||||
} catch (e) {
|
||||
debugPrint('결제 알림 재예약 중 오류 발생: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addSubscription({
|
||||
required String serviceName,
|
||||
required double monthlyCost,
|
||||
@@ -145,6 +153,8 @@ class SubscriptionProvider extends ChangeNotifier {
|
||||
if (isEventActive && eventEndDate != null) {
|
||||
await _scheduleEventEndNotification(subscription);
|
||||
}
|
||||
|
||||
await _reschedulePaymentNotifications();
|
||||
} catch (e) {
|
||||
debugPrint('구독 추가 중 오류 발생: $e');
|
||||
rethrow;
|
||||
@@ -176,6 +186,8 @@ class SubscriptionProvider extends ChangeNotifier {
|
||||
debugPrint('[SubscriptionProvider] 구독 업데이트 완료, '
|
||||
'현재 총 월간 지출: ${totalMonthlyExpense.toStringAsFixed(2)}');
|
||||
notifyListeners();
|
||||
|
||||
await _reschedulePaymentNotifications();
|
||||
} catch (e) {
|
||||
debugPrint('구독 업데이트 중 오류 발생: $e');
|
||||
rethrow;
|
||||
@@ -186,6 +198,8 @@ class SubscriptionProvider extends ChangeNotifier {
|
||||
try {
|
||||
await _subscriptionBox.delete(id);
|
||||
await refreshSubscriptions();
|
||||
|
||||
await _reschedulePaymentNotifications();
|
||||
} catch (e) {
|
||||
debugPrint('구독 삭제 중 오류 발생: $e');
|
||||
rethrow;
|
||||
@@ -213,6 +227,8 @@ class SubscriptionProvider extends ChangeNotifier {
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
await _reschedulePaymentNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +242,7 @@ class SubscriptionProvider extends ChangeNotifier {
|
||||
title: '이벤트 종료 알림',
|
||||
body: '${subscription.serviceName}의 할인 이벤트가 종료되었습니다.',
|
||||
scheduledDate: subscription.eventEndDate!,
|
||||
channelId: NotificationService.expirationChannelId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../widgets/add_subscription/add_subscription_save_button.dart';
|
||||
|
||||
/// 새로운 구독을 추가하는 화면
|
||||
class AddSubscriptionScreen extends StatefulWidget {
|
||||
const AddSubscriptionScreen({Key? key}) : super(key: key);
|
||||
const AddSubscriptionScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AddSubscriptionScreen> createState() => _AddSubscriptionScreenState();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode;
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/notification_provider.dart';
|
||||
import 'dart:io';
|
||||
@@ -694,6 +694,25 @@ class SettingsScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (kDebugMode)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
top: 16.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons
|
||||
.notifications_active),
|
||||
label:
|
||||
const Text('테스트 알림'),
|
||||
onPressed: () {
|
||||
NotificationService
|
||||
.showTestPaymentNotification();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,18 +16,21 @@ class SmsScanScreen extends StatefulWidget {
|
||||
|
||||
class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
late SmsScanController _controller;
|
||||
late final ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = SmsScanController();
|
||||
_controller.addListener(_handleControllerUpdate);
|
||||
_scrollController = ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_handleControllerUpdate);
|
||||
_controller.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -93,16 +96,37 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
websiteUrlController: _controller.websiteUrlController,
|
||||
selectedCategoryId: _controller.selectedCategoryId,
|
||||
onCategoryChanged: _controller.setSelectedCategoryId,
|
||||
onAdd: () => _controller.addCurrentSubscription(context),
|
||||
onSkip: () => _controller.skipCurrentSubscription(context),
|
||||
onAdd: _handleAddSubscription,
|
||||
onSkip: _handleSkipSubscription,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleAddSubscription() async {
|
||||
await _controller.addCurrentSubscription(context);
|
||||
if (!mounted) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToTop());
|
||||
}
|
||||
|
||||
void _handleSkipSubscription() {
|
||||
_controller.skipCurrentSubscription(context);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToTop());
|
||||
}
|
||||
|
||||
void _scrollToTop() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
_scrollController.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
@@ -186,7 +186,7 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
|
||||
// 상단 원형 장식 제거(단색 배경 유지)
|
||||
Positioned(
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/// URL Matcher 패키지의 export 파일
|
||||
// URL Matcher 패키지의 export 파일
|
||||
export 'models/service_info.dart';
|
||||
|
||||
@@ -298,7 +298,7 @@ class EventAnalysisCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -226,10 +226,10 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
barTouchData: BarTouchData(
|
||||
enabled: true,
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
tooltipBgColor: Theme.of(context)
|
||||
tooltipBorderRadius: BorderRadius.circular(8),
|
||||
getTooltipColor: (group) => Theme.of(context)
|
||||
.colorScheme
|
||||
.inverseSurface,
|
||||
tooltipRoundedRadius: 8,
|
||||
getTooltipItem:
|
||||
(group, groupIndex, rod, rodIndex) {
|
||||
return BarTooltipItem(
|
||||
@@ -261,10 +261,10 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
swapAnimationDuration: ReduceMotion.isEnabled(context)
|
||||
? const Duration(milliseconds: 0)
|
||||
duration: ReduceMotion.isEnabled(context)
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 300),
|
||||
swapAnimationCurve: Curves.easeOut,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -390,12 +390,10 @@ class _SubscriptionPieChartCardState extends State<SubscriptionPieChartCard> {
|
||||
},
|
||||
),
|
||||
),
|
||||
swapAnimationDuration:
|
||||
ReduceMotion.isEnabled(context)
|
||||
? const Duration(milliseconds: 0)
|
||||
: const Duration(
|
||||
milliseconds: 300),
|
||||
swapAnimationCurve: Curves.easeOut,
|
||||
duration: ReduceMotion.isEnabled(context)
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -10,10 +10,10 @@ class AnimatedWaveBackground extends StatelessWidget {
|
||||
final AnimationController pulseController;
|
||||
|
||||
const AnimatedWaveBackground({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.pulseController,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -15,14 +15,14 @@ class CategoryHeaderWidget extends StatelessWidget {
|
||||
final double totalCostCNY;
|
||||
|
||||
const CategoryHeaderWidget({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.categoryName,
|
||||
required this.subscriptionCount,
|
||||
required this.totalCostUSD,
|
||||
required this.totalCostKRW,
|
||||
required this.totalCostJPY,
|
||||
required this.totalCostCNY,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -17,12 +17,12 @@ class EmptyStateWidget extends StatelessWidget {
|
||||
final VoidCallback onAddPressed;
|
||||
|
||||
const EmptyStateWidget({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.fadeController,
|
||||
required this.rotateController,
|
||||
required this.slideController,
|
||||
required this.onAddPressed,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -18,13 +18,13 @@ class MainScreenSummaryCard extends StatelessWidget {
|
||||
final AnimationController waveController;
|
||||
final AnimationController slideController;
|
||||
const MainScreenSummaryCard({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.provider,
|
||||
required this.fadeController,
|
||||
required this.pulseController,
|
||||
required this.waveController,
|
||||
required this.slideController,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -11,8 +11,7 @@ import '../theme/ui_constants.dart';
|
||||
/// SRP에 따라 광고 전용 위젯으로 분리
|
||||
class NativeAdWidget extends StatefulWidget {
|
||||
final bool useOuterPadding; // true이면 외부에서 페이지 패딩을 제공
|
||||
const NativeAdWidget({Key? key, this.useOuterPadding = false})
|
||||
: super(key: key);
|
||||
const NativeAdWidget({super.key, this.useOuterPadding = false});
|
||||
|
||||
@override
|
||||
State<NativeAdWidget> createState() => _NativeAdWidgetState();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
// import '../../theme/app_colors.dart';
|
||||
import '../../widgets/native_ad_widget.dart';
|
||||
import '../../widgets/themed_text.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
@@ -8,29 +8,33 @@ class ScanLoadingWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ThemedText(
|
||||
AppLocalizations.of(context).scanningMessages,
|
||||
forceDark: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ThemedText(
|
||||
AppLocalizations.of(context).findingSubscriptions,
|
||||
opacity: 0.7,
|
||||
forceDark: true,
|
||||
),
|
||||
],
|
||||
return Column(
|
||||
children: [
|
||||
const NativeAdWidget(key: ValueKey('sms_scan_loading_ad')),
|
||||
const SizedBox(height: 48),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ThemedText(
|
||||
AppLocalizations.of(context).scanningMessages,
|
||||
forceDark: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ThemedText(
|
||||
AppLocalizations.of(context).findingSubscriptions,
|
||||
opacity: 0.7,
|
||||
forceDark: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ class SubscriptionListWidget extends StatelessWidget {
|
||||
final AnimationController fadeController;
|
||||
|
||||
const SubscriptionListWidget({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.categorizedSubscriptions,
|
||||
required this.fadeController,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -188,9 +188,9 @@ class MultiSliver extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
const MultiSliver({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.children,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
Reference in New Issue
Block a user