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 :
|