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