실시간 출석 현황: 검정 배너 제거 + 기능들을 런치패드 메뉴로 통합
- 상단 검정 AppBar 대신 투명 배경 + 가운데 둥근 알약 제목으로 변경 - 새로고침/자습실 출석시간 설정/반출 허용 시간 설정/하교 처리/테스트용 초기화를 "자습실 시간 설정 메뉴" 하나로 모아서, 메인 대시보드의 로그아웃/설정 메뉴와 같은 런치패드 스타일(배경 블러) 오버레이로 표시 - 데스크탑은 사이드바에 전용 버튼을 추가하고, 폰에서는 앱바의 "..." 아이콘으로 같은 메뉴에 접근 (모바일 기능 접근성 유지) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -205,66 +205,36 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppPalette.mist,
|
||||
// 🪶 검정 배너 대신 가운데에 둥근 제목 알약만 띄우고, 나머지 기능은 전부
|
||||
// "자습실 시간 설정 메뉴"(런치패드 스타일 오버레이) 하나로 모은다.
|
||||
// 폰에서는 사이드바가 없으니 여기 아이콘으로도 같은 메뉴를 열 수 있게 둔다.
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'실시간 출석 현황',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: AppPalette.ink,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: AppPalette.ink,
|
||||
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: [
|
||||
IconButton(
|
||||
onPressed: (_controller.isLoading || _controller.isRefreshing)
|
||||
? null
|
||||
: _controller.manualRefresh,
|
||||
icon: _controller.isRefreshing
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.refresh_rounded),
|
||||
tooltip: '새로고침',
|
||||
icon: const Icon(Icons.more_horiz_rounded),
|
||||
tooltip: '자습실 시간 설정 메뉴',
|
||||
onPressed: _showTimeSettingsMenu,
|
||||
),
|
||||
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),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
body: _controller.isLoading
|
||||
@@ -567,11 +537,49 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
_sidebarStatPill('미출석 학생수', absent, 'ABSENT'),
|
||||
const SizedBox(height: 20),
|
||||
_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) {
|
||||
final bool selected = _controller.filterType == filterValue;
|
||||
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) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user