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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user