## 주요 변경사항: ### UI/UX 개선 - shadcn/ui 스타일 기반의 새로운 디자인 시스템 도입 - 모든 주요 화면에 대한 리디자인 구현 완료 - 로그인 화면: 모던한 카드 스타일 적용 - 대시보드: 통계 카드와 차트를 활용한 개요 화면 - 리스트 화면들: 일관된 테이블 디자인과 검색/필터 기능 - 다크모드 지원을 위한 테마 시스템 구축 ### 기능 개선 - Equipment List: 고급 필터링 (상태, 담당자별) - Company List: 검색 및 정렬 기능 강화 - User List: 역할별 필터링 추가 - License List: 만료일 기반 상태 표시 - Warehouse Location: 재고 수준 시각화 ### 기술적 개선 - 재사용 가능한 컴포넌트 라이브러리 구축 - 일관된 코드 패턴 가이드라인 작성 - 프로젝트 구조 분석 및 문서화 ### 문서화 - 프로젝트 분석 문서 추가 - UI 리디자인 진행 상황 문서 - 코드 패턴 가이드 작성 - Equipment 기능 격차 분석 및 구현 계획 ### 삭제/리팩토링 - goods_list.dart 제거 (equipment_list로 통합) - 불필요한 import 및 코드 정리 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
575 lines
17 KiB
Dart
575 lines
17 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/overview_screen_redesign.dart';
|
|
import 'package:superport/screens/equipment/equipment_list_redesign.dart';
|
|
import 'package:superport/screens/company/company_list_redesign.dart';
|
|
import 'package:superport/screens/user/user_list_redesign.dart';
|
|
import 'package:superport/screens/license/license_list_redesign.dart';
|
|
import 'package:superport/screens/warehouse_location/warehouse_location_list_redesign.dart';
|
|
import 'package:superport/utils/constants.dart';
|
|
|
|
/// Microsoft Dynamics 365 스타일의 메인 레이아웃
|
|
/// 상단 헤더 + 좌측 사이드바 + 메인 콘텐츠 구조
|
|
class AppLayoutRedesign extends StatefulWidget {
|
|
final String initialRoute;
|
|
|
|
const AppLayoutRedesign({Key? key, this.initialRoute = Routes.home})
|
|
: super(key: key);
|
|
|
|
@override
|
|
State<AppLayoutRedesign> createState() => _AppLayoutRedesignState();
|
|
}
|
|
|
|
class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
|
with TickerProviderStateMixin {
|
|
late String _currentRoute;
|
|
bool _sidebarCollapsed = false;
|
|
late AnimationController _sidebarAnimationController;
|
|
late Animation<double> _sidebarAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_currentRoute = widget.initialRoute;
|
|
_setupAnimations();
|
|
}
|
|
|
|
void _setupAnimations() {
|
|
_sidebarAnimationController = AnimationController(
|
|
duration: const Duration(milliseconds: 300),
|
|
vsync: this,
|
|
);
|
|
_sidebarAnimation = Tween<double>(begin: 280.0, end: 72.0).animate(
|
|
CurvedAnimation(
|
|
parent: _sidebarAnimationController,
|
|
curve: Curves.easeInOut,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_sidebarAnimationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
/// 현재 경로에 따라 적절한 컨텐츠 섹션을 반환
|
|
Widget _getContentForRoute(String route) {
|
|
switch (route) {
|
|
case Routes.home:
|
|
return const OverviewScreenRedesign();
|
|
case Routes.equipment:
|
|
case Routes.equipmentInList:
|
|
case Routes.equipmentOutList:
|
|
case Routes.equipmentRentList:
|
|
return EquipmentListRedesign(currentRoute: route);
|
|
case Routes.company:
|
|
return const CompanyListRedesign();
|
|
case Routes.user:
|
|
return const UserListRedesign();
|
|
case Routes.license:
|
|
return const LicenseListRedesign();
|
|
case Routes.warehouseLocation:
|
|
return const WarehouseLocationListRedesign();
|
|
default:
|
|
return const OverviewScreenRedesign();
|
|
}
|
|
}
|
|
|
|
/// 경로 변경 메서드
|
|
void _navigateTo(String route) {
|
|
setState(() {
|
|
_currentRoute = route;
|
|
});
|
|
}
|
|
|
|
/// 사이드바 토글
|
|
void _toggleSidebar() {
|
|
setState(() {
|
|
_sidebarCollapsed = !_sidebarCollapsed;
|
|
});
|
|
|
|
if (_sidebarCollapsed) {
|
|
_sidebarAnimationController.forward();
|
|
} else {
|
|
_sidebarAnimationController.reverse();
|
|
}
|
|
}
|
|
|
|
/// 현재 페이지 제목 가져오기
|
|
String _getPageTitle() {
|
|
switch (_currentRoute) {
|
|
case Routes.home:
|
|
return '대시보드';
|
|
case Routes.equipment:
|
|
case Routes.equipmentInList:
|
|
case Routes.equipmentOutList:
|
|
case Routes.equipmentRentList:
|
|
return '장비 관리';
|
|
case Routes.company:
|
|
return '회사 관리';
|
|
case Routes.license:
|
|
return '유지보수 관리';
|
|
case Routes.warehouseLocation:
|
|
return '입고지 관리';
|
|
default:
|
|
return '대시보드';
|
|
}
|
|
}
|
|
|
|
/// 브레드크럼 경로 가져오기
|
|
List<String> _getBreadcrumbs() {
|
|
switch (_currentRoute) {
|
|
case Routes.home:
|
|
return ['홈', '대시보드'];
|
|
case Routes.equipment:
|
|
return ['홈', '장비 관리', '전체'];
|
|
case Routes.equipmentInList:
|
|
return ['홈', '장비 관리', '입고'];
|
|
case Routes.equipmentOutList:
|
|
return ['홈', '장비 관리', '출고'];
|
|
case Routes.equipmentRentList:
|
|
return ['홈', '장비 관리', '대여'];
|
|
case Routes.company:
|
|
return ['홈', '회사 관리'];
|
|
case Routes.license:
|
|
return ['홈', '유지보수 관리'];
|
|
case Routes.warehouseLocation:
|
|
return ['홈', '입고지 관리'];
|
|
default:
|
|
return ['홈', '대시보드'];
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: ShadcnTheme.muted,
|
|
body: Column(
|
|
children: [
|
|
// 상단 헤더
|
|
_buildTopHeader(),
|
|
|
|
// 메인 콘텐츠 영역
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
// 좌측 사이드바
|
|
AnimatedBuilder(
|
|
animation: _sidebarAnimation,
|
|
builder: (context, child) {
|
|
return SizedBox(
|
|
width: _sidebarAnimation.value,
|
|
child: _buildSidebar(),
|
|
);
|
|
},
|
|
),
|
|
|
|
// 메인 콘텐츠
|
|
Expanded(
|
|
child: Container(
|
|
margin: const EdgeInsets.all(ShadcnTheme.spacing4),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.background,
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
|
border: Border.all(color: ShadcnTheme.border),
|
|
boxShadow: ShadcnTheme.cardShadow,
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// 페이지 헤더
|
|
_buildPageHeader(),
|
|
|
|
// 메인 콘텐츠
|
|
Expanded(
|
|
child: ClipRRect(
|
|
borderRadius: const BorderRadius.only(
|
|
bottomLeft: Radius.circular(ShadcnTheme.radiusLg),
|
|
bottomRight: Radius.circular(
|
|
ShadcnTheme.radiusLg,
|
|
),
|
|
),
|
|
child: _getContentForRoute(_currentRoute),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 상단 헤더 빌드
|
|
Widget _buildTopHeader() {
|
|
return Container(
|
|
height: 64,
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.background,
|
|
border: Border(bottom: BorderSide(color: ShadcnTheme.border)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing4),
|
|
child: Row(
|
|
children: [
|
|
// 사이드바 토글 버튼
|
|
IconButton(
|
|
onPressed: _toggleSidebar,
|
|
icon: Icon(
|
|
_sidebarCollapsed ? Icons.menu : Icons.menu_open,
|
|
color: ShadcnTheme.foreground,
|
|
),
|
|
tooltip: _sidebarCollapsed ? '사이드바 펼치기' : '사이드바 접기',
|
|
),
|
|
|
|
const SizedBox(width: ShadcnTheme.spacing4),
|
|
|
|
// 앱 로고 및 제목
|
|
Container(
|
|
padding: const EdgeInsets.all(ShadcnTheme.spacing2),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [ShadcnTheme.gradient1, ShadcnTheme.gradient2],
|
|
),
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
child: Icon(
|
|
Icons.directions_boat,
|
|
size: 24,
|
|
color: ShadcnTheme.primaryForeground,
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: ShadcnTheme.spacing3),
|
|
|
|
Text('supERPort', style: ShadcnTheme.headingH4),
|
|
|
|
const Spacer(),
|
|
|
|
// 상단 액션 버튼들
|
|
_buildTopActions(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 상단 액션 버튼들
|
|
Widget _buildTopActions() {
|
|
return Row(
|
|
children: [
|
|
// 알림 버튼
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.muted,
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
child: IconButton(
|
|
onPressed: () {
|
|
// 알림 기능
|
|
},
|
|
icon: Stack(
|
|
children: [
|
|
Icon(
|
|
Icons.notifications_outlined,
|
|
color: ShadcnTheme.foreground,
|
|
),
|
|
Positioned(
|
|
right: 0,
|
|
top: 0,
|
|
child: Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.destructive,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
tooltip: '알림',
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: ShadcnTheme.spacing2),
|
|
|
|
// 설정 버튼
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.muted,
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
child: IconButton(
|
|
onPressed: () {
|
|
// 설정 기능
|
|
},
|
|
icon: Icon(Icons.settings_outlined, color: ShadcnTheme.foreground),
|
|
tooltip: '설정',
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: ShadcnTheme.spacing4),
|
|
|
|
// 프로필 아바타
|
|
GestureDetector(
|
|
onTap: () {
|
|
_showProfileMenu(context);
|
|
},
|
|
child: ShadcnAvatar(initials: 'A', size: 36),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 사이드바 빌드
|
|
Widget _buildSidebar() {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.background,
|
|
border: Border(right: BorderSide(color: ShadcnTheme.border)),
|
|
),
|
|
child: SidebarMenuRedesign(
|
|
currentRoute: _currentRoute,
|
|
onRouteChanged: _navigateTo,
|
|
collapsed: _sidebarCollapsed,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 페이지 헤더 빌드
|
|
Widget _buildPageHeader() {
|
|
final breadcrumbs = _getBreadcrumbs();
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
|
|
decoration: BoxDecoration(
|
|
border: Border(bottom: BorderSide(color: ShadcnTheme.border)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 브레드크럼
|
|
Row(
|
|
children: [
|
|
for (int i = 0; i < breadcrumbs.length; i++) ...[
|
|
if (i > 0) ...[
|
|
const SizedBox(width: ShadcnTheme.spacing2),
|
|
Icon(
|
|
Icons.chevron_right,
|
|
size: 16,
|
|
color: ShadcnTheme.mutedForeground,
|
|
),
|
|
const SizedBox(width: ShadcnTheme.spacing2),
|
|
],
|
|
Text(
|
|
breadcrumbs[i],
|
|
style:
|
|
i == breadcrumbs.length - 1
|
|
? ShadcnTheme.bodyMedium
|
|
: ShadcnTheme.bodyMuted,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 프로필 메뉴 표시
|
|
void _showProfileMenu(BuildContext context) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: ShadcnTheme.background,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(ShadcnTheme.radiusXl),
|
|
),
|
|
),
|
|
builder:
|
|
(context) => Container(
|
|
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// 프로필 정보
|
|
Row(
|
|
children: [
|
|
ShadcnAvatar(initials: 'A', size: 48),
|
|
const SizedBox(width: ShadcnTheme.spacing4),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('관리자', style: ShadcnTheme.headingH4),
|
|
Text(
|
|
'admin@superport.com',
|
|
style: ShadcnTheme.bodyMuted,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: ShadcnTheme.spacing6),
|
|
const ShadcnSeparator(),
|
|
const SizedBox(height: ShadcnTheme.spacing4),
|
|
|
|
// 로그아웃 버튼
|
|
ShadcnButton(
|
|
text: '로그아웃',
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
Navigator.of(context).pushReplacementNamed('/login');
|
|
},
|
|
variant: ShadcnButtonVariant.destructive,
|
|
fullWidth: true,
|
|
icon: Icon(Icons.logout),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 재설계된 사이드바 메뉴 (접기/펼치기 지원)
|
|
class SidebarMenuRedesign extends StatelessWidget {
|
|
final String currentRoute;
|
|
final Function(String) onRouteChanged;
|
|
final bool collapsed;
|
|
|
|
const SidebarMenuRedesign({
|
|
Key? key,
|
|
required this.currentRoute,
|
|
required this.onRouteChanged,
|
|
required this.collapsed,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
|
child: Column(
|
|
children: [
|
|
_buildMenuItem(
|
|
icon: Icons.dashboard,
|
|
title: '대시보드',
|
|
route: Routes.home,
|
|
isActive: currentRoute == Routes.home,
|
|
),
|
|
|
|
const SizedBox(height: ShadcnTheme.spacing2),
|
|
|
|
_buildMenuItem(
|
|
icon: Icons.inventory,
|
|
title: '장비 관리',
|
|
route: Routes.equipment,
|
|
isActive: [
|
|
Routes.equipment,
|
|
Routes.equipmentInList,
|
|
Routes.equipmentOutList,
|
|
Routes.equipmentRentList,
|
|
].contains(currentRoute),
|
|
),
|
|
|
|
const SizedBox(height: ShadcnTheme.spacing2),
|
|
|
|
_buildMenuItem(
|
|
icon: Icons.location_on,
|
|
title: '입고지 관리',
|
|
route: Routes.warehouseLocation,
|
|
isActive: currentRoute == Routes.warehouseLocation,
|
|
),
|
|
|
|
const SizedBox(height: ShadcnTheme.spacing2),
|
|
|
|
_buildMenuItem(
|
|
icon: Icons.business,
|
|
title: '회사 관리',
|
|
route: Routes.company,
|
|
isActive: currentRoute == Routes.company,
|
|
),
|
|
|
|
const SizedBox(height: ShadcnTheme.spacing2),
|
|
|
|
_buildMenuItem(
|
|
icon: Icons.vpn_key,
|
|
title: '유지보수 관리',
|
|
route: Routes.license,
|
|
isActive: currentRoute == Routes.license,
|
|
),
|
|
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMenuItem({
|
|
required IconData icon,
|
|
required String title,
|
|
required String route,
|
|
required bool isActive,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: () => onRouteChanged(route),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: collapsed ? ShadcnTheme.spacing2 : ShadcnTheme.spacing4,
|
|
vertical: ShadcnTheme.spacing3,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: isActive ? ShadcnTheme.primary : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 20,
|
|
color:
|
|
isActive
|
|
? ShadcnTheme.primaryForeground
|
|
: ShadcnTheme.foreground,
|
|
),
|
|
if (!collapsed) ...[
|
|
const SizedBox(width: ShadcnTheme.spacing3),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: ShadcnTheme.bodyMedium.copyWith(
|
|
color:
|
|
isActive
|
|
? ShadcnTheme.primaryForeground
|
|
: ShadcnTheme.foreground,
|
|
fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|