실시간 출석 현황: 검정 배너 제거 + 기능들을 런치패드 메뉴로 통합
- 상단 검정 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(
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user