// ๐Ÿ”‘ ๋กœ๊ทธ์ธ ํ™”๋ฉด. ํ•™๋ฒˆ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ธ์ฆ, ์ตœ์ดˆ ๋กœ๊ทธ์ธ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ, ๊ถŒํ•œ(ํ•™์ƒ/๊ต์‚ฌ/๊ด€๋ฆฌ์ž)๋ณ„ ํ™”๋ฉด ๋ถ„๊ธฐ๋ฅผ ๋‹ด๋‹นํ•œ๋‹ค. import 'dart:convert'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:firebase_messaging/firebase_messaging.dart'; import '../config.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 TextEditingController _idController = TextEditingController(); final TextEditingController _pwController = TextEditingController(); bool _isLoading = false; Future _login() async { String currentInputId = _idController.text.trim(); String currentInputPw = _pwController.text.trim(); if (currentInputId.isEmpty || currentInputPw.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('โš ๏ธ ํ•™๋ฒˆ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.'))); return; } // ๐Ÿ”ฅ [๊ฐœ๋ฐœ์ž ๋งˆ์Šคํ„ฐ ๊ณ„์ •] if (currentInputId == "2061") { if (currentInputPw == "happy9642!") { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('๐Ÿ‘‘ ๊ฐœ๋ฐœ์ž ์ตœ๊ณ  ๊ถŒํ•œ์œผ๋กœ ๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')), ); Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => const StudentDashboard( studentId: "2061", studentName: "ํ›—์ถง๊ฐ€๋ฃป", isDeviceMatched: true, // ๐Ÿ†• [์ˆ˜์ •] ๋งˆ์Šคํ„ฐ ๊ณ„์ •์€ ์–ธ์ œ๋‚˜ ํ”„๋ฆฌํŒจ์Šค์ด๋ฏ€๋กœ true ๋ณ€๊ฒฝ! ), ), ); return; } else { _showErrorDialog('๊ฐœ๋ฐœ์ž ๊ณ„์ •์˜ ๋งˆ์Šคํ„ฐ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'); return; } } setState(() => _isLoading = true); try { String deviceUuid = await getDeviceUuid(); String fcmToken = ""; try { fcmToken = kIsWeb ? await FirebaseMessaging.instance.getToken( vapidKey: webVapidKey, ) ?? "" : await FirebaseMessaging.instance.getToken() ?? ""; print("๋ฐœ๊ธ‰๋œ FCM ํ† ํฐ: $fcmToken"); } catch (e) { print("FCM ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ (ํŒŒ์ด์–ด๋ฒ ์ด์Šค ๋ฏธ์„ค์ •): $e"); } final response = await http.post( Uri.parse('$baseUrl/login'), headers: {"Content-Type": "application/json"}, body: jsonEncode({ "studentId": currentInputId, "password": currentInputPw, "deviceUuid": deviceUuid, "fcmToken": fcmToken, }), ); final resData = jsonDecode(utf8.decode(response.bodyBytes)); if (response.statusCode == 200) { print("๐Ÿšจ ์„œ๋ฒ„ ์‘๋‹ต ๋ฐ์ดํ„ฐ: $resData"); String role = ''; String name = ''; String studentId = ''; int isFirstLogin = 0; var userObj = resData['user']; if (userObj != null) { role = userObj['role']?.toString() ?? ''; name = userObj['name']?.toString() ?? userObj['studentName']?.toString() ?? userObj['student_name']?.toString() ?? ''; studentId = userObj['studentId']?.toString() ?? userObj['student_id']?.toString() ?? currentInputId; var rawFirst = userObj['isFirstLogin'] ?? userObj['is_first_login']; isFirstLogin = (rawFirst is bool) ? (rawFirst ? 1 : 0) : (int.tryParse(rawFirst.toString()) ?? 0); } else { role = resData['role']?.toString() ?? ''; name = resData['name']?.toString() ?? resData['studentName']?.toString() ?? ''; studentId = resData['studentId']?.toString() ?? currentInputId; var rawFirst = resData['isFirstLogin'] ?? resData['is_first_login']; isFirstLogin = (rawFirst is bool) ? (rawFirst ? 1 : 0) : (int.tryParse(rawFirst.toString()) ?? 0); } if (name.trim().isEmpty) { name = "์•Œ์ˆ˜์—†์Œ"; } // ๐Ÿ†• [์ถ”๊ฐ€] ์„œ๋ฒ„ ์‘๋‹ต ๋ฐ์ดํ„ฐ์—์„œ ๊ธฐ๊ธฐ ์ผ์น˜ ์—ฌ๋ถ€ ์ถ”์ถœํ•˜๊ธฐ // (์„œ๋ฒ„๊ฐ€ ์ฃผ๋Š” Key ์ด๋ฆ„์— ๋งž์ถฐ์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์•„๋ž˜ ํ•ญ๋ชฉ ์ค‘ ๋งž๋Š” ๊ฒŒ ์•Œ์•„์„œ ๋“ค์–ด๊ฐ) bool isDeviceMatched = resData['isDeviceMatched'] ?? resData['isUuidMatched'] ?? resData['is_matched'] ?? (userObj != null ? (userObj['isDeviceMatched'] ?? userObj['is_matched'] ?? false) : false); if (isFirstLogin == 1) { _showFirstLoginPasswordDialog( studentId, name, role, isDeviceMatched, ); // ๐Ÿ†• ๋งค๊ฐœ๋ณ€์ˆ˜ ์ถ”๊ฐ€ return; } ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('โœ… $name๋‹˜ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!'))); _navigateBasedOnRole( role, studentId, name, isDeviceMatched, ); // ๐Ÿ†• [์ˆ˜์ •] ๊ธฐ๊ธฐ ์ธ์ฆ ๊ฒฐ๊ณผ ์ „๋‹ฌ } else { String errorMsg = '๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'; if (resData is Map) { errorMsg = resData['detail']?.toString() ?? resData['message']?.toString() ?? errorMsg; } _showErrorDialog(errorMsg); } } catch (e) { _showErrorDialog('์„œ๋ฒ„์™€ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”!\n($e)'); } finally { setState(() => _isLoading = false); } } // ๐Ÿ› ๏ธ ์ดˆ๊ธฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ํŒ์—…์ฐฝ (๊ธฐ๊ธฐ ๋งค์นญ ๋ฐ์ดํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€) 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); try { final response = await http.post( Uri.parse('$baseUrl/api/users/change-password'), headers: {"Content-Type": "application/json"}, body: jsonEncode({ "studentId": studentId, "newPassword": newPassword, }), ); final resData = jsonDecode( utf8.decode(response.bodyBytes), ); setDialogState(() => isUpdating = false); if (response.statusCode == 200 && resData['status'] == 'success') { Navigator.pop(dialogContext); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!'), ), ); _navigateBasedOnRole( role, studentId, name, isDeviceMatched, ); // ๐Ÿ†• ์ˆ˜์ • } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('โŒ ์‹คํŒจ: ${resData['message']}'), ), ); } } catch (e) { setDialogState(() => isUpdating = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('์„œ๋ฒ„ ์—๋Ÿฌ ๋ฐœ์ƒ: $e')), ); } }, child: const Text('๋ณ€๊ฒฝํ•˜๊ณ  ์‹œ์ž‘ํ•˜๊ธฐ'), ), ], ); }, ); }, ); } // ๐Ÿ†• [์ˆ˜์ •] ๊ถŒํ•œ๋ณ„ ํ™”๋ฉด ์ด๋™์‹œ ๊ธฐ๊ธฐ ์ผ์น˜ ์—ฌ๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ(`isDeviceMatched`) ์ˆ˜์‹  ๋ฐ ๋Œ€์‹œ๋ณด๋“œ ์ „๋‹ฌ void _navigateBasedOnRole( String role, String studentId, String name, bool isDeviceMatched, ) { if (role == 'teacher') { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const TeacherDashboard()), ); } 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, // ๐Ÿ†• [์ˆ˜์ •] ๋”์ด์ƒ false ๊ณ ์ •์ด ์•„๋‹ˆ๋ผ ์„œ๋ฒ„ ํŒ๋‹จ ๊ฒฐ๊ณผ ์ „์†ก! ), ), ); } } 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 Scaffold( backgroundColor: Colors.grey[50], body: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.school, size: 80, color: Colors.indigo), const SizedBox(height: 16), const Text( '๋ฐฑ์‚ฐ๊ณ ๋“ฑํ•™๊ต ๋ชจ๋‹ˆํ„ฐ', style: 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), _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(), ), ), ], ), ], ), ), ), ); } }