Line data Source code
1 : import 'package:flutter/material.dart';
2 : import 'package:provider/provider.dart';
3 : import 'package:intl/intl.dart';
4 : import 'package:file_picker/file_picker.dart';
5 :
6 : import '../../utils/event_excel_parser.dart';
7 : import '../../services/auth_service.dart';
8 : import '../../services/club_service.dart';
9 :
10 : import '../../models/event_model.dart';
11 : import '../../services/event_service.dart';
12 : import '../../widgets/loading_indicator.dart';
13 : import 'event_details_screen.dart';
14 : import 'event_form_screen.dart';
15 : import 'event_calendar_screen.dart';
16 :
17 : class EventsScreen extends StatefulWidget {
18 24 : const EventsScreen({super.key});
19 :
20 0 : @override
21 0 : State<EventsScreen> createState() => _EventsScreenState();
22 : }
23 :
24 : class _EventsScreenState extends State<EventsScreen> {
25 : bool _isInit = false;
26 : bool _isLoading = false;
27 : String _searchQuery = '';
28 : final TextEditingController _searchController = TextEditingController();
29 : String _filterType = '전체'; // '전체', '예정', '지난'
30 : String _filterStatus = '모든 상태'; // '모든 상태', '활성', '대기', '취소', '완료'
31 : String _sortBy = '날짜순'; // '날짜순', '이름순'
32 :
33 0 : @override
34 : void dispose() {
35 0 : _searchController.dispose();
36 0 : super.dispose();
37 : }
38 :
39 0 : @override
40 : void didChangeDependencies() {
41 0 : super.didChangeDependencies();
42 0 : if (!_isInit) {
43 0 : _loadEvents();
44 0 : _isInit = true;
45 : }
46 : }
47 :
48 0 : Future<void> _loadEvents() async {
49 0 : setState(() {
50 0 : _isLoading = true;
51 : });
52 :
53 : try {
54 0 : final authService = Provider.of<AuthService>(context, listen: false);
55 0 : final clubService = Provider.of<ClubService>(context, listen: false);
56 0 : final eventService = Provider.of<EventService>(context, listen: false);
57 :
58 0 : if (clubService.currentClub != null) {
59 0 : eventService.initialize(authService.token!);
60 0 : await eventService.fetchClubEvents();
61 : }
62 : } catch (e) {
63 0 : if (mounted) {
64 0 : ScaffoldMessenger.of(
65 0 : context,
66 0 : ).showSnackBar(SnackBar(content: Text('이벤트 목록을 불러오는데 실패했습니다: $e')));
67 : }
68 : } finally {
69 0 : if (mounted) {
70 0 : setState(() {
71 0 : _isLoading = false;
72 : });
73 : }
74 : }
75 : }
76 :
77 0 : List<Event> _getFilteredEvents(List<Event> events) {
78 : // 검색어 필터링
79 : List<Event> filteredEvents = events;
80 0 : if (_searchQuery.isNotEmpty) {
81 0 : final query = _searchQuery.toLowerCase();
82 0 : filteredEvents = filteredEvents.where((event) {
83 0 : return event.title.toLowerCase().contains(query) ||
84 0 : (event.description?.toLowerCase().contains(query) ?? false) ||
85 0 : (event.location?.toLowerCase().contains(query) ?? false);
86 0 : }).toList();
87 : }
88 :
89 : // 날짜 필터링
90 0 : final now = DateTime.now();
91 0 : if (_filterType == '예정') {
92 : filteredEvents = filteredEvents
93 0 : .where((event) => event.startDate.isAfter(now))
94 0 : .toList();
95 0 : } else if (_filterType == '지난') {
96 : filteredEvents = filteredEvents
97 0 : .where((event) => event.startDate.isBefore(now))
98 0 : .toList();
99 : }
100 :
101 : // 상태 필터링
102 0 : if (_filterStatus != '모든 상태') {
103 : filteredEvents = filteredEvents
104 0 : .where((event) => event.status == _filterStatus)
105 0 : .toList();
106 : }
107 :
108 : // 정렬
109 0 : if (_sortBy == '날짜순') {
110 0 : filteredEvents.sort((a, b) => a.startDate.compareTo(b.startDate));
111 0 : } else if (_sortBy == '이름순') {
112 0 : filteredEvents.sort((a, b) => a.title.compareTo(b.title));
113 : }
114 :
115 : return filteredEvents;
116 : }
117 :
118 0 : @override
119 : Widget build(BuildContext context) {
120 0 : return Scaffold(
121 0 : body: _isLoading
122 : ? const LoadingIndicator()
123 0 : : Column(
124 0 : children: [
125 : // 상단 버튼 영역
126 0 : Padding(
127 : padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0),
128 0 : child: Row(
129 : mainAxisAlignment: MainAxisAlignment.end,
130 0 : children: [
131 0 : OutlinedButton.icon(
132 0 : onPressed: _navigateToCalendarView,
133 : icon: const Icon(Icons.calendar_today),
134 : label: const Text('캘린더 보기'),
135 0 : style: OutlinedButton.styleFrom(
136 : foregroundColor: Colors.blue,
137 : side: const BorderSide(color: Colors.blue),
138 : ),
139 : ),
140 : const SizedBox(width: 8),
141 0 : OutlinedButton.icon(
142 0 : onPressed: _showFileUploadDialog,
143 : icon: const Icon(Icons.file_upload),
144 : label: const Text('파일에서 생성'),
145 0 : style: OutlinedButton.styleFrom(
146 : foregroundColor: Colors.blue,
147 : side: const BorderSide(color: Colors.blue),
148 : ),
149 : ),
150 : ],
151 : ),
152 : ),
153 : // 검색 바
154 0 : Padding(
155 : padding: const EdgeInsets.all(16.0),
156 0 : child: Column(
157 0 : children: [
158 0 : TextField(
159 0 : controller: _searchController,
160 0 : decoration: InputDecoration(
161 : hintText: '이벤트 검색',
162 : prefixIcon: const Icon(Icons.search),
163 0 : border: OutlineInputBorder(
164 0 : borderRadius: BorderRadius.circular(12),
165 : ),
166 : contentPadding: const EdgeInsets.symmetric(
167 : vertical: 0,
168 : ),
169 0 : suffixIcon: _searchQuery.isNotEmpty
170 0 : ? IconButton(
171 : icon: const Icon(Icons.clear),
172 0 : onPressed: () {
173 0 : _searchController.clear();
174 0 : setState(() {
175 0 : _searchQuery = '';
176 : });
177 : },
178 : )
179 : : null,
180 : ),
181 0 : onChanged: (value) {
182 0 : setState(() {
183 0 : _searchQuery = value;
184 : });
185 : },
186 : ),
187 : const SizedBox(height: 8),
188 :
189 : // 날짜 필터 버튼들
190 0 : SingleChildScrollView(
191 : scrollDirection: Axis.horizontal,
192 0 : child: Row(
193 0 : children: [
194 0 : _buildFilterChip('전체'),
195 0 : _buildFilterChip('예정'),
196 0 : _buildFilterChip('지난'),
197 : ],
198 : ),
199 : ),
200 :
201 : const SizedBox(height: 8),
202 :
203 : // 상태 필터 버튼들
204 0 : SingleChildScrollView(
205 : scrollDirection: Axis.horizontal,
206 0 : child: Row(
207 0 : children: [
208 0 : _buildStatusFilterChip('모든 상태'),
209 0 : _buildStatusFilterChip('활성'),
210 0 : _buildStatusFilterChip('대기'),
211 0 : _buildStatusFilterChip('취소'),
212 0 : _buildStatusFilterChip('완료'),
213 : ],
214 : ),
215 : ),
216 :
217 : const SizedBox(height: 8),
218 :
219 : // 정렬 옵션
220 0 : Row(
221 0 : children: [
222 : const Text(
223 : '정렬: ',
224 : style: TextStyle(fontWeight: FontWeight.bold),
225 : ),
226 0 : DropdownButton<String>(
227 0 : value: _sortBy,
228 : items: const [
229 : DropdownMenuItem(
230 : value: '날짜순',
231 : child: Text('날짜순'),
232 : ),
233 : DropdownMenuItem(
234 : value: '이름순',
235 : child: Text('이름순'),
236 : ),
237 : ],
238 0 : onChanged: (value) {
239 : if (value != null) {
240 0 : setState(() {
241 0 : _sortBy = value;
242 : });
243 : }
244 : },
245 : ),
246 : ],
247 : ),
248 : ],
249 : ),
250 : ),
251 :
252 : // 이벤트 목록
253 0 : Expanded(
254 0 : child: Consumer<EventService>(
255 0 : builder: (context, eventService, _) {
256 0 : if (eventService.isLoading) {
257 : return const Center(child: CircularProgressIndicator());
258 : }
259 :
260 0 : final filteredEvents = _getFilteredEvents(
261 0 : eventService.events,
262 : );
263 :
264 0 : if (filteredEvents.isEmpty) {
265 0 : return Center(
266 0 : child: Text(
267 0 : _searchQuery.isEmpty
268 : ? '등록된 이벤트가 없습니다'
269 : : '검색 결과가 없습니다',
270 : ),
271 : );
272 : }
273 :
274 0 : return RefreshIndicator(
275 0 : onRefresh: _loadEvents,
276 0 : child: ListView.builder(
277 : padding: const EdgeInsets.only(bottom: 16),
278 0 : itemCount: filteredEvents.length,
279 0 : itemBuilder: (context, index) {
280 0 : final event = filteredEvents[index];
281 0 : return _buildEventCard(event);
282 : },
283 : ),
284 : );
285 : },
286 : ),
287 : ),
288 : ],
289 : ),
290 0 : floatingActionButton: FloatingActionButton(
291 0 : onPressed: _showAddEventDialog,
292 : backgroundColor: Colors.blue,
293 : child: const Icon(Icons.add, color: Colors.white),
294 : ),
295 : );
296 : }
297 :
298 0 : Widget _buildFilterChip(String label) {
299 0 : final isSelected = _filterType == label;
300 :
301 0 : return Padding(
302 : padding: const EdgeInsets.only(right: 8.0),
303 0 : child: FilterChip(
304 0 : label: Text(label),
305 : selected: isSelected,
306 0 : onSelected: (selected) {
307 0 : setState(() {
308 : if (selected) {
309 0 : _filterType = label;
310 : } else {
311 0 : _filterType = '전체';
312 : }
313 : });
314 : },
315 0 : backgroundColor: Colors.grey.shade200,
316 0 : selectedColor: Colors.blue.shade100,
317 : ),
318 : );
319 : }
320 :
321 0 : Widget _buildStatusFilterChip(String label) {
322 0 : final isSelected = _filterStatus == label;
323 :
324 0 : return Padding(
325 : padding: const EdgeInsets.only(right: 8.0),
326 0 : child: FilterChip(
327 0 : label: Text(label),
328 : selected: isSelected,
329 0 : onSelected: (selected) {
330 0 : setState(() {
331 : if (selected) {
332 0 : _filterStatus = label;
333 : } else {
334 0 : _filterStatus = '모든 상태';
335 : }
336 : });
337 : },
338 0 : backgroundColor: Colors.grey.shade200,
339 0 : selectedColor: Colors.green.shade100,
340 : ),
341 : );
342 : }
343 :
344 0 : Widget _buildEventCard(Event event) {
345 0 : final dateFormat = DateFormat('yyyy년 MM월 dd일 HH:mm');
346 0 : final now = DateTime.now();
347 0 : final isPast = event.startDate.isBefore(now);
348 :
349 0 : return Card(
350 : margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
351 0 : shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
352 0 : child: ListTile(
353 : contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
354 0 : leading: Container(
355 : width: 48,
356 : height: 48,
357 0 : decoration: BoxDecoration(
358 0 : color: isPast ? Colors.grey[200] : Colors.blue[100],
359 0 : borderRadius: BorderRadius.circular(8),
360 : ),
361 0 : child: Icon(
362 : Icons.event,
363 0 : color: isPast ? Colors.grey[600] : Colors.blue,
364 : ),
365 : ),
366 0 : title: Text(
367 0 : event.title,
368 0 : style: TextStyle(
369 : fontWeight: FontWeight.bold,
370 0 : color: isPast ? Colors.grey[600] : Colors.black,
371 : ),
372 : ),
373 0 : subtitle: Column(
374 : crossAxisAlignment: CrossAxisAlignment.start,
375 0 : children: [
376 : const SizedBox(height: 4),
377 0 : Text(
378 0 : dateFormat.format(event.startDate),
379 0 : style: TextStyle(
380 0 : color: isPast ? Colors.grey[500] : Colors.grey[700],
381 : ),
382 : ),
383 0 : if (event.location != null)
384 0 : Text(
385 0 : event.location!,
386 0 : style: TextStyle(
387 0 : color: isPast ? Colors.grey[500] : Colors.grey[600],
388 : ),
389 : ),
390 : ],
391 : ),
392 0 : trailing: PopupMenuButton<String>(
393 : icon: const Icon(Icons.more_vert),
394 0 : onSelected: (value) {
395 0 : if (value == 'edit') {
396 0 : _showEditEventDialog(event);
397 0 : } else if (value == 'delete') {
398 0 : _showDeleteConfirmationDialog(event);
399 : }
400 : },
401 0 : itemBuilder: (context) => [
402 : const PopupMenuItem<String>(
403 : value: 'edit',
404 : child: Row(
405 : children: [
406 : Icon(Icons.edit, size: 18),
407 : SizedBox(width: 8),
408 : Text('수정'),
409 : ],
410 : ),
411 : ),
412 : const PopupMenuItem<String>(
413 : value: 'delete',
414 : child: Row(
415 : children: [
416 : Icon(Icons.delete, size: 18, color: Colors.red),
417 : SizedBox(width: 8),
418 : Text('삭제', style: TextStyle(color: Colors.red)),
419 : ],
420 : ),
421 : ),
422 : ],
423 : ),
424 0 : onTap: () => _navigateToEventDetails(event),
425 : ),
426 : );
427 : }
428 :
429 0 : void _showAddEventDialog() {
430 0 : Navigator.of(context)
431 0 : .push(MaterialPageRoute(builder: (context) => const EventFormScreen()))
432 0 : .then((result) {
433 0 : if (result == true) {
434 : // 이벤트가 추가되었으면 목록 새로고침
435 0 : _loadEvents();
436 : }
437 : });
438 : }
439 :
440 0 : void _showEditEventDialog(Event event) {
441 0 : final titleController = TextEditingController(text: event.title);
442 0 : final descriptionController = TextEditingController(
443 0 : text: event.description ?? '',
444 : );
445 0 : final locationController = TextEditingController(
446 0 : text: event.location ?? '',
447 : );
448 :
449 0 : DateTime selectedDate = event.startDate;
450 0 : TimeOfDay selectedTime = TimeOfDay(
451 0 : hour: event.startDate.hour,
452 0 : minute: event.startDate.minute,
453 : );
454 :
455 0 : showDialog(
456 0 : context: context,
457 0 : builder: (context) => AlertDialog(
458 : title: const Text('이벤트 수정'),
459 0 : content: SingleChildScrollView(
460 0 : child: Column(
461 : mainAxisSize: MainAxisSize.min,
462 0 : children: [
463 0 : TextField(
464 : controller: titleController,
465 : decoration: const InputDecoration(
466 : labelText: '제목',
467 : border: OutlineInputBorder(),
468 : ),
469 : ),
470 : const SizedBox(height: 16),
471 0 : TextField(
472 : controller: descriptionController,
473 : maxLines: 3,
474 : decoration: const InputDecoration(
475 : labelText: '설명',
476 : border: OutlineInputBorder(),
477 : ),
478 : ),
479 : const SizedBox(height: 16),
480 0 : TextField(
481 : controller: locationController,
482 : decoration: const InputDecoration(
483 : labelText: '장소',
484 : border: OutlineInputBorder(),
485 : ),
486 : ),
487 : const SizedBox(height: 16),
488 0 : ListTile(
489 : title: const Text('날짜'),
490 0 : subtitle: Text(
491 0 : DateFormat('yyyy년 MM월 dd일').format(selectedDate),
492 : ),
493 : trailing: const Icon(Icons.calendar_today),
494 0 : onTap: () async {
495 0 : final pickedDate = await showDatePicker(
496 : context: context,
497 : initialDate: selectedDate,
498 0 : firstDate: DateTime.now().subtract(
499 : const Duration(days: 365),
500 : ),
501 0 : lastDate: DateTime.now().add(const Duration(days: 365)),
502 : );
503 0 : if (pickedDate != null && context.mounted) {
504 0 : setState(() {
505 : selectedDate = pickedDate;
506 : });
507 : }
508 : },
509 : ),
510 0 : ListTile(
511 : title: const Text('시간'),
512 0 : subtitle: Text(selectedTime.format(context)),
513 : trailing: const Icon(Icons.access_time),
514 0 : onTap: () async {
515 0 : final pickedTime = await showTimePicker(
516 : context: context,
517 : initialTime: selectedTime,
518 : );
519 0 : if (pickedTime != null && context.mounted) {
520 0 : setState(() {
521 : selectedTime = pickedTime;
522 : });
523 : }
524 : },
525 : ),
526 : ],
527 : ),
528 : ),
529 0 : actions: [
530 0 : TextButton(
531 0 : onPressed: () => Navigator.of(context).pop(),
532 : child: const Text('취소'),
533 : ),
534 0 : ElevatedButton(
535 0 : onPressed: () async {
536 0 : if (titleController.text.isEmpty) {
537 0 : ScaffoldMessenger.of(context).showSnackBar(
538 : const SnackBar(content: Text('제목은 필수 입력 항목입니다')),
539 : );
540 : return;
541 : }
542 :
543 : try {
544 0 : final eventService = Provider.of<EventService>(
545 : context,
546 : listen: false,
547 : );
548 :
549 : // 날짜와 시간 결합
550 0 : final eventDateTime = DateTime(
551 0 : selectedDate.year,
552 0 : selectedDate.month,
553 0 : selectedDate.day,
554 0 : selectedTime.hour,
555 0 : selectedTime.minute,
556 : );
557 :
558 0 : await eventService.updateEvent(event.id, {
559 0 : 'title': titleController.text.trim(),
560 0 : 'description': descriptionController.text.trim(),
561 0 : 'location': locationController.text.trim(),
562 0 : 'startDate': eventDateTime.toIso8601String(),
563 : });
564 :
565 0 : if (mounted) {
566 0 : Navigator.of(context).pop();
567 0 : ScaffoldMessenger.of(context).showSnackBar(
568 : const SnackBar(content: Text('이벤트가 업데이트되었습니다')),
569 : );
570 : }
571 : } catch (e) {
572 0 : ScaffoldMessenger.of(
573 : context,
574 0 : ).showSnackBar(SnackBar(content: Text('이벤트 업데이트에 실패했습니다: $e')));
575 : }
576 : },
577 : child: const Text('저장'),
578 : ),
579 : ],
580 : ),
581 : );
582 : }
583 :
584 0 : void _navigateToEventDetails(Event event) {
585 0 : Navigator.of(context)
586 0 : .push(
587 0 : MaterialPageRoute(
588 0 : builder: (context) => EventDetailsScreen(event: event),
589 : ),
590 : )
591 0 : .then((result) {
592 0 : if (result == true) {
593 : // 이벤트가 수정되었으면 목록 새로고침
594 0 : _loadEvents();
595 : }
596 : });
597 : }
598 :
599 0 : void _showDeleteConfirmationDialog(Event event) {
600 0 : showDialog(
601 0 : context: context,
602 0 : builder: (context) => AlertDialog(
603 : title: const Text('이벤트 삭제'),
604 0 : content: Text('${event.title} 이벤트를 정말 삭제하시겠습니까?'),
605 0 : actions: [
606 0 : TextButton(
607 0 : onPressed: () => Navigator.of(context).pop(),
608 : child: const Text('취소'),
609 : ),
610 0 : TextButton(
611 0 : onPressed: () async {
612 : try {
613 0 : final eventService = Provider.of<EventService>(
614 : context,
615 : listen: false,
616 : );
617 :
618 0 : await eventService.deleteEvent(event.id);
619 :
620 0 : if (mounted) {
621 0 : Navigator.of(context).pop();
622 0 : ScaffoldMessenger.of(
623 : context,
624 0 : ).showSnackBar(const SnackBar(content: Text('이벤트가 삭제되었습니다')));
625 : }
626 : } catch (e) {
627 0 : ScaffoldMessenger.of(
628 : context,
629 0 : ).showSnackBar(SnackBar(content: Text('이벤트 삭제에 실패했습니다: $e')));
630 : }
631 : },
632 0 : style: TextButton.styleFrom(foregroundColor: Colors.red),
633 : child: const Text('삭제'),
634 : ),
635 : ],
636 : ),
637 : );
638 : }
639 :
640 : // 캘린더 보기 화면으로 이동
641 0 : void _navigateToCalendarView() {
642 0 : Navigator.of(context).push(
643 0 : MaterialPageRoute(builder: (context) => const EventCalendarScreen()),
644 : );
645 : }
646 :
647 : // 파일에서 이벤트 생성 다이얼로그 표시
648 0 : void _showFileUploadDialog() {
649 0 : showDialog(
650 0 : context: context,
651 0 : builder: (context) => AlertDialog(
652 : title: const Text('파일에서 이벤트 생성'),
653 0 : content: SingleChildScrollView(
654 0 : child: Column(
655 : mainAxisSize: MainAxisSize.min,
656 : crossAxisAlignment: CrossAxisAlignment.start,
657 0 : children: [
658 : const Text('엑셀 파일(.xlsx, .xls)에서 이벤트를 일괄 생성할 수 있습니다.'),
659 : const SizedBox(height: 16),
660 : const Text(
661 : '파일 형식 안내:',
662 : style: TextStyle(fontWeight: FontWeight.bold),
663 : ),
664 : const SizedBox(height: 8),
665 : const Text(
666 : '• 첫 번째 행은 헤더로 사용됩니다.',
667 : style: TextStyle(fontSize: 14),
668 : ),
669 : const Text(
670 : '• 필수 필드: title(이벤트 제목), startDate(시작일)',
671 : style: TextStyle(fontSize: 14),
672 : ),
673 : const Text(
674 : '• 선택 필드: description(설명), location(장소), endDate(종료일), status(상태)',
675 : style: TextStyle(fontSize: 14),
676 : ),
677 : const Text(
678 : '• 날짜 형식: YYYY-MM-DD 또는 YYYY/MM/DD',
679 : style: TextStyle(fontSize: 14),
680 : ),
681 : const SizedBox(height: 16),
682 0 : Row(
683 0 : children: [
684 0 : Expanded(
685 0 : child: ElevatedButton.icon(
686 0 : onPressed: _pickAndUploadFile,
687 : icon: const Icon(Icons.file_upload),
688 : label: const Text('파일 선택'),
689 0 : style: ElevatedButton.styleFrom(
690 0 : backgroundColor: Theme.of(context).primaryColor,
691 : foregroundColor: Colors.white,
692 : ),
693 : ),
694 : ),
695 : ],
696 : ),
697 : const SizedBox(height: 8),
698 0 : TextButton.icon(
699 0 : onPressed: () {
700 : // 여기서는 예시 템플릿을 제공하는 기능을 구현할 수 있습니다.
701 : // 현재는 안내 메시지만 표시합니다.
702 0 : ScaffoldMessenger.of(context).showSnackBar(
703 : const SnackBar(content: Text('예시 템플릿 준비 중입니다.')),
704 : );
705 : },
706 : icon: const Icon(Icons.download),
707 : label: const Text('예시 템플릿 다운로드'),
708 : ),
709 : ],
710 : ),
711 : ),
712 0 : actions: [
713 0 : TextButton(
714 0 : onPressed: () => Navigator.of(context).pop(),
715 : child: const Text('닫기'),
716 : ),
717 : ],
718 : ),
719 : );
720 : }
721 :
722 : // 파일 선택 및 업로드
723 0 : Future<void> _pickAndUploadFile() async {
724 : try {
725 : // 파일 피커를 사용하여 엑셀 파일 선택
726 0 : final result = await FilePicker.platform.pickFiles(
727 : type: FileType.custom,
728 0 : allowedExtensions: ['xlsx', 'xls'],
729 : allowMultiple: false,
730 : );
731 :
732 0 : if (result == null || result.files.isEmpty) {
733 : // 사용자가 파일 선택을 취소함
734 : return;
735 : }
736 :
737 0 : final file = result.files.first;
738 0 : final fileName = file.name;
739 0 : final bytes = file.bytes;
740 :
741 : if (bytes == null) {
742 0 : ScaffoldMessenger.of(context).showSnackBar(
743 : const SnackBar(
744 : content: Text('파일을 읽을 수 없습니다. 다른 파일을 선택해주세요.'),
745 : duration: Duration(seconds: 3),
746 : ),
747 : );
748 : return;
749 : }
750 :
751 : // 로딩 다이얼로그 표시
752 0 : showDialog(
753 0 : context: context,
754 : barrierDismissible: false,
755 0 : builder: (context) => AlertDialog(
756 0 : content: Column(
757 : mainAxisSize: MainAxisSize.min,
758 0 : children: [
759 : const CircularProgressIndicator(),
760 : const SizedBox(height: 16),
761 0 : Text('파일 "$fileName" 처리 중...'),
762 : const SizedBox(height: 8),
763 : const Text('이벤트 데이터를 추출하고 있습니다.'),
764 : ],
765 : ),
766 : ),
767 : );
768 :
769 : // EventExcelParser를 사용하여 엑셀 파일 파싱
770 0 : final parseResult = EventExcelParser.parseExcelBytes(bytes);
771 0 : final processedRows = parseResult['processedRows'] as int;
772 0 : final events = parseResult['validEvents'] as List<Map<String, dynamic>>;
773 0 : final invalidRows = parseResult['invalidRows'] as int;
774 0 : final error = parseResult['error'] as String?;
775 :
776 : // 오류가 있거나 이벤트가 없는 경우
777 : if (error != null) {
778 0 : Navigator.of(context).pop(); // 로딩 다이얼로그 닫기
779 0 : ScaffoldMessenger.of(context).showSnackBar(
780 0 : SnackBar(
781 0 : content: Text(error),
782 : duration: const Duration(seconds: 4),
783 : ),
784 : );
785 : return;
786 : }
787 :
788 : // 로딩 다이얼로그 업데이트
789 0 : if (mounted) {
790 0 : Navigator.of(context).pop();
791 0 : showDialog(
792 0 : context: context,
793 : barrierDismissible: false,
794 0 : builder: (context) => AlertDialog(
795 0 : content: Column(
796 : mainAxisSize: MainAxisSize.min,
797 0 : children: [
798 : const CircularProgressIndicator(),
799 : const SizedBox(height: 16),
800 0 : Text('이벤트 ${events.length}개 생성 중...'),
801 : ],
802 : ),
803 : ),
804 : );
805 : }
806 :
807 : // 이벤트 서비스를 통해 이벤트 생성
808 0 : final eventService = Provider.of<EventService>(context, listen: false);
809 0 : final results = await eventService.createEventsFromFile(events);
810 :
811 : // 로딩 다이얼로그 닫기
812 0 : if (mounted) Navigator.of(context).pop();
813 :
814 : // 결과 표시
815 0 : if (mounted) {
816 : // 상세 결과 다이얼로그 표시
817 0 : showDialog(
818 0 : context: context,
819 0 : builder: (context) => AlertDialog(
820 : title: const Text('파일 처리 결과'),
821 0 : content: Column(
822 : mainAxisSize: MainAxisSize.min,
823 : crossAxisAlignment: CrossAxisAlignment.start,
824 0 : children: [
825 0 : Text('파일명: $fileName'),
826 : const SizedBox(height: 8),
827 0 : Text('처리된 행: $processedRows개'),
828 0 : Text('생성된 이벤트: ${results.length}개'),
829 0 : if (invalidRows > 0) Text('유효하지 않은 행: $invalidRows개'),
830 : ],
831 : ),
832 0 : actions: [
833 0 : TextButton(
834 0 : onPressed: () {
835 0 : Navigator.of(context).pop();
836 : // 이벤트 목록 새로고침
837 0 : _loadEvents();
838 : },
839 : child: const Text('확인'),
840 : ),
841 : ],
842 : ),
843 : );
844 : }
845 : } catch (e) {
846 : // 로딩 다이얼로그가 열려 있으면 닫기
847 0 : if (mounted) Navigator.of(context).pop();
848 :
849 : // 사용자 친화적인 오류 메시지 표시
850 0 : ScaffoldMessenger.of(context).showSnackBar(
851 0 : SnackBar(
852 0 : content: Text('파일 처리 중 오류가 발생했습니다: $e'),
853 : duration: const Duration(seconds: 4),
854 : ),
855 : );
856 : }
857 : }
858 : }
|