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