// ============================================================ // 생산관리 > 반제품소요량 + 원자재소요량 — wace productionplanning.xml 1:1. // // 매퍼 매핑: // getMbomListWithPartName → getMbomOptions() (4017) // getMbomSemiProductItems → getSemiRequirement() (5252) // getMbomRawMaterialItems → getRawRequirement() (5273) 1차 — 구매품 // getMbomRawSourceItems → getRawRequirement() (5298) 2차 — 원소재 // // 서비스 로직(ProductionPlanningService.java:2030~2270) 의 LinkedHashMap 합산을 // 자바스크립트 Map 으로 1:1 이식. // ============================================================ import { getPool } from "../database/db"; // ─── 입력 / 출력 ──────────────────────────────────────────── export interface MbomRequirementInputItem { mbomObjid: string; qty: number | string; } export interface SemiRequirementRow { PART_NO: string; PART_NAME: string; CATEGORY_NAME: string; UNIT: string; MATERIAL: string; SPEC: string; REQUIRED_QTY: number; } export interface RawRequirementRow { PART_NO: string; PART_NAME: string; CATEGORY_NAME: string; // '구매품' | '원소재' UNIT: string; MATERIAL: string; SPEC: string; REQUIRED_QTY: number | string; RAW_MATERIAL: string; RAW_MATERIAL_SIZE: string; MATERIAL_PART_NO: string; MATERIAL_REQUIRED_QTY: number | string; } function toInt(v: any): number { if (v == null) return 0; if (typeof v === "number") return Math.trunc(v); const n = Number(String(v)); return Number.isFinite(n) ? Math.trunc(n) : 0; } function toNum(v: any): number { if (v == null) return 0; if (typeof v === "number") return v; const n = Number(String(v)); return Number.isFinite(n) ? n : 0; } // ─── M-BOM 옵션 조회 (셀렉트박스용 + 품명 매핑) ──────────────── export async function getMbomOptions(): Promise> { const pool = getPool(); const r = await pool.query(` SELECT OBJID::VARCHAR AS objid, COALESCE(MBOM_NO, '') AS mbom_no, COALESCE(PART_NAME, '') AS part_name FROM MBOM_HEADER WHERE STATUS = 'Y' ORDER BY REGDATE DESC, MBOM_NO `); return r.rows; } // ─── 메뉴 3: 반제품 소요량 ────────────────────────────────── // // 매퍼: getMbomSemiProductItems (5252~5270) // 조건: MBOM_DETAIL × PART_MNG, PART_TYPE IN ('0001812', '0001813'), 1레벨 제외. // 자바 서비스: 동일 PART_NO 합산 (입력수량 × 항목수량). export async function getSemiRequirement(items: MbomRequirementInputItem[]): Promise { if (!Array.isArray(items) || items.length === 0) return []; const pool = getPool(); // PART_NO 기준 LinkedHashMap (자바 LinkedHashMap 동일 보장) const partMap = new Map(); for (const it of items) { const mbomObjid = String(it?.mbomObjid ?? "").trim(); const inputQty = toInt(it?.qty); if (!mbomObjid || inputQty <= 0) continue; const r = await pool.query( ` SELECT MD.PART_NO, MD.PART_NAME, COALESCE(NULLIF(MD.QTY, '')::INTEGER, 1) AS ITEM_QTY, P.PART_TYPE, COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = P.PART_TYPE LIMIT 1), '') AS CATEGORY_NAME, COALESCE(P.UNIT, '') AS UNIT, COALESCE(P.MATERIAL, '') AS MATERIAL, COALESCE(P.SPEC, '') AS SPEC FROM MBOM_DETAIL MD INNER JOIN PART_MNG P ON P.OBJID::VARCHAR = MD.PART_OBJID WHERE MD.MBOM_HEADER_OBJID = $1 AND MD.STATUS = 'ACTIVE' AND (MD.PARENT_OBJID IS NOT NULL AND MD.PARENT_OBJID != '') AND P.PART_TYPE IN ('0001812', '0001813') ORDER BY P.PART_TYPE, MD.PART_NO `, [mbomObjid], ); for (const row of r.rows) { const partNo = String(row.part_no ?? "").trim(); if (!partNo) continue; const itemQty = toInt(row.item_qty); const required = inputQty * itemQty; const existing = partMap.get(partNo); if (existing) { existing.REQUIRED_QTY += required; } else { partMap.set(partNo, { PART_NO: partNo, PART_NAME: row.part_name ?? "", CATEGORY_NAME: row.category_name ?? "", UNIT: row.unit ?? "", MATERIAL: row.material ?? "", SPEC: row.spec ?? "", REQUIRED_QTY: required, }); } } } return Array.from(partMap.values()); } // ─── 메뉴 4: 원자재 소요량 ────────────────────────────────── // // 매퍼: getMbomRawMaterialItems (5273~5295) + getMbomRawSourceItems (5298~5318) // 자바 서비스: 구매품/원소재 두 LinkedHashMap. 원소재는 소수점 합산 후 올림. export async function getRawRequirement(items: MbomRequirementInputItem[]): Promise { if (!Array.isArray(items) || items.length === 0) return []; const pool = getPool(); // 운영판 1:1 — 두 갈래 LinkedHashMap (구매품 / 원소재) const purchaseMap = new Map(); // 원소재는 소수점 합산을 위해 임시 number 보관 후 마지막에 올림 처리 const rawSourceMap = new Map(); for (const it of items) { const mbomObjid = String(it?.mbomObjid ?? "").trim(); const inputQty = toInt(it?.qty); if (!mbomObjid || inputQty <= 0) continue; // 1) 구매품 (PART_TYPE = '0000063') const r1 = await pool.query( ` SELECT MD.PART_NO, MD.PART_NAME, COALESCE(NULLIF(MD.QTY, '')::INTEGER, 1) AS ITEM_QTY, COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = P.PART_TYPE LIMIT 1), '') AS CATEGORY_NAME, COALESCE(P.UNIT, '') AS UNIT FROM MBOM_DETAIL MD INNER JOIN PART_MNG P ON P.OBJID::VARCHAR = MD.PART_OBJID WHERE MD.MBOM_HEADER_OBJID = $1 AND MD.STATUS = 'ACTIVE' AND (MD.PARENT_OBJID IS NOT NULL AND MD.PARENT_OBJID != '') AND P.PART_TYPE = '0000063' ORDER BY MD.PART_NO `, [mbomObjid], ); for (const row of r1.rows) { const partNo = String(row.part_no ?? "").trim(); if (!partNo) continue; const itemQty = toInt(row.item_qty); const required = inputQty * itemQty; const existing = purchaseMap.get(partNo); if (existing) { existing.REQUIRED_QTY = toInt(existing.REQUIRED_QTY) + required; } else { purchaseMap.set(partNo, { PART_NO: partNo, PART_NAME: row.part_name ?? "", CATEGORY_NAME: row.category_name ?? "", UNIT: row.unit ?? "", MATERIAL: "", SPEC: "", REQUIRED_QTY: required, RAW_MATERIAL: "", RAW_MATERIAL_SIZE: "", MATERIAL_PART_NO: "", MATERIAL_REQUIRED_QTY: "", }); } } // 2) 원소재 (RAW_MATERIAL_PART_NO 가 있는 항목) const r2 = await pool.query( ` SELECT MD.RAW_MATERIAL_PART_NO AS PART_NO, MD.RAW_MATERIAL AS PART_NAME, COALESCE(NULLIF(MD.REQUIRED_QTY, '')::NUMERIC, 0) AS ITEM_QTY, MD.RAW_MATERIAL, MD.RAW_MATERIAL_SIZE FROM MBOM_DETAIL MD WHERE MD.MBOM_HEADER_OBJID = $1 AND MD.STATUS = 'ACTIVE' AND MD.RAW_MATERIAL_PART_NO IS NOT NULL AND MD.RAW_MATERIAL_PART_NO != '' ORDER BY MD.RAW_MATERIAL_PART_NO `, [mbomObjid], ); for (const row of r2.rows) { const materialPartNo = String(row.part_no ?? "").trim(); if (!materialPartNo) continue; const itemQty = toNum(row.item_qty); const required = inputQty * itemQty; const existing = rawSourceMap.get(materialPartNo); if (existing) { existing.__rawSum += required; } else { rawSourceMap.set(materialPartNo, { PART_NO: "", PART_NAME: "", CATEGORY_NAME: "원소재", UNIT: "", MATERIAL: row.raw_material ?? "", SPEC: row.raw_material_size ?? "", REQUIRED_QTY: "", RAW_MATERIAL: row.raw_material ?? "", RAW_MATERIAL_SIZE: row.raw_material_size ?? "", MATERIAL_PART_NO: materialPartNo, MATERIAL_REQUIRED_QTY: "", __rawSum: required, }); } } } // 구매품 먼저, 원소재(올림) 뒤 const result: RawRequirementRow[] = []; for (const v of purchaseMap.values()) result.push(v); for (const v of rawSourceMap.values()) { const ceilQty = Math.ceil(v.__rawSum); const { __rawSum, ...rest } = v; result.push({ ...rest, MATERIAL_REQUIRED_QTY: ceilQty }); } return result; }