refactor: Clean Architecture 적용 및 코드베이스 전면 리팩토링
## 주요 변경사항 ### 아키텍처 개선 - Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리) - Use Case 패턴 도입으로 비즈니스 로직 캡슐화 - Repository 패턴으로 데이터 접근 추상화 - 의존성 주입 구조 개선 ### 상태 관리 최적화 - 모든 Controller에서 불필요한 상태 관리 로직 제거 - 페이지네이션 로직 통일 및 간소화 - 에러 처리 로직 개선 (에러 메시지 한글화) - 로딩 상태 관리 최적화 ### Mock 서비스 제거 - MockDataService 완전 제거 - 모든 화면을 실제 API 전용으로 전환 - 불필요한 Mock 관련 코드 정리 ### UI/UX 개선 - Overview 화면 대시보드 기능 강화 - 라이선스 만료 알림 위젯 추가 - 사이드바 네비게이션 개선 - 일관된 UI 컴포넌트 사용 ### 코드 품질 - 중복 코드 제거 및 함수 추출 - 파일별 책임 분리 명확화 - 테스트 코드 업데이트 ## 영향 범위 - 모든 화면의 Controller 리팩토링 - API 통신 레이어 구조 개선 - 에러 처리 및 로깅 시스템 개선 ## 향후 계획 - 단위 테스트 커버리지 확대 - 통합 테스트 시나리오 추가 - 성능 모니터링 도구 통합
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/overview/overview_screen_redesign.dart';
|
||||
@@ -10,6 +11,7 @@ import 'package:superport/screens/license/license_list_redesign.dart';
|
||||
import 'package:superport/screens/warehouse_location/warehouse_location_list_redesign.dart';
|
||||
import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/services/dashboard_service.dart';
|
||||
import 'package:superport/services/lookup_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
|
||||
@@ -34,6 +36,7 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
AuthUser? _currentUser;
|
||||
late final AuthService _authService;
|
||||
late final DashboardService _dashboardService;
|
||||
late final LookupService _lookupService;
|
||||
late Animation<double> _sidebarAnimation;
|
||||
int _expiringLicenseCount = 0; // 30일 내 만료 예정 라이선스 수
|
||||
|
||||
@@ -50,8 +53,10 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
_setupAnimations();
|
||||
_authService = GetIt.instance<AuthService>();
|
||||
_dashboardService = GetIt.instance<DashboardService>();
|
||||
_lookupService = GetIt.instance<LookupService>();
|
||||
_loadCurrentUser();
|
||||
_loadLicenseExpirySummary();
|
||||
_initializeLookupData(); // Lookup 데이터 초기화
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentUser() async {
|
||||
@@ -92,6 +97,38 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
print('[ERROR] 스택 트레이스: ${StackTrace.current}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup 데이터 초기화 (앱 시작 시 한 번만 호출)
|
||||
Future<void> _initializeLookupData() async {
|
||||
try {
|
||||
print('[DEBUG] Lookup 데이터 초기화 시작...');
|
||||
|
||||
// 캐시가 유효하지 않을 때만 로드
|
||||
if (!_lookupService.isCacheValid) {
|
||||
await _lookupService.loadAllLookups();
|
||||
|
||||
if (_lookupService.hasData) {
|
||||
print('[DEBUG] Lookup 데이터 로드 성공!');
|
||||
print('[DEBUG] - 장비 타입: ${_lookupService.equipmentTypes.length}개');
|
||||
print('[DEBUG] - 장비 상태: ${_lookupService.equipmentStatuses.length}개');
|
||||
print('[DEBUG] - 라이선스 타입: ${_lookupService.licenseTypes.length}개');
|
||||
print('[DEBUG] - 제조사: ${_lookupService.manufacturers.length}개');
|
||||
print('[DEBUG] - 사용자 역할: ${_lookupService.userRoles.length}개');
|
||||
print('[DEBUG] - 회사 상태: ${_lookupService.companyStatuses.length}개');
|
||||
} else {
|
||||
print('[WARNING] Lookup 데이터가 비어있습니다.');
|
||||
}
|
||||
} else {
|
||||
print('[DEBUG] Lookup 데이터 캐시 사용 (유효)');
|
||||
}
|
||||
|
||||
if (_lookupService.error != null) {
|
||||
print('[ERROR] Lookup 데이터 로드 실패: ${_lookupService.error}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('[ERROR] Lookup 데이터 초기화 중 예외 발생: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_sidebarAnimationController = AnimationController(
|
||||
@@ -222,81 +259,84 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isWideScreen = screenWidth >= 1920;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: ShadcnTheme.backgroundSecondary,
|
||||
body: Column(
|
||||
children: [
|
||||
// F-Pattern: 1차 시선 - 상단 헤더
|
||||
_buildTopHeader(),
|
||||
return Provider<AuthService>.value(
|
||||
value: _authService,
|
||||
child: Scaffold(
|
||||
backgroundColor: ShadcnTheme.backgroundSecondary,
|
||||
body: Column(
|
||||
children: [
|
||||
// F-Pattern: 1차 시선 - 상단 헤더
|
||||
_buildTopHeader(),
|
||||
|
||||
// 메인 콘텐츠 영역
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// 좌측 사이드바
|
||||
AnimatedBuilder(
|
||||
animation: _sidebarAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: _sidebarAnimation.value,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: ShadcnTheme.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildSidebar(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 메인 콘텐츠 (최대 너비 제한)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isWideScreen ? _maxContentWidth : double.infinity,
|
||||
),
|
||||
padding: EdgeInsets.all(
|
||||
isWideScreen ? ShadcnTheme.spacing6 : ShadcnTheme.spacing4
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// F-Pattern: 2차 시선 - 페이지 헤더 + 액션
|
||||
_buildPageHeader(),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// F-Pattern: 주요 작업 영역
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
||||
border: Border.all(
|
||||
color: ShadcnTheme.border,
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: ShadcnTheme.shadowSm,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg - 1),
|
||||
child: _getContentForRoute(_currentRoute),
|
||||
),
|
||||
// 메인 콘텐츠 영역
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// 좌측 사이드바
|
||||
AnimatedBuilder(
|
||||
animation: _sidebarAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: _sidebarAnimation.value,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: ShadcnTheme.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _buildSidebar(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 메인 콘텐츠 (최대 너비 제한)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isWideScreen ? _maxContentWidth : double.infinity,
|
||||
),
|
||||
padding: EdgeInsets.all(
|
||||
isWideScreen ? ShadcnTheme.spacing6 : ShadcnTheme.spacing4
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// F-Pattern: 2차 시선 - 페이지 헤더 + 액션
|
||||
_buildPageHeader(),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// F-Pattern: 주요 작업 영역
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
||||
border: Border.all(
|
||||
color: ShadcnTheme.border,
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: ShadcnTheme.shadowSm,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg - 1),
|
||||
child: _getContentForRoute(_currentRoute),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ class AppThemeTailwind {
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: primary,
|
||||
secondary: secondary,
|
||||
background: surface,
|
||||
surface: cardBackground,
|
||||
surface: surface,
|
||||
error: danger,
|
||||
),
|
||||
scaffoldBackgroundColor: surface,
|
||||
|
||||
@@ -117,7 +117,6 @@ class _AddressInputState extends State<AddressInput> {
|
||||
/// 시/도 드롭다운 오버레이를 표시합니다.
|
||||
void _showRegionOverlay() {
|
||||
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
final availableHeight =
|
||||
@@ -142,7 +141,7 @@ class _AddressInputState extends State<AddressInput> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
|
||||
Reference in New Issue
Block a user