엑셀 일괄등록: 일부 엑셀 파일 읽기 실패 버그 수정
"custom numFmtId starts at 164 but found a value of 0" 에러 - 구글시트/WPS 등에서 내보낸 일부 xlsx 파일은 styles.xml에 커스텀 숫자서식 id를 164 미만으로 적어두는데, excel 패키지가 이를 엄격하게 검증하며 예외를 던져 파일 전체를 못 읽었다. 파싱 전에 zip 안의 styles.xml만 열어 그 규격 위반 서식 선언을 제거하고 다시 압축해서 넘기도록 전처리 단계를 추가. 합성 xlsx로 버그 재현 + 수정 후 정상 파싱까지 직접 검증함. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// 👥 교사용 학생 계정 관리 화면의 기능(서버 통신/상태/엑셀 일괄등록) 담당 컨트롤러.
|
||||
import 'dart:convert';
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:excel/excel.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
@@ -91,7 +92,7 @@ class TeacherStudentManagementController extends ChangeNotifier {
|
||||
/// A열=학번, B열=이름, C열=학년(선택, 숫자) 순서를 기대한다.
|
||||
/// 학번이나 이름이 비어있는 줄은 조용히 무시한다.
|
||||
List<BulkStudentRow> parseExcelBytes(Uint8List bytes) {
|
||||
final excel = Excel.decodeBytes(bytes);
|
||||
final excel = Excel.decodeBytes(_sanitizeXlsxBytes(bytes));
|
||||
if (excel.tables.isEmpty) return [];
|
||||
|
||||
final sheet = excel.tables.values.first;
|
||||
@@ -108,6 +109,70 @@ class TeacherStudentManagementController extends ChangeNotifier {
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// 🩹 [엑셀 파싱 버그 우회] 일부 엑셀 파일(구글시트/WPS 등에서 내보낸 파일 포함)은
|
||||
/// styles.xml에 커스텀 숫자서식 id를 164 미만(예: 0)으로 잘못 적어두는데, excel
|
||||
/// 패키지가 이걸 엄격하게 걸러내며 예외를 던져서 파일 전체를 못 읽게 만든다.
|
||||
/// 우리는 셀 값(학번/이름/학년)만 필요하고 서식은 필요 없으니, zip 안의 styles.xml만
|
||||
/// 열어서 그 잘못된 커스텀 서식 선언을 지우고 다시 압축해서 넘긴다.
|
||||
Uint8List _sanitizeXlsxBytes(Uint8List bytes) {
|
||||
try {
|
||||
final zip = ZipDecoder().decodeBytes(bytes);
|
||||
final stylesFile = zip.files.cast<ArchiveFile?>().firstWhere(
|
||||
(f) => f?.name == 'xl/styles.xml',
|
||||
orElse: () => null,
|
||||
);
|
||||
if (stylesFile == null) return bytes;
|
||||
|
||||
final xml = utf8.decode(stylesFile.content as List<int>);
|
||||
final fixedXml = _stripInvalidCustomNumFmts(xml);
|
||||
if (fixedXml == xml) return bytes; // 고칠 게 없었으면 원본 그대로.
|
||||
|
||||
final newZip = Archive();
|
||||
for (final file in zip.files) {
|
||||
if (!file.isFile) continue;
|
||||
if (file.name == 'xl/styles.xml') {
|
||||
final content = utf8.encode(fixedXml);
|
||||
newZip.addFile(ArchiveFile('xl/styles.xml', content.length, content));
|
||||
} else {
|
||||
newZip.addFile(ArchiveFile(file.name, file.size, file.content));
|
||||
}
|
||||
}
|
||||
final rezipped = ZipEncoder().encode(newZip);
|
||||
return rezipped != null ? Uint8List.fromList(rezipped) : bytes;
|
||||
} catch (_) {
|
||||
return bytes; // 손대다 실패하면 원본 그대로 시도 (기존 동작 유지).
|
||||
}
|
||||
}
|
||||
|
||||
/// styles.xml 문자열에서 `<numFmt numFmtId="N" .../>` 태그 중 N이 164 미만인
|
||||
/// (규격 위반인) 것만 통째로 제거한다. 그 서식을 참조하던 셀은 기본 서식으로 처리될
|
||||
/// 뿐이라 값 읽기에는 영향이 없다.
|
||||
String _stripInvalidCustomNumFmts(String xml) {
|
||||
final buffer = StringBuffer();
|
||||
int i = 0;
|
||||
while (true) {
|
||||
final tagStart = xml.indexOf('<numFmt ', i);
|
||||
if (tagStart == -1) {
|
||||
buffer.write(xml.substring(i));
|
||||
break;
|
||||
}
|
||||
final tagEnd = xml.indexOf('/>', tagStart);
|
||||
if (tagEnd == -1) {
|
||||
buffer.write(xml.substring(i));
|
||||
break;
|
||||
}
|
||||
buffer.write(xml.substring(i, tagStart));
|
||||
final tag = xml.substring(tagStart, tagEnd + 2);
|
||||
final idMatch = RegExp(r'numFmtId="(\d+)"').firstMatch(tag);
|
||||
final id = idMatch != null ? int.tryParse(idMatch.group(1)!) : null;
|
||||
if (id == null || id >= 164) {
|
||||
buffer.write(tag); // 정상 범위의 커스텀 서식은 그대로 둔다.
|
||||
}
|
||||
i = tagEnd + 2;
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String? _cellText(CellValue? value) {
|
||||
if (value == null) return null;
|
||||
String text;
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
archive:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: archive
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
|
||||
@@ -54,6 +54,7 @@ dependencies:
|
||||
# 📳 무단 반출 감지 시 진동 (백그라운드 서비스에서도 동작하도록 HapticFeedback 대신 사용)
|
||||
vibration: ^3.2.0
|
||||
excel: ^4.0.6
|
||||
archive: ^3.6.1
|
||||
file_picker: ^12.1.2
|
||||
desktop_drop: ^0.8.3
|
||||
web: ^1.1.1
|
||||
|
||||
Reference in New Issue
Block a user