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:
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user