런치패드 스타일 화면 전환 + 설정/로그아웃 메뉴 오버레이 + 알약형 알림 추가
- 좌우 슬라이드 대신 맥북 런치패드처럼 확대+페이드로 나타나고 전환 중 뒤 화면이 블러 처리되는 LaunchpadPageRoute 추가, 대시보드의 모든 기능 타일 이동에 적용 - 오른쪽 위 설정 아이콘 + 로그아웃 아이콘 2개를 ">" 버튼 1개로 통합하고, 누르면 로그아웃/설정 메뉴가 런치패드 스타일 오버레이(배경 블러 유지)로 뜨게 변경 - 기능 타일을 누를 때 뜨는 안내 메시지를 스낵바 대신 알약(pill) 모양 알림으로 교체 (AppNotice) - 웹에서는 왼쪽 위, 앱에서는 기존처럼 아래쪽에 표시. 푸시 알림과는 무관. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+139
-59
@@ -10,9 +10,11 @@ import '../function/student_dashboard_controller.dart';
|
||||
import '../models/dashboard_tile_layout.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
import 'admin_dashboard.dart';
|
||||
import 'app_notice.dart';
|
||||
import 'dashboard_settings_page.dart';
|
||||
import 'device_checkout_ledger_page.dart';
|
||||
import 'device_checkout_request_screen.dart';
|
||||
import 'launchpad_transition.dart';
|
||||
import 'login_screen.dart';
|
||||
import 'nfc_poccket_checkin_screen.dart';
|
||||
import 'nfc_tag_writer_screen.dart';
|
||||
@@ -74,13 +76,11 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
}
|
||||
|
||||
void _openPocketCheckIn(BuildContext context) {
|
||||
Navigator.push(
|
||||
pushLaunchpad(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NfcPocketCheckInScreen(
|
||||
studentId: _displayId,
|
||||
studentName: _displayName,
|
||||
),
|
||||
(context) => NfcPocketCheckInScreen(
|
||||
studentId: _displayId,
|
||||
studentName: _displayName,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -88,9 +88,7 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
Future<void> _deleteUser(String userId) async {
|
||||
final (_, message) = await _controller.deleteUser(userId);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
AppNotice.show(context, message);
|
||||
}
|
||||
|
||||
void _showDeleteUserDialog(BuildContext context) {
|
||||
@@ -191,10 +189,35 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
);
|
||||
}
|
||||
|
||||
// 🚀 오른쪽 위 ">" 버튼 — 로그아웃/설정을 런치패드 스타일 메뉴로 띄운다.
|
||||
Future<void> _openMenu(BuildContext context) async {
|
||||
final action = await showLaunchpadMenu<String>(
|
||||
context: context,
|
||||
builder: (context) => Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8, right: 12),
|
||||
child: _DashboardMenuCard(
|
||||
onLogout: () => Navigator.pop(context, 'logout'),
|
||||
onSettings: () => Navigator.pop(context, 'settings'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (action == 'logout') {
|
||||
_logout(context);
|
||||
} else if (action == 'settings') {
|
||||
_openSettings(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openSettings(BuildContext context) async {
|
||||
final result = await Navigator.push<String>(
|
||||
final result = await pushLaunchpad<String>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const DashboardSettingsPage()),
|
||||
(context) => const DashboardSettingsPage(),
|
||||
);
|
||||
if (result == 'edit_ui' && kTileEditingEnabled) {
|
||||
_layout.enterEditMode();
|
||||
@@ -249,13 +272,11 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
color: widget.isDeviceMatched ? AppPalette.ink : AppPalette.sage,
|
||||
onTap: widget.isDeviceMatched
|
||||
? () => _openPocketCheckIn(context)
|
||||
: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('대리 출석 방지를 위해 등록된 본인 스마트폰에서만 출석 가능합니다.'),
|
||||
),
|
||||
);
|
||||
},
|
||||
: () => AppNotice.show(
|
||||
context,
|
||||
'대리 출석 방지를 위해 등록된 본인 스마트폰에서만 출석 가능합니다.',
|
||||
icon: Icons.lock_rounded,
|
||||
),
|
||||
isActionButton: widget.isDeviceMatched,
|
||||
isLoading: _controller.isLoading,
|
||||
),
|
||||
@@ -267,11 +288,11 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
title: '실시간 학교 상황',
|
||||
subtitle: '급식실 줄 & 매점 재고 확인',
|
||||
color: AppPalette.ink,
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('실시간 학교 상황 페이지로 이동합니다.')),
|
||||
);
|
||||
},
|
||||
onTap: () => AppNotice.show(
|
||||
context,
|
||||
'실시간 학교 상황 페이지로 이동합니다.',
|
||||
icon: Icons.fastfood_rounded,
|
||||
),
|
||||
),
|
||||
));
|
||||
tiles.add((
|
||||
@@ -281,13 +302,11 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
title: '스마트기기 반출',
|
||||
subtitle: '패드 사용 신청 (시간/목적)',
|
||||
color: AppPalette.ink,
|
||||
onTap: () => Navigator.push(
|
||||
onTap: () => pushLaunchpad(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DeviceCheckoutRequestScreen(
|
||||
studentId: _displayId,
|
||||
studentName: _displayName,
|
||||
),
|
||||
(context) => DeviceCheckoutRequestScreen(
|
||||
studentId: _displayId,
|
||||
studentName: _displayName,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -302,11 +321,9 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
title: '실시간 출석 확인',
|
||||
subtitle: '학생 제출 로그 모니터링',
|
||||
color: AppPalette.ink,
|
||||
onTap: () => Navigator.push(
|
||||
onTap: () => pushLaunchpad(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const TeacherAttendancePage(),
|
||||
),
|
||||
(context) => const TeacherAttendancePage(),
|
||||
),
|
||||
),
|
||||
));
|
||||
@@ -317,11 +334,9 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
title: '학생 계정 관리',
|
||||
subtitle: '계정 추가 및 강제 리셋',
|
||||
color: AppPalette.ink,
|
||||
onTap: () => Navigator.push(
|
||||
onTap: () => pushLaunchpad(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const TeacherStudentManagementPage(),
|
||||
),
|
||||
(context) => const TeacherStudentManagementPage(),
|
||||
),
|
||||
),
|
||||
));
|
||||
@@ -332,13 +347,11 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
title: '스마트기기 반출 대장',
|
||||
subtitle: '패드 반출 신청 승인/거절',
|
||||
color: AppPalette.ink,
|
||||
onTap: () => Navigator.push(
|
||||
onTap: () => pushLaunchpad(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DeviceCheckoutLedgerPage(
|
||||
teacherId: widget.userId,
|
||||
teacherName: widget.userName,
|
||||
),
|
||||
(context) => DeviceCheckoutLedgerPage(
|
||||
teacherId: widget.userId,
|
||||
teacherName: widget.userName,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -353,10 +366,8 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
title: '서버 DB 제어',
|
||||
subtitle: '시스템 원격 초기화',
|
||||
color: AppPalette.ink,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const AdminDashboard()),
|
||||
),
|
||||
onTap: () =>
|
||||
pushLaunchpad(context, (context) => const AdminDashboard()),
|
||||
),
|
||||
));
|
||||
tiles.add((
|
||||
@@ -376,10 +387,8 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
title: 'NFC 태그 쓰기',
|
||||
subtitle: '주머니 스티커 초기 설정',
|
||||
color: AppPalette.ink,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const NfcTagWriterScreen()),
|
||||
),
|
||||
onTap: () =>
|
||||
pushLaunchpad(context, (context) => const NfcTagWriterScreen()),
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -477,14 +486,9 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
tooltip: '설정',
|
||||
onPressed: () => _openSettings(context),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout_rounded),
|
||||
tooltip: '로그아웃',
|
||||
onPressed: () => _logout(context),
|
||||
icon: const Icon(Icons.chevron_right_rounded),
|
||||
tooltip: '메뉴',
|
||||
onPressed: () => _openMenu(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -812,3 +816,79 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 오른쪽 위 ">" 버튼을 누르면 뜨는 런치패드 스타일 메뉴 카드.
|
||||
class _DashboardMenuCard extends StatelessWidget {
|
||||
final VoidCallback onLogout;
|
||||
final VoidCallback onSettings;
|
||||
|
||||
const _DashboardMenuCard({required this.onLogout, required this.onSettings});
|
||||
|
||||
@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: 180),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_MenuRow(
|
||||
icon: Icons.logout_rounded,
|
||||
label: '로그아웃',
|
||||
onTap: onLogout,
|
||||
),
|
||||
const Divider(height: 1, color: AppPalette.sage),
|
||||
_MenuRow(
|
||||
icon: Icons.settings_outlined,
|
||||
label: '설정',
|
||||
onTap: onSettings,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _MenuRow({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@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: [
|
||||
Icon(icon, size: 20, color: AppPalette.ink),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppPalette.ink,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user