실시간 출석 현황: 검정 배너 제거 + 기능들을 런치패드 메뉴로 통합

- 상단 검정 AppBar 대신 투명 배경 + 가운데 둥근 알약 제목으로 변경
- 새로고침/자습실 출석시간 설정/반출 허용 시간 설정/하교 처리/테스트용
  초기화를 "자습실 시간 설정 메뉴" 하나로 모아서, 메인 대시보드의
  로그아웃/설정 메뉴와 같은 런치패드 스타일(배경 블러) 오버레이로 표시
- 데스크탑은 사이드바에 전용 버튼을 추가하고, 폰에서는 앱바의 "..."
  아이콘으로 같은 메뉴에 접근 (모바일 기능 접근성 유지)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 09:31:17 +09:00
co-authored by Claude Sonnet 5
parent 2d0768da73
commit 20f751b65e
+212 -55
View File
@@ -205,66 +205,36 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
return Scaffold( return Scaffold(
backgroundColor: AppPalette.mist, backgroundColor: AppPalette.mist,
// 🪶 검정 배너 대신 가운데에 둥근 제목 알약만 띄우고, 나머지 기능은 전부
// "자습실 시간 설정 메뉴"(런치패드 스타일 오버레이) 하나로 모은다.
// 폰에서는 사이드바가 없으니 여기 아이콘으로도 같은 메뉴를 열 수 있게 둔다.
appBar: AppBar( appBar: AppBar(
title: const Text( backgroundColor: Colors.transparent,
'실시간 출석 현황', foregroundColor: AppPalette.ink,
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: AppPalette.ink,
foregroundColor: Colors.white,
elevation: 0, elevation: 0,
centerTitle: true,
title: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: AppPalette.ink,
borderRadius: BorderRadius.circular(999),
),
child: const Text(
'실시간 출석 현황',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
actions: [ actions: [
IconButton( IconButton(
onPressed: (_controller.isLoading || _controller.isRefreshing) icon: const Icon(Icons.more_horiz_rounded),
? null tooltip: '자습실 시간 설정 메뉴',
: _controller.manualRefresh, onPressed: _showTimeSettingsMenu,
icon: _controller.isRefreshing
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
), ),
) const SizedBox(width: 4),
: const Icon(Icons.refresh_rounded),
tooltip: '새로고침',
),
TextButton.icon(
onPressed: _showAttendanceTimeDialog,
icon: const Icon(
Icons.access_time_rounded,
color: Colors.white,
),
label: Text(
_controller.attendanceTime != null
? '출석시간 ${_controller.attendanceTime}'
: '자습실 출석시간 설정',
style: const TextStyle(color: Colors.white),
),
),
TextButton.icon(
onPressed: _showPermissionWindowDialog,
icon: const Icon(Icons.timer_outlined, color: Colors.white),
label: const Text(
'반출 허용 시간 설정',
style: TextStyle(color: Colors.white),
),
),
TextButton.icon(
onPressed: _showDismissalConfirmDialog,
icon: const Icon(Icons.school_rounded, color: Colors.white),
label: const Text('하교', style: TextStyle(color: Colors.white)),
),
IconButton(
onPressed: _showTestResetConfirmDialog,
icon: const Icon(
Icons.bug_report_outlined,
color: Colors.white70,
),
tooltip: '테스트용: 허용시간 초기화',
),
const SizedBox(width: 8),
], ],
), ),
body: _controller.isLoading body: _controller.isLoading
@@ -567,11 +537,49 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
_sidebarStatPill('미출석 학생수', absent, 'ABSENT'), _sidebarStatPill('미출석 학생수', absent, 'ABSENT'),
const SizedBox(height: 20), const SizedBox(height: 20),
_sidebarFilterButton(), _sidebarFilterButton(),
const SizedBox(height: 10),
_sidebarTimeSettingsButton(),
], ],
), ),
); );
} }
Widget _sidebarTimeSettingsButton() {
return InkWell(
onTap: _showTimeSettingsMenu,
borderRadius: BorderRadius.circular(28),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
decoration: BoxDecoration(
color: AppPalette.paper,
borderRadius: BorderRadius.circular(28),
border: Border.all(color: AppPalette.sage),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.access_time_rounded,
size: 18,
color: AppPalette.ink,
),
const SizedBox(width: 8),
const Flexible(
child: Text(
'자습실 시간 설정 메뉴',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppPalette.ink,
),
),
),
],
),
),
);
}
Widget _sidebarStatPill(String label, int count, String filterValue) { Widget _sidebarStatPill(String label, int count, String filterValue) {
final bool selected = _controller.filterType == filterValue; final bool selected = _controller.filterType == filterValue;
return InkWell( return InkWell(
@@ -705,6 +713,42 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
); );
} }
// 🚀 "자습실 시간 설정 메뉴" — 새로고침/출석시간/반출허용/하교/테스트리셋을
// 전부 여기 하나로 모아서, 메인 대시보드의 로그아웃/설정 메뉴와 똑같은
// 런치패드 스타일(배경 블러 유지) 오버레이로 띄운다.
Future<void> _showTimeSettingsMenu() async {
final action = await showLaunchpadMenu<String>(
context: context,
builder: (context) => Center(
child: _TimeSettingsMenuCard(
isRefreshing: _controller.isRefreshing,
attendanceTimeLabel: _controller.attendanceTime != null
? '출석시간 ${_controller.attendanceTime}'
: '자습실 출석시간 설정',
onPick: (value) => Navigator.pop(context, value),
),
),
);
if (!mounted || action == null) return;
switch (action) {
case 'refresh':
_controller.manualRefresh();
break;
case 'attendance_time':
_showAttendanceTimeDialog();
break;
case 'permission_window':
_showPermissionWindowDialog();
break;
case 'dismissal':
_showDismissalConfirmDialog();
break;
case 'test_reset':
_showTestResetConfirmDialog();
break;
}
}
// 🪑 좌석표처럼 촘촘하게 학생 한 명씩을 작은 칸에 담는 그리드. // 🪑 좌석표처럼 촘촘하게 학생 한 명씩을 작은 칸에 담는 그리드.
Widget _buildDenseSeatGrid(List<Map<String, dynamic>> students) { Widget _buildDenseSeatGrid(List<Map<String, dynamic>> students) {
if (students.isEmpty) { if (students.isEmpty) {
@@ -915,3 +959,116 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
); );
} }
} }
// 🚀 "자습실 시간 설정 메뉴" 카드 — 메인 대시보드 로그아웃/설정 메뉴와 같은 톤으로 통일.
class _TimeSettingsMenuCard extends StatelessWidget {
final bool isRefreshing;
final String attendanceTimeLabel;
final ValueChanged<String> onPick;
const _TimeSettingsMenuCard({
required this.isRefreshing,
required this.attendanceTimeLabel,
required this.onPick,
});
@override
Widget build(BuildContext context) {
return Material(
color: AppPalette.paper,
elevation: 8,
shadowColor: Colors.black.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20),
child: IntrinsicWidth(
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 220),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_TimeMenuRow(
icon: isRefreshing ? null : Icons.refresh_rounded,
label: '새로고침',
onTap: () => onPick('refresh'),
),
const Divider(height: 1, color: AppPalette.sage),
_TimeMenuRow(
icon: Icons.access_time_rounded,
label: attendanceTimeLabel,
onTap: () => onPick('attendance_time'),
),
const Divider(height: 1, color: AppPalette.sage),
_TimeMenuRow(
icon: Icons.timer_outlined,
label: '반출 허용 시간 설정',
onTap: () => onPick('permission_window'),
),
const Divider(height: 1, color: AppPalette.sage),
_TimeMenuRow(
icon: Icons.school_rounded,
label: '하교 처리',
onTap: () => onPick('dismissal'),
),
const Divider(height: 1, color: AppPalette.sage),
_TimeMenuRow(
icon: Icons.bug_report_outlined,
label: '테스트용: 허용시간 초기화',
dim: true,
onTap: () => onPick('test_reset'),
),
],
),
),
),
);
}
}
class _TimeMenuRow extends StatelessWidget {
final IconData? icon;
final String label;
final VoidCallback onTap;
final bool dim;
const _TimeMenuRow({
required this.icon,
required this.label,
required this.onTap,
this.dim = false,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 20,
height: 20,
child: icon == null
? const CircularProgressIndicator(strokeWidth: 2)
: Icon(
icon,
size: 20,
color: dim ? Colors.grey : AppPalette.ink,
),
),
const SizedBox(width: 12),
Flexible(
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.w600,
color: dim ? Colors.grey[600] : AppPalette.ink,
),
),
),
],
),
),
);
}
}