Files
bowlingManager/mobile/lib/utils/event_csv_parser.dart
T
2025-10-23 22:04:03 +09:00

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');
}
}