import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:provider/provider.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.dart'; import 'package:superport/screens/vendor/vendor_list_screen.dart'; import 'package:superport/screens/vendor/controllers/vendor_controller.dart'; import 'package:superport/screens/model/model_list_screen.dart'; import 'package:superport/screens/zipcode/zipcode_search_screen.dart'; import 'package:superport/screens/zipcode/controllers/zipcode_controller.dart'; import 'package:superport/screens/equipment/equipment_list.dart'; import 'package:superport/screens/company/company_list.dart'; import 'package:superport/screens/user/user_list.dart'; import 'package:superport/screens/warehouse_location/warehouse_location_list.dart'; import 'package:superport/screens/inventory/inventory_history_screen.dart'; import 'package:superport/screens/maintenance/maintenance_schedule_screen.dart'; import 'package:superport/screens/maintenance/maintenance_alert_dashboard.dart'; import 'package:superport/screens/maintenance/maintenance_history_screen.dart' as maint; import 'package:superport/screens/maintenance/controllers/maintenance_controller.dart'; import 'package:superport/screens/rent/rent_list_screen.dart'; import 'package:superport/screens/rent/controllers/rent_controller.dart'; import 'package:superport/services/auth_service.dart'; import 'package:superport/core/services/lookups_service.dart'; import 'package:superport/injection_container.dart' as di; import 'package:superport/utils/constants.dart'; import 'package:superport/data/models/auth/auth_user.dart'; /// ERP 시스템 최적화 메인 레이아웃 /// F-Pattern 레이아웃 적용 (1920x1080 최적화) /// 상단 헤더 + 좌측 사이드바 + 메인 콘텐츠 구조 class AppLayout extends StatefulWidget { final String initialRoute; const AppLayout({super.key, this.initialRoute = Routes.home}); @override State createState() => _AppLayoutState(); } class _AppLayoutState extends State with TickerProviderStateMixin { late String _currentRoute; bool _sidebarCollapsed = false; late AnimationController _sidebarAnimationController; AuthUser? _currentUser; late final AuthService _authService; late final LookupsService _lookupsService; late Animation _sidebarAnimation; int _expiringMaintenanceCount = 0; // 30일 내 만료 예정 유지보수 수 // 레이아웃 상수 (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(); _lookupsService = GetIt.instance(); _loadCurrentUser(); _loadMaintenanceAlerts(); _initializeLookupData(); // Lookup 데이터 초기화 } Future _loadCurrentUser() async { try { // 서버에서 최신 관리자 정보 가져오기 final result = await _authService.getCurrentAdminFromServer(); result.fold( (failure) { print('[AppLayout] 서버에서 관리자 정보 로드 실패: ${failure.message}'); // 실패 시 로컬 스토리지에서 캐시된 정보 사용 _loadCurrentUserFromLocal(); }, (user) { if (mounted) { setState(() { _currentUser = user; }); print('[AppLayout] 서버에서 관리자 정보 로드 성공: ${user.name} (${user.email})'); } }, ); } catch (e) { print('[AppLayout] 관리자 정보 로드 중 예외 발생: $e'); // 예외 발생 시 로컬 스토리지에서 캐시된 정보 사용 _loadCurrentUserFromLocal(); } } /// 로컬 스토리지에서 캐시된 사용자 정보 로드 (fallback) Future _loadCurrentUserFromLocal() async { final user = await _authService.getCurrentUser(); if (mounted) { setState(() { _currentUser = user; }); print('[AppLayout] 로컬에서 관리자 정보 로드: ${user?.name ?? 'Unknown'}'); } } Future _loadMaintenanceAlerts() async { try { print('[DEBUG] 유지보수 알림 정보 로드 시작...'); // TODO: MaintenanceController를 통해 알림 정보 로드 // 현재는 임시로 0으로 설정 if (mounted) { setState(() { _expiringMaintenanceCount = 0; print('[DEBUG] 유지보수 알림 상태 업데이트 완료: $_expiringMaintenanceCount'); }); } } catch (e) { print('[ERROR] 유지보수 알림 정보 로드 중 예외 발생: $e'); print('[ERROR] 스택 트레이스: ${StackTrace.current}'); } } /// Lookup 데이터 초기화 (앱 시작 시 한 번만 호출) Future _initializeLookupData() async { try { print('[DEBUG] Lookups 서비스 초기화 시작...'); if (!_lookupsService.isInitialized) { final result = await _lookupsService.initialize(); result.fold( (failure) { print('[ERROR] Lookups 초기화 실패: ${failure.message}'); }, (success) { print('[DEBUG] Lookups 서비스 초기화 성공!'); final stats = _lookupsService.getCacheStats(); print('[DEBUG] - 제조사: ${stats['manufacturers_count']}개'); print('[DEBUG] - 장비명: ${stats['equipment_names_count']}개'); print('[DEBUG] - 장비 카테고리: ${stats['equipment_categories_count']}개'); print('[DEBUG] - 장비 상태: ${stats['equipment_statuses_count']}개'); }, ); } else { print('[DEBUG] Lookups 서비스 이미 초기화됨 (캐시 사용)'); } } catch (e) { print('[ERROR] Lookups 초기화 중 예외 발생: $e'); } } 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 ChangeNotifierProvider( create: (context) => di.sl(), child: const VendorListScreen(), ); case Routes.vendor: return ChangeNotifierProvider( create: (context) => di.sl(), child: const VendorListScreen(), ); case Routes.model: return const ModelListScreen(); case Routes.equipment: case Routes.equipmentInList: case Routes.equipmentOutList: case Routes.equipmentRentList: return EquipmentList(currentRoute: route); case Routes.company: return const CompanyList(); case Routes.user: return const UserList(); // License 시스템이 Maintenance로 대체됨 case Routes.maintenance: case Routes.maintenanceSchedule: return ChangeNotifierProvider( create: (_) => GetIt.instance(), child: const MaintenanceScheduleScreen(), ); case Routes.maintenanceAlert: return ChangeNotifierProvider( create: (_) => GetIt.instance(), child: const MaintenanceAlertDashboard(), ); case Routes.maintenanceHistory: return ChangeNotifierProvider( create: (_) => GetIt.instance(), child: const maint.MaintenanceHistoryScreen(), ); case Routes.warehouseLocation: return const WarehouseLocationList(); case Routes.zipcode: return ChangeNotifierProvider( create: (context) => di.sl(), child: const ZipcodeSearchScreen(), ); case Routes.inventory: case Routes.inventoryHistory: return const InventoryHistoryScreen(); case Routes.rent: return ChangeNotifierProvider( create: (_) => GetIt.instance(), child: const RentListScreen(), ); case '/test/api': // Navigator를 사용하여 별도 화면으로 이동 WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.pushNamed(context, '/test/api'); }); return const Center(child: CircularProgressIndicator()); default: return ChangeNotifierProvider( create: (context) => di.sl(), child: const VendorListScreen(), ); } } /// 경로 변경 메서드 void _navigateTo(String route) { setState(() { _currentRoute = route; }); // 유지보수 화면으로 이동할 때 알림 정보 새로고침 if (route == Routes.maintenance || route == Routes.maintenanceAlert) { _loadMaintenanceAlerts(); } } /// 사이드바 토글 void _toggleSidebar() { setState(() { _sidebarCollapsed = !_sidebarCollapsed; }); if (_sidebarCollapsed) { _sidebarAnimationController.forward(); } else { _sidebarAnimationController.reverse(); } } /// 현재 페이지 제목 가져오기 @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; final isWideScreen = screenWidth >= 1920; return Provider.value( value: _authService, child: 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 SidebarMenu( currentRoute: _currentRoute, onRouteChanged: _navigateTo, collapsed: _sidebarCollapsed, expiringMaintenanceCount: _expiringMaintenanceCount, ); } /// 프로필 메뉴 표시 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.lock_outline, title: '비밀번호 변경', onTap: () { Navigator.pop(context); _showChangePasswordDialog(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, ), ], ), ), ); } /// 비밀번호 변경 다이얼로그 void _showChangePasswordDialog(BuildContext context) { final oldPasswordController = TextEditingController(); final newPasswordController = TextEditingController(); final confirmPasswordController = TextEditingController(); bool isLoading = false; bool obscureOldPassword = true; bool obscureNewPassword = true; bool obscureConfirmPassword = true; showDialog( context: context, barrierDismissible: false, builder: (context) => StatefulBuilder( builder: (context, setState) => AlertDialog( backgroundColor: ShadcnTheme.background, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg), ), title: Row( children: [ Icon( Icons.lock_outline, color: ShadcnTheme.primary, size: 24, ), const SizedBox(width: ShadcnTheme.spacing3), Text( '비밀번호 변경', style: ShadcnTheme.headingH5, ), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( '보안을 위해 기존 비밀번호를 입력하고 새 비밀번호를 설정해주세요.', style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.foregroundMuted, ), ), const SizedBox(height: ShadcnTheme.spacing6), // 기존 비밀번호 Text( '기존 비밀번호', style: ShadcnTheme.labelMedium, ), const SizedBox(height: ShadcnTheme.spacing2), TextFormField( controller: oldPasswordController, obscureText: obscureOldPassword, decoration: InputDecoration( hintText: '현재 사용중인 비밀번호를 입력하세요', suffixIcon: IconButton( onPressed: () { setState(() { obscureOldPassword = !obscureOldPassword; }); }, icon: Icon( obscureOldPassword ? Icons.visibility_off : Icons.visibility, color: ShadcnTheme.foregroundMuted, ), ), ), ), const SizedBox(height: ShadcnTheme.spacing4), // 새 비밀번호 Text( '새 비밀번호', style: ShadcnTheme.labelMedium, ), const SizedBox(height: ShadcnTheme.spacing2), TextFormField( controller: newPasswordController, obscureText: obscureNewPassword, decoration: InputDecoration( hintText: '8자 이상의 새 비밀번호를 입력하세요', suffixIcon: IconButton( onPressed: () { setState(() { obscureNewPassword = !obscureNewPassword; }); }, icon: Icon( obscureNewPassword ? Icons.visibility_off : Icons.visibility, color: ShadcnTheme.foregroundMuted, ), ), ), ), const SizedBox(height: ShadcnTheme.spacing4), // 새 비밀번호 확인 Text( '새 비밀번호 확인', style: ShadcnTheme.labelMedium, ), const SizedBox(height: ShadcnTheme.spacing2), TextFormField( controller: confirmPasswordController, obscureText: obscureConfirmPassword, decoration: InputDecoration( hintText: '새 비밀번호를 다시 입력하세요', suffixIcon: IconButton( onPressed: () { setState(() { obscureConfirmPassword = !obscureConfirmPassword; }); }, icon: Icon( obscureConfirmPassword ? Icons.visibility_off : Icons.visibility, color: ShadcnTheme.foregroundMuted, ), ), ), ), ], ), ), actions: [ ShadcnButton( text: '취소', onPressed: isLoading ? null : () { Navigator.of(context).pop(); }, variant: ShadcnButtonVariant.secondary, ), ShadcnButton( text: isLoading ? '변경 중...' : '변경하기', onPressed: isLoading ? null : () async { // 유효성 검사 if (oldPasswordController.text.isEmpty) { _showSnackBar(context, '기존 비밀번호를 입력해주세요.', isError: true); return; } if (newPasswordController.text.length < 8) { _showSnackBar(context, '새 비밀번호는 8자 이상이어야 합니다.', isError: true); return; } if (newPasswordController.text != confirmPasswordController.text) { _showSnackBar(context, '새 비밀번호가 일치하지 않습니다.', isError: true); return; } setState(() { isLoading = true; }); try { // AuthService.changePassword API 호출 final result = await _authService.changePassword( oldPassword: oldPasswordController.text, newPassword: newPasswordController.text, ); result.fold( (failure) { if (context.mounted) { _showSnackBar(context, failure.message, isError: true); } }, (messageResponse) { if (context.mounted) { Navigator.of(context).pop(); _showSnackBar(context, messageResponse.message); } }, ); } catch (e) { if (context.mounted) { _showSnackBar(context, '비밀번호 변경 중 오류가 발생했습니다.', isError: true); } } finally { if (mounted) { setState(() { isLoading = false; }); } } }, variant: ShadcnButtonVariant.primary, icon: isLoading ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( ShadcnTheme.primaryForeground, ), ), ) : Icon(Icons.check, size: 18), ), ], ), ), ); } /// 스낵바 표시 헬퍼 메서드 void _showSnackBar(BuildContext context, String message, {bool isError = false}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? ShadcnTheme.error : ShadcnTheme.success, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), ), ); } } /// 재설계된 사이드바 메뉴 (접기/펼치기 지원) class SidebarMenu extends StatelessWidget { final String currentRoute; final Function(String) onRouteChanged; final bool collapsed; final int expiringMaintenanceCount; const SidebarMenu({ super.key, required this.currentRoute, required this.onRouteChanged, required this.collapsed, required this.expiringMaintenanceCount, }); @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.factory_outlined, title: '벤더 관리', route: Routes.vendor, isActive: currentRoute == Routes.vendor, badge: null, ), _buildMenuItem( icon: Icons.category_outlined, title: '모델 관리', route: Routes.model, isActive: currentRoute == Routes.model, 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.history, title: '재고 이력', route: Routes.inventoryHistory, isActive: [ Routes.inventory, Routes.inventoryHistory, ].contains(currentRoute), badge: null, ), _buildMenuItem( icon: Icons.warehouse_outlined, title: '입고지 관리', route: Routes.warehouseLocation, isActive: currentRoute == Routes.warehouseLocation, badge: null, ), _buildMenuItem( icon: Icons.location_on_outlined, title: '우편번호 검색', route: Routes.zipcode, isActive: currentRoute == Routes.zipcode, badge: null, ), _buildMenuItem( icon: Icons.business_outlined, title: '회사 관리', route: Routes.company, isActive: currentRoute == Routes.company, badge: null, ), _buildMenuItem( icon: Icons.people_outlined, title: '사용자 관리', route: Routes.user, isActive: currentRoute == Routes.user, badge: null, ), _buildMenuItem( icon: Icons.build_circle_outlined, title: '유지보수 관리', route: Routes.maintenance, isActive: currentRoute == Routes.maintenance || currentRoute == Routes.maintenanceSchedule || currentRoute == Routes.maintenanceAlert || currentRoute == Routes.maintenanceHistory, badge: null, hasSubMenu: true, subMenuItems: collapsed ? [] : [ _buildSubMenuItem( title: '일정 관리', route: Routes.maintenanceSchedule, isActive: currentRoute == Routes.maintenanceSchedule, ), _buildSubMenuItem( title: '알림 대시보드', route: Routes.maintenanceAlert, isActive: currentRoute == Routes.maintenanceAlert, ), _buildSubMenuItem( title: '이력 조회', route: Routes.maintenanceHistory, isActive: currentRoute == Routes.maintenanceHistory, ), ], ), 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, bool hasSubMenu = false, List subMenuItems = const [], }) { return Column( children: [ 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: Colors.orange, borderRadius: BorderRadius.circular(10), ), child: Text( badge, style: ShadcnTheme.caption.copyWith( color: Colors.white, fontWeight: FontWeight.w600, ), ), ), ], ], if (collapsed && badge != null) ...[ Positioned( right: 0, top: 0, child: Container( width: 8, height: 8, decoration: BoxDecoration( color: Colors.orange, shape: BoxShape.circle, ), ), ), ], ], ), ), ), ), if (hasSubMenu && subMenuItems.isNotEmpty) ...subMenuItems, ], ); } Widget _buildSubMenuItem({ required String title, required String route, required bool isActive, }) { return AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(left: 40, bottom: 4), child: InkWell( onTap: () => onRouteChanged(route), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), child: Container( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing2, ), decoration: BoxDecoration( color: isActive ? ShadcnTheme.primaryLight.withValues(alpha: 0.5) : Colors.transparent, borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: Row( children: [ Container( width: 4, height: 4, decoration: BoxDecoration( color: isActive ? ShadcnTheme.primary : ShadcnTheme.foregroundSecondary, shape: BoxShape.circle, ), ), const SizedBox(width: ShadcnTheme.spacing3), Expanded( child: Text( title, style: ShadcnTheme.bodySmall.copyWith( color: isActive ? ShadcnTheme.primary : ShadcnTheme.foreground, fontWeight: isActive ? FontWeight.w600 : FontWeight.w400, ), ), ), ], ), ), ), ); } /// 활성화 상태일 때 채워진 아이콘 반환 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.location_on_outlined: return Icons.location_on; case Icons.business_outlined: return Icons.business; case Icons.people_outlined: return Icons.people; case Icons.support_outlined: return Icons.support; case Icons.build_circle_outlined: return Icons.build_circle; case Icons.bug_report_outlined: return Icons.bug_report; case Icons.analytics_outlined: return Icons.analytics; case Icons.factory_outlined: return Icons.factory; case Icons.category_outlined: return Icons.category; default: return outlinedIcon; } } }