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

@@ -0,0 +1,10 @@
/// 재고 이벤트와 연결된 거래처 유형.
enum InventoryCounterpartyType { vendor, customer, unknown }
/// 재고 이벤트의 거래처 정보를 표현한다.
class InventoryCounterparty {
const InventoryCounterparty({required this.type, this.name});
final InventoryCounterpartyType type;
final String? name;
}

View File

@@ -0,0 +1,22 @@
import 'inventory_event.dart';
import 'inventory_product.dart';
import 'inventory_warehouse_balance.dart';
/// 재고 현황 단건 조회 결과.
class InventoryDetail {
const InventoryDetail({
required this.product,
required this.totalQuantity,
required this.warehouseBalances,
required this.recentEvents,
this.updatedAt,
this.lastRefreshedAt,
});
final InventoryProduct product;
final int totalQuantity;
final List<InventoryWarehouseBalance> warehouseBalances;
final List<InventoryEvent> recentEvents;
final DateTime? updatedAt;
final DateTime? lastRefreshedAt;
}

View File

@@ -0,0 +1,45 @@
import 'inventory_counterparty.dart';
import 'inventory_transaction_reference.dart';
import 'inventory_warehouse.dart';
/// 재고 변동 이벤트 요약/상세 정보.
class InventoryEvent {
const InventoryEvent({
required this.eventId,
required this.eventKind,
required this.eventLabel,
required this.deltaQuantity,
required this.occurredAt,
this.counterparty,
this.warehouse,
this.transaction,
this.line,
});
/// 이벤트 식별자.
final int eventId;
/// 이벤트 종류(`receipt`, `issue`, `rental_out`, `rental_return`).
final String eventKind;
/// 현지화된 이벤트 라벨.
final String eventLabel;
/// 수량 증감.
final int deltaQuantity;
/// 발생 시각(UTC).
final DateTime occurredAt;
/// 거래처 정보.
final InventoryCounterparty? counterparty;
/// 이벤트가 발생한 창고 정보.
final InventoryWarehouse? warehouse;
/// 연결된 전표 정보.
final InventoryTransactionReference? transaction;
/// 연결된 라인 정보.
final InventoryEventLineReference? line;
}

View File

@@ -0,0 +1,80 @@
/// 재고 요약 목록 조회 필터.
class InventorySummaryFilter {
const InventorySummaryFilter({
this.page = 1,
this.pageSize = 50,
this.query,
this.productName,
this.vendorName,
this.warehouseId,
this.includeEmpty = false,
this.updatedSince,
this.sort,
this.order,
});
final int page;
final int pageSize;
final String? query;
final String? productName;
final String? vendorName;
final int? warehouseId;
final bool includeEmpty;
final DateTime? updatedSince;
final String? sort;
final String? order;
/// API 요청에 사용할 쿼리 파라미터 맵을 생성한다.
Map<String, dynamic> toQuery() {
final queryMap = <String, dynamic>{'page': page, 'page_size': pageSize};
void put(String key, dynamic value) {
if (value == null) {
return;
}
if (value is String) {
final trimmed = value.trim();
if (trimmed.isEmpty) {
return;
}
queryMap[key] = trimmed;
return;
}
queryMap[key] = value;
}
put('q', query);
put('product_name', productName);
put('vendor_name', vendorName);
if (warehouseId != null) {
queryMap['warehouse_id'] = warehouseId;
}
if (includeEmpty) {
queryMap['include_empty'] = 'true';
}
if (updatedSince != null) {
queryMap['updated_since'] = updatedSince!.toUtc().toIso8601String();
}
put('sort', sort);
final normalizedOrder = order?.trim().toLowerCase();
if (normalizedOrder != null && normalizedOrder.isNotEmpty) {
queryMap['order'] = normalizedOrder;
}
return queryMap;
}
}
/// 재고 단건 조회 필터.
class InventoryDetailFilter {
const InventoryDetailFilter({this.warehouseId, this.eventLimit = 20});
final int? warehouseId;
final int eventLimit;
Map<String, dynamic> toQuery() {
final map = <String, dynamic>{'event_limit': eventLimit};
if (warehouseId != null) {
map['warehouse_id'] = warehouseId;
}
return map;
}
}

View File

@@ -0,0 +1,23 @@
import 'inventory_vendor.dart';
/// 재고 요약/상세 뷰에서 공통으로 사용하는 제품 정보.
class InventoryProduct {
const InventoryProduct({
required this.id,
required this.code,
required this.name,
this.vendor,
});
/// 제품 식별자.
final int id;
/// 제품 코드.
final String code;
/// 제품 명칭.
final String name;
/// 공급사 정보. 없을 수도 있다.
final InventoryVendor? vendor;
}

View File

@@ -0,0 +1,22 @@
import 'inventory_event.dart';
import 'inventory_product.dart';
import 'inventory_warehouse_balance.dart';
/// 재고 현황 목록 항목 엔티티.
class InventorySummary {
const InventorySummary({
required this.product,
required this.totalQuantity,
required this.warehouseBalances,
this.recentEvent,
this.updatedAt,
this.lastRefreshedAt,
});
final InventoryProduct product;
final int totalQuantity;
final List<InventoryWarehouseBalance> warehouseBalances;
final InventoryEvent? recentEvent;
final DateTime? updatedAt;
final DateTime? lastRefreshedAt;
}

View File

@@ -0,0 +1,24 @@
import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'inventory_summary.dart';
/// 재고 요약 목록과 뷰 리프레시 메타데이터를 함께 담는 결과 모델.
class InventorySummaryListResult {
const InventorySummaryListResult({
required this.result,
this.lastRefreshedAt,
});
final PaginatedResult<InventorySummary> result;
final DateTime? lastRefreshedAt;
InventorySummaryListResult copyWith({
PaginatedResult<InventorySummary>? result,
DateTime? lastRefreshedAt,
}) {
return InventorySummaryListResult(
result: result ?? this.result,
lastRefreshedAt: lastRefreshedAt ?? this.lastRefreshedAt,
);
}
}

View File

@@ -0,0 +1,23 @@
/// 재고 이벤트가 속한 전표 정보를 요약한 참조 모델.
class InventoryTransactionReference {
const InventoryTransactionReference({
required this.id,
required this.transactionNo,
});
final int id;
final String transactionNo;
}
/// 재고 이벤트 라인 정보 참조 모델.
class InventoryEventLineReference {
const InventoryEventLineReference({
required this.id,
required this.lineNo,
required this.quantity,
});
final int id;
final int lineNo;
final int quantity;
}

View File

@@ -0,0 +1,10 @@
/// 재고 요약에서 사용하는 공급사 정보를 표현하는 값 객체.
class InventoryVendor {
const InventoryVendor({this.id, required this.name});
/// 공급사 식별자. 미정의일 수 있다.
final int? id;
/// 공급사 명칭.
final String name;
}

View File

@@ -0,0 +1,17 @@
/// 재고 현황에서 참조하는 창고 정보를 표현한다.
class InventoryWarehouse {
const InventoryWarehouse({
required this.id,
required this.code,
required this.name,
});
/// 창고 식별자.
final int id;
/// 창고 코드.
final String code;
/// 창고 명칭.
final String name;
}

View File

@@ -0,0 +1,15 @@
import 'inventory_warehouse.dart';
/// 특정 창고의 재고 수량을 나타내는 모델.
class InventoryWarehouseBalance {
const InventoryWarehouseBalance({
required this.warehouse,
required this.quantity,
});
/// 창고 정보.
final InventoryWarehouse warehouse;
/// 창고 내 잔량.
final int quantity;
}