- 자동 로그인 구현: 앱 시작 시 토큰 확인 후 적절한 화면으로 라우팅 - AuthInterceptor 개선: AuthService와 통합하여 토큰 관리 일원화 - 로그아웃 기능 개선: AuthService를 사용한 API 로그아웃 처리 - 대시보드 API 연동: MockDataService에서 실제 API로 완전 전환 - Dashboard DTO 모델 생성 (OverviewStats, RecentActivity 등) - DashboardRemoteDataSource 및 DashboardService 구현 - OverviewController를 ChangeNotifier 패턴으로 개선 - OverviewScreenRedesign에 Provider 패턴 적용 - API 통합 진행 상황 문서 업데이트
570 lines
18 KiB
Dart
570 lines
18 KiB
Dart
import 'package:flutter/material.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.totalEquipmentIn}',
|
|
Icons.inventory,
|
|
ShadcnTheme.success,
|
|
),
|
|
_buildStatCard(
|
|
'출고 장비',
|
|
'${_controller.totalEquipmentOut}',
|
|
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.withOpacity(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.withOpacity(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) {
|
|
case 'equipment_in':
|
|
return Icons.inventory;
|
|
case 'equipment_out':
|
|
return Icons.local_shipping;
|
|
case 'company':
|
|
return Icons.business;
|
|
case 'user':
|
|
return Icons.person_add;
|
|
default:
|
|
return Icons.settings;
|
|
}
|
|
}
|
|
|
|
// 색상 매핑
|
|
Color getActivityColor(String type) {
|
|
switch (type) {
|
|
case 'equipment_in':
|
|
return ShadcnTheme.success;
|
|
case 'equipment_out':
|
|
return ShadcnTheme.warning;
|
|
case 'company':
|
|
return ShadcnTheme.info;
|
|
case 'user':
|
|
return ShadcnTheme.primary;
|
|
default:
|
|
return ShadcnTheme.mutedForeground;
|
|
}
|
|
}
|
|
|
|
final color = getActivityColor(activity.type);
|
|
final dateFormat = DateFormat('MM/dd HH:mm');
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Icon(
|
|
getActivityIcon(activity.type),
|
|
color: color,
|
|
size: 16,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
activity.title,
|
|
style: ShadcnTheme.bodyMedium,
|
|
),
|
|
Text(
|
|
activity.description,
|
|
style: ShadcnTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Text(
|
|
dateFormat.format(activity.createdAt),
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|