Files
submanager/lib/widgets/sms_scan/subscription_card_widget.dart
JiWoong Sul 186d1bbf66 feat: SMS 스캔 화면 리팩토링 및 MVC 패턴 적용
- SMS 스캔 화면을 컨트롤러/서비스/위젯으로 분리
- 코드 가독성 및 유지보수성 향상
- 새로운 다국어 지원 키 추가
- Git 커밋 가이드라인 문서화
2025-07-17 16:59:19 +09:00

295 lines
9.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../models/subscription.dart';
import '../../providers/category_provider.dart';
import '../../providers/locale_provider.dart';
import '../../widgets/glassmorphism_card.dart';
import '../../widgets/themed_text.dart';
import '../../widgets/common/buttons/primary_button.dart';
import '../../widgets/common/buttons/secondary_button.dart';
import '../../widgets/common/form_fields/base_text_field.dart';
import '../../widgets/common/form_fields/category_selector.dart';
import '../../widgets/common/snackbar/app_snackbar.dart';
import '../../widgets/native_ad_widget.dart';
import '../../theme/app_colors.dart';
import '../../services/currency_util.dart';
import '../../utils/sms_scan/date_formatter.dart';
import '../../utils/sms_scan/category_icon_mapper.dart';
import '../../l10n/app_localizations.dart';
class SubscriptionCardWidget extends StatefulWidget {
final Subscription subscription;
final TextEditingController websiteUrlController;
final String? selectedCategoryId;
final Function(String?) onCategoryChanged;
final VoidCallback onAdd;
final VoidCallback onSkip;
const SubscriptionCardWidget({
super.key,
required this.subscription,
required this.websiteUrlController,
this.selectedCategoryId,
required this.onCategoryChanged,
required this.onAdd,
required this.onSkip,
});
@override
State<SubscriptionCardWidget> createState() => _SubscriptionCardWidgetState();
}
class _SubscriptionCardWidgetState extends State<SubscriptionCardWidget> {
@override
void initState() {
super.initState();
// URL 필드 자동 설정
if (widget.websiteUrlController.text.isEmpty && widget.subscription.websiteUrl != null) {
widget.websiteUrlController.text = widget.subscription.websiteUrl!;
}
}
void _handleCardTap() {
// 디버그 로그 추가
print('[SubscriptionCard] Card tapped! Service: ${widget.subscription.serviceName}');
// 구독 카드 클릭 시 처리
AppSnackBar.showInfo(
context: context,
message: '이 기능은 곧 출시됩니다', // 임시로 하드코딩
icon: Icons.info_outline,
);
}
@override
Widget build(BuildContext context) {
final categoryProvider = Provider.of<CategoryProvider>(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 광고 위젯 추가
const NativeAdWidget(key: ValueKey('sms_scan_result_ad')),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 구독 정보 카드
GlassmorphismCard(
width: double.infinity,
padding: EdgeInsets.zero,
child: Column(
children: [
// 클릭 가능한 정보 영역
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleCardTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _buildInfoSection(categoryProvider),
),
),
// 구분선
Container(
height: 1,
color: AppColors.navyGray.withValues(alpha: 0.1),
),
// 클릭 불가능한 액션 영역
Padding(
padding: const EdgeInsets.all(16.0),
child: _buildActionSection(categoryProvider),
),
],
),
),
],
),
),
],
);
}
// 정보 섹션 (클릭 가능)
Widget _buildInfoSection(CategoryProvider categoryProvider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ThemedText(
AppLocalizations.of(context).foundSubscription,
fontSize: 18,
fontWeight: FontWeight.bold,
forceDark: true,
),
const SizedBox(height: 24),
// 서비스명
ThemedText(
AppLocalizations.of(context).serviceName,
fontWeight: FontWeight.w500,
opacity: 0.7,
forceDark: true,
),
const SizedBox(height: 4),
ThemedText(
widget.subscription.serviceName,
fontSize: 22,
fontWeight: FontWeight.bold,
forceDark: true,
),
const SizedBox(height: 16),
// 금액 및 결제 주기
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ThemedText(
AppLocalizations.of(context).monthlyCost,
fontWeight: FontWeight.w500,
opacity: 0.7,
forceDark: true,
),
const SizedBox(height: 4),
// 언어별 통화 표시
FutureBuilder<String>(
future: CurrencyUtil.formatAmountWithLocale(
widget.subscription.monthlyCost,
widget.subscription.currency,
context.read<LocaleProvider>().locale.languageCode,
),
builder: (context, snapshot) {
return ThemedText(
snapshot.data ?? '-',
fontSize: 18,
fontWeight: FontWeight.bold,
forceDark: true,
);
},
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ThemedText(
AppLocalizations.of(context).billingCycle,
fontWeight: FontWeight.w500,
opacity: 0.7,
forceDark: true,
),
const SizedBox(height: 4),
ThemedText(
widget.subscription.billingCycle,
fontSize: 16,
fontWeight: FontWeight.w500,
forceDark: true,
),
],
),
),
],
),
const SizedBox(height: 16),
// 다음 결제일
ThemedText(
AppLocalizations.of(context).nextBillingDateLabel,
fontWeight: FontWeight.w500,
opacity: 0.7,
forceDark: true,
),
const SizedBox(height: 4),
ThemedText(
SmsDateFormatter.getNextBillingText(
context,
widget.subscription.nextBillingDate,
widget.subscription.billingCycle,
),
fontSize: 16,
fontWeight: FontWeight.w500,
forceDark: true,
),
],
);
}
// 액션 섹션 (클릭 불가능)
Widget _buildActionSection(CategoryProvider categoryProvider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 카테고리 선택
ThemedText(
AppLocalizations.of(context).category,
fontWeight: FontWeight.w500,
opacity: 0.7,
forceDark: true,
),
const SizedBox(height: 8),
CategorySelector(
categories: categoryProvider.categories,
selectedCategoryId: widget.selectedCategoryId ?? widget.subscription.category,
onChanged: widget.onCategoryChanged,
baseColor: _getCategoryColor(categoryProvider),
isGlassmorphism: true,
),
const SizedBox(height: 24),
// 웹사이트 URL 입력 필드
BaseTextField(
controller: widget.websiteUrlController,
label: AppLocalizations.of(context).websiteUrlAuto,
hintText: AppLocalizations.of(context).websiteUrlHint,
prefixIcon: Icon(
Icons.language,
color: AppColors.navyGray,
),
style: TextStyle(
color: AppColors.darkNavy,
),
fillColor: AppColors.pureWhite.withValues(alpha: 0.8),
),
const SizedBox(height: 32),
// 작업 버튼
Row(
children: [
Expanded(
child: SecondaryButton(
text: AppLocalizations.of(context).skip,
onPressed: widget.onSkip,
height: 48,
),
),
const SizedBox(width: 16),
Expanded(
child: PrimaryButton(
text: AppLocalizations.of(context).add,
onPressed: widget.onAdd,
height: 48,
),
),
],
),
],
);
}
Color? _getCategoryColor(CategoryProvider categoryProvider) {
final categoryId = widget.selectedCategoryId ?? widget.subscription.category;
if (categoryId == null) return null;
final category = categoryProvider.getCategoryById(categoryId);
if (category == null) return null;
return CategoryIconMapper.getCategoryColor(category);
}
}