Add BOM Copy Functionality to BOM Management

- Introduced a new API endpoint to copy a BOM tree to multiple target items, allowing for efficient duplication of BOM structures.
- Implemented payload validation to ensure correct data format and integrity during the copy process.
- Added a modal in the frontend for managing the BOM copy operation, including options for conflict resolution and progress tracking.
- Enhanced the BOM service with necessary logic for handling BOM copies, including versioning and error handling.

(TASK: ERP-028)
This commit is contained in:
kjs
2026-05-11 13:27:57 +09:00
parent 2a6577701b
commit 5a4a6d5a5b
9 changed files with 1174 additions and 144 deletions

View File

@@ -143,6 +143,73 @@ export async function initializeBomVersion(req: Request, res: Response) {
}
}
// ─── BOM 복사 (TASK:ERP-028) ─────────────────────────
/**
* POST /bom/:bomId/copy-to-items
* 기준 BOM의 트리(편집본)를 대상 품목 N개에 복제
* - conflictStrategy = "skip": 대상 품목에 BOM 있으면 skipped[]
* - conflictStrategy = "new_version": 대상 품목 BOM에 새 draft 버전 추가 (없으면 새 BOM 생성)
*/
export async function copyBomToItems(req: Request, res: Response) {
try {
const { bomId } = req.params;
const companyCode = (req as any).user?.companyCode || "*";
const userId = (req as any).user?.userName || (req as any).user?.userId || "";
const { targetItemIds, conflictStrategy, editedTree } = req.body || {};
// ─── 페이로드 검증 ─────────────────────
if (!Array.isArray(targetItemIds) || targetItemIds.length === 0) {
res.status(400).json({ success: false, message: "targetItemIds는 1개 이상의 배열이어야 합니다" });
return;
}
if (conflictStrategy !== "skip" && conflictStrategy !== "new_version") {
res.status(400).json({ success: false, message: "conflictStrategy는 'skip' 또는 'new_version'이어야 합니다" });
return;
}
if (!Array.isArray(editedTree) || editedTree.length === 0) {
res.status(400).json({ success: false, message: "editedTree는 1개 이상의 노드 배열이어야 합니다" });
return;
}
// 트리 노드 필수 필드 검증
for (const n of editedTree) {
if (!n || typeof n !== "object") {
res.status(400).json({ success: false, message: "editedTree 노드는 객체여야 합니다" });
return;
}
if (!n.tempId) {
res.status(400).json({ success: false, message: "editedTree 각 노드에는 tempId가 필요합니다" });
return;
}
if (!n.childItemId) {
res.status(400).json({ success: false, message: `노드 ${n.tempId}: childItemId가 필요합니다` });
return;
}
const qty = Number(n.quantity);
if (!Number.isFinite(qty) || qty <= 0) {
res.status(400).json({ success: false, message: `노드 ${n.tempId}: quantity는 0보다 커야 합니다` });
return;
}
}
const data = await bomService.copyBomToItems({
sourceBomId: bomId,
companyCode,
userId,
targetItemIds,
conflictStrategy,
editedTree,
});
res.json({ success: true, data });
} catch (error: any) {
logger.error("BOM 복사 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
// ─── BOM 엑셀 업로드/다운로드 ─────────────────────────
export async function createBomFromExcel(req: Request, res: Response) {