// ๐Ÿ”‘ ๋กœ๊ทธ์ธ ํ™”๋ฉด (UI ์ „์šฉ). ์ž…๋ ฅํผ/๋‹ค์ด์–ผ๋กœ๊ทธ/ํ™”๋ฉด ์ด๋™๋งŒ ๋‹ด๋‹นํ•œ๋‹ค. // ์ธ์ฆ ๋กœ์ง/์„œ๋ฒ„ ํ†ต์‹ ์€ lib/function/login_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../config.dart' show schoolName; import '../function/login_controller.dart'; import 'student_dashboard.dart'; import 'teacher_dashboard.dart'; import 'admin_dashboard.dart'; import 'teacher_register_screen.dart'; // ------------------------------------------------------------- // 1. ๋กœ๊ทธ์ธ ํ™”๋ฉด (LoginScreen) // ------------------------------------------------------------- class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State createState() => _LoginScreenState(); } class _LoginScreenState extends State { final LoginController _controller = LoginController(); final TextEditingController _idController = TextEditingController(); final TextEditingController _pwController = TextEditingController(); @override void dispose() { _controller.dispose(); _idController.dispose(); _pwController.dispose(); super.dispose(); } Future _login() async { final result = await _controller.login( _idController.text.trim(), _pwController.text.trim(), ); if (!mounted) return; switch (result.outcome) { case LoginOutcome.error: _showErrorDialog(result.errorMessage!); break; case LoginOutcome.masterSuccess: ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('๐Ÿ‘‘ ๊ฐœ๋ฐœ์ž ์ตœ๊ณ  ๊ถŒํ•œ์œผ๋กœ ๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')), ); Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => StudentDashboard( studentId: result.studentId!, studentName: result.name!, isDeviceMatched: true, ), ), ); break; case LoginOutcome.needsPasswordChange: _showFirstLoginPasswordDialog( result.studentId!, result.name!, result.role!, result.isDeviceMatched, ); break; case LoginOutcome.success: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('โœ… ${result.name}๋‹˜ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!'))); _navigateBasedOnRole( result.role!, result.studentId!, result.name!, result.isDeviceMatched, ); break; } } // ๐Ÿ› ๏ธ ์ดˆ๊ธฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ํŒ์—…์ฐฝ (๊ธฐ๊ธฐ ๋งค์นญ ๋ฐ์ดํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€) void _showFirstLoginPasswordDialog( String studentId, String name, String role, bool isDeviceMatched, ) { final TextEditingController newPwController = TextEditingController(); showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { bool isUpdating = false; return StatefulBuilder( builder: (context, setDialogState) { return AlertDialog( title: const Text( '๐Ÿ”’ ์ดˆ๊ธฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ', style: TextStyle(fontWeight: FontWeight.bold), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( '๋ณด์•ˆ์„ ์œ„ํ•ด ์ƒˆ๋กœ์šด ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์„ค์ •ํ•ด ์ฃผ์„ธ์š”.', style: TextStyle(color: Colors.redAccent), ), const SizedBox(height: 16), TextField( controller: newPwController, obscureText: true, decoration: const InputDecoration( labelText: '์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ', border: OutlineInputBorder(), prefixIcon: Icon(Icons.lock_reset), ), ), ], ), actions: [ isUpdating ? const Padding( padding: EdgeInsets.only(right: 20.0), child: CircularProgressIndicator(), ) : ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.indigo, foregroundColor: Colors.white, ), onPressed: () async { String newPassword = newPwController.text.trim(); if (newPassword.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('โš ๏ธ ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.'), ), ); return; } setDialogState(() => isUpdating = true); final (success, message) = await _controller .changePassword( studentId: studentId, newPassword: newPassword, ); setDialogState(() => isUpdating = false); if (success) { Navigator.pop(dialogContext); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message)), ); _navigateBasedOnRole( role, studentId, name, isDeviceMatched, ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message)), ); } }, child: const Text('๋ณ€๊ฒฝํ•˜๊ณ  ์‹œ์ž‘ํ•˜๊ธฐ'), ), ], ); }, ); }, ); } // ๐Ÿ†• ๊ถŒํ•œ๋ณ„ ํ™”๋ฉด ์ด๋™์‹œ ๊ธฐ๊ธฐ ์ผ์น˜ ์—ฌ๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ(`isDeviceMatched`) ์ˆ˜์‹  ๋ฐ ๋Œ€์‹œ๋ณด๋“œ ์ „๋‹ฌ void _navigateBasedOnRole( String role, String studentId, String name, bool isDeviceMatched, ) { if (role == 'teacher') { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => TeacherDashboard(teacherId: studentId, teacherName: name), ), ); } else if (role == 'admin') { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const AdminDashboard()), ); } else { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => StudentDashboard( studentId: studentId, studentName: name, isDeviceMatched: isDeviceMatched, ), ), ); } } void _showErrorDialog(String message) { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text( 'โš ๏ธ ์ธ์ฆ ์‹คํŒจ', style: TextStyle(fontWeight: FontWeight.bold), ), content: Text(message), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('ํ™•์ธ'), ), ], ), ); } void _showLegacyPasswordDialog( String title, String correctPassword, Widget nextPage, ) { final TextEditingController passwordController = TextEditingController(); showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { return AlertDialog( title: Text('๐Ÿ”’ $title ๊ถŒํ•œ ์ธ์ฆ (๊ธฐ์กด ๋ฐฉ์‹)'), content: TextField( controller: passwordController, obscureText: true, keyboardType: TextInputType.number, textInputAction: TextInputAction.done, onSubmitted: (value) { if (value == correctPassword) { Navigator.pop(dialogContext); Navigator.push( context, MaterialPageRoute(builder: (context) => nextPage), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('โŒ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.')), ); } }, decoration: const InputDecoration( hintText: '๋น„๋ฐ€๋ฒˆํ˜ธ 4์ž๋ฆฌ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”', border: OutlineInputBorder(), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: const Text('์ทจ์†Œ', style: TextStyle(color: Colors.grey)), ), ElevatedButton( onPressed: () { if (passwordController.text == correctPassword) { Navigator.pop(dialogContext); Navigator.push( context, MaterialPageRoute(builder: (context) => nextPage), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('โŒ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.')), ); } }, child: const Text('์ธ์ฆํ•˜๊ธฐ'), ), ], ); }, ); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _controller, builder: (context, _) { return Scaffold( backgroundColor: Colors.grey[50], body: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.school, size: 80, color: Colors.indigo), const SizedBox(height: 16), Text( '$schoolName ๋ชจ๋‹ˆํ„ฐ', style: const TextStyle( fontSize: 26, fontWeight: FontWeight.bold, color: Colors.indigo, ), ), const SizedBox(height: 8), const Text( 'ํ•™์ƒ ํŽธ์˜ ๋ฐ ํ•™๊ต์ƒํ™œ ๋„์šฐ๋ฏธ', style: TextStyle(color: Colors.grey, fontSize: 15), ), const SizedBox(height: 40), TextField( controller: _idController, textInputAction: TextInputAction.next, decoration: const InputDecoration( labelText: 'ํ•™๋ฒˆ ๋˜๋Š” ๊ต์ง์› ๋ฒˆํ˜ธ', border: OutlineInputBorder(), prefixIcon: Icon(Icons.person), ), ), const SizedBox(height: 16), TextField( controller: _pwController, obscureText: true, textInputAction: TextInputAction.done, onSubmitted: (_) => _login(), decoration: const InputDecoration( labelText: '๋น„๋ฐ€๋ฒˆํ˜ธ', border: OutlineInputBorder(), prefixIcon: Icon(Icons.lock), ), ), const SizedBox(height: 24), _controller.isLoading ? const CircularProgressIndicator() : SizedBox( width: double.infinity, height: 55, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.indigo, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), onPressed: _login, child: const Text( '๋กœ๊ทธ์ธ', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 4), TextButton( onPressed: () => Navigator.push( context, MaterialPageRoute( builder: (context) => const TeacherRegisterScreen(), ), ), child: const Text( '๐Ÿ‘จโ€๐Ÿซ ์„ ์ƒ๋‹˜์ด์‹ ๊ฐ€์š”? ๊ต์‚ฌ ํšŒ์›๊ฐ€์ž… ํ•˜๊ธฐ', style: TextStyle( color: Colors.indigo, fontWeight: FontWeight.bold, ), ), ), const Divider(height: 40), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton.icon( icon: const Icon( Icons.gavel, size: 18, color: Colors.grey, ), label: const Text( '๊ต์‚ฌ ๊ฐ„ํŽธ์ธ์ฆ', style: TextStyle(color: Colors.grey), ), onPressed: () => _showLegacyPasswordDialog( '์„ ์ƒ๋‹˜', '1234', const TeacherDashboard(), ), ), TextButton.icon( icon: const Icon( Icons.settings, size: 18, color: Colors.grey, ), label: const Text( '๊ด€๋ฆฌ์ž ๊ฐ„ํŽธ์ธ์ฆ', style: TextStyle(color: Colors.grey), ), onPressed: () => _showLegacyPasswordDialog( '์‹œ์Šคํ…œ ๊ด€๋ฆฌ์ž', '4936', const AdminDashboard(), ), ), ], ), ], ), ), ), ), ); }, ); } }