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:
@@ -25,14 +25,19 @@ class AuthSessionDto {
|
||||
final expires = _parseDate(_readString(json, 'expires_at'));
|
||||
final userMap = _readMap(json, 'user');
|
||||
final permissionList = _readList(json, 'permissions');
|
||||
final permissionDtos = permissionList
|
||||
.map(AuthPermissionDto.fromJson)
|
||||
.toList(growable: true);
|
||||
final scopeCodes = _readScopeCodes(json);
|
||||
for (final scope in scopeCodes) {
|
||||
permissionDtos.add(AuthPermissionDto.fromScope(scope));
|
||||
}
|
||||
return AuthSessionDto(
|
||||
accessToken: token ?? '',
|
||||
refreshToken: refresh ?? '',
|
||||
expiresAt: expires,
|
||||
user: _parseUser(userMap),
|
||||
permissions: permissionList
|
||||
.map(AuthPermissionDto.fromJson)
|
||||
.toList(growable: false),
|
||||
permissions: List<AuthPermissionDto>.unmodifiable(permissionDtos),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,6 +92,14 @@ class AuthPermissionDto {
|
||||
);
|
||||
}
|
||||
|
||||
factory AuthPermissionDto.fromScope(String scope) {
|
||||
final normalized = scope.trim();
|
||||
if (normalized.isEmpty) {
|
||||
throw const FormatException('권한 스코프 코드가 비어 있습니다.');
|
||||
}
|
||||
return AuthPermissionDto(resource: normalized, actions: const ['view']);
|
||||
}
|
||||
|
||||
AuthPermission toEntity() =>
|
||||
AuthPermission(resource: resource, actions: actions);
|
||||
}
|
||||
@@ -131,6 +144,65 @@ List<Map<String, dynamic>> _readList(Map<String, dynamic> source, String key) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
Set<String> _readScopeCodes(Map<String, dynamic> source) {
|
||||
final codes = <String>{};
|
||||
|
||||
void addCode(String? raw) {
|
||||
final normalized = raw == null ? null : _normalizeScopeCode(raw);
|
||||
if (normalized != null) {
|
||||
codes.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
void parse(dynamic value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (value is String) {
|
||||
addCode(value);
|
||||
return;
|
||||
}
|
||||
if (value is Iterable) {
|
||||
for (final item in value) {
|
||||
parse(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value is Map<String, dynamic>) {
|
||||
for (final key in const [
|
||||
'scope_code',
|
||||
'scope',
|
||||
'code',
|
||||
'permission_code',
|
||||
'permission',
|
||||
'name',
|
||||
'scopeCode',
|
||||
'permissionCode',
|
||||
'scopeName',
|
||||
]) {
|
||||
final candidate = value[key];
|
||||
if (candidate is String && candidate.trim().isNotEmpty) {
|
||||
addCode(candidate);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (final entry in value.entries) {
|
||||
final candidate = entry.value;
|
||||
if (candidate is String && candidate.trim().isNotEmpty) {
|
||||
addCode(candidate);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parse(source['permission_codes']);
|
||||
parse(source['permission_scopes']);
|
||||
parse(source['group_permission_scopes']);
|
||||
|
||||
return codes;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _readMap(Map<String, dynamic> source, String key) {
|
||||
final value = source[key];
|
||||
if (value is Map<String, dynamic>) {
|
||||
@@ -162,3 +234,15 @@ int? _readOptionalInt(Map<String, dynamic>? source, String key) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _normalizeScopeCode(String value) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final lowered = trimmed.toLowerCase();
|
||||
if (lowered.startsWith('scope:')) {
|
||||
return lowered;
|
||||
}
|
||||
return 'scope:$lowered';
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class AuthPermission {
|
||||
Map<String, Set<PermissionAction>> toPermissionMap() {
|
||||
final normalized = PermissionResources.normalize(resource);
|
||||
final actionSet = <PermissionAction>{};
|
||||
final isScope = normalized.startsWith('scope:');
|
||||
for (final raw in actions) {
|
||||
final parsed = _parseAction(raw);
|
||||
if (parsed == null) {
|
||||
@@ -22,6 +23,9 @@ class AuthPermission {
|
||||
}
|
||||
actionSet.add(parsed);
|
||||
}
|
||||
if (actionSet.isEmpty && isScope) {
|
||||
actionSet.add(PermissionAction.view);
|
||||
}
|
||||
if (actionSet.isEmpty) {
|
||||
return <String, Set<PermissionAction>>{};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user