feat(inventory): 재고 현황 요약/상세 플로우를 릴리스

- lib/features/inventory/summary 계층과 warehouse select 위젯을 추가해 목록/상세, 자동 새로고침, 필터, 상세 시트를 구현

- PermissionBootstrapper, scope 파서, 라우트 가드로 inventory.view 기반 권한 부여와 메뉴 노출을 통합(lib/core, lib/main.dart 등)

- Inventory Summary API/QA/Audit 문서와 PR 템플릿, CHANGELOG를 신규 스펙과 검증 커맨드로 업데이트

- DTO 직렬화 의존성을 추가하고 Golden·Widget·단위 테스트를 작성했으며 flutter analyze / flutter test --coverage를 통과
This commit is contained in:
JiWoong Sul
2025-11-09 01:13:02 +09:00
parent 486ab8706f
commit 47cc62a33d
72 changed files with 5453 additions and 1021 deletions

View File

@@ -14,102 +14,157 @@ class _MockGroupPermissionRepository extends Mock
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('그룹 권한을 모두 불러와 PermissionManager에 적용한다', () async {
final repository = _MockGroupPermissionRepository();
final manager = PermissionManager();
final synchronizer = PermissionSynchronizer(
repository: repository,
manager: manager,
pageSize: 1,
);
group('PermissionSynchronizer', () {
test('그룹 권한을 모두 불러와 PermissionManager에 적용한다', () async {
final repository = _MockGroupPermissionRepository();
final manager = PermissionManager();
final synchronizer = PermissionSynchronizer(
repository: repository,
manager: manager,
pageSize: 1,
);
final permissionPage1 = GroupPermission(
id: 1,
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
menu: GroupPermissionMenu(
id: 10,
menuCode: 'INBOUND',
menuName: '입고',
path: '/inventory/inbound',
),
canCreate: true,
canRead: true,
canUpdate: false,
canDelete: false,
);
final permissionPage1 = GroupPermission(
id: 1,
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
menu: GroupPermissionMenu(
id: 10,
menuCode: 'INBOUND',
menuName: '입고',
path: '/inventory/inbound',
),
canCreate: true,
canRead: true,
canUpdate: false,
canDelete: false,
);
final permissionPage2 = GroupPermission(
id: 2,
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
menu: GroupPermissionMenu(
id: 11,
menuCode: 'OUTBOUND',
menuName: '출고',
path: '/inventory/outbound',
),
canCreate: false,
canRead: true,
canUpdate: true,
canDelete: false,
);
final permissionPage2 = GroupPermission(
id: 2,
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
menu: GroupPermissionMenu(
id: 11,
menuCode: 'OUTBOUND',
menuName: '출고',
path: '/inventory/outbound',
),
canCreate: false,
canRead: true,
canUpdate: true,
canDelete: false,
);
when(
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
groupId: any(named: 'groupId'),
menuId: any(named: 'menuId'),
isActive: any(named: 'isActive'),
includeDeleted: any(named: 'includeDeleted'),
),
).thenAnswer((invocation) async {
final page = invocation.namedArguments[#page] as int;
if (page == 1) {
when(
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
groupId: any(named: 'groupId'),
menuId: any(named: 'menuId'),
isActive: any(named: 'isActive'),
includeDeleted: any(named: 'includeDeleted'),
),
).thenAnswer((invocation) async {
final page = invocation.namedArguments[#page] as int;
if (page == 1) {
return PaginatedResult<GroupPermission>(
items: [permissionPage1],
page: 1,
pageSize: 1,
total: 2,
);
}
return PaginatedResult<GroupPermission>(
items: [permissionPage1],
page: 1,
items: [permissionPage2],
page: 2,
pageSize: 1,
total: 2,
);
}
return PaginatedResult<GroupPermission>(
items: [permissionPage2],
page: 2,
pageSize: 1,
total: 2,
});
await synchronizer.syncForGroup(1);
verify(
() => repository.list(
page: any(named: 'page'),
pageSize: 1,
groupId: 1,
menuId: null,
isActive: true,
includeDeleted: false,
),
).called(greaterThanOrEqualTo(1));
expect(
manager.can(
PermissionResources.stockTransactions,
PermissionAction.create,
),
isTrue,
);
expect(
manager.can(
PermissionResources.stockTransactions,
PermissionAction.edit,
),
isTrue,
);
expect(
manager.can(
PermissionResources.stockTransactions,
PermissionAction.delete,
),
isFalse,
);
});
await synchronizer.syncForGroup(1);
verify(
() => repository.list(
page: any(named: 'page'),
test('fetchPermissionMap은 그룹 권한 맵을 반환한다', () async {
final repository = _MockGroupPermissionRepository();
final manager = PermissionManager();
final synchronizer = PermissionSynchronizer(
repository: repository,
manager: manager,
pageSize: 1,
groupId: 1,
menuId: null,
isActive: true,
includeDeleted: false,
),
).called(greaterThanOrEqualTo(1));
);
expect(
manager.can(
PermissionResources.stockTransactions,
PermissionAction.create,
),
isTrue,
);
expect(
manager.can(PermissionResources.stockTransactions, PermissionAction.edit),
isTrue,
);
expect(
manager.can(
PermissionResources.stockTransactions,
PermissionAction.delete,
),
isFalse,
);
when(
() => repository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
groupId: any(named: 'groupId'),
menuId: any(named: 'menuId'),
isActive: any(named: 'isActive'),
includeDeleted: any(named: 'includeDeleted'),
),
).thenAnswer((_) async {
return PaginatedResult<GroupPermission>(
items: [
GroupPermission(
id: 1,
group: GroupPermissionGroup(id: 99, groupName: 'Ops'),
menu: GroupPermissionMenu(
id: 33,
menuCode: 'INVENTORY',
menuName: '재고',
path: '/inventory/summary',
),
canRead: true,
canCreate: false,
canUpdate: false,
canDelete: false,
),
],
page: 1,
pageSize: 1,
total: 1,
);
});
final map = await synchronizer.fetchPermissionMap(10);
expect(
map[PermissionResources.inventorySummary],
contains(PermissionAction.view),
);
});
});
}