From a6222490085554ea0dc32afdee623f576317af48 Mon Sep 17 00:00:00 2001 From: sihoo Date: Sat, 29 Aug 2026 22:44:40 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=99=EC=83=9D=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EC=97=91=EC=85=80=20=EC=9D=BC=EA=B4=84=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(=EC=8B=A0=EC=9E=85?= =?UTF-8?q?=EC=83=9D=20=EB=93=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 학생 계정 관리 화면에 드래그앤드롭/클릭 업로드 영역 추가 (excel, desktop_drop, file_picker 패키지 도입) - .xlsx 파싱 → 미리보기 팝업 → 확인 후 한 명씩 순서대로 계정 생성, 결과(성공/실패)를 요약해서 보여줌. 초기 비밀번호는 1234 고정 - file_picker의 웹 구현체가 pickFile(s)를 아직 구현하지 않아서 (UnimplementedError) 웹에서는 package:web으로 직접 을 다뤄서 우회 (lib/function/excel_file_picker_web.dart), 다른 플랫폼은 기존 file_picker 사용 (excel_file_picker_io.dart) Co-Authored-By: Claude Sonnet 5 --- lib/function/excel_file_picker.dart | 4 + lib/function/excel_file_picker_io.dart | 13 + lib/function/excel_file_picker_web.dart | 31 ++ ...teacher_student_management_controller.dart | 129 ++++++++- lib/ui/teacher_student_management_page.dart | 265 ++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 114 +++++++- pubspec.yaml | 9 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 12 files changed, 568 insertions(+), 10 deletions(-) create mode 100644 lib/function/excel_file_picker.dart create mode 100644 lib/function/excel_file_picker_io.dart create mode 100644 lib/function/excel_file_picker_web.dart diff --git a/lib/function/excel_file_picker.dart b/lib/function/excel_file_picker.dart new file mode 100644 index 0000000..d02437d --- /dev/null +++ b/lib/function/excel_file_picker.dart @@ -0,0 +1,4 @@ +// 📄 엑셀 파일 하나를 골라 바이트로 돌려주는 진입점. 실제 구현은 플랫폼별로 갈라진다 +// (web은 file_picker의 웹 구현체에 pickFile(s)가 아직 없어서 dart:html로 직접 처리). +export 'excel_file_picker_io.dart' + if (dart.library.html) 'excel_file_picker_web.dart'; diff --git a/lib/function/excel_file_picker_io.dart b/lib/function/excel_file_picker_io.dart new file mode 100644 index 0000000..9200e47 --- /dev/null +++ b/lib/function/excel_file_picker_io.dart @@ -0,0 +1,13 @@ +// 📄 [비웹 플랫폼] file_picker로 엑셀 파일 하나를 고른다. +import 'dart:typed_data'; +import 'package:file_picker/file_picker.dart'; + +/// 사용자가 엑셀(.xlsx) 파일을 고르면 바이트를 돌려준다. 취소하면 null. +Future pickExcelFileBytes() async { + final files = await FilePicker.pickFiles( + type: FileType.custom, + allowedExtensions: ['xlsx'], + ); + if (files.isEmpty) return null; + return files.first.readAsBytes(); +} diff --git a/lib/function/excel_file_picker_web.dart b/lib/function/excel_file_picker_web.dart new file mode 100644 index 0000000..cf1453a --- /dev/null +++ b/lib/function/excel_file_picker_web.dart @@ -0,0 +1,31 @@ +// 📄 [웹 전용] file_picker의 웹 구현체에 pickFile(s)가 아직 없어서(UnimplementedError), +// 숨겨진 를 직접 만들어 처리한다. +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:typed_data'; +import 'package:web/web.dart' as web; + +/// 사용자가 엑셀(.xlsx) 파일을 고르면 바이트를 돌려준다. 취소하면 null. +Future pickExcelFileBytes() async { + final completer = Completer(); + final input = web.HTMLInputElement() + ..type = 'file' + ..accept = '.xlsx' + ..style.display = 'none'; + // 문서에 실제로 붙어있지 않으면 일부 브라우저/자동화 환경에서 change 이벤트가 + // 안정적으로 안 잡힌다 — 그래서 body에 잠깐 붙였다가 끝나면 반드시 제거한다. + web.document.body!.append(input); + + input.onchange = (web.Event event) { + final file = input.files?.item(0); + input.remove(); + completer.complete(file); + }.toJS; + + input.click(); + final file = await completer.future; + if (file == null) return null; + + final buffer = await file.arrayBuffer().toDart; + return buffer.toDart.asUint8List(); +} diff --git a/lib/function/teacher_student_management_controller.dart b/lib/function/teacher_student_management_controller.dart index 465cf9a..293d500 100644 --- a/lib/function/teacher_student_management_controller.dart +++ b/lib/function/teacher_student_management_controller.dart @@ -1,13 +1,115 @@ -// 👥 교사용 학생 계정 관리 화면의 기능(서버 통신/상태) 담당 컨트롤러. +// 👥 교사용 학생 계정 관리 화면의 기능(서버 통신/상태/엑셀 일괄등록) 담당 컨트롤러. import 'dart:convert'; +import 'package:excel/excel.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../config.dart'; +/// 📄 엑셀 한 줄에서 뽑아낸 학생 정보 (아직 서버에 보내기 전, 미리보기용). +class BulkStudentRow { + final String studentId; + final String name; + final int? grade; + + const BulkStudentRow({ + required this.studentId, + required this.name, + this.grade, + }); +} + +/// 📊 일괄 등록 결과. [failures]는 "학번 이름: 실패사유" 형태의 문자열 목록. +class BulkImportResult { + final int successCount; + final List failures; + + const BulkImportResult({required this.successCount, required this.failures}); +} + class TeacherStudentManagementController extends ChangeNotifier { bool _isWorking = false; bool get isWorking => _isWorking; + int _bulkTotal = 0; + int _bulkDone = 0; + int get bulkTotal => _bulkTotal; + int get bulkDone => _bulkDone; + + /// 📄 엑셀 파일 바이트를 파싱한다. 1행은 머리글로 보고 건너뛰며, + /// A열=학번, B열=이름, C열=학년(선택, 숫자) 순서를 기대한다. + /// 학번이나 이름이 비어있는 줄은 조용히 무시한다. + List parseExcelBytes(Uint8List bytes) { + final excel = Excel.decodeBytes(bytes); + if (excel.tables.isEmpty) return []; + + final sheet = excel.tables.values.first; + final rows = []; + + for (final row in sheet.rows.skip(1)) { + final studentId = _cellText(row.isNotEmpty ? row[0]?.value : null); + final name = _cellText(row.length > 1 ? row[1]?.value : null); + if (studentId == null || name == null) continue; + + final grade = _cellToGrade(row.length > 2 ? row[2]?.value : null); + rows.add(BulkStudentRow(studentId: studentId, name: name, grade: grade)); + } + return rows; + } + + String? _cellText(CellValue? value) { + if (value == null) return null; + String text; + if (value is IntCellValue) { + text = value.value.toString(); + } else if (value is DoubleCellValue) { + text = value.value == value.value.roundToDouble() + ? value.value.toInt().toString() + : value.value.toString(); + } else { + text = value.toString().trim(); + } + return text.isEmpty ? null : text; + } + + int? _cellToGrade(CellValue? value) { + if (value == null) return null; + if (value is IntCellValue) return value.value; + if (value is DoubleCellValue) return value.value.round(); + return int.tryParse(value.toString().trim()); + } + + /// ➕ 엑셀에서 뽑아낸 학생 목록을 한 명씩 순서대로 서버에 등록한다 (기본 비밀번호 1234). + /// 한 명 실패해도 나머지는 계속 진행하고, 마지막에 성공/실패를 모아서 돌려준다. + Future bulkAddStudents(List rows) async { + _isWorking = true; + _bulkTotal = rows.length; + _bulkDone = 0; + notifyListeners(); + + int successCount = 0; + final failures = []; + + for (final row in rows) { + final (success, message) = await _createStudentRequest( + studentId: row.studentId, + name: row.name, + password: "1234", + grade: row.grade, + ); + if (success) { + successCount++; + } else { + failures.add('${row.studentId} ${row.name}: $message'); + } + _bulkDone++; + notifyListeners(); + } + + _isWorking = false; + notifyListeners(); + return BulkImportResult(successCount: successCount, failures: failures); + } + /// ➕ 학생 계정 추가. (성공여부, 메시지)를 반환한다 — UI가 성공했을 때만 입력칸을 비운다. Future<(bool success, String message)> addStudentAccount({ required String studentId, @@ -17,6 +119,28 @@ class TeacherStudentManagementController extends ChangeNotifier { }) async { _isWorking = true; notifyListeners(); + try { + return await _createStudentRequest( + studentId: studentId, + name: name, + password: password, + grade: grade, + ); + } finally { + _isWorking = false; + notifyListeners(); + } + } + + /// 실제 계정 생성 HTTP 요청. isWorking 상태는 건드리지 않는다 — + /// 단건 등록(addStudentAccount)과 일괄 등록(bulkAddStudents) 양쪽에서 + /// 각자 알맞은 시점에 isWorking을 관리하기 위해 분리했다. + Future<(bool success, String message)> _createStudentRequest({ + required String studentId, + required String name, + required String password, + int? grade, + }) async { try { final url = Uri.parse('$baseUrl/api/users/create'); final response = await http.post( @@ -38,9 +162,6 @@ class TeacherStudentManagementController extends ChangeNotifier { } } catch (e) { return (false, '🚨 네트워크 에러: $e'); - } finally { - _isWorking = false; - notifyListeners(); } } diff --git a/lib/ui/teacher_student_management_page.dart b/lib/ui/teacher_student_management_page.dart index a71263b..0d5a403 100644 --- a/lib/ui/teacher_student_management_page.dart +++ b/lib/ui/teacher_student_management_page.dart @@ -1,6 +1,9 @@ // 👥 교사용 학생 계정 관리 화면 (UI 전용). 신규 학생 계정 추가 폼과 강제 삭제 다이얼로그를 그린다. // 서버 통신/상태는 lib/function/teacher_student_management_controller.dart가 담당한다. +import 'dart:typed_data'; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; +import '../function/excel_file_picker.dart'; import '../function/teacher_student_management_controller.dart'; // ----------------------------------------------------------------------------- @@ -22,6 +25,7 @@ class _TeacherStudentManagementPageState final TextEditingController _addNameController = TextEditingController(); final TextEditingController _addPwController = TextEditingController(); int? _selectedGrade; + bool _isDragging = false; @override void dispose() { @@ -63,6 +67,183 @@ class _TeacherStudentManagementPageState ).showSnackBar(SnackBar(content: Text(message))); } + // 📄 [파일 선택 버튼 동작] "파일 선택"으로 엑셀 고르기 + Future _pickExcelFile() async { + final bytes = await pickExcelFileBytes(); + if (bytes == null) return; + if (!mounted) return; + await _handleExcelBytes(bytes); + } + + // 📄 엑셀 바이트를 파싱해서 미리보기 다이얼로그를 띄운다 (드래그/파일선택 공용). + Future _handleExcelBytes(Uint8List bytes) async { + List rows; + try { + rows = _controller.parseExcelBytes(bytes); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('❌ 엑셀 파일을 읽는 데 실패했습니다: $e'))); + return; + } + if (rows.isEmpty) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('⚠️ 유효한 학생 데이터를 찾지 못했습니다. (1행은 머리글로 건너뜁니다)'), + ), + ); + return; + } + _showBulkPreviewDialog(rows); + } + + // 📋 [일괄 등록 확인 팝업] 엑셀에서 뽑아낸 명단을 미리 보여주고 확정받는다. + void _showBulkPreviewDialog(List rows) { + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + title: Text('📄 ${rows.length}명 확인됨'), + content: SizedBox( + width: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '아래 명단으로 계정을 일괄 생성합니다 (초기 비밀번호: 1234).', + style: TextStyle(color: Colors.black54, fontSize: 13), + ), + const SizedBox(height: 12), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: ListView.builder( + shrinkWrap: true, + itemCount: rows.length, + itemBuilder: (context, i) { + final row = rows[i]; + return ListTile( + dense: true, + leading: CircleAvatar( + radius: 14, + child: Text( + '${i + 1}', + style: const TextStyle(fontSize: 11), + ), + ), + title: Text('${row.name} (${row.studentId})'), + trailing: Text( + row.grade != null ? '${row.grade}학년' : '미배정', + style: TextStyle(color: Colors.grey[600]), + ), + ); + }, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('취소', style: TextStyle(color: Colors.grey)), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + _runBulkImport(rows); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text('${rows.length}명 일괄 등록'), + ), + ], + ), + ); + } + + // 🚀 실제 일괄 등록 실행 + 진행 상황 표시 + 결과 요약 팝업. + Future _runBulkImport(List rows) async { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => ListenableBuilder( + listenable: _controller, + builder: (context, _) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + content: Row( + children: [ + const CircularProgressIndicator(), + const SizedBox(width: 20), + Text( + '등록 중... (${_controller.bulkDone}/${_controller.bulkTotal})', + ), + ], + ), + ), + ), + ); + + final result = await _controller.bulkAddStudents(rows); + + if (!mounted) return; + Navigator.pop(context); // 진행 팝업 닫기 + + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + title: Text( + result.failures.isEmpty ? '✅ 일괄 등록 완료' : '⚠️ 일괄 등록 완료 (일부 실패)', + ), + content: SizedBox( + width: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '성공 ${result.successCount}명 / 실패 ${result.failures.length}명', + ), + if (result.failures.isNotEmpty) ...[ + const SizedBox(height: 12), + const Text( + '실패 목록', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 6), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: SingleChildScrollView( + child: Text( + result.failures.join('\n'), + style: const TextStyle(fontSize: 12, color: Colors.red), + ), + ), + ), + ], + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('확인'), + ), + ], + ), + ); + } + // 🏫 [기존 학생 학년 지정/변경 다이얼로그] void _showUpdateGradeDialog() { final TextEditingController idController = TextEditingController(); @@ -377,6 +558,90 @@ class _TeacherStudentManagementPageState ), const SizedBox(height: 36), + // 🏷️ 인디케이터 바 (엑셀 일괄 등록) + Row( + children: [ + Container( + width: 4, + height: 16, + decoration: BoxDecoration( + color: Colors.orange, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 8), + const Text( + '엑셀로 한번에 추가 (신입생 등)', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + + DropTarget( + onDragEntered: (_) => setState(() => _isDragging = true), + onDragExited: (_) => setState(() => _isDragging = false), + onDragDone: (details) async { + setState(() => _isDragging = false); + if (details.files.isEmpty) return; + final bytes = await details.files.first.readAsBytes(); + if (!mounted) return; + await _handleExcelBytes(bytes); + }, + child: InkWell( + onTap: _pickExcelFile, + borderRadius: BorderRadius.circular(24), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 28), + decoration: BoxDecoration( + color: _isDragging ? Colors.orange[50] : Colors.white, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: _isDragging + ? Colors.orange + : Colors.grey.shade300, + width: _isDragging ? 2 : 1, + ), + ), + child: Column( + children: [ + Icon( + Icons.upload_file_rounded, + size: 40, + color: _isDragging + ? Colors.orange + : Colors.grey[400], + ), + const SizedBox(height: 12), + Text( + _isDragging ? '여기에 놓으세요' : '엑셀 파일을 드래그하거나 눌러서 선택', + style: TextStyle( + fontWeight: FontWeight.bold, + color: _isDragging + ? Colors.orange[800] + : Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + '.xlsx · 1행은 머리글, A열=학번 B열=이름 C열=학년(선택)', + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + const SizedBox(height: 36), + // 🏷️ 인디케이터 바 (기존 학생 학년 지정/변경) Row( children: [ diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..8fdc934 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_drop_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); + desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..721c8d7 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8fac3c8..99d9634 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,13 +5,17 @@ import FlutterMacOS import Foundation +import desktop_drop import device_info_plus +import file_picker_darwin import firebase_core import firebase_messaging import flutter_local_notifications func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index ac6a778..cd5ebe6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.75" + android_file_picker: + dependency: transitive + description: + name: android_file_picker + sha256: "1f111ed6bb33724ba782cc86357e3fd97f57051d55fc61adf33dcfc5049a1581" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" args: dependency: transitive description: @@ -57,6 +73,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6 + url: "https://pub.dev" + source: hosted + version: "0.3.5+5" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -73,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.14" + desktop_drop: + dependency: "direct main" + description: + name: desktop_drop + sha256: c65d1f959ccf4bb6803f984303d03e1e0dd4c0f73a3a2eca2d1b46d48ea16999 + url: "https://pub.dev" + source: hosted + version: "0.8.2" device_info_plus: dependency: "direct main" description: @@ -89,6 +129,22 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + excel: + dependency: "direct main" + description: + name: excel + sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780" + url: "https://pub.dev" + source: hosted + version: "4.0.6" fake_async: dependency: transitive description: @@ -121,6 +177,46 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: b7acb5d123cb398f6b4a8a38e43777545254f8fc1fabef3ee11cbbb56aece9c8 + url: "https://pub.dev" + source: hosted + version: "12.1.2" + file_picker_darwin: + dependency: transitive + description: + name: file_picker_darwin + sha256: "6e7cae501d48fd57a27911608db718ebd898e6ccf934bf09e33a8c0d0365bec0" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + file_picker_linux: + dependency: transitive + description: + name: file_picker_linux + sha256: f0f01ed42967b7355f6f25c8b121ea531d1948e2a9b4b44f4b4de8489d7b04ae + url: "https://pub.dev" + source: hosted + version: "1.0.2" + file_picker_platform_interface: + dependency: transitive + description: + name: file_picker_platform_interface + sha256: "11ef1b5c14d9186b4788cc273dd8d5cb81bca847145060991a28234ff7916fc1" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + file_picker_web: + dependency: transitive + description: + name: file_picker_web + sha256: df472142f63c4557fdfbb375c81454ca1c251491069eefcdc6a866bc12f8750b + url: "https://pub.dev" + source: hosted + version: "3.0.3" firebase_core: dependency: "direct main" description: @@ -517,6 +613,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" vector_math: dependency: transitive description: @@ -550,7 +654,7 @@ packages: source: hosted version: "15.2.0" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" @@ -573,6 +677,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + windows_file_picker: + dependency: transitive + description: + name: windows_file_picker + sha256: "225f58e64c15c2d7b34fb8faf3ca188831967f26b5a723d7c3a7969e41c9b5a5" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9f3c497..5b6c50b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,10 @@ dependencies: # 📳 무단 반출 감지 시 진동 (백그라운드 서비스에서도 동작하도록 HapticFeedback 대신 사용) vibration: ^3.2.0 + excel: ^4.0.6 + file_picker: ^12.1.2 + desktop_drop: ^0.8.2 + web: ^1.1.1 dev_dependencies: flutter_test: @@ -63,11 +67,6 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5893c2f..acda2b1 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopDropPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopDropPlugin")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4e30109..612d637 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop firebase_core permission_handler_windows )