장비관리 화면을 기준으로 전체 화면 UI 일관성 개선: - 모든 화면 검색바/버튼/드롭다운 높이 40px 통일 - 테이블 헤더 패딩 vertical 10px, 행 패딩 vertical 4px 통일 - 배지 스타일 통일 (border 제거, opacity 0.9 적용) - 페이지네이션 10개 이하에서도 항상 표시 - 테이블 헤더 폰트 스타일 통일 (fontSize: 13, fontWeight: w500) 각 화면별 수정사항: 1. 장비관리: 컬럼 너비 최적화, 검색 컴포넌트 높이 명시 2. 입고지 관리: 페이지네이션 조건 개선 3. 회사관리: UnifiedSearchBar 통합, 배지 스타일 개선 4. 유지보수: ListView.builder → map() 변경, 테이블 구조 재설계 키포인트 색상을 teal로 통일하여 브랜드 일관성 확보
653 lines
20 KiB
Dart
653 lines
20 KiB
Dart
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';
|
|
|
|
/// 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;
|
|
AuthUser? _currentUser;
|
|
late final AuthService _authService;
|
|
late Animation<double> _sidebarAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_currentRoute = widget.initialRoute;
|
|
_setupAnimations();
|
|
_authService = GetIt.instance<AuthService>();
|
|
_loadCurrentUser();
|
|
}
|
|
|
|
Future<void> _loadCurrentUser() async {
|
|
final user = await _authService.getCurrentUser();
|
|
if (mounted) {
|
|
setState(() {
|
|
_currentUser = user;
|
|
});
|
|
}
|
|
}
|
|
|
|
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();
|
|
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<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 ['홈', '입고지 관리'];
|
|
case '/test/api':
|
|
return ['홈', '개발자 도구', 'API 테스트'];
|
|
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: Colors.black),
|
|
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,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 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: _currentUser != null ? _currentUser!.name.substring(0, 1).toUpperCase() : 'U',
|
|
size: 36,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 사이드바 빌드
|
|
Widget _buildSidebar() {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.background,
|
|
),
|
|
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: Colors.black)),
|
|
),
|
|
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: _currentUser != null ? _currentUser!.name.substring(0, 1).toUpperCase() : 'U',
|
|
size: 48,
|
|
),
|
|
const SizedBox(width: ShadcnTheme.spacing4),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_currentUser?.name ?? '사용자',
|
|
style: ShadcnTheme.headingH4,
|
|
),
|
|
Text(
|
|
_currentUser?.email ?? '',
|
|
style: ShadcnTheme.bodyMuted,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: ShadcnTheme.spacing6),
|
|
const ShadcnSeparator(),
|
|
const SizedBox(height: ShadcnTheme.spacing4),
|
|
|
|
// 로그아웃 버튼
|
|
ShadcnButton(
|
|
text: '로그아웃',
|
|
onPressed: () async {
|
|
// 로딩 다이얼로그 표시
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
|
|
try {
|
|
// AuthService를 사용하여 로그아웃
|
|
final authService = GetIt.instance<AuthService>();
|
|
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: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
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,
|
|
),
|
|
|
|
const SizedBox(height: ShadcnTheme.spacing4),
|
|
const Divider(),
|
|
const SizedBox(height: ShadcnTheme.spacing4),
|
|
|
|
_buildMenuItem(
|
|
icon: Icons.bug_report,
|
|
title: 'API 테스트',
|
|
route: '/test/api',
|
|
isActive: currentRoute == '/test/api',
|
|
),
|
|
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|