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
+2 -2
View File
@@ -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: [
+12 -9
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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,
+8 -10
View File
@@ -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)
+10 -7
View File
@@ -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),
),
+1 -1
View File
@@ -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,
),
+10 -10
View File
@@ -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"),
),
+3 -3
View File
@@ -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(),
),
),
+7 -11
View File
@@ -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,