// πŸ‘₯ κ΅μ‚¬μš© 학생 계정 관리 ν™”λ©΄μ˜ κΈ°λŠ₯(μ„œλ²„ 톡신/μƒνƒœ/μ—‘μ…€ 일괄등둝) λ‹΄λ‹Ή 컨트둀러. 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; List _students = []; bool _isLoadingStudents = true; List get students => _students; bool get isLoadingStudents => _isLoadingStudents; Future init() => fetchStudents(); /// 🌐 전체 학생 λͺ©λ‘ 뢈러였기 (ν•™λ²ˆ/이름/ν•™λ…„/κΈ°κΈ°λ“±λ‘μƒνƒœ). Future fetchStudents() async { _isLoadingStudents = true; notifyListeners(); try { final response = await http.get(Uri.parse('$baseUrl/api/users')); if (response.statusCode == 200) { final data = jsonDecode(utf8.decode(response.bodyBytes)); _students = data['users'] ?? []; } } catch (_) { // λͺ©λ‘ μƒˆλ‘œκ³ μΉ¨ μ‹€νŒ¨λŠ” 쑰용히 λ¬΄μ‹œ β€” ν™”λ©΄μ—” λ§ˆμ§€λ§‰μœΌλ‘œ 뢈러온 λͺ©λ‘μ΄ λ‚¨λŠ”λ‹€. } finally { _isLoadingStudents = false; notifyListeners(); } } /// 🌐 ν•™μƒμ˜ λ“±λ‘λœ κΈ°κΈ°(슀마트폰) UUID + λΉ„λ°€λ²ˆν˜Έ(1234)λ₯Ό μ΄ˆκΈ°ν™”ν•œλ‹€. /// (학생이 폰을 바꿨을 λ•Œ λ‹€μ‹œ 등둝할 수 있게 ν•΄μ£ΌλŠ” μš©λ„) Future<(bool success, String message)> resetDevice( String studentId, String studentName, ) async { _isWorking = true; notifyListeners(); try { final response = await http.post( Uri.parse('$baseUrl/api/users/reset'), headers: {"Content-Type": "application/json"}, body: jsonEncode({"studentId": studentId}), ); if (response.statusCode == 200) { await fetchStudents(); return (true, '$studentName 학생 κΈ°κΈ° μ΄ˆκΈ°ν™” μ™„λ£Œ! (λΉ„λ°€λ²ˆν˜Έλ„ 1234둜 μ΄ˆκΈ°ν™”λ¨)'); } return (false, 'μ΄ˆκΈ°ν™” 톡신 μ‹€νŒ¨'); } catch (e) { return (false, 'μ΄ˆκΈ°ν™” 톡신 μ‹€νŒ¨: $e'); } finally { _isWorking = false; notifyListeners(); } } /// πŸ“„ μ—‘μ…€ 파일 λ°”μ΄νŠΈλ₯Ό νŒŒμ‹±ν•œλ‹€. 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(); } await fetchStudents(); _isWorking = false; notifyListeners(); return BulkImportResult(successCount: successCount, failures: failures); } /// βž• 학생 계정 μΆ”κ°€. (성곡여뢀, λ©”μ‹œμ§€)λ₯Ό λ°˜ν™˜ν•œλ‹€ β€” UIκ°€ μ„±κ³΅ν–ˆμ„ λ•Œλ§Œ μž…λ ₯칸을 λΉ„μš΄λ‹€. Future<(bool success, String message)> addStudentAccount({ required String studentId, required String name, required String password, int? grade, }) async { _isWorking = true; notifyListeners(); try { final result = await _createStudentRequest( studentId: studentId, name: name, password: password, grade: grade, ); if (result.$1) await fetchStudents(); return result; } 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( url, headers: {"Content-Type": "application/json"}, body: jsonEncode({ "studentId": studentId, "name": name, "password": password, "grade": grade, }), ); final result = jsonDecode(utf8.decode(response.bodyBytes)); if (response.statusCode == 200 && result['status'] == 'success') { return (true, '${result['message'] ?? '계정이 μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'}'); } else { return (false, '생성 μ‹€νŒ¨: ${result['message'] ?? '였λ₯˜ λ°œμƒ'}'); } } catch (e) { return (false, 'λ„€νŠΈμ›Œν¬ μ—λŸ¬: $e'); } } /// 🏫 이미 λ§Œλ“€μ–΄μ§„ 학생 κ³„μ •μ˜ 학년을 μ§€μ •/λ³€κ²½ν•œλ‹€. gradeκ°€ null이면 "λ―Έλ°°μ •"으둜 λ˜λŒλ¦°λ‹€. Future<(bool success, String message)> updateStudentGrade({ required String studentId, int? grade, }) async { _isWorking = true; notifyListeners(); try { final url = Uri.parse('$baseUrl/api/users/update-grade'); final response = await http.post( url, headers: {"Content-Type": "application/json"}, body: jsonEncode({"studentId": studentId, "grade": grade}), ); final result = jsonDecode(utf8.decode(response.bodyBytes)); if (response.statusCode == 200 && result['status'] == 'success') { await fetchStudents(); return (true, '${result['message'] ?? '학년이 μ§€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'}'); } else { return (false, 'μ§€μ • μ‹€νŒ¨: ${result['message'] ?? '였λ₯˜ λ°œμƒ'}'); } } catch (e) { return (false, 'λ„€νŠΈμ›Œν¬ μ—λŸ¬: $e'); } finally { _isWorking = false; notifyListeners(); } } /// ❌ 학생 계정 μ‚­μ œ. Future<(bool success, String message)> deleteStudentAccount( String studentId, ) async { _isWorking = true; notifyListeners(); final url = Uri.parse('$baseUrl/api/users/delete'); try { final response = await http.delete( url, headers: {"Content-Type": "application/json"}, body: jsonEncode({"studentId": studentId}), ); if (response.statusCode == 200) { final resData = jsonDecode(utf8.decode(response.bodyBytes)); await fetchStudents(); return (true, '${resData['message']}'); } else { return (false, 'μ‚­μ œ μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€.'); } } catch (e) { return (false, 'μ„œλ²„ μ—λŸ¬ λ°œμƒ'); } finally { _isWorking = false; notifyListeners(); } } }