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

@@ -4,6 +4,7 @@ import '../../controllers/detail_screen_controller.dart';
import '../../theme/app_colors.dart';
import '../common/form_fields/currency_input_field.dart';
import '../common/form_fields/date_picker_field.dart';
import '../../l10n/app_localizations.dart';
/// 이벤트 가격 섹션
/// 할인 이벤트 정보를 관리하는 섹션입니다.
@@ -75,9 +76,9 @@ class DetailEventSection extends StatelessWidget {
),
),
const SizedBox(width: 12),
const Text(
'이벤트 가격',
style: TextStyle(
Text(
AppLocalizations.of(context).eventPrice,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.darkNavy,
@@ -125,7 +126,7 @@ class DetailEventSection extends StatelessWidget {
const SizedBox(width: 8),
Expanded(
child: Text(
'할인 또는 프로모션 가격을 설정하세요',
AppLocalizations.of(context).eventPriceHint,
style: TextStyle(
fontSize: 14,
color: AppColors.darkNavy,
@@ -151,8 +152,8 @@ class DetailEventSection extends StatelessWidget {
onEndDateSelected: (date) {
controller.eventEndDate = date;
},
startLabel: '시작일',
endLabel: '종료일',
startLabel: AppLocalizations.of(context).startDate,
endLabel: AppLocalizations.of(context).endDate,
primaryColor: baseColor,
),
const SizedBox(height: 20),
@@ -160,16 +161,16 @@ class DetailEventSection extends StatelessWidget {
CurrencyInputField(
controller: controller.eventPriceController,
currency: controller.currency,
label: '이벤트 가격',
hintText: '할인된 가격을 입력하세요',
label: AppLocalizations.of(context).eventPrice,
hintText: AppLocalizations.of(context).eventPriceHint,
validator: controller.isEventActive
? (value) {
if (value == null || value.isEmpty) {
return '이벤트 가격을 입력해주세요';
return AppLocalizations.of(context).eventPriceRequired;
}
final price = double.tryParse(value.replaceAll(',', ''));
if (price == null || price <= 0) {
return '올바른 가격을 입력해주세요';
return AppLocalizations.of(context).invalidPrice;
}
return null;
}
@@ -233,7 +234,7 @@ class _DiscountBadge extends StatelessWidget {
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$discountPercentage% 할인',
AppLocalizations.of(context).discountPercent.replaceAll('@', discountPercentage.toString()),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
@@ -243,9 +244,7 @@ class _DiscountBadge extends StatelessWidget {
),
const SizedBox(width: 12),
Text(
currency == 'KRW'
? '${discountAmount.toInt().toString()}원 절약'
: '\$${discountAmount.toStringAsFixed(2)} 절약',
_getLocalizedDiscountAmount(context, currency, discountAmount),
style: TextStyle(
color: const Color(0xFF15803D),
fontSize: 14,
@@ -256,4 +255,18 @@ class _DiscountBadge extends StatelessWidget {
),
);
}
String _getLocalizedDiscountAmount(BuildContext context, String currency, double amount) {
final loc = AppLocalizations.of(context);
switch (currency) {
case 'KRW':
return loc.discountAmountWon.replaceAll('@', amount.toInt().toString());
case 'JPY':
return loc.discountAmountYen.replaceAll('@', amount.toInt().toString());
case 'CNY':
return loc.discountAmountYuan.replaceAll('@', amount.toStringAsFixed(2));
default: // USD
return loc.discountAmountDollar.replaceAll('@', amount.toStringAsFixed(2));
}
}
}