import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; import 'package:file_picker/file_picker.dart'; import '../../utils/event_excel_parser.dart'; import '../../services/auth_service.dart'; import '../../services/club_service.dart'; import '../../models/event_model.dart'; import '../../services/event_service.dart'; import '../../widgets/loading_indicator.dart'; import 'event_details_screen.dart'; import 'event_form_screen.dart'; import 'event_calendar_screen.dart'; class EventsScreen extends StatefulWidget { const EventsScreen({super.key}); @override State createState() => _EventsScreenState(); } class _EventsScreenState extends State { bool _isInit = false; bool _isLoading = false; String _searchQuery = ''; final TextEditingController _searchController = TextEditingController(); String _filterType = '전체'; // '전체', '예정', '지난' String _filterStatus = '모든 상태'; // '모든 상태', '활성', '대기', '취소', '완료' String _sortBy = '날짜순'; // '날짜순', '이름순' @override void dispose() { _searchController.dispose(); super.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); if (!_isInit) { _loadEvents(); _isInit = true; } } Future _loadEvents() async { setState(() { _isLoading = true; }); try { final authService = Provider.of(context, listen: false); final clubService = Provider.of(context, listen: false); final eventService = Provider.of(context, listen: false); if (clubService.currentClub != null) { eventService.initialize(authService.token!); await eventService.fetchClubEvents(); } } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('이벤트 목록을 불러오는데 실패했습니다: $e'))); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } List _getFilteredEvents(List events) { // 검색어 필터링 List filteredEvents = events; if (_searchQuery.isNotEmpty) { final query = _searchQuery.toLowerCase(); filteredEvents = filteredEvents.where((event) { return event.title.toLowerCase().contains(query) || (event.description?.toLowerCase().contains(query) ?? false) || (event.location?.toLowerCase().contains(query) ?? false); }).toList(); } // 날짜 필터링 final now = DateTime.now(); if (_filterType == '예정') { filteredEvents = filteredEvents .where((event) => event.startDate.isAfter(now)) .toList(); } else if (_filterType == '지난') { filteredEvents = filteredEvents .where((event) => event.startDate.isBefore(now)) .toList(); } // 상태 필터링 if (_filterStatus != '모든 상태') { filteredEvents = filteredEvents .where((event) => event.status == _filterStatus) .toList(); } // 정렬 if (_sortBy == '날짜순') { filteredEvents.sort((a, b) => a.startDate.compareTo(b.startDate)); } else if (_sortBy == '이름순') { filteredEvents.sort((a, b) => a.title.compareTo(b.title)); } return filteredEvents; } @override Widget build(BuildContext context) { return Scaffold( body: _isLoading ? const LoadingIndicator() : Column( children: [ // 상단 버튼 영역 Padding( padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ OutlinedButton.icon( onPressed: _navigateToCalendarView, icon: const Icon(Icons.calendar_today), label: const Text('캘린더 보기'), style: OutlinedButton.styleFrom( foregroundColor: Colors.blue, side: const BorderSide(color: Colors.blue), ), ), const SizedBox(width: 8), OutlinedButton.icon( onPressed: _showFileUploadDialog, icon: const Icon(Icons.file_upload), label: const Text('파일에서 생성'), style: OutlinedButton.styleFrom( foregroundColor: Colors.blue, side: const BorderSide(color: Colors.blue), ), ), ], ), ), // 검색 바 Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: _searchController, decoration: InputDecoration( hintText: '이벤트 검색', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), contentPadding: const EdgeInsets.symmetric( vertical: 0, ), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); setState(() { _searchQuery = ''; }); }, ) : null, ), onChanged: (value) { setState(() { _searchQuery = value; }); }, ), const SizedBox(height: 8), // 날짜 필터 버튼들 SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildFilterChip('전체'), _buildFilterChip('예정'), _buildFilterChip('지난'), ], ), ), const SizedBox(height: 8), // 상태 필터 버튼들 SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildStatusFilterChip('모든 상태'), _buildStatusFilterChip('활성'), _buildStatusFilterChip('대기'), _buildStatusFilterChip('취소'), _buildStatusFilterChip('완료'), ], ), ), const SizedBox(height: 8), // 정렬 옵션 Row( children: [ const Text( '정렬: ', style: TextStyle(fontWeight: FontWeight.bold), ), DropdownButton( value: _sortBy, items: const [ DropdownMenuItem( value: '날짜순', child: Text('날짜순'), ), DropdownMenuItem( value: '이름순', child: Text('이름순'), ), ], onChanged: (value) { if (value != null) { setState(() { _sortBy = value; }); } }, ), ], ), ], ), ), // 이벤트 목록 Expanded( child: Consumer( builder: (context, eventService, _) { if (eventService.isLoading) { return const Center(child: CircularProgressIndicator()); } final filteredEvents = _getFilteredEvents( eventService.events, ); if (filteredEvents.isEmpty) { return Center( child: Text( _searchQuery.isEmpty ? '등록된 이벤트가 없습니다' : '검색 결과가 없습니다', ), ); } return RefreshIndicator( onRefresh: _loadEvents, child: ListView.builder( padding: const EdgeInsets.only(bottom: 16), itemCount: filteredEvents.length, itemBuilder: (context, index) { final event = filteredEvents[index]; return _buildEventCard(event); }, ), ); }, ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: _showAddEventDialog, backgroundColor: Colors.blue, child: const Icon(Icons.add, color: Colors.white), ), ); } Widget _buildFilterChip(String label) { final isSelected = _filterType == label; return Padding( padding: const EdgeInsets.only(right: 8.0), child: FilterChip( label: Text(label), selected: isSelected, onSelected: (selected) { setState(() { if (selected) { _filterType = label; } else { _filterType = '전체'; } }); }, backgroundColor: Colors.grey.shade200, selectedColor: Colors.blue.shade100, ), ); } Widget _buildStatusFilterChip(String label) { final isSelected = _filterStatus == label; return Padding( padding: const EdgeInsets.only(right: 8.0), child: FilterChip( label: Text(label), selected: isSelected, onSelected: (selected) { setState(() { if (selected) { _filterStatus = label; } else { _filterStatus = '모든 상태'; } }); }, backgroundColor: Colors.grey.shade200, selectedColor: Colors.green.shade100, ), ); } Widget _buildEventCard(Event event) { final dateFormat = DateFormat('yyyy년 MM월 dd일 HH:mm'); final now = DateTime.now(); final isPast = event.startDate.isBefore(now); return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), leading: Container( width: 48, height: 48, decoration: BoxDecoration( color: isPast ? Colors.grey[200] : Colors.blue[100], borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.event, color: isPast ? Colors.grey[600] : Colors.blue, ), ), title: Text( event.title, style: TextStyle( fontWeight: FontWeight.bold, color: isPast ? Colors.grey[600] : Colors.black, ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Text( dateFormat.format(event.startDate), style: TextStyle( color: isPast ? Colors.grey[500] : Colors.grey[700], ), ), if (event.location != null) Text( event.location!, style: TextStyle( color: isPast ? Colors.grey[500] : Colors.grey[600], ), ), ], ), trailing: PopupMenuButton( icon: const Icon(Icons.more_vert), onSelected: (value) { if (value == 'edit') { _showEditEventDialog(event); } else if (value == 'delete') { _showDeleteConfirmationDialog(event); } }, itemBuilder: (context) => [ const PopupMenuItem( value: 'edit', child: Row( children: [ Icon(Icons.edit, size: 18), SizedBox(width: 8), Text('수정'), ], ), ), const PopupMenuItem( value: 'delete', child: Row( children: [ Icon(Icons.delete, size: 18, color: Colors.red), SizedBox(width: 8), Text('삭제', style: TextStyle(color: Colors.red)), ], ), ), ], ), onTap: () => _navigateToEventDetails(event), ), ); } void _showAddEventDialog() { Navigator.of(context) .push(MaterialPageRoute(builder: (context) => const EventFormScreen())) .then((result) { if (result == true) { // 이벤트가 추가되었으면 목록 새로고침 _loadEvents(); } }); } void _showEditEventDialog(Event event) { final titleController = TextEditingController(text: event.title); final descriptionController = TextEditingController( text: event.description ?? '', ); final locationController = TextEditingController( text: event.location ?? '', ); DateTime selectedDate = event.startDate; TimeOfDay selectedTime = TimeOfDay( hour: event.startDate.hour, minute: event.startDate.minute, ); showDialog( context: context, builder: (context) => AlertDialog( title: const Text('이벤트 수정'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: titleController, decoration: const InputDecoration( labelText: '제목', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: descriptionController, maxLines: 3, decoration: const InputDecoration( labelText: '설명', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: locationController, decoration: const InputDecoration( labelText: '장소', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), ListTile( title: const Text('날짜'), subtitle: Text( DateFormat('yyyy년 MM월 dd일').format(selectedDate), ), trailing: const Icon(Icons.calendar_today), onTap: () async { final pickedDate = await showDatePicker( context: context, initialDate: selectedDate, firstDate: DateTime.now().subtract( const Duration(days: 365), ), lastDate: DateTime.now().add(const Duration(days: 365)), ); if (pickedDate != null && context.mounted) { setState(() { selectedDate = pickedDate; }); } }, ), ListTile( title: const Text('시간'), subtitle: Text(selectedTime.format(context)), trailing: const Icon(Icons.access_time), onTap: () async { final pickedTime = await showTimePicker( context: context, initialTime: selectedTime, ); if (pickedTime != null && context.mounted) { setState(() { selectedTime = pickedTime; }); } }, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('취소'), ), ElevatedButton( onPressed: () async { if (titleController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('제목은 필수 입력 항목입니다')), ); return; } try { final eventService = Provider.of( context, listen: false, ); // 날짜와 시간 결합 final eventDateTime = DateTime( selectedDate.year, selectedDate.month, selectedDate.day, selectedTime.hour, selectedTime.minute, ); await eventService.updateEvent(event.id, { 'title': titleController.text.trim(), 'description': descriptionController.text.trim(), 'location': locationController.text.trim(), 'startDate': eventDateTime.toIso8601String(), }); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('이벤트가 업데이트되었습니다')), ); } } catch (e) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('이벤트 업데이트에 실패했습니다: $e'))); } }, child: const Text('저장'), ), ], ), ); } void _navigateToEventDetails(Event event) { Navigator.of(context) .push( MaterialPageRoute( builder: (context) => EventDetailsScreen(event: event), ), ) .then((result) { if (result == true) { // 이벤트가 수정되었으면 목록 새로고침 _loadEvents(); } }); } void _showDeleteConfirmationDialog(Event event) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('이벤트 삭제'), content: Text('${event.title} 이벤트를 정말 삭제하시겠습니까?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('취소'), ), TextButton( onPressed: () async { try { final eventService = Provider.of( context, listen: false, ); await eventService.deleteEvent(event.id); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('이벤트가 삭제되었습니다'))); } } catch (e) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('이벤트 삭제에 실패했습니다: $e'))); } }, style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('삭제'), ), ], ), ); } // 캘린더 보기 화면으로 이동 void _navigateToCalendarView() { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const EventCalendarScreen()), ); } // 파일에서 이벤트 생성 다이얼로그 표시 void _showFileUploadDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('파일에서 이벤트 생성'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('엑셀 파일(.xlsx, .xls)에서 이벤트를 일괄 생성할 수 있습니다.'), const SizedBox(height: 16), const Text( '파일 형식 안내:', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( '• 첫 번째 행은 헤더로 사용됩니다.', style: TextStyle(fontSize: 14), ), const Text( '• 필수 필드: title(이벤트 제목), startDate(시작일)', style: TextStyle(fontSize: 14), ), const Text( '• 선택 필드: description(설명), location(장소), endDate(종료일), status(상태)', style: TextStyle(fontSize: 14), ), const Text( '• 날짜 형식: YYYY-MM-DD 또는 YYYY/MM/DD', style: TextStyle(fontSize: 14), ), const SizedBox(height: 16), Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: _pickAndUploadFile, icon: const Icon(Icons.file_upload), label: const Text('파일 선택'), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).primaryColor, foregroundColor: Colors.white, ), ), ), ], ), const SizedBox(height: 8), TextButton.icon( onPressed: () { // 여기서는 예시 템플릿을 제공하는 기능을 구현할 수 있습니다. // 현재는 안내 메시지만 표시합니다. ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('예시 템플릿 준비 중입니다.')), ); }, icon: const Icon(Icons.download), label: const Text('예시 템플릿 다운로드'), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('닫기'), ), ], ), ); } // 파일 선택 및 업로드 Future _pickAndUploadFile() async { try { // 파일 피커를 사용하여 엑셀 파일 선택 final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['xlsx', 'xls'], allowMultiple: false, ); if (result == null || result.files.isEmpty) { // 사용자가 파일 선택을 취소함 return; } final file = result.files.first; final fileName = file.name; final bytes = file.bytes; if (bytes == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('파일을 읽을 수 없습니다. 다른 파일을 선택해주세요.'), duration: Duration(seconds: 3), ), ); return; } // 로딩 다이얼로그 표시 showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text('파일 "$fileName" 처리 중...'), const SizedBox(height: 8), const Text('이벤트 데이터를 추출하고 있습니다.'), ], ), ), ); // EventExcelParser를 사용하여 엑셀 파일 파싱 final parseResult = EventExcelParser.parseExcelBytes(bytes); final processedRows = parseResult['processedRows'] as int; final events = parseResult['validEvents'] as List>; final invalidRows = parseResult['invalidRows'] as int; final error = parseResult['error'] as String?; // 오류가 있거나 이벤트가 없는 경우 if (error != null) { Navigator.of(context).pop(); // 로딩 다이얼로그 닫기 ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(error), duration: const Duration(seconds: 4), ), ); return; } // 로딩 다이얼로그 업데이트 if (mounted) { Navigator.of(context).pop(); showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text('이벤트 ${events.length}개 생성 중...'), ], ), ), ); } // 이벤트 서비스를 통해 이벤트 생성 final eventService = Provider.of(context, listen: false); final results = await eventService.createEventsFromFile(events); // 로딩 다이얼로그 닫기 if (mounted) Navigator.of(context).pop(); // 결과 표시 if (mounted) { // 상세 결과 다이얼로그 표시 showDialog( context: context, builder: (context) => AlertDialog( title: const Text('파일 처리 결과'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('파일명: $fileName'), const SizedBox(height: 8), Text('처리된 행: $processedRows개'), Text('생성된 이벤트: ${results.length}개'), if (invalidRows > 0) Text('유효하지 않은 행: $invalidRows개'), ], ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); // 이벤트 목록 새로고침 _loadEvents(); }, child: const Text('확인'), ), ], ), ); } } catch (e) { // 로딩 다이얼로그가 열려 있으면 닫기 if (mounted) Navigator.of(context).pop(); // 사용자 친화적인 오류 메시지 표시 ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('파일 처리 중 오류가 발생했습니다: $e'), duration: const Duration(seconds: 4), ), ); } } }