diff --git a/lib/function/teacher_student_management_controller.dart b/lib/function/teacher_student_management_controller.dart index deac7e9..38cea7d 100644 --- a/lib/function/teacher_student_management_controller.dart +++ b/lib/function/teacher_student_management_controller.dart @@ -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 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().firstWhere( + (f) => f?.name == 'xl/styles.xml', + orElse: () => null, + ); + if (stylesFile == null) return bytes; + + final xml = utf8.decode(stylesFile.content as List); + 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 λ¬Έμžμ—΄μ—μ„œ `` νƒœκ·Έ 쀑 N이 164 미만인 + /// (규격 μœ„λ°˜μΈ) κ²ƒλ§Œ ν†΅μ§Έλ‘œ μ œκ±°ν•œλ‹€. κ·Έ μ„œμ‹μ„ μ°Έμ‘°ν•˜λ˜ 셀은 κΈ°λ³Έ μ„œμ‹μœΌλ‘œ 처리될 + /// 뿐이라 κ°’ μ½κΈ°μ—λŠ” 영ν–₯이 μ—†λ‹€. + String _stripInvalidCustomNumFmts(String xml) { + final buffer = StringBuffer(); + int i = 0; + while (true) { + final tagStart = 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; diff --git a/pubspec.lock b/pubspec.lock index 19c3143..c938a38 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,7 +18,7 @@ packages: source: hosted version: "1.0.3" archive: - dependency: transitive + dependency: "direct main" description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d diff --git a/pubspec.yaml b/pubspec.yaml index afe4139..5514dcd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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