diff --git a/lib/nfc_poccket_checkin_screen.dart b/lib/nfc_poccket_checkin_screen.dart index 177a275..9e90c0e 100644 --- a/lib/nfc_poccket_checkin_screen.dart +++ b/lib/nfc_poccket_checkin_screen.dart @@ -34,6 +34,7 @@ class _NfcPocketCheckInScreenState extends State { String? _activePocketNumber; StreamSubscription?>? _violationSub; + StreamSubscription?>? _checkedOutSub; bool _isDialogOpen = false; // 출석/위반 팝업이 겹쳐서 뜨는 것을 막기 위한 플래그 bool get _watchServiceSupported => !kIsWeb && Platform.isAndroid; @@ -53,6 +54,16 @@ class _NfcPocketCheckInScreenState extends State { setState(() => _isWatching = false); _showViolationDialog(event?['pocketNumber']?.toString()); }); + // 🏫 하교 처리 등으로 반출이 허용된 상태에서 폰을 꺼내면, 위반이 아니라 정상 회수로 처리된다. + _checkedOutSub = FlutterBackgroundService().on('checked_out').listen((event) { + if (!mounted) return; + setState(() { + _isWatching = false; + _activePocketNumber = null; + _statusMessage = "✅ 폰을 회수했습니다. 수고하셨습니다!"; + }); + _showSnackBar("✅ 폰이 정상적으로 회수되었습니다.", Colors.green); + }); } } @@ -61,6 +72,7 @@ class _NfcPocketCheckInScreenState extends State { // 화면을 나갈 때 NFC 감지만 종료. 백그라운드 감시 서비스는 화면과 무관하게 계속 동작해야 하므로 건드리지 않는다. NfcManager.instance.stopSession(); _violationSub?.cancel(); + _checkedOutSub?.cancel(); super.dispose(); } diff --git a/lib/pocket_watch_service.dart b/lib/pocket_watch_service.dart index 50514be..679b074 100644 --- a/lib/pocket_watch_service.dart +++ b/lib/pocket_watch_service.dart @@ -112,6 +112,30 @@ void onPocketWatchServiceStart(ServiceInstance service) { } } + /// 하교 처리/반출 허용 시간대라면 조용히 감시만 종료하고, 아니면 위반으로 경고한다. + Future 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) { if (violated) return; @@ -129,7 +153,7 @@ void onPocketWatchServiceStart(ServiceInstance service) { if (luxValue > baselineLux! + _luxJumpThreshold) { violated = true; lightSub?.cancel(); - reportViolation(); + handlePhoneRemoved(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 7d101bc..09a8bba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 # 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. -version: 1.1.2+3 +version: 1.1.3+4 environment: sdk: ^3.12.2