Compare commits
8 Commits
0db1f12b40
...
codex/feat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37e797f6c1 | ||
|
|
903906c880 | ||
|
|
5de33992a2 | ||
|
|
cc8bcc7b54 | ||
|
|
9a950ee6c7 | ||
|
|
88569a57bf | ||
|
|
7125a4745a | ||
|
|
8d6b24ed6f |
@@ -17,15 +17,13 @@ import '../providers/category_provider.dart';
|
|||||||
import '../providers/payment_card_provider.dart';
|
import '../providers/payment_card_provider.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../utils/logger.dart';
|
import '../utils/logger.dart';
|
||||||
|
import '../widgets/common/snackbar/app_snackbar.dart';
|
||||||
|
|
||||||
class SmsScanController extends ChangeNotifier {
|
class SmsScanController extends ChangeNotifier {
|
||||||
// 상태 관리
|
// 상태 관리
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
|
|
||||||
String? _errorMessage;
|
|
||||||
String? get errorMessage => _errorMessage;
|
|
||||||
|
|
||||||
List<Subscription> _scannedSubscriptions = [];
|
List<Subscription> _scannedSubscriptions = [];
|
||||||
List<Subscription> get scannedSubscriptions => _scannedSubscriptions;
|
List<Subscription> get scannedSubscriptions => _scannedSubscriptions;
|
||||||
PaymentCardSuggestion? _currentSuggestion;
|
PaymentCardSuggestion? _currentSuggestion;
|
||||||
@@ -109,7 +107,6 @@ class SmsScanController extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> scanSms(BuildContext context) async {
|
Future<void> scanSms(BuildContext context) async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = null;
|
|
||||||
_scannedSubscriptions = [];
|
_scannedSubscriptions = [];
|
||||||
_currentIndex = 0;
|
_currentIndex = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -137,9 +134,12 @@ class SmsScanController extends ChangeNotifier {
|
|||||||
final req = await permission.Permission.sms.request();
|
final req = await permission.Permission.sms.request();
|
||||||
if (!ctx.mounted) return;
|
if (!ctx.mounted) return;
|
||||||
if (!req.isGranted) {
|
if (!req.isGranted) {
|
||||||
// 거부됨: 안내 후 종료
|
// 거부됨: 토스트 표시 후 종료
|
||||||
if (!ctx.mounted) return;
|
if (!ctx.mounted) return;
|
||||||
_errorMessage = AppLocalizations.of(ctx).smsPermissionRequired;
|
AppSnackBar.showError(
|
||||||
|
context: ctx,
|
||||||
|
message: AppLocalizations.of(ctx).smsPermissionRequired,
|
||||||
|
);
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
@@ -162,7 +162,10 @@ class SmsScanController extends ChangeNotifier {
|
|||||||
|
|
||||||
if (scanResults.isEmpty) {
|
if (scanResults.isEmpty) {
|
||||||
Log.i('스캔된 구독이 없음');
|
Log.i('스캔된 구독이 없음');
|
||||||
_errorMessage = AppLocalizations.of(context).subscriptionNotFound;
|
AppSnackBar.showError(
|
||||||
|
context: context,
|
||||||
|
message: AppLocalizations.of(context).subscriptionNotFound,
|
||||||
|
);
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
@@ -184,7 +187,10 @@ class SmsScanController extends ChangeNotifier {
|
|||||||
|
|
||||||
if (repeatSubscriptions.isEmpty) {
|
if (repeatSubscriptions.isEmpty) {
|
||||||
Log.i('반복 결제된 구독이 없음');
|
Log.i('반복 결제된 구독이 없음');
|
||||||
_errorMessage = AppLocalizations.of(context).repeatSubscriptionNotFound;
|
AppSnackBar.showError(
|
||||||
|
context: context,
|
||||||
|
message: AppLocalizations.of(context).repeatSubscriptionNotFound,
|
||||||
|
);
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
@@ -223,8 +229,10 @@ class SmsScanController extends ChangeNotifier {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.e('SMS 스캔 중 오류 발생', e);
|
Log.e('SMS 스캔 중 오류 발생', e);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
_errorMessage =
|
AppSnackBar.showError(
|
||||||
AppLocalizations.of(context).smsScanErrorWithMessage(e.toString());
|
context: context,
|
||||||
|
message: AppLocalizations.of(context).smsScanErrorWithMessage(e.toString()),
|
||||||
|
);
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -343,7 +351,6 @@ class SmsScanController extends ChangeNotifier {
|
|||||||
void resetState() {
|
void resetState() {
|
||||||
_scannedSubscriptions = [];
|
_scannedSubscriptions = [];
|
||||||
_currentIndex = 0;
|
_currentIndex = 0;
|
||||||
_errorMessage = null;
|
|
||||||
_selectedPaymentCardId = null;
|
_selectedPaymentCardId = null;
|
||||||
_currentSuggestion = null;
|
_currentSuggestion = null;
|
||||||
_shouldSuggestCardCreation = false;
|
_shouldSuggestCardCreation = false;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode;
|
import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import '../providers/notification_provider.dart';
|
import '../providers/notification_provider.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import '../services/notification_service.dart';
|
import '../services/notification_service.dart';
|
||||||
@@ -884,22 +885,28 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
.withValues(alpha: 0.5),
|
.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: FutureBuilder<PackageInfo>(
|
||||||
contentPadding: const EdgeInsets.all(8),
|
future: PackageInfo.fromPlatform(),
|
||||||
title: Text(
|
builder: (context, snapshot) {
|
||||||
AppLocalizations.of(context).appInfo,
|
final version = snapshot.data?.version ?? '-';
|
||||||
style: TextStyle(
|
return ListTile(
|
||||||
color: Theme.of(context).colorScheme.onSurface),
|
contentPadding: const EdgeInsets.all(8),
|
||||||
),
|
title: Text(
|
||||||
subtitle: Text(
|
AppLocalizations.of(context).appInfo,
|
||||||
'${AppLocalizations.of(context).version} 1.0.0',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: Theme.of(context).colorScheme.onSurface),
|
||||||
color:
|
),
|
||||||
Theme.of(context).colorScheme.onSurfaceVariant),
|
subtitle: Text(
|
||||||
),
|
'${AppLocalizations.of(context).version} $version',
|
||||||
leading: Icon(Icons.info,
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
color: Theme.of(context)
|
||||||
onTap: null,
|
.colorScheme
|
||||||
|
.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
leading: Icon(Icons.info,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.onSurfaceVariant),
|
||||||
|
onTap: null,
|
||||||
// onTap: () async {
|
// onTap: () async {
|
||||||
// // 항상 앱 내 About 다이얼로그를 우선 표시 (현재 미사용)
|
// // 항상 앱 내 About 다이얼로그를 우선 표시 (현재 미사용)
|
||||||
// showAboutDialog(
|
// showAboutDialog(
|
||||||
@@ -954,6 +961,8 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
// ],
|
// ],
|
||||||
// );
|
// );
|
||||||
// },
|
// },
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// FloatingNavigationBar를 위한 충분한 하단 여백
|
// FloatingNavigationBar를 위한 충분한 하단 여백
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
if (_controller.scannedSubscriptions.isEmpty) {
|
if (_controller.scannedSubscriptions.isEmpty) {
|
||||||
return ScanInitialWidget(
|
return ScanInitialWidget(
|
||||||
onScanPressed: () => _controller.startScan(context),
|
onScanPressed: () => _controller.startScan(context),
|
||||||
errorMessage: _controller.errorMessage,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +76,6 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
});
|
});
|
||||||
return ScanInitialWidget(
|
return ScanInitialWidget(
|
||||||
onScanPressed: () => _controller.startScan(context),
|
onScanPressed: () => _controller.startScan(context),
|
||||||
errorMessage: _controller.errorMessage,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
child: FadeTransition(
|
child: FadeTransition(
|
||||||
opacity: _fadeAnimation,
|
opacity: _fadeAnimation,
|
||||||
child: Text(
|
child: Text(
|
||||||
'© 2025 NatureBridgeAI. All rights reserved.',
|
'© 2025 NatureBridgeAI @ cclabs. All rights reserved.',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
|
|||||||
@@ -214,20 +214,20 @@ class BillingCostUtil {
|
|||||||
// 결제 주기에 따른 개월 수
|
// 결제 주기에 따른 개월 수
|
||||||
final cycleMonths = _getCycleMonths(normalizedCycle);
|
final cycleMonths = _getCycleMonths(normalizedCycle);
|
||||||
|
|
||||||
// 결제 발생 월 계산 (nextBillingDate 기준으로 역산)
|
// 연도+월을 포함한 개월 차이 계산
|
||||||
final billingMonth = nextBillingDate.month;
|
// nextBillingDate와 target 월 사이의 차이가 cycleMonths의 배수인지 확인
|
||||||
|
final targetStart = DateTime(targetYear, targetMonth, 1);
|
||||||
|
final billingStart =
|
||||||
|
DateTime(nextBillingDate.year, nextBillingDate.month, 1);
|
||||||
|
|
||||||
// 대상 월이 결제 발생 월과 일치하는지 확인
|
final monthDiff = (billingStart.year - targetStart.year) * 12 +
|
||||||
// 예: 연간 결제(1월), targetMonth = 1 → true
|
(billingStart.month - targetStart.month);
|
||||||
// 예: 연간 결제(1월), targetMonth = 2 → false
|
|
||||||
for (int i = 0; i < 12; i += cycleMonths) {
|
|
||||||
final checkMonth = ((billingMonth - 1 + i) % 12) + 1;
|
|
||||||
if (checkMonth == targetMonth) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
// monthDiff가 cycleMonths의 배수이면 해당 월에 결제 발생
|
||||||
|
// 예: 연간 결제(2027-01), target=2026-01 → monthDiff=12, 12%12=0 → true (이전 결제)
|
||||||
|
// 예: 연간 결제(2027-01), target=2026-02 → monthDiff=11, 11%12≠0 → false
|
||||||
|
// 예: 연간 결제(2027-01), target=2027-01 → monthDiff=0, 0%12=0 → true
|
||||||
|
return monthDiff % cycleMonths == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 결제 주기별 개월 수 반환
|
/// 결제 주기별 개월 수 반환
|
||||||
|
|||||||
@@ -161,24 +161,14 @@ class _HomeContentState extends State<HomeContent> {
|
|||||||
).animate(CurvedAnimation(
|
).animate(CurvedAnimation(
|
||||||
parent: widget.slideController,
|
parent: widget.slideController,
|
||||||
curve: Curves.easeOutCubic)),
|
curve: Curves.easeOutCubic)),
|
||||||
child: Row(
|
child: Text(
|
||||||
children: [
|
AppLocalizations.of(context).subscriptionCount(
|
||||||
Text(
|
subscriptionProvider.subscriptions.length),
|
||||||
AppLocalizations.of(context).subscriptionCount(
|
style: TextStyle(
|
||||||
subscriptionProvider.subscriptions.length),
|
fontSize: 14,
|
||||||
style: TextStyle(
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 14,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
fontWeight: FontWeight.w600,
|
),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 14,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import '../../l10n/app_localizations.dart';
|
|||||||
|
|
||||||
class ScanInitialWidget extends StatelessWidget {
|
class ScanInitialWidget extends StatelessWidget {
|
||||||
final VoidCallback onScanPressed;
|
final VoidCallback onScanPressed;
|
||||||
final String? errorMessage;
|
|
||||||
|
|
||||||
const ScanInitialWidget({
|
const ScanInitialWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onScanPressed,
|
required this.onScanPressed,
|
||||||
this.errorMessage,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -24,15 +22,6 @@ class ScanInitialWidget extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (errorMessage != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 24.0),
|
|
||||||
child: ThemedText(
|
|
||||||
errorMessage!,
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ThemedText(
|
ThemedText(
|
||||||
AppLocalizations.of(context).findRepeatSubscriptions,
|
AppLocalizations.of(context).findRepeatSubscriptions,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Foundation
|
|||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
import flutter_secure_storage_darwin
|
import flutter_secure_storage_darwin
|
||||||
import local_auth_darwin
|
import local_auth_darwin
|
||||||
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
@@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||||
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
||||||
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -733,6 +733,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
package_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: package_info_plus
|
||||||
|
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.3.1"
|
||||||
|
package_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_platform_interface
|
||||||
|
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: submanager
|
name: submanager
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.0.5+7
|
version: 1.0.8+10
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@@ -42,6 +42,7 @@ dependencies:
|
|||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
image: ^4.5.4
|
image: ^4.5.4
|
||||||
google_mobile_ads: ^6.0.0
|
google_mobile_ads: ^6.0.0
|
||||||
|
package_info_plus: ^8.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user