import 'package:excel/excel.dart'; /// 엑셀 파일에서 이벤트 데이터를 파싱하는 유틸리티 클래스 class EventExcelParser { /// 엑셀 파일 바이트 데이터에서 이벤트 데이터 목록을 추출합니다. /// /// [bytes] 엑셀 파일의 바이트 데이터 /// /// 반환값: /// - processedRows: 처리된 총 행 수 /// - validEvents: 유효한 이벤트 데이터 목록 /// - invalidRows: 유효하지 않은 행 수 static Map parseExcelBytes(List bytes) { final excel = Excel.decodeBytes(bytes); final events = >[]; int invalidRows = 0; int processedRows = 0; final List invalidRowIndices = []; final Map invalidRowReasons = {}; // 첫 번째 시트 사용 final sheet = excel.tables.keys.first; final rows = excel.tables[sheet]?.rows; if (rows == null || rows.isEmpty) { return { 'processedRows': 0, 'validEvents': [], 'invalidRows': 0, 'error': '엑셀 파일에 데이터가 없습니다.' }; } // 헤더 행 추출 (첫 번째 행) final headers = []; for (final cell in rows[0]) { if (cell?.value != null) { headers.add(cell!.value.toString().trim().toLowerCase()); } } // 필수 필드 확인 if (!headers.contains('title') || !headers.contains('startdate')) { return { 'processedRows': 0, 'validEvents': [], 'invalidRows': 0, 'error': '필수 필드(title, startDate)가 없습니다.' }; } // 데이터 행 처리 (헤더 제외) for (var i = 1; i < rows.length; i++) { processedRows++; final row = rows[i]; final event = {}; // 각 셀 처리 for (var j = 0; j < headers.length && j < row.length; j++) { final header = headers[j]; // 날짜 필드 특별 처리 if (header == 'startdate' || header == 'enddate') { DateTime? date = _parseDate(row[j]?.value); // 날짜 값 설정: 파싱 성공 시만 값 지정, 실패 시 null 유지 final normalizedHeader = header == 'startdate' ? 'startDate' : 'endDate'; event[normalizedHeader] = (date != null) ? date.toIso8601String() : null; } else { // 문자열/일반 필드: excel v4의 TextCellValue 등 실제 값 추출 String raw = ''; final cellVal = row[j]?.value; if (cellVal is TextCellValue) { raw = cellVal.value.toString(); } else if (cellVal != null) { raw = cellVal.toString(); } final stringValue = raw.trim(); // 헤더 이름 정규화 (특별한 경우) String normalizedHeader = header; if (header == 'title' || header == 'description' || header == 'location' || header == 'status' || header == 'type' || header == 'maxparticipants') { normalizedHeader = header == 'maxparticipants' ? 'maxParticipants' : header; } // 빈 문자열은 null로 처리 event[normalizedHeader] = stringValue.isEmpty ? null : stringValue; } } // 필수 필드가 있는 경우만 추가 final hasTitle = event.containsKey('title') && (event['title']?.toString().trim().isNotEmpty ?? false); final hasStart = event.containsKey('startDate') && (event['startDate']?.toString().trim().isNotEmpty ?? false); if (hasTitle && hasStart) { events.add(event); } else { invalidRows++; // 데이터 행 기준(헤더 제외) 1-based가 아닌, 테스트 기대값에 맞춰 i 사용 // 즉, 헤더가 0, 첫 데이터 행이 i=1이므로 무효 행 번호로 i를 기록 final rowNo = i; invalidRowIndices.add(rowNo); final reasons = []; if (!hasTitle) reasons.add('title 누락'); if (!hasStart) reasons.add('startDate 누락'); invalidRowReasons[rowNo] = reasons.join(', '); } } return { 'processedRows': processedRows, 'validEvents': events, 'invalidRows': invalidRows, 'invalidRowIndices': invalidRowIndices, 'invalidRowReasons': invalidRowReasons, 'error': events.isEmpty ? '유효한 이벤트 데이터가 없습니다.' : null }; } /// 다양한 형식의 날짜 값을 DateTime 객체로 파싱합니다. static DateTime? _parseDate(dynamic value) { DateTime? date; // DateTime 타입인 경우 if (value is DateTime) { return value; } // 문자열인 경우 if (value != null) { final strValue = value.toString(); if (strValue.isNotEmpty) { // YYYY-MM-DD 형식 시도 try { date = DateTime.parse(strValue); } catch (_) { // 실패 시 다른 형식 시도 } // YYYY/MM/DD 형식 시도 if (date == null && strValue.contains('/')) { try { final parts = strValue.split('/'); if (parts.length == 3) { date = DateTime( int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2]), ); } } catch (_) { // 실패 시 무시 } } } } return date; } }