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