From d864927957d57ab7c5d45f1e285370c5df0284d0 Mon Sep 17 00:00:00 2001 From: sihoo Date: Tue, 4 Aug 2026 14:55:22 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EB=B0=98=EC=B6=9C=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9=20=EC=A4=91=EC=9D=BC=20=EB=95=8C=20=EB=8C=80?= =?UTF-8?q?=EC=8B=9C=EB=B3=B4=EB=93=9C=EC=97=90=20=EB=B0=B0=EB=84=88=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/screens/teacher_attendance_page.dart | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/lib/screens/teacher_attendance_page.dart b/lib/screens/teacher_attendance_page.dart index c065f69..359d8e1 100644 --- a/lib/screens/teacher_attendance_page.dart +++ b/lib/screens/teacher_attendance_page.dart @@ -26,6 +26,7 @@ class _TeacherAttendancePageState extends State { Map _activeViolationsByStudentId = {}; // 무단반출 중인 학생 (/api/violations/active) String? _dismissedAt; // 오늘 가장 최근 하교 처리 시각 (/api/dismissal/latest). 이 시각 이후 기록만 "오늘 출석"으로 표시. String? _attendanceTime; // 선생님이 지정한 자습실 출석시간 "HH:MM" (/api/settings/attendance-time). null이면 미설정. + String? _globalPermissionUntil; // 하교(12시간)/반출허용시간설정으로 전체 반출이 허용된 경우 그 만료 시각 (/api/permissions/status). null이면 비활성. Timer? _timer; bool _isLoading = true; bool _isRefreshing = false; // 새로고침 버튼 클릭 시 잠깐 도는 표시용 @@ -58,6 +59,7 @@ class _TeacherAttendancePageState extends State { http.get(Uri.parse('$baseUrl/api/violations/active')), http.get(Uri.parse('$baseUrl/api/dismissal/latest')), http.get(Uri.parse('$baseUrl/api/settings/attendance-time')), + http.get(Uri.parse('$baseUrl/api/permissions/status')), ]); final usersRes = results[0]; @@ -65,6 +67,7 @@ class _TeacherAttendancePageState extends State { final violationsRes = results[2]; final dismissalRes = results[3]; final attendanceTimeRes = results[4]; + final permissionStatusRes = results[5]; if (usersRes.statusCode == 200 && logsRes.statusCode == 200) { final usersData = jsonDecode(utf8.decode(usersRes.bodyBytes)); @@ -91,12 +94,22 @@ class _TeacherAttendancePageState extends State { attendanceTime = attendanceTimeData['attendanceTime']; } + String? globalPermissionUntil; + if (permissionStatusRes.statusCode == 200) { + final permissionStatusData = + jsonDecode(utf8.decode(permissionStatusRes.bodyBytes)); + if (permissionStatusData['active'] == true) { + globalPermissionUntil = permissionStatusData['permittedUntil']; + } + } + setState(() { _roster = usersData['users'] ?? []; _logs = logsData['logs'] ?? []; _activeViolationsByStudentId = activeViolations; _dismissedAt = dismissedAt; _attendanceTime = attendanceTime; + _globalPermissionUntil = globalPermissionUntil; _isLoading = false; }); } else { @@ -534,6 +547,7 @@ class _TeacherAttendancePageState extends State { return Column( children: [ _buildSummaryCards(total, checkedIn, absent), + _buildPermissionBanner(), _buildFilterChips(), Expanded( child: _filteredStudents.isEmpty @@ -736,6 +750,7 @@ class _TeacherAttendancePageState extends State { ], ), const SizedBox(height: 20), + _buildPermissionBanner(), _buildFilterChips(), const SizedBox(height: 12), Expanded( @@ -988,6 +1003,39 @@ class _TeacherAttendancePageState extends State { } /// 🔘 필터 칩버튼 (전체 / 출석자 / 미출석자) + /// 🔓 하교(12시간)/반출 허용 시간 설정으로 지금 전체 반출이 허용 중이면 눈에 띄게 배너로 알려준다. + /// (허용 중일 땐 무단반출을 감지해도 대시보드에 뜨지 않기 때문에, 왜 안 뜨는지 헷갈리지 않게 하기 위함) + Widget _buildPermissionBanner() { + if (_globalPermissionUntil == null) return const SizedBox.shrink(); + + return Container( + width: double.infinity, + margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.amber[100], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.amber[400]!), + ), + child: Row( + children: [ + Icon(Icons.lock_open_rounded, color: Colors.amber[800], size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + '🔓 지금 전체 반출 허용 중입니다 ($_globalPermissionUntil 까지) — 이 시간 동안은 무단반출 경고가 뜨지 않아요.', + style: TextStyle( + color: Colors.amber[900], + fontWeight: FontWeight.bold, + fontSize: 12.5, + ), + ), + ), + ], + ), + ); + } + Widget _buildFilterChips() { return Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),