Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ab1c4c37c | ||
|
|
3a62ebacd8 | ||
|
|
26fb4c32cf |
@@ -34,6 +34,8 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
|||||||
String? _activePocketNumber;
|
String? _activePocketNumber;
|
||||||
|
|
||||||
StreamSubscription<Map<String, dynamic>?>? _violationSub;
|
StreamSubscription<Map<String, dynamic>?>? _violationSub;
|
||||||
|
StreamSubscription<Map<String, dynamic>?>? _checkedOutSub;
|
||||||
|
bool _isDialogOpen = false; // 출석/위반 팝업이 겹쳐서 뜨는 것을 막기 위한 플래그
|
||||||
|
|
||||||
bool get _watchServiceSupported => !kIsWeb && Platform.isAndroid;
|
bool get _watchServiceSupported => !kIsWeb && Platform.isAndroid;
|
||||||
|
|
||||||
@@ -52,6 +54,16 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
|||||||
setState(() => _isWatching = false);
|
setState(() => _isWatching = false);
|
||||||
_showViolationDialog(event?['pocketNumber']?.toString());
|
_showViolationDialog(event?['pocketNumber']?.toString());
|
||||||
});
|
});
|
||||||
|
// 🏫 하교 처리 등으로 반출이 허용된 상태에서 폰을 꺼내면, 위반이 아니라 정상 회수로 처리된다.
|
||||||
|
_checkedOutSub = FlutterBackgroundService().on('checked_out').listen((event) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isWatching = false;
|
||||||
|
_activePocketNumber = null;
|
||||||
|
_statusMessage = "✅ 폰을 회수했습니다. 수고하셨습니다!";
|
||||||
|
});
|
||||||
|
_showSnackBar("✅ 폰이 정상적으로 회수되었습니다.", Colors.green);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +72,7 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
|||||||
// 화면을 나갈 때 NFC 감지만 종료. 백그라운드 감시 서비스는 화면과 무관하게 계속 동작해야 하므로 건드리지 않는다.
|
// 화면을 나갈 때 NFC 감지만 종료. 백그라운드 감시 서비스는 화면과 무관하게 계속 동작해야 하므로 건드리지 않는다.
|
||||||
NfcManager.instance.stopSession();
|
NfcManager.instance.stopSession();
|
||||||
_violationSub?.cancel();
|
_violationSub?.cancel();
|
||||||
|
_checkedOutSub?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +224,18 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
|||||||
_showSnackBar("✅ 감시가 종료되었습니다. 수고하셨습니다!", Colors.green);
|
_showSnackBar("✅ 감시가 종료되었습니다. 수고하셨습니다!", Colors.green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 이미 떠 있는 팝업(출석 완료/무단반출 감지)이 있으면 새 팝업을 띄우기 전에 먼저 닫는다.
|
||||||
|
/// (태깅→반출→재태깅이 빠르게 반복되면 팝업이 여러 개 겹쳐 쌓이는 것을 방지)
|
||||||
|
void _closeAnyOpenDialog() {
|
||||||
|
if (_isDialogOpen && mounted) {
|
||||||
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
|
}
|
||||||
|
_isDialogOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
void _showViolationDialog(String? pocketNumber) {
|
void _showViolationDialog(String? pocketNumber) {
|
||||||
|
_closeAnyOpenDialog();
|
||||||
|
_isDialogOpen = true;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -230,11 +254,13 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
).then((_) => _isDialogOpen = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🎊 출석 성공 알림창
|
/// 🎊 출석 성공 알림창
|
||||||
void _showSuccessDialog(String pocketNumber) {
|
void _showSuccessDialog(String pocketNumber) {
|
||||||
|
_closeAnyOpenDialog();
|
||||||
|
_isDialogOpen = true;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -249,7 +275,7 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
).then((_) => _isDialogOpen = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showSnackBar(String text, Color color) {
|
void _showSnackBar(String text, Color color) {
|
||||||
|
|||||||
@@ -112,6 +112,30 @@ void onPocketWatchServiceStart(ServiceInstance service) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 하교 처리/반출 허용 시간대라면 조용히 감시만 종료하고, 아니면 위반으로 경고한다.
|
||||||
|
Future<void> handlePhoneRemoved() async {
|
||||||
|
bool isPermitted = false;
|
||||||
|
try {
|
||||||
|
final res = await http.get(
|
||||||
|
Uri.parse("$baseUrl/api/permissions/check?studentId=$studentId"),
|
||||||
|
);
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
final data = jsonDecode(utf8.decode(res.bodyBytes));
|
||||||
|
isPermitted = data['permitted'] == true;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
isPermitted = false; // 네트워크 오류 시엔 안전하게 위반으로 처리
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPermitted) {
|
||||||
|
updateNotification("✅ 폰 회수 완료", "[$pocketNumber] 주머니에서 정상적으로 회수되었습니다.");
|
||||||
|
service.invoke('checked_out', {"pocketNumber": pocketNumber});
|
||||||
|
service.stopSelf();
|
||||||
|
} else {
|
||||||
|
reportViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onLightReading(int luxValue) {
|
void onLightReading(int luxValue) {
|
||||||
if (violated) return;
|
if (violated) return;
|
||||||
|
|
||||||
@@ -129,7 +153,7 @@ void onPocketWatchServiceStart(ServiceInstance service) {
|
|||||||
if (luxValue > baselineLux! + _luxJumpThreshold) {
|
if (luxValue > baselineLux! + _luxJumpThreshold) {
|
||||||
violated = true;
|
violated = true;
|
||||||
lightSub?.cancel();
|
lightSub?.cancel();
|
||||||
reportViolation();
|
handlePhoneRemoved();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,54 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
_setAttendanceTime(formatted);
|
_setAttendanceTime(formatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🧪 테스트용: 하교(12시간)/반출 허용 시간 설정 등으로 켜져 있는 허용 시간대를 즉시 해제한다.
|
||||||
|
Future<void> _resetTestPermissions() async {
|
||||||
|
try {
|
||||||
|
final response = await http.post(Uri.parse('$baseUrl/api/debug/reset-permissions'));
|
||||||
|
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')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTestResetConfirmDialog() {
|
||||||
|
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);
|
||||||
|
_resetTestPermissions();
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[700],
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('초기화'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showAllowDialog(String studentId, String studentName) {
|
void _showAllowDialog(String studentId, String studentName) {
|
||||||
final controller = TextEditingController(text: "5");
|
final controller = TextEditingController(text: "5");
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -445,6 +493,11 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _showTestResetConfirmDialog,
|
||||||
|
icon: const Icon(Icons.bug_report_outlined, color: Colors.white70),
|
||||||
|
tooltip: '🧪 테스트용: 허용시간 초기화',
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.1+2
|
version: 1.1.3+4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.12.2
|
sdk: ^3.12.2
|
||||||
|
|||||||
Reference in New Issue
Block a user