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:
@@ -11,7 +11,7 @@ import '../widgets/dialogs/delete_confirmation_dialog.dart';
|
||||
import '../widgets/common/snackbar/app_snackbar.dart';
|
||||
|
||||
/// DetailScreen의 비즈니스 로직을 관리하는 Controller
|
||||
class DetailScreenController {
|
||||
class DetailScreenController extends ChangeNotifier {
|
||||
final BuildContext context;
|
||||
final SubscriptionModel subscription;
|
||||
|
||||
@@ -22,16 +22,85 @@ class DetailScreenController {
|
||||
late TextEditingController eventPriceController;
|
||||
|
||||
// Form State
|
||||
late String billingCycle;
|
||||
late DateTime nextBillingDate;
|
||||
String? selectedCategoryId;
|
||||
late String currency;
|
||||
bool isLoading = false;
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
late String _billingCycle;
|
||||
late DateTime _nextBillingDate;
|
||||
String? _selectedCategoryId;
|
||||
late String _currency;
|
||||
bool _isLoading = false;
|
||||
|
||||
// Event State
|
||||
late bool isEventActive;
|
||||
DateTime? eventStartDate;
|
||||
DateTime? eventEndDate;
|
||||
late bool _isEventActive;
|
||||
DateTime? _eventStartDate;
|
||||
DateTime? _eventEndDate;
|
||||
|
||||
// Getters
|
||||
String get billingCycle => _billingCycle;
|
||||
DateTime get nextBillingDate => _nextBillingDate;
|
||||
String? get selectedCategoryId => _selectedCategoryId;
|
||||
String get currency => _currency;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isEventActive => _isEventActive;
|
||||
DateTime? get eventStartDate => _eventStartDate;
|
||||
DateTime? get eventEndDate => _eventEndDate;
|
||||
|
||||
// Setters
|
||||
set billingCycle(String value) {
|
||||
if (_billingCycle != value) {
|
||||
_billingCycle = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set nextBillingDate(DateTime value) {
|
||||
if (_nextBillingDate != value) {
|
||||
_nextBillingDate = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set selectedCategoryId(String? value) {
|
||||
if (_selectedCategoryId != value) {
|
||||
_selectedCategoryId = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set currency(String value) {
|
||||
if (_currency != value) {
|
||||
_currency = value;
|
||||
_updateMonthlyCostFormat();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set isLoading(bool value) {
|
||||
if (_isLoading != value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set isEventActive(bool value) {
|
||||
if (_isEventActive != value) {
|
||||
_isEventActive = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set eventStartDate(DateTime? value) {
|
||||
if (_eventStartDate != value) {
|
||||
_eventStartDate = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set eventEndDate(DateTime? value) {
|
||||
if (_eventEndDate != value) {
|
||||
_eventEndDate = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Focus Nodes
|
||||
final serviceNameFocus = FocusNode();
|
||||
@@ -70,15 +139,15 @@ class DetailScreenController {
|
||||
eventPriceController = TextEditingController();
|
||||
|
||||
// Form State 초기화
|
||||
billingCycle = subscription.billingCycle;
|
||||
nextBillingDate = subscription.nextBillingDate;
|
||||
selectedCategoryId = subscription.categoryId;
|
||||
currency = subscription.currency;
|
||||
_billingCycle = subscription.billingCycle;
|
||||
_nextBillingDate = subscription.nextBillingDate;
|
||||
_selectedCategoryId = subscription.categoryId;
|
||||
_currency = subscription.currency;
|
||||
|
||||
// Event State 초기화
|
||||
isEventActive = subscription.isEventActive;
|
||||
eventStartDate = subscription.eventStartDate;
|
||||
eventEndDate = subscription.eventEndDate;
|
||||
_isEventActive = subscription.isEventActive;
|
||||
_eventStartDate = subscription.eventStartDate;
|
||||
_eventEndDate = subscription.eventEndDate;
|
||||
|
||||
// 이벤트 가격 초기화
|
||||
if (subscription.eventPrice != null) {
|
||||
@@ -137,6 +206,7 @@ class DetailScreenController {
|
||||
}
|
||||
|
||||
/// 리소스 정리
|
||||
@override
|
||||
void dispose() {
|
||||
// Controllers
|
||||
serviceNameController.dispose();
|
||||
@@ -158,11 +228,13 @@ class DetailScreenController {
|
||||
|
||||
// Scroll
|
||||
scrollController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 통화 단위에 따른 금액 표시 형식 업데이트
|
||||
void _updateMonthlyCostFormat() {
|
||||
if (currency == 'KRW') {
|
||||
if (_currency == 'KRW') {
|
||||
// 원화는 소수점 없이 표시
|
||||
final intValue = subscription.monthlyCost.toInt();
|
||||
monthlyCostController.text = NumberFormat.decimalPattern().format(intValue);
|
||||
@@ -197,7 +269,7 @@ class DetailScreenController {
|
||||
serviceName.contains('coupang play') ||
|
||||
serviceName.contains('쿠팡플레이')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '엔터테인먼트',
|
||||
(cat) => cat.name == 'OTT 서비스',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -209,7 +281,7 @@ class DetailScreenController {
|
||||
serviceName.contains('플로') ||
|
||||
serviceName.contains('벅스')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '음악',
|
||||
(cat) => cat.name == '음악 서비스',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -222,18 +294,18 @@ class DetailScreenController {
|
||||
serviceName.contains('icloud') ||
|
||||
serviceName.contains('adobe')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '생산성',
|
||||
(cat) => cat.name == '오피스/협업 툴',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
// 게임 관련 키워드
|
||||
else if (serviceName.contains('xbox') ||
|
||||
serviceName.contains('playstation') ||
|
||||
serviceName.contains('nintendo') ||
|
||||
serviceName.contains('steam') ||
|
||||
serviceName.contains('게임')) {
|
||||
// AI 관련 키워드
|
||||
else if (serviceName.contains('chatgpt') ||
|
||||
serviceName.contains('claude') ||
|
||||
serviceName.contains('gemini') ||
|
||||
serviceName.contains('copilot') ||
|
||||
serviceName.contains('midjourney')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '게임',
|
||||
(cat) => cat.name == 'AI 서비스',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -244,7 +316,7 @@ class DetailScreenController {
|
||||
serviceName.contains('패스트캠퍼스') ||
|
||||
serviceName.contains('클래스101')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '교육',
|
||||
(cat) => cat.name == '프로그래밍/개발',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -255,7 +327,7 @@ class DetailScreenController {
|
||||
serviceName.contains('네이버') ||
|
||||
serviceName.contains('11번가')) {
|
||||
matchedCategory = categories.firstWhere(
|
||||
(cat) => cat.name == '쇼핑',
|
||||
(cat) => cat.name == '기타 서비스',
|
||||
orElse: () => categories.first,
|
||||
);
|
||||
}
|
||||
@@ -267,6 +339,15 @@ class DetailScreenController {
|
||||
|
||||
/// 구독 정보 업데이트
|
||||
Future<void> updateSubscription() async {
|
||||
// Form 검증
|
||||
if (formKey.currentState != null && !formKey.currentState!.validate()) {
|
||||
AppSnackBar.showError(
|
||||
context: context,
|
||||
message: '필수 항목을 모두 입력해주세요',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final provider = Provider.of<SubscriptionProvider>(context, listen: false);
|
||||
|
||||
// 웹사이트 URL이 비어있는 경우 자동 매칭 다시 시도
|
||||
@@ -289,18 +370,18 @@ class DetailScreenController {
|
||||
subscription.serviceName = serviceNameController.text;
|
||||
subscription.monthlyCost = monthlyCost;
|
||||
subscription.websiteUrl = websiteUrl;
|
||||
subscription.billingCycle = billingCycle;
|
||||
subscription.nextBillingDate = nextBillingDate;
|
||||
subscription.categoryId = selectedCategoryId;
|
||||
subscription.currency = currency;
|
||||
subscription.billingCycle = _billingCycle;
|
||||
subscription.nextBillingDate = _nextBillingDate;
|
||||
subscription.categoryId = _selectedCategoryId;
|
||||
subscription.currency = _currency;
|
||||
|
||||
// 이벤트 정보 업데이트
|
||||
subscription.isEventActive = isEventActive;
|
||||
subscription.eventStartDate = eventStartDate;
|
||||
subscription.eventEndDate = eventEndDate;
|
||||
subscription.isEventActive = _isEventActive;
|
||||
subscription.eventStartDate = _eventStartDate;
|
||||
subscription.eventEndDate = _eventEndDate;
|
||||
|
||||
// 이벤트 가격 파싱
|
||||
if (isEventActive && eventPriceController.text.isNotEmpty) {
|
||||
if (_isEventActive && eventPriceController.text.isNotEmpty) {
|
||||
try {
|
||||
subscription.eventPrice =
|
||||
double.parse(eventPriceController.text.replaceAll(',', ''));
|
||||
@@ -345,7 +426,7 @@ class DetailScreenController {
|
||||
await provider.deleteSubscription(subscription.id);
|
||||
|
||||
if (context.mounted) {
|
||||
AppSnackBar.showSuccess(
|
||||
AppSnackBar.showError(
|
||||
context: context,
|
||||
message: '구독이 삭제되었습니다.',
|
||||
icon: Icons.delete_forever_rounded,
|
||||
|
||||
Reference in New Issue
Block a user