LCOV - code coverage report
Current view: | top level - lib/screens/club - events_screen.dart |  |  | Coverage | Total | Hit
Test: | lcov.info |  | Lines: | 0.3 % | 345 | 1
Test Date: | 2025-08-12 03:21:51 |  | Functions: | - | 0 | 0

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

Generated by: LCOV version 2.3.1-1