From 3946e2ef048a7a6d1b8cfbec0ad0c64b8d43e1fb Mon Sep 17 00:00:00 2001 From: sihoo Date: Tue, 8 Sep 2026 13:11:55 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=B1=20=EC=A0=84=EC=B2=B4=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=EC=9D=84=20=EC=95=8C=EC=95=BD=ED=98=95(AppNotice)?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 나머지 화면들(교사 회원가입, 학생 계정 관리, 관리자 대시보드, 스마트기기 반출 신청/대장, NFC 태그 쓰기/체크인, 비밀번호 변경)에 남아있던 예전 ScaffoldMessenger.showSnackBar를 전부 AppNotice로 교체. - AppNotice.show에 선택적 color 파라미터 추가 - NFC 태그 쓰기/체크인 화면처럼 성공(초록)/실패(빨강) 등 색으로 구분하던 알림은 그 색을 그대로 유지하면서 모양/위치만 통일된 알약 스타일로 바뀜 - 알림 직후 화면을 전환(로그인 성공 후 대시보드 이동, 회원가입 성공 후 로그인 화면 복귀 등)하던 곳들도, AppNotice가 화면(Scaffold)이 아니라 전역 Overlay를 쓰기 때문에 전환 중에 알림이 잘리지 않고 새 화면 위에 계속 보임 (기존 스낵바 방식보다 개선됨) Co-Authored-By: Claude Sonnet 5 --- lib/screens/change_password_screen.dart | 13 +++------ lib/ui/admin_dashboard.dart | 5 ++-- lib/ui/app_notice.dart | 17 +++++++++--- lib/ui/device_checkout_ledger_page.dart | 13 +++------ lib/ui/device_checkout_request_screen.dart | 9 +++---- lib/ui/login_screen.dart | 29 ++++++--------------- lib/ui/nfc_poccket_checkin_screen.dart | 5 ++-- lib/ui/nfc_tag_writer_screen.dart | 11 ++++---- lib/ui/teacher_register_screen.dart | 9 +++---- lib/ui/teacher_student_management_page.dart | 29 ++++++--------------- 10 files changed, 53 insertions(+), 87 deletions(-) diff --git a/lib/screens/change_password_screen.dart b/lib/screens/change_password_screen.dart index 9f060bf..b0d1b66 100644 --- a/lib/screens/change_password_screen.dart +++ b/lib/screens/change_password_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '../config.dart'; import '../theme/app_palette.dart'; +import '../ui/app_notice.dart'; import '../ui/login_screen.dart'; // 💡 main.dart 파일의 최하단(다른 클래스 중괄호 밖)에 붙여넣으세요. @@ -22,9 +23,7 @@ class _ChangePasswordScreenState extends State { Future _updatePassword() async { String newPw = _pwController.text.trim(); if (newPw.isEmpty || newPw == "1234") { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('초기 비밀번호와 다른 안전한 비밀번호를 입력하세요.')), - ); + AppNotice.show(context, '초기 비밀번호와 다른 안전한 비밀번호를 입력하세요.'); return; } @@ -38,9 +37,7 @@ class _ChangePasswordScreenState extends State { final res = jsonDecode(response.body); if (response.statusCode == 200 && res['status'] == 'success') { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('비밀번호 변경 완료! 다시 로그인해 주세요.')), - ); + AppNotice.show(context, '비밀번호 변경 완료! 다시 로그인해 주세요.'); // 비밀번호를 바꿨으니 다시 로그인 화면으로 강제 이동 Navigator.pushReplacement( context, @@ -48,9 +45,7 @@ class _ChangePasswordScreenState extends State { ); } } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('통신 실패'))); + AppNotice.show(context, '통신 실패'); } finally { setState(() => _isLoading = false); } diff --git a/lib/ui/admin_dashboard.dart b/lib/ui/admin_dashboard.dart index 2eba821..183d4e1 100644 --- a/lib/ui/admin_dashboard.dart +++ b/lib/ui/admin_dashboard.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../function/admin_controller.dart'; import '../theme/app_palette.dart'; +import 'app_notice.dart'; import 'login_screen.dart'; // ========================================== @@ -27,9 +28,7 @@ class _AdminDashboardState extends State { Future _resetDatabase() async { final message = await _controller.resetDatabase(); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); } void _showResetConfirmDialog() { diff --git a/lib/ui/app_notice.dart b/lib/ui/app_notice.dart index cb5c138..f51a4e8 100644 --- a/lib/ui/app_notice.dart +++ b/lib/ui/app_notice.dart @@ -9,8 +9,13 @@ class AppNotice { static final List<_QueuedNotice> _queue = []; static bool _showing = false; - static void show(BuildContext context, String message, {IconData? icon}) { - _queue.add(_QueuedNotice(context, message, icon)); + static void show( + BuildContext context, + String message, { + IconData? icon, + Color? color, + }) { + _queue.add(_QueuedNotice(context, message, icon, color)); _tryShowNext(); } @@ -28,6 +33,7 @@ class AppNotice { builder: (context) => _NoticeBanner( message: next.message, icon: next.icon, + color: next.color, onDone: () { entry.remove(); _showing = false; @@ -43,17 +49,20 @@ class _QueuedNotice { final BuildContext context; final String message; final IconData? icon; - _QueuedNotice(this.context, this.message, this.icon); + final Color? color; + _QueuedNotice(this.context, this.message, this.icon, this.color); } class _NoticeBanner extends StatefulWidget { final String message; final IconData? icon; + final Color? color; final VoidCallback onDone; const _NoticeBanner({ required this.message, required this.icon, + required this.color, required this.onDone, }); @@ -102,7 +111,7 @@ class _NoticeBannerState extends State<_NoticeBanner> constraints: const BoxConstraints(maxWidth: 360), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), decoration: BoxDecoration( - color: AppPalette.ink, + color: widget.color ?? AppPalette.ink, borderRadius: BorderRadius.circular(999), boxShadow: [ BoxShadow( diff --git a/lib/ui/device_checkout_ledger_page.dart b/lib/ui/device_checkout_ledger_page.dart index 0c4a65e..47eaae2 100644 --- a/lib/ui/device_checkout_ledger_page.dart +++ b/lib/ui/device_checkout_ledger_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../function/device_checkout_ledger_controller.dart'; import '../theme/app_palette.dart'; +import 'app_notice.dart'; class DeviceCheckoutLedgerPage extends StatefulWidget { final String? teacherId; @@ -38,26 +39,20 @@ class _DeviceCheckoutLedgerPageState extends State { teacherName: widget.teacherName ?? '간편인증 선생', ); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); } Future _reject(int id) async { final (_, message) = await _controller.reject(id); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); } // 🖐️ [하드웨어 자동 감지 전까지 임시] 기기를 실제로 돌려받았을 때 누르는 버튼. Future _confirmReturn(int id) async { final (_, message) = await _controller.confirmReturn(id); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); } ({Color color, String label}) _statusInfo(String status) { diff --git a/lib/ui/device_checkout_request_screen.dart b/lib/ui/device_checkout_request_screen.dart index 734be69..c588b17 100644 --- a/lib/ui/device_checkout_request_screen.dart +++ b/lib/ui/device_checkout_request_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../function/device_checkout_controller.dart'; import '../theme/app_palette.dart'; +import 'app_notice.dart'; class DeviceCheckoutRequestScreen extends StatefulWidget { final String studentId; @@ -57,9 +58,7 @@ class _DeviceCheckoutRequestScreenState Future _submit() async { final purpose = _purposeController.text.trim(); if (_startTime == null || _endTime == null || purpose.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('시작/종료 시각과 사용 목적을 모두 입력해주세요.')), - ); + AppNotice.show(context, '시작/종료 시각과 사용 목적을 모두 입력해주세요.'); return; } @@ -71,9 +70,7 @@ class _DeviceCheckoutRequestScreenState endTime: _formatTime(_endTime!), ); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); if (success) Navigator.pop(context); } diff --git a/lib/ui/login_screen.dart b/lib/ui/login_screen.dart index 6ac3348..9395d1d 100644 --- a/lib/ui/login_screen.dart +++ b/lib/ui/login_screen.dart @@ -5,6 +5,7 @@ import '../config.dart' show schoolName; import '../function/login_controller.dart'; import '../function/session_store.dart'; import '../theme/app_palette.dart'; +import 'app_notice.dart'; import 'main_dashboard.dart'; import 'teacher_register_screen.dart'; @@ -58,9 +59,7 @@ class _LoginScreenState extends State { await SessionStore.clear(); } if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('개발자 최고 권한으로 로그인되었습니다.'))); + AppNotice.show(context, '개발자 최고 권한으로 로그인되었습니다.'); Navigator.pushReplacement( context, MaterialPageRoute( @@ -82,9 +81,7 @@ class _LoginScreenState extends State { ); break; case LoginOutcome.success: - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('${result.name}님 환영합니다!'))); + AppNotice.show(context, '${result.name}님 환영합니다!'); _navigateBasedOnRole( result.role!, result.studentId!, @@ -150,9 +147,7 @@ class _LoginScreenState extends State { onPressed: () async { String newPassword = newPwController.text.trim(); if (newPassword.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('새 비밀번호를 입력해 주세요.')), - ); + AppNotice.show(context, '새 비밀번호를 입력해 주세요.'); return; } @@ -167,9 +162,7 @@ class _LoginScreenState extends State { if (success) { Navigator.pop(dialogContext); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); _navigateBasedOnRole( role, studentId, @@ -177,9 +170,7 @@ class _LoginScreenState extends State { isDeviceMatched, ); } else { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); } }, child: const Text('변경하고 시작하기'), @@ -269,9 +260,7 @@ class _LoginScreenState extends State { MaterialPageRoute(builder: (context) => nextPage), ); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('비밀번호가 올바르지 않습니다.')), - ); + AppNotice.show(context, '비밀번호가 올바르지 않습니다.'); } }, decoration: const InputDecoration( @@ -293,9 +282,7 @@ class _LoginScreenState extends State { MaterialPageRoute(builder: (context) => nextPage), ); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('비밀번호가 올바르지 않습니다.')), - ); + AppNotice.show(context, '비밀번호가 올바르지 않습니다.'); } }, child: const Text('인증하기'), diff --git a/lib/ui/nfc_poccket_checkin_screen.dart b/lib/ui/nfc_poccket_checkin_screen.dart index d541f1c..c803b33 100644 --- a/lib/ui/nfc_poccket_checkin_screen.dart +++ b/lib/ui/nfc_poccket_checkin_screen.dart @@ -5,6 +5,7 @@ import 'dart:io' show Platform; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import '../function/nfc_pocket_checkin_controller.dart'; +import 'app_notice.dart'; class NfcPocketCheckInScreen extends StatefulWidget { final String studentId; // 로그인된 학생 학번 (예: "2061") @@ -143,9 +144,7 @@ class _NfcPocketCheckInScreenState extends State { void _showSnackBar(String text, Color color) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(text), backgroundColor: color)); + AppNotice.show(context, text, color: color); } @override diff --git a/lib/ui/nfc_tag_writer_screen.dart b/lib/ui/nfc_tag_writer_screen.dart index b435e4d..02db293 100644 --- a/lib/ui/nfc_tag_writer_screen.dart +++ b/lib/ui/nfc_tag_writer_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../function/nfc_tag_writer_controller.dart'; import '../theme/app_palette.dart'; +import 'app_notice.dart'; class NfcTagWriterScreen extends StatefulWidget { const NfcTagWriterScreen({super.key}); @@ -35,11 +36,11 @@ class _NfcTagWriterScreenState extends State { void _showSnackBar(String text, bool isError) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(text), - backgroundColor: isError ? Colors.red : Colors.green, - ), + AppNotice.show( + context, + text, + color: isError ? Colors.red[700] : Colors.green[700], + icon: isError ? Icons.error_outline_rounded : Icons.check_circle_rounded, ); } diff --git a/lib/ui/teacher_register_screen.dart b/lib/ui/teacher_register_screen.dart index 5e6842a..87ee51f 100644 --- a/lib/ui/teacher_register_screen.dart +++ b/lib/ui/teacher_register_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../function/teacher_register_controller.dart'; import '../theme/app_palette.dart'; +import 'app_notice.dart'; class TeacherRegisterScreen extends StatefulWidget { const TeacherRegisterScreen({super.key}); @@ -35,9 +36,7 @@ class _TeacherRegisterScreenState extends State { String secret = _secretController.text.trim(); if (id.isEmpty || pw.isEmpty || name.isEmpty || secret.isEmpty) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('모든 빈칸을 입력해 주세요.'))); + AppNotice.show(context, '모든 빈칸을 입력해 주세요.'); return; } @@ -48,9 +47,7 @@ class _TeacherRegisterScreenState extends State { secretCode: secret, ); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); if (success) { Navigator.pop(context); // 가입 성공 시 로그인 화면으로 복귀 } diff --git a/lib/ui/teacher_student_management_page.dart b/lib/ui/teacher_student_management_page.dart index 784f74f..0a2673d 100644 --- a/lib/ui/teacher_student_management_page.dart +++ b/lib/ui/teacher_student_management_page.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import '../function/excel_file_picker.dart'; import '../function/teacher_student_management_controller.dart'; import '../theme/app_palette.dart'; +import 'app_notice.dart'; // 📐 "실시간 출석 확인" 목록(teacher_attendance_page.dart)의 타일 규격과 동일하게 맞춘다. const double kAttendanceTileWidth = 220; @@ -51,9 +52,7 @@ class _TeacherStudentManagementPageState final sName = _addNameController.text.trim(); if (sId.isEmpty || sName.isEmpty) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('모든 입력란을 채워주세요.'))); + AppNotice.show(context, '모든 입력란을 채워주세요.'); return; } @@ -69,9 +68,7 @@ class _TeacherStudentManagementPageState _addNameController.clear(); setState(() => _selectedGrade = null); } - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); } // 📄 [파일 선택 버튼 동작] "파일 선택"으로 엑셀 고르기 @@ -89,16 +86,12 @@ class _TeacherStudentManagementPageState rows = _controller.parseExcelBytes(bytes); } catch (e) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('엑셀 파일을 읽는 데 실패했습니다: $e'))); + AppNotice.show(context, '엑셀 파일을 읽는 데 실패했습니다: $e'); return; } if (rows.isEmpty) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('유효한 학생 데이터를 찾지 못했습니다. (1행은 머리글로 건너뜁니다)')), - ); + AppNotice.show(context, '유효한 학생 데이터를 찾지 못했습니다. (1행은 머리글로 건너뜁니다)'); return; } _showBulkPreviewDialog(rows); @@ -280,9 +273,7 @@ class _TeacherStudentManagementPageState grade: selected, ); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); } // 🔓 [목록 행의 "기기 리셋" 버튼] 학생이 폰을 바꿨을 때 등록 초기화. @@ -312,9 +303,7 @@ class _TeacherStudentManagementPageState studentName, ); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); }, child: const Text('초기화 승인'), ), @@ -358,9 +347,7 @@ class _TeacherStudentManagementPageState studentId, ); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + AppNotice.show(context, message); }, child: const Text('영구 삭제'), ),