Compare commits
13
Commits
49ab432fe9
...
v1.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e12b279e2 | ||
|
|
d864927957 | ||
|
|
61aedac312 | ||
|
|
5ab1c4c37c | ||
|
|
3a62ebacd8 | ||
|
|
26fb4c32cf | ||
|
|
4137a6815c | ||
|
|
4ff063c76d | ||
|
|
77688170b7 | ||
|
|
c17a28bad9 | ||
|
|
9adaba10cd | ||
|
|
77ef666e16 | ||
|
|
c8ed8f2fd2 |
+1
-1
@@ -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"}}}}}}
|
||||
{"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"}]}]}}
|
||||
@@ -34,6 +34,8 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
||||
String? _activePocketNumber;
|
||||
|
||||
StreamSubscription<Map<String, dynamic>?>? _violationSub;
|
||||
StreamSubscription<Map<String, dynamic>?>? _checkedOutSub;
|
||||
bool _isDialogOpen = false; // 출석/위반 팝업이 겹쳐서 뜨는 것을 막기 위한 플래그
|
||||
|
||||
bool get _watchServiceSupported => !kIsWeb && Platform.isAndroid;
|
||||
|
||||
@@ -43,10 +45,24 @@ 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());
|
||||
});
|
||||
// 🏫 하교 처리 등으로 반출이 허용된 상태에서 폰을 꺼내면, 위반이 아니라 정상 회수로 처리된다.
|
||||
_checkedOutSub = FlutterBackgroundService().on('checked_out').listen((event) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isWatching = false;
|
||||
_activePocketNumber = null;
|
||||
_statusMessage = "✅ 폰을 회수했습니다. 수고하셨습니다!";
|
||||
});
|
||||
_showSnackBar("✅ 폰이 정상적으로 회수되었습니다.", Colors.green);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -56,6 +72,7 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
||||
// 화면을 나갈 때 NFC 감지만 종료. 백그라운드 감시 서비스는 화면과 무관하게 계속 동작해야 하므로 건드리지 않는다.
|
||||
NfcManager.instance.stopSession();
|
||||
_violationSub?.cancel();
|
||||
_checkedOutSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -207,7 +224,18 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
||||
_showSnackBar("✅ 감시가 종료되었습니다. 수고하셨습니다!", Colors.green);
|
||||
}
|
||||
|
||||
/// 이미 떠 있는 팝업(출석 완료/무단반출 감지)이 있으면 새 팝업을 띄우기 전에 먼저 닫는다.
|
||||
/// (태깅→반출→재태깅이 빠르게 반복되면 팝업이 여러 개 겹쳐 쌓이는 것을 방지)
|
||||
void _closeAnyOpenDialog() {
|
||||
if (_isDialogOpen && mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
_isDialogOpen = false;
|
||||
}
|
||||
|
||||
void _showViolationDialog(String? pocketNumber) {
|
||||
_closeAnyOpenDialog();
|
||||
_isDialogOpen = true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -226,11 +254,13 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
).then((_) => _isDialogOpen = false);
|
||||
}
|
||||
|
||||
/// 🎊 출석 성공 알림창
|
||||
void _showSuccessDialog(String pocketNumber) {
|
||||
_closeAnyOpenDialog();
|
||||
_isDialogOpen = true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -245,7 +275,7 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
).then((_) => _isDialogOpen = false);
|
||||
}
|
||||
|
||||
void _showSnackBar(String text, Color color) {
|
||||
@@ -354,22 +384,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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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) {
|
||||
if (violated) return;
|
||||
|
||||
@@ -129,7 +153,7 @@ void onPocketWatchServiceStart(ServiceInstance service) {
|
||||
if (luxValue > baselineLux! + _luxJumpThreshold) {
|
||||
violated = true;
|
||||
lightSub?.cancel();
|
||||
reportViolation();
|
||||
handlePhoneRemoved();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -308,7 +308,10 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
if (role == 'teacher') {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const TeacherDashboard()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
TeacherDashboard(teacherId: studentId, teacherName: name),
|
||||
),
|
||||
);
|
||||
} else if (role == 'admin') {
|
||||
Navigator.pushReplacement(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -135,16 +135,20 @@ class _TeacherDashboardState extends State<TeacherDashboard> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 📊 [그리드 레이아웃 메뉴] 시후의 카드 컴포넌트 스타일 적용
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 0.95,
|
||||
children: [
|
||||
// 🖥️ 데스크톱 브라우저에서 카드가 지나치게 커지지 않도록 최대 너비를 제한한다.
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 700),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 0.95,
|
||||
children: [
|
||||
_buildModernCard(
|
||||
icon: Icons.assignment_turned_in_rounded,
|
||||
title: '실시간 출석 확인',
|
||||
@@ -170,7 +174,9 @@ class _TeacherDashboardState extends State<TeacherDashboard> {
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
+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
|
||||
# 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.0.0+1
|
||||
version: 1.1.4+5
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
Reference in New Issue
Block a user