UI 문구 이모티콘 제거 + 계정 강제 삭제 아이콘 검정으로 통일

화면 타이틀, 스낵바/다이얼로그 메시지, 백그라운드 알림 문구 등
사용자에게 노출되는 텍스트에서 이모티콘을 전부 제거 (코드 주석은
대상 아님). 아이콘 위젯은 그대로 유지.

main_dashboard.dart의 "계정 강제 삭제" 타일 아이콘 색상도 빨간색
대신 다른 타일과 동일한 검정(AppPalette.ink)으로 통일.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 22:31:03 +09:00
co-authored by Claude Sonnet 5
parent dce5f1ad1b
commit 9089b9032e
22 changed files with 157 additions and 160 deletions
+3 -3
View File
@@ -17,12 +17,12 @@ class AdminController extends ChangeNotifier {
try { try {
final response = await http.get(url); final response = await http.get(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return '💥 DB가 깔끔하게 초기화되었습니다! (출석 번호 1번부터 시작)'; return 'DB가 깔끔하게 초기화되었습니다! (출석 번호 1번부터 시작)';
} else { } else {
return '❌ 초기화 실패: 서버 권한 오류 (${response.statusCode})'; return '초기화 실패: 서버 권한 오류 (${response.statusCode})';
} }
} catch (e) { } catch (e) {
return '❌ 네트워크 에러: 서버와 연결할 수 없습니다.'; return '네트워크 에러: 서버와 연결할 수 없습니다.';
} finally { } finally {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
+3 -3
View File
@@ -34,12 +34,12 @@ class DeviceCheckoutController extends ChangeNotifier {
); );
final result = jsonDecode(utf8.decode(response.bodyBytes)); final result = jsonDecode(utf8.decode(response.bodyBytes));
if (response.statusCode == 200 && result['status'] == 'success') { if (response.statusCode == 200 && result['status'] == 'success') {
return (true, '✅ ${result['message'] ?? '요청이 접수되었습니다.'}'); return (true, '${result['message'] ?? '요청이 접수되었습니다.'}');
} else { } else {
return (false, '❌ ${result['message'] ?? '요청 실패'}'); return (false, '${result['message'] ?? '요청 실패'}');
} }
} catch (e) { } catch (e) {
return (false, '🚨 네트워크 에러: $e'); return (false, '네트워크 에러: $e');
} finally { } finally {
_isSubmitting = false; _isSubmitting = false;
notifyListeners(); notifyListeners();
@@ -87,11 +87,11 @@ class DeviceCheckoutLedgerController extends ChangeNotifier {
final result = jsonDecode(utf8.decode(response.bodyBytes)); final result = jsonDecode(utf8.decode(response.bodyBytes));
if (response.statusCode == 200 && result['status'] == 'success') { if (response.statusCode == 200 && result['status'] == 'success') {
await fetchAll(); await fetchAll();
return (true, '✅ ${result['message'] ?? '처리되었습니다.'}'); return (true, '${result['message'] ?? '처리되었습니다.'}');
} }
return (false, '❌ ${result['message'] ?? '처리 실패'}'); return (false, '${result['message'] ?? '처리 실패'}');
} catch (e) { } catch (e) {
return (false, '🚨 네트워크 에러: $e'); return (false, '네트워크 에러: $e');
} finally { } finally {
_isWorking = false; _isWorking = false;
_safeNotify(); _safeNotify();
+6 -11
View File
@@ -17,10 +17,8 @@ class LoginResult {
final bool isDeviceMatched; final bool isDeviceMatched;
final String? errorMessage; final String? errorMessage;
const LoginResult.masterSuccess({ const LoginResult.masterSuccess({required this.studentId, required this.name})
required this.studentId, : outcome = LoginOutcome.masterSuccess,
required this.name,
}) : outcome = LoginOutcome.masterSuccess,
role = null, role = null,
isDeviceMatched = true, isDeviceMatched = true,
errorMessage = null; errorMessage = null;
@@ -55,16 +53,13 @@ class LoginController extends ChangeNotifier {
Future<LoginResult> login(String id, String password) async { Future<LoginResult> login(String id, String password) async {
if (id.isEmpty || password.isEmpty) { if (id.isEmpty || password.isEmpty) {
return const LoginResult.error('⚠️ 학번과 비밀번호를 모두 입력해 주세요.'); return const LoginResult.error('학번과 비밀번호를 모두 입력해 주세요.');
} }
// 🔥 [개발자 마스터 계정] // 🔥 [개발자 마스터 계정]
if (id == "2061") { if (id == "2061") {
if (password == "happy9642!") { if (password == "happy9642!") {
return const LoginResult.masterSuccess( return const LoginResult.masterSuccess(studentId: "2061", name: "훗춧가룻");
studentId: "2061",
name: "훗춧가룻",
);
} else { } else {
return const LoginResult.error('개발자 계정의 마스터 비밀번호가 올바르지 않습니다.'); return const LoginResult.error('개발자 계정의 마스터 비밀번호가 올바르지 않습니다.');
} }
@@ -200,9 +195,9 @@ class LoginController extends ChangeNotifier {
final resData = jsonDecode(utf8.decode(response.bodyBytes)); final resData = jsonDecode(utf8.decode(response.bodyBytes));
if (response.statusCode == 200 && resData['status'] == 'success') { if (response.statusCode == 200 && resData['status'] == 'success') {
return (true, '✅ 비밀번호가 변경되었습니다!'); return (true, '비밀번호가 변경되었습니다!');
} else { } else {
return (false, '❌ 실패: ${resData['message']}'); return (false, '실패: ${resData['message']}');
} }
} catch (e) { } catch (e) {
return (false, '서버 에러 발생: $e'); return (false, '서버 에러 발생: $e');
+25 -21
View File
@@ -17,8 +17,12 @@ import '../config.dart' show baseUrl;
// 🛡️ iOS 근접센서 감시용 네이티브 채널 (ios/Runner/ProximityMonitorPlugin.swift 참고). // 🛡️ iOS 근접센서 감시용 네이티브 채널 (ios/Runner/ProximityMonitorPlugin.swift 참고).
// 조도 센서 API가 없는 iOS에서, 통화 중 화면이 꺼지는 것과 같은 원리인 // 조도 센서 API가 없는 iOS에서, 통화 중 화면이 꺼지는 것과 같은 원리인
// 근접센서로 "주머니 안/밖" 상태 변화를 감지해 대체한다. // 근접센서로 "주머니 안/밖" 상태 변화를 감지해 대체한다.
const MethodChannel _proximityMethodChannel = MethodChannel('proximity_monitor'); const MethodChannel _proximityMethodChannel = MethodChannel(
const EventChannel _proximityEventChannel = EventChannel('proximity_monitor/events'); 'proximity_monitor',
);
const EventChannel _proximityEventChannel = EventChannel(
'proximity_monitor/events',
);
enum WatchMessageLevel { info, success, warning, error } enum WatchMessageLevel { info, success, warning, error }
@@ -78,9 +82,9 @@ class NfcPocketCheckinController extends ChangeNotifier {
// 화면이 열려있는 동안은 실시간으로 위반 알림을 받아 팝업을 띄운다. // 화면이 열려있는 동안은 실시간으로 위반 알림을 받아 팝업을 띄운다.
// 무단 반출이 한 번 감지되면 "신뢰된 감시 세션"은 끝난 것으로 보고, // 무단 반출이 한 번 감지되면 "신뢰된 감시 세션"은 끝난 것으로 보고,
// 다음 태깅은 체크아웃이 아니라 새 출석(재출석)으로 처리되도록 감시 상태를 해제한다. // 다음 태깅은 체크아웃이 아니라 새 출석(재출석)으로 처리되도록 감시 상태를 해제한다.
_violationSub = FlutterBackgroundService().on('violation_detected').listen(( _violationSub = FlutterBackgroundService()
event, .on('violation_detected')
) { .listen((event) {
_isWatching = false; _isWatching = false;
notifyListeners(); notifyListeners();
onViolationDetected(event?['pocketNumber']?.toString()); onViolationDetected(event?['pocketNumber']?.toString());
@@ -91,9 +95,9 @@ class NfcPocketCheckinController extends ChangeNotifier {
) { ) {
_isWatching = false; _isWatching = false;
_activePocketNumber = null; _activePocketNumber = null;
_statusMessage = "✅ 폰을 회수했습니다. 수고하셨습니다!"; _statusMessage = "폰을 회수했습니다. 수고하셨습니다!";
notifyListeners(); notifyListeners();
onMessage("✅ 폰이 정상적으로 회수되었습니다.", WatchMessageLevel.success); onMessage("폰이 정상적으로 회수되었습니다.", WatchMessageLevel.success);
}); });
} }
} }
@@ -117,7 +121,7 @@ class NfcPocketCheckinController extends ChangeNotifier {
void _startNfcSession() async { void _startNfcSession() async {
bool isAvailable = await NfcManager.instance.isAvailable(); bool isAvailable = await NfcManager.instance.isAvailable();
if (!isAvailable) { if (!isAvailable) {
_statusMessage = "❌ 이 스마트폰은 NFC 기능이 꺼져있거나 지원되지 않습니다."; _statusMessage = "이 스마트폰은 NFC 기능이 꺼져있거나 지원되지 않습니다.";
notifyListeners(); notifyListeners();
return; return;
} }
@@ -134,7 +138,7 @@ class NfcPocketCheckinController extends ChangeNotifier {
} }
_isProcessing = true; _isProcessing = true;
_statusMessage = "⏳ 태그 인식 완료! 서버에 출석 전송 중..."; _statusMessage = "태그 인식 완료! 서버에 출석 전송 중...";
notifyListeners(); notifyListeners();
try { try {
@@ -163,7 +167,7 @@ class NfcPocketCheckinController extends ChangeNotifier {
tagUid: _getTagUid(tag), tagUid: _getTagUid(tag),
); );
} catch (e) { } catch (e) {
onMessage("❌ 태그 읽기 실패: $e", WatchMessageLevel.error); onMessage("태그 읽기 실패: $e", WatchMessageLevel.error);
} finally { } finally {
// 3초 후 연타 방지 해제 (쿨다운) // 3초 후 연타 방지 해제 (쿨다운)
await Future.delayed(const Duration(seconds: 3)); await Future.delayed(const Duration(seconds: 3));
@@ -216,16 +220,16 @@ class NfcPocketCheckinController extends ChangeNotifier {
onMessage("ℹ️ 이 기기는 백그라운드 감시를 지원하지 않습니다.", WatchMessageLevel.info); onMessage("ℹ️ 이 기기는 백그라운드 감시를 지원하지 않습니다.", WatchMessageLevel.info);
} }
} else { } else {
onMessage("⚠️ 출석 실패: ${result['message']}", WatchMessageLevel.warning); onMessage("출석 실패: ${result['message']}", WatchMessageLevel.warning);
} }
} else { } else {
onMessage( onMessage(
"🚨 서버 에러 (코드: ${response.statusCode})", "서버 에러 (코드: ${response.statusCode})",
WatchMessageLevel.error, WatchMessageLevel.error,
); );
} }
} catch (e) { } catch (e) {
onMessage("❌ 서버 연결 실패: $e", WatchMessageLevel.error); onMessage("서버 연결 실패: $e", WatchMessageLevel.error);
} }
} }
@@ -255,7 +259,7 @@ class NfcPocketCheckinController extends ChangeNotifier {
_isWatching = true; _isWatching = true;
_activePocketNumber = pocketNumber; _activePocketNumber = pocketNumber;
_statusMessage = "🛡️ 백그라운드에서 감시 중입니다. 화면을 꺼도 계속 감시돼요."; _statusMessage = "백그라운드에서 감시 중입니다. 화면을 꺼도 계속 감시돼요.";
notifyListeners(); notifyListeners();
} }
@@ -268,9 +272,9 @@ class NfcPocketCheckinController extends ChangeNotifier {
} }
_isWatching = false; _isWatching = false;
_activePocketNumber = null; _activePocketNumber = null;
_statusMessage = "✅ 폰을 회수했습니다. 감시가 종료되었습니다."; _statusMessage = "폰을 회수했습니다. 감시가 종료되었습니다.";
notifyListeners(); notifyListeners();
onMessage("✅ 감시가 종료되었습니다. 수고하셨습니다!", WatchMessageLevel.success); onMessage("감시가 종료되었습니다. 수고하셨습니다!", WatchMessageLevel.success);
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -291,13 +295,13 @@ class NfcPocketCheckinController extends ChangeNotifier {
_activePocketNumber = pocketNumber; _activePocketNumber = pocketNumber;
_isWatching = true; _isWatching = true;
_statusMessage = "📥 주머니에 넣는 중..."; _statusMessage = "주머니에 넣는 중...";
notifyListeners(); notifyListeners();
try { try {
await _proximityMethodChannel.invokeMethod('start'); await _proximityMethodChannel.invokeMethod('start');
} catch (e) { } catch (e) {
onMessage("❌ 근접센서 감시 시작 실패: $e", WatchMessageLevel.error); onMessage("근접센서 감시 시작 실패: $e", WatchMessageLevel.error);
_isWatching = false; _isWatching = false;
_isIosProximityWatch = false; _isIosProximityWatch = false;
notifyListeners(); notifyListeners();
@@ -307,7 +311,7 @@ class NfcPocketCheckinController extends ChangeNotifier {
// 학생이 폰을 주머니에 넣을 시간을 준 뒤, 그 시점 상태를 기준값으로 삼는다. // 학생이 폰을 주머니에 넣을 시간을 준 뒤, 그 시점 상태를 기준값으로 삼는다.
await Future.delayed(const Duration(seconds: 4)); await Future.delayed(const Duration(seconds: 4));
_statusMessage = "🛡️ 감시 중입니다. 화면을 잠그지 말고 주머니에 넣어주세요."; _statusMessage = "감시 중입니다. 화면을 잠그지 말고 주머니에 넣어주세요.";
notifyListeners(); notifyListeners();
_proximitySub = _proximityEventChannel.receiveBroadcastStream().listen(( _proximitySub = _proximityEventChannel.receiveBroadcastStream().listen((
@@ -359,9 +363,9 @@ class NfcPocketCheckinController extends ChangeNotifier {
if (isPermitted) { if (isPermitted) {
_activePocketNumber = null; _activePocketNumber = null;
_statusMessage = "✅ 폰을 회수했습니다. 수고하셨습니다!"; _statusMessage = "폰을 회수했습니다. 수고하셨습니다!";
notifyListeners(); notifyListeners();
onMessage("✅ 폰이 정상적으로 회수되었습니다.", WatchMessageLevel.success); onMessage("폰이 정상적으로 회수되었습니다.", WatchMessageLevel.success);
return; return;
} }
+9 -9
View File
@@ -37,19 +37,19 @@ class NfcTagWriterController extends ChangeNotifier {
Future<void> startWriteSession(String number) async { Future<void> startWriteSession(String number) async {
if (number.isEmpty) { if (number.isEmpty) {
onMessage("⚠️ 주머니 번호를 입력해주세요.", true); onMessage("주머니 번호를 입력해주세요.", true);
return; return;
} }
final String pocketLabel = "POCKET_$number"; final String pocketLabel = "POCKET_$number";
bool isAvailable = await NfcManager.instance.isAvailable(); bool isAvailable = await NfcManager.instance.isAvailable();
if (!isAvailable) { if (!isAvailable) {
onMessage("❌ NFC가 꺼져있거나 지원되지 않습니다.", true); onMessage("NFC가 꺼져있거나 지원되지 않습니다.", true);
return; return;
} }
_isWriting = true; _isWriting = true;
_statusMessage = "📡 [$pocketLabel] 쓸 준비 완료! 스티커에 폰 뒷면을 대주세요."; _statusMessage = "[$pocketLabel] 쓸 준비 완료! 스티커에 폰 뒷면을 대주세요.";
notifyListeners(); notifyListeners();
NfcManager.instance.startSession( NfcManager.instance.startSession(
@@ -58,11 +58,11 @@ class NfcTagWriterController extends ChangeNotifier {
try { try {
final ndef = Ndef.from(tag); final ndef = Ndef.from(tag);
if (ndef == null) { if (ndef == null) {
onMessage("❌ 이 태그는 NDEF 쓰기를 지원하지 않는 종류입니다.", true); onMessage("이 태그는 NDEF 쓰기를 지원하지 않는 종류입니다.", true);
return; return;
} }
if (!ndef.isWritable) { if (!ndef.isWritable) {
onMessage("❌ 이 태그는 쓰기 잠금(read-only) 상태입니다.", true); onMessage("이 태그는 쓰기 잠금(read-only) 상태입니다.", true);
return; return;
} }
@@ -78,8 +78,8 @@ class NfcTagWriterController extends ChangeNotifier {
onMessage( onMessage(
tagUid != null tagUid != null
? "✅ [$pocketLabel] 쓰기 + UID 등록 성공!" ? "[$pocketLabel] 쓰기 + UID 등록 성공!"
: "⚠️ [$pocketLabel] 쓰기는 성공했지만 UID를 못 읽어 등록은 안 됐습니다.", : "[$pocketLabel] 쓰기는 성공했지만 UID를 못 읽어 등록은 안 됐습니다.",
tagUid == null, tagUid == null,
); );
// 다음 스티커를 연달아 쓰기 편하도록 번호를 자동으로 1 올려준다. // 다음 스티커를 연달아 쓰기 편하도록 번호를 자동으로 1 올려준다.
@@ -88,7 +88,7 @@ class NfcTagWriterController extends ChangeNotifier {
_statusMessage = "다음 번호를 확인하고 '쓰기 시작'을 다시 눌러주세요."; _statusMessage = "다음 번호를 확인하고 '쓰기 시작'을 다시 눌러주세요.";
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
onMessage("❌ 쓰기 실패: $e", true); onMessage("쓰기 실패: $e", true);
} finally { } finally {
await NfcManager.instance.stopSession(); await NfcManager.instance.stopSession();
_isWriting = false; _isWriting = false;
@@ -117,7 +117,7 @@ class NfcTagWriterController extends ChangeNotifier {
body: jsonEncode({"tagUid": tagUid, "pocketNumber": pocketLabel}), body: jsonEncode({"tagUid": tagUid, "pocketNumber": pocketLabel}),
); );
} catch (e) { } catch (e) {
onMessage("❌ 서버에 UID 등록 실패: $e", true); onMessage("서버에 UID 등록 실패: $e", true);
} }
} }
@@ -18,13 +18,13 @@ class StudentDashboardController extends ChangeNotifier {
final response = await http.delete(url); final response = await http.delete(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
final responseData = jsonDecode(response.body); final responseData = jsonDecode(response.body);
return (true, '✅ ${responseData['message']}'); return (true, '${responseData['message']}');
} else { } else {
final errorData = jsonDecode(response.body); final errorData = jsonDecode(response.body);
return (false, '❌ 삭제 실패: ${errorData['detail'] ?? '알 수 없는 오류'}'); return (false, '삭제 실패: ${errorData['detail'] ?? '알 수 없는 오류'}');
} }
} catch (e) { } catch (e) {
return (false, '❌ 서버와 연결할 수 없습니다. (네트워크 에러)'); return (false, '서버와 연결할 수 없습니다. (네트워크 에러)');
} finally { } finally {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
@@ -145,7 +145,7 @@ class TeacherAttendanceController extends ChangeNotifier {
await fetchAll(); await fetchAll();
return result['message'] ?? '하교 처리되었습니다.'; return result['message'] ?? '하교 처리되었습니다.';
} catch (e) { } catch (e) {
return '❌ 하교 처리 실패: $e'; return '하교 처리 실패: $e';
} }
} }
@@ -161,7 +161,7 @@ class TeacherAttendanceController extends ChangeNotifier {
await fetchAll(); await fetchAll();
return result['message'] ?? '처리되었습니다.'; return result['message'] ?? '처리되었습니다.';
} catch (e) { } catch (e) {
return '❌ 반출 허용 실패: $e'; return '반출 허용 실패: $e';
} }
} }
@@ -177,7 +177,7 @@ class TeacherAttendanceController extends ChangeNotifier {
await fetchAll(); await fetchAll();
return result['message'] ?? '처리되었습니다.'; return result['message'] ?? '처리되었습니다.';
} catch (e) { } catch (e) {
return '❌ 설정 실패: $e'; return '설정 실패: $e';
} }
} }
@@ -193,7 +193,7 @@ class TeacherAttendanceController extends ChangeNotifier {
await fetchAll(); await fetchAll();
return result['message'] ?? '처리되었습니다.'; return result['message'] ?? '처리되었습니다.';
} catch (e) { } catch (e) {
return '❌ 출석시간 설정 실패: $e'; return '출석시간 설정 실패: $e';
} }
} }
@@ -207,7 +207,7 @@ class TeacherAttendanceController extends ChangeNotifier {
await fetchAll(); await fetchAll();
return result['message'] ?? '처리되었습니다.'; return result['message'] ?? '처리되었습니다.';
} catch (e) { } catch (e) {
return '❌ 초기화 실패: $e'; return '초기화 실패: $e';
} }
} }
@@ -36,13 +36,13 @@ class TeacherRegisterController extends ChangeNotifier {
final res = jsonDecode(response.body); final res = jsonDecode(response.body);
if (response.statusCode == 200 && res['status'] == 'success') { if (response.statusCode == 200 && res['status'] == 'success') {
return (true, '✅ ${res['message']}'); return (true, '${res['message']}');
} else { } else {
String errorMsg = res['detail'] ?? res['message'] ?? '회원가입에 실패했습니다.'; String errorMsg = res['detail'] ?? res['message'] ?? '회원가입에 실패했습니다.';
return (false, '❌ $errorMsg'); return (false, errorMsg);
} }
} catch (e) { } catch (e) {
return (false, '❌ 서버와 통신에 실패했습니다.'); return (false, '서버와 통신에 실패했습니다.');
} finally { } finally {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
@@ -76,11 +76,11 @@ class TeacherStudentManagementController extends ChangeNotifier {
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
await fetchStudents(); await fetchStudents();
return (true, '✅ $studentName 학생 기기 초기화 완료! (비밀번호도 1234로 초기화됨)'); return (true, '$studentName 학생 기기 초기화 완료! (비밀번호도 1234로 초기화됨)');
} }
return (false, '❌ 초기화 통신 실패'); return (false, '초기화 통신 실패');
} catch (e) { } catch (e) {
return (false, '❌ 초기화 통신 실패: $e'); return (false, '초기화 통신 실패: $e');
} finally { } finally {
_isWorking = false; _isWorking = false;
notifyListeners(); notifyListeners();
@@ -211,12 +211,12 @@ class TeacherStudentManagementController extends ChangeNotifier {
final result = jsonDecode(utf8.decode(response.bodyBytes)); final result = jsonDecode(utf8.decode(response.bodyBytes));
if (response.statusCode == 200 && result['status'] == 'success') { if (response.statusCode == 200 && result['status'] == 'success') {
return (true, '✅ ${result['message'] ?? '계정이 생성되었습니다.'}'); return (true, '${result['message'] ?? '계정이 생성되었습니다.'}');
} else { } else {
return (false, '❌ 생성 실패: ${result['message'] ?? '오류 발생'}'); return (false, '생성 실패: ${result['message'] ?? '오류 발생'}');
} }
} catch (e) { } catch (e) {
return (false, '🚨 네트워크 에러: $e'); return (false, '네트워크 에러: $e');
} }
} }
@@ -238,12 +238,12 @@ class TeacherStudentManagementController extends ChangeNotifier {
if (response.statusCode == 200 && result['status'] == 'success') { if (response.statusCode == 200 && result['status'] == 'success') {
await fetchStudents(); await fetchStudents();
return (true, '✅ ${result['message'] ?? '학년이 지정되었습니다.'}'); return (true, '${result['message'] ?? '학년이 지정되었습니다.'}');
} else { } else {
return (false, '❌ 지정 실패: ${result['message'] ?? '오류 발생'}'); return (false, '지정 실패: ${result['message'] ?? '오류 발생'}');
} }
} catch (e) { } catch (e) {
return (false, '🚨 네트워크 에러: $e'); return (false, '네트워크 에러: $e');
} finally { } finally {
_isWorking = false; _isWorking = false;
notifyListeners(); notifyListeners();
@@ -266,12 +266,12 @@ class TeacherStudentManagementController extends ChangeNotifier {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final resData = jsonDecode(utf8.decode(response.bodyBytes)); final resData = jsonDecode(utf8.decode(response.bodyBytes));
await fetchStudents(); await fetchStudents();
return (true, '✅ ${resData['message']}'); return (true, '${resData['message']}');
} else { } else {
return (false, '❌ 삭제 실패하였습니다.'); return (false, '삭제 실패하였습니다.');
} }
} catch (e) { } catch (e) {
return (false, '❌ 서버 에러 발생'); return (false, '서버 에러 발생');
} finally { } finally {
_isWorking = false; _isWorking = false;
notifyListeners(); notifyListeners();
+8 -8
View File
@@ -44,7 +44,7 @@ Future<void> initializePocketWatchService() async {
autoStartOnBoot: false, autoStartOnBoot: false,
isForegroundMode: true, isForegroundMode: true,
notificationChannelId: pocketWatchNotificationChannelId, notificationChannelId: pocketWatchNotificationChannelId,
initialNotificationTitle: '🛡️ 주머니 감시 대기 중', initialNotificationTitle: '주머니 감시 대기 중',
initialNotificationContent: '출석 태깅을 기다리는 중입니다.', initialNotificationContent: '출석 태깅을 기다리는 중입니다.',
foregroundServiceNotificationId: pocketWatchNotificationId, foregroundServiceNotificationId: pocketWatchNotificationId,
foregroundServiceTypes: [AndroidForegroundType.specialUse], foregroundServiceTypes: [AndroidForegroundType.specialUse],
@@ -88,7 +88,7 @@ void onPocketWatchServiceStart(ServiceInstance service) {
Future<void> reportViolation() async { Future<void> reportViolation() async {
// 진동 3연타로 강하게 경고 (백그라운드 서비스에서는 HapticFeedback이 아닌 vibration 패키지를 써야 동작함) // 진동 3연타로 강하게 경고 (백그라운드 서비스에서는 HapticFeedback이 아닌 vibration 패키지를 써야 동작함)
Vibration.vibrate(pattern: [0, 400, 150, 400, 150, 400]); Vibration.vibrate(pattern: [0, 400, 150, 400, 150, 400]);
updateNotification("🚨 무단 반출 감지!", "[$pocketNumber] 주머니에서 폰이 감지되지 않습니다."); updateNotification("무단 반출 감지!", "[$pocketNumber] 주머니에서 폰이 감지되지 않습니다.");
service.invoke('violation_detected', { service.invoke('violation_detected', {
"pocketNumber": pocketNumber, "pocketNumber": pocketNumber,
"timestamp": DateTime.now().toIso8601String(), "timestamp": DateTime.now().toIso8601String(),
@@ -128,7 +128,7 @@ void onPocketWatchServiceStart(ServiceInstance service) {
} }
if (isPermitted) { if (isPermitted) {
updateNotification("✅ 폰 회수 완료", "[$pocketNumber] 주머니에서 정상적으로 회수되었습니다."); updateNotification("폰 회수 완료", "[$pocketNumber] 주머니에서 정상적으로 회수되었습니다.");
service.invoke('checked_out', {"pocketNumber": pocketNumber}); service.invoke('checked_out', {"pocketNumber": pocketNumber});
service.stopSelf(); service.stopSelf();
} else { } else {
@@ -145,7 +145,7 @@ void onPocketWatchServiceStart(ServiceInstance service) {
baselineLux = baselineLux =
calibrationSamples.reduce((a, b) => a + b) ~/ calibrationSamples.reduce((a, b) => a + b) ~/
calibrationSamples.length; calibrationSamples.length;
updateNotification("🛡️ 감시 중", "[$pocketNumber] 폰이 주머니 안에 있는지 감시 중"); updateNotification("감시 중", "[$pocketNumber] 폰이 주머니 안에 있는지 감시 중");
} }
return; return;
} }
@@ -163,22 +163,22 @@ void onPocketWatchServiceStart(ServiceInstance service) {
baselineLux = null; baselineLux = null;
violated = false; violated = false;
updateNotification("📥 주머니에 넣는 중...", "잠시 후 감시가 시작됩니다."); updateNotification("주머니에 넣는 중...", "잠시 후 감시가 시작됩니다.");
await Future.delayed(_placePhoneDelay); await Future.delayed(_placePhoneDelay);
updateNotification("🔎 밝기 측정 중...", "주머니 속 밝기를 기준값으로 설정하는 중입니다."); updateNotification("밝기 측정 중...", "주머니 속 밝기를 기준값으로 설정하는 중입니다.");
try { try {
await Light().requestAuthorization(); await Light().requestAuthorization();
lightSub = Light().lightSensorStream.listen( lightSub = Light().lightSensorStream.listen(
onLightReading, onLightReading,
onError: (Object error) { onError: (Object error) {
updateNotification("❌ 조도 센서 오류", "$error"); updateNotification("조도 센서 오류", "$error");
}, },
); );
} catch (e) { } catch (e) {
updateNotification("❌ 조도 센서 시작 실패", "$e"); updateNotification("조도 센서 시작 실패", "$e");
} }
} }
+4 -4
View File
@@ -22,7 +22,7 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
String newPw = _pwController.text.trim(); String newPw = _pwController.text.trim();
if (newPw.isEmpty || newPw == "1234") { if (newPw.isEmpty || newPw == "1234") {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('⚠️ 초기 비밀번호와 다른 안전한 비밀번호를 입력하세요.')), const SnackBar(content: Text('초기 비밀번호와 다른 안전한 비밀번호를 입력하세요.')),
); );
return; return;
} }
@@ -38,7 +38,7 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
final res = jsonDecode(response.body); final res = jsonDecode(response.body);
if (response.statusCode == 200 && res['status'] == 'success') { if (response.statusCode == 200 && res['status'] == 'success') {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('🔒 비밀번호 변경 완료! 다시 로그인해 주세요.')), const SnackBar(content: Text('비밀번호 변경 완료! 다시 로그인해 주세요.')),
); );
// 비밀번호를 바꿨으니 다시 로그인 화면으로 강제 이동 // 비밀번호를 바꿨으니 다시 로그인 화면으로 강제 이동
Navigator.pushReplacement( Navigator.pushReplacement(
@@ -49,7 +49,7 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
} catch (e) { } catch (e) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(const SnackBar(content: Text('❌ 통신 실패'))); ).showSnackBar(const SnackBar(content: Text('통신 실패')));
} finally { } finally {
setState(() => _isLoading = false); setState(() => _isLoading = false);
} }
@@ -65,7 +65,7 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( const Text(
'🔒 보안을 위해\n비밀번호를 변경해 주세요', '보안을 위해\n비밀번호를 변경해 주세요',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
+2 -2
View File
@@ -38,7 +38,7 @@ class _AdminDashboardState extends State<AdminDashboard> {
builder: (BuildContext dialogContext) { builder: (BuildContext dialogContext) {
return AlertDialog( return AlertDialog(
title: const Text( title: const Text(
'⚠️ DB 초기화 경고', 'DB 초기화 경고',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
), ),
content: const Text( content: const Text(
@@ -74,7 +74,7 @@ class _AdminDashboardState extends State<AdminDashboard> {
return Scaffold( return Scaffold(
backgroundColor: AppPalette.mist, backgroundColor: AppPalette.mist,
appBar: AppBar( appBar: AppBar(
title: const Text('🛠️ 관리자 시스템'), title: const Text('관리자 시스템'),
backgroundColor: AppPalette.ink, backgroundColor: AppPalette.ink,
foregroundColor: AppPalette.paper, foregroundColor: AppPalette.paper,
actions: [ actions: [
+12 -9
View File
@@ -63,13 +63,13 @@ class _DeviceCheckoutLedgerPageState extends State<DeviceCheckoutLedgerPage> {
({Color color, String label}) _statusInfo(String status) { ({Color color, String label}) _statusInfo(String status) {
switch (status) { switch (status) {
case 'PENDING': case 'PENDING':
return (color: Colors.orange, label: '⏳ 대기중'); return (color: Colors.orange, label: '대기중');
case 'APPROVED': case 'APPROVED':
return (color: Colors.blue, label: '✅ 승인됨'); return (color: Colors.blue, label: '승인됨');
case 'RETURNED': case 'RETURNED':
return (color: Colors.grey, label: '↩️ 반납완료'); return (color: Colors.grey, label: '반납완료');
case 'REJECTED': case 'REJECTED':
return (color: Colors.red, label: '🚫 거절됨'); return (color: Colors.red, label: '거절됨');
default: default:
return (color: Colors.grey, label: status); return (color: Colors.grey, label: status);
} }
@@ -95,7 +95,7 @@ class _DeviceCheckoutLedgerPageState extends State<DeviceCheckoutLedgerPage> {
backgroundColor: AppPalette.mist, backgroundColor: AppPalette.mist,
appBar: AppBar( appBar: AppBar(
title: const Text( title: const Text(
'📱 스마트기기 반출 대장', '스마트기기 반출 대장',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
backgroundColor: AppPalette.ink, backgroundColor: AppPalette.ink,
@@ -127,7 +127,7 @@ class _DeviceCheckoutLedgerPageState extends State<DeviceCheckoutLedgerPage> {
border: Border.all(color: Colors.orange[400]!), border: Border.all(color: Colors.orange[400]!),
), ),
child: Text( child: Text(
'⏳ 승인 대기 중인 요청이 $pendingCount건 있습니다.', '승인 대기 중인 요청이 $pendingCount건 있습니다.',
style: TextStyle( style: TextStyle(
color: Colors.orange[900], color: Colors.orange[900],
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -207,7 +207,10 @@ class _DeviceCheckoutLedgerPageState extends State<DeviceCheckoutLedgerPage> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
'⏰ ${_timeRange(r['requestedStart'], r['requestedEnd'])}', _timeRange(
r['requestedStart'],
r['requestedEnd'],
),
style: TextStyle( style: TextStyle(
color: Colors.grey[700], color: Colors.grey[700],
fontSize: 13, fontSize: 13,
@@ -215,7 +218,7 @@ class _DeviceCheckoutLedgerPageState extends State<DeviceCheckoutLedgerPage> {
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
'📝 ${r['purpose']}', r['purpose'],
style: TextStyle( style: TextStyle(
color: Colors.grey[700], color: Colors.grey[700],
fontSize: 13, fontSize: 13,
@@ -226,7 +229,7 @@ class _DeviceCheckoutLedgerPageState extends State<DeviceCheckoutLedgerPage> {
null) ...[ null) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
'👤 ${r['approvedByTeacherName']} 선생님이 허용했습니다', '${r['approvedByTeacherName']} 선생님이 허용했습니다',
style: TextStyle( style: TextStyle(
color: Colors.blue[700], color: Colors.blue[700],
fontSize: 12, fontSize: 12,
+2 -2
View File
@@ -58,7 +58,7 @@ class _DeviceCheckoutRequestScreenState
final purpose = _purposeController.text.trim(); final purpose = _purposeController.text.trim();
if (_startTime == null || _endTime == null || purpose.isEmpty) { if (_startTime == null || _endTime == null || purpose.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('⚠️ 시작/종료 시각과 사용 목적을 모두 입력해주세요.')), const SnackBar(content: Text('시작/종료 시각과 사용 목적을 모두 입력해주세요.')),
); );
return; return;
} }
@@ -86,7 +86,7 @@ class _DeviceCheckoutRequestScreenState
backgroundColor: AppPalette.mist, backgroundColor: AppPalette.mist,
appBar: AppBar( appBar: AppBar(
title: const Text( title: const Text(
'📱 스마트기기 반출 신청', '스마트기기 반출 신청',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
backgroundColor: AppPalette.ink, backgroundColor: AppPalette.ink,
+11 -13
View File
@@ -43,9 +43,9 @@ class _LoginScreenState extends State<LoginScreen> {
_showErrorDialog(result.errorMessage!); _showErrorDialog(result.errorMessage!);
break; break;
case LoginOutcome.masterSuccess: case LoginOutcome.masterSuccess:
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
const SnackBar(content: Text('👑 개발자 최고 권한으로 로그인되었습니다.')), context,
); ).showSnackBar(const SnackBar(content: Text('개발자 최고 권한으로 로그인되었습니다.')));
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -69,7 +69,7 @@ class _LoginScreenState extends State<LoginScreen> {
case LoginOutcome.success: case LoginOutcome.success:
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(SnackBar(content: Text('✅ ${result.name}님 환영합니다!'))); ).showSnackBar(SnackBar(content: Text('${result.name}님 환영합니다!')));
_navigateBasedOnRole( _navigateBasedOnRole(
result.role!, result.role!,
result.studentId!, result.studentId!,
@@ -99,7 +99,7 @@ class _LoginScreenState extends State<LoginScreen> {
builder: (context, setDialogState) { builder: (context, setDialogState) {
return AlertDialog( return AlertDialog(
title: const Text( title: const Text(
'🔒 초기 비밀번호 변경', '초기 비밀번호 변경',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
content: Column( content: Column(
@@ -136,9 +136,7 @@ class _LoginScreenState extends State<LoginScreen> {
String newPassword = newPwController.text.trim(); String newPassword = newPwController.text.trim();
if (newPassword.isEmpty) { if (newPassword.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('새 비밀번호를 입력해 주세요.')),
content: Text('⚠️ 새 비밀번호를 입력해 주세요.'),
),
); );
return; return;
} }
@@ -204,7 +202,7 @@ class _LoginScreenState extends State<LoginScreen> {
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text( title: const Text(
'⚠️ 인증 실패', '인증 실패',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
content: Text(message), content: Text(message),
@@ -229,7 +227,7 @@ class _LoginScreenState extends State<LoginScreen> {
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext dialogContext) { builder: (BuildContext dialogContext) {
return AlertDialog( return AlertDialog(
title: Text('🔒 $title 권한 인증 (기존 방식)'), title: Text('$title 권한 인증 (기존 방식)'),
content: TextField( content: TextField(
controller: passwordController, controller: passwordController,
obscureText: true, obscureText: true,
@@ -244,7 +242,7 @@ class _LoginScreenState extends State<LoginScreen> {
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('❌ 비밀번호가 올바르지 않습니다.')), const SnackBar(content: Text('비밀번호가 올바르지 않습니다.')),
); );
} }
}, },
@@ -268,7 +266,7 @@ class _LoginScreenState extends State<LoginScreen> {
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('❌ 비밀번호가 올바르지 않습니다.')), const SnackBar(content: Text('비밀번호가 올바르지 않습니다.')),
); );
} }
}, },
@@ -365,7 +363,7 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
), ),
child: const Text( child: const Text(
'👨‍🏫 선생님이신가요? 교사 회원가입 하기', '선생님이신가요? 교사 회원가입 하기',
style: TextStyle( style: TextStyle(
color: AppPalette.ink, color: AppPalette.ink,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
+8 -10
View File
@@ -165,28 +165,28 @@ class _MainDashboardState extends State<MainDashboard> {
({String title, String subtitle, Color color, IconData icon}) _theme() { ({String title, String subtitle, Color color, IconData icon}) _theme() {
if (_isDeveloper) { if (_isDeveloper) {
return ( return (
title: '👑 MASTER CONTROL', title: 'MASTER CONTROL',
subtitle: '최고 관리 권한 활성화됨', subtitle: '최고 관리 권한 활성화됨',
color: AppPalette.ink, color: AppPalette.ink,
icon: Icons.admin_panel_settings_rounded, icon: Icons.admin_panel_settings_rounded,
); );
} else if (widget.role == 'admin') { } else if (widget.role == 'admin') {
return ( return (
title: '🛠️ ADMIN CONTROL', title: 'ADMIN CONTROL',
subtitle: '시스템 관리자 권한 활성화됨', subtitle: '시스템 관리자 권한 활성화됨',
color: AppPalette.ink, color: AppPalette.ink,
icon: Icons.admin_panel_settings_rounded, icon: Icons.admin_panel_settings_rounded,
); );
} else if (widget.role == 'teacher') { } else if (widget.role == 'teacher') {
return ( return (
title: '👨‍🏫 TEACHER PORTAL', title: 'TEACHER PORTAL',
subtitle: '교직원 번호: $_displayId | 교사 권한 활성화됨', subtitle: '교직원 번호: $_displayId | 교사 권한 활성화됨',
color: AppPalette.ink, color: AppPalette.ink,
icon: Icons.admin_panel_settings_rounded, icon: Icons.admin_panel_settings_rounded,
); );
} }
return ( return (
title: '🎓 STUDENT PORTAL', title: 'STUDENT PORTAL',
subtitle: '학번: $_displayId | 인증 완료', subtitle: '학번: $_displayId | 인증 완료',
color: AppPalette.ink, color: AppPalette.ink,
icon: Icons.school_rounded, icon: Icons.school_rounded,
@@ -334,7 +334,7 @@ class _MainDashboardState extends State<MainDashboard> {
title: 'NFC 태그', title: 'NFC 태그',
subtitle: widget.isDeviceMatched subtitle: widget.isDeviceMatched
? '출석 및 폰 수거 완료' ? '출석 및 폰 수거 완료'
: '⚠️ 본인 인증 기기 전용', : '본인 인증 기기 전용',
color: widget.isDeviceMatched color: widget.isDeviceMatched
? AppPalette.ink ? AppPalette.ink
: AppPalette.sage, : AppPalette.sage,
@@ -346,7 +346,7 @@ class _MainDashboardState extends State<MainDashboard> {
).showSnackBar( ).showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text(
'🚨 대리 출석 방지를 위해 등록된 본인 스마트폰에서만 출석 가능합니다.', '대리 출석 방지를 위해 등록된 본인 스마트폰에서만 출석 가능합니다.',
), ),
), ),
); );
@@ -363,9 +363,7 @@ class _MainDashboardState extends State<MainDashboard> {
onTap: () { onTap: () {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text('실시간 학교 상황 페이지로 이동합니다.'),
'🍔 실시간 학교 상황 페이지로 이동합니다.',
),
), ),
); );
}, },
@@ -455,7 +453,7 @@ class _MainDashboardState extends State<MainDashboard> {
icon: Icons.delete_sweep_rounded, icon: Icons.delete_sweep_rounded,
title: '계정 강제 삭제', title: '계정 강제 삭제',
subtitle: '학생 및 교사 DB 삭제', subtitle: '학생 및 교사 DB 삭제',
color: Colors.red[600]!, color: AppPalette.ink,
onTap: () => _showDeleteUserDialog(context), onTap: () => _showDeleteUserDialog(context),
), ),
if (_isAdmin) if (_isAdmin)
+10 -7
View File
@@ -72,7 +72,7 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Colors.red[50], backgroundColor: Colors.red[50],
title: const Text( title: const Text(
"🚨 무단 반출 감지", "무단 반출 감지",
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
), ),
content: Text( content: Text(
@@ -98,7 +98,7 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text("🛡️ 감시 모드 켜기 전에"), title: const Text("감시 모드 켜기 전에"),
content: const Text( content: const Text(
"iOS는 화면을 잠그면 감시가 끊겨요. 아래 순서로 '가이드 접근'을 먼저 켜주세요.\n\n" "iOS는 화면을 잠그면 감시가 끊겨요. 아래 순서로 '가이드 접근'을 먼저 켜주세요.\n\n"
"1. 사이드(또는 홈) 버튼 3번 빠르게 누르기\n" "1. 사이드(또는 홈) 버튼 3번 빠르게 누르기\n"
@@ -127,9 +127,9 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text("🎉 자습실 출석 완료!"), title: const Text("자습실 출석 완료!"),
content: Text( content: Text(
"${widget.studentName} 학생!\n[$pocketNumber] 주머니 출석이 확인되었습니다.\n\n폰을 주머니에 쏙 넣고 자습에 집중해 주세요! ✏️", "${widget.studentName} 학생!\n[$pocketNumber] 주머니 출석이 확인되었습니다.\n\n폰을 주머니에 쏙 넣고 자습에 집중해 주세요!",
), ),
actions: [ actions: [
TextButton( TextButton(
@@ -161,7 +161,10 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
appBar: AppBar( appBar: AppBar(
title: const Text( title: const Text(
"자습실 NFC 출석체크", "자습실 NFC 출석체크",
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
), ),
backgroundColor: const Color.fromARGB(255, 48, 48, 52), backgroundColor: const Color.fromARGB(255, 48, 48, 52),
), ),
@@ -258,8 +261,8 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
!kIsWeb && Platform.isIOS !kIsWeb && Platform.isIOS
? "🔒 가이드 접근을 켠 채로 화면만 잠기도록 두세요.\n(임의로 앱을 나가면 감시가 끊깁니다)\n감시를 끝내려면 가이드 접근을 먼저 끄고 다시 태깅하세요." ? "가이드 접근을 켠 채로 화면만 잠기도록 두세요.\n(임의로 앱을 나가면 감시가 끊깁니다)\n감시를 끝내려면 가이드 접근을 먼저 끄고 다시 태깅하세요."
: "📴 화면을 꺼도 감시는 계속됩니다.\n알림바에서 상태를 확인할 수 있어요.\n(폰을 꺼내 다시 태깅하면 반출 처리됩니다)", : "화면을 꺼도 감시는 계속됩니다.\n알림바에서 상태를 확인할 수 있어요.\n(폰을 꺼내 다시 태깅하면 반출 처리됩니다)",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white38, fontSize: 13), style: const TextStyle(color: Colors.white38, fontSize: 13),
), ),
+1 -1
View File
@@ -50,7 +50,7 @@ class _NfcTagWriterScreenState extends State<NfcTagWriterScreen> {
final bool isWriting = _controller.isWriting; final bool isWriting = _controller.isWriting;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("🏷️ NFC 주머니 태그 쓰기"), title: const Text("NFC 주머니 태그 쓰기"),
backgroundColor: Colors.deepPurple, backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
+10 -10
View File
@@ -63,7 +63,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('🧪 테스트용 허용시간 초기화'), title: const Text('테스트용 허용시간 초기화'),
content: const Text( content: const Text(
'하교 처리나 반출 허용 시간 설정으로 켜져 있는 모든 허용 시간대를 지금 즉시 해제합니다.\n' '하교 처리나 반출 허용 시간 설정으로 켜져 있는 모든 허용 시간대를 지금 즉시 해제합니다.\n'
'(무단반출 감지 테스트할 때만 사용하세요)', '(무단반출 감지 테스트할 때만 사용하세요)',
@@ -126,7 +126,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('⏰ 전체 학생 반출 허용 시간 설정'), title: const Text('전체 학생 반출 허용 시간 설정'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -169,7 +169,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('🏫 하교 처리'), title: const Text('하교 처리'),
content: const Text( content: const Text(
'지금부터 모든 학생의 반출이 자동으로 허용되며, 더 이상 무단반출 경고가 뜨지 않습니다.\n하교 처리하시겠습니까?', '지금부터 모든 학생의 반출이 자동으로 허용되며, 더 이상 무단반출 경고가 뜨지 않습니다.\n하교 처리하시겠습니까?',
), ),
@@ -206,7 +206,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
backgroundColor: AppPalette.mist, backgroundColor: AppPalette.mist,
appBar: AppBar( appBar: AppBar(
title: const Text( title: const Text(
'📋 실시간 출석 현황', '실시간 출석 현황',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
backgroundColor: AppPalette.ink, backgroundColor: AppPalette.ink,
@@ -261,7 +261,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
Icons.bug_report_outlined, Icons.bug_report_outlined,
color: Colors.white70, color: Colors.white70,
), ),
tooltip: '🧪 테스트용: 허용시간 초기화', tooltip: '테스트용: 허용시간 초기화',
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
@@ -387,9 +387,9 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
? Colors.red ? Colors.red
: (isPending ? Colors.orange : (isComplete ? Colors.blue : Colors.red)); : (isPending ? Colors.orange : (isComplete ? Colors.blue : Colors.red));
final String statusLine = hasViolation final String statusLine = hasViolation
? '🚨 무단반출 (${student['violationTime']})' ? '무단반출 (${student['violationTime']})'
: (isPending : (isPending
? '⏳ 미완료 (${student['checkInTime']})' ? '미완료 (${student['checkInTime']})'
: (isComplete : (isComplete
? '${student['checkInTime']}' ? '${student['checkInTime']}'
: (student['hasEverCheckedIn'] == true ? '미제출' : '미등록'))); : (student['hasEverCheckedIn'] == true ? '미제출' : '미등록')));
@@ -691,7 +691,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
'🔓 지금 전체 반출 허용 중입니다 ($until 까지) — 이 시간 동안은 무단반출 경고가 뜨지 않아요.', '지금 전체 반출 허용 중입니다 ($until 까지) — 이 시간 동안은 무단반출 경고가 뜨지 않아요.',
style: TextStyle( style: TextStyle(
color: Colors.amber[900], color: Colors.amber[900],
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -721,12 +721,12 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
onSelected: (_) => _controller.setFilter("ALL"), onSelected: (_) => _controller.setFilter("ALL"),
), ),
FilterChip( FilterChip(
label: const Text("🟢 출석자"), label: const Text("출석자"),
selected: _controller.filterType == "CHECKED_IN", selected: _controller.filterType == "CHECKED_IN",
onSelected: (_) => _controller.setFilter("CHECKED_IN"), onSelected: (_) => _controller.setFilter("CHECKED_IN"),
), ),
FilterChip( FilterChip(
label: const Text("🔴 미출석자"), label: const Text("미출석자"),
selected: _controller.filterType == "ABSENT", selected: _controller.filterType == "ABSENT",
onSelected: (_) => _controller.setFilter("ABSENT"), onSelected: (_) => _controller.setFilter("ABSENT"),
), ),
+3 -3
View File
@@ -36,7 +36,7 @@ class _TeacherRegisterScreenState extends State<TeacherRegisterScreen> {
if (id.isEmpty || pw.isEmpty || name.isEmpty || secret.isEmpty) { if (id.isEmpty || pw.isEmpty || name.isEmpty || secret.isEmpty) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(const SnackBar(content: Text('⚠️ 모든 빈칸을 입력해 주세요.'))); ).showSnackBar(const SnackBar(content: Text('모든 빈칸을 입력해 주세요.')));
return; return;
} }
@@ -72,7 +72,7 @@ class _TeacherRegisterScreenState extends State<TeacherRegisterScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( const Text(
'👨‍🏫 교직원 전용 인증', '교직원 전용 인증',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -85,7 +85,7 @@ class _TeacherRegisterScreenState extends State<TeacherRegisterScreen> {
controller: _secretController, controller: _secretController,
obscureText: true, obscureText: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: '🔑 교사 인증 비밀코드 입력', labelText: '교사 인증 비밀코드 입력',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
), ),
+7 -11
View File
@@ -53,7 +53,7 @@ class _TeacherStudentManagementPageState
if (sId.isEmpty || sName.isEmpty) { if (sId.isEmpty || sName.isEmpty) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(const SnackBar(content: Text('⚠️ 모든 입력란을 채워주세요.'))); ).showSnackBar(const SnackBar(content: Text('모든 입력란을 채워주세요.')));
return; return;
} }
@@ -91,15 +91,13 @@ class _TeacherStudentManagementPageState
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(SnackBar(content: Text('❌ 엑셀 파일을 읽는 데 실패했습니다: $e'))); ).showSnackBar(SnackBar(content: Text('엑셀 파일을 읽는 데 실패했습니다: $e')));
return; return;
} }
if (rows.isEmpty) { if (rows.isEmpty) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('유효한 학생 데이터를 찾지 못했습니다. (1행은 머리글로 건너뜁니다)')),
content: Text('⚠️ 유효한 학생 데이터를 찾지 못했습니다. (1행은 머리글로 건너뜁니다)'),
),
); );
return; return;
} }
@@ -112,7 +110,7 @@ class _TeacherStudentManagementPageState
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
title: Text('📄 ${rows.length}명 확인됨'), title: Text('${rows.length}명 확인됨'),
content: SizedBox( content: SizedBox(
width: 400, width: 400,
child: Column( child: Column(
@@ -209,9 +207,7 @@ class _TeacherStudentManagementPageState
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
title: Text( title: Text(result.failures.isEmpty ? '일괄 등록 완료' : '일괄 등록 완료 (일부 실패)'),
result.failures.isEmpty ? '✅ 일괄 등록 완료' : '⚠️ 일괄 등록 완료 (일부 실패)',
),
content: SizedBox( content: SizedBox(
width: 400, width: 400,
child: Column( child: Column(
@@ -295,7 +291,7 @@ class _TeacherStudentManagementPageState
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text( title: const Text(
'⚠️ 기기 등록 초기화', '기기 등록 초기화',
style: TextStyle(fontWeight: FontWeight.bold, color: AppPalette.ink), style: TextStyle(fontWeight: FontWeight.bold, color: AppPalette.ink),
), ),
content: Text('$studentName 학생의 스마트폰 기기 등록과 비밀번호(1234)를 초기화하시겠습니까?'), content: Text('$studentName 학생의 스마트폰 기기 등록과 비밀번호(1234)를 초기화하시겠습니까?'),
@@ -676,7 +672,7 @@ class _TeacherStudentManagementPageState
backgroundColor: AppPalette.mist, backgroundColor: AppPalette.mist,
appBar: AppBar( appBar: AppBar(
title: const Text( title: const Text(
'⚙️ 학생 통합 관리 센터', '학생 통합 관리 센터',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
backgroundColor: AppPalette.ink, backgroundColor: AppPalette.ink,