refactor: 인벤토리 테이블 스펙과 도메인 계층 정비

This commit is contained in:
JiWoong Sul
2025-10-14 18:09:26 +09:00
parent 8d3b2c1e20
commit 1325109fba
32 changed files with 5550 additions and 290 deletions

View File

@@ -0,0 +1,295 @@
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction.dart';
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
/// 라인 편집 폼에서 수집한 입력 값을 정규화한 모델.
class TransactionLineDraft {
TransactionLineDraft({
this.id,
required this.lineNo,
required this.productId,
required this.quantity,
required this.unitPrice,
this.note,
});
/// 기존 라인의 식별자. 신규 라인은 `null`이다.
final int? id;
/// 서버에 전달할 라인 순번.
final int lineNo;
/// 선택된 제품의 식별자.
final int productId;
/// 수량.
final int quantity;
/// 단가.
final double unitPrice;
/// 비고. 공백일 경우 `null`로 정규화한다.
final String? note;
}
/// 라인 변경 사항을 서버 호출 단위로 정리한 결과.
class TransactionLineSyncPlan {
TransactionLineSyncPlan({
this.createdLines = const [],
this.updatedLines = const [],
this.deletedLineIds = const [],
});
/// 새로 추가해야 할 라인 목록.
final List<TransactionLineCreateInput> createdLines;
/// 수정이 필요한 기존 라인 목록.
final List<TransactionLineUpdateInput> updatedLines;
/// 삭제해야 할 라인 ID 목록.
final List<int> deletedLineIds;
/// 변경 사항이 있는지 여부.
bool get hasChanges =>
createdLines.isNotEmpty ||
updatedLines.isNotEmpty ||
deletedLineIds.isNotEmpty;
}
/// 고객 연결 입력 값을 정규화한 모델.
class TransactionCustomerDraft {
TransactionCustomerDraft({this.id, required this.customerId, this.note});
/// 기존 고객 연결 ID. 신규 연결은 `null`이다.
final int? id;
/// 선택된 고객 ID.
final int customerId;
/// 비고. 공백일 경우 `null`.
final String? note;
}
/// 고객 연결 변경 사항을 서버 호출 단위로 정리한 결과.
class TransactionCustomerSyncPlan {
TransactionCustomerSyncPlan({
this.createdCustomers = const [],
this.updatedCustomers = const [],
this.deletedCustomerIds = const [],
});
/// 새로 추가할 고객 연결 입력.
final List<TransactionCustomerCreateInput> createdCustomers;
/// 비고 등 속성이 변경된 고객 연결 입력.
final List<TransactionCustomerUpdateInput> updatedCustomers;
/// 삭제 대상 고객 연결 ID.
final List<int> deletedCustomerIds;
/// 변경 사항이 있는지 여부.
bool get hasChanges =>
createdCustomers.isNotEmpty ||
updatedCustomers.isNotEmpty ||
deletedCustomerIds.isNotEmpty;
}
/// 재고 트랜잭션 라인·고객 입력을 기존 데이터와 비교해 동기화 계획을 산출하는 유틸리티.
class TransactionDetailSyncService {
const TransactionDetailSyncService();
/// 현재 서버 상태와 폼 입력을 비교해 라인 변경 계획을 생성한다.
TransactionLineSyncPlan buildLinePlan({
required List<TransactionLineDraft> drafts,
required List<StockTransactionLine> currentLines,
}) {
final createdLines = <TransactionLineCreateInput>[];
final updatedLines = <TransactionLineUpdateInput>[];
final deletedLineIds = <int>{};
final currentById = <int, StockTransactionLine>{};
for (final line in currentLines) {
final id = line.id;
if (id != null) {
currentById[id] = line;
}
}
final remainingIds = currentById.keys.toSet();
for (final draft in drafts) {
final draftId = draft.id;
final normalizedNote = _normalizeOptionalText(draft.note);
if (draftId == null) {
createdLines.add(
TransactionLineCreateInput(
lineNo: draft.lineNo,
productId: draft.productId,
quantity: draft.quantity,
unitPrice: draft.unitPrice,
note: normalizedNote,
),
);
continue;
}
final current = currentById[draftId];
if (current == null) {
createdLines.add(
TransactionLineCreateInput(
lineNo: draft.lineNo,
productId: draft.productId,
quantity: draft.quantity,
unitPrice: draft.unitPrice,
note: normalizedNote,
),
);
continue;
}
remainingIds.remove(draftId);
if (current.product.id != draft.productId) {
deletedLineIds.add(draftId);
createdLines.add(
TransactionLineCreateInput(
lineNo: draft.lineNo,
productId: draft.productId,
quantity: draft.quantity,
unitPrice: draft.unitPrice,
note: normalizedNote,
),
);
continue;
}
int? nextLineNo;
if (current.lineNo != draft.lineNo) {
nextLineNo = draft.lineNo;
}
int? nextQuantity;
if (current.quantity != draft.quantity) {
nextQuantity = draft.quantity;
}
double? nextUnitPrice;
if ((current.unitPrice - draft.unitPrice).abs() > 0.0001) {
nextUnitPrice = draft.unitPrice;
}
final currentNote = _normalizeOptionalText(current.note);
String? nextNote;
if (currentNote != normalizedNote) {
nextNote = normalizedNote;
}
if (nextLineNo != null ||
nextQuantity != null ||
nextUnitPrice != null ||
nextNote != null) {
updatedLines.add(
TransactionLineUpdateInput(
id: draftId,
lineNo: nextLineNo,
quantity: nextQuantity,
unitPrice: nextUnitPrice,
note: nextNote,
),
);
}
}
for (final id in remainingIds) {
deletedLineIds.add(id);
}
return TransactionLineSyncPlan(
createdLines: createdLines,
updatedLines: updatedLines,
deletedLineIds: deletedLineIds.toList(),
);
}
/// 현재 서버 상태와 폼 입력을 비교해 고객 연결 변경 계획을 생성한다.
TransactionCustomerSyncPlan buildCustomerPlan({
required List<TransactionCustomerDraft> drafts,
required List<StockTransactionCustomer> currentCustomers,
}) {
final createdCustomers = <TransactionCustomerCreateInput>[];
final updatedCustomers = <TransactionCustomerUpdateInput>[];
final deletedCustomerIds = <int>{};
final currentById = <int, StockTransactionCustomer>{};
final currentByCustomerId = <int, StockTransactionCustomer>{};
for (final customer in currentCustomers) {
final id = customer.id;
if (id != null) {
currentById[id] = customer;
}
currentByCustomerId[customer.customer.id] = customer;
}
final remainingIds = currentById.keys.toSet();
for (final draft in drafts) {
final draftId = draft.id;
final normalizedNote = _normalizeOptionalText(draft.note);
if (draftId != null) {
final currentByLink = currentById[draftId];
if (currentByLink != null) {
remainingIds.remove(draftId);
final currentNote = _normalizeOptionalText(currentByLink.note);
if (currentNote != normalizedNote) {
updatedCustomers.add(
TransactionCustomerUpdateInput(id: draftId, note: normalizedNote),
);
}
continue;
}
}
final fallback = currentByCustomerId[draft.customerId];
if (fallback != null && fallback.id != null) {
final fallbackId = fallback.id!;
remainingIds.remove(fallbackId);
final currentNote = _normalizeOptionalText(fallback.note);
if (currentNote != normalizedNote) {
updatedCustomers.add(
TransactionCustomerUpdateInput(
id: fallbackId,
note: normalizedNote,
),
);
}
continue;
}
createdCustomers.add(
TransactionCustomerCreateInput(
customerId: draft.customerId,
note: normalizedNote,
),
);
}
for (final id in remainingIds) {
deletedCustomerIds.add(id);
}
return TransactionCustomerSyncPlan(
createdCustomers: createdCustomers,
updatedCustomers: updatedCustomers,
deletedCustomerIds: deletedCustomerIds.toList(),
);
}
}
String? _normalizeOptionalText(String? value) {
if (value == null) {
return null;
}
final trimmed = value.trim();
if (trimmed.isEmpty) {
return null;
}
return trimmed;
}