구매관리 7메뉴 신규 + M-BOM PR-B3·B5 + 발주관리 DataGrid 통일 + 생산계획&실적 라우트
구매관리 (wace 1:1) - backend: services/purchaseService.ts (7 list + 옵션 3종) + controllers/purchaseController.ts + routes/purchaseRoutes.ts (/api/purchase 마운트) - frontend: lib/api/purchase.ts + 7 page.tsx (list/quote-request/proposal/inbound/inbound-by-item/inbound-by-date/project-status) - 영업관리 4메뉴 DataGrid 패턴 통일 — pageSizeOptions=[10,15,20,50,100], emptyMessage, showColumnSettings/summaryStats/onRefresh/onDownload/showChart - 마스터단독 데이터(sales_request_master, project_mgmt+mbom_detail) 노출, detail/part 누락 테이블 의존은 빈 그리드 + UI 발주관리 (purchase/order/page.tsx) - EDataTable → DataGrid 교체 + logicstudio 6종 props + 날짜/숫자 pre-format M-BOM PR-B3 — 구매리스트 생성 (wace createPurchaseListFromMBom.do 1:1) - mbomService.createSalesRequest + controller + route POST /api/production/mbom/sales-request - 단건 체크 + 1:1 강제 + R-YYYYMMDD-NNN 채번 + sales_request_master 단건 INSERT - production/mbom/page.tsx 에 [구매리스트 생성] 버튼 M-BOM PR-B5 — BOM 할당 (mBomEbomSelectPopup.do) - mbomService.searchAssignableEboms/assignBom + controller + routes - MbomAssignDialog 신규, MbomDetailDialog 통합 생산관리 4메뉴 라우트 (생산계획&실적, 소요량) - prodPlanResultService/Controller + productionPlanResultRoutes (planResult/mbomReq) - mbomRequirementService + 4 page.tsx (prod-plan-result, prod-plan-result-equip, raw-material-requirement, semi-product-requirement) - lib/api/prodPlanResult.ts Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
// ============================================================
|
||||
// 생산관리 > 반제품소요량 + 원자재소요량 — 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<Array<{ objid: string; mbom_no: string; part_name: string }>> {
|
||||
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<SemiRequirementRow[]> {
|
||||
if (!Array.isArray(items) || items.length === 0) return [];
|
||||
const pool = getPool();
|
||||
|
||||
// PART_NO 기준 LinkedHashMap (자바 LinkedHashMap 동일 보장)
|
||||
const partMap = new Map<string, SemiRequirementRow>();
|
||||
|
||||
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<RawRequirementRow[]> {
|
||||
if (!Array.isArray(items) || items.length === 0) return [];
|
||||
const pool = getPool();
|
||||
|
||||
// 운영판 1:1 — 두 갈래 LinkedHashMap (구매품 / 원소재)
|
||||
const purchaseMap = new Map<string, RawRequirementRow>();
|
||||
// 원소재는 소수점 합산을 위해 임시 number 보관 후 마지막에 올림 처리
|
||||
const rawSourceMap = new Map<string, RawRequirementRow & { __rawSum: number }>();
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user