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:
@@ -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(
|
||||
|
||||
@@ -49,6 +49,89 @@ class SubscriptionUrlMatcher {
|
||||
'타이달': 'https://www.tidal.com',
|
||||
};
|
||||
|
||||
// 저장 (클라우드/파일) 서비스
|
||||
static final Map<String, String> storageServices = {
|
||||
'google drive': 'https://www.google.com/drive/',
|
||||
'구글 드라이브': 'https://www.google.com/drive/',
|
||||
'dropbox': 'https://www.dropbox.com',
|
||||
'드롭박스': 'https://www.dropbox.com',
|
||||
'onedrive': 'https://www.onedrive.com',
|
||||
'원드라이브': 'https://www.onedrive.com',
|
||||
'icloud': 'https://www.icloud.com',
|
||||
'아이클라우드': 'https://www.icloud.com',
|
||||
'box': 'https://www.box.com',
|
||||
'박스': 'https://www.box.com',
|
||||
'pcloud': 'https://www.pcloud.com',
|
||||
'mega': 'https://mega.nz',
|
||||
'메가': 'https://mega.nz',
|
||||
'naver mybox': 'https://mybox.naver.com',
|
||||
'네이버 마이박스': 'https://mybox.naver.com',
|
||||
};
|
||||
|
||||
// 통신 · 인터넷 · TV 서비스
|
||||
static final Map<String, String> telecomServices = {
|
||||
'skt': 'https://www.sktelecom.com',
|
||||
'sk텔레콤': 'https://www.sktelecom.com',
|
||||
'kt': 'https://www.kt.com',
|
||||
'lgu+': 'https://www.lguplus.com',
|
||||
'lg유플러스': 'https://www.lguplus.com',
|
||||
'olleh tv': 'https://www.kt.com/olleh_tv',
|
||||
'올레 tv': 'https://www.kt.com/olleh_tv',
|
||||
'b tv': 'https://www.skbroadband.com',
|
||||
'비티비': 'https://www.skbroadband.com',
|
||||
'u+모바일tv': 'https://www.lguplus.com',
|
||||
'유플러스모바일tv': 'https://www.lguplus.com',
|
||||
};
|
||||
|
||||
// 생활/라이프스타일 서비스
|
||||
static final Map<String, String> lifestyleServices = {
|
||||
'네이버 플러스': 'https://plus.naver.com',
|
||||
'naver plus': 'https://plus.naver.com',
|
||||
'카카오 구독': 'https://subscribe.kakao.com',
|
||||
'kakao subscribe': 'https://subscribe.kakao.com',
|
||||
'쿠팡 와우': 'https://www.coupang.com/np/coupangplus',
|
||||
'coupang wow': 'https://www.coupang.com/np/coupangplus',
|
||||
'스타벅스 버디': 'https://www.starbucks.co.kr',
|
||||
'starbucks buddy': 'https://www.starbucks.co.kr',
|
||||
'cu 구독': 'https://cu.bgfretail.com',
|
||||
'gs25 구독': 'https://gs25.gsretail.com',
|
||||
'현대차 구독': 'https://www.hyundai.com/kr/ko/eco/vehicle-subscription',
|
||||
'lg전자 구독': 'https://www.lge.co.kr',
|
||||
'삼성전자 구독': 'https://www.samsung.com/sec',
|
||||
'다이슨 케어': 'https://www.dyson.co.kr',
|
||||
'dyson care': 'https://www.dyson.co.kr',
|
||||
'마켓컬리': 'https://www.kurly.com',
|
||||
'kurly': 'https://www.kurly.com',
|
||||
'헬로네이처': 'https://www.hellonature.com',
|
||||
'hello nature': 'https://www.hellonature.com',
|
||||
'이마트 트레이더스': 'https://www.emarttraders.co.kr',
|
||||
'홈플러스': 'https://www.homeplus.co.kr',
|
||||
'hellofresh': 'https://www.hellofresh.com',
|
||||
'헬로프레시': 'https://www.hellofresh.com',
|
||||
'bespoke post': 'https://www.bespokepost.com',
|
||||
};
|
||||
|
||||
// 쇼핑/이커머스 서비스
|
||||
static final Map<String, String> shoppingServices = {
|
||||
'amazon prime': 'https://www.amazon.com/prime',
|
||||
'아마존 프라임': 'https://www.amazon.com/prime',
|
||||
'walmart+': 'https://www.walmart.com/plus',
|
||||
'월마트플러스': 'https://www.walmart.com/plus',
|
||||
'chewy': 'https://www.chewy.com',
|
||||
'츄이': 'https://www.chewy.com',
|
||||
'dollar shave club': 'https://www.dollarshaveclub.com',
|
||||
'달러셰이브클럽': 'https://www.dollarshaveclub.com',
|
||||
'instacart': 'https://www.instacart.com',
|
||||
'인스타카트': 'https://www.instacart.com',
|
||||
'shipt': 'https://www.shipt.com',
|
||||
'십트': 'https://www.shipt.com',
|
||||
'grove': 'https://grove.co',
|
||||
'그로브': 'https://grove.co',
|
||||
'cratejoy': 'https://www.cratejoy.com',
|
||||
'shopify': 'https://www.shopify.com',
|
||||
'쇼피파이': 'https://www.shopify.com',
|
||||
};
|
||||
|
||||
// AI 서비스
|
||||
static final Map<String, String> aiServices = {
|
||||
'chatgpt': 'https://chat.openai.com',
|
||||
|
||||
Reference in New Issue
Block a user