lib/ 코드를 UI(lib/ui/)와 기능(lib/function/)으로 분리
- 화면마다 위젯/스타일만 담당하는 UI 파일과, 서버통신/상태/파생로직만 담당하는 ChangeNotifier 컨트롤러 파일로 1:1 분리 (lib/screens/ -> lib/ui/ + lib/function/) - UI는 ListenableBuilder로 컨트롤러를 구독해서 재렌더링, 버튼은 컨트롤러 메서드만 호출 - 다이얼로그/스낵바 등 위젯 코드는 전부 UI 파일에 남기고, 컨트롤러는 결과값(성공여부+메시지) 또는 콜백으로만 UI와 통신 (BuildContext/Widget 의존성 없음) - 디자인/레이아웃은 기존과 완전히 동일하게 유지 (순수 코드 재배치) - change_password_screen.dart, debug_pocket_main.dart(미사용 파일)는 깨지지 않게 import 경로만 갱신하고 리팩터링은 보류
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
// 🔑 로그인 화면 (UI 전용). 입력폼/다이얼로그/화면 이동만 담당한다.
|
||||
// 인증 로직/서버 통신은 lib/function/login_controller.dart가 담당한다.
|
||||
import 'package:flutter/material.dart';
|
||||
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),
|
||||
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),
|
||||
_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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user