3 Commits
Author SHA1 Message Date
sihoo 4e12b279e2 선생님 정상 로그인 시 대시보드 이름 표시 버그 수정 + 앱 버전 1.1.4
- 로그인 성공 후 TeacherDashboard로 이동할 때 실제 이름/학번을 안 넘겨줘서
  항상 "간편인증 선생"으로 표시되던 문제 수정
2026-08-05 14:01:50 +09:00
sihoo d864927957 전체 반출 허용 중일 때 대시보드에 배너 표시 추가 2026-08-04 14:55:22 +09:00
sihoo 61aedac312 한 번도 태깅 안 한 학생은 미제출 대신 미등록으로 표시 2026-08-04 14:47:15 +09:00
3 changed files with 72 additions and 4 deletions
+4 -1
View File
@@ -308,7 +308,10 @@ class _LoginScreenState extends State<LoginScreen> {
if (role == 'teacher') {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const TeacherDashboard()),
MaterialPageRoute(
builder: (context) =>
TeacherDashboard(teacherId: studentId, teacherName: name),
),
);
} else if (role == 'admin') {
Navigator.pushReplacement(
+67 -2
View File
@@ -26,6 +26,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
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; // 새로고침 버튼 클릭 시 잠깐 도는 표시용
@@ -58,6 +59,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
http.get(Uri.parse('$baseUrl/api/violations/active')),
http.get(Uri.parse('$baseUrl/api/dismissal/latest')),
http.get(Uri.parse('$baseUrl/api/settings/attendance-time')),
http.get(Uri.parse('$baseUrl/api/permissions/status')),
]);
final usersRes = results[0];
@@ -65,6 +67,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
final violationsRes = results[2];
final dismissalRes = results[3];
final attendanceTimeRes = results[4];
final permissionStatusRes = results[5];
if (usersRes.statusCode == 200 && logsRes.statusCode == 200) {
final usersData = jsonDecode(utf8.decode(usersRes.bodyBytes));
@@ -91,12 +94,22 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
attendanceTime = attendanceTimeData['attendanceTime'];
}
String? globalPermissionUntil;
if (permissionStatusRes.statusCode == 200) {
final permissionStatusData =
jsonDecode(utf8.decode(permissionStatusRes.bodyBytes));
if (permissionStatusData['active'] == true) {
globalPermissionUntil = permissionStatusData['permittedUntil'];
}
}
setState(() {
_roster = usersData['users'] ?? [];
_logs = logsData['logs'] ?? [];
_activeViolationsByStudentId = activeViolations;
_dismissedAt = dismissedAt;
_attendanceTime = attendanceTime;
_globalPermissionUntil = globalPermissionUntil;
_isLoading = false;
});
} else {
@@ -391,6 +404,17 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
return result;
}
/// 지금까지(오늘 이전 포함) 단 한 번이라도 태깅한 적 있는 학생 학번 집합.
/// (오늘 미제출인 학생 중에서도 "한 번도 태깅 안 해본 애"를 구분하기 위함)
Set<String> get _everCheckedInStudentIds {
final Set<String> ids = {};
for (final log in _logs) {
final String studentId = log['student_id']?.toString() ?? '';
if (studentId.isNotEmpty) ids.add(studentId);
}
return ids;
}
/// ⏰ 태깅 시간과 지정된 자습실 출석시간을 비교해 'NONE' / 'PENDING' / 'COMPLETE'를 반환한다.
/// - NONE: 아직 태깅 안 함
/// - PENDING: 태깅은 했지만 지정된 출석시간 이전이라 아직 "출석 완료"로 안 침
@@ -406,6 +430,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
List<Map<String, dynamic>> get _combinedStudentStatus {
final checkIns = _todaysCheckInsByStudentId;
final everCheckedIn = _everCheckedInStudentIds;
return _roster.map((u) {
final String id = u['id']?.toString() ?? '';
final checkIn = checkIns[id];
@@ -419,6 +444,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
'checkInTime': checkIn?['time'],
'attendanceStatus': attendanceStatus,
'isAttendanceComplete': attendanceStatus == 'COMPLETE',
'hasEverCheckedIn': everCheckedIn.contains(id),
'hasActiveViolation': violation != null,
'violationPocket': violation?['pocket_number'],
'violationTime': violation?['time'],
@@ -521,6 +547,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
return Column(
children: [
_buildSummaryCards(total, checkedIn, absent),
_buildPermissionBanner(),
_buildFilterChips(),
Expanded(
child: _filteredStudents.isEmpty
@@ -608,7 +635,9 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
? '학번: ${student['studentId']} | ⏳ 출석 미완료 (제출: ${student['checkInTime']})'
: (isComplete
? '학번: ${student['studentId']} | 제출시간: ${student['checkInTime']}'
: '학번: ${student['studentId']} | 미제출')),
: (student['hasEverCheckedIn'] == true
? '학번: ${student['studentId']} | 미제출'
: '학번: ${student['studentId']} | 미등록'))),
style: TextStyle(
color: hasViolation
? Colors.red[700]
@@ -721,6 +750,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
],
),
const SizedBox(height: 20),
_buildPermissionBanner(),
_buildFilterChips(),
const SizedBox(height: 12),
Expanded(
@@ -809,7 +839,9 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
? '⏳ 출석 미완료 (제출: ${student['checkInTime']})'
: (isComplete
? '${student['checkInTime']}'
: '미제출')),
: (student['hasEverCheckedIn'] == true
? '미제출'
: '미등록'))),
style: TextStyle(
color: hasViolation
? Colors.red[700]
@@ -971,6 +1003,39 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
}
/// 🔘 필터 칩버튼 (전체 / 출석자 / 미출석자)
/// 🔓 하교(12시간)/반출 허용 시간 설정으로 지금 전체 반출이 허용 중이면 눈에 띄게 배너로 알려준다.
/// (허용 중일 땐 무단반출을 감지해도 대시보드에 뜨지 않기 때문에, 왜 안 뜨는지 헷갈리지 않게 하기 위함)
Widget _buildPermissionBanner() {
if (_globalPermissionUntil == null) return const SizedBox.shrink();
return Container(
width: double.infinity,
margin: const EdgeInsets.fromLTRB(16, 8, 16, 0),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Colors.amber[100],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.amber[400]!),
),
child: Row(
children: [
Icon(Icons.lock_open_rounded, color: Colors.amber[800], size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
'🔓 지금 전체 반출 허용 중입니다 ($_globalPermissionUntil 까지) — 이 시간 동안은 무단반출 경고가 뜨지 않아요.',
style: TextStyle(
color: Colors.amber[900],
fontWeight: FontWeight.bold,
fontSize: 12.5,
),
),
),
],
),
);
}
Widget _buildFilterChips() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.1.3+4
version: 1.1.4+5
environment:
sdk: ^3.12.2