lib/ 코드를 UI(lib/ui/)와 기능(lib/function/)으로 분리
- 화면마다 위젯/스타일만 담당하는 UI 파일과, 서버통신/상태/파생로직만 담당하는 ChangeNotifier 컨트롤러 파일로 1:1 분리 (lib/screens/ -> lib/ui/ + lib/function/) - UI는 ListenableBuilder로 컨트롤러를 구독해서 재렌더링, 버튼은 컨트롤러 메서드만 호출 - 다이얼로그/스낵바 등 위젯 코드는 전부 UI 파일에 남기고, 컨트롤러는 결과값(성공여부+메시지) 또는 콜백으로만 UI와 통신 (BuildContext/Widget 의존성 없음) - 디자인/레이아웃은 기존과 완전히 동일하게 유지 (순수 코드 재배치) - change_password_screen.dart, debug_pocket_main.dart(미사용 파일)는 깨지지 않게 import 경로만 갱신하고 리팩터링은 보류
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
// 📲 자습실 NFC 출석체크 화면 (UI 전용). 태깅 상태 화면/팝업/스낵바만 그린다.
|
||||
// NFC 세션, 서버 통신, 백그라운드 감시 연동은
|
||||
// lib/function/nfc_pocket_checkin_controller.dart가 담당한다.
|
||||
import 'package:flutter/material.dart';
|
||||
import '../function/nfc_pocket_checkin_controller.dart';
|
||||
|
||||
class NfcPocketCheckInScreen extends StatefulWidget {
|
||||
final String studentId; // 로그인된 학생 학번 (예: "2061")
|
||||
final String studentName; // 로그인된 학생 이름 (예: "시후")
|
||||
|
||||
const NfcPocketCheckInScreen({
|
||||
super.key,
|
||||
required this.studentId,
|
||||
required this.studentName,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NfcPocketCheckInScreen> createState() => _NfcPocketCheckInScreenState();
|
||||
}
|
||||
|
||||
class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
|
||||
late final NfcPocketCheckinController _controller;
|
||||
bool _isDialogOpen = false; // 출석/위반 팝업이 겹쳐서 뜨는 것을 막기 위한 플래그
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = NfcPocketCheckinController(
|
||||
studentId: widget.studentId,
|
||||
studentName: widget.studentName,
|
||||
onMessage: _handleMessage,
|
||||
onCheckInSuccess: _showSuccessDialog,
|
||||
onViolationDetected: _showViolationDialog,
|
||||
);
|
||||
_controller.init();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleMessage(String text, WatchMessageLevel level) {
|
||||
final Color color = switch (level) {
|
||||
WatchMessageLevel.info => Colors.blueGrey,
|
||||
WatchMessageLevel.success => Colors.green,
|
||||
WatchMessageLevel.warning => Colors.orange,
|
||||
WatchMessageLevel.error => Colors.red,
|
||||
};
|
||||
_showSnackBar(text, color);
|
||||
}
|
||||
|
||||
/// 이미 떠 있는 팝업(출석 완료/무단반출 감지)이 있으면 새 팝업을 띄우기 전에 먼저 닫는다.
|
||||
/// (태깅→반출→재태깅이 빠르게 반복되면 팝업이 여러 개 겹쳐 쌓이는 것을 방지)
|
||||
void _closeAnyOpenDialog() {
|
||||
if (_isDialogOpen && mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
_isDialogOpen = false;
|
||||
}
|
||||
|
||||
void _showViolationDialog(String? pocketNumber) {
|
||||
if (!mounted) return;
|
||||
_closeAnyOpenDialog();
|
||||
_isDialogOpen = true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.red[50],
|
||||
title: const Text(
|
||||
"🚨 무단 반출 감지",
|
||||
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: Text(
|
||||
"[${pocketNumber ?? _controller.activePocketNumber}] 주머니에서 휴대폰이 꺼내진 것으로 감지되었습니다.\n담당 선생님께 알림이 전송되었습니다.",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("확인"),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((_) => _isDialogOpen = false);
|
||||
}
|
||||
|
||||
/// 🎊 출석 성공 알림창
|
||||
void _showSuccessDialog(String pocketNumber) {
|
||||
if (!mounted) return;
|
||||
_closeAnyOpenDialog();
|
||||
_isDialogOpen = true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("🎉 자습실 출석 완료!"),
|
||||
content: Text(
|
||||
"${widget.studentName} 학생!\n[$pocketNumber] 주머니 출석이 확인되었습니다.\n\n폰을 주머니에 쏙 넣고 자습에 집중해 주세요! ✏️",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("확인"),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((_) => _isDialogOpen = false);
|
||||
}
|
||||
|
||||
void _showSnackBar(String text, Color color) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(text), backgroundColor: color));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _controller,
|
||||
builder: (context, _) {
|
||||
if (_controller.isWatching) {
|
||||
return _buildPocketWatchScreen();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"자습실 NFC 출석체크",
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: const Color.fromARGB(255, 48, 48, 52),
|
||||
),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// NFC 애니메이션 아이콘
|
||||
Icon(
|
||||
_controller.isProcessing ? Icons.sync : Icons.nfc,
|
||||
size: 100,
|
||||
color: _controller.isProcessing
|
||||
? Colors.orange
|
||||
: Colors.blueAccent,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// 상태 메시지 표시
|
||||
Text(
|
||||
_controller.statusMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _controller.isProcessing
|
||||
? Colors.orange
|
||||
: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
const Text(
|
||||
"자기 주머니 번호 숫자에 폰 뒷면을 '톡' 대면\n자동으로 출석 처리됩니다.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 💳 삼성페이 결제창 느낌의 전체화면 "주머니 제출 모드" 안내 UI
|
||||
/// (실제 감시 로직은 pocket_watch_service.dart의 백그라운드 서비스가 담당하므로,
|
||||
/// 이 화면은 상태를 보여주고 화면을 꺼도 된다는 것만 안내한다.)
|
||||
Widget _buildPocketWatchScreen() {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0B1120),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 160,
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.greenAccent.withValues(alpha: 0.15),
|
||||
border: Border.all(color: Colors.greenAccent, width: 3),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.shield_rounded,
|
||||
size: 72,
|
||||
color: Colors.greenAccent,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
_controller.activePocketNumber ?? "",
|
||||
style: const TextStyle(
|
||||
color: Colors.white54,
|
||||
fontSize: 14,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_controller.statusMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
"📴 화면을 꺼도 감시는 계속됩니다.\n알림바에서 상태를 확인할 수 있어요.\n(폰을 꺼내 다시 태깅하면 반출 처리됩니다)",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.white38, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user