Refactor screens to MVC architecture with modular widgets

- Extract business logic from screens into dedicated controllers
- Split large screen files into smaller, reusable widget components
- Add controllers for AddSubscriptionScreen and DetailScreen
- Create modular widgets for subscription and detail features
- Improve code organization and maintainability
- Remove duplicated code and improve reusability

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-07-11 00:21:18 +09:00
parent 4731288622
commit 83c5e3d64e
56 changed files with 9092 additions and 4579 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,7 @@ class AppLockScreen extends StatelessWidget {
onPressed: () async {
final appLock = context.read<AppLockProvider>();
final success = await appLock.authenticate();
if (!success) {
if (!success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('인증에 실패했습니다. 다시 시도해주세요.'),

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/category_provider.dart';
import '../models/category_model.dart';
class CategoryManagementScreen extends StatefulWidget {
const CategoryManagementScreen({super.key});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../providers/subscription_provider.dart';
import '../providers/app_lock_provider.dart';
import '../providers/navigation_provider.dart';
import '../theme/app_colors.dart';
@@ -13,7 +12,6 @@ import 'sms_scan_screen.dart';
import '../utils/animation_controller_helper.dart';
import '../widgets/floating_navigation_bar.dart';
import '../widgets/glassmorphic_scaffold.dart';
import '../widgets/glassmorphic_app_bar.dart';
import '../widgets/home_content.dart';
class MainScreen extends StatefulWidget {
@@ -33,7 +31,6 @@ class _MainScreenState extends State<MainScreen>
late AnimationController _waveController;
late ScrollController _scrollController;
late FloatingNavBarScrollController _navBarScrollController;
bool _isNavBarVisible = true;
// 화면 목록
late final List<Widget> _screens;
@@ -67,8 +64,8 @@ class _MainScreenState extends State<MainScreen>
_navBarScrollController = FloatingNavBarScrollController(
scrollController: _scrollController,
onHide: () => setState(() => _isNavBarVisible = false),
onShow: () => setState(() => _isNavBarVisible = true),
onHide: () {},
onShow: () {},
);
// 화면 목록 초기화
@@ -162,17 +159,18 @@ class _MainScreenState extends State<MainScreen>
// 구독이 성공적으로 추가된 경우
if (result == true) {
// 상단에 스낵바 표시
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
content: const Row(
children: [
const Icon(
Icon(
Icons.check_circle,
color: Colors.white,
size: 20,
),
const SizedBox(width: 12),
const Text(
SizedBox(width: 12),
Text(
'구독이 추가되었습니다',
style: TextStyle(
fontSize: 16,

View File

@@ -1,24 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:provider/provider.dart';
import '../providers/app_lock_provider.dart';
import '../providers/notification_provider.dart';
import '../providers/subscription_provider.dart';
import '../providers/navigation_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:path/path.dart' as path;
import '../services/notification_service.dart';
import '../screens/sms_scan_screen.dart';
import 'package:url_launcher/url_launcher.dart';
import '../providers/theme_provider.dart';
import '../theme/adaptive_theme.dart';
import '../widgets/glassmorphic_scaffold.dart';
import '../widgets/glassmorphic_app_bar.dart';
import '../widgets/glassmorphism_card.dart';
import '../widgets/app_navigator.dart';
import '../theme/app_colors.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@@ -76,65 +65,7 @@ class SettingsScreen extends StatelessWidget {
);
}
Future<void> _backupData(BuildContext context) async {
try {
final provider = context.read<SubscriptionProvider>();
final subscriptions = provider.subscriptions;
// 임시 디렉토리에 백업 파일 생성
final tempDir = await getTemporaryDirectory();
final backupFile =
File(path.join(tempDir.path, 'submanager_backup.json'));
// 구독 데이터를 JSON 형식으로 저장
final jsonData = subscriptions
.map((sub) => {
'id': sub.id,
'serviceName': sub.serviceName,
'monthlyCost': sub.monthlyCost,
'billingCycle': sub.billingCycle,
'nextBillingDate': sub.nextBillingDate.toIso8601String(),
'isAutoDetected': sub.isAutoDetected,
'repeatCount': sub.repeatCount,
'lastPaymentDate': sub.lastPaymentDate?.toIso8601String(),
})
.toList();
await backupFile.writeAsString(jsonData.toString());
// 파일 공유
await Share.shareXFiles(
[XFile(backupFile.path)],
text: 'SubManager 백업 파일',
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('백업 파일이 생성되었습니다')),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('백업 중 오류가 발생했습니다: $e')),
);
}
}
}
// SMS 스캔 화면으로 이동
void _navigateToSmsScan(BuildContext context) async {
final added = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (context) => const SmsScanScreen()),
);
if (added == true && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('구독이 성공적으로 추가되었습니다')),
);
}
}
@override
Widget build(BuildContext context) {
@@ -455,10 +386,10 @@ class SettingsScreen extends StatelessWidget {
),
// 데이터 관리
GlassmorphismCard(
const GlassmorphismCard(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(8),
child: Column(
child: const Column(
children: [
// 데이터 백업 기능 비활성화
// ListTile(

View File

@@ -7,11 +7,8 @@ import '../models/subscription.dart';
import '../models/subscription_model.dart';
import '../services/subscription_url_matcher.dart';
import 'package:intl/intl.dart'; // NumberFormat을 사용하기 위한 import 추가
import '../widgets/glassmorphic_scaffold.dart';
import '../widgets/glassmorphic_app_bar.dart';
import '../widgets/glassmorphism_card.dart';
import '../widgets/themed_text.dart';
import '../theme/app_colors.dart';
class SmsScanScreen extends StatefulWidget {
const SmsScanScreen({super.key});

View File

@@ -1,14 +1,8 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_lock_provider.dart';
import '../providers/navigation_provider.dart';
import '../theme/app_colors.dart';
import '../widgets/glassmorphism_card.dart';
import '../routes/app_routes.dart';
import 'app_lock_screen.dart';
import 'main_screen.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@@ -289,7 +283,7 @@ class _SplashScreenState extends State<SplashScreen>
BlendMode.srcIn,
shaderCallback:
(bounds) =>
LinearGradient(
const LinearGradient(
colors: AppColors
.blueGradient,
begin: