From 20f751b65e989d20d438c4c1a4c9d322142e4afe Mon Sep 17 00:00:00 2001 From: sihoo Date: Tue, 8 Sep 2026 09:31:17 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=B6=9C?= =?UTF-8?q?=EC=84=9D=20=ED=98=84=ED=99=A9:=20=EA=B2=80=EC=A0=95=20?= =?UTF-8?q?=EB=B0=B0=EB=84=88=20=EC=A0=9C=EA=B1=B0=20+=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EB=93=A4=EC=9D=84=20=EB=9F=B0=EC=B9=98=ED=8C=A8?= =?UTF-8?q?=EB=93=9C=20=EB=A9=94=EB=89=B4=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상단 검정 AppBar 대신 투명 배경 + 가운데 둥근 알약 제목으로 변경 - 새로고침/자습실 출석시간 설정/반출 허용 시간 설정/하교 처리/테스트용 초기화를 "자습실 시간 설정 메뉴" 하나로 모아서, 메인 대시보드의 로그아웃/설정 메뉴와 같은 런치패드 스타일(배경 블러) 오버레이로 표시 - 데스크탑은 사이드바에 전용 버튼을 추가하고, 폰에서는 앱바의 "..." 아이콘으로 같은 메뉴에 접근 (모바일 기능 접근성 유지) Co-Authored-By: Claude Sonnet 5 --- lib/ui/teacher_attendance_page.dart | 267 ++++++++++++++++++++++------ 1 file changed, 212 insertions(+), 55 deletions(-) diff --git a/lib/ui/teacher_attendance_page.dart b/lib/ui/teacher_attendance_page.dart index 81375c4..613159e 100644 --- a/lib/ui/teacher_attendance_page.dart +++ b/lib/ui/teacher_attendance_page.dart @@ -205,66 +205,36 @@ class _TeacherAttendancePageState extends State { 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 { _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 { ); } + // 🚀 "자습실 시간 설정 메뉴" — 새로고침/출석시간/반출허용/하교/테스트리셋을 + // 전부 여기 하나로 모아서, 메인 대시보드의 로그아웃/설정 메뉴와 똑같은 + // 런치패드 스타일(배경 블러 유지) 오버레이로 띄운다. + Future _showTimeSettingsMenu() async { + final action = await showLaunchpadMenu( + 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> students) { if (students.isEmpty) { @@ -915,3 +959,116 @@ class _TeacherAttendancePageState extends State { ); } } + +// 🚀 "자습실 시간 설정 메뉴" 카드 — 메인 대시보드 로그아웃/설정 메뉴와 같은 톤으로 통일. +class _TimeSettingsMenuCard extends StatelessWidget { + final bool isRefreshing; + final String attendanceTimeLabel; + final ValueChanged 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, + ), + ), + ), + ], + ), + ), + ); + } +}