프로젝트 최초 커밋

This commit is contained in:
JiWoong Sul
2025-07-02 17:45:44 +09:00
commit e346f83c97
235 changed files with 23139 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/screens/sidebar/widgets/sidebar_menu_header.dart';
import 'package:superport/screens/sidebar/widgets/sidebar_menu_footer.dart';
import 'package:superport/screens/sidebar/widgets/sidebar_menu_item.dart';
import 'package:superport/screens/sidebar/widgets/sidebar_menu_submenu.dart';
import 'package:superport/screens/sidebar/widgets/sidebar_menu_types.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
import 'package:superport/screens/login/widgets/login_view.dart'; // AnimatedBoatIcon import
import 'package:wave/wave.dart';
import 'package:wave/config.dart';
// 사이드바 메뉴 메인 위젯 (조립만 담당)
class SidebarMenu extends StatefulWidget {
final String currentRoute;
final Function(String) onRouteChanged;
const SidebarMenu({
super.key,
required this.currentRoute,
required this.onRouteChanged,
});
@override
State<SidebarMenu> createState() => _SidebarMenuState();
}
class _SidebarMenuState extends State<SidebarMenu> {
// 장비 관리 메뉴 확장 상태
bool _isEquipmentMenuExpanded = false;
// hover 상태 관리
String? _hoveredRoute;
@override
void initState() {
super.initState();
_updateExpandedState();
}
@override
void didUpdateWidget(SidebarMenu oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.currentRoute != widget.currentRoute) {
_updateExpandedState();
}
}
// 현재 경로에 따라 장비 관리 메뉴 확장 상태 업데이트
void _updateExpandedState() {
final bool isEquipmentRoute =
widget.currentRoute == Routes.equipment ||
widget.currentRoute == Routes.equipmentInList ||
widget.currentRoute == Routes.equipmentOutList ||
widget.currentRoute == Routes.equipmentRentList;
setState(() {
_isEquipmentMenuExpanded = isEquipmentRoute;
});
}
// 장비 관리 메뉴 확장/축소 토글
void _toggleEquipmentMenu() {
setState(() {
_isEquipmentMenuExpanded = !_isEquipmentMenuExpanded;
});
}
@override
Widget build(BuildContext context) {
// SRP 분할: 각 역할별 위젯 조립
return Container(
width: 260,
color: const Color(0xFFF4F6F8), // 연회색 배경
child: Column(
children: [
const SidebarMenuHeader(),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SidebarMenuItem(
icon: Icons.dashboard,
title: '대시보드',
route: Routes.home,
isActive: widget.currentRoute == Routes.home,
isHovered: _hoveredRoute == Routes.home,
onTap: () => widget.onRouteChanged(Routes.home),
),
const SizedBox(height: 4),
SidebarMenuWithSubmenu(
icon: Icons.inventory,
title: '장비 관리',
route: Routes.equipment,
subItems: const [
SidebarSubMenuItem(
title: '입고',
route: Routes.equipmentInList,
),
SidebarSubMenuItem(
title: '출고',
route: Routes.equipmentOutList,
),
SidebarSubMenuItem(
title: '대여',
route: Routes.equipmentRentList,
),
],
isExpanded: _isEquipmentMenuExpanded,
isMenuActive: widget.currentRoute == Routes.equipment,
isSubMenuActive: [
Routes.equipmentInList,
Routes.equipmentOutList,
Routes.equipmentRentList,
].contains(widget.currentRoute),
isHovered: _hoveredRoute == Routes.equipment,
onToggleExpanded: _toggleEquipmentMenu,
currentRoute: widget.currentRoute,
onRouteChanged: widget.onRouteChanged,
),
const SizedBox(height: 4),
SidebarMenuItem(
icon: Icons.location_on,
title: '입고지 관리',
route: Routes.warehouseLocation,
isActive: widget.currentRoute == Routes.warehouseLocation,
isHovered: _hoveredRoute == Routes.warehouseLocation,
onTap:
() => widget.onRouteChanged(Routes.warehouseLocation),
),
const SizedBox(height: 4),
SidebarMenuItem(
icon: Icons.business,
title: '회사 관리',
route: Routes.company,
isActive: widget.currentRoute == Routes.company,
isHovered: _hoveredRoute == Routes.company,
onTap: () => widget.onRouteChanged(Routes.company),
),
const SizedBox(height: 4),
SidebarMenuItem(
icon: Icons.vpn_key,
title: '유지보수 관리',
route: Routes.license,
isActive: widget.currentRoute == Routes.license,
isHovered: _hoveredRoute == Routes.license,
onTap: () => widget.onRouteChanged(Routes.license),
),
const SizedBox(height: 4),
SidebarMenuItem(
icon: Icons.category,
title: '물품 관리',
route: Routes.goods,
isActive: widget.currentRoute == Routes.goods,
isHovered: _hoveredRoute == Routes.goods,
onTap: () => widget.onRouteChanged(Routes.goods),
),
],
),
),
),
),
const SidebarMenuFooter(),
],
),
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
// 사이드바 푸터 위젯
class SidebarMenuFooter extends StatelessWidget {
const SidebarMenuFooter({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 48,
alignment: Alignment.center,
child: const Text(
'© 2025 CClabs. All rights reserved.',
style: TextStyle(fontSize: 11, color: Colors.black), // 블랙으로 변경
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:wave/wave.dart';
import 'package:wave/config.dart';
import 'package:superport/screens/login/widgets/login_view.dart'; // AnimatedBoatIcon import
// 사이드바 헤더 위젯
class SidebarMenuHeader extends StatelessWidget {
const SidebarMenuHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 88,
width: double.infinity,
padding: const EdgeInsets.only(left: 0, right: 0), // 아이콘을 더 좌측으로
child: Stack(
alignment: Alignment.centerLeft,
children: [
// Wave 배경
Positioned.fill(
child: Opacity(
opacity: 0.50, // subtle하게
child: WaveWidget(
config: CustomConfig(
gradients: [
[Color(0xFFB6E0FE), Color(0xFF3182CE)],
[
Color.fromARGB(255, 31, 83, 132),
Color.fromARGB(255, 9, 49, 92),
],
],
durations: [4800, 6000],
heightPercentages: [0.48, 0.38],
blur: const MaskFilter.blur(BlurStyle.solid, 6),
gradientBegin: Alignment.topLeft,
gradientEnd: Alignment.bottomRight,
),
waveAmplitude: 8,
size: Size.infinite,
),
),
),
// 아이콘+텍스트
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(width: 24), // 아이콘을 더 좌측으로
SizedBox(
width: 36,
height: 36,
child: AnimatedBoatIcon(color: Colors.white, size: 60),
),
const SizedBox(width: 24),
const Text(
'supERPort',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: -2.5,
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
// 단일 메뉴 아이템 위젯
class SidebarMenuItem extends StatelessWidget {
final IconData icon;
final String title;
final String route;
final bool isActive;
final bool isHovered;
final bool isSubItem;
final VoidCallback onTap;
const SidebarMenuItem({
super.key,
required this.icon,
required this.title,
required this.route,
required this.isActive,
required this.isHovered,
this.isSubItem = false,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: onTap,
child: Container(
height: 44,
alignment: Alignment.centerLeft,
margin: const EdgeInsets.symmetric(
vertical: 2,
horizontal: 6,
), // 외부 여백
padding: EdgeInsets.only(left: isSubItem ? 48 : 24, right: 24),
decoration: BoxDecoration(
color:
isActive
? Colors.white
: (isHovered
? const Color(0xFFE9EDF2)
: Colors.transparent),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(
icon,
size: 18,
color:
isActive ? AppThemeTailwind.primary : AppThemeTailwind.dark,
),
const SizedBox(width: 10),
Text(
title,
style: TextStyle(
color:
isActive
? AppThemeTailwind.primary
: AppThemeTailwind.dark,
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
fontSize: 14,
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/sidebar/widgets/sidebar_menu_item.dart';
import 'package:superport/screens/sidebar/widgets/sidebar_menu_types.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
// 서브메뉴(확장/축소, 하위 아이템) 위젯
class SidebarMenuWithSubmenu extends StatelessWidget {
final IconData icon;
final String title;
final String route;
final List<SidebarSubMenuItem> subItems;
final bool isExpanded;
final bool isMenuActive;
final bool isSubMenuActive;
final bool isHovered;
final VoidCallback onToggleExpanded;
final String currentRoute;
final void Function(String) onRouteChanged;
const SidebarMenuWithSubmenu({
super.key,
required this.icon,
required this.title,
required this.route,
required this.subItems,
required this.isExpanded,
required this.isMenuActive,
required this.isSubMenuActive,
required this.isHovered,
required this.onToggleExpanded,
required this.currentRoute,
required this.onRouteChanged,
});
@override
Widget build(BuildContext context) {
final bool isHighlighted = isMenuActive || isSubMenuActive;
return Column(
children: [
MouseRegion(
cursor: SystemMouseCursors.click,
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () {
onToggleExpanded();
onRouteChanged(route);
},
child: Container(
height: 44,
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
padding: const EdgeInsets.only(left: 24, right: 24),
decoration: BoxDecoration(
color:
isMenuActive
? Colors.white
: (isHovered
? const Color(0xFFE9EDF2)
: Colors.transparent),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(
icon,
size: 18,
color:
isHighlighted
? AppThemeTailwind.primary
: AppThemeTailwind.dark,
),
const SizedBox(width: 10),
Text(
title,
style: TextStyle(
color:
isHighlighted
? AppThemeTailwind.primary
: AppThemeTailwind.dark,
fontWeight:
isHighlighted ? FontWeight.bold : FontWeight.normal,
fontSize: 14,
),
),
const Spacer(),
Icon(
isExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 20,
color: AppThemeTailwind.muted,
),
],
),
),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: isExpanded ? 1 : 0,
child: Column(
children:
subItems.map((item) {
return SidebarMenuItem(
icon: Icons.circle,
title: item.title,
route: item.route,
isActive: currentRoute == item.route,
isHovered: false, // hover는 상위에서 관리
isSubItem: true,
onTap: () => onRouteChanged(item.route),
);
}).toList(),
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,11 @@
// 서브메뉴 아이템 타입 정의 파일
// 이 파일은 사이드바 메뉴에서 사용하는 서브메뉴 아이템 타입만 정의합니다.
class SidebarSubMenuItem {
// 서브메뉴의 제목
final String title;
// 서브메뉴의 라우트
final String route;
const SidebarSubMenuItem({required this.title, required this.route});
}