// ๐ŸŽ“ ํ•™์ƒ ๋Œ€์‹œ๋ณด๋“œ (UI ์ „์šฉ). NFC ์ฃผ๋จธ๋‹ˆ ์ฒดํฌ์ธ ์ง„์ž…, ์‹ค์‹œ๊ฐ„ ํ•™๊ต ์ƒํ™ฉ ์•ˆ๋‚ด, // ๊ฐœ๋ฐœ์ž ๋งˆ์Šคํ„ฐ ๊ณ„์ • ์ „์šฉ ๊ด€๋ฆฌ ๋ฉ”๋‰ด(ํ˜„ํ™ฉ/DB์ œ์–ด/๊ณ„์ •๊ด€๋ฆฌ/์‚ญ์ œ) ์นด๋“œ๋ฅผ ๊ทธ๋ฆฐ๋‹ค. // ๊ณ„์ • ์‚ญ์ œ ์„œ๋ฒ„ ํ†ต์‹ ์€ lib/function/student_dashboard_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../function/student_dashboard_controller.dart'; import 'nfc_poccket_checkin_screen.dart'; import 'login_screen.dart'; import 'teacher_attendance_page.dart'; import 'admin_dashboard.dart'; import 'student_management_screen.dart'; import 'nfc_tag_writer_screen.dart'; // ------------------------------------------------------------- // 2. ํ•™์ƒ ๋Œ€์‹œ๋ณด๋“œ (StudentDashboard) // ------------------------------------------------------------- class StudentDashboard extends StatefulWidget { final String studentId; final String studentName; final bool isDeviceMatched; // ๐Ÿ†• [๋ณ€๊ฒฝ] ๊ธฐ๊ธฐ UUID๊ฐ€ ๋งค์นญ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ณ€์ˆ˜ ์ถ”๊ฐ€ const StudentDashboard({ super.key, required this.studentId, required this.studentName, required this.isDeviceMatched, // ๐Ÿ†• [๋ณ€๊ฒฝ] ํ•„์ˆ˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋“ฑ๋ก }); @override State createState() => _StudentDashboardState(); } class _StudentDashboardState extends State { final StudentDashboardController _controller = StudentDashboardController(); @override void dispose() { _controller.dispose(); super.dispose(); } // 1๏ธโƒฃ NFC ํƒœ๊ทธ ์นด๋“œ โ†’ ์‹ค์ œ NFC ์ฃผ๋จธ๋‹ˆ ์ฒดํฌ์ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ void _openPocketCheckIn(BuildContext context) { Navigator.push( context, MaterialPageRoute( builder: (context) => NfcPocketCheckInScreen( studentId: widget.studentId, studentName: widget.studentName, ), ), ); } // 2๏ธโƒฃ [๊ธฐ์กด ๋™์ผ] ๋งˆ์Šคํ„ฐ ๊ณ„์ • ์ „์šฉ ํšŒ์› ์‚ญ์ œ ๋ฒ„ํŠผ ๋™์ž‘ Future _deleteUser(String userId) async { final (_, message) = await _controller.deleteUser(userId); if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); } // 3๏ธโƒฃ [๊ธฐ์กด ๋™์ผ] ํ•™๋ฒˆ/๊ต์ง์› ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ๋ฐ›๋Š” ๋ชจ๋˜ ํŒ์—…์ฐฝ(Dialog) 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), ), ), ], ); }, ); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _controller, builder: (context, _) { final bool isDeveloper = widget.studentId == "2061"; return Scaffold( backgroundColor: Colors.grey[100], appBar: AppBar( title: Text( isDeveloper ? '๐Ÿ‘‘ MASTER CONTROL' : '๐ŸŽ“ STUDENT PORTAL', style: const TextStyle( fontWeight: FontWeight.bold, letterSpacing: 1.2, ), ), centerTitle: true, backgroundColor: isDeveloper ? Colors.deepPurple[700] : Colors.indigo[700], 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: isDeveloper ? Colors.deepPurple[700] : Colors.indigo[700], 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: isDeveloper ? Colors.deepPurple[50] : Colors.indigo[50], child: Icon( isDeveloper ? Icons.admin_panel_settings_rounded : Icons.school_rounded, size: 32, color: isDeveloper ? Colors.deepPurple : Colors.indigo, ), ), ), const SizedBox(width: 18), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${widget.studentName} ๋‹˜', style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 4), Text( isDeveloper ? '์ตœ๊ณ  ๊ด€๋ฆฌ ๊ถŒํ•œ ํ™œ์„ฑํ™”๋จ' : 'ํ•™๋ฒˆ: ${widget.studentId} | ์ธ์ฆ ์™„๋ฃŒ', 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: isDeveloper ? Colors.deepPurple : Colors.indigo, 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: [ // ๐Ÿ†• [๋ณ€๊ฒฝ ์ง€์ ] ๊ธฐ๊ธฐ๊ฐ€ ์ผ์น˜ํ•˜๋ฉด ์ •์ƒ ํ™œ์„ฑํ™”, ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ์ž๋ฌผ์‡ Lock ์ฒ˜๋ฆฌ if (isDeveloper || widget.studentId != "2061") _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, ), // ๐Ÿ†• [์ถ”๊ฐ€ ์ง€์ ] ๊ธฐ๊ธฐ ์ผ์น˜ ์—ฌ๋ถ€ ์ƒ๊ด€์—†์ด ํƒœ๋ธ”๋ฆฟ์—์„œ๋„ ๋ˆ„๊ตฌ๋‚˜ ํ™•์ธ ๊ฐ€๋Šฅํ•œ ํ•™๊ต ์ƒํ™ฉํŒ ์นด๋“œ _buildModernCard( icon: Icons.fastfood_rounded, title: '์‹ค์‹œ๊ฐ„ ํ•™๊ต ์ƒํ™ฉ', subtitle: '๊ธ‰์‹์‹ค ์ค„ & ๋งค์  ์žฌ๊ณ  ํ™•์ธ', color: Colors.orange[700]!, onTap: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('๐Ÿ” ์‹ค์‹œ๊ฐ„ ํ•™๊ต ์ƒํ™ฉ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.'), ), ); }, ), if (isDeveloper) _buildModernCard( icon: Icons.monitor_heart_rounded, title: '์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ', subtitle: '๊ต์‚ฌ์šฉ ์ˆ˜๊ฑฐ ๋ชจ๋‹ˆํ„ฐ๋ง', color: Colors.teal, onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) => const TeacherAttendancePage(), ), ), ), if (isDeveloper) _buildModernCard( icon: Icons.terminal_rounded, title: '์„œ๋ฒ„ DB ์ œ์–ด', subtitle: '์‹œ์Šคํ…œ ์›๊ฒฉ ์ดˆ๊ธฐํ™”', color: Colors.amber[800]!, onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) => const AdminDashboard(), ), ), ), if (isDeveloper) _buildModernCard( icon: Icons.add_moderator_rounded, title: 'ํ•™์ƒ ๊ณ„์ • ๊ด€๋ฆฌ', subtitle: 'UUID ๋ฆฌ์…‹ ๋ฐ ์Šน์ธ', color: Colors.purple, onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) => const StudentManagementScreen(), ), ), ), if (isDeveloper) _buildModernCard( icon: Icons.delete_sweep_rounded, title: '๊ณ„์ • ๊ฐ•์ œ ์‚ญ์ œ', subtitle: 'ํ•™์ƒ ๋ฐ ๊ต์‚ฌ DB ์‚ญ์ œ', color: Colors.red[600]!, onTap: () => _showDeleteUserDialog(context), ), if (isDeveloper) _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), ], ), ), ); }, ); } // 5๏ธโƒฃ [์นด๋“œ ๋””์ž์ธ ์œ„์ ฏ] - ๊ธฐ์กด ํ˜•ํƒœ ์™„์ „ ๋ณด์กด 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: [ Text( title, style: const TextStyle( fontSize: 16, 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, ), ), ], ), ], ), ), ); } }