Files
school-attendance/lib/ui/login_screen.dart
T
sihoo f9fa05f817 학교별 배포를 위해 서버주소/학교이름을 빌드타임 설정으로 전환
- config.dart의 baseUrl/webVapidKey와 학교 이름을 String.fromEnvironment로 전환
- 기본값은 기존 백산고 값 그대로라 인자 없이 빌드하면 지금과 동일하게 동작
- 다른 학교는 --dart-define=BASE_URL=...=SCHOOL_NAME=...=WEB_VAPID_KEY=...로 빌드
2026-08-06 14:36:10 +09:00

437 lines
15 KiB
Dart

// 🔑 로그인 화면 (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<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final LoginController _controller = LoginController();
final TextEditingController _idController = TextEditingController();
final TextEditingController _pwController = TextEditingController();
@override
void dispose() {
_controller.dispose();
_idController.dispose();
_pwController.dispose();
super.dispose();
}
Future<void> _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(),
),
),
],
),
],
),
),
),
),
);
},
);
}
}