diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 217a2ef..89f49a1 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -47,5 +47,8 @@
NSMessageUsageDescription
구독 결제 정보를 자동으로 추가하기 위해 SMS 접근이 필요합니다.
+
+ GADApplicationIdentifier
+ ca-app-pub-6691216385521068~6638409932
diff --git a/lib/controllers/add_subscription_controller.dart b/lib/controllers/add_subscription_controller.dart
index 5f925d5..6a44951 100644
--- a/lib/controllers/add_subscription_controller.dart
+++ b/lib/controllers/add_subscription_controller.dart
@@ -13,29 +13,29 @@ import '../l10n/app_localizations.dart';
/// AddSubscriptionScreen의 비즈니스 로직을 관리하는 Controller
class AddSubscriptionController {
final BuildContext context;
-
+
// Form Key
final formKey = GlobalKey();
-
+
// Text Controllers
final serviceNameController = TextEditingController();
final monthlyCostController = TextEditingController();
final nextBillingDateController = TextEditingController();
final websiteUrlController = TextEditingController();
final eventPriceController = TextEditingController();
-
+
// Form State
String billingCycle = 'monthly';
String currency = 'KRW';
DateTime? nextBillingDate;
bool isLoading = false;
String? selectedCategoryId;
-
+
// Event State
bool isEventActive = false;
DateTime? eventStartDate = DateTime.now();
DateTime? eventEndDate = DateTime.now().add(const Duration(days: 30));
-
+
// Focus Nodes
final serviceNameFocus = FocusNode();
final monthlyCostFocus = FocusNode();
@@ -44,20 +44,20 @@ class AddSubscriptionController {
final websiteUrlFocus = FocusNode();
final categoryFocus = FocusNode();
final currencyFocus = FocusNode();
-
+
// Animation Controller
AnimationController? animationController;
Animation? fadeAnimation;
Animation? slideAnimation;
-
+
// Scroll Controller
final ScrollController scrollController = ScrollController();
double scrollOffset = 0;
-
+
// UI State
int currentEditingField = -1;
bool isSaveHovered = false;
-
+
// Gradient Colors
final List gradientColors = [
const Color(0xFF3B82F6),
@@ -71,19 +71,19 @@ class AddSubscriptionController {
void initialize({required TickerProvider vsync}) {
// 결제일 기본값을 오늘 날짜로 설정
nextBillingDate = DateTime.now();
-
+
// 서비스명 컨트롤러에 리스너 추가
serviceNameController.addListener(onServiceNameChanged);
-
+
// 웹사이트 URL 컨트롤러에 리스너 추가
websiteUrlController.addListener(onWebsiteUrlChanged);
-
+
// 애니메이션 컨트롤러 초기화
animationController = AnimationController(
vsync: vsync,
duration: const Duration(milliseconds: 800),
);
-
+
fadeAnimation = Tween(
begin: 0.0,
end: 1.0,
@@ -91,7 +91,7 @@ class AddSubscriptionController {
parent: animationController!,
curve: Curves.easeIn,
));
-
+
slideAnimation = Tween(
begin: const Offset(0.0, 0.2),
end: Offset.zero,
@@ -99,12 +99,12 @@ class AddSubscriptionController {
parent: animationController!,
curve: Curves.easeOut,
));
-
+
// 스크롤 리스너
scrollController.addListener(() {
scrollOffset = scrollController.offset;
});
-
+
// 애니메이션 시작
animationController!.forward();
}
@@ -117,7 +117,7 @@ class AddSubscriptionController {
nextBillingDateController.dispose();
websiteUrlController.dispose();
eventPriceController.dispose();
-
+
// Focus Nodes
serviceNameFocus.dispose();
monthlyCostFocus.dispose();
@@ -126,10 +126,10 @@ class AddSubscriptionController {
websiteUrlFocus.dispose();
categoryFocus.dispose();
currencyFocus.dispose();
-
+
// Animation
animationController?.dispose();
-
+
// Scroll
scrollController.dispose();
}
@@ -138,43 +138,46 @@ class AddSubscriptionController {
void onServiceNameChanged() {
autoSelectCategory();
}
-
+
/// 웹사이트 URL 변경시 호출
void onWebsiteUrlChanged() async {
final url = websiteUrlController.text.trim();
-
+
// URL이 비어있거나 너무 짧으면 무시
if (url.isEmpty || url.length < 5) return;
-
+
// 이미 서비스명이 입력되어 있으면 자동 매칭하지 않음
if (serviceNameController.text.isNotEmpty) return;
-
+
try {
// URL로 서비스 정보 찾기
final serviceInfo = await SubscriptionUrlMatcher.findServiceByUrl(url);
-
+
if (serviceInfo != null && context.mounted) {
// 서비스명 자동 입력
serviceNameController.text = serviceInfo.serviceName;
-
+
// 카테고리 자동 선택
- final categoryProvider = Provider.of(context, listen: false);
+ final categoryProvider =
+ Provider.of(context, listen: false);
final categories = categoryProvider.categories;
-
+
// 카테고리 ID로 매칭
final matchedCategory = categories.firstWhere(
- (cat) => cat.name == serviceInfo.categoryNameKr ||
- cat.name == serviceInfo.categoryNameEn,
+ (cat) =>
+ cat.name == serviceInfo.categoryNameKr ||
+ cat.name == serviceInfo.categoryNameEn,
orElse: () => categories.first,
);
-
+
selectedCategoryId = matchedCategory.id;
-
+
// 스낵바로 알림
if (context.mounted) {
AppSnackBar.showSuccess(
context: context,
- message: AppLocalizations.of(context).serviceRecognized(serviceInfo.serviceName),
+ message: AppLocalizations.of(context)
+ .serviceRecognized(serviceInfo.serviceName),
);
}
}
@@ -187,17 +190,18 @@ class AddSubscriptionController {
/// 카테고리 자동 선택
void autoSelectCategory() {
- final categoryProvider = Provider.of(context, listen: false);
+ final categoryProvider =
+ Provider.of(context, listen: false);
final categories = categoryProvider.categories;
-
+
final serviceName = serviceNameController.text.toLowerCase();
-
+
// 서비스명에 기반한 카테고리 매칭 로직
dynamic matchedCategory;
-
+
// 엔터테인먼트 관련 키워드
- if (serviceName.contains('netflix') ||
- serviceName.contains('youtube') ||
+ if (serviceName.contains('netflix') ||
+ serviceName.contains('youtube') ||
serviceName.contains('disney') ||
serviceName.contains('왓챠') ||
serviceName.contains('티빙') ||
@@ -210,64 +214,64 @@ class AddSubscriptionController {
);
}
// 음악 관련 키워드
- else if (serviceName.contains('spotify') ||
- serviceName.contains('apple music') ||
- serviceName.contains('멜론') ||
- serviceName.contains('지니') ||
- serviceName.contains('플로') ||
- serviceName.contains('벅스')) {
+ else if (serviceName.contains('spotify') ||
+ serviceName.contains('apple music') ||
+ serviceName.contains('멜론') ||
+ serviceName.contains('지니') ||
+ serviceName.contains('플로') ||
+ serviceName.contains('벅스')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == 'music',
orElse: () => categories.first,
);
}
// 생산성 관련 키워드
- else if (serviceName.contains('notion') ||
- serviceName.contains('microsoft') ||
- serviceName.contains('office') ||
- serviceName.contains('google') ||
- serviceName.contains('dropbox') ||
- serviceName.contains('icloud') ||
- serviceName.contains('adobe')) {
+ else if (serviceName.contains('notion') ||
+ serviceName.contains('microsoft') ||
+ serviceName.contains('office') ||
+ serviceName.contains('google') ||
+ serviceName.contains('dropbox') ||
+ serviceName.contains('icloud') ||
+ serviceName.contains('adobe')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == '생산성',
orElse: () => categories.first,
);
}
// 게임 관련 키워드
- else if (serviceName.contains('xbox') ||
- serviceName.contains('playstation') ||
- serviceName.contains('nintendo') ||
- serviceName.contains('steam') ||
- serviceName.contains('게임')) {
+ else if (serviceName.contains('xbox') ||
+ serviceName.contains('playstation') ||
+ serviceName.contains('nintendo') ||
+ serviceName.contains('steam') ||
+ serviceName.contains('게임')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == '게임',
orElse: () => categories.first,
);
}
// 교육 관련 키워드
- else if (serviceName.contains('coursera') ||
- serviceName.contains('udemy') ||
- serviceName.contains('인프런') ||
- serviceName.contains('패스트캠퍼스') ||
- serviceName.contains('클래스101')) {
+ else if (serviceName.contains('coursera') ||
+ serviceName.contains('udemy') ||
+ serviceName.contains('인프런') ||
+ serviceName.contains('패스트캠퍼스') ||
+ serviceName.contains('클래스101')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == '교육',
orElse: () => categories.first,
);
}
// 쇼핑 관련 키워드
- else if (serviceName.contains('쿠팡') ||
- serviceName.contains('coupang') ||
- serviceName.contains('amazon') ||
- serviceName.contains('네이버') ||
- serviceName.contains('11번가')) {
+ else if (serviceName.contains('쿠팡') ||
+ serviceName.contains('coupang') ||
+ serviceName.contains('amazon') ||
+ serviceName.contains('네이버') ||
+ serviceName.contains('11번가')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == '쇼핑',
orElse: () => categories.first,
);
}
-
+
if (matchedCategory != null) {
selectedCategoryId = matchedCategory.id;
}
@@ -276,9 +280,9 @@ class AddSubscriptionController {
/// SMS 스캔
Future scanSMS({required Function setState}) async {
if (kIsWeb) return;
-
+
setState(() => isLoading = true);
-
+
try {
if (!await SMSService.hasSMSPermission()) {
final granted = await SMSService.requestSMSPermission();
@@ -292,7 +296,7 @@ class AddSubscriptionController {
return;
}
}
-
+
final subscriptions = await SMSService.scanSubscriptions();
if (subscriptions.isEmpty) {
if (context.mounted) {
@@ -303,48 +307,51 @@ class AddSubscriptionController {
}
return;
}
-
+
final subscription = subscriptions.first;
-
+
// SMS에서 서비스 정보 추출 시도
ServiceInfo? serviceInfo;
final smsContent = subscription['smsContent'] ?? '';
-
+
if (smsContent.isNotEmpty) {
try {
- serviceInfo = await SubscriptionUrlMatcher.extractServiceFromSms(smsContent);
+ serviceInfo =
+ await SubscriptionUrlMatcher.extractServiceFromSms(smsContent);
} catch (e) {
if (kDebugMode) {
print('AddSubscriptionController: SMS 서비스 추출 실패 - $e');
}
}
}
-
+
setState(() {
// 서비스 정보가 있으면 우선 사용, 없으면 SMS에서 추출한 정보 사용
if (serviceInfo != null) {
serviceNameController.text = serviceInfo.serviceName;
websiteUrlController.text = serviceInfo.serviceUrl ?? '';
-
+
// 카테고리 자동 선택
- final categoryProvider = Provider.of(context, listen: false);
+ final categoryProvider =
+ Provider.of(context, listen: false);
final categories = categoryProvider.categories;
-
+
final matchedCategory = categories.firstWhere(
- (cat) => cat.name == serviceInfo!.categoryNameKr ||
- cat.name == serviceInfo.categoryNameEn,
+ (cat) =>
+ cat.name == serviceInfo!.categoryNameKr ||
+ cat.name == serviceInfo.categoryNameEn,
orElse: () => categories.first,
);
-
+
selectedCategoryId = matchedCategory.id;
} else {
// 기존 로직 사용
serviceNameController.text = subscription['serviceName'] ?? '';
}
-
+
// 비용 처리 및 통화 단위 자동 감지
final costValue = subscription['monthlyCost']?.toString() ?? '';
-
+
if (costValue.isNotEmpty) {
// 달러 표시가 있거나 소수점이 있으면 달러로 판단
if (costValue.contains('\$') || costValue.contains('.')) {
@@ -353,41 +360,41 @@ class AddSubscriptionController {
if (!numericValue.contains('.')) {
numericValue = '$numericValue.00';
}
- final double parsedValue =
+ final double parsedValue =
double.tryParse(numericValue.replaceAll(',', '')) ?? 0.0;
- monthlyCostController.text =
+ monthlyCostController.text =
NumberFormat('#,##0.00').format(parsedValue);
} else {
currency = 'KRW';
- String numericValue =
+ String numericValue =
costValue.replaceAll('₩', '').replaceAll(',', '').trim();
final int parsedValue = int.tryParse(numericValue) ?? 0;
- monthlyCostController.text =
+ monthlyCostController.text =
NumberFormat.decimalPattern().format(parsedValue);
}
} else {
monthlyCostController.text = '';
}
-
+
billingCycle = subscription['billingCycle'] ?? '월간';
nextBillingDate = subscription['nextBillingDate'] != null
? DateTime.parse(subscription['nextBillingDate'])
: DateTime.now();
-
+
// 서비스 정보가 없고 서비스명이 있으면 URL 자동 매칭 시도
- if (serviceInfo == null &&
- subscription['serviceName'] != null &&
+ if (serviceInfo == null &&
+ subscription['serviceName'] != null &&
subscription['serviceName'].isNotEmpty) {
- final suggestedUrl =
+ final suggestedUrl =
SubscriptionUrlMatcher.suggestUrl(subscription['serviceName']);
if (suggestedUrl != null && websiteUrlController.text.isEmpty) {
websiteUrlController.text = suggestedUrl;
}
-
+
// 서비스명 기반으로 카테고리 자동 선택
autoSelectCategory();
}
-
+
// 애니메이션 재생
animationController!.reset();
animationController!.forward();
@@ -396,7 +403,8 @@ class AddSubscriptionController {
if (context.mounted) {
AppSnackBar.showError(
context: context,
- message: AppLocalizations.of(context).smsScanErrorWithMessage(e.toString()),
+ message: AppLocalizations.of(context)
+ .smsScanErrorWithMessage(e.toString()),
);
}
} finally {
@@ -412,20 +420,19 @@ class AddSubscriptionController {
setState(() {
isLoading = true;
});
-
+
try {
// 콤마 제거하고 숫자만 추출
- final monthlyCost =
+ final monthlyCost =
double.parse(monthlyCostController.text.replaceAll(',', ''));
-
+
// 이벤트 가격 파싱
double? eventPrice;
if (isEventActive && eventPriceController.text.isNotEmpty) {
- eventPrice = double.tryParse(
- eventPriceController.text.replaceAll(',', '')
- );
+ eventPrice =
+ double.tryParse(eventPriceController.text.replaceAll(',', ''));
}
-
+
await Provider.of(context, listen: false)
.addSubscription(
serviceName: serviceNameController.text.trim(),
@@ -440,7 +447,7 @@ class AddSubscriptionController {
eventEndDate: eventEndDate,
eventPrice: eventPrice,
);
-
+
if (context.mounted) {
Navigator.pop(context, true); // 성공 여부 반환
}
@@ -448,11 +455,12 @@ class AddSubscriptionController {
setState(() {
isLoading = false;
});
-
+
if (context.mounted) {
AppSnackBar.showError(
context: context,
- message: AppLocalizations.of(context).saveErrorWithMessage(e.toString()),
+ message:
+ AppLocalizations.of(context).saveErrorWithMessage(e.toString()),
);
}
}
@@ -464,4 +472,4 @@ class AddSubscriptionController {
);
}
}
-}
\ No newline at end of file
+}
diff --git a/lib/controllers/detail_screen_controller.dart b/lib/controllers/detail_screen_controller.dart
index 0036f79..a2125f3 100644
--- a/lib/controllers/detail_screen_controller.dart
+++ b/lib/controllers/detail_screen_controller.dart
@@ -17,17 +17,17 @@ import '../l10n/app_localizations.dart';
class DetailScreenController extends ChangeNotifier {
final BuildContext context;
final SubscriptionModel subscription;
-
+
// Text Controllers
late TextEditingController serviceNameController;
late TextEditingController monthlyCostController;
late TextEditingController websiteUrlController;
late TextEditingController eventPriceController;
-
+
// Display Names
String? _displayName;
String? get displayName => _displayName;
-
+
// Form State
final GlobalKey formKey = GlobalKey();
late String _billingCycle;
@@ -35,12 +35,12 @@ class DetailScreenController extends ChangeNotifier {
String? _selectedCategoryId;
late String _currency;
bool _isLoading = false;
-
+
// Event State
late bool _isEventActive;
DateTime? _eventStartDate;
DateTime? _eventEndDate;
-
+
// Getters
String get billingCycle => _billingCycle;
DateTime get nextBillingDate => _nextBillingDate;
@@ -50,7 +50,7 @@ class DetailScreenController extends ChangeNotifier {
bool get isEventActive => _isEventActive;
DateTime? get eventStartDate => _eventStartDate;
DateTime? get eventEndDate => _eventEndDate;
-
+
// Setters
set billingCycle(String value) {
if (_billingCycle != value) {
@@ -58,21 +58,21 @@ class DetailScreenController extends ChangeNotifier {
notifyListeners();
}
}
-
+
set nextBillingDate(DateTime value) {
if (_nextBillingDate != value) {
_nextBillingDate = value;
notifyListeners();
}
}
-
+
set selectedCategoryId(String? value) {
if (_selectedCategoryId != value) {
_selectedCategoryId = value;
notifyListeners();
}
}
-
+
set currency(String value) {
if (_currency != value) {
_currency = value;
@@ -80,35 +80,35 @@ class DetailScreenController extends ChangeNotifier {
notifyListeners();
}
}
-
+
set isLoading(bool value) {
if (_isLoading != value) {
_isLoading = value;
notifyListeners();
}
}
-
+
set isEventActive(bool value) {
if (_isEventActive != value) {
_isEventActive = value;
notifyListeners();
}
}
-
+
set eventStartDate(DateTime? value) {
if (_eventStartDate != value) {
_eventStartDate = value;
notifyListeners();
}
}
-
+
set eventEndDate(DateTime? value) {
if (_eventEndDate != value) {
_eventEndDate = value;
notifyListeners();
}
}
-
+
// Focus Nodes
final serviceNameFocus = FocusNode();
final monthlyCostFocus = FocusNode();
@@ -117,7 +117,7 @@ class DetailScreenController extends ChangeNotifier {
final websiteUrlFocus = FocusNode();
final categoryFocus = FocusNode();
final currencyFocus = FocusNode();
-
+
// UI State
final ScrollController scrollController = ScrollController();
double scrollOffset = 0;
@@ -125,7 +125,7 @@ class DetailScreenController extends ChangeNotifier {
bool isDeleteHovered = false;
bool isSaveHovered = false;
bool isCancelHovered = false;
-
+
// Animation Controller
AnimationController? animationController;
Animation? fadeAnimation;
@@ -140,42 +140,45 @@ class DetailScreenController extends ChangeNotifier {
/// 초기화
void initialize({required TickerProvider vsync}) {
// Text Controllers 초기화
- serviceNameController = TextEditingController(text: subscription.serviceName);
- monthlyCostController = TextEditingController(text: subscription.monthlyCost.toString());
- websiteUrlController = TextEditingController(text: subscription.websiteUrl ?? '');
+ serviceNameController =
+ TextEditingController(text: subscription.serviceName);
+ monthlyCostController =
+ TextEditingController(text: subscription.monthlyCost.toString());
+ websiteUrlController =
+ TextEditingController(text: subscription.websiteUrl ?? '');
eventPriceController = TextEditingController();
-
+
// Form State 초기화
_billingCycle = subscription.billingCycle;
_nextBillingDate = subscription.nextBillingDate;
_selectedCategoryId = subscription.categoryId;
_currency = subscription.currency;
-
+
// Event State 초기화
_isEventActive = subscription.isEventActive;
_eventStartDate = subscription.eventStartDate;
_eventEndDate = subscription.eventEndDate;
-
+
// 이벤트 가격 초기화
if (subscription.eventPrice != null) {
if (currency == 'KRW') {
eventPriceController.text = NumberFormat.decimalPattern()
.format(subscription.eventPrice!.toInt());
} else {
- eventPriceController.text =
+ eventPriceController.text =
NumberFormat('#,##0.00').format(subscription.eventPrice!);
}
}
-
+
// 통화 단위에 따른 금액 표시 형식 조정
_updateMonthlyCostFormat();
-
+
// 애니메이션 초기화
animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: vsync,
);
-
+
fadeAnimation = Tween(
begin: 0.0,
end: 1.0,
@@ -183,7 +186,7 @@ class DetailScreenController extends ChangeNotifier {
parent: animationController!,
curve: Curves.easeInOut,
));
-
+
slideAnimation = Tween(
begin: const Offset(0.0, 0.3),
end: Offset.zero,
@@ -191,7 +194,7 @@ class DetailScreenController extends ChangeNotifier {
parent: animationController!,
curve: Curves.easeOutCubic,
));
-
+
rotateAnimation = Tween(
begin: 0.0,
end: 1.0,
@@ -199,16 +202,16 @@ class DetailScreenController extends ChangeNotifier {
parent: animationController!,
curve: Curves.easeOutCubic,
));
-
+
// 애니메이션 시작
animationController!.forward();
-
+
// 로케일에 맞는 서비스명 로드
_loadDisplayName();
-
+
// 서비스명 변경 감지 리스너
serviceNameController.addListener(onServiceNameChanged);
-
+
// 스크롤 리스너
scrollController.addListener(() {
scrollOffset = scrollController.offset;
@@ -219,16 +222,16 @@ class DetailScreenController extends ChangeNotifier {
Future _loadDisplayName() async {
final localeProvider = context.read();
final locale = localeProvider.locale.languageCode;
-
+
final displayName = await SubscriptionUrlMatcher.getServiceDisplayName(
serviceName: subscription.serviceName,
locale: locale,
);
-
+
_displayName = displayName;
notifyListeners();
}
-
+
/// 리소스 정리
@override
void dispose() {
@@ -237,7 +240,7 @@ class DetailScreenController extends ChangeNotifier {
monthlyCostController.dispose();
websiteUrlController.dispose();
eventPriceController.dispose();
-
+
// Focus Nodes
serviceNameFocus.dispose();
monthlyCostFocus.dispose();
@@ -246,13 +249,13 @@ class DetailScreenController extends ChangeNotifier {
websiteUrlFocus.dispose();
categoryFocus.dispose();
currencyFocus.dispose();
-
+
// Animation
animationController?.dispose();
-
+
// Scroll
scrollController.dispose();
-
+
super.dispose();
}
@@ -261,10 +264,12 @@ class DetailScreenController extends ChangeNotifier {
if (_currency == 'KRW') {
// 원화는 소수점 없이 표시
final intValue = subscription.monthlyCost.toInt();
- monthlyCostController.text = NumberFormat.decimalPattern().format(intValue);
+ monthlyCostController.text =
+ NumberFormat.decimalPattern().format(intValue);
} else {
// 달러는 소수점 2자리까지 표시
- monthlyCostController.text = NumberFormat('#,##0.00').format(subscription.monthlyCost);
+ monthlyCostController.text =
+ NumberFormat('#,##0.00').format(subscription.monthlyCost);
}
}
@@ -275,17 +280,18 @@ class DetailScreenController extends ChangeNotifier {
/// 카테고리 자동 선택
void autoSelectCategory() {
- final categoryProvider = Provider.of(context, listen: false);
+ final categoryProvider =
+ Provider.of(context, listen: false);
final categories = categoryProvider.categories;
-
+
final serviceName = serviceNameController.text.toLowerCase();
-
+
// 서비스명에 기반한 카테고리 매칭 로직
CategoryModel? matchedCategory;
-
+
// 엔터테인먼트 관련 키워드
- if (serviceName.contains('netflix') ||
- serviceName.contains('youtube') ||
+ if (serviceName.contains('netflix') ||
+ serviceName.contains('youtube') ||
serviceName.contains('disney') ||
serviceName.contains('왓챠') ||
serviceName.contains('티빙') ||
@@ -298,64 +304,64 @@ class DetailScreenController extends ChangeNotifier {
);
}
// 음악 관련 키워드
- else if (serviceName.contains('spotify') ||
- serviceName.contains('apple music') ||
- serviceName.contains('멜론') ||
- serviceName.contains('지니') ||
- serviceName.contains('플로') ||
- serviceName.contains('벅스')) {
+ else if (serviceName.contains('spotify') ||
+ serviceName.contains('apple music') ||
+ serviceName.contains('멜론') ||
+ serviceName.contains('지니') ||
+ serviceName.contains('플로') ||
+ serviceName.contains('벅스')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == 'music',
orElse: () => categories.first,
);
}
// 생산성 관련 키워드
- else if (serviceName.contains('notion') ||
- serviceName.contains('microsoft') ||
- serviceName.contains('office') ||
- serviceName.contains('google') ||
- serviceName.contains('dropbox') ||
- serviceName.contains('icloud') ||
- serviceName.contains('adobe')) {
+ else if (serviceName.contains('notion') ||
+ serviceName.contains('microsoft') ||
+ serviceName.contains('office') ||
+ serviceName.contains('google') ||
+ serviceName.contains('dropbox') ||
+ serviceName.contains('icloud') ||
+ serviceName.contains('adobe')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == 'collaborationOffice',
orElse: () => categories.first,
);
}
// AI 관련 키워드
- else if (serviceName.contains('chatgpt') ||
- serviceName.contains('claude') ||
- serviceName.contains('gemini') ||
- serviceName.contains('copilot') ||
- serviceName.contains('midjourney')) {
+ else if (serviceName.contains('chatgpt') ||
+ serviceName.contains('claude') ||
+ serviceName.contains('gemini') ||
+ serviceName.contains('copilot') ||
+ serviceName.contains('midjourney')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == 'aiService',
orElse: () => categories.first,
);
}
// 교육 관련 키워드
- else if (serviceName.contains('coursera') ||
- serviceName.contains('udemy') ||
- serviceName.contains('인프런') ||
- serviceName.contains('패스트캠퍼스') ||
- serviceName.contains('클래스101')) {
+ else if (serviceName.contains('coursera') ||
+ serviceName.contains('udemy') ||
+ serviceName.contains('인프런') ||
+ serviceName.contains('패스트캠퍼스') ||
+ serviceName.contains('클래스101')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == 'programming',
orElse: () => categories.first,
);
}
// 쇼핑 관련 키워드
- else if (serviceName.contains('쿠팡') ||
- serviceName.contains('coupang') ||
- serviceName.contains('amazon') ||
- serviceName.contains('네이버') ||
- serviceName.contains('11번가')) {
+ else if (serviceName.contains('쿠팡') ||
+ serviceName.contains('coupang') ||
+ serviceName.contains('amazon') ||
+ serviceName.contains('네이버') ||
+ serviceName.contains('11번가')) {
matchedCategory = categories.firstWhere(
(cat) => cat.name == 'other',
orElse: () => categories.first,
);
}
-
+
if (matchedCategory != null) {
selectedCategoryId = matchedCategory.id;
}
@@ -371,30 +377,32 @@ class DetailScreenController extends ChangeNotifier {
);
return;
}
-
+
final provider = Provider.of(context, listen: false);
-
+
// 웹사이트 URL이 비어있는 경우 자동 매칭 다시 시도
String? websiteUrl = websiteUrlController.text;
if (websiteUrl.isEmpty) {
- websiteUrl = SubscriptionUrlMatcher.suggestUrl(serviceNameController.text);
+ websiteUrl =
+ SubscriptionUrlMatcher.suggestUrl(serviceNameController.text);
}
-
+
// 구독 정보 업데이트
-
+
// 콤마 제거하고 숫자만 추출
double monthlyCost = 0.0;
try {
- monthlyCost = double.parse(monthlyCostController.text.replaceAll(',', ''));
+ monthlyCost =
+ double.parse(monthlyCostController.text.replaceAll(',', ''));
} catch (e) {
// 파싱 오류 발생 시 기본값 사용
monthlyCost = subscription.monthlyCost;
}
-
+
debugPrint('[DetailScreenController] 구독 업데이트 시작: '
'${subscription.serviceName} → ${serviceNameController.text}, '
'금액: ${subscription.monthlyCost} → $monthlyCost ${_currency}');
-
+
subscription.serviceName = serviceNameController.text;
subscription.monthlyCost = monthlyCost;
subscription.websiteUrl = websiteUrl;
@@ -402,16 +410,16 @@ class DetailScreenController extends ChangeNotifier {
subscription.nextBillingDate = _nextBillingDate;
subscription.categoryId = _selectedCategoryId;
subscription.currency = _currency;
-
+
// 이벤트 정보 업데이트
subscription.isEventActive = _isEventActive;
subscription.eventStartDate = _eventStartDate;
subscription.eventEndDate = _eventEndDate;
-
+
// 이벤트 가격 파싱
if (_isEventActive && eventPriceController.text.isNotEmpty) {
try {
- subscription.eventPrice =
+ subscription.eventPrice =
double.parse(eventPriceController.text.replaceAll(',', ''));
} catch (e) {
subscription.eventPrice = null;
@@ -419,20 +427,20 @@ class DetailScreenController extends ChangeNotifier {
} else {
subscription.eventPrice = null;
}
-
+
debugPrint('[DetailScreenController] 업데이트 정보: '
'현재가격=${subscription.currentPrice}, '
'이벤트활성=${subscription.isEventActive}');
-
+
// 구독 업데이트
await provider.updateSubscription(subscription);
-
+
if (context.mounted) {
AppSnackBar.showSuccess(
context: context,
message: AppLocalizations.of(context).subscriptionUpdated,
);
-
+
// 변경 사항이 반영될 시간을 주기 위해 짧게 지연 후 결과 반환
await Future.delayed(const Duration(milliseconds: 100));
if (context.mounted) {
@@ -445,30 +453,33 @@ class DetailScreenController extends ChangeNotifier {
Future deleteSubscription() async {
if (context.mounted) {
// 로케일에 맞는 서비스명 가져오기
- final localeProvider = Provider.of(context, listen: false);
+ final localeProvider =
+ Provider.of(context, listen: false);
final locale = localeProvider.locale.languageCode;
final displayName = await SubscriptionUrlMatcher.getServiceDisplayName(
serviceName: subscription.serviceName,
locale: locale,
);
-
+
// 삭제 확인 다이얼로그 표시
final shouldDelete = await DeleteConfirmationDialog.show(
context: context,
serviceName: displayName,
);
-
+
if (!shouldDelete) return;
-
+
// 사용자가 확인한 경우에만 삭제 진행
if (context.mounted) {
- final provider = Provider.of(context, listen: false);
+ final provider =
+ Provider.of(context, listen: false);
await provider.deleteSubscription(subscription.id);
-
+
if (context.mounted) {
AppSnackBar.showError(
context: context,
- message: AppLocalizations.of(context).subscriptionDeleted(displayName),
+ message:
+ AppLocalizations.of(context).subscriptionDeleted(displayName),
icon: Icons.delete_forever_rounded,
);
Navigator.of(context).pop();
@@ -482,19 +493,22 @@ class DetailScreenController extends ChangeNotifier {
try {
// 1. 현재 언어 설정 가져오기
final locale = Localizations.localeOf(context).languageCode;
-
+
// 2. 해지 안내 URL 찾기
- String? cancellationUrl = await SubscriptionUrlMatcher.findCancellationUrl(
+ String? cancellationUrl =
+ await SubscriptionUrlMatcher.findCancellationUrl(
serviceName: subscription.serviceName,
websiteUrl: subscription.websiteUrl,
locale: locale == 'ko' ? 'kr' : 'en',
);
-
+
// 3. 해지 안내 URL이 없으면 구글 검색
if (cancellationUrl == null) {
- final searchQuery = '${subscription.serviceName} ${locale == 'ko' ? '해지 방법' : 'cancel subscription'}';
- cancellationUrl = 'https://www.google.com/search?q=${Uri.encodeComponent(searchQuery)}';
-
+ final searchQuery =
+ '${subscription.serviceName} ${locale == 'ko' ? '해지 방법' : 'cancel subscription'}';
+ cancellationUrl =
+ 'https://www.google.com/search?q=${Uri.encodeComponent(searchQuery)}';
+
if (context.mounted) {
AppSnackBar.showInfo(
context: context,
@@ -502,7 +516,7 @@ class DetailScreenController extends ChangeNotifier {
);
}
}
-
+
// 4. URL 열기
final Uri url = Uri.parse(cancellationUrl);
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
@@ -517,9 +531,9 @@ class DetailScreenController extends ChangeNotifier {
if (kDebugMode) {
print('DetailScreenController: 해지 페이지 열기 실패 - $e');
}
-
+
// 오류 발생시 일반 웹사이트로 폴백
- if (subscription.websiteUrl != null &&
+ if (subscription.websiteUrl != null &&
subscription.websiteUrl!.isNotEmpty) {
final Uri url = Uri.parse(subscription.websiteUrl!);
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
@@ -554,7 +568,7 @@ class DetailScreenController extends ChangeNotifier {
const Color(0xFF0EA5E9), // 하늘
const Color(0xFFEC4899), // 분홍
];
-
+
return colors[hash % colors.length];
}
@@ -569,4 +583,4 @@ class DetailScreenController extends ChangeNotifier {
end: Alignment.bottomRight,
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/controllers/sms_scan_controller.dart b/lib/controllers/sms_scan_controller.dart
index 7551cab..489819c 100644
--- a/lib/controllers/sms_scan_controller.dart
+++ b/lib/controllers/sms_scan_controller.dart
@@ -59,11 +59,13 @@ class SmsScanController extends ChangeNotifier {
try {
// SMS 스캔 실행
print('SMS 스캔 시작');
- final scannedSubscriptionModels = await _smsScanner.scanForSubscriptions();
+ final scannedSubscriptionModels =
+ await _smsScanner.scanForSubscriptions();
print('스캔된 구독: ${scannedSubscriptionModels.length}개');
if (scannedSubscriptionModels.isNotEmpty) {
- print('첫 번째 구독: ${scannedSubscriptionModels[0].serviceName}, 반복 횟수: ${scannedSubscriptionModels[0].repeatCount}');
+ print(
+ '첫 번째 구독: ${scannedSubscriptionModels[0].serviceName}, 반복 횟수: ${scannedSubscriptionModels[0].repeatCount}');
}
if (!context.mounted) return;
@@ -77,14 +79,17 @@ class SmsScanController extends ChangeNotifier {
}
// SubscriptionModel을 Subscription으로 변환
- final scannedSubscriptions = _converter.convertModelsToSubscriptions(scannedSubscriptionModels);
+ final scannedSubscriptions =
+ _converter.convertModelsToSubscriptions(scannedSubscriptionModels);
// 2회 이상 반복 결제된 구독만 필터링
- final repeatSubscriptions = _filter.filterByRepeatCount(scannedSubscriptions, 2);
+ final repeatSubscriptions =
+ _filter.filterByRepeatCount(scannedSubscriptions, 2);
print('반복 결제된 구독: ${repeatSubscriptions.length}개');
if (repeatSubscriptions.isNotEmpty) {
- print('첫 번째 반복 구독: ${repeatSubscriptions[0].serviceName}, 반복 횟수: ${repeatSubscriptions[0].repeatCount}');
+ print(
+ '첫 번째 반복 구독: ${repeatSubscriptions[0].serviceName}, 반복 횟수: ${repeatSubscriptions[0].repeatCount}');
}
if (repeatSubscriptions.isEmpty) {
@@ -96,16 +101,19 @@ class SmsScanController extends ChangeNotifier {
}
// 구독 목록 가져오기
- final provider = Provider.of(context, listen: false);
+ final provider =
+ Provider.of(context, listen: false);
final existingSubscriptions = provider.subscriptions;
print('기존 구독: ${existingSubscriptions.length}개');
// 중복 구독 필터링
- final filteredSubscriptions = _filter.filterDuplicates(repeatSubscriptions, existingSubscriptions);
+ final filteredSubscriptions =
+ _filter.filterDuplicates(repeatSubscriptions, existingSubscriptions);
print('중복 제거 후 구독: ${filteredSubscriptions.length}개');
if (filteredSubscriptions.isNotEmpty) {
- print('첫 번째 필터링된 구독: ${filteredSubscriptions[0].serviceName}, 반복 횟수: ${filteredSubscriptions[0].repeatCount}');
+ print(
+ '첫 번째 필터링된 구독: ${filteredSubscriptions[0].serviceName}, 반복 횟수: ${filteredSubscriptions[0].repeatCount}');
}
// 중복 제거 후 신규 구독이 없는 경우
@@ -123,7 +131,8 @@ class SmsScanController extends ChangeNotifier {
} catch (e) {
print('SMS 스캔 중 오류 발생: $e');
if (context.mounted) {
- _errorMessage = AppLocalizations.of(context).smsScanErrorWithMessage(e.toString());
+ _errorMessage =
+ AppLocalizations.of(context).smsScanErrorWithMessage(e.toString());
_isLoading = false;
notifyListeners();
}
@@ -134,20 +143,25 @@ class SmsScanController extends ChangeNotifier {
if (_currentIndex >= _scannedSubscriptions.length) return;
final subscription = _scannedSubscriptions[_currentIndex];
-
+
try {
- final provider = Provider.of(context, listen: false);
- final categoryProvider = Provider.of(context, listen: false);
-
- final finalCategoryId = _selectedCategoryId ?? subscription.category ?? getDefaultCategoryId(categoryProvider);
-
+ final provider =
+ Provider.of(context, listen: false);
+ final categoryProvider =
+ Provider.of(context, listen: false);
+
+ final finalCategoryId = _selectedCategoryId ??
+ subscription.category ??
+ getDefaultCategoryId(categoryProvider);
+
// websiteUrl 처리
- final websiteUrl = websiteUrlController.text.trim().isNotEmpty
- ? websiteUrlController.text.trim()
+ final websiteUrl = websiteUrlController.text.trim().isNotEmpty
+ ? websiteUrlController.text.trim()
: subscription.websiteUrl;
- print('구독 추가 시도: ${subscription.serviceName}, 카테고리: $finalCategoryId, URL: $websiteUrl');
-
+ print(
+ '구독 추가 시도: ${subscription.serviceName}, 카테고리: $finalCategoryId, URL: $websiteUrl');
+
// addSubscription 호출
await provider.addSubscription(
serviceName: subscription.serviceName,
@@ -161,9 +175,9 @@ class SmsScanController extends ChangeNotifier {
categoryId: finalCategoryId,
currency: subscription.currency,
);
-
+
print('구독 추가 성공: ${subscription.serviceName}');
-
+
moveToNextSubscription(context);
} catch (e) {
print('구독 추가 중 오류 발생: $e');
@@ -187,13 +201,14 @@ class SmsScanController extends ChangeNotifier {
if (_currentIndex >= _scannedSubscriptions.length) {
navigateToHome(context);
}
-
+
notifyListeners();
}
void navigateToHome(BuildContext context) {
// NavigationProvider를 사용하여 홈 화면으로 이동
- final navigationProvider = Provider.of(context, listen: false);
+ final navigationProvider =
+ Provider.of(context, listen: false);
navigationProvider.updateCurrentIndex(0);
}
@@ -221,4 +236,4 @@ class SmsScanController extends ChangeNotifier {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index d22b157..49ae593 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -14,22 +14,21 @@ class AppLocalizations {
// JSON 파일에서 번역 데이터 로드
Future load() async {
- String jsonString =
- await rootBundle.loadString('assets/data/text.json');
+ String jsonString = await rootBundle.loadString('assets/data/text.json');
Map jsonMap = json.decode(jsonString);
_localizedStrings = jsonMap[locale.languageCode];
}
String get appTitle => _localizedStrings['appTitle'] ?? 'SubManager';
- String get appSubtitle => _localizedStrings['appSubtitle'] ?? 'Manage subscriptions easily';
+ String get appSubtitle =>
+ _localizedStrings['appSubtitle'] ?? 'Manage subscriptions easily';
String get subscriptionManagement =>
_localizedStrings['subscriptionManagement'] ?? 'Subscription Management';
String get addSubscription =>
_localizedStrings['addSubscription'] ?? 'Add Subscription';
String get subscriptionName =>
_localizedStrings['subscriptionName'] ?? 'Service Name';
- String get monthlyCost =>
- _localizedStrings['monthlyCost'] ?? 'Monthly Cost';
+ String get monthlyCost => _localizedStrings['monthlyCost'] ?? 'Monthly Cost';
String get billingCycle =>
_localizedStrings['billingCycle'] ?? 'Billing Cycle';
String get nextBillingDate =>
@@ -55,12 +54,9 @@ class AppLocalizations {
_localizedStrings['categoryManagement'] ?? 'Category Management';
String get categoryName =>
_localizedStrings['categoryName'] ?? 'Category Name';
- String get selectColor =>
- _localizedStrings['selectColor'] ?? 'Select Color';
- String get selectIcon =>
- _localizedStrings['selectIcon'] ?? 'Select Icon';
- String get addCategory =>
- _localizedStrings['addCategory'] ?? 'Add Category';
+ String get selectColor => _localizedStrings['selectColor'] ?? 'Select Color';
+ String get selectIcon => _localizedStrings['selectIcon'] ?? 'Select Icon';
+ String get addCategory => _localizedStrings['addCategory'] ?? 'Add Category';
String get settings => _localizedStrings['settings'] ?? 'Settings';
String get darkMode => _localizedStrings['darkMode'] ?? 'Dark Mode';
String get language => _localizedStrings['language'] ?? 'Language';
@@ -71,13 +67,15 @@ class AppLocalizations {
String get notificationPermission =>
_localizedStrings['notificationPermission'] ?? 'Notification Permission';
String get notificationPermissionDesc =>
- _localizedStrings['notificationPermissionDesc'] ?? 'Permission is required to receive notifications';
+ _localizedStrings['notificationPermissionDesc'] ??
+ 'Permission is required to receive notifications';
String get requestPermission =>
_localizedStrings['requestPermission'] ?? 'Request Permission';
String get paymentNotification =>
_localizedStrings['paymentNotification'] ?? 'Payment Due Notification';
String get paymentNotificationDesc =>
- _localizedStrings['paymentNotificationDesc'] ?? 'Receive notification on payment due date';
+ _localizedStrings['paymentNotificationDesc'] ??
+ 'Receive notification on payment due date';
String get notificationTiming =>
_localizedStrings['notificationTiming'] ?? 'Notification Timing';
String get notificationTime =>
@@ -85,11 +83,14 @@ class AppLocalizations {
String get dailyReminder =>
_localizedStrings['dailyReminder'] ?? 'Daily Reminder';
String get dailyReminderEnabled =>
- _localizedStrings['dailyReminderEnabled'] ?? 'Receive daily notifications until payment date';
+ _localizedStrings['dailyReminderEnabled'] ??
+ 'Receive daily notifications until payment date';
String get dailyReminderDisabled =>
- _localizedStrings['dailyReminderDisabled'] ?? 'Receive notification @ day(s) before payment';
+ _localizedStrings['dailyReminderDisabled'] ??
+ 'Receive notification @ day(s) before payment';
String get notificationPermissionDenied =>
- _localizedStrings['notificationPermissionDenied'] ?? 'Notification permission denied';
+ _localizedStrings['notificationPermissionDenied'] ??
+ 'Notification permission denied';
// 앱 정보
String get appInfo => _localizedStrings['appInfo'] ?? 'App Info';
String get version => _localizedStrings['version'] ?? 'Version';
@@ -102,7 +103,8 @@ class AppLocalizations {
String get lightTheme => _localizedStrings['lightTheme'] ?? 'Light';
String get darkTheme => _localizedStrings['darkTheme'] ?? 'Dark';
String get oledTheme => _localizedStrings['oledTheme'] ?? 'OLED Black';
- String get systemTheme => _localizedStrings['systemTheme'] ?? 'System Default';
+ String get systemTheme =>
+ _localizedStrings['systemTheme'] ?? 'System Default';
// 기타 메시지
String get subscriptionAdded =>
_localizedStrings['subscriptionAdded'] ?? 'Subscription added';
@@ -112,127 +114,198 @@ class AppLocalizations {
String get japanese => _localizedStrings['japanese'] ?? '日本語';
String get chinese => _localizedStrings['chinese'] ?? '中文';
// 날짜
- String get oneDayBefore => _localizedStrings['oneDayBefore'] ?? '1 day before';
- String get twoDaysBefore => _localizedStrings['twoDaysBefore'] ?? '2 days before';
- String get threeDaysBefore => _localizedStrings['threeDaysBefore'] ?? '3 days before';
+ String get oneDayBefore =>
+ _localizedStrings['oneDayBefore'] ?? '1 day before';
+ String get twoDaysBefore =>
+ _localizedStrings['twoDaysBefore'] ?? '2 days before';
+ String get threeDaysBefore =>
+ _localizedStrings['threeDaysBefore'] ?? '3 days before';
// 추가 메시지
- String get requiredFieldsError => _localizedStrings['requiredFieldsError'] ?? 'Please fill in all required fields';
- String get subscriptionUpdated => _localizedStrings['subscriptionUpdated'] ?? 'Subscription information has been updated';
- String get officialCancelPageNotFound => _localizedStrings['officialCancelPageNotFound'] ?? 'Official cancellation page not found. Redirecting to Google search.';
- String get cannotOpenWebsite => _localizedStrings['cannotOpenWebsite'] ?? 'Cannot open website';
- String get noWebsiteInfo => _localizedStrings['noWebsiteInfo'] ?? 'No website information available. Please cancel through the website.';
+ String get requiredFieldsError =>
+ _localizedStrings['requiredFieldsError'] ??
+ 'Please fill in all required fields';
+ String get subscriptionUpdated =>
+ _localizedStrings['subscriptionUpdated'] ??
+ 'Subscription information has been updated';
+ String get officialCancelPageNotFound =>
+ _localizedStrings['officialCancelPageNotFound'] ??
+ 'Official cancellation page not found. Redirecting to Google search.';
+ String get cannotOpenWebsite =>
+ _localizedStrings['cannotOpenWebsite'] ?? 'Cannot open website';
+ String get noWebsiteInfo =>
+ _localizedStrings['noWebsiteInfo'] ??
+ 'No website information available. Please cancel through the website.';
String get editMode => _localizedStrings['editMode'] ?? 'Edit Mode';
- String get changesAppliedAfterSave => _localizedStrings['changesAppliedAfterSave'] ?? 'Changes will be applied after saving';
+ String get changesAppliedAfterSave =>
+ _localizedStrings['changesAppliedAfterSave'] ??
+ 'Changes will be applied after saving';
String get saveChanges => _localizedStrings['saveChanges'] ?? 'Save Changes';
- String get monthlyExpense => _localizedStrings['monthlyExpense'] ?? 'Monthly Expense';
+ String get monthlyExpense =>
+ _localizedStrings['monthlyExpense'] ?? 'Monthly Expense';
String get websiteUrl => _localizedStrings['websiteUrl'] ?? 'Website URL';
- String get websiteUrlOptional => _localizedStrings['websiteUrlOptional'] ?? 'Website URL (Optional)';
+ String get websiteUrlOptional =>
+ _localizedStrings['websiteUrlOptional'] ?? 'Website URL (Optional)';
String get eventPrice => _localizedStrings['eventPrice'] ?? 'Event Price';
- String get eventPriceHint => _localizedStrings['eventPriceHint'] ?? 'Enter discounted price';
- String get eventPriceRequired => _localizedStrings['eventPriceRequired'] ?? 'Please enter event price';
- String get invalidPrice => _localizedStrings['invalidPrice'] ?? 'Please enter a valid price';
+ String get eventPriceHint =>
+ _localizedStrings['eventPriceHint'] ?? 'Enter discounted price';
+ String get eventPriceRequired =>
+ _localizedStrings['eventPriceRequired'] ?? 'Please enter event price';
+ String get invalidPrice =>
+ _localizedStrings['invalidPrice'] ?? 'Please enter a valid price';
String get smsScanLabel => _localizedStrings['smsScanLabel'] ?? 'SMS';
String get home => _localizedStrings['home'] ?? 'Home';
String get analysis => _localizedStrings['analysis'] ?? 'Analysis';
String get back => _localizedStrings['back'] ?? 'Back';
String get exitApp => _localizedStrings['exitApp'] ?? 'Exit App';
- String get exitAppConfirm => _localizedStrings['exitAppConfirm'] ?? 'Are you sure you want to exit SubManager?';
+ String get exitAppConfirm =>
+ _localizedStrings['exitAppConfirm'] ??
+ 'Are you sure you want to exit SubManager?';
String get exit => _localizedStrings['exit'] ?? 'Exit';
- String get pageNotFound => _localizedStrings['pageNotFound'] ?? 'Page not found';
- String get serviceNameExample => _localizedStrings['serviceNameExample'] ?? 'e.g. Netflix, Spotify';
- String get urlExample => _localizedStrings['urlExample'] ?? 'https://example.com';
- String get appLockDesc => _localizedStrings['appLockDesc'] ?? 'App lock with biometric authentication';
- String get unlockWithBiometric => _localizedStrings['unlockWithBiometric'] ?? 'Unlock with biometric authentication';
- String get authenticationFailed => _localizedStrings['authenticationFailed'] ?? 'Authentication failed. Please try again.';
- String get smsPermissionRequired => _localizedStrings['smsPermissionRequired'] ?? 'SMS permission required';
- String get noSubscriptionSmsFound => _localizedStrings['noSubscriptionSmsFound'] ?? 'No subscription related SMS found';
- String get smsScanError => _localizedStrings['smsScanError'] ?? 'Error occurred during SMS scan';
- String get saveError => _localizedStrings['saveError'] ?? 'Error occurred while saving';
- String get newSubscriptionSmsNotFound => _localizedStrings['newSubscriptionSmsNotFound'] ?? 'No new subscription SMS found';
- String get subscriptionAddError => _localizedStrings['subscriptionAddError'] ?? 'Error adding subscription';
- String get allSubscriptionsProcessed => _localizedStrings['allSubscriptionsProcessed'] ?? 'All subscriptions have been processed.';
- String get websiteUrlExtracted => _localizedStrings['websiteUrlExtracted'] ?? 'Website URL (Auto-extracted)';
+ String get pageNotFound =>
+ _localizedStrings['pageNotFound'] ?? 'Page not found';
+ String get serviceNameExample =>
+ _localizedStrings['serviceNameExample'] ?? 'e.g. Netflix, Spotify';
+ String get urlExample =>
+ _localizedStrings['urlExample'] ?? 'https://example.com';
+ String get appLockDesc =>
+ _localizedStrings['appLockDesc'] ??
+ 'App lock with biometric authentication';
+ String get unlockWithBiometric =>
+ _localizedStrings['unlockWithBiometric'] ??
+ 'Unlock with biometric authentication';
+ String get authenticationFailed =>
+ _localizedStrings['authenticationFailed'] ??
+ 'Authentication failed. Please try again.';
+ String get smsPermissionRequired =>
+ _localizedStrings['smsPermissionRequired'] ?? 'SMS permission required';
+ String get noSubscriptionSmsFound =>
+ _localizedStrings['noSubscriptionSmsFound'] ??
+ 'No subscription related SMS found';
+ String get smsScanError =>
+ _localizedStrings['smsScanError'] ?? 'Error occurred during SMS scan';
+ String get saveError =>
+ _localizedStrings['saveError'] ?? 'Error occurred while saving';
+ String get newSubscriptionSmsNotFound =>
+ _localizedStrings['newSubscriptionSmsNotFound'] ??
+ 'No new subscription SMS found';
+ String get subscriptionAddError =>
+ _localizedStrings['subscriptionAddError'] ?? 'Error adding subscription';
+ String get allSubscriptionsProcessed =>
+ _localizedStrings['allSubscriptionsProcessed'] ??
+ 'All subscriptions have been processed.';
+ String get websiteUrlExtracted =>
+ _localizedStrings['websiteUrlExtracted'] ??
+ 'Website URL (Auto-extracted)';
String get startDate => _localizedStrings['startDate'] ?? 'Start Date';
String get endDate => _localizedStrings['endDate'] ?? 'End Date';
-
+
// 새로 추가된 항목들
- String get monthlyTotalSubscriptionCost => _localizedStrings['monthlyTotalSubscriptionCost'] ?? 'Total Monthly Subscription Cost';
- String get todaysExchangeRate => _localizedStrings['todaysExchangeRate'] ?? 'Today\'s Exchange Rate';
+ String get monthlyTotalSubscriptionCost =>
+ _localizedStrings['monthlyTotalSubscriptionCost'] ??
+ 'Total Monthly Subscription Cost';
+ String get todaysExchangeRate =>
+ _localizedStrings['todaysExchangeRate'] ?? 'Today\'s Exchange Rate';
String get won => _localizedStrings['won'] ?? 'KRW';
- String get estimatedAnnualCost => _localizedStrings['estimatedAnnualCost'] ?? 'Estimated Annual Cost';
- String get totalSubscriptionServices => _localizedStrings['totalSubscriptionServices'] ?? 'Total Subscription Services';
+ String get estimatedAnnualCost =>
+ _localizedStrings['estimatedAnnualCost'] ?? 'Estimated Annual Cost';
+ String get totalSubscriptionServices =>
+ _localizedStrings['totalSubscriptionServices'] ??
+ 'Total Subscription Services';
String get services => _localizedStrings['services'] ?? 'services';
- String get eventDiscountActive => _localizedStrings['eventDiscountActive'] ?? 'Event Discount Active';
+ String get eventDiscountActive =>
+ _localizedStrings['eventDiscountActive'] ?? 'Event Discount Active';
String get saving => _localizedStrings['saving'] ?? 'Saving';
- String get paymentDueToday => _localizedStrings['paymentDueToday'] ?? 'Payment Due Today';
- String get paymentInfoNeeded => _localizedStrings['paymentInfoNeeded'] ?? 'Payment Info Needed';
+ String get paymentDueToday =>
+ _localizedStrings['paymentDueToday'] ?? 'Payment Due Today';
+ String get paymentInfoNeeded =>
+ _localizedStrings['paymentInfoNeeded'] ?? 'Payment Info Needed';
String get event => _localizedStrings['event'] ?? 'Event';
-
+
// 카테고리 getter들
String get categoryMusic => _localizedStrings['categoryMusic'] ?? 'Music';
- String get categoryOttVideo => _localizedStrings['categoryOttVideo'] ?? 'OTT(Video)';
- String get categoryStorageCloud => _localizedStrings['categoryStorageCloud'] ?? 'Storage/Cloud';
- String get categoryTelecomInternetTv => _localizedStrings['categoryTelecomInternetTv'] ?? 'Telecom · Internet · TV';
- String get categoryLifestyle => _localizedStrings['categoryLifestyle'] ?? 'Lifestyle';
- String get categoryShoppingEcommerce => _localizedStrings['categoryShoppingEcommerce'] ?? 'Shopping/E-commerce';
- String get categoryProgramming => _localizedStrings['categoryProgramming'] ?? 'Programming';
- String get categoryCollaborationOffice => _localizedStrings['categoryCollaborationOffice'] ?? 'Collaboration/Office';
- String get categoryAiService => _localizedStrings['categoryAiService'] ?? 'AI Service';
+ String get categoryOttVideo =>
+ _localizedStrings['categoryOttVideo'] ?? 'OTT(Video)';
+ String get categoryStorageCloud =>
+ _localizedStrings['categoryStorageCloud'] ?? 'Storage/Cloud';
+ String get categoryTelecomInternetTv =>
+ _localizedStrings['categoryTelecomInternetTv'] ??
+ 'Telecom · Internet · TV';
+ String get categoryLifestyle =>
+ _localizedStrings['categoryLifestyle'] ?? 'Lifestyle';
+ String get categoryShoppingEcommerce =>
+ _localizedStrings['categoryShoppingEcommerce'] ?? 'Shopping/E-commerce';
+ String get categoryProgramming =>
+ _localizedStrings['categoryProgramming'] ?? 'Programming';
+ String get categoryCollaborationOffice =>
+ _localizedStrings['categoryCollaborationOffice'] ??
+ 'Collaboration/Office';
+ String get categoryAiService =>
+ _localizedStrings['categoryAiService'] ?? 'AI Service';
String get categoryOther => _localizedStrings['categoryOther'] ?? 'Other';
-
+
// 동적 메시지 생성 메서드
String daysBefore(int days) {
return '$days${_localizedStrings['daysBefore'] ?? 'day(s) before'}';
}
-
+
String dailyReminderDisabledWithDays(int days) {
- final template = _localizedStrings['dailyReminderDisabled'] ?? 'Receive notification @ day(s) before payment';
+ final template = _localizedStrings['dailyReminderDisabled'] ??
+ 'Receive notification @ day(s) before payment';
return template.replaceAll('@', days.toString());
}
-
+
String subscriptionAddedWithName(String serviceName) {
- final template = _localizedStrings['subscriptionAddedTemplate'] ?? '@ 구독이 추가되었습니다.';
+ final template =
+ _localizedStrings['subscriptionAddedTemplate'] ?? '@ 구독이 추가되었습니다.';
return template.replaceAll('@', serviceName);
}
-
+
String subscriptionDeleted(String serviceName) {
- final template = _localizedStrings['subscriptionDeleted'] ?? '@ subscription has been deleted';
+ final template = _localizedStrings['subscriptionDeleted'] ??
+ '@ subscription has been deleted';
return template.replaceAll('@', serviceName);
}
-
+
String totalExpenseCopied(String amount) {
- final template = _localizedStrings['totalExpenseCopied'] ?? 'Total expense copied: @';
+ final template =
+ _localizedStrings['totalExpenseCopied'] ?? 'Total expense copied: @';
return template.replaceAll('@', amount);
}
-
+
String serviceRecognized(String serviceName) {
- final template = _localizedStrings['serviceRecognized'] ?? '@ service has been recognized automatically.';
+ final template = _localizedStrings['serviceRecognized'] ??
+ '@ service has been recognized automatically.';
return template.replaceAll('@', serviceName);
}
-
+
String smsScanErrorWithMessage(String error) {
- final template = _localizedStrings['smsScanError'] ?? 'Error occurred during SMS scan: @';
+ final template = _localizedStrings['smsScanError'] ??
+ 'Error occurred during SMS scan: @';
return template.replaceAll('@', error);
}
-
+
String saveErrorWithMessage(String error) {
- final template = _localizedStrings['saveError'] ?? 'Error occurred while saving: @';
+ final template =
+ _localizedStrings['saveError'] ?? 'Error occurred while saving: @';
return template.replaceAll('@', error);
}
-
+
String subscriptionAddErrorWithMessage(String error) {
- final template = _localizedStrings['subscriptionAddError'] ?? 'Error adding subscription: @';
+ final template = _localizedStrings['subscriptionAddError'] ??
+ 'Error adding subscription: @';
return template.replaceAll('@', error);
}
-
+
String subscriptionSkipped(String serviceName) {
- final template = _localizedStrings['subscriptionSkipped'] ?? '@ subscription skipped.';
+ final template =
+ _localizedStrings['subscriptionSkipped'] ?? '@ subscription skipped.';
return template.replaceAll('@', serviceName);
}
-
+
// 홈화면 관련
- String get mySubscriptions => _localizedStrings['mySubscriptions'] ?? 'My Subscriptions';
-
+ String get mySubscriptions =>
+ _localizedStrings['mySubscriptions'] ?? 'My Subscriptions';
+
String subscriptionCount(int count) {
if (locale.languageCode == 'ko') {
return '${count}개';
@@ -244,58 +317,99 @@ class AppLocalizations {
return count.toString();
}
}
-
+
// 분석화면 관련
- String get monthlyExpenseTitle => _localizedStrings['monthlyExpenseTitle'] ?? 'Monthly Expense Status';
- String get recentSixMonthsTrend => _localizedStrings['recentSixMonthsTrend'] ?? 'Recent 6 months trend';
- String get monthlySubscriptionExpense => _localizedStrings['monthlySubscriptionExpense'] ?? 'Monthly subscription expense';
- String get subscriptionServiceRatio => _localizedStrings['subscriptionServiceRatio'] ?? 'Subscription Service Ratio';
- String get monthlyExpenseBasis => _localizedStrings['monthlyExpenseBasis'] ?? 'Based on monthly expense';
- String get noSubscriptionServices => _localizedStrings['noSubscriptionServices'] ?? 'No subscription services';
- String get totalExpenseSummary => _localizedStrings['totalExpenseSummary'] ?? 'Total Expense Summary';
- String get monthlyTotalAmount => _localizedStrings['monthlyTotalAmount'] ?? 'Monthly Total Amount';
- String get totalExpense => _localizedStrings['totalExpense'] ?? 'Total Expense';
- String get totalServices => _localizedStrings['totalServices'] ?? 'Total Services';
+ String get monthlyExpenseTitle =>
+ _localizedStrings['monthlyExpenseTitle'] ?? 'Monthly Expense Status';
+ String get recentSixMonthsTrend =>
+ _localizedStrings['recentSixMonthsTrend'] ?? 'Recent 6 months trend';
+ String get monthlySubscriptionExpense =>
+ _localizedStrings['monthlySubscriptionExpense'] ??
+ 'Monthly subscription expense';
+ String get subscriptionServiceRatio =>
+ _localizedStrings['subscriptionServiceRatio'] ??
+ 'Subscription Service Ratio';
+ String get monthlyExpenseBasis =>
+ _localizedStrings['monthlyExpenseBasis'] ?? 'Based on monthly expense';
+ String get noSubscriptionServices =>
+ _localizedStrings['noSubscriptionServices'] ?? 'No subscription services';
+ String get totalExpenseSummary =>
+ _localizedStrings['totalExpenseSummary'] ?? 'Total Expense Summary';
+ String get monthlyTotalAmount =>
+ _localizedStrings['monthlyTotalAmount'] ?? 'Monthly Total Amount';
+ String get totalExpense =>
+ _localizedStrings['totalExpense'] ?? 'Total Expense';
+ String get totalServices =>
+ _localizedStrings['totalServices'] ?? 'Total Services';
String get servicesUnit => _localizedStrings['servicesUnit'] ?? 'services';
String get averageCost => _localizedStrings['averageCost'] ?? 'Average Cost';
- String get eventDiscountStatus => _localizedStrings['eventDiscountStatus'] ?? 'Event Discount Status';
- String get inProgressUnit => _localizedStrings['inProgressUnit'] ?? 'in progress';
- String get monthlySavingAmount => _localizedStrings['monthlySavingAmount'] ?? 'Monthly Saving Amount';
- String get eventsInProgress => _localizedStrings['eventsInProgress'] ?? 'Events in Progress';
- String get discountPercent => _localizedStrings['discountPercent'] ?? '% discount';
+ String get eventDiscountStatus =>
+ _localizedStrings['eventDiscountStatus'] ?? 'Event Discount Status';
+ String get inProgressUnit =>
+ _localizedStrings['inProgressUnit'] ?? 'in progress';
+ String get monthlySavingAmount =>
+ _localizedStrings['monthlySavingAmount'] ?? 'Monthly Saving Amount';
+ String get eventsInProgress =>
+ _localizedStrings['eventsInProgress'] ?? 'Events in Progress';
+ String get discountPercent =>
+ _localizedStrings['discountPercent'] ?? '% discount';
String get currencyWon => _localizedStrings['currencyWon'] ?? 'KRW';
-
+
// SMS 스캔 관련
- String get scanningMessages => _localizedStrings['scanningMessages'] ?? 'Scanning SMS messages...';
- String get findingSubscriptions => _localizedStrings['findingSubscriptions'] ?? 'Finding subscription services';
- String get subscriptionNotFound => _localizedStrings['subscriptionNotFound'] ?? 'Subscription information not found.';
- String get repeatSubscriptionNotFound => _localizedStrings['repeatSubscriptionNotFound'] ?? 'No repeated subscription information found.';
- String get newSubscriptionNotFound => _localizedStrings['newSubscriptionNotFound'] ?? 'No new subscription SMS found';
- String get findRepeatSubscriptions => _localizedStrings['findRepeatSubscriptions'] ?? 'Find subscriptions paid 2+ times';
- String get scanTextMessages => _localizedStrings['scanTextMessages'] ?? 'Scan text messages to automatically find repeatedly paid subscriptions. Service names and amounts can be extracted for easy subscription addition.';
- String get startScanning => _localizedStrings['startScanning'] ?? 'Start Scanning';
- String get foundSubscription => _localizedStrings['foundSubscription'] ?? 'Found subscription';
+ String get scanningMessages =>
+ _localizedStrings['scanningMessages'] ?? 'Scanning SMS messages...';
+ String get findingSubscriptions =>
+ _localizedStrings['findingSubscriptions'] ??
+ 'Finding subscription services';
+ String get subscriptionNotFound =>
+ _localizedStrings['subscriptionNotFound'] ??
+ 'Subscription information not found.';
+ String get repeatSubscriptionNotFound =>
+ _localizedStrings['repeatSubscriptionNotFound'] ??
+ 'No repeated subscription information found.';
+ String get newSubscriptionNotFound =>
+ _localizedStrings['newSubscriptionNotFound'] ??
+ 'No new subscription SMS found';
+ String get findRepeatSubscriptions =>
+ _localizedStrings['findRepeatSubscriptions'] ??
+ 'Find subscriptions paid 2+ times';
+ String get scanTextMessages =>
+ _localizedStrings['scanTextMessages'] ??
+ 'Scan text messages to automatically find repeatedly paid subscriptions. Service names and amounts can be extracted for easy subscription addition.';
+ String get startScanning =>
+ _localizedStrings['startScanning'] ?? 'Start Scanning';
+ String get foundSubscription =>
+ _localizedStrings['foundSubscription'] ?? 'Found subscription';
String get serviceName => _localizedStrings['serviceName'] ?? 'Service Name';
- String get nextBillingDateLabel => _localizedStrings['nextBillingDateLabel'] ?? 'Next Billing Date';
+ String get nextBillingDateLabel =>
+ _localizedStrings['nextBillingDateLabel'] ?? 'Next Billing Date';
String get category => _localizedStrings['category'] ?? 'Category';
- String get websiteUrlAuto => _localizedStrings['websiteUrlAuto'] ?? 'Website URL (Auto-extracted)';
- String get websiteUrlHint => _localizedStrings['websiteUrlHint'] ?? 'Edit website URL or leave empty';
+ String get websiteUrlAuto =>
+ _localizedStrings['websiteUrlAuto'] ?? 'Website URL (Auto-extracted)';
+ String get websiteUrlHint =>
+ _localizedStrings['websiteUrlHint'] ?? 'Edit website URL or leave empty';
String get skip => _localizedStrings['skip'] ?? 'Skip';
String get add => _localizedStrings['add'] ?? 'Add';
- String get nextBillingDateRequired => _localizedStrings['nextBillingDateRequired'] ?? 'Next billing date verification required';
-
+ String get nextBillingDateRequired =>
+ _localizedStrings['nextBillingDateRequired'] ??
+ 'Next billing date verification required';
+
String nextBillingDateEstimated(String date, int days) {
- final template = _localizedStrings['nextBillingDateEstimated'] ?? 'Next estimated billing date: @ (# days later)';
+ final template = _localizedStrings['nextBillingDateEstimated'] ??
+ 'Next estimated billing date: @ (# days later)';
return template.replaceAll('@', date).replaceAll('#', days.toString());
}
-
+
String nextBillingDateInfo(String date, int days) {
- final template = _localizedStrings['nextBillingDateInfo'] ?? 'Next billing date: @ (# days later)';
+ final template = _localizedStrings['nextBillingDateInfo'] ??
+ 'Next billing date: @ (# days later)';
return template.replaceAll('@', date).replaceAll('#', days.toString());
}
-
- String get nextBillingDatePastRequired => _localizedStrings['nextBillingDatePastRequired'] ?? 'Next billing date verification required (past date)';
-
+
+ String get nextBillingDatePastRequired =>
+ _localizedStrings['nextBillingDatePastRequired'] ??
+ 'Next billing date verification required (past date)';
+
String formatDate(DateTime date) {
if (locale.languageCode == 'ko') {
return '${date.year}년 ${date.month}월 ${date.day}일';
@@ -304,16 +418,30 @@ class AppLocalizations {
} else if (locale.languageCode == 'zh') {
return '${date.year}年${date.month}月${date.day}日';
} else {
- final months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ final months = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec'
+ ];
return '${months[date.month - 1]} ${date.day}, ${date.year}';
}
}
-
+
String repeatCountDetected(int count) {
- final template = _localizedStrings['repeatCountDetected'] ?? '@ payment(s) detected';
+ final template =
+ _localizedStrings['repeatCountDetected'] ?? '@ payment(s) detected';
return template.replaceAll('@', count.toString());
}
-
+
String servicesInProgress(int count) {
if (locale.languageCode == 'ko') {
return '${count}개 진행중';
@@ -325,92 +453,130 @@ class AppLocalizations {
return '$count in progress';
}
}
-
+
// 새로 추가된 동적 메서드들
String paymentDueInDays(int days) {
- final template = _localizedStrings['paymentDueInDays'] ?? 'Payment due in @ days';
+ final template =
+ _localizedStrings['paymentDueInDays'] ?? 'Payment due in @ days';
return template.replaceAll('@', days.toString());
}
-
+
String daysRemaining(int days) {
final template = _localizedStrings['daysRemaining'] ?? '@ days remaining';
return template.replaceAll('@', days.toString());
}
-
+
String exchangeRateFormat(String rate) {
- final template = _localizedStrings['exchangeRateFormat'] ?? 'Today\'s rate: @';
+ final template =
+ _localizedStrings['exchangeRateFormat'] ?? 'Today\'s rate: @';
return template.replaceAll('@', rate);
}
-
+
// 결제 주기 결제 메시지
- String get billingCyclePayment => _localizedStrings['billingCyclePayment'] ?? '@ Payment';
-
+ String get billingCyclePayment =>
+ _localizedStrings['billingCyclePayment'] ?? '@ Payment';
+
// 할인 금액 표시 getter들
- String get discountAmountWon => _localizedStrings['discountAmountWon'] ?? 'Save ₩@';
- String get discountAmountDollar => _localizedStrings['discountAmountDollar'] ?? 'Save \$@';
- String get discountAmountYen => _localizedStrings['discountAmountYen'] ?? 'Save ¥@';
- String get discountAmountYuan => _localizedStrings['discountAmountYuan'] ?? 'Save ¥@';
-
+ String get discountAmountWon =>
+ _localizedStrings['discountAmountWon'] ?? 'Save ₩@';
+ String get discountAmountDollar =>
+ _localizedStrings['discountAmountDollar'] ?? 'Save \$@';
+ String get discountAmountYen =>
+ _localizedStrings['discountAmountYen'] ?? 'Save ¥@';
+ String get discountAmountYuan =>
+ _localizedStrings['discountAmountYuan'] ?? 'Save ¥@';
+
// 결제 주기 관련 getter
String get monthly => _localizedStrings['monthly'] ?? 'Monthly';
String get weekly => _localizedStrings['weekly'] ?? 'Weekly';
String get yearly => _localizedStrings['yearly'] ?? 'Yearly';
- String get billingCycleMonthly => _localizedStrings['billingCycleMonthly'] ?? 'Monthly';
- String get billingCycleQuarterly => _localizedStrings['billingCycleQuarterly'] ?? 'Quarterly';
- String get billingCycleHalfYearly => _localizedStrings['billingCycleHalfYearly'] ?? 'Half-Yearly';
- String get billingCycleYearly => _localizedStrings['billingCycleYearly'] ?? 'Yearly';
-
+ String get billingCycleMonthly =>
+ _localizedStrings['billingCycleMonthly'] ?? 'Monthly';
+ String get billingCycleQuarterly =>
+ _localizedStrings['billingCycleQuarterly'] ?? 'Quarterly';
+ String get billingCycleHalfYearly =>
+ _localizedStrings['billingCycleHalfYearly'] ?? 'Half-Yearly';
+ String get billingCycleYearly =>
+ _localizedStrings['billingCycleYearly'] ?? 'Yearly';
+
// 색상 관련 getter
String get colorBlue => _localizedStrings['colorBlue'] ?? 'Blue';
String get colorGreen => _localizedStrings['colorGreen'] ?? 'Green';
String get colorOrange => _localizedStrings['colorOrange'] ?? 'Orange';
String get colorRed => _localizedStrings['colorRed'] ?? 'Red';
String get colorPurple => _localizedStrings['colorPurple'] ?? 'Purple';
-
+
// 날짜 형식 관련 getter
- String get dateFormatFull => _localizedStrings['dateFormatFull'] ?? 'MMM dd, yyyy';
+ String get dateFormatFull =>
+ _localizedStrings['dateFormatFull'] ?? 'MMM dd, yyyy';
String get dateFormatShort => _localizedStrings['dateFormatShort'] ?? 'MM/dd';
-
+
// USD 환율 표시 형식
- String get exchangeRateDisplay => _localizedStrings['exchangeRateDisplay'] ?? '\$1 = @';
-
+ String get exchangeRateDisplay =>
+ _localizedStrings['exchangeRateDisplay'] ?? '\$1 = @';
+
// 라벨 및 힌트 텍스트
- String get labelServiceName => _localizedStrings['labelServiceName'] ?? 'Service Name';
- String get hintServiceName => _localizedStrings['hintServiceName'] ?? 'e.g. Netflix, Spotify';
- String get labelMonthlyExpense => _localizedStrings['labelMonthlyExpense'] ?? 'Monthly Expense';
- String get labelNextBillingDate => _localizedStrings['labelNextBillingDate'] ?? 'Next Billing Date';
- String get labelWebsiteUrl => _localizedStrings['labelWebsiteUrl'] ?? 'Website URL (Optional)';
- String get hintWebsiteUrl => _localizedStrings['hintWebsiteUrl'] ?? 'https://example.com';
- String get labelEventPrice => _localizedStrings['labelEventPrice'] ?? 'Event Price';
- String get hintEventPrice => _localizedStrings['hintEventPrice'] ?? 'Enter discounted price';
+ String get labelServiceName =>
+ _localizedStrings['labelServiceName'] ?? 'Service Name';
+ String get hintServiceName =>
+ _localizedStrings['hintServiceName'] ?? 'e.g. Netflix, Spotify';
+ String get labelMonthlyExpense =>
+ _localizedStrings['labelMonthlyExpense'] ?? 'Monthly Expense';
+ String get labelNextBillingDate =>
+ _localizedStrings['labelNextBillingDate'] ?? 'Next Billing Date';
+ String get labelWebsiteUrl =>
+ _localizedStrings['labelWebsiteUrl'] ?? 'Website URL (Optional)';
+ String get hintWebsiteUrl =>
+ _localizedStrings['hintWebsiteUrl'] ?? 'https://example.com';
+ String get labelEventPrice =>
+ _localizedStrings['labelEventPrice'] ?? 'Event Price';
+ String get hintEventPrice =>
+ _localizedStrings['hintEventPrice'] ?? 'Enter discounted price';
String get labelCategory => _localizedStrings['labelCategory'] ?? 'Category';
-
+
// 기타 번역
- String get subscription => _localizedStrings['subscription'] ?? 'Subscription';
+ String get subscription =>
+ _localizedStrings['subscription'] ?? 'Subscription';
String get movie => _localizedStrings['movie'] ?? 'Movie';
- String get music => _localizedStrings['music'] ?? 'Music';
+ String get music => _localizedStrings['music'] ?? 'Music';
String get exercise => _localizedStrings['exercise'] ?? 'Exercise';
String get shopping => _localizedStrings['shopping'] ?? 'Shopping';
String get currency => _localizedStrings['currency'] ?? 'Currency';
- String get websiteInfo => _localizedStrings['websiteInfo'] ?? 'Website Information';
- String get cancelGuide => _localizedStrings['cancelGuide'] ?? 'Cancellation Guide';
- String get cancelServiceGuide => _localizedStrings['cancelServiceGuide'] ?? 'To cancel this service, please go to the cancellation page through the link below.';
- String get goToCancelPage => _localizedStrings['goToCancelPage'] ?? 'Go to Cancellation Page';
- String get urlAutoMatchInfo => _localizedStrings['urlAutoMatchInfo'] ?? 'If URL is empty, it will be automatically matched based on the service name';
+ String get websiteInfo =>
+ _localizedStrings['websiteInfo'] ?? 'Website Information';
+ String get cancelGuide =>
+ _localizedStrings['cancelGuide'] ?? 'Cancellation Guide';
+ String get cancelServiceGuide =>
+ _localizedStrings['cancelServiceGuide'] ??
+ 'To cancel this service, please go to the cancellation page through the link below.';
+ String get goToCancelPage =>
+ _localizedStrings['goToCancelPage'] ?? 'Go to Cancellation Page';
+ String get urlAutoMatchInfo =>
+ _localizedStrings['urlAutoMatchInfo'] ??
+ 'If URL is empty, it will be automatically matched based on the service name';
String get dateSelect => _localizedStrings['dateSelect'] ?? 'Select';
-
+
// 새로 추가된 getter들
- String get serviceInfo => _localizedStrings['serviceInfo'] ?? 'Service Information';
- String get newSubscriptionAdd => _localizedStrings['newSubscriptionAdd'] ?? 'Add New Subscription';
- String get enterServiceInfo => _localizedStrings['enterServiceInfo'] ?? 'Enter service information';
- String get addSubscriptionButton => _localizedStrings['addSubscriptionButton'] ?? 'Add Subscription';
- String get serviceNameRequired => _localizedStrings['serviceNameRequired'] ?? 'Please enter service name';
- String get amountRequired => _localizedStrings['amountRequired'] ?? 'Please enter amount';
- String get subscriptionDetail => _localizedStrings['subscriptionDetail'] ?? 'Subscription Detail';
+ String get serviceInfo =>
+ _localizedStrings['serviceInfo'] ?? 'Service Information';
+ String get newSubscriptionAdd =>
+ _localizedStrings['newSubscriptionAdd'] ?? 'Add New Subscription';
+ String get enterServiceInfo =>
+ _localizedStrings['enterServiceInfo'] ?? 'Enter service information';
+ String get addSubscriptionButton =>
+ _localizedStrings['addSubscriptionButton'] ?? 'Add Subscription';
+ String get serviceNameRequired =>
+ _localizedStrings['serviceNameRequired'] ?? 'Please enter service name';
+ String get amountRequired =>
+ _localizedStrings['amountRequired'] ?? 'Please enter amount';
+ String get subscriptionDetail =>
+ _localizedStrings['subscriptionDetail'] ?? 'Subscription Detail';
String get enterAmount => _localizedStrings['enterAmount'] ?? 'Enter amount';
- String get invalidAmount => _localizedStrings['invalidAmount'] ?? 'Please enter a valid amount';
- String get featureComingSoon => _localizedStrings['featureComingSoon'] ?? 'This feature is coming soon';
-
+ String get invalidAmount =>
+ _localizedStrings['invalidAmount'] ?? 'Please enter a valid amount';
+ String get featureComingSoon =>
+ _localizedStrings['featureComingSoon'] ?? 'This feature is coming soon';
+
// 결제 주기를 키값으로 변환하여 번역된 이름 반환
String getBillingCycleName(String billingCycleKey) {
switch (billingCycleKey) {
@@ -433,7 +599,7 @@ class AppLocalizations {
return billingCycleKey; // 매칭되지 않으면 원본 반환
}
}
-
+
// 카테고리 이름을 키로 변환하여 번역된 이름 반환
String getCategoryName(String categoryKey) {
switch (categoryKey) {
@@ -467,7 +633,8 @@ class AppLocalizationsDelegate extends LocalizationsDelegate {
const AppLocalizationsDelegate();
@override
- bool isSupported(Locale locale) => ['en', 'ko', 'ja', 'zh'].contains(locale.languageCode);
+ bool isSupported(Locale locale) =>
+ ['en', 'ko', 'ja', 'zh'].contains(locale.languageCode);
@override
Future load(Locale locale) async {
diff --git a/lib/main.dart b/lib/main.dart
index 590296a..9303428 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -26,7 +26,7 @@ import 'utils/performance_optimizer.dart';
import 'navigator_key.dart';
// AdMob 활성화 플래그 (개발 중 false, 프로덕션 시 true로 변경)
-const bool enableAdMob = false;
+const bool enableAdMob = true;
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
diff --git a/lib/models/subscription_model.dart b/lib/models/subscription_model.dart
index 594c7ce..c5ec3d1 100644
--- a/lib/models/subscription_model.dart
+++ b/lib/models/subscription_model.dart
@@ -75,7 +75,7 @@ class SubscriptionModel extends HiveObject {
if (!isEventActive || eventStartDate == null || eventEndDate == null) {
return false;
}
-
+
final now = DateTime.now();
return now.isAfter(eventStartDate!) && now.isBefore(eventEndDate!);
}
@@ -98,7 +98,7 @@ class SubscriptionModel extends HiveObject {
// 원래 가격 (이벤트와 관계없이 항상 정상 가격)
double get originalPrice => monthlyCost;
-
+
// 결제 주기를 영어 키값으로 정규화
static String normalizeBillingCycle(String cycle) {
switch (cycle.toLowerCase()) {
@@ -121,7 +121,7 @@ class SubscriptionModel extends HiveObject {
return 'monthly'; // 기본값은 monthly
}
}
-
+
// 결제 주기를 영어 키값으로 반환 (내부 사용)
String get billingCycleKey => normalizeBillingCycle(billingCycle);
}
diff --git a/lib/navigation/app_navigation_observer.dart b/lib/navigation/app_navigation_observer.dart
index 3727f99..82db49d 100644
--- a/lib/navigation/app_navigation_observer.dart
+++ b/lib/navigation/app_navigation_observer.dart
@@ -37,22 +37,24 @@ class AppNavigationObserver extends NavigatorObserver {
if (newRoute != null) {
_updateNavigationState(newRoute);
}
- debugPrint('Navigation: Replace ${oldRoute?.settings.name} with ${newRoute?.settings.name}');
+ debugPrint(
+ 'Navigation: Replace ${oldRoute?.settings.name} with ${newRoute?.settings.name}');
}
void _updateNavigationState(Route route) {
if (navigator?.context == null) return;
-
+
final routeName = route.settings.name;
if (routeName == null) return;
-
+
// build 완료 후 업데이트하도록 변경
WidgetsBinding.instance.addPostFrameCallback((_) {
if (navigator?.context == null) return;
-
+
try {
final context = navigator!.context;
- final navigationProvider = Provider.of(context, listen: false);
+ final navigationProvider =
+ Provider.of(context, listen: false);
navigationProvider.updateByRoute(routeName);
} catch (e) {
debugPrint('Failed to update navigation state: $e');
@@ -62,18 +64,19 @@ class AppNavigationObserver extends NavigatorObserver {
void _handlePopWithProvider() {
if (navigator?.context == null) return;
-
+
// build 완료 후 업데이트하도록 변경
WidgetsBinding.instance.addPostFrameCallback((_) {
if (navigator?.context == null) return;
-
+
try {
final context = navigator!.context;
- final navigationProvider = Provider.of(context, listen: false);
+ final navigationProvider =
+ Provider.of(context, listen: false);
navigationProvider.pop();
} catch (e) {
debugPrint('Failed to handle pop with provider: $e');
}
});
}
-}
\ No newline at end of file
+}
diff --git a/lib/providers/category_provider.dart b/lib/providers/category_provider.dart
index 585eadf..57ceed6 100644
--- a/lib/providers/category_provider.dart
+++ b/lib/providers/category_provider.dart
@@ -28,14 +28,14 @@ class CategoryProvider extends ChangeNotifier {
sortedCategories.sort((a, b) {
final aIndex = _categoryOrder.indexOf(a.name);
final bIndex = _categoryOrder.indexOf(b.name);
-
+
// 순서 목록에 없는 카테고리는 맨 뒤로
if (aIndex == -1) return 1;
if (bIndex == -1) return -1;
-
+
return aIndex.compareTo(bIndex);
});
-
+
return sortedCategories;
}
@@ -59,9 +59,17 @@ class CategoryProvider extends ChangeNotifier {
{'name': 'storageCloud', 'color': '#2196F3', 'icon': 'cloud'},
{'name': 'telecomInternetTv', 'color': '#00BCD4', 'icon': 'wifi'},
{'name': 'lifestyle', 'color': '#4CAF50', 'icon': 'home'},
- {'name': 'shoppingEcommerce', 'color': '#FF9800', 'icon': 'shopping_cart'},
+ {
+ 'name': 'shoppingEcommerce',
+ 'color': '#FF9800',
+ 'icon': 'shopping_cart'
+ },
{'name': 'programming', 'color': '#795548', 'icon': 'code'},
- {'name': 'collaborationOffice', 'color': '#607D8B', 'icon': 'business_center'},
+ {
+ 'name': 'collaborationOffice',
+ 'color': '#607D8B',
+ 'icon': 'business_center'
+ },
{'name': 'aiService', 'color': '#673AB7', 'icon': 'smart_toy'},
{'name': 'other', 'color': '#9E9E9E', 'icon': 'category'},
];
@@ -117,7 +125,7 @@ class CategoryProvider extends ChangeNotifier {
return null;
}
}
-
+
// 카테고리 이름을 현재 언어에 맞게 반환
String getLocalizedCategoryName(BuildContext context, String categoryKey) {
final localizations = AppLocalizations.of(context);
diff --git a/lib/providers/locale_provider.dart b/lib/providers/locale_provider.dart
index 36dcb49..48c844f 100644
--- a/lib/providers/locale_provider.dart
+++ b/lib/providers/locale_provider.dart
@@ -5,24 +5,24 @@ import 'dart:ui' as ui;
class LocaleProvider extends ChangeNotifier {
late Box _localeBox;
Locale _locale = const Locale('ko');
-
+
static const List supportedLanguages = ['en', 'ko', 'ja', 'zh'];
Locale get locale => _locale;
Future init() async {
_localeBox = await Hive.openBox('locale');
-
+
// 저장된 언어 설정 확인
final savedLocale = _localeBox.get('locale');
-
+
if (savedLocale != null) {
// 저장된 언어가 있으면 사용
_locale = Locale(savedLocale);
} else {
// 저장된 언어가 없으면 시스템 언어 감지
final systemLocale = ui.PlatformDispatcher.instance.locale;
-
+
// 시스템 언어가 지원되는 언어인지 확인
if (supportedLanguages.contains(systemLocale.languageCode)) {
_locale = Locale(systemLocale.languageCode);
@@ -30,11 +30,11 @@ class LocaleProvider extends ChangeNotifier {
// 지원되지 않는 언어면 영어 사용
_locale = const Locale('en');
}
-
+
// 감지된 언어 저장
await _localeBox.put('locale', _locale.languageCode);
}
-
+
notifyListeners();
}
diff --git a/lib/providers/navigation_provider.dart b/lib/providers/navigation_provider.dart
index 584b9fe..011d619 100644
--- a/lib/providers/navigation_provider.dart
+++ b/lib/providers/navigation_provider.dart
@@ -36,25 +36,25 @@ class NavigationProvider extends ChangeNotifier {
void updateCurrentIndex(int index, {bool addToHistory = true}) {
if (_currentIndex == index) return;
-
+
_currentIndex = index;
_currentRoute = indexToRoute[index] ?? '/';
_currentTitle = indexToTitle[index] ?? 'home';
-
+
if (addToHistory && index >= 0) {
_navigationHistory.add(index);
if (_navigationHistory.length > 10) {
_navigationHistory.removeAt(0);
}
}
-
+
notifyListeners();
}
void updateByRoute(String route) {
final index = routeToIndex[route] ?? 0;
_currentRoute = route;
-
+
if (index >= 0) {
_currentIndex = index;
_currentTitle = indexToTitle[index] ?? 'home';
@@ -70,7 +70,7 @@ class NavigationProvider extends ChangeNotifier {
_currentTitle = 'home';
}
}
-
+
notifyListeners();
}
@@ -103,4 +103,4 @@ class NavigationProvider extends ChangeNotifier {
_navigationHistory.add(0);
notifyListeners();
}
-}
\ No newline at end of file
+}
diff --git a/lib/providers/notification_provider.dart b/lib/providers/notification_provider.dart
index d790852..de6a0f5 100644
--- a/lib/providers/notification_provider.dart
+++ b/lib/providers/notification_provider.dart
@@ -86,12 +86,12 @@ class NotificationProvider extends ChangeNotifier {
try {
_isEnabled = value;
await NotificationService.setNotificationEnabled(value);
-
+
// 첫 권한 부여 시 기본 설정 적용
if (value) {
await initializeDefaultSettingsOnFirstPermission();
}
-
+
notifyListeners();
} catch (e) {
debugPrint('알림 활성화 설정 중 오류 발생: $e');
@@ -270,15 +270,17 @@ class NotificationProvider extends ChangeNotifier {
// 첫 권한 부여 시 기본 설정 초기화
Future initializeDefaultSettingsOnFirstPermission() async {
try {
- final firstGranted = await _secureStorage.read(key: _firstPermissionGrantedKey);
+ final firstGranted =
+ await _secureStorage.read(key: _firstPermissionGrantedKey);
if (firstGranted != 'true') {
// 첫 권한 부여 시 기본값 설정
await setReminderDays(2); // 2일 전 알림
await setDailyReminderEnabled(true); // 반복 알림 활성화
await setPaymentEnabled(true); // 결제 예정 알림 활성화
-
+
// 첫 권한 부여 플래그 저장
- await _secureStorage.write(key: _firstPermissionGrantedKey, value: 'true');
+ await _secureStorage.write(
+ key: _firstPermissionGrantedKey, value: 'true');
}
} catch (e) {
debugPrint('기본 설정 초기화 중 오류 발생: $e');
diff --git a/lib/providers/subscription_provider.dart b/lib/providers/subscription_provider.dart
index 339d32f..897e410 100644
--- a/lib/providers/subscription_provider.dart
+++ b/lib/providers/subscription_provider.dart
@@ -19,9 +19,9 @@ class SubscriptionProvider extends ChangeNotifier {
double get totalMonthlyExpense {
final exchangeRateService = ExchangeRateService();
- final rate = exchangeRateService.cachedUsdToKrwRate ??
- ExchangeRateService.DEFAULT_USD_TO_KRW_RATE;
-
+ final rate = exchangeRateService.cachedUsdToKrwRate ??
+ ExchangeRateService.DEFAULT_USD_TO_KRW_RATE;
+
final total = _subscriptions.fold(
0.0,
(sum, subscription) {
@@ -31,11 +31,12 @@ class SubscriptionProvider extends ChangeNotifier {
'\$${price} × ₩$rate = ₩${price * rate}');
return sum + (price * rate);
}
- debugPrint('[SubscriptionProvider] ${subscription.serviceName}: ₩$price');
+ debugPrint(
+ '[SubscriptionProvider] ${subscription.serviceName}: ₩$price');
return sum + price;
},
);
-
+
debugPrint('[SubscriptionProvider] totalMonthlyExpense 계산 완료: '
'${_subscriptions.length}개 구독, 총액 ₩$total');
return total;
@@ -69,10 +70,10 @@ class SubscriptionProvider extends ChangeNotifier {
_subscriptionBox = await Hive.openBox('subscriptions');
await refreshSubscriptions();
-
+
// categoryId 마이그레이션
await _migrateCategoryIds();
-
+
// 앱 시작 시 이벤트 상태 확인
await checkAndUpdateEventStatus();
@@ -90,11 +91,11 @@ class SubscriptionProvider extends ChangeNotifier {
try {
_subscriptions = _subscriptionBox.values.toList()
..sort((a, b) => a.nextBillingDate.compareTo(b.nextBillingDate));
-
+
debugPrint('[SubscriptionProvider] refreshSubscriptions 완료: '
'${_subscriptions.length}개 구독, '
'총 월간 지출: ${totalMonthlyExpense.toStringAsFixed(2)}');
-
+
notifyListeners();
} catch (e) {
debugPrint('구독 목록 새로고침 중 오류 발생: $e');
@@ -139,7 +140,7 @@ class SubscriptionProvider extends ChangeNotifier {
await _subscriptionBox.put(subscription.id, subscription);
await refreshSubscriptions();
-
+
// 이벤트가 활성화된 경우 알림 스케줄 재설정
if (isEventActive && eventEndDate != null) {
await _scheduleEventEndNotification(subscription);
@@ -191,7 +192,6 @@ class SubscriptionProvider extends ChangeNotifier {
}
}
-
Future clearAllSubscriptions() async {
_isLoading = true;
notifyListeners();
@@ -217,8 +217,9 @@ class SubscriptionProvider extends ChangeNotifier {
}
/// 이벤트 종료 알림을 스케줄링합니다.
- Future _scheduleEventEndNotification(SubscriptionModel subscription) async {
- if (subscription.eventEndDate != null &&
+ Future _scheduleEventEndNotification(
+ SubscriptionModel subscription) async {
+ if (subscription.eventEndDate != null &&
subscription.eventEndDate!.isAfter(DateTime.now())) {
await NotificationService.scheduleNotification(
id: '${subscription.id}_event_end'.hashCode,
@@ -232,19 +233,18 @@ class SubscriptionProvider extends ChangeNotifier {
/// 모든 구독의 이벤트 상태를 확인하고 업데이트합니다.
Future checkAndUpdateEventStatus() async {
bool hasChanges = false;
-
+
for (var subscription in _subscriptions) {
// 이벤트가 종료되었지만 아직 활성화되어 있는 경우
- if (subscription.isEventActive &&
+ if (subscription.isEventActive &&
subscription.eventEndDate != null &&
subscription.eventEndDate!.isBefore(DateTime.now())) {
-
subscription.isEventActive = false;
await _subscriptionBox.put(subscription.id, subscription);
hasChanges = true;
}
}
-
+
if (hasChanges) {
await refreshSubscriptions();
}
@@ -253,70 +253,73 @@ class SubscriptionProvider extends ChangeNotifier {
/// 총 월간 지출을 계산합니다. (로케일별 기본 통화로 환산)
Future calculateTotalExpense({String? locale}) async {
if (_subscriptions.isEmpty) return 0.0;
-
+
// locale이 제공되지 않으면 현재 로케일 사용
- final targetCurrency = locale != null
- ? CurrencyUtil.getDefaultCurrency(locale)
- : 'KRW'; // 기본값
-
+ final targetCurrency =
+ locale != null ? CurrencyUtil.getDefaultCurrency(locale) : 'KRW'; // 기본값
+
debugPrint('[calculateTotalExpense] 계산 시작 - 타겟 통화: $targetCurrency');
double total = 0.0;
-
+
for (final subscription in _subscriptions) {
final currentPrice = subscription.currentPrice;
debugPrint('[calculateTotalExpense] ${subscription.serviceName}: '
'${currentPrice} ${subscription.currency} (이벤트 적용: ${subscription.isCurrentlyInEvent})');
-
+
final converted = await ExchangeRateService().convertBetweenCurrencies(
currentPrice,
subscription.currency,
targetCurrency,
);
-
+
total += converted ?? currentPrice;
}
-
+
debugPrint('[calculateTotalExpense] 총 지출 계산 완료: $total $targetCurrency');
return total;
}
/// 최근 6개월의 월별 지출 데이터를 반환합니다. (로케일별 기본 통화로 환산)
- Future>> getMonthlyExpenseData({String? locale}) async {
+ Future>> getMonthlyExpenseData(
+ {String? locale}) async {
final now = DateTime.now();
final List