Files
superport/lib/screens/common/app_layout_redesign.dart
JiWoong Sul b8f10dd588 refactor: UI 일관성 개선 및 테이블 구조 통일
장비관리 화면을 기준으로 전체 화면 UI 일관성 개선:

- 모든 화면 검색바/버튼/드롭다운 높이 40px 통일
- 테이블 헤더 패딩 vertical 10px, 행 패딩 vertical 4px 통일
- 배지 스타일 통일 (border 제거, opacity 0.9 적용)
- 페이지네이션 10개 이하에서도 항상 표시
- 테이블 헤더 폰트 스타일 통일 (fontSize: 13, fontWeight: w500)

각 화면별 수정사항:
1. 장비관리: 컬럼 너비 최적화, 검색 컴포넌트 높이 명시
2. 입고지 관리: 페이지네이션 조건 개선
3. 회사관리: UnifiedSearchBar 통합, 배지 스타일 개선
4. 유지보수: ListView.builder → map() 변경, 테이블 구조 재설계

키포인트 색상을 teal로 통일하여 브랜드 일관성 확보
2025-08-08 18:03:07 +09:00

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,
),
),
),
],
],
),
),
);
}
}