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

@@ -19,6 +19,22 @@ class PermissionSynchronizer {
/// 지정한 [groupId]의 메뉴 권한을 조회해 [PermissionManager]에 적용한다.
Future<void> syncForGroup(int groupId) async {
final permissionMap = await fetchPermissionMap(groupId);
_manager.applyServerPermissions(permissionMap);
}
/// 지정한 [groupId]의 메뉴 권한을 조회해 맵 형태로 반환한다.
Future<Map<String, Set<PermissionAction>>> fetchPermissionMap(
int groupId,
) async {
final collected = await _collectPermissions(groupId);
if (collected.isEmpty) {
return const {};
}
return buildPermissionMap(collected);
}
Future<List<GroupPermission>> _collectPermissions(int groupId) async {
final collected = <GroupPermission>[];
var page = 1;
@@ -45,7 +61,6 @@ class PermissionSynchronizer {
page += 1;
}
final permissionMap = buildPermissionMap(collected);
_manager.applyServerPermissions(permissionMap);
return collected;
}
}

View File

@@ -35,10 +35,8 @@ class UserDetailDialogResult {
}
typedef UserCreateCallback = Future<UserAccount?> Function(UserInput input);
typedef UserUpdateCallback = Future<UserAccount?> Function(
int id,
UserInput input,
);
typedef UserUpdateCallback =
Future<UserAccount?> Function(int id, UserInput input);
typedef UserDeleteCallback = Future<bool> Function(int id);
typedef UserRestoreCallback = Future<UserAccount?> Function(int id);
typedef UserResetPasswordCallback = Future<UserAccount?> Function(int id);
@@ -141,10 +139,8 @@ Future<UserDetailDialogResult?> showUserDetailDialog({
id: _UserDetailSections.overview,
label: '상세',
icon: LucideIcons.info,
builder: (_) => _UserOverviewSection(
user: detailUser,
dateFormat: dateFormat,
),
builder: (_) =>
_UserOverviewSection(user: detailUser, dateFormat: dateFormat),
),
if (isDetail)
SuperportDetailDialogSection(
@@ -217,9 +213,7 @@ Future<UserDetailDialogResult?> showUserDetailDialog({
),
SuperportDetailMetadata.text(
label: '이메일',
value: detailUser.email?.isEmpty ?? true
? '-'
: detailUser.email!,
value: detailUser.email?.isEmpty ?? true ? '-' : detailUser.email!,
),
SuperportDetailMetadata.text(
label: '연락처',
@@ -229,17 +223,13 @@ Future<UserDetailDialogResult?> showUserDetailDialog({
),
SuperportDetailMetadata.text(
label: '비고',
value: detailUser.note?.isEmpty ?? true
? '-'
: detailUser.note!,
value: detailUser.note?.isEmpty ?? true ? '-' : detailUser.note!,
),
SuperportDetailMetadata.text(
label: '비밀번호 변경일시',
value: detailUser.passwordUpdatedAt == null
? '-'
: dateFormat.format(
detailUser.passwordUpdatedAt!.toLocal(),
),
: dateFormat.format(detailUser.passwordUpdatedAt!.toLocal()),
),
SuperportDetailMetadata.text(
label: '생성일시',
@@ -285,10 +275,7 @@ class _UserDetailSections {
/// 사용자 주요 정보를 표시하는 섹션이다.
class _UserOverviewSection extends StatelessWidget {
const _UserOverviewSection({
required this.user,
required this.dateFormat,
});
const _UserOverviewSection({required this.user, required this.dateFormat});
final UserAccount user;
final intl.DateFormat dateFormat;
@@ -339,10 +326,7 @@ class _UserOverviewSection extends StatelessWidget {
),
const SizedBox(width: 12),
Expanded(
child: Text(
rows[i].value,
style: theme.textTheme.small,
),
child: Text(rows[i].value, style: theme.textTheme.small),
),
],
),
@@ -426,8 +410,9 @@ class _UserSecurityContentState extends State<_UserSecurityContent> {
label: '비밀번호 변경일시',
value: widget.user.passwordUpdatedAt == null
? '-'
: widget.dateFormat
.format(widget.user.passwordUpdatedAt!.toLocal()),
: widget.dateFormat.format(
widget.user.passwordUpdatedAt!.toLocal(),
),
),
const SizedBox(height: 12),
_KeyValueColumn(
@@ -571,8 +556,7 @@ class _UserFormState extends State<_UserForm> {
_groupIdNotifier = ValueNotifier<int?>(user?.group?.id);
_isActiveNotifier = ValueNotifier<bool>(user?.isActive ?? true);
if (_groupIdNotifier.value == null &&
widget.groupOptions.length == 1) {
if (_groupIdNotifier.value == null && widget.groupOptions.length == 1) {
_groupIdNotifier.value = widget.groupOptions.first.id;
}
}
@@ -613,8 +597,7 @@ class _UserFormState extends State<_UserForm> {
}
},
),
if (_employeeError != null)
_ErrorText(_employeeError!),
if (_employeeError != null) _ErrorText(_employeeError!),
],
),
),