diff --git a/lib/screens/teacher_attendance_page.dart b/lib/screens/teacher_attendance_page.dart index 3629ff4..9385796 100644 --- a/lib/screens/teacher_attendance_page.dart +++ b/lib/screens/teacher_attendance_page.dart @@ -24,6 +24,7 @@ class _TeacherAttendancePageState extends State { List _roster = []; // 전체 학생 명단 (/api/users) List _logs = []; // 출석 로그 (/api/logs) Map _activeViolationsByStudentId = {}; // 무단반출 중인 학생 (/api/violations/active) + String? _dismissedAt; // 오늘 가장 최근 하교 처리 시각 (/api/dismissal/latest). 이 시각 이후 기록만 "오늘 출석"으로 표시. Timer? _timer; bool _isLoading = true; bool _isRefreshing = false; // 새로고침 버튼 클릭 시 잠깐 도는 표시용 @@ -54,11 +55,13 @@ class _TeacherAttendancePageState extends State { http.get(Uri.parse('$baseUrl/api/users')), http.get(Uri.parse('$baseUrl/api/logs')), http.get(Uri.parse('$baseUrl/api/violations/active')), + http.get(Uri.parse('$baseUrl/api/dismissal/latest')), ]); final usersRes = results[0]; final logsRes = results[1]; final violationsRes = results[2]; + final dismissalRes = results[3]; if (usersRes.statusCode == 200 && logsRes.statusCode == 200) { final usersData = jsonDecode(utf8.decode(usersRes.bodyBytes)); @@ -72,10 +75,17 @@ class _TeacherAttendancePageState extends State { } } + String? dismissedAt; + if (dismissalRes.statusCode == 200) { + final dismissalData = jsonDecode(utf8.decode(dismissalRes.bodyBytes)); + dismissedAt = dismissalData['dismissedAt']; + } + setState(() { _roster = usersData['users'] ?? []; _logs = logsData['logs'] ?? []; _activeViolationsByStudentId = activeViolations; + _dismissedAt = dismissedAt; _isLoading = false; }); } else { @@ -86,6 +96,24 @@ class _TeacherAttendancePageState extends State { } } + /// 🏫 하교 처리: 전체 반출 허용 + 오늘 출석 표시 기준선을 지금 시각으로 옮긴다. + Future _dismissAll() async { + try { + final response = await http.post(Uri.parse('$baseUrl/api/dismiss')); + final result = jsonDecode(utf8.decode(response.bodyBytes)); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(result['message'] ?? '하교 처리되었습니다.')), + ); + _fetchAll(); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('❌ 하교 처리 실패: $e')), + ); + } + } + /// 🚨 선생님이 특정 학생에게 지금부터 N분간 반출을 허용한다. Future _allowRemoval(String studentId, int minutes) async { try { @@ -222,7 +250,7 @@ class _TeacherAttendancePageState extends State { ElevatedButton( onPressed: () { Navigator.pop(context); - _setPermissionWindow(720); // 12시간 (사실상 하교~다음날까지 허용) + _dismissAll(); }, child: const Text('하교 처리'), ), @@ -231,17 +259,19 @@ class _TeacherAttendancePageState extends State { ); } - /// 오늘 날짜 기준으로 학번별 "가장 최근 출석 기록"만 남긴 맵을 만든다. + /// "오늘 출석"의 기준선. 하교 처리를 했다면 그 시각 이후, 안 했다면 오늘 자정부터. Map> get _todaysCheckInsByStudentId { - final String todayPrefix = DateTime.now().toIso8601String().substring( - 0, - 10, - ); // "YYYY-MM-DD" + // "YYYY-MM-DD HH:MM:SS" 형태의 문자열끼리는 그대로 비교해도 시간 순서가 맞는다. + final String cutoff = + _dismissedAt ?? + '${DateTime.now().toIso8601String().substring(0, 10)} 00:00:00'; final Map> result = {}; for (final log in _logs) { final String time = log['time']?.toString() ?? ''; - if (!time.startsWith(todayPrefix)) continue; // 오늘 기록이 아니면 무시 + if (time.isEmpty || time.compareTo(cutoff) <= 0) { + continue; // 하교 처리 시각(또는 오늘 자정) 이전 기록이면 무시 + } final String studentId = log['student_id']?.toString() ?? ''; if (studentId.isEmpty) continue;