Files
superport/lib/screens/common/app_layout_redesign.dart
JiWoong Sul a13c485302 feat: API 통합 2차 작업 완료
- 자동 로그인 구현: 앱 시작 시 토큰 확인 후 적절한 화면으로 라우팅
- AuthInterceptor 개선: AuthService와 통합하여 토큰 관리 일원화
- 로그아웃 기능 개선: AuthService를 사용한 API 로그아웃 처리
- 대시보드 API 연동: MockDataService에서 실제 API로 완전 전환
  - Dashboard DTO 모델 생성 (OverviewStats, RecentActivity 등)
  - DashboardRemoteDataSource 및 DashboardService 구현
  - OverviewController를 ChangeNotifier 패턴으로 개선
  - OverviewScreenRedesign에 Provider 패턴 적용
- API 통합 진행 상황 문서 업데이트
2025-07-24 15:55:05 +09:00

611 lines
19 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';
/// 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: () 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,
),
],
),
),
),
],
);
}
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,
),
),
),
],
],
),
),
);
}
}