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

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
/// 결제수단 관련 공통 유틸리티
class PaymentCardUtils {
static const List<String> colorPalette = [
'#FF6B6B',
'#F97316',
'#F59E0B',
'#10B981',
'#06B6D4',
'#3B82F6',
'#6366F1',
'#8B5CF6',
'#EC4899',
'#14B8A6',
'#0EA5E9',
'#94A3B8',
];
static const Map<String, IconData> iconMap = {
'credit_card': Icons.credit_card_rounded,
'payments': Icons.payments_rounded,
'wallet': Icons.account_balance_wallet_rounded,
'bank': Icons.account_balance_rounded,
'shopping': Icons.shopping_bag_rounded,
'subscriptions': Icons.subscriptions_rounded,
'bolt': Icons.bolt_rounded,
};
static IconData iconForName(String name) {
return iconMap[name] ?? Icons.credit_card_rounded;
}
static Color colorFromHex(String hex) {
var value = hex.replaceAll('#', '');
if (value.length == 6) {
value = 'ff$value';
}
return Color(int.parse(value, radix: 16));
}
}

View File

@@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
import '../models/payment_card_model.dart';
import '../models/subscription_model.dart';
import '../providers/category_provider.dart';
import '../providers/payment_card_provider.dart';
import 'subscription_category_helper.dart';
enum SubscriptionGroupingMode { category, paymentCard }
class SubscriptionGroupData {
final String id;
final String title;
final List<SubscriptionModel> subscriptions;
final SubscriptionGroupingMode mode;
final PaymentCardModel? paymentCard;
final bool isUnassignedCard;
final String? categoryKey;
final String? subtitle;
const SubscriptionGroupData({
required this.id,
required this.title,
required this.subscriptions,
required this.mode,
this.paymentCard,
this.isUnassignedCard = false,
this.categoryKey,
this.subtitle,
});
}
class SubscriptionGroupingHelper {
static const _unassignedCardKey = '__unassigned__';
static List<SubscriptionGroupData> buildGroups({
required BuildContext context,
required List<SubscriptionModel> subscriptions,
required SubscriptionGroupingMode mode,
required CategoryProvider categoryProvider,
required PaymentCardProvider paymentCardProvider,
}) {
if (mode == SubscriptionGroupingMode.paymentCard) {
return _groupByPaymentCard(
context: context,
subscriptions: subscriptions,
paymentCardProvider: paymentCardProvider,
);
}
return _groupByCategory(
context: context,
subscriptions: subscriptions,
categoryProvider: categoryProvider,
);
}
static List<SubscriptionGroupData> _groupByCategory({
required BuildContext context,
required List<SubscriptionModel> subscriptions,
required CategoryProvider categoryProvider,
}) {
final localizedMap = SubscriptionCategoryHelper.categorizeSubscriptions(
subscriptions, categoryProvider, context);
final orderMap = <String, int>{};
for (var i = 0; i < categoryProvider.categories.length; i++) {
orderMap[categoryProvider.categories[i].name] = i;
}
final groups = localizedMap.entries.map((entry) {
final title =
categoryProvider.getLocalizedCategoryName(context, entry.key);
return SubscriptionGroupData(
id: entry.key,
title: title,
subscriptions: entry.value,
mode: SubscriptionGroupingMode.category,
categoryKey: entry.key,
);
}).toList();
groups.sort((a, b) {
final ai = orderMap[a.categoryKey] ?? 999;
final bi = orderMap[b.categoryKey] ?? 999;
if (ai != bi) {
return ai.compareTo(bi);
}
return a.title.compareTo(b.title);
});
return groups;
}
static List<SubscriptionGroupData> _groupByPaymentCard({
required BuildContext context,
required List<SubscriptionModel> subscriptions,
required PaymentCardProvider paymentCardProvider,
}) {
final map = <String, List<SubscriptionModel>>{};
for (final sub in subscriptions) {
final key = sub.paymentCardId ?? _unassignedCardKey;
map.putIfAbsent(key, () => []).add(sub);
}
final loc = AppLocalizations.of(context);
final groups = <SubscriptionGroupData>[];
map.forEach((key, subs) {
if (key == _unassignedCardKey) {
groups.add(
SubscriptionGroupData(
id: key,
title: loc.paymentCardUnassigned,
subtitle: loc.paymentCardUnassigned,
subscriptions: subs,
mode: SubscriptionGroupingMode.paymentCard,
isUnassignedCard: true,
),
);
} else {
final card = paymentCardProvider.getCardById(key);
final title = card?.issuerName ?? loc.paymentCardUnassigned;
final subtitle =
card != null ? '****${card.last4}' : loc.paymentCardUnassigned;
groups.add(
SubscriptionGroupData(
id: key,
title: title,
subtitle: subtitle,
subscriptions: subs,
mode: SubscriptionGroupingMode.paymentCard,
paymentCard: card,
),
);
}
});
groups.sort((a, b) {
if (a.isUnassignedCard != b.isUnassignedCard) {
return a.isUnassignedCard ? 1 : -1;
}
final aDefault = a.paymentCard?.isDefault ?? false;
final bDefault = b.paymentCard?.isDefault ?? false;
if (aDefault != bDefault) {
return aDefault ? -1 : 1;
}
return a.title.toLowerCase().compareTo(b.title.toLowerCase());
});
return groups;
}
}