import 'package:flutter/material.dart'; import 'package:get_it/get_it.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/services/auth_service.dart'; import 'package:superport/utils/constants.dart'; import 'package:superport/data/models/auth/auth_user.dart'; /// ERP 시스템 최적화 메인 레이아웃 /// F-Pattern 레이아웃 적용 (1920x1080 최적화) /// 상단 헤더 + 좌측 사이드바 + 메인 콘텐츠 구조 class AppLayoutRedesign extends StatefulWidget { final String initialRoute; const AppLayoutRedesign({Key? key, this.initialRoute = Routes.home}) : super(key: key); @override State createState() => _AppLayoutRedesignState(); } class _AppLayoutRedesignState extends State with TickerProviderStateMixin { late String _currentRoute; bool _sidebarCollapsed = false; late AnimationController _sidebarAnimationController; AuthUser? _currentUser; late final AuthService _authService; late Animation _sidebarAnimation; // 레이아웃 상수 (1920x1080 최적화) static const double _sidebarExpandedWidth = 260.0; static const double _sidebarCollapsedWidth = 72.0; static const double _headerHeight = 64.0; static const double _maxContentWidth = 1440.0; @override void initState() { super.initState(); _currentRoute = widget.initialRoute; _setupAnimations(); _authService = GetIt.instance(); _loadCurrentUser(); } Future _loadCurrentUser() async { final user = await _authService.getCurrentUser(); if (mounted) { setState(() { _currentUser = user; }); } } void _setupAnimations() { _sidebarAnimationController = AnimationController( duration: const Duration(milliseconds: 250), vsync: this, ); _sidebarAnimation = Tween( begin: _sidebarExpandedWidth, end: _sidebarCollapsedWidth ).animate( CurvedAnimation( parent: _sidebarAnimationController, curve: Curves.easeInOutCubic, ), ); } @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(); case '/test/api': // Navigator를 사용하여 별도 화면으로 이동 WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.pushNamed(context, '/test/api'); }); return const Center(child: CircularProgressIndicator()); 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 '입고지 관리'; case '/test/api': return 'API 테스트'; default: return '대시보드'; } } /// 브레드크럼 경로 가져오기 List _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 ['홈', '입고지 관리']; case '/test/api': return ['홈', '개발자 도구', 'API 테스트']; default: return ['홈', '대시보드']; } } @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; final isWideScreen = screenWidth >= 1920; return 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), ), ), ), ], ), ), ), ), ], ), ), ], ), ); } /// F-Pattern 1차 시선: 상단 헤더 (로고, 주요 메뉴, 알림, 프로필) Widget _buildTopHeader() { return Container( height: _headerHeight, decoration: BoxDecoration( color: ShadcnTheme.background, border: Border( bottom: BorderSide( color: ShadcnTheme.border, width: 1, ), ), boxShadow: ShadcnTheme.shadowXs, ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing4), child: Row( children: [ // 왼쪽: 로고 + 사이드바 토글 Row( children: [ // 사이드바 토글 버튼 SizedBox( width: 40, height: 40, child: IconButton( onPressed: _toggleSidebar, icon: AnimatedRotation( turns: _sidebarCollapsed ? 0 : 0.5, duration: const Duration(milliseconds: 250), child: Icon( Icons.menu, color: ShadcnTheme.foregroundSecondary, size: 20, ), ), tooltip: _sidebarCollapsed ? '사이드바 펼치기' : '사이드바 접기', ), ), const SizedBox(width: ShadcnTheme.spacing3), // 앱 로고 및 제목 Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( gradient: LinearGradient( colors: [ ShadcnTheme.primary, ShadcnTheme.primaryDark, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: Icon( Icons.directions_boat, size: 20, color: ShadcnTheme.primaryForeground, ), ), const SizedBox(width: ShadcnTheme.spacing3), Text( 'supERPort', style: ShadcnTheme.headingH5.copyWith( fontWeight: FontWeight.w600, ), ), ], ), ], ), const Spacer(), // 오른쪽: 알림 + 프로필 Row( children: [ // 검색 SizedBox( width: 40, height: 40, child: IconButton( onPressed: () { // 전역 검색 기능 }, icon: Icon( Icons.search, color: ShadcnTheme.foregroundSecondary, size: 20, ), tooltip: '검색', ), ), const SizedBox(width: ShadcnTheme.spacing2), // 알림 SizedBox( width: 40, height: 40, child: Stack( children: [ IconButton( onPressed: () { // 알림 기능 }, icon: Icon( Icons.notifications_outlined, color: ShadcnTheme.foregroundSecondary, size: 20, ), tooltip: '알림', ), Positioned( right: 10, top: 10, child: Container( width: 8, height: 8, decoration: BoxDecoration( color: ShadcnTheme.error, shape: BoxShape.circle, border: Border.all( color: ShadcnTheme.background, width: 1.5, ), ), ), ), ], ), ), const SizedBox(width: ShadcnTheme.spacing3), const ShadcnSeparator( direction: Axis.vertical, thickness: 1, ), const SizedBox(width: ShadcnTheme.spacing3), // 프로필 InkWell( onTap: () => _showProfileMenu(context), borderRadius: BorderRadius.circular(ShadcnTheme.radiusFull), child: Padding( padding: const EdgeInsets.all(ShadcnTheme.spacing1), child: Row( children: [ ShadcnAvatar( initials: _currentUser?.name.substring(0, 1).toUpperCase() ?? 'U', size: 32, backgroundColor: ShadcnTheme.primaryLight, textColor: ShadcnTheme.primary, showBorder: false, ), const SizedBox(width: ShadcnTheme.spacing2), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _currentUser?.name ?? '사용자', style: ShadcnTheme.labelMedium, ), Text( _getUserRoleText(_currentUser?.role), style: ShadcnTheme.caption.copyWith( color: ShadcnTheme.foregroundMuted, ), ), ], ), const SizedBox(width: ShadcnTheme.spacing2), Icon( Icons.expand_more, size: 16, color: ShadcnTheme.foregroundMuted, ), ], ), ), ), ], ), ], ), ), ); } /// 사용자 역할 텍스트 변환 String _getUserRoleText(String? role) { switch (role) { case 'admin': return '관리자'; case 'manager': return '매니저'; case 'member': return '일반 사용자'; default: return '사용자'; } } /// 사이드바 빌드 Widget _buildSidebar() { return SidebarMenuRedesign( currentRoute: _currentRoute, onRouteChanged: _navigateTo, collapsed: _sidebarCollapsed, ); } /// F-Pattern 2차 시선: 페이지 헤더 (제목 + 주요 액션 버튼) Widget _buildPageHeader() { final breadcrumbs = _getBreadcrumbs(); return Container( padding: const EdgeInsets.all(ShadcnTheme.spacing6), decoration: BoxDecoration( color: ShadcnTheme.background, borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg), border: Border.all( color: ShadcnTheme.border, width: 1, ), ), child: Row( children: [ // 왼쪽: 페이지 제목 + 브레드크럼 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 페이지 제목 Text( _getPageTitle(), style: ShadcnTheme.headingH4, ), const SizedBox(height: ShadcnTheme.spacing1), // 브레드크럼 Row( children: [ for (int i = 0; i < breadcrumbs.length; i++) ...[ if (i > 0) ...[ const SizedBox(width: ShadcnTheme.spacing1), Icon( Icons.chevron_right, size: 14, color: ShadcnTheme.foregroundSubtle, ), const SizedBox(width: ShadcnTheme.spacing1), ], Text( breadcrumbs[i], style: i == breadcrumbs.length - 1 ? ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.foreground, fontWeight: FontWeight.w500, ) : ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.foregroundMuted, ), ), ], ], ), ], ), ), // 오른쪽: 페이지별 주요 액션 버튼들 if (_currentRoute != Routes.home) ...[ Row( children: [ // 새로고침 ShadcnButton( text: '', icon: Icon(Icons.refresh, size: 18), onPressed: () { // 페이지 새로고침 setState(() {}); }, variant: ShadcnButtonVariant.ghost, size: ShadcnButtonSize.small, ), const SizedBox(width: ShadcnTheme.spacing2), // 추가 버튼 (리스트 페이지에서만) if (_isListPage()) ...[ ShadcnButton( text: '추가', icon: Icon(Icons.add, size: 18), onPressed: () { _handleAddAction(); }, variant: ShadcnButtonVariant.primary, size: ShadcnButtonSize.small, ), ], ], ), ], ], ), ); } /// 리스트 페이지 여부 확인 bool _isListPage() { return [ Routes.equipment, Routes.equipmentInList, Routes.equipmentOutList, Routes.equipmentRentList, Routes.company, Routes.license, Routes.warehouseLocation, ].contains(_currentRoute); } /// 추가 액션 처리 void _handleAddAction() { String addRoute = ''; switch (_currentRoute) { case Routes.equipment: case Routes.equipmentInList: addRoute = '/equipment/in'; break; case Routes.equipmentOutList: addRoute = '/equipment/out'; break; case Routes.company: addRoute = '/company/add'; break; case Routes.license: addRoute = '/license/add'; break; case Routes.warehouseLocation: addRoute = '/warehouse-location/add'; break; } if (addRoute.isNotEmpty) { Navigator.pushNamed(context, addRoute).then((result) { if (result == true) { setState(() {}); } }); } } /// 프로필 메뉴 표시 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: [ // 핸들바 Container( width: 40, height: 4, decoration: BoxDecoration( color: ShadcnTheme.border, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: ShadcnTheme.spacing6), // 프로필 정보 Row( children: [ ShadcnAvatar( initials: _currentUser?.name.substring(0, 1).toUpperCase() ?? 'U', size: 56, backgroundColor: ShadcnTheme.primaryLight, textColor: ShadcnTheme.primary, ), const SizedBox(width: ShadcnTheme.spacing4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _currentUser?.name ?? '사용자', style: ShadcnTheme.headingH5, ), const SizedBox(height: 2), Text( _currentUser?.email ?? '', style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.foregroundMuted, ), ), const SizedBox(height: 4), ShadcnBadge( text: _getUserRoleText(_currentUser?.role), variant: ShadcnBadgeVariant.primary, size: ShadcnBadgeSize.small, ), ], ), ), ], ), const SizedBox(height: ShadcnTheme.spacing6), const ShadcnSeparator(), const SizedBox(height: ShadcnTheme.spacing4), // 메뉴 항목들 _buildProfileMenuItem( icon: Icons.person_outline, title: '프로필 설정', onTap: () { Navigator.pop(context); // 프로필 설정 화면으로 이동 }, ), _buildProfileMenuItem( icon: Icons.settings_outlined, title: '환경 설정', onTap: () { Navigator.pop(context); // 환경 설정 화면으로 이동 }, ), const SizedBox(height: ShadcnTheme.spacing4), const ShadcnSeparator(), const SizedBox(height: ShadcnTheme.spacing4), // 로그아웃 버튼 ShadcnButton( text: '로그아웃', onPressed: () async { // 로딩 다이얼로그 표시 showDialog( context: context, barrierDismissible: false, builder: (context) => Center( child: Container( padding: const EdgeInsets.all(ShadcnTheme.spacing6), decoration: BoxDecoration( color: ShadcnTheme.background, borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg), ), child: const CircularProgressIndicator(), ), ), ); try { // AuthService를 사용하여 로그아웃 final authService = GetIt.instance(); await authService.logout(); // 로딩 다이얼로그와 현재 모달 닫기 if (context.mounted) { Navigator.of(context).pop(); // 로딩 다이얼로그 Navigator.of(context).pop(); // 프로필 메뉴 // 로그인 화면으로 이동 Navigator.of(context).pushNamedAndRemoveUntil( '/login', (route) => false, ); } } catch (e) { // 에러 처리 if (context.mounted) { Navigator.of(context).pop(); // 로딩 다이얼로그 닫기 ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('로그아웃 중 오류가 발생했습니다.'), backgroundColor: ShadcnTheme.error, ), ); } } }, variant: ShadcnButtonVariant.destructive, fullWidth: true, icon: Icon(Icons.logout, size: 18), ), ], ), ), ); } /// 프로필 메뉴 아이템 Widget _buildProfileMenuItem({ required IconData icon, required String title, required VoidCallback onTap, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), child: Padding( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing3, ), child: Row( children: [ Icon( icon, size: 20, color: ShadcnTheme.foregroundSecondary, ), const SizedBox(width: ShadcnTheme.spacing3), Text( title, style: ShadcnTheme.bodyMedium, ), ], ), ), ); } } /// 재설계된 사이드바 메뉴 (접기/펼치기 지원) 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: EdgeInsets.all( collapsed ? ShadcnTheme.spacing2 : ShadcnTheme.spacing3 ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!collapsed) ...[ Padding( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing2, ), child: Text( '메인 메뉴', style: ShadcnTheme.caption.copyWith( color: ShadcnTheme.foregroundMuted, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), ), const SizedBox(height: ShadcnTheme.spacing1), ], _buildMenuItem( icon: Icons.dashboard_outlined, title: '대시보드', route: Routes.home, isActive: currentRoute == Routes.home, badge: null, ), _buildMenuItem( icon: Icons.inventory_2_outlined, title: '장비 관리', route: Routes.equipment, isActive: [ Routes.equipment, Routes.equipmentInList, Routes.equipmentOutList, Routes.equipmentRentList, ].contains(currentRoute), badge: null, ), _buildMenuItem( icon: Icons.warehouse_outlined, title: '입고지 관리', route: Routes.warehouseLocation, isActive: currentRoute == Routes.warehouseLocation, badge: null, ), _buildMenuItem( icon: Icons.business_outlined, title: '회사 관리', route: Routes.company, isActive: currentRoute == Routes.company, badge: null, ), _buildMenuItem( icon: Icons.support_outlined, title: '유지보수 관리', route: Routes.license, isActive: currentRoute == Routes.license, badge: '3', // 만료 임박 라이선스 수 ), if (!collapsed) ...[ const SizedBox(height: ShadcnTheme.spacing4), const ShadcnSeparator(), const SizedBox(height: ShadcnTheme.spacing4), Padding( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing2, ), child: Text( '개발자 도구', style: ShadcnTheme.caption.copyWith( color: ShadcnTheme.foregroundMuted, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), ), const SizedBox(height: ShadcnTheme.spacing1), ], _buildMenuItem( icon: Icons.bug_report_outlined, title: 'API 테스트', route: '/test/api', isActive: currentRoute == '/test/api', badge: null, ), ], ), ), ), // 하단 버전 정보 if (!collapsed) ...[ const ShadcnSeparator(), Container( padding: const EdgeInsets.all(ShadcnTheme.spacing3), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'supERPort v1.0.0', style: ShadcnTheme.caption.copyWith( color: ShadcnTheme.foregroundMuted, ), ), const SizedBox(height: 2), Text( '© 2025 Superport', style: ShadcnTheme.caption.copyWith( color: ShadcnTheme.foregroundSubtle, ), ), ], ), ), ], ], ); } Widget _buildMenuItem({ required IconData icon, required String title, required String route, required bool isActive, String? badge, }) { return AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing1), child: InkWell( onTap: () => onRouteChanged(route), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), child: Container( padding: EdgeInsets.symmetric( horizontal: collapsed ? ShadcnTheme.spacing3 : ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing2 + 2, ), decoration: BoxDecoration( color: isActive ? ShadcnTheme.primaryLight : Colors.transparent, borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: Row( children: [ Icon( isActive ? _getFilledIcon(icon) : icon, size: 20, color: isActive ? ShadcnTheme.primary : ShadcnTheme.foregroundSecondary, ), if (!collapsed) ...[ const SizedBox(width: ShadcnTheme.spacing3), Expanded( child: Text( title, style: ShadcnTheme.bodyMedium.copyWith( color: isActive ? ShadcnTheme.primary : ShadcnTheme.foreground, fontWeight: isActive ? FontWeight.w600 : FontWeight.w400, ), ), ), if (badge != null) ...[ Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: ShadcnTheme.error, borderRadius: BorderRadius.circular(10), ), child: Text( badge, style: ShadcnTheme.caption.copyWith( color: ShadcnTheme.errorForeground, fontWeight: FontWeight.w600, ), ), ), ], ], if (collapsed && badge != null) ...[ Positioned( right: 0, top: 0, child: Container( width: 8, height: 8, decoration: BoxDecoration( color: ShadcnTheme.error, shape: BoxShape.circle, ), ), ), ], ], ), ), ), ); } /// 활성화 상태일 때 채워진 아이콘 반환 IconData _getFilledIcon(IconData outlinedIcon) { switch (outlinedIcon) { case Icons.dashboard_outlined: return Icons.dashboard; case Icons.inventory_2_outlined: return Icons.inventory_2; case Icons.warehouse_outlined: return Icons.warehouse; case Icons.business_outlined: return Icons.business; case Icons.support_outlined: return Icons.support; case Icons.bug_report_outlined: return Icons.bug_report; default: return outlinedIcon; } } }