UI 전체 리디자인 및 개선사항 적용

## 주요 변경사항:

### 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>
This commit is contained in:
JiWoong Sul
2025-07-07 19:45:32 +09:00
parent e346f83c97
commit e0bc5894b2
34 changed files with 7764 additions and 571 deletions

View File

@@ -0,0 +1,574 @@
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,
),
),
),
],
],
),
),
);
}
}