feat(menu-permissions): 메뉴 API 연동으로 사이드바 권한 정비

- .env.development.example과 lib/core/config/environment.dart, lib/core/permissions/permission_manager.dart에서 PERMISSION__ 폴백을 view 전용으로 좁히고 기본 정책을 명시적으로 거부하도록 재정비했다

- lib/core/navigation/*, lib/core/routing/app_router.dart, lib/widgets/app_shell.dart, lib/main.dart에서 메뉴 매니페스트·카탈로그를 도입해 /menus 응답을 캐싱하고 라우터·사이드바·Breadcrumb가 동일 menu_code/route_path를 쓰도록 리팩터링했다

- lib/core/permissions/permission_resources.dart와 그룹 권한/메뉴 마스터 모듈을 menu_code 기반 CRUD 및 Catalog 경로 정합성 검사로 전환하고 PermissionSynchronizer·PermissionBootstrapper를 확장했다

- test/helpers/test_permissions.dart, test/widgets/app_shell_test.dart 등 신규 구조를 반영하는 테스트·골든과 doc/frontend_menu_permission_tasks.md 문서를 보강했다
This commit is contained in:
JiWoong Sul
2025-11-12 18:29:03 +09:00
parent f767c44573
commit 753f76e952
72 changed files with 1914 additions and 704 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:superport_v2/core/navigation/menu_catalog.dart';
import 'package:superport_v2/core/network/failure.dart';
import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'package:superport_v2/core/common/utils/pagination_utils.dart';
@@ -24,15 +25,18 @@ class GroupPermissionController extends ChangeNotifier {
required GroupPermissionRepository permissionRepository,
required GroupRepository groupRepository,
required MenuRepository menuRepository,
MenuCatalog? menuCatalog,
PermissionManager? permissionManager,
}) : _permissionRepository = permissionRepository,
_groupRepository = groupRepository,
_menuRepository = menuRepository,
_menuCatalog = menuCatalog,
_permissionManager = permissionManager;
final GroupPermissionRepository _permissionRepository;
final GroupRepository _groupRepository;
final MenuRepository _menuRepository;
final MenuCatalog? _menuCatalog;
final PermissionManager? _permissionManager;
PaginatedResult<GroupPermission>? _result;
@@ -43,7 +47,7 @@ class GroupPermissionController extends ChangeNotifier {
String? _errorMessage;
GroupPermissionStatusFilter _statusFilter = GroupPermissionStatusFilter.all;
int? _groupFilter;
int? _menuFilter;
String? _menuFilter;
bool _includeDeleted = false;
final List<Group> _groups = [];
final List<MenuItem> _menus = [];
@@ -56,7 +60,7 @@ class GroupPermissionController extends ChangeNotifier {
String? get errorMessage => _errorMessage;
GroupPermissionStatusFilter get statusFilter => _statusFilter;
int? get groupFilter => _groupFilter;
int? get menuFilter => _menuFilter;
String? get menuFilter => _menuFilter;
bool get includeDeleted => _includeDeleted;
List<Group> get groups => List.unmodifiable(_groups);
List<MenuItem> get menus => List.unmodifiable(_menus);
@@ -87,13 +91,7 @@ class GroupPermissionController extends ChangeNotifier {
_isLoadingMenus = true;
notifyListeners();
try {
final menus = await fetchAllPaginatedItems<MenuItem>(
request: (page, pageSize) => _menuRepository.list(
page: page,
pageSize: pageSize,
includeDeleted: false,
),
);
final menus = _sortMenus(await _resolveMenus());
_menus
..clear()
..addAll(menus);
@@ -132,7 +130,7 @@ class GroupPermissionController extends ChangeNotifier {
page: resolvedPage,
pageSize: _result?.pageSize ?? 20,
groupId: _groupFilter,
menuId: _menuFilter,
menuCode: _menuFilter,
isActive: isActive,
includeDeleted: _includeDeleted,
);
@@ -153,8 +151,9 @@ class GroupPermissionController extends ChangeNotifier {
}
/// 메뉴 필터를 변경한다.
void updateMenuFilter(int? menuId) {
_menuFilter = menuId;
void updateMenuFilter(String? menuCode) {
final trimmed = menuCode?.trim();
_menuFilter = trimmed?.isEmpty ?? true ? null : trimmed;
notifyListeners();
}
@@ -266,6 +265,7 @@ class GroupPermissionController extends ChangeNotifier {
final synchronizer = PermissionSynchronizer(
repository: _permissionRepository,
manager: manager,
menuCatalog: _menuCatalog,
);
await synchronizer.syncForGroup(groupId);
} catch (_) {
@@ -285,4 +285,53 @@ class GroupPermissionController extends ChangeNotifier {
}
return null;
}
List<MenuItem> _sortMenus(List<MenuItem> menus) {
// 백엔드 menus 테이블 순서를 재현하기 위해 display_order → 메뉴명 순으로 정렬한다.
final visibleMenus = menus.where((menu) => !menu.isDeleted).toList();
visibleMenus.sort(_compareMenuItems);
return visibleMenus;
}
int _compareMenuItems(MenuItem a, MenuItem b) {
final parentCompare = (a.parent?.menuName ?? '').compareTo(
b.parent?.menuName ?? '',
);
if (parentCompare != 0) {
return parentCompare;
}
final orderCompare = _compareDisplayOrder(a.displayOrder, b.displayOrder);
if (orderCompare != 0) {
return orderCompare;
}
return a.menuName.compareTo(b.menuName);
}
int _compareDisplayOrder(int? a, int? b) {
if (a == null && b == null) {
return 0;
}
if (a == null) {
return 1;
}
if (b == null) {
return -1;
}
return a.compareTo(b);
}
Future<List<MenuItem>> _resolveMenus() async {
final catalog = _menuCatalog;
if (catalog != null) {
final menus = await catalog.ensureLoaded();
return List<MenuItem>.from(menus);
}
return fetchAllPaginatedItems<MenuItem>(
request: (page, pageSize) => _menuRepository.list(
page: page,
pageSize: pageSize,
includeDeleted: false,
),
);
}
}