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,10 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../services/currency_util.dart';
|
||||
import '../../providers/locale_provider.dart';
|
||||
import '../../theme/app_colors.dart';
|
||||
import '../glassmorphism_card.dart';
|
||||
import '../themed_text.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// 월별 지출 현황을 차트로 보여주는 카드 위젯
|
||||
class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
@@ -17,12 +20,64 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
required this.animationController,
|
||||
});
|
||||
|
||||
/// Y축 최대값을 계산합니다 (언어별 통화 단위에 맞춰)
|
||||
double _calculateChartMaxY(double maxValue, String locale) {
|
||||
final currency = CurrencyUtil.getDefaultCurrency(locale);
|
||||
|
||||
if (currency == 'KRW' || currency == 'JPY') {
|
||||
// 소수점이 없는 통화 (원화, 엔화)
|
||||
if (maxValue <= 0) return 100000;
|
||||
if (maxValue <= 10000) return 10000;
|
||||
if (maxValue <= 50000) return 50000;
|
||||
if (maxValue <= 100000) return 100000;
|
||||
if (maxValue <= 200000) return 200000;
|
||||
if (maxValue <= 500000) return 500000;
|
||||
if (maxValue <= 1000000) return 1000000;
|
||||
|
||||
// 큰 금액은 자릿수에 맞춰 반올림
|
||||
final magnitude = math.pow(10, maxValue.toString().split('.')[0].length - 1).toDouble();
|
||||
return ((maxValue / magnitude).ceil() * magnitude).toDouble();
|
||||
} else {
|
||||
// 소수점이 있는 통화 (달러, 위안)
|
||||
if (maxValue <= 0) return 100.0;
|
||||
if (maxValue <= 10) return 10.0;
|
||||
if (maxValue <= 25) return 25.0;
|
||||
if (maxValue <= 50) return 50.0;
|
||||
if (maxValue <= 100) return 100.0;
|
||||
if (maxValue <= 250) return 250.0;
|
||||
if (maxValue <= 500) return 500.0;
|
||||
if (maxValue <= 1000) return 1000.0;
|
||||
|
||||
// 큰 금액은 100 단위로 반올림
|
||||
return ((maxValue / 100).ceil() * 100).toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
/// 그리드 라인 간격을 계산합니다
|
||||
double _calculateGridInterval(double maxY, String currency) {
|
||||
if (currency == 'KRW' || currency == 'JPY') {
|
||||
// 4등분하되 깔끔한 숫자로
|
||||
if (maxY <= 40000) return 10000;
|
||||
if (maxY <= 100000) return 25000;
|
||||
if (maxY <= 200000) return 50000;
|
||||
if (maxY <= 400000) return 100000;
|
||||
return maxY / 4;
|
||||
} else {
|
||||
// 달러 등은 4등분
|
||||
if (maxY <= 40) return 10;
|
||||
if (maxY <= 100) return 25;
|
||||
if (maxY <= 200) return 50;
|
||||
if (maxY <= 400) return 100;
|
||||
return maxY / 4;
|
||||
}
|
||||
}
|
||||
|
||||
// 월간 지출 차트 데이터
|
||||
List<BarChartGroupData> _getMonthlyBarGroups() {
|
||||
List<BarChartGroupData> _getMonthlyBarGroups(String locale) {
|
||||
final List<BarChartGroupData> barGroups = [];
|
||||
final calculatedMax = monthlyData.fold<double>(
|
||||
0, (max, data) => math.max(max, data['totalExpense'] as double));
|
||||
final maxAmount = calculatedMax > 0 ? calculatedMax : 100000.0; // 기본값 10만원
|
||||
final maxAmount = _calculateChartMaxY(calculatedMax, locale);
|
||||
|
||||
for (int i = 0; i < monthlyData.length; i++) {
|
||||
final data = monthlyData[i];
|
||||
@@ -44,7 +99,7 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
backDrawRodData: BackgroundBarChartRodData(
|
||||
show: true,
|
||||
toY: maxAmount + (maxAmount * 0.1),
|
||||
toY: maxAmount,
|
||||
color: AppColors.navyGray.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
@@ -58,6 +113,7 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locale = context.watch<LocaleProvider>().locale.languageCode;
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -84,14 +140,14 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ThemedText.headline(
|
||||
text: '월별 지출 현황',
|
||||
text: AppLocalizations.of(context).monthlyExpenseTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ThemedText.subtitle(
|
||||
text: '최근 6개월간 추이',
|
||||
text: AppLocalizations.of(context).recentSixMonthsTrend,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
@@ -103,25 +159,26 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: math.max(
|
||||
maxY: _calculateChartMaxY(
|
||||
monthlyData.fold<double>(
|
||||
0,
|
||||
(max, data) => math.max(
|
||||
max, data['totalExpense'] as double)) *
|
||||
1.2,
|
||||
100000.0 // 최소값 10만원
|
||||
max, data['totalExpense'] as double)),
|
||||
locale
|
||||
),
|
||||
barGroups: _getMonthlyBarGroups(),
|
||||
barGroups: _getMonthlyBarGroups(locale),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: math.max(
|
||||
monthlyData.fold<double>(
|
||||
horizontalInterval: _calculateGridInterval(
|
||||
_calculateChartMaxY(
|
||||
monthlyData.fold<double>(
|
||||
0,
|
||||
(max, data) => math.max(max,
|
||||
data['totalExpense'] as double)) /
|
||||
4,
|
||||
25000.0 // 최소 간격 2.5만원
|
||||
data['totalExpense'] as double)),
|
||||
locale
|
||||
),
|
||||
CurrencyUtil.getDefaultCurrency(locale)
|
||||
),
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
@@ -176,9 +233,10 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: CurrencyUtil.formatTotalAmount(
|
||||
text: CurrencyUtil.formatTotalAmountWithLocale(
|
||||
monthlyData[group.x]['totalExpense']
|
||||
as double),
|
||||
as double,
|
||||
locale),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFBBF24),
|
||||
fontSize: 14,
|
||||
@@ -196,7 +254,7 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: ThemedText.caption(
|
||||
text: '월 구독 지출',
|
||||
text: AppLocalizations.of(context).monthlySubscriptionExpense,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
||||
Reference in New Issue
Block a user