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:
JiWoong Sul
2025-07-16 17:34:32 +09:00
parent 4d1c0f5dab
commit 0f0b02bf08
55 changed files with 4100 additions and 1197 deletions

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../theme/app_colors.dart';
import '../../../l10n/app_localizations.dart';
/// 결제 주기 선택 위젯
/// 월간, 분기별, 반기별, 연간 중 선택할 수 있습니다.
@@ -21,10 +22,21 @@ class BillingCycleSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
final localization = AppLocalizations.of(context);
// 상세 화면에서는 '매월', 추가 화면에서는 '월간'으로 표시
final cycles = isGlassmorphism
? ['매월', '분기별', '반기별', '매년']
: ['월간', '분기별', '반기별', '연간'];
? [
localization.billingCycleMonthly,
localization.billingCycleQuarterly,
localization.billingCycleHalfYearly,
localization.billingCycleYearly,
]
: [
localization.monthly,
localization.billingCycleQuarterly,
localization.billingCycleHalfYearly,
localization.yearly,
];
return SingleChildScrollView(
scrollDirection: Axis.horizontal,

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../theme/app_colors.dart';
import '../../../providers/category_provider.dart';
/// 카테고리 선택 위젯
/// 구독 서비스의 카테고리를 선택할 수 있습니다.
@@ -50,13 +52,17 @@ class CategorySelector extends StatelessWidget {
color: _getTextColor(isSelected),
),
const SizedBox(width: 6),
Text(
category.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _getTextColor(isSelected),
),
Consumer<CategoryProvider>(
builder: (context, categoryProvider, child) {
return Text(
categoryProvider.getLocalizedCategoryName(context, category.name),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _getTextColor(isSelected),
),
);
},
),
],
),
@@ -69,25 +75,25 @@ class CategorySelector extends StatelessWidget {
IconData _getCategoryIcon(dynamic category) {
// 카테고리명에 따른 아이콘 반환
switch (category.name) {
case '음악':
case 'music':
return Icons.music_note_rounded;
case 'OTT(동영상)':
case 'ottVideo':
return Icons.movie_filter_rounded;
case '저장/클라우드':
case 'storageCloud':
return Icons.cloud_outlined;
case '통신 · 인터넷 · TV':
case 'telecomInternetTv':
return Icons.wifi_rounded;
case '생활/라이프스타일':
case 'lifestyle':
return Icons.home_outlined;
case '쇼핑/이커머스':
case 'shoppingEcommerce':
return Icons.shopping_cart_outlined;
case '프로그래밍':
case 'programming':
return Icons.code_rounded;
case '협업/오피스':
case 'collaborationOffice':
return Icons.business_center_outlined;
case 'AI 서비스':
case 'aiService':
return Icons.smart_toy_outlined;
case '기타':
case 'other':
default:
return Icons.category_outlined;
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'base_text_field.dart';
import '../../../l10n/app_localizations.dart';
/// 통화 입력 필드 위젯
/// 원화(KRW)와 달러(USD)를 지원하며 자동 포맷팅을 제공합니다.
@@ -112,8 +113,8 @@ class _CurrencyInputFieldState extends State<CurrencyInputField> {
return widget.currency == 'KRW' ? '' : '\$ ';
}
String get _defaultHintText {
return widget.currency == 'KRW' ? '금액을 입력하세요' : 'Enter amount';
String _getDefaultHintText(BuildContext context) {
return AppLocalizations.of(context).enterAmount;
}
@override
@@ -122,7 +123,7 @@ class _CurrencyInputFieldState extends State<CurrencyInputField> {
controller: widget.controller,
focusNode: _focusNode,
label: widget.label,
hintText: widget.hintText ?? _defaultHintText,
hintText: widget.hintText ?? _getDefaultHintText(context),
textInputAction: widget.textInputAction,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
@@ -158,11 +159,11 @@ class _CurrencyInputFieldState extends State<CurrencyInputField> {
},
validator: widget.validator ?? (value) {
if (value == null || value.isEmpty) {
return '금액을 입력해주세요';
return AppLocalizations.of(context).amountRequired;
}
final parsedValue = _parseValue(value);
if (parsedValue == null || parsedValue <= 0) {
return '올바른 금액을 입력해주세요';
return AppLocalizations.of(context).invalidAmount;
}
return null;
},

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../../theme/app_colors.dart';
/// 통화 선택 위젯
/// KRW(원화) USD(달러) 중 선택할 수 있습니다.
/// KRW(원화), USD(달러), JPY(엔화), CNY(위안화) 중 선택할 수 있습니다.
class CurrencySelector extends StatelessWidget {
final String currency;
final ValueChanged<String> onChanged;
@@ -17,22 +17,48 @@ class CurrencySelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
return Column(
children: [
_CurrencyOption(
label: '',
value: 'KRW',
isSelected: currency == 'KRW',
onTap: () => onChanged('KRW'),
isGlassmorphism: isGlassmorphism,
Row(
children: [
_CurrencyOption(
label: '',
value: 'KRW',
isSelected: currency == 'KRW',
onTap: () => onChanged('KRW'),
isGlassmorphism: isGlassmorphism,
),
const SizedBox(width: 8),
_CurrencyOption(
label: '\$',
value: 'USD',
isSelected: currency == 'USD',
onTap: () => onChanged('USD'),
isGlassmorphism: isGlassmorphism,
),
],
),
const SizedBox(width: 8),
_CurrencyOption(
label: '\$',
value: 'USD',
isSelected: currency == 'USD',
onTap: () => onChanged('USD'),
isGlassmorphism: isGlassmorphism,
const SizedBox(height: 8),
Row(
children: [
_CurrencyOption(
label: '¥',
value: 'JPY',
subtitle: 'JPY',
isSelected: currency == 'JPY',
onTap: () => onChanged('JPY'),
isGlassmorphism: isGlassmorphism,
),
const SizedBox(width: 8),
_CurrencyOption(
label: '¥',
value: 'CNY',
subtitle: 'CNY',
isSelected: currency == 'CNY',
onTap: () => onChanged('CNY'),
isGlassmorphism: isGlassmorphism,
),
],
),
],
);
@@ -43,6 +69,7 @@ class CurrencySelector extends StatelessWidget {
class _CurrencyOption extends StatelessWidget {
final String label;
final String value;
final String? subtitle;
final bool isSelected;
final VoidCallback onTap;
final bool isGlassmorphism;
@@ -50,6 +77,7 @@ class _CurrencyOption extends StatelessWidget {
const _CurrencyOption({
required this.label,
required this.value,
this.subtitle,
required this.isSelected,
required this.onTap,
required this.isGlassmorphism,
@@ -71,13 +99,29 @@ class _CurrencyOption extends StatelessWidget {
border: _getBorder(),
),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _getTextColor(),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _getTextColor(),
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: _getTextColor().withValues(alpha: 0.8),
),
),
],
],
),
),
),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../theme/app_colors.dart';
import '../../../l10n/app_localizations.dart';
/// 날짜 선택 필드 위젯
/// 탭하면 날짜 선택기가 표시되며, 선택된 날짜를 보기 좋은 형식으로 표시합니다.
@@ -38,7 +39,9 @@ class DatePickerField extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final effectivePrimaryColor = primaryColor ?? theme.primaryColor;
final effectiveDateFormat = dateFormat ?? 'yyyy년 MM월 dd일';
final localizations = AppLocalizations.of(context);
final effectiveDateFormat = dateFormat ?? localizations.dateFormatFull;
final locale = Localizations.localeOf(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -94,7 +97,7 @@ class DatePickerField extends StatelessWidget {
children: [
Expanded(
child: Text(
DateFormat(effectiveDateFormat).format(selectedDate),
DateFormat(effectiveDateFormat, locale.toString()).format(selectedDate),
style: TextStyle(
fontSize: 16,
color: enabled
@@ -249,8 +252,8 @@ class _DateRangeItem extends StatelessWidget {
const SizedBox(height: 4),
Text(
date != null
? DateFormat('MM/dd').format(date!)
: '선택',
? DateFormat(AppLocalizations.of(context).dateFormatShort).format(date!)
: AppLocalizations.of(context).dateSelect,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,