전역 구조 리팩터링 및 테스트 확장
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
||||
|
||||
import '../core/constants/app_sections.dart';
|
||||
import '../core/theme/theme_controller.dart';
|
||||
import '../core/permissions/permission_manager.dart';
|
||||
|
||||
class AppShell extends StatelessWidget {
|
||||
const AppShell({
|
||||
@@ -19,6 +21,13 @@ class AppShell extends StatelessWidget {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth >= 960;
|
||||
final manager = PermissionScope.of(context);
|
||||
final filteredPages = <AppPageDescriptor>[
|
||||
for (final section in appSections)
|
||||
for (final page in section.pages)
|
||||
if (manager.can(page.path, PermissionAction.view)) page,
|
||||
];
|
||||
final pages = filteredPages.isEmpty ? allAppPages : filteredPages;
|
||||
if (isWide) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -26,14 +35,14 @@ class AppShell extends StatelessWidget {
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '로그아웃',
|
||||
icon: const Icon(LucideIcons.logOut),
|
||||
icon: const Icon(lucide.LucideIcons.logOut),
|
||||
onPressed: () => context.go(loginRoutePath),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Row(
|
||||
children: [
|
||||
_NavigationRail(currentLocation: currentLocation),
|
||||
_NavigationRail(currentLocation: currentLocation, pages: pages),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(child: child),
|
||||
],
|
||||
@@ -47,7 +56,7 @@ class AppShell extends StatelessWidget {
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '로그아웃',
|
||||
icon: const Icon(LucideIcons.logOut),
|
||||
icon: const Icon(lucide.LucideIcons.logOut),
|
||||
onPressed: () => context.go(loginRoutePath),
|
||||
),
|
||||
],
|
||||
@@ -60,6 +69,7 @@ class AppShell extends StatelessWidget {
|
||||
Navigator.of(context).pop();
|
||||
context.go(path);
|
||||
},
|
||||
pages: pages,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -71,16 +81,18 @@ class AppShell extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _NavigationRail extends StatelessWidget {
|
||||
const _NavigationRail({required this.currentLocation});
|
||||
const _NavigationRail({required this.currentLocation, required this.pages});
|
||||
|
||||
final String currentLocation;
|
||||
final List<AppPageDescriptor> pages;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pages = allAppPages;
|
||||
final selectedIndex = _selectedIndex(currentLocation, pages);
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final themeController = ThemeControllerScope.of(context);
|
||||
final currentThemeMode = themeController.mode;
|
||||
|
||||
return Container(
|
||||
width: 104,
|
||||
@@ -151,6 +163,13 @@ class _NavigationRail extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 16),
|
||||
child: _ThemeMenuButton(
|
||||
mode: currentThemeMode,
|
||||
onChanged: themeController.update,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -158,19 +177,37 @@ class _NavigationRail extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _NavigationList extends StatelessWidget {
|
||||
const _NavigationList({required this.currentLocation, required this.onTap});
|
||||
const _NavigationList({
|
||||
required this.currentLocation,
|
||||
required this.onTap,
|
||||
required this.pages,
|
||||
});
|
||||
|
||||
final String currentLocation;
|
||||
final ValueChanged<String> onTap;
|
||||
final List<AppPageDescriptor> pages;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pages = allAppPages;
|
||||
final selectedIndex = _selectedIndex(currentLocation, pages);
|
||||
final themeController = ThemeControllerScope.of(context);
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: pages.length,
|
||||
itemCount: pages.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == pages.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||
child: _ThemeMenuButton(
|
||||
mode: themeController.mode,
|
||||
onChanged: (mode) {
|
||||
themeController.update(mode);
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final page = pages[index];
|
||||
final selected = index == selectedIndex;
|
||||
return ListTile(
|
||||
@@ -190,6 +227,77 @@ class _NavigationList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ThemeMenuButton extends StatelessWidget {
|
||||
const _ThemeMenuButton({required this.mode, required this.onChanged});
|
||||
|
||||
final ThemeMode mode;
|
||||
final ValueChanged<ThemeMode> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final label = _label(mode);
|
||||
final icon = _icon(mode);
|
||||
|
||||
return PopupMenuButton<ThemeMode>(
|
||||
tooltip: '테마 변경',
|
||||
onSelected: onChanged,
|
||||
itemBuilder: (context) => ThemeMode.values
|
||||
.map(
|
||||
(value) => PopupMenuItem<ThemeMode>(
|
||||
value: value,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(_icon(value), size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(_label(value)),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text('테마 · $label', style: theme.textTheme.labelSmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _label(ThemeMode mode) {
|
||||
switch (mode) {
|
||||
case ThemeMode.system:
|
||||
return '시스템';
|
||||
case ThemeMode.light:
|
||||
return '라이트';
|
||||
case ThemeMode.dark:
|
||||
return '다크';
|
||||
}
|
||||
}
|
||||
|
||||
static IconData _icon(ThemeMode mode) {
|
||||
switch (mode) {
|
||||
case ThemeMode.system:
|
||||
return lucide.LucideIcons.monitorCog;
|
||||
case ThemeMode.light:
|
||||
return lucide.LucideIcons.sun;
|
||||
case ThemeMode.dark:
|
||||
return lucide.LucideIcons.moon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _selectedIndex(String location, List<AppPageDescriptor> pages) {
|
||||
final normalized = location.toLowerCase();
|
||||
final exact = pages.indexWhere(
|
||||
|
||||
Reference in New Issue
Block a user