학생/교사/관리자 대시보드를 하나의 공통 화면으로 통합
StudentDashboard, TeacherDashboard, AdminDashboard(진입 라우팅)를 MainDashboard 하나로 합치고, 역할(role)에 따라 배너 색상/문구와 보이는 타일만 달라지게 함. 그리드 레이아웃/카드 디자인은 이제 한 곳에서만 관리되어, 앞으로 UI를 고치면 모든 역할에 자동 적용됨. - 2061(개발자) 계정에 기존에 빠져있던 "스마트기기 반출 대장" 타일 추가 - role=='admin' 실 계정도 이제 학생 계정 관리/출석 확인 등 교사 기능에 접근 가능 (관리자는 교사 권한을 포함하는 것으로 정리) - 작은 DB 초기화 전용 화면(admin_dashboard.dart)은 "서버 DB 제어" 타일의 목적지로만 유지, 로그인 직후 랜딩 화면 역할은 제거 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
// 🏠 통합 대시보드 (UI 전용). 학생/교사/관리자가 전부 이 화면 하나를 공유하고,
|
||||
// 권한(role)에 따라 배너 색상/문구와 보이는 타일만 달라진다.
|
||||
// 레이아웃(그리드, 카드 디자인)을 한 곳에서 고치면 모든 역할에 동시에 적용된다.
|
||||
// 계정 강제 삭제 서버 통신은 lib/function/student_dashboard_controller.dart가 담당한다.
|
||||
import 'package:flutter/material.dart';
|
||||
import '../function/student_dashboard_controller.dart';
|
||||
import 'admin_dashboard.dart';
|
||||
import 'device_checkout_ledger_page.dart';
|
||||
import 'device_checkout_request_screen.dart';
|
||||
import 'login_screen.dart';
|
||||
import 'nfc_poccket_checkin_screen.dart';
|
||||
import 'nfc_tag_writer_screen.dart';
|
||||
import 'teacher_attendance_page.dart';
|
||||
import 'teacher_student_management_page.dart';
|
||||
|
||||
class MainDashboard extends StatefulWidget {
|
||||
final String? userId;
|
||||
final String? userName;
|
||||
final String role; // 'student' | 'teacher' | 'admin'
|
||||
final bool isDeviceMatched;
|
||||
|
||||
const MainDashboard({
|
||||
super.key,
|
||||
this.userId,
|
||||
this.userName,
|
||||
required this.role,
|
||||
this.isDeviceMatched = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MainDashboard> createState() => _MainDashboardState();
|
||||
}
|
||||
|
||||
class _MainDashboardState extends State<MainDashboard> {
|
||||
final StudentDashboardController _controller = StudentDashboardController();
|
||||
|
||||
// 🔑 [권한 판정] 2061은 role이 'student'로 저장돼 있지만 실질적으로 최고 권한을 가진다.
|
||||
bool get _isDeveloper => widget.userId == '2061';
|
||||
bool get _isAdmin => widget.role == 'admin' || _isDeveloper;
|
||||
bool get _isTeacherOrAbove => widget.role == 'teacher' || _isAdmin;
|
||||
bool get _isStudent => widget.role == 'student';
|
||||
|
||||
String get _displayId =>
|
||||
widget.userId ?? (widget.role == 'admin' ? '관리자' : '간편인증');
|
||||
String get _displayName =>
|
||||
widget.userName ?? (widget.role == 'admin' ? '시스템 관리자' : '간편인증 선생');
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _openPocketCheckIn(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NfcPocketCheckInScreen(
|
||||
studentId: _displayId,
|
||||
studentName: _displayName,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteUser(String userId) async {
|
||||
final (_, message) = await _controller.deleteUser(userId);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
void _showDeleteUserDialog(BuildContext context) {
|
||||
final TextEditingController idController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: Colors.redAccent),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'계정 강제 삭제',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'학생의 학번 또는 교사의 교직원 번호를 입력하세요.\nDB에서 해당 계정과 토큰이 즉시 삭제됩니다.',
|
||||
style: TextStyle(
|
||||
color: Colors.black54,
|
||||
fontSize: 13,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: idController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '학번 또는 교직원 번호',
|
||||
hintText: '예: 201101 또는 T1001',
|
||||
labelStyle: TextStyle(color: Colors.red[400]),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: Colors.red[400]!, width: 2),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
prefixIcon: const Icon(Icons.person_remove_alt_1_rounded),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'취소',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final inputId = idController.text.trim();
|
||||
if (inputId.isNotEmpty) {
|
||||
Navigator.pop(context);
|
||||
_deleteUser(inputId);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.redAccent,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'삭제 실행',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 🎨 [역할별 테마] 배너 색상/제목/문구만 역할에 따라 다르게. 레이아웃 자체는 공통.
|
||||
({String title, String subtitle, Color color, IconData icon}) _theme() {
|
||||
if (_isDeveloper) {
|
||||
return (
|
||||
title: '👑 MASTER CONTROL',
|
||||
subtitle: '최고 관리 권한 활성화됨',
|
||||
color: Colors.deepPurple[700]!,
|
||||
icon: Icons.admin_panel_settings_rounded,
|
||||
);
|
||||
} else if (widget.role == 'admin') {
|
||||
return (
|
||||
title: '🛠️ ADMIN CONTROL',
|
||||
subtitle: '시스템 관리자 권한 활성화됨',
|
||||
color: Colors.deepPurple[700]!,
|
||||
icon: Icons.admin_panel_settings_rounded,
|
||||
);
|
||||
} else if (widget.role == 'teacher') {
|
||||
return (
|
||||
title: '👨🏫 TEACHER PORTAL',
|
||||
subtitle: '교직원 번호: $_displayId | 교사 권한 활성화됨',
|
||||
color: Colors.green[700]!,
|
||||
icon: Icons.admin_panel_settings_rounded,
|
||||
);
|
||||
}
|
||||
return (
|
||||
title: '🎓 STUDENT PORTAL',
|
||||
subtitle: '학번: $_displayId | 인증 완료',
|
||||
color: Colors.indigo[700]!,
|
||||
icon: Icons.school_rounded,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _controller,
|
||||
builder: (context, _) {
|
||||
final theme = _theme();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
theme.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: theme.color,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout_rounded),
|
||||
onPressed: () => Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const LoginScreen()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 28,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.color,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(32),
|
||||
bottomRight: Radius.circular(32),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: theme.color.withValues(alpha: 0.08),
|
||||
child: Icon(theme.icon, size: 32, color: theme.color),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 18),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$_displayName 님',
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
theme.subtitle,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'스마트 관리 시스템 메뉴',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// 🖥️ 웹(넓은 화면)은 한 줄에 5개씩, 폰(좁은 화면)은 기존 2개 그대로.
|
||||
final bool isWide = constraints.maxWidth >= 800;
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isWide ? 1300 : 700,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: isWide ? 5 : 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 0.95,
|
||||
children: [
|
||||
// 👨🎓 학생 전용 -----------------------------------
|
||||
if (_isStudent)
|
||||
_buildModernCard(
|
||||
icon: widget.isDeviceMatched
|
||||
? Icons.contactless_rounded
|
||||
: Icons.lock_rounded,
|
||||
title: 'NFC 태그',
|
||||
subtitle: widget.isDeviceMatched
|
||||
? '출석 및 폰 수거 완료'
|
||||
: '⚠️ 본인 인증 기기 전용',
|
||||
color: widget.isDeviceMatched
|
||||
? Colors.blue
|
||||
: Colors.grey[400]!,
|
||||
onTap: widget.isDeviceMatched
|
||||
? () => _openPocketCheckIn(context)
|
||||
: () {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'🚨 대리 출석 방지를 위해 등록된 본인 스마트폰에서만 출석 가능합니다.',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
isActionButton: widget.isDeviceMatched,
|
||||
isLoading: _controller.isLoading,
|
||||
),
|
||||
if (_isStudent)
|
||||
_buildModernCard(
|
||||
icon: Icons.fastfood_rounded,
|
||||
title: '실시간 학교 상황',
|
||||
subtitle: '급식실 줄 & 매점 재고 확인',
|
||||
color: Colors.orange[700]!,
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'🍔 실시간 학교 상황 페이지로 이동합니다.',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (_isStudent)
|
||||
_buildModernCard(
|
||||
icon: Icons.tablet_mac_rounded,
|
||||
title: '스마트기기 반출',
|
||||
subtitle: '패드 사용 신청 (시간/목적)',
|
||||
color: Colors.teal,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
DeviceCheckoutRequestScreen(
|
||||
studentId: _displayId,
|
||||
studentName: _displayName,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 👨🏫 교사 이상 -----------------------------------
|
||||
if (_isTeacherOrAbove)
|
||||
_buildModernCard(
|
||||
icon: Icons.assignment_turned_in_rounded,
|
||||
title: '실시간 출석 확인',
|
||||
subtitle: '학생 제출 로그 모니터링',
|
||||
color: Colors.blue,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const TeacherAttendancePage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isTeacherOrAbove)
|
||||
_buildModernCard(
|
||||
icon: Icons.manage_accounts_rounded,
|
||||
title: '학생 계정 관리',
|
||||
subtitle: '계정 추가 및 강제 리셋',
|
||||
color: Colors.orange,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const TeacherStudentManagementPage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isTeacherOrAbove)
|
||||
_buildModernCard(
|
||||
icon: Icons.tablet_mac_rounded,
|
||||
title: '스마트기기 반출 대장',
|
||||
subtitle: '패드 반출 신청 승인/거절',
|
||||
color: Colors.teal,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
DeviceCheckoutLedgerPage(
|
||||
teacherId: widget.userId,
|
||||
teacherName: widget.userName,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 🛠️ 관리자 전용 -----------------------------------
|
||||
if (_isAdmin)
|
||||
_buildModernCard(
|
||||
icon: Icons.terminal_rounded,
|
||||
title: '서버 DB 제어',
|
||||
subtitle: '시스템 원격 초기화',
|
||||
color: Colors.amber[800]!,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const AdminDashboard(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isAdmin)
|
||||
_buildModernCard(
|
||||
icon: Icons.delete_sweep_rounded,
|
||||
title: '계정 강제 삭제',
|
||||
subtitle: '학생 및 교사 DB 삭제',
|
||||
color: Colors.red[600]!,
|
||||
onTap: () => _showDeleteUserDialog(context),
|
||||
),
|
||||
if (_isAdmin)
|
||||
_buildModernCard(
|
||||
icon: Icons.edit_note_rounded,
|
||||
title: 'NFC 태그 쓰기',
|
||||
subtitle: '주머니 스티커 초기 설정',
|
||||
color: Colors.deepPurple,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const NfcTagWriterScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 💎 [카드 디자인 위젯] 학생/교사 대시보드에서 쓰던 것과 동일 — 이제 한 곳에만 존재.
|
||||
Widget _buildModernCard({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
bool isActionButton = false,
|
||||
bool isLoading = false,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: isLoading ? null : onTap,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 28),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isLoading)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
else if (isActionButton)
|
||||
Icon(
|
||||
Icons.touch_app_rounded,
|
||||
size: 16,
|
||||
color: color.withValues(alpha: 0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user