/// 메인 셸 - 툴바 + 바텀 네비게이션 library; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../core/constants.dart'; /// 메인 앱 셸 (툴바 + 바텀 네비게이션 + 콘텐츠) class MainShell extends StatefulWidget { final Widget child; const MainShell({super.key, required this.child}); @override State createState() => _MainShellState(); } class _MainShellState extends State { DateTime? _lastBackPressed; /// 뒤로가기 처리 - 두 번 눌러서 종료 Future _onWillPop() async { final now = DateTime.now(); if (_lastBackPressed == null || now.difference(_lastBackPressed!) > const Duration(seconds: 2)) { _lastBackPressed = now; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('한 번 더 누르면 종료됩니다'), duration: Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); return false; } // 앱 종료 SystemNavigator.pop(); return true; } @override Widget build(BuildContext context) { final location = GoRouterState.of(context).uri.path; final isMembersPage = location == '/members'; return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; await _onWillPop(); }, child: Scaffold( backgroundColor: AppColors.background, // 앱바 (툴바) - 멤버 페이지에서는 그림자 제거 (인디케이터와 계단 효과 방지) appBar: PreferredSize( preferredSize: const Size.fromHeight(56), child: Container( decoration: BoxDecoration( color: Colors.white, boxShadow: isMembersPage ? null : [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: SafeArea( child: SizedBox( height: 56, child: Center( child: Text( _getTitle(context), style: const TextStyle( fontFamily: 'Pretendard', color: AppColors.primary, fontSize: 20, fontWeight: FontWeight.bold, ), ), ), ), ), ), ), // 콘텐츠 body: widget.child, // 바텀 네비게이션 bottomNavigationBar: const _BottomNavBar(), ), ); } /// 현재 경로에 따른 타이틀 반환 String _getTitle(BuildContext context) { final location = GoRouterState.of(context).uri.path; switch (location) { case '/': return 'fromis_9'; case '/members': return '멤버'; case '/album': return '앨범'; case '/schedule': return '일정'; default: return 'fromis_9'; } } } /// 바텀 네비게이션 바 class _BottomNavBar extends StatelessWidget { const _BottomNavBar(); @override Widget build(BuildContext context) { final location = GoRouterState.of(context).uri.path; return Container( decoration: const BoxDecoration( color: Colors.white, border: Border( top: BorderSide(color: AppColors.border, width: 1), ), ), child: SafeArea( child: SizedBox( height: 64, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _NavItem( iconName: 'home', label: '홈', isActive: location == '/', onTap: () => context.go('/'), ), _NavItem( iconName: 'users', label: '멤버', isActive: location == '/members', onTap: () => context.go('/members'), ), _NavItem( iconName: 'disc-3', label: '앨범', isActive: location.startsWith('/album'), onTap: () => context.go('/album'), ), _NavItem( iconName: 'calendar', label: '일정', isActive: location.startsWith('/schedule'), onTap: () => context.go('/schedule'), ), ], ), ), ), ); } } /// 네비게이션 아이템 - SVG 아이콘 사용으로 strokeWidth 조절 가능 class _NavItem extends StatelessWidget { final String iconName; final String label; final bool isActive; final VoidCallback onTap; const _NavItem({ required this.iconName, required this.label, required this.isActive, required this.onTap, }); /// SVG 아이콘 문자열 생성 (strokeWidth 동적 조절) String _getSvgString(String name, double strokeWidth) { const icons = { 'home': '', 'users': '', 'disc-3': '', 'calendar': '', }; return '''${icons[name]}'''; } @override Widget build(BuildContext context) { final color = isActive ? AppColors.primary : AppColors.textTertiary; // 웹과 동일: 활성화 시 strokeWidth=2.5, 비활성화 시 strokeWidth=2 final strokeWidth = isActive ? 2.5 : 2.0; return Expanded( child: InkWell( onTap: onTap, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 22, height: 22, child: SvgPicture.string( _getSvgString(iconName, strokeWidth), colorFilter: ColorFilter.mode(color, BlendMode.srcIn), ), ), const SizedBox(height: 4), Text( label, style: TextStyle( fontSize: 12, fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, color: color, ), ), ], ), ), ); } }