feat: 다국어 지원 및 다중 통화 환율 변환 기능 확대
- ExchangeRateService에 JPY, CNY 환율 지원 추가 - 구독 서비스별 다국어 표시 이름 지원 - 분석 화면 차트 및 UI/UX 개선 - 설정 화면 전면 리팩토링 - SMS 스캔 기능 사용성 개선 - 전체 앱 다국어 번역 확대 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,13 @@ import '../models/subscription_model.dart';
|
||||
import '../models/category_model.dart';
|
||||
import '../providers/subscription_provider.dart';
|
||||
import '../providers/category_provider.dart';
|
||||
import '../providers/locale_provider.dart';
|
||||
import '../services/subscription_url_matcher.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../widgets/dialogs/delete_confirmation_dialog.dart';
|
||||
import '../widgets/common/snackbar/app_snackbar.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
/// DetailScreen의 비즈니스 로직을 관리하는 Controller
|
||||
class DetailScreenController extends ChangeNotifier {
|
||||
@@ -22,6 +24,10 @@ class DetailScreenController extends ChangeNotifier {
|
||||
late TextEditingController websiteUrlController;
|
||||
late TextEditingController eventPriceController;
|
||||
|
||||
// Display Names
|
||||
String? _displayName;
|
||||
String? get displayName => _displayName;
|
||||
|
||||
// Form State
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
late String _billingCycle;
|
||||
@@ -197,6 +203,9 @@ class DetailScreenController extends ChangeNotifier {
|
||||
// 애니메이션 시작
|
||||
animationController!.forward();
|
||||
|
||||
// 로케일에 맞는 서비스명 로드
|
||||
_loadDisplayName();
|
||||
|
||||
// 서비스명 변경 감지 리스너
|
||||
serviceNameController.addListener(onServiceNameChanged);
|
||||
|
||||
@@ -206,6 +215,20 @@ class DetailScreenController extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
/// 로케일에 맞는 서비스명 로드
|
||||
Future<void> _loadDisplayName() async {
|
||||
final localeProvider = context.read<LocaleProvider>();
|
||||
final locale = localeProvider.locale.languageCode;
|
||||
|
||||
final displayName = await SubscriptionUrlMatcher.getServiceDisplayName(
|
||||
serviceName: subscription.serviceName,
|
||||
locale: locale,
|
||||
);
|
||||
|
||||
_displayName = displayName;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 리소스 정리
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -282,7 +305,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
serviceName.contains('플로') ||
|
||||
serviceName.contains('벅스')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '음악 서비스',
|
||||
(cat) => cat.name == 'music',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -295,7 +318,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
serviceName.contains('icloud') ||
|
||||
serviceName.contains('adobe')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '오피스/협업 툴',
|
||||
(cat) => cat.name == 'collaborationOffice',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -306,7 +329,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
serviceName.contains('copilot') ||
|
||||
serviceName.contains('midjourney')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == 'AI 서비스',
|
||||
(cat) => cat.name == 'aiService',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -317,7 +340,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
serviceName.contains('패스트캠퍼스') ||
|
||||
serviceName.contains('클래스101')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '프로그래밍/개발',
|
||||
(cat) => cat.name == 'programming',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -328,7 +351,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
serviceName.contains('네이버') ||
|
||||
serviceName.contains('11번가')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '기타 서비스',
|
||||
(cat) => cat.name == 'other',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -344,7 +367,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
if (formKey.currentState != null && !formKey.currentState!.validate()) {
|
||||
AppSnackBar.showError(
|
||||
context: context,
|
||||
message: '필수 항목을 모두 입력해주세요',
|
||||
message: AppLocalizations.of(context).requiredFieldsError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -368,6 +391,10 @@ class DetailScreenController extends ChangeNotifier {
|
||||
monthlyCost = subscription.monthlyCost;
|
||||
}
|
||||
|
||||
debugPrint('[DetailScreenController] 구독 업데이트 시작: '
|
||||
'${subscription.serviceName} → ${serviceNameController.text}, '
|
||||
'금액: ${subscription.monthlyCost} → $monthlyCost ${_currency}');
|
||||
|
||||
subscription.serviceName = serviceNameController.text;
|
||||
subscription.monthlyCost = monthlyCost;
|
||||
subscription.websiteUrl = websiteUrl;
|
||||
@@ -393,13 +420,17 @@ class DetailScreenController extends ChangeNotifier {
|
||||
subscription.eventPrice = null;
|
||||
}
|
||||
|
||||
debugPrint('[DetailScreenController] 업데이트 정보: '
|
||||
'현재가격=${subscription.currentPrice}, '
|
||||
'이벤트활성=${subscription.isEventActive}');
|
||||
|
||||
// 구독 업데이트
|
||||
await provider.updateSubscription(subscription);
|
||||
|
||||
if (context.mounted) {
|
||||
AppSnackBar.showSuccess(
|
||||
context: context,
|
||||
message: '구독 정보가 업데이트되었습니다.',
|
||||
message: AppLocalizations.of(context).subscriptionUpdated,
|
||||
);
|
||||
|
||||
// 변경 사항이 반영될 시간을 주기 위해 짧게 지연 후 결과 반환
|
||||
@@ -413,10 +444,18 @@ class DetailScreenController extends ChangeNotifier {
|
||||
/// 구독 삭제
|
||||
Future<void> deleteSubscription() async {
|
||||
if (context.mounted) {
|
||||
// 로케일에 맞는 서비스명 가져오기
|
||||
final localeProvider = Provider.of<LocaleProvider>(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: subscription.serviceName,
|
||||
serviceName: displayName,
|
||||
);
|
||||
|
||||
if (!shouldDelete) return;
|
||||
@@ -429,7 +468,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
if (context.mounted) {
|
||||
AppSnackBar.showError(
|
||||
context: context,
|
||||
message: '구독이 삭제되었습니다.',
|
||||
message: AppLocalizations.of(context).subscriptionDeleted(displayName),
|
||||
icon: Icons.delete_forever_rounded,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
@@ -459,7 +498,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
if (context.mounted) {
|
||||
AppSnackBar.showInfo(
|
||||
context: context,
|
||||
message: '공식 해지 페이지를 찾을 수 없어 구글 검색으로 연결합니다.',
|
||||
message: AppLocalizations.of(context).officialCancelPageNotFound,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -470,7 +509,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
if (context.mounted) {
|
||||
AppSnackBar.showError(
|
||||
context: context,
|
||||
message: '웹사이트를 열 수 없습니다.',
|
||||
message: AppLocalizations.of(context).cannotOpenWebsite,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -487,7 +526,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
if (context.mounted) {
|
||||
AppSnackBar.showError(
|
||||
context: context,
|
||||
message: '웹사이트를 열 수 없습니다.',
|
||||
message: AppLocalizations.of(context).cannotOpenWebsite,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -495,7 +534,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
if (context.mounted) {
|
||||
AppSnackBar.showWarning(
|
||||
context: context,
|
||||
message: '웹사이트 정보가 없습니다. 해지는 웹사이트에서 진행해주세요.',
|
||||
message: AppLocalizations.of(context).noWebsiteInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user