Files
superport/lib/screens/overview/overview_screen_redesign.dart
JiWoong Sul c8dd1ff815
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled
refactor: 프로젝트 구조 개선 및 테스트 시스템 강화
주요 변경사항:
- CLAUDE.md: 프로젝트 규칙 v2.0으로 업데이트, 아키텍처 명확화
- 불필요한 문서 제거: NEXT_TASKS.md, TEST_PROGRESS.md, test_results 파일들
- 테스트 시스템 개선: 실제 API 테스트 스위트 추가 (15개 새 테스트 파일)
- License 관리: DTO 모델 개선, API 응답 처리 최적화
- 에러 처리: Interceptor 로직 강화, 상세 로깅 추가
- Company/User/Warehouse 테스트: 자동화 테스트 안정성 향상
- Phone Utils: 전화번호 포맷팅 로직 개선
- Overview Controller: 대시보드 데이터 로딩 최적화
- Analysis Options: Flutter 린트 규칙 추가

테스트 개선:
- company_real_api_test.dart: 실제 API 회사 관리 테스트
- equipment_in/out_real_api_test.dart: 장비 입출고 API 테스트
- license_real_api_test.dart: 라이선스 관리 API 테스트
- user_real_api_test.dart: 사용자 관리 API 테스트
- warehouse_location_real_api_test.dart: 창고 위치 API 테스트
- filter_sort_test.dart: 필터링/정렬 기능 테스트
- pagination_test.dart: 페이지네이션 테스트
- interactive_search_test.dart: 검색 기능 테스트
- overview_dashboard_test.dart: 대시보드 통합 테스트

코드 품질:
- 모든 서비스에 에러 처리 강화
- DTO 모델 null safety 개선
- 테스트 커버리지 확대
- 불필요한 로그 파일 제거로 리포지토리 정리

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 17:16:30 +09:00

586 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
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';
/// shadcn/ui 스타일로 재설계된 대시보드 화면
class OverviewScreenRedesign extends StatefulWidget {
const OverviewScreenRedesign({Key? key}) : super(key: key);
@override
State<OverviewScreenRedesign> createState() => _OverviewScreenRedesignState();
}
class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
late final OverviewController _controller;
@override
void initState() {
super.initState();
_controller = OverviewController();
_loadData();
}
Future<void> _loadData() async {
await _controller.loadDashboardData();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: _controller,
child: Consumer<OverviewController>(
builder: (context, controller, child) {
if (controller.isLoading) {
return _buildLoadingState();
}
if (controller.error != null) {
return _buildErrorState(controller.error!);
}
return Container(
color: ShadcnTheme.background,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 환영 섹션
ShadcnCard(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('안녕하세요, 관리자님! 👋', style: ShadcnTheme.headingH3),
const SizedBox(height: 8),
Text(
'오늘의 포트 운영 현황을 확인해보세요.',
style: ShadcnTheme.bodyMuted,
),
const SizedBox(height: 16),
Row(
children: [
ShadcnBadge(
text: '실시간 모니터링',
variant: ShadcnBadgeVariant.success,
),
const SizedBox(width: 8),
ShadcnBadge(
text: '업데이트됨',
variant: ShadcnBadgeVariant.outline,
),
],
),
],
),
),
],
),
),
const SizedBox(height: 32),
// 통계 카드 그리드 (반응형)
LayoutBuilder(
builder: (context, constraints) {
final crossAxisCount =
constraints.maxWidth > 1200
? 4
: constraints.maxWidth > 800
? 2
: 1;
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.5,
children: [
_buildStatCard(
'총 회사 수',
'${_controller.totalCompanies}',
Icons.business,
ShadcnTheme.gradient1,
),
_buildStatCard(
'총 사용자 수',
'${_controller.totalUsers}',
Icons.people,
ShadcnTheme.gradient2,
),
_buildStatCard(
'입고 장비',
'${_controller.equipmentStatus?.available ?? 0}',
Icons.inventory,
ShadcnTheme.success,
),
_buildStatCard(
'출고 장비',
'${_controller.equipmentStatus?.inUse ?? 0}',
Icons.local_shipping,
ShadcnTheme.warning,
),
],
);
},
),
const SizedBox(height: 32),
// 하단 콘텐츠
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 1000) {
// 큰 화면: 가로로 배치
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 2, child: _buildLeftColumn()),
const SizedBox(width: 24),
Expanded(flex: 1, child: _buildRightColumn()),
],
);
} else {
// 작은 화면: 세로로 배치
return Column(
children: [
_buildLeftColumn(),
const SizedBox(height: 24),
_buildRightColumn(),
],
);
}
},
),
],
),
),
);
},
),
);
}
Widget _buildLoadingState() {
return Container(
color: ShadcnTheme.background,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: ShadcnTheme.primary),
const SizedBox(height: ShadcnTheme.spacing4),
Text('대시보드를 불러오는 중...', style: ShadcnTheme.bodyMuted),
],
),
),
);
}
Widget _buildErrorState(String error) {
return Container(
color: ShadcnTheme.background,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48,
color: ShadcnTheme.error,
),
const SizedBox(height: ShadcnTheme.spacing4),
Text('오류가 발생했습니다', style: ShadcnTheme.headingH4),
const SizedBox(height: ShadcnTheme.spacing2),
Text(error, style: ShadcnTheme.bodyMuted),
const SizedBox(height: ShadcnTheme.spacing4),
ShadcnButton(
text: '다시 시도',
onPressed: _loadData,
variant: ShadcnButtonVariant.primary,
),
],
),
),
);
}
Widget _buildLeftColumn() {
return Column(
children: [
// 차트 카드
ShadcnCard(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('월별 활동 현황', style: ShadcnTheme.headingH4),
Text('최근 6개월 데이터', style: ShadcnTheme.bodyMuted),
],
),
ShadcnButton(
text: '상세보기',
onPressed: () {},
variant: ShadcnButtonVariant.ghost,
size: ShadcnButtonSize.small,
),
],
),
const SizedBox(height: 24),
Container(
height: 200,
decoration: BoxDecoration(
color: ShadcnTheme.muted,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.analytics,
size: 48,
color: ShadcnTheme.mutedForeground,
),
const SizedBox(height: 12),
Text('차트 영역', style: ShadcnTheme.bodyMuted),
Text(
'fl_chart 라이브러리로 구현 예정',
style: ShadcnTheme.bodySmall,
),
],
),
),
),
],
),
),
const SizedBox(height: 24),
// 최근 활동
ShadcnCard(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('최근 활동', style: ShadcnTheme.headingH4),
ShadcnButton(
text: '전체보기',
onPressed: () {},
variant: ShadcnButtonVariant.ghost,
size: ShadcnButtonSize.small,
),
],
),
const SizedBox(height: 16),
Consumer<OverviewController>(
builder: (context, controller, child) {
final activities = controller.recentActivities;
if (activities.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Center(
child: Text(
'최근 활동이 없습니다',
style: ShadcnTheme.bodyMuted,
),
),
);
}
return Column(
children: activities.take(5).map((activity) =>
_buildActivityItem(activity),
).toList(),
);
},
),
],
),
),
],
);
}
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,
'회사 등록',
'새 회사 추가',
),
],
),
),
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('백업', '완료'),
],
),
),
],
);
}
Widget _buildStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return ShadcnCard(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Icon(icon, color: color, size: 20),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: ShadcnTheme.success.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.trending_up,
size: 12,
color: ShadcnTheme.success,
),
const SizedBox(width: 4),
Text(
'+2.3%',
style: ShadcnTheme.labelSmall.copyWith(
color: ShadcnTheme.success,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Text(value, style: ShadcnTheme.headingH2),
const SizedBox(height: 4),
Text(title, style: ShadcnTheme.bodyMedium),
Text('등록된 항목', style: ShadcnTheme.bodySmall),
],
),
);
}
Widget _buildActivityItem(dynamic activity) {
// 아이콘 매핑
IconData getActivityIcon(String? type) {
switch (type?.toLowerCase()) {
case 'equipment_in':
case '장비 입고':
return Icons.inventory;
case 'equipment_out':
case '장비 출고':
return Icons.local_shipping;
case 'company':
case '회사':
return Icons.business;
case 'user':
case '사용자':
return Icons.person_add;
default:
return Icons.settings;
}
}
// 색상 매핑
Color getActivityColor(String? type) {
switch (type?.toLowerCase()) {
case 'equipment_in':
case '장비 입고':
return ShadcnTheme.success;
case 'equipment_out':
case '장비 출고':
return ShadcnTheme.warning;
case 'company':
case '회사':
return ShadcnTheme.info;
case 'user':
case '사용자':
return ShadcnTheme.primary;
default:
return ShadcnTheme.mutedForeground;
}
}
final activityType = activity.activityType ?? '';
final color = getActivityColor(activityType);
final dateFormat = DateFormat('MM/dd HH:mm');
final timestamp = activity.timestamp ?? DateTime.now();
final entityName = activity.entityName ?? '이름 없음';
final description = activity.description ?? '설명 없음';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
getActivityIcon(activityType),
color: color,
size: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entityName,
style: ShadcnTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
Text(
description,
style: ShadcnTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
],
),
),
Text(
dateFormat.format(timestamp),
style: ShadcnTheme.bodySmall,
),
],
),
);
}
Widget _buildQuickActionButton(IconData icon, String title, String subtitle) {
return GestureDetector(
onTap: () {
// 실제 기능 구현
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$title 기능은 개발 중입니다.'),
backgroundColor: ShadcnTheme.info,
),
);
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: ShadcnTheme.border),
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
Icon(icon, color: ShadcnTheme.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: ShadcnTheme.bodyMedium),
Text(subtitle, style: ShadcnTheme.bodySmall),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: ShadcnTheme.mutedForeground,
),
],
),
),
);
}
Widget _buildStatusItem(String label, String status) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: ShadcnTheme.bodyMedium),
ShadcnBadge(
text: status,
variant: ShadcnBadgeVariant.success,
size: ShadcnBadgeSize.small,
),
],
),
);
}
}