feat: 폼 필드 컴포넌트 분리 및 구독 카드 인터랙션 개선

- billing_cycle_selector, category_selector, currency_selector 컴포넌트 분리
- 구독 카드 클릭 이슈 해결을 위한 리팩토링
- SMS 스캔 화면 UI/UX 개선 및 기능 강화
- 상세 화면 컨트롤러 로직 개선
- 알림 서비스 및 구독 URL 매칭 기능 추가
- CLAUDE.md 프로젝트 가이드라인 대폭 확장
- 전반적인 코드 구조 개선 및 타입 안정성 강화

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-07-14 15:47:46 +09:00
parent 2f60ef585a
commit 111c519883
39 changed files with 2376 additions and 1231 deletions

View File

@@ -137,10 +137,7 @@ class NotificationService {
// 각 구독에 대해 알림 재설정
for (final subscription in subscriptions) {
await schedulePaymentReminder(
id: subscription.id.hashCode,
serviceName: subscription.serviceName,
amount: subscription.monthlyCost,
billingDate: subscription.nextBillingDate,
subscription: subscription,
reminderDays: reminderDays,
reminderHour: reminderHour,
reminderMinute: reminderMinute,
@@ -421,10 +418,7 @@ class NotificationService {
}
static Future<void> schedulePaymentReminder({
required int id,
required String serviceName,
required double amount,
required DateTime billingDate,
required SubscriptionModel subscription,
int reminderDays = 3,
int reminderHour = 10,
int reminderMinute = 0,
@@ -457,7 +451,7 @@ class NotificationService {
// 기본 알림 예약 (지정된 일수 전)
final scheduledDate =
billingDate.subtract(Duration(days: reminderDays)).copyWith(
subscription.nextBillingDate.subtract(Duration(days: reminderDays)).copyWith(
hour: reminderHour,
minute: reminderMinute,
second: 0,
@@ -471,10 +465,27 @@ class NotificationService {
daysText = '내일';
}
// 이벤트 종료로 인한 가격 변동 확인
String notificationBody;
if (subscription.isEventActive &&
subscription.eventEndDate != null &&
subscription.eventEndDate!.isBefore(subscription.nextBillingDate) &&
subscription.eventEndDate!.isAfter(DateTime.now())) {
// 이벤트가 결제일 전에 종료되는 경우
final eventPrice = subscription.eventPrice ?? subscription.monthlyCost;
final normalPrice = subscription.monthlyCost;
notificationBody = '${subscription.serviceName} 구독료 ${normalPrice.toStringAsFixed(0)}원이 $daysText 결제 예정입니다.\n'
'⚠️ 이벤트 종료로 가격이 ${eventPrice.toStringAsFixed(0)}원에서 ${normalPrice.toStringAsFixed(0)}원으로 변경됩니다.';
} else {
// 일반 알림
final currentPrice = subscription.currentPrice;
notificationBody = '${subscription.serviceName} 구독료 ${currentPrice.toStringAsFixed(0)}원이 $daysText 결제 예정입니다.';
}
await _notifications.zonedSchedule(
id,
subscription.id.hashCode,
'구독 결제 예정 알림',
'$serviceName 구독료 ${amount.toStringAsFixed(0)}원이 $daysText 결제 예정입니다.',
notificationBody,
tz.TZDateTime.from(scheduledDate, location),
const NotificationDetails(
android: AndroidNotificationDetails(
@@ -495,7 +506,7 @@ class NotificationService {
if (isDailyReminder && reminderDays >= 2) {
// 첫 번째 알림 다음 날부터 결제일 전날까지 매일 알림 예약
for (int i = reminderDays - 1; i >= 1; i--) {
final dailyDate = billingDate.subtract(Duration(days: i)).copyWith(
final dailyDate = subscription.nextBillingDate.subtract(Duration(days: i)).copyWith(
hour: reminderHour,
minute: reminderMinute,
second: 0,
@@ -509,10 +520,25 @@ class NotificationService {
remainingDaysText = '내일';
}
// 각 날짜에 대한 이벤트 종료 확인
String dailyNotificationBody;
if (subscription.isEventActive &&
subscription.eventEndDate != null &&
subscription.eventEndDate!.isBefore(subscription.nextBillingDate) &&
subscription.eventEndDate!.isAfter(DateTime.now())) {
final eventPrice = subscription.eventPrice ?? subscription.monthlyCost;
final normalPrice = subscription.monthlyCost;
dailyNotificationBody = '${subscription.serviceName} 구독료 ${normalPrice.toStringAsFixed(0)}원이 $remainingDaysText 결제 예정입니다.\n'
'⚠️ 이벤트 종료로 가격이 ${eventPrice.toStringAsFixed(0)}원에서 ${normalPrice.toStringAsFixed(0)}원으로 변경됩니다.';
} else {
final currentPrice = subscription.currentPrice;
dailyNotificationBody = '${subscription.serviceName} 구독료 ${currentPrice.toStringAsFixed(0)}원이 $remainingDaysText 결제 예정입니다.';
}
await _notifications.zonedSchedule(
id + i, // 고유한 ID 생성을 위해 날짜 차이 더함
subscription.id.hashCode + i, // 고유한 ID 생성을 위해 날짜 차이 더함
'구독 결제 예정 알림',
'$serviceName 구독료 ${amount.toStringAsFixed(0)}원이 $remainingDaysText 결제 예정입니다.',
dailyNotificationBody,
tz.TZDateTime.from(dailyDate, location),
const NotificationDetails(
android: AndroidNotificationDetails(