164 lines
5.5 KiB
Dart
164 lines
5.5 KiB
Dart
import 'dart:convert';
|
|
|
|
/// CSV 파일에서 이벤트 데이터를 파싱하는 유틸리티 클래스
|
|
/// 반환 형식은 EventExcelParser와 동일하게 맞춥니다.
|
|
class EventCsvParser {
|
|
/// CSV 바이트에서 이벤트 데이터를 추출합니다.
|
|
/// 헤더는 첫 줄로 가정하며, 필수 컬럼은 title, startDate 입니다.
|
|
/// 반환값 키:
|
|
/// - processedRows, validEvents, invalidRows, invalidRowIndices, error
|
|
static Map<String, dynamic> parseCsvBytes(List<int> bytes) {
|
|
final content = utf8.decode(bytes, allowMalformed: true);
|
|
final lines = _splitLines(content)
|
|
.where((e) => e.trim().isNotEmpty)
|
|
.toList();
|
|
|
|
if (lines.isEmpty) {
|
|
return {
|
|
'processedRows': 0,
|
|
'validEvents': <Map<String, dynamic>>[],
|
|
'invalidRows': 0,
|
|
'invalidRowIndices': <int>[],
|
|
'error': 'CSV 파일에 데이터가 없습니다.'
|
|
};
|
|
}
|
|
|
|
final headersRaw = _parseCsvLine(lines.first);
|
|
final headers = headersRaw
|
|
.map((e) => e.trim())
|
|
.toList();
|
|
|
|
// 한글 헤더를 영문 키로 정규화하기 위한 매핑
|
|
String _normalizeHeader(String raw) {
|
|
final v = raw.trim().toLowerCase();
|
|
// 공백 제거 버전도 별도로 준비
|
|
final vn = v.replaceAll(' ', '');
|
|
// 한국어 -> 영문 매핑
|
|
if (v == '제목') return 'title';
|
|
if (v == '시작일') return 'startDate';
|
|
if (v == '종료일') return 'endDate';
|
|
if (v == '설명') return 'description';
|
|
if (v == '장소') return 'location';
|
|
if (v == '상태') return 'status';
|
|
if (v == '유형') return 'type';
|
|
if (v == '게임 수' || vn == '게임수') return 'gameCount';
|
|
if (v == '참가비') return 'participantFee';
|
|
if (v == '신청마감' || v == '신청 마감') return 'registrationDeadline';
|
|
if (v == '최대 참가자' || vn == '최대참가자') return 'maxParticipants';
|
|
|
|
// 기존 영문 키들 정규화
|
|
if (vn == 'maxparticipants') return 'maxParticipants';
|
|
if (vn == 'startdate') return 'startDate';
|
|
if (vn == 'enddate') return 'endDate';
|
|
return raw.trim();
|
|
}
|
|
|
|
final normalizedHeaders = headers.map(_normalizeHeader).toList();
|
|
|
|
if (!normalizedHeaders.contains('title') || !normalizedHeaders.contains('startDate')) {
|
|
return {
|
|
'processedRows': 0,
|
|
'validEvents': <Map<String, dynamic>>[],
|
|
'invalidRows': 0,
|
|
'invalidRowIndices': <int>[],
|
|
'error': '필수 필드(title, startDate)가 없습니다.'
|
|
};
|
|
}
|
|
|
|
final events = <Map<String, dynamic>>[];
|
|
int processedRows = 0;
|
|
int invalidRows = 0;
|
|
final invalidRowIndices = <int>[];
|
|
final invalidRowReasons = <int, String>{};
|
|
|
|
for (var i = 1; i < lines.length; i++) {
|
|
final row = _parseCsvLine(lines[i]);
|
|
if (row.isEmpty || row.every((c) => c.trim().isEmpty)) {
|
|
// 빈 행은 스킵
|
|
continue;
|
|
}
|
|
processedRows++;
|
|
|
|
final event = <String, dynamic>{};
|
|
for (var j = 0; j < normalizedHeaders.length && j < row.length; j++) {
|
|
final header = normalizedHeaders[j];
|
|
var value = row[j].trim();
|
|
|
|
if (header == 'startDate' || header == 'endDate') {
|
|
// 날짜 문자열은 그대로 전달하고, 화면/서버 측에서 ISO 변환/검증 처리
|
|
// 비어있으면 startDate는 필수, endDate는 null
|
|
if (header == 'startDate') {
|
|
event['startDate'] = value.isEmpty ? DateTime.now().toIso8601String() : value;
|
|
} else {
|
|
event['endDate'] = value.isEmpty ? null : value;
|
|
}
|
|
} else {
|
|
event[header] = value.isEmpty ? null : value;
|
|
}
|
|
}
|
|
|
|
final titleVal = (event['title'] ?? '').toString().trim();
|
|
final startVal = (event['startDate'] ?? '').toString().trim();
|
|
if (titleVal.isNotEmpty && startVal.isNotEmpty) {
|
|
events.add(event);
|
|
} else {
|
|
invalidRows++;
|
|
final rowNo = i + 1;
|
|
invalidRowIndices.add(rowNo); // 헤더가 1행이므로 +1
|
|
final reasons = <String>[];
|
|
if (titleVal.isEmpty) reasons.add('title 누락');
|
|
if (startVal.isEmpty) reasons.add('startDate 누락');
|
|
invalidRowReasons[rowNo] = reasons.join(', ');
|
|
}
|
|
}
|
|
|
|
return {
|
|
'processedRows': processedRows,
|
|
'validEvents': events,
|
|
'invalidRows': invalidRows,
|
|
'invalidRowIndices': invalidRowIndices,
|
|
'invalidRowReasons': invalidRowReasons,
|
|
'error': events.isEmpty ? '유효한 이벤트 데이터가 없습니다.' : null
|
|
};
|
|
}
|
|
|
|
// 간단한 CSV 라인 파서: 따옴표 포함 필드와 이스케이프된 따옴표("") 처리
|
|
static List<String> _parseCsvLine(String line) {
|
|
final result = <String>[];
|
|
final buf = StringBuffer();
|
|
bool inQuotes = false;
|
|
|
|
for (int i = 0; i < line.length; i++) {
|
|
final ch = line[i];
|
|
if (inQuotes) {
|
|
if (ch == '"') {
|
|
// 이스케이프된 따옴표
|
|
if (i + 1 < line.length && line[i + 1] == '"') {
|
|
buf.write('"');
|
|
i++;
|
|
} else {
|
|
inQuotes = false;
|
|
}
|
|
} else {
|
|
buf.write(ch);
|
|
}
|
|
} else {
|
|
if (ch == ',') {
|
|
result.add(buf.toString());
|
|
buf.clear();
|
|
} else if (ch == '"') {
|
|
inQuotes = true;
|
|
} else {
|
|
buf.write(ch);
|
|
}
|
|
}
|
|
}
|
|
result.add(buf.toString());
|
|
return result;
|
|
}
|
|
|
|
static List<String> _splitLines(String content) {
|
|
return content.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n');
|
|
}
|
|
}
|