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:
@@ -4,11 +4,15 @@ import 'package:intl/intl.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/overview/controllers/overview_controller.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
// MockDataService 제거 - 실제 API 사용
|
||||
import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/services/health_check_service.dart';
|
||||
import 'package:superport/core/widgets/auth_guard.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 대시보드 화면
|
||||
class OverviewScreenRedesign extends StatefulWidget {
|
||||
const OverviewScreenRedesign({Key? key}) : super(key: key);
|
||||
const OverviewScreenRedesign({super.key});
|
||||
|
||||
@override
|
||||
State<OverviewScreenRedesign> createState() => _OverviewScreenRedesignState();
|
||||
@@ -16,21 +20,44 @@ class OverviewScreenRedesign extends StatefulWidget {
|
||||
|
||||
class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
late final OverviewController _controller;
|
||||
late final HealthCheckService _healthCheckService;
|
||||
Map<String, dynamic>? _healthStatus;
|
||||
bool _isHealthCheckLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = OverviewController();
|
||||
_healthCheckService = HealthCheckService();
|
||||
_loadData();
|
||||
_checkHealthStatus();
|
||||
// 주기적인 헬스체크 시작 (30초마다)
|
||||
_healthCheckService.startPeriodicHealthCheck();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
await _controller.loadDashboardData();
|
||||
}
|
||||
|
||||
Future<void> _checkHealthStatus() async {
|
||||
setState(() {
|
||||
_isHealthCheckLoading = true;
|
||||
});
|
||||
|
||||
final result = await _healthCheckService.checkHealth();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_healthStatus = result;
|
||||
_isHealthCheckLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_healthCheckService.stopPeriodicHealthCheck();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -70,7 +97,13 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('안녕하세요, 관리자님! 👋', style: ShadcnTheme.headingH3),
|
||||
FutureBuilder<AuthUser?>(
|
||||
future: context.read<AuthService>().getCurrentUser(),
|
||||
builder: (context, snapshot) {
|
||||
final userName = snapshot.data?.name ?? '사용자';
|
||||
return Text('안녕하세요, $userName님! 👋', style: ShadcnTheme.headingH3);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'오늘의 포트 운영 현황을 확인해보세요.',
|
||||
@@ -333,51 +366,79 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
}
|
||||
|
||||
Widget _buildRightColumn() {
|
||||
return Column(
|
||||
children: [
|
||||
// 빠른 작업
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('빠른 작업', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 16),
|
||||
_buildQuickActionButton(Icons.add_box, '장비 입고', '새 장비 등록'),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActionButton(
|
||||
Icons.local_shipping,
|
||||
'장비 출고',
|
||||
'장비 대여 처리',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActionButton(
|
||||
Icons.business_center,
|
||||
'회사 등록',
|
||||
'새 회사 추가',
|
||||
),
|
||||
],
|
||||
return FutureBuilder<AuthUser?>(
|
||||
future: context.read<AuthService>().getCurrentUser(),
|
||||
builder: (context, snapshot) {
|
||||
final userRole = snapshot.data?.role?.toLowerCase() ?? '';
|
||||
final isAdminOrManager = userRole == 'admin' || userRole == 'manager';
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 빠른 작업 (Admin과 Manager만 표시)
|
||||
if (isAdminOrManager) ...[
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('빠른 작업', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 16),
|
||||
_buildQuickActionButton(Icons.add_box, '장비 입고', '새 장비 등록'),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActionButton(
|
||||
Icons.local_shipping,
|
||||
'장비 출고',
|
||||
'장비 대여 처리',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActionButton(
|
||||
Icons.business_center,
|
||||
'회사 등록',
|
||||
'새 회사 추가',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 시스템 상태
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('시스템 상태', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusItem('서버 상태', '정상'),
|
||||
_buildStatusItem('데이터베이스', '정상'),
|
||||
_buildStatusItem('네트워크', '정상'),
|
||||
_buildStatusItem('백업', '완료'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
// 시스템 상태 (실시간 헬스체크)
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('시스템 상태', style: ShadcnTheme.headingH4),
|
||||
IconButton(
|
||||
icon: _isHealthCheckLoading
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(ShadcnTheme.primary),
|
||||
),
|
||||
)
|
||||
: Icon(Icons.refresh, size: 20, color: ShadcnTheme.muted),
|
||||
onPressed: _isHealthCheckLoading ? null : _checkHealthStatus,
|
||||
tooltip: '새로고침',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildHealthStatusItem('서버 상태', _getServerStatus()),
|
||||
_buildHealthStatusItem('데이터베이스', _getDatabaseStatus()),
|
||||
_buildHealthStatusItem('API 응답', _getApiResponseTime()),
|
||||
_buildHealthStatusItem('최종 체크', _getLastCheckTime()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -664,4 +725,151 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 헬스 상태 아이템 빌더
|
||||
Widget _buildHealthStatusItem(String label, Map<String, dynamic> statusInfo) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodyMedium),
|
||||
Row(
|
||||
children: [
|
||||
if (statusInfo['icon'] != null) ...[
|
||||
Icon(
|
||||
statusInfo['icon'] as IconData,
|
||||
size: 16,
|
||||
color: statusInfo['color'] as Color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
ShadcnBadge(
|
||||
text: statusInfo['text'] as String,
|
||||
variant: statusInfo['variant'] as ShadcnBadgeVariant,
|
||||
size: ShadcnBadgeSize.small,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 서버 상태 가져오기
|
||||
Map<String, dynamic> _getServerStatus() {
|
||||
if (_healthStatus == null) {
|
||||
return {
|
||||
'text': '확인 중',
|
||||
'variant': ShadcnBadgeVariant.secondary,
|
||||
'icon': Icons.pending,
|
||||
'color': ShadcnTheme.muted,
|
||||
};
|
||||
}
|
||||
|
||||
final isHealthy = _healthStatus!['success'] == true &&
|
||||
_healthStatus!['data']?['status'] == 'healthy';
|
||||
|
||||
return {
|
||||
'text': isHealthy ? '정상' : '오류',
|
||||
'variant': isHealthy ? ShadcnBadgeVariant.success : ShadcnBadgeVariant.destructive,
|
||||
'icon': isHealthy ? Icons.check_circle : Icons.error,
|
||||
'color': isHealthy ? ShadcnTheme.success : ShadcnTheme.destructive,
|
||||
};
|
||||
}
|
||||
|
||||
/// 데이터베이스 상태 가져오기
|
||||
Map<String, dynamic> _getDatabaseStatus() {
|
||||
if (_healthStatus == null) {
|
||||
return {
|
||||
'text': '확인 중',
|
||||
'variant': ShadcnBadgeVariant.secondary,
|
||||
'icon': Icons.pending,
|
||||
'color': ShadcnTheme.muted,
|
||||
};
|
||||
}
|
||||
|
||||
final dbStatus = _healthStatus!['data']?['database']?['status'] ?? 'unknown';
|
||||
final isConnected = dbStatus == 'connected';
|
||||
|
||||
return {
|
||||
'text': isConnected ? '연결됨' : '연결 안됨',
|
||||
'variant': isConnected ? ShadcnBadgeVariant.success : ShadcnBadgeVariant.warning,
|
||||
'icon': isConnected ? Icons.storage : Icons.cloud_off,
|
||||
'color': isConnected ? ShadcnTheme.success : ShadcnTheme.warning,
|
||||
};
|
||||
}
|
||||
|
||||
/// API 응답 시간 가져오기
|
||||
Map<String, dynamic> _getApiResponseTime() {
|
||||
if (_healthStatus == null) {
|
||||
return {
|
||||
'text': '측정 중',
|
||||
'variant': ShadcnBadgeVariant.secondary,
|
||||
'icon': Icons.timer,
|
||||
'color': ShadcnTheme.muted,
|
||||
};
|
||||
}
|
||||
|
||||
final responseTime = _healthStatus!['data']?['responseTime'] ?? 0;
|
||||
final timeMs = responseTime is num ? responseTime : 0;
|
||||
|
||||
ShadcnBadgeVariant variant;
|
||||
Color color;
|
||||
if (timeMs < 100) {
|
||||
variant = ShadcnBadgeVariant.success;
|
||||
color = ShadcnTheme.success;
|
||||
} else if (timeMs < 500) {
|
||||
variant = ShadcnBadgeVariant.warning;
|
||||
color = ShadcnTheme.warning;
|
||||
} else {
|
||||
variant = ShadcnBadgeVariant.destructive;
|
||||
color = ShadcnTheme.destructive;
|
||||
}
|
||||
|
||||
return {
|
||||
'text': '${timeMs}ms',
|
||||
'variant': variant,
|
||||
'icon': Icons.speed,
|
||||
'color': color,
|
||||
};
|
||||
}
|
||||
|
||||
/// 마지막 체크 시간 가져오기
|
||||
Map<String, dynamic> _getLastCheckTime() {
|
||||
if (_healthStatus == null) {
|
||||
return {
|
||||
'text': '없음',
|
||||
'variant': ShadcnBadgeVariant.secondary,
|
||||
'icon': Icons.access_time,
|
||||
'color': ShadcnTheme.muted,
|
||||
};
|
||||
}
|
||||
|
||||
final timestamp = _healthStatus!['data']?['timestamp'];
|
||||
if (timestamp != null) {
|
||||
try {
|
||||
final date = DateTime.parse(timestamp);
|
||||
final formatter = DateFormat('HH:mm:ss');
|
||||
return {
|
||||
'text': formatter.format(date),
|
||||
'variant': ShadcnBadgeVariant.outline,
|
||||
'icon': Icons.access_time,
|
||||
'color': ShadcnTheme.foreground,
|
||||
};
|
||||
} catch (e) {
|
||||
// 파싱 실패
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 시간 사용
|
||||
final now = DateTime.now();
|
||||
final formatter = DateFormat('HH:mm:ss');
|
||||
return {
|
||||
'text': formatter.format(now),
|
||||
'variant': ShadcnBadgeVariant.outline,
|
||||
'icon': Icons.access_time,
|
||||
'color': ShadcnTheme.foreground,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user