Add All BOM Substitutes API and Update BOM Management

- Introduced a new API endpoint to retrieve all substitutes for a given BOM ID, allowing for bulk retrieval of substitute items.
- Enhanced the BOM service to support the new functionality, including company code filtering and versioning options.
- Updated the BOM management page to integrate the new substitute retrieval feature, enabling users to manage substitutes more effectively during the copy process.
- Added necessary state management and UI elements for handling substitutes in the copy modal.

(TASK: ERP-028)
This commit is contained in:
kjs
2026-05-11 15:06:13 +09:00
parent 571d4fab83
commit 663a51e94d
4 changed files with 753 additions and 168 deletions

View File

@@ -306,6 +306,20 @@ export async function getBomDetailSubstitutes(req: Request, res: Response) {
}
}
// bom_id 전체의 대체품 일괄 조회 (복사 모달이 detail_id별 매핑 만들 때 사용)
export async function getAllBomSubstitutes(req: Request, res: Response) {
try {
const { bomId } = req.params;
const companyCode = (req as any).user?.companyCode || "*";
const versionId = (req.query.versionId as string) || undefined;
const data = await bomService.getAllBomSubstitutes(bomId, companyCode, versionId);
res.json({ success: true, data });
} catch (error: any) {
logger.error("BOM 전체 대체품 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: error.message });
}
}
export async function getBomSubstituteCounts(req: Request, res: Response) {
try {
const { bomId } = req.params;

View File

@@ -35,6 +35,7 @@ router.delete("/:bomId/versions/:versionId", bomController.deleteBomVersion);
// 대체품 (Substitute)
router.get("/:bomId/substitute-counts", bomController.getBomSubstituteCounts);
router.get("/:bomId/substitutes-all", bomController.getAllBomSubstitutes);
router.get("/details/:detailId/substitutes", bomController.getBomDetailSubstitutes);
router.post("/details/:detailId/substitutes", bomController.createBomDetailSubstitute);
router.put("/substitutes/:id", bomController.updateBomDetailSubstitute);

View File

@@ -848,6 +848,40 @@ export async function deleteBomVersion(
// ─── 대체품 (Substitute) ─────────────────────────────
/**
* bom_id 하나에 속한 모든 detail의 대체품을 일괄 조회.
* 반환: detail_id 기준으로 grouping된 substitute 행 배열 (회사 단위 필터).
* 복사 모달이 트리 전체의 substitute를 한 번에 미리 받아오기 위해 추가 (TASK:ERP-028 옵션 B).
*/
export async function getAllBomSubstitutes(bomId: string, companyCode: string, versionId?: string) {
const params: any[] = [bomId];
let sql = `
SELECT s.*, d.bom_id, d.version_id,
i.item_name AS substitute_item_name,
i.item_number AS substitute_item_number,
i.unit AS substitute_unit,
i.inventory_unit AS substitute_inventory_unit
FROM bom_detail_substitute s
JOIN bom_detail d ON d.id = s.bom_detail_id
LEFT JOIN item_info i ON s.substitute_item_id = i.id
WHERE d.bom_id = $1
`;
if (companyCode !== "*") {
params.push(companyCode);
sql += ` AND d.company_code = $${params.length}`;
}
if (versionId) {
params.push(versionId);
sql += ` AND d.version_id = $${params.length}`;
}
sql += `
ORDER BY
CASE WHEN s.priority ~ '^[0-9]+$' THEN s.priority::int ELSE 9999 END ASC,
s.created_date ASC
`;
return query(sql, params);
}
export async function getBomDetailSubstitutes(detailId: string, companyCode: string) {
const sql = `
SELECT s.*,
@@ -971,6 +1005,15 @@ export interface BomTreeNodeInput {
processType?: string | null;
lossRate?: number | string | null;
remark?: string | null;
/** 이 자품목의 대체품 목록 — 복사 시 새 detail_id로 INSERT (TASK:ERP-028 옵션 B) */
substitutes?: BomSubstituteInput[];
}
export interface BomSubstituteInput {
substituteItemId: string;
priority?: string | number | null;
ratio?: string | number | null;
remark?: string | null;
}
export interface CopyBomToItemsParams {
@@ -1153,7 +1196,30 @@ export async function insertVersionAndDetails(
params.companyCode,
],
);
tempToNewId[node.tempId] = insertRes.rows[0].id;
const newDetailId = insertRes.rows[0].id;
tempToNewId[node.tempId] = newDetailId;
// 대체품 INSERT — 페이로드에 substitutes가 있으면 새 detail_id로 매핑하여 추가 (TASK:ERP-028 옵션 B)
if (Array.isArray(node.substitutes) && node.substitutes.length > 0) {
for (const s of node.substitutes) {
if (!s.substituteItemId) continue;
await client.query(
`INSERT INTO bom_detail_substitute
(bom_detail_id, substitute_item_id, priority, ratio, remark, status, company_code, writer)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
newDetailId,
s.substituteItemId,
s.priority != null ? String(s.priority) : null,
s.ratio != null ? String(s.ratio) : null,
s.remark || null,
"active",
params.companyCode,
params.createdBy,
],
);
}
}
}
return { versionId: newVersionId, insertedCount: params.sortedTree.length };

File diff suppressed because it is too large Load Diff