선생님 대시보드 학년별 카테고리 분류 + 학생 계정 관리 버그 수정
- 실시간 출석 현황을 학년별 섹션으로 묶어서 표시 (백엔드에 grade 필드 추가)
- 학생 계정 추가 폼에 학년 선택 드롭다운 추가
- 학생 계정 생성/삭제가 실제로는 존재하지 않는 엔드포인트를 호출하던
버그 수정 (/api/users/register-student, /api/users/delete/{id} →
/api/users/create, /api/users/delete)
- 학생 대시보드(개발자 모드) 카드 그리드가 넓은 화면에서 과도하게
커지던 레이아웃 버그 수정, 웹은 5열/폰은 2열로 반응형 처리
- 버전 1.2.0+6
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,14 @@ final RegExp _logNamePattern = RegExp(r'^(.*?)\s*\(([^)]*)\)$');
|
||||
class TeacherAttendanceController extends ChangeNotifier {
|
||||
List<dynamic> _roster = []; // 전체 학생 명단 (/api/users)
|
||||
List<dynamic> _logs = []; // 출석 로그 (/api/logs)
|
||||
Map<String, dynamic> _activeViolationsByStudentId = {}; // 무단반출 중인 학생 (/api/violations/active)
|
||||
String? _dismissedAt; // 오늘 가장 최근 하교 처리 시각 (/api/dismissal/latest). 이 시각 이후 기록만 "오늘 출석"으로 표시.
|
||||
String? _attendanceTime; // 선생님이 지정한 자습실 출석시간 "HH:MM" (/api/settings/attendance-time). null이면 미설정.
|
||||
String? _globalPermissionUntil; // 하교(12시간)/반출허용시간설정으로 전체 반출이 허용된 경우 그 만료 시각 (/api/permissions/status). null이면 비활성.
|
||||
Map<String, dynamic> _activeViolationsByStudentId =
|
||||
{}; // 무단반출 중인 학생 (/api/violations/active)
|
||||
String?
|
||||
_dismissedAt; // 오늘 가장 최근 하교 처리 시각 (/api/dismissal/latest). 이 시각 이후 기록만 "오늘 출석"으로 표시.
|
||||
String?
|
||||
_attendanceTime; // 선생님이 지정한 자습실 출석시간 "HH:MM" (/api/settings/attendance-time). null이면 미설정.
|
||||
String?
|
||||
_globalPermissionUntil; // 하교(12시간)/반출허용시간설정으로 전체 반출이 허용된 경우 그 만료 시각 (/api/permissions/status). null이면 비활성.
|
||||
Timer? _timer;
|
||||
bool _isLoading = true;
|
||||
bool _isRefreshing = false; // 새로고침 버튼 클릭 시 잠깐 도는 표시용
|
||||
@@ -81,7 +85,9 @@ class TeacherAttendanceController extends ChangeNotifier {
|
||||
|
||||
Map<String, dynamic> activeViolations = {};
|
||||
if (violationsRes.statusCode == 200) {
|
||||
final violationsData = jsonDecode(utf8.decode(violationsRes.bodyBytes));
|
||||
final violationsData = jsonDecode(
|
||||
utf8.decode(violationsRes.bodyBytes),
|
||||
);
|
||||
for (final v in (violationsData['violations'] ?? [])) {
|
||||
activeViolations[v['student_id'].toString()] = v;
|
||||
}
|
||||
@@ -95,15 +101,17 @@ class TeacherAttendanceController extends ChangeNotifier {
|
||||
|
||||
String? attendanceTime;
|
||||
if (attendanceTimeRes.statusCode == 200) {
|
||||
final attendanceTimeData =
|
||||
jsonDecode(utf8.decode(attendanceTimeRes.bodyBytes));
|
||||
final attendanceTimeData = jsonDecode(
|
||||
utf8.decode(attendanceTimeRes.bodyBytes),
|
||||
);
|
||||
attendanceTime = attendanceTimeData['attendanceTime'];
|
||||
}
|
||||
|
||||
String? globalPermissionUntil;
|
||||
if (permissionStatusRes.statusCode == 200) {
|
||||
final permissionStatusData =
|
||||
jsonDecode(utf8.decode(permissionStatusRes.bodyBytes));
|
||||
final permissionStatusData = jsonDecode(
|
||||
utf8.decode(permissionStatusRes.bodyBytes),
|
||||
);
|
||||
if (permissionStatusData['active'] == true) {
|
||||
globalPermissionUntil = permissionStatusData['permittedUntil'];
|
||||
}
|
||||
@@ -272,10 +280,13 @@ class TeacherAttendanceController extends ChangeNotifier {
|
||||
final String id = u['id']?.toString() ?? '';
|
||||
final checkIn = checkIns[id];
|
||||
final violation = _activeViolationsByStudentId[id];
|
||||
final String attendanceStatus = _computeAttendanceStatus(checkIn?['time']);
|
||||
final String attendanceStatus = _computeAttendanceStatus(
|
||||
checkIn?['time'],
|
||||
);
|
||||
return {
|
||||
'studentId': id,
|
||||
'studentName': u['name']?.toString() ?? '',
|
||||
'grade': u['grade'] as int?,
|
||||
'isCheckedIn': checkIn != null,
|
||||
'pocketNumber': checkIn?['pocketNumber'],
|
||||
'checkInTime': checkIn?['time'],
|
||||
@@ -298,4 +309,29 @@ class TeacherAttendanceController extends ChangeNotifier {
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/// 🏫 [학년별 카테고리] filteredStudents를 학년(1~3학년) 순서로 묶는다.
|
||||
/// 학년이 아직 지정 안 된 학생은 맨 뒤 "미배정" 그룹으로 모인다.
|
||||
List<MapEntry<String, List<Map<String, dynamic>>>> get studentsByGrade {
|
||||
final Map<int, List<Map<String, dynamic>>> byGrade = {};
|
||||
final List<Map<String, dynamic>> ungraded = [];
|
||||
|
||||
for (final student in filteredStudents) {
|
||||
final int? grade = student['grade'] as int?;
|
||||
if (grade == null) {
|
||||
ungraded.add(student);
|
||||
} else {
|
||||
byGrade.putIfAbsent(grade, () => []).add(student);
|
||||
}
|
||||
}
|
||||
|
||||
final sortedGrades = byGrade.keys.toList()..sort();
|
||||
final groups = <MapEntry<String, List<Map<String, dynamic>>>>[
|
||||
for (final grade in sortedGrades) MapEntry('$grade학년', byGrade[grade]!),
|
||||
];
|
||||
if (ungraded.isNotEmpty) {
|
||||
groups.add(MapEntry('미배정', ungraded));
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ class TeacherStudentManagementController extends ChangeNotifier {
|
||||
required String studentId,
|
||||
required String name,
|
||||
required String password,
|
||||
int? grade,
|
||||
}) async {
|
||||
_isWorking = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
final url = Uri.parse('$baseUrl/api/users/register-student');
|
||||
final url = Uri.parse('$baseUrl/api/users/create');
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
@@ -25,6 +26,7 @@ class TeacherStudentManagementController extends ChangeNotifier {
|
||||
"studentId": studentId,
|
||||
"name": name,
|
||||
"password": password,
|
||||
"grade": grade,
|
||||
}),
|
||||
);
|
||||
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
@@ -48,9 +50,13 @@ class TeacherStudentManagementController extends ChangeNotifier {
|
||||
) async {
|
||||
_isWorking = true;
|
||||
notifyListeners();
|
||||
final url = Uri.parse('$baseUrl/api/users/delete/$studentId');
|
||||
final url = Uri.parse('$baseUrl/api/users/delete');
|
||||
try {
|
||||
final response = await http.delete(url);
|
||||
final response = await http.delete(
|
||||
url,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({"studentId": studentId}),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final resData = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
return (true, '✅ ${resData['message']}');
|
||||
|
||||
Reference in New Issue
Block a user