하교 처리 시 출석 현황 표시도 함께 초기화

하교 버튼을 누르면 반출 허용뿐 아니라, 그 시각을 기준선으로 삼아 대시보드의
"출석 완료" 표시도 미제출로 초기화되도록 함. 실제 출석 기록(attendance 테이블)은
삭제하지 않고, 대시보드가 "오늘 출석"으로 인정하는 기준 시각만 옮기는 방식이라
데이터 손실 없이 표시만 리셋된다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 01:25:20 +09:00
co-authored by Claude Sonnet 5
parent 77688170b7
commit 4ff063c76d
+37 -7
View File
@@ -24,6 +24,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
List<dynamic> _roster = []; // 전체 학생 명단 (/api/users)
List<dynamic> _logs = []; // 출석 로그 (/api/logs)
Map<String, dynamic> _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<TeacherAttendancePage> {
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<TeacherAttendancePage> {
}
}
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<TeacherAttendancePage> {
}
}
/// 🏫 하교 처리: 전체 반출 허용 + 오늘 출석 표시 기준선을 지금 시각으로 옮긴다.
Future<void> _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<void> _allowRemoval(String studentId, int minutes) async {
try {
@@ -222,7 +250,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_setPermissionWindow(720); // 12시간 (사실상 하교~다음날까지 허용)
_dismissAll();
},
child: const Text('하교 처리'),
),
@@ -231,17 +259,19 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
);
}
/// 오늘 날짜 기준으로 학번별 "가장 최근 출석 기록"만 남긴 맵을 만든다.
/// "오늘 출석"의 기준선. 하교 처리를 했다면 그 시각 이후, 안 했다면 오늘 자정부터.
Map<String, Map<String, String>> 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<String, Map<String, String>> 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;