feat: add payment card grouping and analysis

This commit is contained in:
JiWoong Sul
2025-11-14 16:53:41 +09:00
parent cba7d082bd
commit 132ae758de
40 changed files with 2846 additions and 522 deletions

View File

@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/subscription_model.dart';
import '../providers/category_provider.dart';
import '../providers/payment_card_provider.dart';
import '../providers/locale_provider.dart';
import '../services/subscription_url_matcher.dart';
import '../services/currency_util.dart';
import '../utils/billing_date_util.dart';
import '../utils/payment_card_utils.dart';
import 'website_icon.dart';
import 'app_navigator.dart';
// import '../theme/app_colors.dart';
@@ -299,6 +301,7 @@ class _SubscriptionCardState extends State<SubscriptionCard>
Widget build(BuildContext context) {
// LocaleProvider를 watch하여 언어 변경시 자동 업데이트
final localeProvider = context.watch<LocaleProvider>();
final paymentCardProvider = context.watch<PaymentCardProvider>();
// 언어가 변경되면 displayName 다시 로드
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -464,7 +467,10 @@ class _SubscriptionCardState extends State<SubscriptionCard>
],
),
const SizedBox(height: 6),
const SizedBox(height: 8),
_buildPaymentCardBadge(
context, paymentCardProvider),
const SizedBox(height: 8),
// 가격 정보
Row(
@@ -673,4 +679,57 @@ class _SubscriptionCardState extends State<SubscriptionCard>
),
);
}
Widget _buildPaymentCardBadge(
BuildContext context, PaymentCardProvider provider) {
final scheme = Theme.of(context).colorScheme;
final loc = AppLocalizations.of(context);
final card = provider.getCardById(widget.subscription.paymentCardId);
if (card == null) {
return Chip(
avatar: Icon(
Icons.credit_card_off_rounded,
size: 14,
color: scheme.onSurfaceVariant,
),
label: Text(
loc.paymentCardUnassigned,
style: TextStyle(
fontSize: 12,
color: scheme.onSurfaceVariant,
),
),
backgroundColor: scheme.surfaceContainerHighest.withValues(alpha: 0.5),
padding: const EdgeInsets.symmetric(horizontal: 6),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
final color = PaymentCardUtils.colorFromHex(card.colorHex);
final icon = PaymentCardUtils.iconForName(card.iconName);
return Chip(
avatar: CircleAvatar(
backgroundColor: Colors.white,
child: Icon(
icon,
size: 14,
color: color,
),
),
label: Text(
'${card.issuerName} · ****${card.last4}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: color,
),
),
side: BorderSide(color: color.withValues(alpha: 0.3)),
backgroundColor: color.withValues(alpha: 0.12),
padding: const EdgeInsets.symmetric(horizontal: 8),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
}