LCOV - code coverage report
Current view: top level - lib - main.dart Coverage Total Hit
Test: lcov.info Lines: 16.4 % 134 22
Test Date: 2025-08-12 03:21:51 Functions: - 0 0

            Line data    Source code
       1              : import 'package:flutter/material.dart';
       2              : import 'dart:async';
       3              : import 'package:provider/provider.dart';
       4              : import 'package:flutter_localizations/flutter_localizations.dart';
       5              : 
       6              : import 'services/auth_service.dart';
       7              : import 'services/club_service.dart';
       8              : import 'services/member_service.dart';
       9              : import 'services/event_service.dart';
      10              : import 'services/score_service.dart';
      11              : import 'services/notification_service.dart';
      12              : import 'screens/auth/login_screen.dart';
      13              : import 'screens/auth/profile_screen.dart';
      14              : import 'screens/club/dashboard_screen.dart';
      15              : import 'screens/club/members_screen.dart';
      16              : import 'screens/club/events_screen.dart';
      17              : import 'screens/club/club_settings_screen.dart';
      18              : import 'screens/score/club_statistics_screen.dart';
      19              : 
      20              : // 전역 네비게이터 키 (인증 오류 처리용)
      21            3 : final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
      22              : 
      23            0 : void main() async {
      24            0 :   WidgetsFlutterBinding.ensureInitialized();
      25              :   
      26              :   // 알림 서비스 초기화
      27            0 :   final notificationService = NotificationService();
      28            0 :   await notificationService.initialize();
      29              :   
      30            0 :   runApp(const MyApp());
      31              : }
      32              : 
      33              : class MyApp extends StatelessWidget {
      34           25 :   const MyApp({super.key});
      35              : 
      36            1 :   @override
      37              :   Widget build(BuildContext context) {
      38            1 :     return MultiProvider(
      39            1 :       providers: [
      40            3 :         ChangeNotifierProvider(create: (_) => AuthService()),
      41            1 :         ChangeNotifierProvider(create: (_) => ClubService()),
      42            1 :         ChangeNotifierProvider(create: (_) => MemberService()),
      43            1 :         ChangeNotifierProvider(create: (_) => EventService()),
      44            1 :         ChangeNotifierProvider(create: (_) => ScoreService()),
      45              :       ],
      46            1 :       child: Consumer<AuthService>(
      47            1 :         builder: (context, authService, _) {
      48              :           // 앱 시작 시 인증 서비스 초기화 (타이머 생성 회피: microtask 사용)
      49            2 :           Future.microtask(() {
      50            1 :             if (!authService.isInitialized) {
      51            1 :               authService.initialize();
      52              :             }
      53              :           });
      54              :           
      55            1 :           return MaterialApp(
      56            1 :             navigatorKey: navigatorKey, // 인증 오류 처리를 위한 전역 네비게이터 키
      57              :             title: '볼링 매니저',
      58            1 :             theme: ThemeData(
      59            1 :               colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
      60              :               useMaterial3: true,
      61              :               appBarTheme: const AppBarTheme(
      62              :                 backgroundColor: Colors.blue,
      63              :                 foregroundColor: Colors.white,
      64              :               ),
      65              :             ),
      66              :             // 로케일 설정 추가
      67              :             localizationsDelegates: const [
      68              :               GlobalMaterialLocalizations.delegate,
      69              :               GlobalWidgetsLocalizations.delegate,
      70              :               GlobalCupertinoLocalizations.delegate,
      71              :             ],
      72              :             supportedLocales: const [
      73              :               Locale('ko', 'KR'),
      74              :               Locale('en', 'US'),
      75              :             ],
      76              :             locale: const Locale('ko', 'KR'),
      77            1 :             home: authService.isAuthenticated ? const HomeScreen() : const LoginScreen(),
      78            1 :             routes: {
      79            0 :               '/login': (context) => const LoginScreen(),
      80            0 :               '/profile': (context) => const ProfileScreen(),
      81            0 :               ClubSettingsScreen.routeName: (context) => const ClubSettingsScreen(),
      82              :             },
      83              :           );
      84              :         },
      85              :       ),
      86              :     );
      87              :   }
      88              : }
      89              : 
      90              : class HomeScreen extends StatefulWidget {
      91           24 :   const HomeScreen({super.key});
      92              : 
      93            0 :   @override
      94            0 :   State<HomeScreen> createState() => _HomeScreenState();
      95              : }
      96              : 
      97              : class _HomeScreenState extends State<HomeScreen> {
      98              :   int _selectedIndex = 0;
      99              :   bool _isInit = false;
     100              :   
     101            0 :   static final List<Widget> _widgetOptions = <Widget>[
     102              :     const DashboardScreen(),
     103              :     const MembersScreen(),
     104              :     const EventsScreen(),
     105              :     const ClubStatisticsScreen(),
     106              :     const ProfileScreen(),
     107              :   ];
     108              :   
     109            0 :   void _onItemTapped(int index) {
     110            0 :     setState(() {
     111            0 :       _selectedIndex = index;
     112              :     });
     113              :   }
     114              :   
     115              :   // 클럽 선택 다이얼로그 표시
     116            0 :   Future<void> _showClubSelectionDialog(BuildContext context) async {
     117            0 :     final clubService = Provider.of<ClubService>(context, listen: false);
     118            0 :     final authService = Provider.of<AuthService>(context, listen: false);
     119              :     
     120              :     // 사용자의 모든 클럽 가져오기
     121            0 :     if (clubService.clubs.isEmpty) {
     122              :       try {
     123            0 :         await clubService.fetchUserClubs();
     124              :       } catch (e) {
     125            0 :         ScaffoldMessenger.of(context).showSnackBar(
     126            0 :           SnackBar(content: Text('클럽 목록을 불러오는데 실패했습니다: $e')),
     127              :         );
     128              :         return;
     129              :       }
     130              :     }
     131              :     
     132            0 :     if (clubService.clubs.isEmpty) {
     133            0 :       ScaffoldMessenger.of(context).showSnackBar(
     134              :         const SnackBar(content: Text('소속된 클럽이 없습니다')),
     135              :       );
     136              :       return;
     137              :     }
     138              :     
     139            0 :     if (!context.mounted) return;
     140              :     
     141              :     // 다이얼로그 표시
     142            0 :     showDialog(
     143              :       context: context,
     144            0 :       builder: (BuildContext context) {
     145            0 :         return AlertDialog(
     146              :           title: const Text('클럽 선택'),
     147            0 :           content: SizedBox(
     148              :             width: double.maxFinite,
     149            0 :             child: ListView.builder(
     150              :               shrinkWrap: true,
     151            0 :               itemCount: clubService.clubs.length,
     152            0 :               itemBuilder: (context, index) {
     153            0 :                 final club = clubService.clubs[index];
     154            0 :                 final isSelected = clubService.currentClub?.id == club.id;
     155              :                 
     156            0 :                 return ListTile(
     157            0 :                   title: Text(club.name),
     158            0 :                   subtitle: Text(club.description ?? ''),
     159              :                   trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null,
     160            0 :                   onTap: () async {
     161            0 :                     Navigator.of(context).pop();
     162              :                     
     163              :                     if (!isSelected) {
     164              :                       try {
     165              :                         // 백엔드 세션에 clubId 저장하고 클럽 정보 가져오기
     166            0 :                         await clubService.selectClub(club.id);
     167              :                         
     168              :                         // 관련 서비스 초기화
     169            0 :                         final memberService = Provider.of<MemberService>(context, listen: false);
     170            0 :                         final eventService = Provider.of<EventService>(context, listen: false);
     171            0 :                         final scoreService = Provider.of<ScoreService>(context, listen: false);
     172              :                         
     173            0 :                         memberService.initialize(authService.token!);
     174            0 :                         eventService.initialize(authService.token!);
     175            0 :                         scoreService.initialize(authService.token!);
     176              :                         
     177              :                         // 데이터 다시 로드
     178            0 :                         await Future.wait([
     179            0 :                           memberService.fetchClubMembers(),
     180            0 :                           eventService.fetchClubEvents(),
     181              :                         ]);
     182              :                         
     183            0 :                         if (context.mounted) {
     184            0 :                           ScaffoldMessenger.of(context).showSnackBar(
     185            0 :                             SnackBar(content: Text('${club.name} 클럽으로 변경되었습니다')),
     186              :                           );
     187              :                         }
     188              :                       } catch (e) {
     189            0 :                         if (context.mounted) {
     190            0 :                           ScaffoldMessenger.of(context).showSnackBar(
     191            0 :                             SnackBar(content: Text('클럽 변경 중 오류가 발생했습니다: $e')),
     192              :                           );
     193              :                         }
     194              :                       }
     195              :                     }
     196              :                   },
     197              :                 );
     198              :               },
     199              :             ),
     200              :           ),
     201            0 :           actions: [
     202            0 :             TextButton(
     203            0 :               onPressed: () => Navigator.of(context).pop(),
     204              :               child: const Text('취소'),
     205              :             ),
     206              :           ],
     207              :         );
     208              :       },
     209              :     );
     210              :   }
     211              : 
     212            0 :   @override
     213              :   void didChangeDependencies() {
     214            0 :     super.didChangeDependencies();
     215            0 :     if (!_isInit) {
     216            0 :       _initializeServices();
     217            0 :       _isInit = true;
     218              :     }
     219              :   }
     220              : 
     221            0 :   Future<void> _initializeServices() async {
     222              :     try {
     223            0 :       final authService = Provider.of<AuthService>(context, listen: false);
     224            0 :       final clubService = Provider.of<ClubService>(context, listen: false);
     225            0 :       final scoreService = Provider.of<ScoreService>(context, listen: false);
     226              :       
     227            0 :       if (authService.isAuthenticated && authService.token != null) {
     228              :         // 클럽 서비스 초기화
     229            0 :         await clubService.initialize(authService.token!);
     230              :         
     231              :         // 클럽이 선택된 경우 점수 서비스 초기화
     232            0 :         if (clubService.currentClub != null) {
     233            0 :           scoreService.initialize(authService.token!);
     234              :         } else {
     235              :           // 현재 선택된 클럽이 없는 경우
     236              :           // 사용자의 클럽 목록 가져오기
     237            0 :           await clubService.fetchUserClubs();
     238              :           
     239              :           // 클럽이 하나만 있으면 자동으로 선택
     240            0 :           if (clubService.clubs.length == 1) {
     241              :             try {
     242            0 :               await clubService.selectClub(clubService.clubs[0].id);
     243            0 :               scoreService.initialize(authService.token!);
     244              :               
     245            0 :               if (context.mounted) {
     246            0 :                 ScaffoldMessenger.of(context).showSnackBar(
     247            0 :                   SnackBar(content: Text('${clubService.clubs[0].name} 클럽이 자동으로 선택되었습니다')),
     248              :                 );
     249              :               }
     250              :             } catch (e) {
     251            0 :               if (context.mounted) {
     252            0 :                 ScaffoldMessenger.of(context).showSnackBar(
     253            0 :                   SnackBar(content: Text('클럽 자동 선택 중 오류가 발생했습니다: $e')),
     254              :                 );
     255              :               }
     256              :             }
     257            0 :           } else if (clubService.clubs.isNotEmpty) {
     258              :             // 클럽이 여러 개 있으면 선택 다이얼로그 표시 (타이머 생성 회피: microtask 사용)
     259            0 :             Future.microtask(() {
     260            0 :               if (context.mounted) {
     261            0 :                 _showClubSelectionDialog(context);
     262              :               }
     263              :             });
     264              :           } else {
     265              :             // 클럽이 없는 경우 메시지 표시
     266            0 :             if (context.mounted) {
     267            0 :               ScaffoldMessenger.of(context).showSnackBar(
     268              :                 const SnackBar(content: Text('소속된 클럽이 없습니다. 클럽을 생성하거나 초대를 요청하세요.')),
     269              :               );
     270              :             }
     271              :           }
     272              :         }
     273              :       }
     274              :     } catch (e) {
     275              :       // 오류 처리
     276            0 :       if (context.mounted) {
     277            0 :         ScaffoldMessenger.of(context).showSnackBar(
     278            0 :           SnackBar(content: Text('서비스 초기화 중 오류가 발생했습니다: $e')),
     279              :         );
     280              :       }
     281              :     }
     282              :   }
     283              : 
     284            0 :   @override
     285              :   Widget build(BuildContext context) {
     286            0 :     return Scaffold(
     287            0 :       appBar: AppBar(
     288            0 :         title: Consumer<ClubService>(
     289            0 :           builder: (context, clubService, child) {
     290            0 :             return InkWell(
     291            0 :               onTap: () => _showClubSelectionDialog(context),
     292            0 :               child: Row(
     293              :                 mainAxisSize: MainAxisSize.min,
     294            0 :                 children: [
     295            0 :                   Text(
     296            0 :                     clubService.currentClub?.name ?? '볼링 매니저',
     297              :                     style: const TextStyle(fontWeight: FontWeight.bold),
     298              :                   ),
     299              :                   const SizedBox(width: 4),
     300              :                   const Icon(Icons.arrow_drop_down, size: 20),
     301              :                 ],
     302              :               ),
     303              :             );
     304              :           },
     305              :         ),
     306            0 :         actions: [
     307            0 :           IconButton(
     308              :             icon: const Icon(Icons.notifications),
     309            0 :             onPressed: () async {
     310              :               // 알림 권한 요청
     311            0 :               final notificationService = NotificationService();
     312            0 :               final hasPermission = await notificationService.requestPermission();
     313              :               
     314            0 :               if (context.mounted) {
     315              :                 if (hasPermission) {
     316            0 :                   ScaffoldMessenger.of(context).showSnackBar(
     317              :                     const SnackBar(content: Text('알림 권한이 허용되었습니다')),
     318              :                   );
     319              :                 } else {
     320            0 :                   ScaffoldMessenger.of(context).showSnackBar(
     321              :                     const SnackBar(content: Text('알림 권한이 거부되었습니다. 설정에서 권한을 허용해주세요')),
     322              :                   );
     323              :                 }
     324              :               }
     325              :             },
     326              :           ),
     327              :         ],
     328              :       ),
     329            0 :       body: _widgetOptions.elementAt(_selectedIndex),
     330            0 :       bottomNavigationBar: BottomNavigationBar(
     331              :         type: BottomNavigationBarType.fixed,
     332              :         items: const <BottomNavigationBarItem>[
     333              :           BottomNavigationBarItem(
     334              :             icon: Icon(Icons.dashboard),
     335              :             label: '대시보드',
     336              :           ),
     337              :           BottomNavigationBarItem(
     338              :             icon: Icon(Icons.people),
     339              :             label: '회원',
     340              :           ),
     341              :           BottomNavigationBarItem(
     342              :             icon: Icon(Icons.event),
     343              :             label: '이벤트',
     344              :           ),
     345              :           BottomNavigationBarItem(
     346              :             icon: Icon(Icons.score),
     347              :             label: '통계',
     348              :           ),
     349              :           BottomNavigationBarItem(
     350              :             icon: Icon(Icons.person),
     351              :             label: '프로필',
     352              :           ),
     353              :         ],
     354            0 :         currentIndex: _selectedIndex,
     355              :         selectedItemColor: Colors.blue,
     356            0 :         onTap: _onItemTapped,
     357              :       ),
     358              :     );
     359              :   }
     360              : }
     361              : 
     362              : 
        

Generated by: LCOV version 2.3.1-1