// ๐Ÿ”‘ ๋กœ๊ทธ์ธ ํ™”๋ฉด (UI ์ „์šฉ). ์ž…๋ ฅํผ/๋‹ค์ด์–ผ๋กœ๊ทธ/ํ™”๋ฉด ์ด๋™๋งŒ ๋‹ด๋‹นํ•œ๋‹ค. // ์ธ์ฆ ๋กœ์ง/์„œ๋ฒ„ ํ†ต์‹ ์€ lib/function/login_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; 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'; // ------------------------------------------------------------- // 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(); bool _keepLoggedIn = true; @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: if (_keepLoggedIn) { await SessionStore.save( SavedSession( userId: result.studentId!, userName: result.name!, role: 'student', isDeviceMatched: true, ), ); } else { await SessionStore.clear(); } if (!mounted) return; AppNotice.show(context, '๊ฐœ๋ฐœ์ž ์ตœ๊ณ  ๊ถŒํ•œ์œผ๋กœ ๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => MainDashboard( userId: result.studentId!, userName: result.name!, role: 'student', isDeviceMatched: true, ), ), ); break; case LoginOutcome.needsPasswordChange: _showFirstLoginPasswordDialog( result.studentId!, result.name!, result.role!, result.isDeviceMatched, result.grade, ); break; case LoginOutcome.success: AppNotice.show(context, '${result.name}๋‹˜ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!'); _navigateBasedOnRole( result.role!, result.studentId!, result.name!, result.isDeviceMatched, result.grade, ); break; } } // ๐Ÿ› ๏ธ ์ดˆ๊ธฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ํŒ์—…์ฐฝ (๊ธฐ๊ธฐ ๋งค์นญ ๋ฐ์ดํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€) void _showFirstLoginPasswordDialog( String studentId, String name, String role, bool isDeviceMatched, int? grade, ) { 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: AppPalette.ink, foregroundColor: Colors.white, ), onPressed: () async { String newPassword = newPwController.text.trim(); if (newPassword.isEmpty) { AppNotice.show(context, '์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.'); return; } setDialogState(() => isUpdating = true); final (success, message) = await _controller .changePassword( studentId: studentId, newPassword: newPassword, ); setDialogState(() => isUpdating = false); if (success) { Navigator.pop(dialogContext); AppNotice.show(context, message); _navigateBasedOnRole( role, studentId, name, isDeviceMatched, grade, ); } else { AppNotice.show(context, message); } }, child: const Text('๋ณ€๊ฒฝํ•˜๊ณ  ์‹œ์ž‘ํ•˜๊ธฐ'), ), ], ); }, ); }, ); } // ๐Ÿ†• ๊ถŒํ•œ๋ณ„ ํ™”๋ฉด ์ด๋™์‹œ ๊ธฐ๊ธฐ ์ผ์น˜ ์—ฌ๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ(`isDeviceMatched`) ์ˆ˜์‹  ๋ฐ ๋Œ€์‹œ๋ณด๋“œ ์ „๋‹ฌ Future _navigateBasedOnRole( String role, String studentId, String name, bool isDeviceMatched, int? grade, ) async { if (_keepLoggedIn) { await SessionStore.save( SavedSession( userId: studentId, userName: name, role: role, isDeviceMatched: isDeviceMatched, grade: grade, ), ); } else { await SessionStore.clear(); } if (!mounted) return; Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => MainDashboard( userId: studentId, userName: name, role: role, isDeviceMatched: isDeviceMatched, grade: grade, ), ), ); } 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 { AppNotice.show(context, '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'); } }, 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 { AppNotice.show(context, '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'); } }, child: const Text('์ธ์ฆํ•˜๊ธฐ'), ), ], ); }, ); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _controller, builder: (context, _) { return Scaffold( backgroundColor: AppPalette.mist, 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: AppPalette.ink), const SizedBox(height: 16), Text( '$schoolName ๋ชจ๋‹ˆํ„ฐ', style: const TextStyle( fontSize: 26, fontWeight: FontWeight.bold, color: AppPalette.ink, ), ), 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: 8), InkWell( onTap: () => setState(() => _keepLoggedIn = !_keepLoggedIn), borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ Checkbox( value: _keepLoggedIn, activeColor: AppPalette.ink, onChanged: (value) => setState(() => _keepLoggedIn = value ?? true), ), const Text( '๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€', style: TextStyle(color: AppPalette.ink), ), ], ), ), ), const SizedBox(height: 16), _controller.isLoading ? const CircularProgressIndicator() : SizedBox( width: double.infinity, height: 55, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, 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: AppPalette.ink, 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 MainDashboard(role: 'teacher'), ), ), TextButton.icon( icon: const Icon( Icons.settings, size: 18, color: Colors.grey, ), label: const Text( '๊ด€๋ฆฌ์ž ๊ฐ„ํŽธ์ธ์ฆ', style: TextStyle(color: Colors.grey), ), onPressed: () => _showLegacyPasswordDialog( '์‹œ์Šคํ…œ ๊ด€๋ฆฌ์ž', '4936', const MainDashboard(role: 'admin'), ), ), ], ), ], ), ), ), ), ); }, ); } }