feat: 알림 재예약 개선과 패키지 업그레이드

This commit is contained in:
JiWoong Sul
2025-09-19 18:10:47 +09:00
parent e909ba59a4
commit 87f82546a4
24 changed files with 210 additions and 122 deletions

View File

@@ -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());

View File

@@ -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,
);
}
}

View File

@@ -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();

View File

@@ -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();
},
),
),
),
],
),
),

View File

@@ -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: [

View File

@@ -186,7 +186,7 @@ class _SplashScreenState extends State<SplashScreen>
),
),
);
}).toList(),
}),
// 상단 원형 장식 제거(단색 배경 유지)
Positioned(

View File

@@ -1,2 +1,2 @@
/// URL Matcher 패키지의 export 파일
// URL Matcher 패키지의 export 파일
export 'models/service_info.dart';

View File

@@ -298,7 +298,7 @@ class EventAnalysisCard extends StatelessWidget {
],
),
);
}).toList(),
}),
],
),
),

View File

@@ -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,
),
),
),

View File

@@ -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,
),
);
},

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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,
),
],
),
),
),
],
);
}
}

View File

@@ -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) {