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

@@ -3,7 +3,10 @@ import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../models/subscription_model.dart';
import '../../controllers/detail_screen_controller.dart';
import '../../providers/locale_provider.dart';
import '../../services/currency_util.dart';
import '../website_icon.dart';
import '../../l10n/app_localizations.dart';
/// 상세 화면 상단 헤더 섹션
/// 서비스 아이콘, 이름, 결제 정보를 표시합니다.
@@ -134,7 +137,7 @@ class DetailHeaderSection extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.serviceNameController.text,
controller.displayName ?? controller.serviceNameController.text,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
@@ -151,7 +154,8 @@ class DetailHeaderSection extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
'${controller.billingCycle} 결제',
AppLocalizations.of(context).billingCyclePayment.replaceAll('@',
_getLocalizedBillingCycle(context, controller.billingCycle)),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
@@ -175,25 +179,28 @@ class DetailHeaderSection extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_InfoColumn(
label: '다음 결제일',
value: DateFormat('yyyy년 MM월 dd일')
.format(controller.nextBillingDate),
label: AppLocalizations.of(context).nextBillingDate,
value: AppLocalizations.of(context).formatDate(controller.nextBillingDate),
),
_InfoColumn(
label: '월 지출',
value: NumberFormat.currency(
locale: controller.currency == 'KRW'
? 'ko_KR'
: 'en_US',
symbol: controller.currency == 'KRW'
? ''
: '\$',
decimalDigits:
controller.currency == 'KRW' ? 0 : 2,
).format(double.tryParse(
controller.monthlyCostController.text.replaceAll(',', '')
) ?? 0),
alignment: CrossAxisAlignment.end,
FutureBuilder<String>(
future: () async {
final locale = context.read<LocaleProvider>().locale.languageCode;
final amount = double.tryParse(
controller.monthlyCostController.text.replaceAll(',', '')
) ?? 0;
return CurrencyUtil.formatAmountWithLocale(
amount,
controller.currency,
locale,
);
}(),
builder: (context, snapshot) {
return _InfoColumn(
label: AppLocalizations.of(context).monthlyExpense,
value: snapshot.data ?? '-',
alignment: CrossAxisAlignment.end,
);
},
),
],
),
@@ -212,6 +219,33 @@ class DetailHeaderSection extends StatelessWidget {
},
);
}
String _getLocalizedBillingCycle(BuildContext context, String cycle) {
final loc = AppLocalizations.of(context);
switch (cycle.toLowerCase()) {
case '매월':
case 'monthly':
case '毎月':
case '每月':
return loc.billingCycleMonthly;
case '분기별':
case 'quarterly':
case '四半期':
case '每季度':
return loc.billingCycleQuarterly;
case '반기별':
case 'half-yearly':
case '半年ごと':
case '每半年':
return loc.billingCycleHalfYearly;
case '매년':
case 'yearly':
case '年間':
case '每年':
return loc.billingCycleYearly;
default:
return cycle;
}
}
}
/// 정보 표시 컬럼