diff --git a/lib/function/student_management_controller.dart b/lib/function/student_management_controller.dart deleted file mode 100644 index 7e71c20..0000000 --- a/lib/function/student_management_controller.dart +++ /dev/null @@ -1,102 +0,0 @@ -// πŸ” (λ§ˆμŠ€ν„° κ³„μ •μš©) 학생 계정 및 κΈ°κΈ° 관리 ν™”λ©΄μ˜ κΈ°λŠ₯(μ„œλ²„ 톡신/μƒνƒœ) λ‹΄λ‹Ή 컨트둀러. -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; -import '../config.dart'; - -class StudentManagementController extends ChangeNotifier { - List _students = []; - bool _isLoading = true; - - List get students => _students; - bool get isLoading => _isLoading; - - Future init() => fetchStudents(); - - /// 🌐 μ„œλ²„μ—μ„œ 전체 학생 λͺ©λ‘ 뢈러였기. μ‹€νŒ¨ μ‹œ μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό λ°˜ν™˜ν•œλ‹€(성곡 μ‹œ null). - Future fetchStudents() async { - _isLoading = 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'] ?? []; - _isLoading = false; - notifyListeners(); - return null; - } - _isLoading = false; - notifyListeners(); - return '데이터 뢈러였기 μ‹€νŒ¨ (${response.statusCode})'; - } catch (e) { - _isLoading = false; - notifyListeners(); - return '데이터 뢈러였기 μ‹€νŒ¨: $e'; - } - } - - /// 🌐 μ„œλ²„λ‘œ κΈ°κΈ° μ΄ˆκΈ°ν™”(리셋) λͺ…λ Ή 보내기 - Future<(bool success, String message)> resetDevice( - String studentId, - String studentName, - ) async { - 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 학생 κΈ°κΈ° μ΄ˆκΈ°ν™” μ™„λ£Œ!'); - } - return (false, '❌ μ΄ˆκΈ°ν™” 톡신 μ‹€νŒ¨'); - } catch (e) { - return (false, '❌ μ΄ˆκΈ°ν™” 톡신 μ‹€νŒ¨'); - } - } - - /// 🌐 μ„œλ²„λ‘œ 계정 μ™„μ „ μ‚­μ œ λͺ…λ Ή 보내기 - Future<(bool success, String message)> deleteUser( - String studentId, - String studentName, - ) async { - try { - final response = await http.delete( - Uri.parse('$baseUrl/api/users/delete'), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({"studentId": studentId}), - ); - if (response.statusCode == 200) { - await fetchStudents(); - return (true, 'πŸ’₯ $studentName ν•™μƒμ˜ 계정이 영ꡬ μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); - } - return (false, '❌ 계정 μ‚­μ œ 톡신 μ‹€νŒ¨'); - } catch (e) { - return (false, '❌ 계정 μ‚­μ œ 톡신 μ‹€νŒ¨'); - } - } - - /// 🌐 μ‹ κ·œ 학생 계정 생성 - Future<(bool success, String message)> createUser( - String studentId, - String name, - ) async { - try { - final response = await http.post( - Uri.parse('$baseUrl/api/users/create'), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({"studentId": studentId, "name": name}), - ); - final res = jsonDecode(response.body); - if (res['status'] == 'success') { - await fetchStudents(); - return (true, 'βœ… ${res['message']}'); - } - return (false, '❌ ${res['message']}'); - } catch (e) { - return (false, '❌ 학생 등둝 μ‹€νŒ¨ (톡신 μ—λŸ¬)'); - } - } -} diff --git a/lib/function/teacher_student_management_controller.dart b/lib/function/teacher_student_management_controller.dart index 293d500..988af49 100644 --- a/lib/function/teacher_student_management_controller.dart +++ b/lib/function/teacher_student_management_controller.dart @@ -35,6 +35,58 @@ class TeacherStudentManagementController extends ChangeNotifier { 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μ—΄=ν•™λ…„(선택, 숫자) μˆœμ„œλ₯Ό κΈ°λŒ€ν•œλ‹€. /// ν•™λ²ˆμ΄λ‚˜ 이름이 λΉ„μ–΄μžˆλŠ” 쀄은 쑰용히 λ¬΄μ‹œν•œλ‹€. @@ -105,6 +157,7 @@ class TeacherStudentManagementController extends ChangeNotifier { notifyListeners(); } + await fetchStudents(); _isWorking = false; notifyListeners(); return BulkImportResult(successCount: successCount, failures: failures); @@ -120,12 +173,14 @@ class TeacherStudentManagementController extends ChangeNotifier { _isWorking = true; notifyListeners(); try { - return await _createStudentRequest( + final result = await _createStudentRequest( studentId: studentId, name: name, password: password, grade: grade, ); + if (result.$1) await fetchStudents(); + return result; } finally { _isWorking = false; notifyListeners(); @@ -182,6 +237,7 @@ class TeacherStudentManagementController extends ChangeNotifier { 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'] ?? '였λ₯˜ λ°œμƒ'}'); @@ -209,6 +265,7 @@ class TeacherStudentManagementController extends ChangeNotifier { ); if (response.statusCode == 200) { final resData = jsonDecode(utf8.decode(response.bodyBytes)); + await fetchStudents(); return (true, 'βœ… ${resData['message']}'); } else { return (false, '❌ μ‚­μ œ μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€.'); diff --git a/lib/ui/student_dashboard.dart b/lib/ui/student_dashboard.dart index fac5f0f..fa0a9e0 100644 --- a/lib/ui/student_dashboard.dart +++ b/lib/ui/student_dashboard.dart @@ -7,7 +7,7 @@ import 'nfc_poccket_checkin_screen.dart'; import 'login_screen.dart'; import 'teacher_attendance_page.dart'; import 'admin_dashboard.dart'; -import 'student_management_screen.dart'; +import 'teacher_student_management_page.dart'; import 'nfc_tag_writer_screen.dart'; // ------------------------------------------------------------- @@ -384,7 +384,7 @@ class _StudentDashboardState extends State { context, MaterialPageRoute( builder: (context) => - const StudentManagementScreen(), + const TeacherStudentManagementPage(), ), ), ), diff --git a/lib/ui/student_management_screen.dart b/lib/ui/student_management_screen.dart deleted file mode 100644 index 77232d2..0000000 --- a/lib/ui/student_management_screen.dart +++ /dev/null @@ -1,307 +0,0 @@ -// πŸ” (λ§ˆμŠ€ν„° κ³„μ •μš©) 학생 계정 및 κΈ°κΈ° 관리 ν™”λ©΄ (UI μ „μš©). κΈ°κΈ° UUID μ΄ˆκΈ°ν™”/계정 μ‚­μ œ/생성 λ‹€μ΄μ–Όλ‘œκ·Έλ₯Ό κ·Έλ¦°λ‹€. -// μ„œλ²„ 톡신/μƒνƒœλŠ” lib/function/student_management_controller.dartκ°€ λ‹΄λ‹Ήν•œλ‹€. -import 'package:flutter/material.dart'; -import '../function/student_management_controller.dart'; - -// ========================================== -// πŸ” 6. 학생 계정 및 κΈ°κΈ° 관리 ν™”λ©΄ (κΈ°κΈ° 리셋 + 계정 μ‚­μ œ μ™„λ³Έ) -// ========================================== -class StudentManagementScreen extends StatefulWidget { - const StudentManagementScreen({super.key}); - - @override - State createState() => - _StudentManagementScreenState(); -} - -class _StudentManagementScreenState extends State { - final StudentManagementController _controller = - StudentManagementController(); - - @override - void initState() { - super.initState(); - _controller.init(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - Future _fetchStudents() async { - final error = await _controller.fetchStudents(); - if (error != null && mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error))); - } - } - - // 🌐 μ„œλ²„λ‘œ κΈ°κΈ° μ΄ˆκΈ°ν™”(리셋) λͺ…λ Ή 보내기 - void _resetDevice(String studentId, String studentName) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text( - '⚠️ κΈ°κΈ° 잠금 ν•΄μ œ', - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.purple), - ), - content: Text('$studentName ν•™μƒμ˜ 슀마트폰 κΈ°κΈ° 등둝을 μ΄ˆκΈ°ν™”ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('μ·¨μ†Œ', style: TextStyle(color: Colors.grey)), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.purple, - foregroundColor: Colors.white, - ), - onPressed: () async { - Navigator.pop(ctx); - final (_, message) = await _controller.resetDevice( - studentId, - studentName, - ); - if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); - }, - child: const Text('μ΄ˆκΈ°ν™” 승인'), - ), - ], - ), - ); - } - - // 🌐 [μƒˆ κΈ°λŠ₯] μ„œλ²„λ‘œ 계정 μ™„μ „ μ‚­μ œ λͺ…λ Ή 보내기 - void _deleteUser(String studentId, String studentName) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text( - '🚨 계정 μ™„μ „ μ‚­μ œ κ²½κ³ ', - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red), - ), - content: Text( - 'μ •λ§λ‘œ $studentName ($studentId) ν•™μƒμ˜ 계정을 μ‹œμŠ€ν…œμ—μ„œ νƒˆν‡΄(μ‚­μ œ)μ‹œν‚€κ² μŠ΅λ‹ˆκΉŒ?\n\n이 μž‘μ—…μ€ 되돌릴 수 μ—†μœΌλ©°, ν•΄λ‹Ή 학생은 λ‹€μ‹œ νšŒμ›κ°€μž…μ„ μ§„ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('μ·¨μ†Œ', style: TextStyle(color: Colors.grey)), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - onPressed: () async { - Navigator.pop(ctx); - final (_, message) = await _controller.deleteUser( - studentId, - studentName, - ); - if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); - }, - child: const Text('영ꡬ μ‚­μ œ'), - ), - ], - ), - ); - } - - void _showCreateUserDialog() { - final idController = TextEditingController(); - final nameController = TextEditingController(); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('πŸ‘€ μ‹ κ·œ 학생 계정 μΆ”κ°€'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: idController, - decoration: const InputDecoration(labelText: 'ν•™λ²ˆ μž…λ ₯ (예: 20101)'), - keyboardType: TextInputType.number, - ), - const SizedBox(height: 8), - TextField( - controller: nameController, - decoration: const InputDecoration(labelText: '학생 이름 μž…λ ₯'), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('μ·¨μ†Œ'), - ), - ElevatedButton( - onPressed: () async { - String studentId = idController.text.trim(); - String name = nameController.text.trim(); - - if (studentId.isEmpty || name.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('⚠️ ν•™λ²ˆκ³Ό 이름을 λͺ¨λ‘ μž…λ ₯ν•˜μ„Έμš”.')), - ); - return; - } - - final (success, message) = await _controller.createUser( - studentId, - name, - ); - if (!mounted) return; - if (success) { - Navigator.pop(context); // νŒμ—… λ‹«κΈ° - } - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); - }, - child: const Text('생성'), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - return ListenableBuilder( - listenable: _controller, - builder: (context, _) { - final realStudents = _controller.students; - return Scaffold( - backgroundColor: Colors.grey[50], - appBar: AppBar( - title: const Text( - '학생 계정 및 κΈ°κΈ° 관리', - style: TextStyle(fontWeight: FontWeight.bold), - ), - backgroundColor: Colors.purple, - foregroundColor: Colors.white, - actions: [ - IconButton( - icon: const Icon(Icons.person_add_alt_1_rounded), - onPressed: () => _showCreateUserDialog(), - ), - IconButton(icon: const Icon(Icons.refresh), onPressed: _fetchStudents), - ], - ), - body: _controller.isLoading - ? const Center( - child: CircularProgressIndicator(color: Colors.purple), - ) - : realStudents.isEmpty - ? const Center( - child: Text( - 'κ°€μž…λœ 학생 계정이 μ—†μŠ΅λ‹ˆλ‹€.', - style: TextStyle(color: Colors.grey, fontSize: 16), - ), - ) - : ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: realStudents.length, - itemBuilder: (context, index) { - final student = realStudents[index]; - final bool needsReset = student['device'] == "μ΄ˆκΈ°ν™” ν•„μš”"; - - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: ListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - leading: CircleAvatar( - backgroundColor: needsReset - ? Colors.red[50] - : Colors.purple[50], - child: Icon( - needsReset ? Icons.lock_reset : Icons.person, - color: needsReset ? Colors.red : Colors.purple, - ), - ), - title: Text( - '${student['name']} (${student['id']})', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - subtitle: Text( - needsReset ? 'κΈ°κΈ° μ΄ˆκΈ°ν™” 승인 λŒ€κΈ°μ€‘' : '정상 등둝 μƒνƒœ', - style: TextStyle( - color: needsReset ? Colors.red : Colors.grey, - fontWeight: needsReset - ? FontWeight.bold - : FontWeight.normal, - ), - ), - // πŸ•ΉοΈ 였λ₯Έμͺ½ 끝에 [κΈ°κΈ° 리셋] λ²„νŠΌκ³Ό [μ“°λ ˆκΈ°ν†΅(μ‚­μ œ)] λ²„νŠΌμ„ λ‚˜λž€νžˆ λ°°μΉ˜ν•˜λŠ” Row λ ˆμ΄μ•„μ›ƒ 기획 - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!needsReset) - OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.purple, - side: BorderSide( - color: Colors.purple.withValues( - alpha: 0.5, - ), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: () => _resetDevice( - student['id'], - student['name'], - ), - child: const Text('κΈ°κΈ° 리셋'), - ) - else - const Icon( - Icons.check_circle, - color: Colors.green, - ), - - const SizedBox(width: 8), - - // πŸ”΄ [μƒˆ λ²„νŠΌ] μ΅œκ³ κΆŒν•œ λ§ˆμŠ€ν„° μ „μš© 계정 영ꡬ μ‚­μ œ μ“°λ ˆκΈ°ν†΅ λ²„νŠΌ! - IconButton( - icon: const Icon( - Icons.delete_forever_rounded, - color: Colors.redAccent, - ), - tooltip: '계정 영ꡬ μ‚­μ œ', - onPressed: () => - _deleteUser(student['id'], student['name']), - ), - ], - ), - ), - ); - }, - ), - ); - }, - ); - } -} diff --git a/lib/ui/teacher_student_management_page.dart b/lib/ui/teacher_student_management_page.dart index 0d5a403..bccdd5f 100644 --- a/lib/ui/teacher_student_management_page.dart +++ b/lib/ui/teacher_student_management_page.dart @@ -27,6 +27,12 @@ class _TeacherStudentManagementPageState int? _selectedGrade; bool _isDragging = false; + @override + void initState() { + super.initState(); + _controller.init(); + } + @override void dispose() { _controller.dispose(); @@ -244,185 +250,125 @@ class _TeacherStudentManagementPageState ); } - // 🏫 [κΈ°μ‘΄ 학생 ν•™λ…„ μ§€μ •/λ³€κ²½ λ‹€μ΄μ–Όλ‘œκ·Έ] - void _showUpdateGradeDialog() { - final TextEditingController idController = TextEditingController(); - int? grade; + // 🏫 [λͺ©λ‘ ν–‰μ˜ "ν•™λ…„" μΉ©] λˆŒλŸ¬μ„œ λ°”λ‘œ ν•™λ…„ λ³€κ²½ (λ―Έλ°°μ •/1/2/3ν•™λ…„ 쀑 선택). + Future _showGradeMenu( + BuildContext tileContext, + String studentId, + String studentName, + int? currentGrade, + ) async { + final RenderBox box = tileContext.findRenderObject() as RenderBox; + final Offset position = box.localToGlobal(Offset.zero); - showDialog( - context: context, - builder: (context) { - return StatefulBuilder( - builder: (context, setDialogState) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - title: const Text( - '🏫 ν•™λ…„ μ§€μ •/λ³€κ²½', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '이미 λ§Œλ“€μ–΄μ§„ 학생 κ³„μ •μ˜ 학년을 μ§€μ •ν•˜κ±°λ‚˜ λ°”κΏ‰λ‹ˆλ‹€.', - style: TextStyle(color: Colors.black54, fontSize: 13), - ), - const SizedBox(height: 16), - TextField( - controller: idController, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'ν•™λ²ˆ', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.badge), - ), - ), - const SizedBox(height: 12), - DropdownButtonFormField( - initialValue: grade, - decoration: const InputDecoration( - labelText: 'ν•™λ…„', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.class_), - ), - items: const [ - DropdownMenuItem(value: null, child: Text('λ―Έλ°°μ •')), - DropdownMenuItem(value: 1, child: Text('1ν•™λ…„')), - DropdownMenuItem(value: 2, child: Text('2ν•™λ…„')), - DropdownMenuItem(value: 3, child: Text('3ν•™λ…„')), - ], - onChanged: (value) => setDialogState(() => grade = value), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('μ·¨μ†Œ', style: TextStyle(color: Colors.grey)), - ), - ElevatedButton( - onPressed: () async { - final inputId = idController.text.trim(); - if (inputId.isEmpty) return; - Navigator.pop(context); - final (_, message) = await _controller.updateStudentGrade( - studentId: inputId, - grade: grade, - ); - if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text( - '적용', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ], - ); - }, - ); - }, + final selected = await showMenu( + context: tileContext, + position: RelativeRect.fromLTRB( + position.dx, + position.dy + box.size.height, + position.dx, + 0, + ), + items: const [ + PopupMenuItem(value: null, child: Text('λ―Έλ°°μ •')), + PopupMenuItem(value: 1, child: Text('1ν•™λ…„')), + PopupMenuItem(value: 2, child: Text('2ν•™λ…„')), + PopupMenuItem(value: 3, child: Text('3ν•™λ…„')), + ], ); - } + if (selected == currentGrade) return; // μ·¨μ†Œν–ˆκ±°λ‚˜ 같은 κ°’ 선택 + if (!mounted) return; - // ❌ [학생 μ‚­μ œ λ²„νŠΌ λ™μž‘] - Future _deleteStudentAccount(String studentId) async { - final (_, message) = await _controller.deleteStudentAccount(studentId); + final (_, message) = await _controller.updateStudentGrade( + studentId: studentId, + grade: selected, + ); if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); } - // 🚨 계정 μ‚­μ œ 확인 νŒμ—…μ°½ λͺ¨λ‹¬ - void _showDeleteDialog() { - final TextEditingController deleteIdController = TextEditingController(); + // πŸ”“ [λͺ©λ‘ ν–‰μ˜ "κΈ°κΈ° 리셋" λ²„νŠΌ] 학생이 폰을 바꿨을 λ•Œ 등둝 μ΄ˆκΈ°ν™”. + void _confirmResetDevice(String studentId, String studentName) { showDialog( context: context, - builder: (context) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), + builder: (ctx) => AlertDialog( + title: const Text( + '⚠️ κΈ°κΈ° 등둝 μ΄ˆκΈ°ν™”', + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.purple), + ), + content: Text('$studentName ν•™μƒμ˜ 슀마트폰 κΈ°κΈ° 등둝과 λΉ„λ°€λ²ˆν˜Έ(1234)λ₯Ό μ΄ˆκΈ°ν™”ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('μ·¨μ†Œ', style: TextStyle(color: Colors.grey)), ), - title: Row( - children: [ - Icon(Icons.warning_amber_rounded, color: Colors.orange[700]), - const SizedBox(width: 10), - const Text( - '학생 계정 κ°•μ œ μ‚­μ œ', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - '영ꡬ μ‚­μ œν•  ν•™μƒμ˜ ν•™λ²ˆμ„ μ •ν™•ν•˜κ²Œ μž…λ ₯ν•˜μ„Έμš”.', - style: TextStyle(color: Colors.black54, fontSize: 13), - ), - const SizedBox(height: 16), - TextField( - controller: deleteIdController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'ν•™λ²ˆ μž…λ ₯', - labelStyle: TextStyle(color: Colors.orange[700]), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: Colors.orange[700]!, - width: 2, - ), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - ), - prefixIcon: const Icon(Icons.no_accounts_rounded), - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('μ·¨μ†Œ', style: TextStyle(color: Colors.grey)), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.purple, + foregroundColor: Colors.white, ), - ElevatedButton( - onPressed: () { - final inputId = deleteIdController.text.trim(); - if (inputId.isNotEmpty) { - Navigator.pop(context); - _deleteStudentAccount(inputId); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange[700], - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text( - 'μ‚­μ œ ν™•μ •', - style: TextStyle(fontWeight: FontWeight.bold), - ), + onPressed: () async { + Navigator.pop(ctx); + final (_, message) = await _controller.resetDevice( + studentId, + studentName, + ); + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + }, + child: const Text('μ΄ˆκΈ°ν™” 승인'), + ), + ], + ), + ); + } + + // 🚨 [λͺ©λ‘ ν–‰μ˜ μ‚­μ œ λ²„νŠΌ] 계정 μ™„μ „ μ‚­μ œ 확인 νŒμ—…. + void _confirmDeleteStudent(String studentId, String studentName) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + title: Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Colors.red[700]), + const SizedBox(width: 10), + const Text( + '계정 μ™„μ „ μ‚­μ œ', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), ], - ); - }, + ), + content: Text( + 'μ •λ§λ‘œ $studentName ($studentId) ν•™μƒμ˜ 계정을 영ꡬ μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?\n이 μž‘μ—…μ€ 되돌릴 수 μ—†μŠ΅λ‹ˆλ‹€.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('μ·¨μ†Œ', style: TextStyle(color: Colors.grey)), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red[600], + foregroundColor: Colors.white, + ), + onPressed: () async { + Navigator.pop(ctx); + final (_, message) = await _controller.deleteStudentAccount( + studentId, + ); + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + }, + child: const Text('영ꡬ μ‚­μ œ'), + ), + ], + ), ); } @@ -642,7 +588,7 @@ class _TeacherStudentManagementPageState ), const SizedBox(height: 36), - // 🏷️ 인디케이터 λ°” (κΈ°μ‘΄ 학생 ν•™λ…„ μ§€μ •/λ³€κ²½) + // 🏷️ 인디케이터 λ°” (전체 학생 λͺ©λ‘) Row( children: [ Container( @@ -655,139 +601,134 @@ class _TeacherStudentManagementPageState ), const SizedBox(width: 8), const Text( - 'κΈ°μ‘΄ 학생 ν•™λ…„ μ§€μ •/λ³€κ²½', + '전체 학생 λͺ©λ‘', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), - ], - ), - const SizedBox(height: 16), - - InkWell( - onTap: _showUpdateGradeDialog, - borderRadius: BorderRadius.circular(24), - child: Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.blue[50], - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.blue.shade200), - ), - child: const Row( - children: [ - Icon(Icons.class_rounded, color: Colors.blue, size: 32), - SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'ν•™λ²ˆμœΌλ‘œ ν•™λ…„ μ§€μ •', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.blue, - ), - ), - SizedBox(height: 2), - Text( - '이미 λ§Œλ“  κ³„μ •μ˜ 학년을 λ‚˜μ€‘μ— μ§€μ •ν•˜κ±°λ‚˜ λ°”κΏ€ λ•Œ μ‚¬μš©', - style: TextStyle( - fontSize: 12, - color: Colors.blueAccent, - ), - ), - ], - ), - ), - Icon( - Icons.arrow_forward_ios_rounded, - color: Colors.blue, - size: 16, - ), - ], - ), - ), - ), - const SizedBox(height: 36), - - // 🏷️ 인디케이터 λ°” 2 (μ‚­μ œ κΆŒν•œ μ œμ–΄λ©”λ‰΄) - Row( - children: [ - Container( - width: 4, - height: 16, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 8), - const Text( - 'μœ„ν—˜ ꡬ역 (Account Reset)', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.redAccent, - ), + const Spacer(), + IconButton( + icon: const Icon(Icons.refresh_rounded), + onPressed: _controller.isLoadingStudents + ? null + : _controller.fetchStudents, + tooltip: 'μƒˆλ‘œκ³ μΉ¨', ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 8), + const Text( + 'ν•™λ…„ 칩을 눌러 학년을 λ°”κΎΈκ³ , κΈ°κΈ° 리셋은 학생이 폰을 바꿨을 λ•Œ μ‚¬μš©ν•˜μ„Έμš”.', + style: TextStyle(color: Colors.black54, fontSize: 12), + ), + const SizedBox(height: 12), - // 🚨 학생 μ‚­μ œ 트리거 μΉ΄λ“œ - InkWell( - onTap: _showDeleteDialog, - borderRadius: BorderRadius.circular(24), - child: Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.red[50], - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.red.shade200), - ), - child: const Row( - children: [ - Icon( - Icons.delete_sweep_rounded, - color: Colors.red, - size: 32, - ), - SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '학생 계정 κ°•μ œ 원격 μ‚­μ œ', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.red, - ), - ), - SizedBox(height: 2), - Text( - '인증 μ΄ˆκΈ°ν™” 및 DB 제거용', - style: TextStyle( - fontSize: 12, - color: Colors.redAccent, - ), - ), - ], + _controller.isLoadingStudents + ? const Padding( + padding: EdgeInsets.symmetric(vertical: 40), + child: Center(child: CircularProgressIndicator()), + ) + : _controller.students.isEmpty + ? const Padding( + padding: EdgeInsets.symmetric(vertical: 24), + child: Center( + child: Text( + 'κ°€μž…λœ 학생 계정이 μ—†μŠ΅λ‹ˆλ‹€.', + style: TextStyle(color: Colors.grey), ), ), - Icon( - Icons.arrow_forward_ios_rounded, - color: Colors.red, - size: 16, - ), - ], - ), - ), - ), + ) + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _controller.students.length, + itemBuilder: (context, index) { + final student = _controller.students[index]; + final String studentId = student['id'].toString(); + final String studentName = student['name'].toString(); + final int? grade = student['grade'] as int?; + final bool needsReset = student['device'] == 'μ΄ˆκΈ°ν™” ν•„μš”'; + + return Card( + elevation: 1, + margin: const EdgeInsets.only(bottom: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + leading: CircleAvatar( + backgroundColor: needsReset + ? Colors.red[50] + : Colors.blue[50], + child: Icon( + needsReset ? Icons.lock_reset : Icons.person, + color: needsReset ? Colors.red : Colors.blue, + ), + ), + title: Text( + '$studentName ($studentId)', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + needsReset ? 'κΈ°κΈ° μ΄ˆκΈ°ν™” 승인 λŒ€κΈ°μ€‘' : '정상 등둝 μƒνƒœ', + style: TextStyle( + color: needsReset ? Colors.red : Colors.grey, + fontWeight: needsReset + ? FontWeight.bold + : FontWeight.normal, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Builder( + builder: (chipContext) => ActionChip( + label: Text( + grade != null ? '$gradeν•™λ…„' : 'λ―Έλ°°μ •', + ), + onPressed: () => _showGradeMenu( + chipContext, + studentId, + studentName, + grade, + ), + ), + ), + IconButton( + icon: const Icon( + Icons.lock_reset, + color: Colors.purple, + ), + tooltip: 'κΈ°κΈ° 리셋', + onPressed: () => _confirmResetDevice( + studentId, + studentName, + ), + ), + IconButton( + icon: const Icon( + Icons.delete_forever_rounded, + color: Colors.redAccent, + ), + tooltip: '계정 영ꡬ μ‚­μ œ', + onPressed: () => _confirmDeleteStudent( + studentId, + studentName, + ), + ), + ], + ), + ), + ); + }, + ), ], ), ),