From 4ff063c76dbe53e35a394df41f5e4342fc2419c0 Mon Sep 17 00:00:00 2001 From: sihoo Date: Tue, 4 Aug 2026 01:25:20 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=98=EA=B5=90=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=8B=9C=20=EC=B6=9C=EC=84=9D=20=ED=98=84=ED=99=A9=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=EB=8F=84=20=ED=95=A8=EA=BB=98=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 하교 버튼을 누르면 반출 허용뿐 아니라, 그 시각을 기준선으로 삼아 대시보드의 "출석 완료" 표시도 미제출로 초기화되도록 함. 실제 출석 기록(attendance 테이블)은 삭제하지 않고, 대시보드가 "오늘 출석"으로 인정하는 기준 시각만 옮기는 방식이라 데이터 손실 없이 표시만 리셋된다. Co-Authored-By: Claude Sonnet 5 --- lib/screens/teacher_attendance_page.dart | 44 ++++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) 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;