3 Commits
Author SHA1 Message Date
sihooandClaude Sonnet 5 77688170b7 하교 버튼 추가 + 무단반출 후 재태깅 시 재출석 처리 + 테스트용 감시종료 버튼 제거
- 선생님 대시보드에 "하교" 버튼 추가: 시간 입력 없이 바로 전체 학생 반출 허용
  (실수 클릭 방지를 위해 확인 다이얼로그 한 번 거침)
- 무단반출이 감지되면 클라이언트의 감시 상태를 해제해서, 다음 태깅이
  체크아웃이 아니라 새 출석으로 처리되도록 수정
- 주머니 감시 화면에 있던 "폰 회수하고 감시 종료 (테스트용)" 버튼 제거
  (정식 플로우는 재태깅으로만 체크아웃/재출석 처리)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 01:08:48 +09:00
sihooandClaude Sonnet 5 c17a28bad9 새로고침 버튼에 로딩 표시 추가
버튼을 눌러도 아무 반응이 없어 보인다는 피드백 반영 - 클릭 시 아이콘이
잠깐 스피너로 바뀌도록 해서 실제로 새로고침이 진행 중임을 알 수 있게 함.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 00:52:26 +09:00
sihooandClaude Sonnet 5 9adaba10cd 교사 대시보드에 새로고침 버튼 추가 + 웹 배포 캐시 무효화 설정
- TeacherAttendancePage 앱바에 수동 새로고침 버튼 추가
- Firebase Hosting에서 index.html/flutter_service_worker.js/version.json을
  no-cache로 설정해서, 재배포 후 브라우저가 예전 버전을 계속 들고 있는 문제 방지

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 00:42:15 +09:00
3 changed files with 62 additions and 15 deletions
+1 -1
View File
@@ -1 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"school-display-ff28f","appId":"1:137322214849:android:2595999513206bdeb16b40","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"school-display-ff28f","configurations":{"android":"1:137322214849:android:2595999513206bdeb16b40","web":"1:137322214849:web:0423e5c788d95761b16b40"}}}}},"hosting":{"public":"build/web","ignore":["firebase.json","**/.*"],"rewrites":[{"source":"**","destination":"/index.html"}]}}
{"flutter":{"platforms":{"android":{"default":{"projectId":"school-display-ff28f","appId":"1:137322214849:android:2595999513206bdeb16b40","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"school-display-ff28f","configurations":{"android":"1:137322214849:android:2595999513206bdeb16b40","web":"1:137322214849:web:0423e5c788d95761b16b40"}}}}},"hosting":{"public":"build/web","ignore":["firebase.json","**/.*"],"rewrites":[{"source":"**","destination":"/index.html"}],"headers":[{"source":"/index.html","headers":[{"key":"Cache-Control","value":"no-cache, no-store, must-revalidate"}]},{"source":"/flutter_service_worker.js","headers":[{"key":"Cache-Control","value":"no-cache, no-store, must-revalidate"}]},{"source":"/version.json","headers":[{"key":"Cache-Control","value":"no-cache, no-store, must-revalidate"}]}]}}
+6 -14
View File
@@ -43,10 +43,14 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
_startNfcSession();
if (_watchServiceSupported) {
// 화면이 열려있는 동안은 실시간으로 위반 알림을 받아 팝업을 띄운다.
// 무단 반출이 한 번 감지되면 "신뢰된 감시 세션"은 끝난 것으로 보고,
// 다음 태깅은 체크아웃이 아니라 새 출석(재출석)으로 처리되도록 감시 상태를 해제한다.
_violationSub = FlutterBackgroundService().on('violation_detected').listen((
event,
) {
if (mounted) _showViolationDialog(event?['pocketNumber']?.toString());
if (!mounted) return;
setState(() => _isWatching = false);
_showViolationDialog(event?['pocketNumber']?.toString());
});
}
}
@@ -354,22 +358,10 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
),
const SizedBox(height: 8),
const Text(
"📴 화면을 꺼도 감시는 계속됩니다.\n알림바에서 상태를 확인할 수 있어요.",
"📴 화면을 꺼도 감시는 계속됩니다.\n알림바에서 상태를 확인할 수 있어요.\n(폰을 꺼내 다시 태깅하면 반출 처리됩니다)",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white38, fontSize: 13),
),
const SizedBox(height: 40),
OutlinedButton.icon(
onPressed: _checkOut,
icon: const Icon(Icons.logout_rounded, color: Colors.white70),
label: const Text(
"폰 회수하고 감시 종료 (테스트용)",
style: TextStyle(color: Colors.white70),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.white30),
),
),
],
),
),
+55
View File
@@ -26,6 +26,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
Map<String, dynamic> _activeViolationsByStudentId = {}; // 무단반출 중인 학생 (/api/violations/active)
Timer? _timer;
bool _isLoading = true;
bool _isRefreshing = false; // 새로고침 버튼 클릭 시 잠깐 도는 표시용
String _filterType = "ALL"; // "ALL", "CHECKED_IN", "ABSENT"
@override
@@ -41,6 +42,12 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
super.dispose();
}
Future<void> _manualRefresh() async {
setState(() => _isRefreshing = true);
await _fetchAll();
if (mounted) setState(() => _isRefreshing = false);
}
Future<void> _fetchAll() async {
try {
final results = await Future.wait([
@@ -198,6 +205,32 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
);
}
/// 🏫 하교 처리: 시간 입력 없이 바로 전체 학생의 반출을 (사실상 무기한) 허용한다.
void _showDismissalConfirmDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('🏫 하교 처리'),
content: const Text(
'지금부터 모든 학생의 반출이 자동으로 허용되며, 더 이상 무단반출 경고가 뜨지 않습니다.\n하교 처리하시겠습니까?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('취소'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_setPermissionWindow(720); // 12시간 (사실상 하교~다음날까지 허용)
},
child: const Text('하교 처리'),
),
],
),
);
}
/// 오늘 날짜 기준으로 학번별 "가장 최근 출석 기록"만 남긴 맵을 만든다.
Map<String, Map<String, String>> get _todaysCheckInsByStudentId {
final String todayPrefix = DateTime.now().toIso8601String().substring(
@@ -274,6 +307,20 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
foregroundColor: Colors.white,
elevation: 0,
actions: [
IconButton(
onPressed: (_isLoading || _isRefreshing) ? null : _manualRefresh,
icon: _isRefreshing
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.refresh_rounded),
tooltip: '새로고침',
),
TextButton.icon(
onPressed: _showPermissionWindowDialog,
icon: const Icon(Icons.timer_outlined, color: Colors.white),
@@ -282,6 +329,14 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
style: TextStyle(color: Colors.white),
),
),
TextButton.icon(
onPressed: _showDismissalConfirmDialog,
icon: const Icon(Icons.school_rounded, color: Colors.white),
label: const Text(
'하교',
style: TextStyle(color: Colors.white),
),
),
const SizedBox(width: 8),
],
),