154 lines
4.8 KiB
Dart
154 lines
4.8 KiB
Dart
import 'package:excel/excel.dart';
|
|
|
|
/// 엑셀 파일에서 이벤트 데이터를 파싱하는 유틸리티 클래스
|
|
class EventExcelParser {
|
|
/// 엑셀 파일 바이트 데이터에서 이벤트 데이터 목록을 추출합니다.
|
|
///
|
|
/// [bytes] 엑셀 파일의 바이트 데이터
|
|
///
|
|
/// 반환값:
|
|
/// - processedRows: 처리된 총 행 수
|
|
/// - validEvents: 유효한 이벤트 데이터 목록
|
|
/// - invalidRows: 유효하지 않은 행 수
|
|
static Map<String, dynamic> parseExcelBytes(List<int> bytes) {
|
|
final excel = Excel.decodeBytes(bytes);
|
|
final events = <Map<String, dynamic>>[];
|
|
int invalidRows = 0;
|
|
int processedRows = 0;
|
|
|
|
// 첫 번째 시트 사용
|
|
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 = <String>[];
|
|
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 = <String, dynamic>{};
|
|
|
|
// 각 셀 처리
|
|
for (var j = 0; j < headers.length && j < row.length; j++) {
|
|
final header = headers[j];
|
|
final value = row[j]?.value;
|
|
|
|
// 날짜 필드 특별 처리
|
|
if (header == 'startdate' || header == 'enddate') {
|
|
DateTime? date = _parseDate(value);
|
|
|
|
// 날짜 값 설정
|
|
if (date != null) {
|
|
// 헤더 이름 정규화 (startdate -> startDate)
|
|
final normalizedHeader = header == 'startdate' ? 'startDate' : 'endDate';
|
|
event[normalizedHeader] = date.toIso8601String();
|
|
} else {
|
|
// 날짜 파싱 실패 시 startDate는 필수이므로 현재 날짜 사용
|
|
if (header == 'startdate') {
|
|
event['startDate'] = DateTime.now().toIso8601String();
|
|
} else {
|
|
// endDate는 선택적이므로 null 설정
|
|
event['endDate'] = null;
|
|
}
|
|
}
|
|
} else {
|
|
// 문자열 필드의 경우 빈 문자열은 null로 처리
|
|
final stringValue = value?.toString().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;
|
|
}
|
|
}
|
|
|
|
// 필수 필드가 있는 경우만 추가
|
|
if (event.containsKey('title') && event.containsKey('startDate')) {
|
|
events.add(event);
|
|
} else {
|
|
invalidRows++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
'processedRows': processedRows,
|
|
'validEvents': events,
|
|
'invalidRows': invalidRows,
|
|
'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;
|
|
}
|
|
}
|