import 'package:flutter/material.dart'; import 'dart:async'; import 'package:provider/provider.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'services/auth_service.dart'; import 'services/club_service.dart'; import 'services/member_service.dart'; import 'services/event_service.dart'; import 'services/score_service.dart'; import 'services/notification_service.dart'; import 'screens/auth/login_screen.dart'; import 'screens/auth/profile_screen.dart'; import 'screens/club/dashboard_screen.dart'; import 'screens/club/members_screen.dart'; import 'screens/club/events_screen.dart'; import 'screens/club/club_settings_screen.dart'; import 'screens/score/club_statistics_screen.dart'; import 'widgets/dialog_actions.dart'; // 전역 네비게이터 키 (인증 오류 처리용) final GlobalKey navigatorKey = GlobalKey(); void main() async { WidgetsFlutterBinding.ensureInitialized(); // 알림 서비스 초기화 final notificationService = NotificationService(); await notificationService.initialize(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => AuthService()), ChangeNotifierProvider(create: (_) => ClubService()), ChangeNotifierProvider(create: (_) => MemberService()), ChangeNotifierProvider(create: (_) => EventService()), ChangeNotifierProvider(create: (_) => ScoreService()), ], child: Consumer( builder: (context, authService, _) { // 앱 시작 시 인증 서비스 초기화 (타이머 생성 회피: microtask 사용) Future.microtask(() { if (!authService.isInitialized) { authService.initialize(); } }); return MaterialApp( navigatorKey: navigatorKey, // 인증 오류 처리를 위한 전역 네비게이터 키 title: '볼링 매니저', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, visualDensity: VisualDensity.adaptivePlatformDensity, appBarTheme: const AppBarTheme( backgroundColor: Colors.blue, foregroundColor: Colors.white, ), chipTheme: ChipThemeData( shape: StadiumBorder(side: BorderSide.none), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 0), ), dialogTheme: const DialogThemeData( shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87), contentTextStyle: TextStyle(fontSize: 14, color: Colors.black87), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), shape: const StadiumBorder(), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), shape: const StadiumBorder(), ), ), cardTheme: const CardThemeData( elevation: 1.5, margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6), shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), ), inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), ), snackBarTheme: const SnackBarThemeData( behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8))), actionTextColor: Colors.white, ), ), darkTheme: ThemeData( brightness: Brightness.dark, colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark), useMaterial3: true, visualDensity: VisualDensity.adaptivePlatformDensity, textTheme: const TextTheme( bodyMedium: TextStyle(color: Colors.white70), bodySmall: TextStyle(color: Colors.white60), labelMedium: TextStyle(color: Colors.white70), ), iconTheme: IconThemeData( color: Colors.grey.shade200, // 다크에서 아이콘 대비 상향 ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), shape: const StadiumBorder(), side: BorderSide(color: Colors.grey.shade400), // 다크에서 border 대비 강화 ), ), cardTheme: const CardThemeData( elevation: 0.5, // 다크에서 쉐도우 약화 margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6), shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), ), snackBarTheme: const SnackBarThemeData( behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8))), ), ), // 로케일 설정 추가 localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ Locale('ko', 'KR'), Locale('en', 'US'), ], locale: const Locale('ko', 'KR'), home: authService.isAuthenticated ? const HomeScreen() : const LoginScreen(), routes: { '/login': (context) => const LoginScreen(), '/profile': (context) => const ProfileScreen(), ClubSettingsScreen.routeName: (context) => const ClubSettingsScreen(), }, ); }, ), ); } } class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { int _selectedIndex = 0; bool _isInit = false; static final List _widgetOptions = [ const DashboardScreen(), const MembersScreen(), const EventsScreen(), const ClubStatisticsScreen(), const ProfileScreen(), ]; void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); } // 클럽 선택 다이얼로그 표시 (context를 인자로 받지 않음) Future _showClubSelectionDialog() async { // 현재 시점의 컨텍스트를 통해 의존 객체를 가져오되, BuildContext 자체는 보관하지 않는다. final currentContext = navigatorKey.currentContext!; final clubService = Provider.of(currentContext, listen: false); final authService = Provider.of(currentContext, listen: false); final messenger = ScaffoldMessenger.of(currentContext); // onTap 내부에서 사용할 서비스들을 await 이전에 캡처 final memberService = Provider.of(currentContext, listen: false); final eventService = Provider.of(currentContext, listen: false); final scoreService = Provider.of(currentContext, listen: false); // 사용자의 모든 클럽 가져오기 if (clubService.clubs.isEmpty) { try { await clubService.fetchUserClubs(); } catch (e) { messenger.showSnackBar( SnackBar(content: Text('클럽 목록을 불러오는데 실패했습니다: $e')), ); return; } } if (clubService.clubs.isEmpty) { messenger.showSnackBar( const SnackBar(content: Text('소속된 클럽이 없습니다')), ); return; } // 다이얼로그 표시 showDialog( context: navigatorKey.currentContext!, builder: (BuildContext dialogContext) { return AlertDialog( title: const Text('클럽 선택'), content: SizedBox( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: clubService.clubs.length, itemBuilder: (context, index) { final club = clubService.clubs[index]; final isSelected = clubService.currentClub?.id == club.id; return ListTile( title: Text(club.name), subtitle: Text(club.description ?? ''), trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null, onTap: () async { Navigator.of(dialogContext).pop(); if (!isSelected) { try { // 백엔드 세션에 clubId 저장하고 클럽 정보 가져오기 await clubService.selectClub(club.id); // 관련 서비스 초기화 memberService.initialize(authService.token!); eventService.initialize(authService.token!); scoreService.initialize(authService.token!); // 데이터 다시 로드 await Future.wait([ memberService.fetchClubMembers(), eventService.fetchClubEvents(), ]); messenger.showSnackBar( SnackBar(content: Text('${club.name} 클럽으로 변경되었습니다')), ); } catch (e) { messenger.showSnackBar( SnackBar(content: Text('클럽 변경 중 오류가 발생했습니다: $e')), ); } } }, ); }, ), ), actions: DialogActions.confirm( onCancel: () => Navigator.of(dialogContext).pop(), onConfirm: () => Navigator.of(dialogContext).pop(), confirmText: '닫기', ), ); }, ); } @override void didChangeDependencies() { super.didChangeDependencies(); if (!_isInit) { _initializeServices(); _isInit = true; } } Future _initializeServices() async { // messenger는 try/catch 전역에서 재사용 (await 이후 context 접근 회피) final messenger = ScaffoldMessenger.of(context); try { final authService = Provider.of(context, listen: false); final clubService = Provider.of(context, listen: false); final scoreService = Provider.of(context, listen: false); if (authService.isAuthenticated && authService.token != null) { // 클럽 서비스 초기화 await clubService.initialize(authService.token!); // 클럽이 선택된 경우 점수 서비스 초기화 if (clubService.currentClub != null) { scoreService.initialize(authService.token!); } else { // 현재 선택된 클럽이 없는 경우 // 사용자의 클럽 목록 가져오기 await clubService.fetchUserClubs(); // 클럽이 하나만 있으면 자동으로 선택 if (clubService.clubs.length == 1) { try { await clubService.selectClub(clubService.clubs[0].id); scoreService.initialize(authService.token!); messenger.showSnackBar( SnackBar(content: Text('${clubService.clubs[0].name} 클럽이 자동으로 선택되었습니다')), ); } catch (e) { messenger.showSnackBar( SnackBar(content: Text('클럽 자동 선택 중 오류가 발생했습니다: $e')), ); } } else if (clubService.clubs.isNotEmpty) { // 클럽이 여러 개 있으면 선택 다이얼로그 표시 (타이머 생성 회피: microtask 사용) if (!mounted) return; Future.microtask(() { _showClubSelectionDialog(); }); } else { // 클럽이 없는 경우 메시지 표시 messenger.showSnackBar( const SnackBar(content: Text('소속된 클럽이 없습니다. 클럽을 생성하거나 초대를 요청하세요.')), ); } } } } catch (e) { // 오류 처리 messenger.showSnackBar( SnackBar(content: Text('서비스 초기화 중 오류가 발생했습니다: $e')), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Consumer( builder: (context, clubService, child) { return InkWell( onTap: () => _showClubSelectionDialog(), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( clubService.currentClub?.name ?? '볼링 매니저', style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(width: 4), const Icon(Icons.arrow_drop_down, size: 20), ], ), ); }, ), actions: [ IconButton( icon: const Icon(Icons.notifications), onPressed: () async { // 알림 권한 요청 final notificationService = NotificationService(); final messenger = ScaffoldMessenger.of(context); final hasPermission = await notificationService.requestPermission(); if (hasPermission) { if (context.mounted) { messenger.showSnackBar( const SnackBar(content: Text('알림 권한이 허용되었습니다')), ); } } else { if (context.mounted) { messenger.showSnackBar( const SnackBar(content: Text('알림 권한이 거부되었습니다. 설정에서 권한을 허용해주세요')), ); } } }, ), ], ), body: _widgetOptions.elementAt(_selectedIndex), bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, items: const [ BottomNavigationBarItem( icon: Icon(Icons.dashboard), label: '대시보드', ), BottomNavigationBarItem( icon: Icon(Icons.people), label: '회원', ), BottomNavigationBarItem( icon: Icon(Icons.event), label: '이벤트', ), BottomNavigationBarItem( icon: Icon(Icons.score), label: '통계', ), BottomNavigationBarItem( icon: Icon(Icons.person), label: '프로필', ), ], currentIndex: _selectedIndex, selectedItemColor: Colors.blue, onTap: _onItemTapped, ), ); } }