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:
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'dart:math' as math;
|
||||
import '../../controllers/add_subscription_controller.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// 구독 추가 화면의 App Bar
|
||||
class AddSubscriptionAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
@@ -49,7 +50,7 @@ class AddSubscriptionAppBar extends StatelessWidget implements PreferredSizeWidg
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: Text(
|
||||
'구독 추가',
|
||||
AppLocalizations.of(context).addSubscription,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Montserrat',
|
||||
fontSize: 24,
|
||||
@@ -93,7 +94,7 @@ class AddSubscriptionAppBar extends StatelessWidget implements PreferredSizeWidg
|
||||
color: Color(0xFF3B82F6),
|
||||
),
|
||||
onPressed: onScanSMS,
|
||||
tooltip: 'SMS에서 구독 정보 스캔',
|
||||
tooltip: AppLocalizations.of(context).scanTextMessages,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import '../../controllers/add_subscription_controller.dart';
|
||||
import '../common/form_fields/currency_input_field.dart';
|
||||
import '../common/form_fields/date_picker_field.dart';
|
||||
import '../../theme/app_colors.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// 구독 추가 화면의 이벤트/할인 섹션
|
||||
class AddSubscriptionEventSection extends StatelessWidget {
|
||||
@@ -75,13 +76,32 @@ class AddSubscriptionEventSection extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'이벤트 가격',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.darkNavy,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final locale = Localizations.localeOf(context);
|
||||
String titleText;
|
||||
switch (locale.languageCode) {
|
||||
case 'ko':
|
||||
titleText = '이벤트 가격';
|
||||
break;
|
||||
case 'ja':
|
||||
titleText = 'イベント価格';
|
||||
break;
|
||||
case 'zh':
|
||||
titleText = '活动价格';
|
||||
break;
|
||||
default:
|
||||
titleText = 'Event Price';
|
||||
}
|
||||
return Text(
|
||||
titleText,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.darkNavy,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -133,13 +153,32 @@ class AddSubscriptionEventSection extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'할인 또는 프로모션 가격을 설정하세요',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.darkNavy,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final locale = Localizations.localeOf(context);
|
||||
String infoText;
|
||||
switch (locale.languageCode) {
|
||||
case 'ko':
|
||||
infoText = '할인 또는 프로모션 가격을 설정하세요';
|
||||
break;
|
||||
case 'ja':
|
||||
infoText = '割引またはプロモーション価格を設定してください';
|
||||
break;
|
||||
case 'zh':
|
||||
infoText = '设置折扣或促销价格';
|
||||
break;
|
||||
default:
|
||||
infoText = 'Set up discount or promotion price';
|
||||
}
|
||||
return Text(
|
||||
infoText,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.darkNavy,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -148,35 +187,88 @@ class AddSubscriptionEventSection extends StatelessWidget {
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 이벤트 기간
|
||||
DateRangePickerField(
|
||||
startDate: controller.eventStartDate,
|
||||
endDate: controller.eventEndDate,
|
||||
onStartDateSelected: (date) {
|
||||
setState(() {
|
||||
controller.eventStartDate = date;
|
||||
// 시작일 설정 시 종료일이 없으면 자동으로 1달 후로 설정
|
||||
if (date != null && controller.eventEndDate == null) {
|
||||
controller.eventEndDate = date.add(const Duration(days: 30));
|
||||
}
|
||||
});
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final locale = Localizations.localeOf(context);
|
||||
String startLabel;
|
||||
String endLabel;
|
||||
switch (locale.languageCode) {
|
||||
case 'ko':
|
||||
startLabel = '시작일';
|
||||
endLabel = '종료일';
|
||||
break;
|
||||
case 'ja':
|
||||
startLabel = '開始日';
|
||||
endLabel = '終了日';
|
||||
break;
|
||||
case 'zh':
|
||||
startLabel = '开始日期';
|
||||
endLabel = '结束日期';
|
||||
break;
|
||||
default:
|
||||
startLabel = 'Start Date';
|
||||
endLabel = 'End Date';
|
||||
}
|
||||
return DateRangePickerField(
|
||||
startDate: controller.eventStartDate,
|
||||
endDate: controller.eventEndDate,
|
||||
onStartDateSelected: (date) {
|
||||
setState(() {
|
||||
controller.eventStartDate = date;
|
||||
// 시작일 설정 시 종료일이 없으면 자동으로 1달 후로 설정
|
||||
if (date != null && controller.eventEndDate == null) {
|
||||
controller.eventEndDate = date.add(const Duration(days: 30));
|
||||
}
|
||||
});
|
||||
},
|
||||
onEndDateSelected: (date) {
|
||||
setState(() {
|
||||
controller.eventEndDate = date;
|
||||
});
|
||||
},
|
||||
startLabel: startLabel,
|
||||
endLabel: endLabel,
|
||||
primaryColor: controller.gradientColors[0],
|
||||
);
|
||||
},
|
||||
onEndDateSelected: (date) {
|
||||
setState(() {
|
||||
controller.eventEndDate = date;
|
||||
});
|
||||
},
|
||||
startLabel: '시작일',
|
||||
endLabel: '종료일',
|
||||
primaryColor: controller.gradientColors[0],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 이벤트 가격
|
||||
CurrencyInputField(
|
||||
controller: controller.eventPriceController,
|
||||
currency: controller.currency,
|
||||
label: '이벤트 가격',
|
||||
hintText: '할인된 가격을 입력하세요',
|
||||
Builder(
|
||||
builder: (BuildContext innerContext) {
|
||||
// 현재 로케일 확인
|
||||
final currentLocale = Localizations.localeOf(innerContext);
|
||||
|
||||
// 로케일에 따라 직접 텍스트 설정
|
||||
String eventPriceLabel;
|
||||
String eventPriceHint;
|
||||
|
||||
switch (currentLocale.languageCode) {
|
||||
case 'ko':
|
||||
eventPriceLabel = '이벤트 가격';
|
||||
eventPriceHint = '할인된 가격을 입력하세요';
|
||||
break;
|
||||
case 'ja':
|
||||
eventPriceLabel = 'イベント価格';
|
||||
eventPriceHint = '割引価格を入力してください';
|
||||
break;
|
||||
case 'zh':
|
||||
eventPriceLabel = '活动价格';
|
||||
eventPriceHint = '输入折扣价格';
|
||||
break;
|
||||
default:
|
||||
eventPriceLabel = 'Event Price';
|
||||
eventPriceHint = 'Enter discounted price';
|
||||
}
|
||||
|
||||
return CurrencyInputField(
|
||||
controller: controller.eventPriceController,
|
||||
currency: controller.currency,
|
||||
label: eventPriceLabel,
|
||||
hintText: eventPriceHint,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import '../../controllers/add_subscription_controller.dart';
|
||||
import '../../providers/category_provider.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../common/form_fields/base_text_field.dart';
|
||||
import '../common/form_fields/currency_input_field.dart';
|
||||
import '../common/form_fields/date_picker_field.dart';
|
||||
@@ -67,9 +68,9 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'서비스 정보',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
AppLocalizations.of(context).serviceInfo,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.5,
|
||||
@@ -84,15 +85,15 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
BaseTextField(
|
||||
controller: controller.serviceNameController,
|
||||
focusNode: controller.serviceNameFocus,
|
||||
label: '서비스명',
|
||||
hintText: '예: Netflix, Spotify',
|
||||
label: AppLocalizations.of(context).labelServiceName,
|
||||
hintText: AppLocalizations.of(context).hintServiceName,
|
||||
textInputAction: TextInputAction.next,
|
||||
onEditingComplete: () {
|
||||
controller.monthlyCostFocus.requestFocus();
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '서비스명을 입력해주세요';
|
||||
return AppLocalizations.of(context).serviceNameRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -108,7 +109,7 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
child: CurrencyInputField(
|
||||
controller: controller.monthlyCostController,
|
||||
currency: controller.currency,
|
||||
label: '월 지출',
|
||||
label: AppLocalizations.of(context).labelMonthlyExpense,
|
||||
focusNode: controller.monthlyCostFocus,
|
||||
textInputAction: TextInputAction.next,
|
||||
onEditingComplete: () {
|
||||
@@ -116,7 +117,7 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '금액을 입력해주세요';
|
||||
return AppLocalizations.of(context).amountRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -127,9 +128,9 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'통화',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
AppLocalizations.of(context).currency,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -155,9 +156,10 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'결제 주기',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
AppLocalizations.of(context).billingCycle,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -185,7 +187,7 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
controller.nextBillingDate = date;
|
||||
});
|
||||
},
|
||||
label: '다음 결제일',
|
||||
label: AppLocalizations.of(context).nextBillingDate,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365 * 2)),
|
||||
primaryColor: controller.gradientColors[0],
|
||||
@@ -196,8 +198,8 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
BaseTextField(
|
||||
controller: controller.websiteUrlController,
|
||||
focusNode: controller.websiteUrlFocus,
|
||||
label: '웹사이트 URL (선택)',
|
||||
hintText: 'https://example.com',
|
||||
label: AppLocalizations.of(context).websiteUrlOptional,
|
||||
hintText: AppLocalizations.of(context).hintWebsiteUrl,
|
||||
keyboardType: TextInputType.url,
|
||||
prefixIcon: Icon(
|
||||
Icons.link_rounded,
|
||||
@@ -212,9 +214,9 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'카테고리',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
AppLocalizations.of(context).category,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -243,4 +245,3 @@ class AddSubscriptionForm extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../controllers/add_subscription_controller.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// 구독 추가 화면의 헤더 섹션
|
||||
class AddSubscriptionHeader extends StatelessWidget {
|
||||
@@ -54,23 +55,23 @@ class AddSubscriptionHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'새 구독 추가',
|
||||
style: TextStyle(
|
||||
AppLocalizations.of(context).newSubscriptionAdd,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'서비스 정보를 입력해주세요',
|
||||
style: TextStyle(
|
||||
AppLocalizations.of(context).enterServiceInfo,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white70,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../controllers/add_subscription_controller.dart';
|
||||
import '../common/buttons/primary_button.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// 구독 추가 화면의 저장 버튼
|
||||
class AddSubscriptionSaveButton extends StatelessWidget {
|
||||
@@ -37,7 +38,7 @@ class AddSubscriptionSaveButton extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
child: PrimaryButton(
|
||||
text: '구독 추가하기',
|
||||
text: AppLocalizations.of(context).addSubscriptionButton,
|
||||
icon: Icons.add_circle_outline,
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
|
||||
Reference in New Issue
Block a user