생산관리 M-BOM 본 편집(PR-B1) + 폴더 컬럼 + DataGrid 서버 페이지네이션 + bigint=varchar fix
PR-B1 본 편집/저장 (운영 saveMbom.do 1:1)
· 매퍼 7종 1:1 (insert/updateMbomHeader, insert/updateMbomDetail,
deleteMbomDetailByObjid, insertMbomHistory, updateProjectMbomStatus)
· 신규 CREATE: createObjId + generateMbomNo(M-{partNo}-YYMMDD-NN) +
child_objid 재매핑 + detail 일괄 insert + history(CREATE) + project_mgmt.mbom_status='Y'
· 수정 UPDATE: 기존 mbom_header.objid UPSERT(insert/update/delete) + history(UPDATE)
· POST /api/production/mbom/save (BEGIN/COMMIT/ROLLBACK 트랜잭션)
· MbomDetailDialog: '본 편집' 토글 + 13개 셀 인라인 편집 + 저장/취소 가드
M-BOM 컬럼 폴더 아이콘
· production/mbom/page.tsx: mbom_status 컬럼 → mbom_has(0/1) renderType=folder
· onClick → MbomDetailDialog 오픈 (행 더블클릭도 그대로 유지)
· 운영판 wace 견적/partMng 폴더 아이콘 패턴 1:1
DataGrid 서버 페이지네이션
· props 신설: serverPaging/serverPage/serverPageSize/serverTotalItems
+ onPageChange/onPageSizeChange
· 5메뉴 적용: production/mbom, development/change-list/ebom-regist/part-search/part-regist
· pageSizeOptions=[10,15,20,50,100,200,500] 통일
· 클라이언트 모드 하위호환 유지
bigint=varchar fix (mbom 트리 SQL 4종)
· ATTACH_FILE_INFO 서브쿼리: P.OBJID(bigint) = F.TARGET_OBJID(varchar) → P.OBJID::varchar 캐스트
· EBOM_WORKING_TREE_SQL INNER JOIN: P.OBJID = COALESCE(V.LAST_PART_OBJID,V.PART_NO) → ::varchar 캐스트
· 사용자 보고: 폴더 클릭 시 'operator does not exist: bigint = character varying' 토스트
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,3 +52,22 @@ export async function getTree(req: AuthenticatedRequest, res: Response) {
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// PR-B1 — 본 편집 저장 (운영 saveMbom.do 1:1)
|
||||
export async function save(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const payload = req.body as svc.MbomSavePayload;
|
||||
if (!payload?.project_obj_id) {
|
||||
return res.status(400).json({ success: false, message: "project_obj_id 누락" });
|
||||
}
|
||||
if (!Array.isArray(payload.rows)) {
|
||||
return res.status(400).json({ success: false, message: "rows 누락" });
|
||||
}
|
||||
const userId = req.user?.userId ?? "system";
|
||||
const data = await svc.save(payload, userId);
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("M-BOM 저장 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ router.use(authenticateToken);
|
||||
router.get("/list", ctrl.getList);
|
||||
router.get("/detail/:objid", ctrl.getDetail);
|
||||
router.get("/tree/:objid", ctrl.getTree);
|
||||
router.post("/save", ctrl.save); // PR-B1 본 편집 저장
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
// ============================================================
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
import { createObjId } from "../utils/objidUtil";
|
||||
|
||||
// ─── 필터/페이지 타입 ──────────────────────────────────────────
|
||||
|
||||
@@ -611,9 +612,9 @@ SELECT
|
||||
P.SOURCING_CODE, P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT,
|
||||
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.UNIT) AS unit_title,
|
||||
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.PART_TYPE) AS part_type_title,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD')) AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD')) AS cu03_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD')) AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD')) AS cu03_cnt,
|
||||
COALESCE((SELECT NULLIF(MP.UNIT_QTY, '')::numeric FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_qty,
|
||||
COALESCE((SELECT NULLIF(MP.UNIT_LENGTH, '')::numeric FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_length
|
||||
FROM VIEW_BOM V
|
||||
@@ -686,9 +687,9 @@ SELECT
|
||||
P.SOURCING_CODE, P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT,
|
||||
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.UNIT) AS unit_title,
|
||||
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.PART_TYPE) AS part_type_title,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD')) AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD')) AS cu03_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD')) AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD')) AS cu03_cnt,
|
||||
COALESCE((SELECT NULLIF(MP.UNIT_QTY, '')::numeric FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_qty,
|
||||
COALESCE((SELECT NULLIF(MP.UNIT_LENGTH, '')::numeric FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_length
|
||||
FROM VIEW_BOM V
|
||||
@@ -770,9 +771,9 @@ SELECT
|
||||
P.SOURCING_CODE, P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT,
|
||||
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.UNIT) AS unit_title,
|
||||
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.PART_TYPE) AS part_type_title,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD')) AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD')) AS cu03_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD')) AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD')) AS cu03_cnt,
|
||||
COALESCE((SELECT NULLIF(MP.UNIT_QTY, '')::numeric FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_qty,
|
||||
COALESCE((SELECT NULLIF(MP.UNIT_LENGTH, '')::numeric FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_length
|
||||
FROM VIEW_BOM V
|
||||
@@ -780,6 +781,449 @@ LEFT JOIN PART_MNG P ON V.PART_OBJID = P.OBJID
|
||||
ORDER BY V.PATH2
|
||||
`;
|
||||
|
||||
// ─── 저장 (PR-B1) ───────────────────────────────────────────
|
||||
//
|
||||
// 운영판 ProductionPlanningController.saveMbom (1549~1645) + 서비스 saveMbom (1192~1574) 1:1.
|
||||
// 매퍼: insertMbomHeader / updateMbomHeader / insertMbomDetail / updateMbomDetail
|
||||
// / deleteMbomDetailByObjid / insertMbomHistory / updateProjectMbomStatus.
|
||||
//
|
||||
// 분기 처리:
|
||||
// isUpdate=false (최초 저장) — 새 mbom_header 생성 + child_objid 재매핑 후 detail 일괄 insert
|
||||
// + history(CREATE) + project_mgmt.mbom_status='Y'
|
||||
// isUpdate=true (수정 저장) — 기존 mbom_header.objid 조회 → updateMbomHeader
|
||||
// → mbom_data 의 objid 기준 UPSERT(insert/update) + 누락분 delete
|
||||
// + history(UPDATE, 변경 행수 description)
|
||||
|
||||
export interface MbomSaveRow {
|
||||
objid?: string | null;
|
||||
parent_objid?: string | null;
|
||||
child_objid?: string | null;
|
||||
seq?: number | string | null;
|
||||
level?: number | string | null;
|
||||
part_objid?: string | number | null;
|
||||
part_no?: string | null;
|
||||
part_name?: string | null;
|
||||
qty?: number | string | null;
|
||||
item_qty?: number | string | null;
|
||||
unit?: string | null;
|
||||
supply_type?: string | null;
|
||||
make_or_buy?: string | null;
|
||||
raw_material_no?: string | null;
|
||||
raw_material_spec?: string | null;
|
||||
raw_material?: string | null;
|
||||
size?: string | null; // tree row alias → raw_material_size
|
||||
raw_material_size?: string | null;
|
||||
processing_vendor?: string | null;
|
||||
processing_deadline?: string | null;
|
||||
grinding_deadline?: string | null;
|
||||
required_qty?: number | string | null;
|
||||
order_qty?: number | string | null;
|
||||
production_qty?: number | string | null;
|
||||
stock_qty?: number | string | null;
|
||||
shortage_qty?: number | string | null;
|
||||
vendor?: string | null;
|
||||
unit_price?: number | string | null;
|
||||
processing_unit_price?: number | string | null;
|
||||
total_price?: number | string | null;
|
||||
currency?: string | null;
|
||||
lead_time?: number | string | null;
|
||||
min_order_qty?: number | string | null;
|
||||
remark?: string | null;
|
||||
}
|
||||
|
||||
export interface MbomSavePayload {
|
||||
project_obj_id: string;
|
||||
is_update: boolean;
|
||||
mbom_part_no?: string | null; // 최상위 제품 변경 시 (PR-B1 에서는 신규/유지만)
|
||||
rows: MbomSaveRow[];
|
||||
}
|
||||
|
||||
export interface MbomSaveResult {
|
||||
mode: "CREATE" | "UPDATE";
|
||||
mbom_header_objid: string;
|
||||
mbom_no: string;
|
||||
inserted: number;
|
||||
updated: number;
|
||||
deleted: number;
|
||||
}
|
||||
|
||||
// generateMbomNo — wace generateMbomNo(EBOM/TEMPLATE) 1:1.
|
||||
// 패턴: M-{cleanPartNo}-{YYMMDD}-{NN} (NN = 동일 prefix 마지막+1, 미존재 시 01)
|
||||
async function generateMbomNo(
|
||||
client: any,
|
||||
sourceBomType: string,
|
||||
basePartNo: string,
|
||||
): Promise<string> {
|
||||
let cleanPartNo = (basePartNo || "").trim();
|
||||
if (cleanPartNo.startsWith("M-")) cleanPartNo = cleanPartNo.substring(2);
|
||||
const now = new Date();
|
||||
const yy = String(now.getFullYear()).slice(-2);
|
||||
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(now.getDate()).padStart(2, "0");
|
||||
const dateStr = `${yy}${mm}${dd}`;
|
||||
const prefix = `M-${cleanPartNo}-${dateStr}`;
|
||||
|
||||
const r = await client.query(
|
||||
`SELECT MBOM_NO FROM MBOM_HEADER
|
||||
WHERE MBOM_NO LIKE $1 || '-%'
|
||||
ORDER BY MBOM_NO DESC LIMIT 1`,
|
||||
[prefix],
|
||||
);
|
||||
let seq = 1;
|
||||
if (r.rows[0]?.mbom_no) {
|
||||
const m = String(r.rows[0].mbom_no).match(/-(\d{2})$/);
|
||||
if (m) seq = Math.min(99, Number(m[1]) + 1);
|
||||
}
|
||||
return `${prefix}-${String(seq).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
const DETAIL_INSERT_SQL = `
|
||||
INSERT INTO MBOM_DETAIL (
|
||||
OBJID, MBOM_HEADER_OBJID, PARENT_OBJID, CHILD_OBJID, SEQ, LEVEL,
|
||||
PART_OBJID, PART_NO, PART_NAME, QTY, ITEM_QTY, UNIT,
|
||||
SUPPLY_TYPE, MAKE_OR_BUY,
|
||||
RAW_MATERIAL_PART_NO, RAW_MATERIAL_SPEC, RAW_MATERIAL, RAW_MATERIAL_SIZE,
|
||||
PROCESSING_VENDOR, PROCESSING_DEADLINE, GRINDING_DEADLINE,
|
||||
REQUIRED_QTY, ORDER_QTY, PRODUCTION_QTY, STOCK_QTY, SHORTAGE_QTY,
|
||||
VENDOR, UNIT_PRICE, PROCESSING_UNIT_PRICE, TOTAL_PRICE, CURRENCY,
|
||||
LEAD_TIME, MIN_ORDER_QTY,
|
||||
STATUS, WRITER, REGDATE, EDITER, EDIT_DATE, REMARK
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,
|
||||
$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,
|
||||
$27,$28,$29,$30,$31,$32,$33,
|
||||
'ACTIVE', $34, NOW(), $34, NOW(), $35
|
||||
)`;
|
||||
|
||||
const DETAIL_UPDATE_SQL = `
|
||||
UPDATE MBOM_DETAIL SET
|
||||
PARENT_OBJID = NULLIF($1, ''),
|
||||
SEQ = $2,
|
||||
LEVEL = $3,
|
||||
PART_OBJID = $4,
|
||||
PART_NO = NULLIF($5, ''),
|
||||
PART_NAME = NULLIF($6, ''),
|
||||
QTY = $7,
|
||||
ITEM_QTY = $8,
|
||||
UNIT = NULLIF($9, ''),
|
||||
SUPPLY_TYPE = NULLIF($10, ''),
|
||||
MAKE_OR_BUY = NULLIF($11, ''),
|
||||
RAW_MATERIAL_PART_NO = NULLIF($12, ''),
|
||||
RAW_MATERIAL_SPEC = NULLIF($13, ''),
|
||||
RAW_MATERIAL = NULLIF($14, ''),
|
||||
RAW_MATERIAL_SIZE = NULLIF($15, ''),
|
||||
PROCESSING_VENDOR = NULLIF($16, ''),
|
||||
PROCESSING_DEADLINE = NULLIF($17, ''),
|
||||
GRINDING_DEADLINE = NULLIF($18, ''),
|
||||
REQUIRED_QTY = $19,
|
||||
ORDER_QTY = $20,
|
||||
PRODUCTION_QTY = $21,
|
||||
STOCK_QTY = $22,
|
||||
SHORTAGE_QTY = $23,
|
||||
EDITER = $24,
|
||||
EDIT_DATE = NOW(),
|
||||
REMARK = NULLIF($25, '')
|
||||
WHERE OBJID = $26`;
|
||||
|
||||
function n(v: any): number | null {
|
||||
if (v == null || v === "") return null;
|
||||
const num = Number(v);
|
||||
return Number.isFinite(num) ? num : null;
|
||||
}
|
||||
function s(v: any): string | null {
|
||||
if (v == null) return null;
|
||||
const str = String(v).trim();
|
||||
return str === "" ? null : str;
|
||||
}
|
||||
function bi(v: any): string | null {
|
||||
// part_objid 는 bigint — 숫자/문자열 모두 수용, 빈값/NaN 은 null
|
||||
if (v == null || v === "") return null;
|
||||
const num = Number(v);
|
||||
return Number.isFinite(num) ? String(Math.trunc(num)) : null;
|
||||
}
|
||||
|
||||
function detailInsertParams(
|
||||
row: MbomSaveRow,
|
||||
objid: string,
|
||||
mbomHeaderObjid: string,
|
||||
childObjid: string,
|
||||
parentObjid: string | null,
|
||||
userId: string,
|
||||
): any[] {
|
||||
return [
|
||||
objid,
|
||||
mbomHeaderObjid,
|
||||
parentObjid,
|
||||
childObjid,
|
||||
n(row.seq) ?? 999,
|
||||
n(row.level) ?? 1,
|
||||
bi(row.part_objid),
|
||||
s(row.part_no),
|
||||
s(row.part_name),
|
||||
n(row.qty),
|
||||
n(row.item_qty),
|
||||
s(row.unit),
|
||||
s(row.supply_type),
|
||||
s(row.make_or_buy),
|
||||
s(row.raw_material_no),
|
||||
s(row.raw_material_spec),
|
||||
s(row.raw_material),
|
||||
s(row.raw_material_size ?? row.size),
|
||||
s(row.processing_vendor),
|
||||
s(row.processing_deadline),
|
||||
s(row.grinding_deadline),
|
||||
n(row.required_qty),
|
||||
n(row.order_qty),
|
||||
n(row.production_qty),
|
||||
n(row.stock_qty),
|
||||
n(row.shortage_qty),
|
||||
s(row.vendor),
|
||||
n(row.unit_price),
|
||||
n(row.processing_unit_price),
|
||||
n(row.total_price),
|
||||
s(row.currency) ?? "KRW",
|
||||
n(row.lead_time),
|
||||
n(row.min_order_qty),
|
||||
userId,
|
||||
s(row.remark),
|
||||
];
|
||||
}
|
||||
|
||||
function detailUpdateParams(row: MbomSaveRow, objid: string, userId: string): any[] {
|
||||
return [
|
||||
s(row.parent_objid) ?? "",
|
||||
n(row.seq) ?? 999,
|
||||
n(row.level) ?? 1,
|
||||
bi(row.part_objid),
|
||||
s(row.part_no) ?? "",
|
||||
s(row.part_name) ?? "",
|
||||
n(row.qty),
|
||||
n(row.item_qty),
|
||||
s(row.unit) ?? "",
|
||||
s(row.supply_type) ?? "",
|
||||
s(row.make_or_buy) ?? "",
|
||||
s(row.raw_material_no) ?? "",
|
||||
s(row.raw_material_spec) ?? "",
|
||||
s(row.raw_material) ?? "",
|
||||
s(row.raw_material_size ?? row.size) ?? "",
|
||||
s(row.processing_vendor) ?? "",
|
||||
s(row.processing_deadline) ?? "",
|
||||
s(row.grinding_deadline) ?? "",
|
||||
n(row.required_qty),
|
||||
n(row.order_qty),
|
||||
n(row.production_qty),
|
||||
n(row.stock_qty),
|
||||
n(row.shortage_qty),
|
||||
userId,
|
||||
s(row.remark) ?? "",
|
||||
objid,
|
||||
];
|
||||
}
|
||||
|
||||
export async function save(payload: MbomSavePayload, sessionUserId: string): Promise<MbomSaveResult> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const projectObjId = String(payload.project_obj_id ?? "").trim();
|
||||
if (!projectObjId) throw new Error("project_obj_id 누락");
|
||||
const userId = String(sessionUserId ?? "").trim() || "system";
|
||||
|
||||
// 1) 프로젝트 + 할당 정보 조회 (sourceBomType + basePartNo)
|
||||
const proj = await client.query(
|
||||
`SELECT PM.OBJID::VARCHAR AS objid,
|
||||
PM.PART_NO AS part_no, PM.PART_NAME AS part_name,
|
||||
PM.SOURCE_BOM_TYPE AS source_bom_type,
|
||||
PM.SOURCE_EBOM_OBJID AS source_ebom_objid,
|
||||
PM.SOURCE_MBOM_OBJID AS source_mbom_objid,
|
||||
CM.PRODUCT AS product_code,
|
||||
(SELECT PBR.PART_NO FROM PART_BOM_REPORT PBR
|
||||
WHERE PBR.OBJID::VARCHAR = PM.SOURCE_EBOM_OBJID LIMIT 1) AS ebom_part_no,
|
||||
(SELECT MH.MBOM_NO FROM MBOM_HEADER MH
|
||||
WHERE MH.OBJID = PM.SOURCE_MBOM_OBJID LIMIT 1) AS source_mbom_no
|
||||
FROM PROJECT_MGMT PM
|
||||
INNER JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID
|
||||
WHERE PM.OBJID::VARCHAR = $1 LIMIT 1`,
|
||||
[projectObjId],
|
||||
);
|
||||
if (!proj.rows[0]) throw new Error("프로젝트 정보를 찾을 수 없습니다");
|
||||
const p = proj.rows[0];
|
||||
|
||||
let sourceBomType: string = p.source_bom_type ?? "";
|
||||
let sourceEbomObjid: string | null = null;
|
||||
let sourceMbomObjid: string | null = null;
|
||||
let basePartNo = "";
|
||||
|
||||
if (sourceBomType === "EBOM") {
|
||||
sourceEbomObjid = p.source_ebom_objid ?? null;
|
||||
basePartNo = p.ebom_part_no ?? p.part_no ?? "";
|
||||
} else if (sourceBomType === "MBOM") {
|
||||
sourceMbomObjid = p.source_mbom_objid ?? null;
|
||||
basePartNo = p.source_mbom_no ?? p.part_no ?? "";
|
||||
} else {
|
||||
// Machine 이외(product != 0000928) + part_no 가 있으면 TEMPLATE
|
||||
if (p.product_code !== "0000928" && p.part_no) {
|
||||
sourceBomType = "TEMPLATE";
|
||||
basePartNo = p.part_no;
|
||||
} else {
|
||||
throw new Error("M-BOM 기준 정보가 없습니다. BOM 복사 팝업에서 먼저 기준을 설정해주세요.");
|
||||
}
|
||||
}
|
||||
|
||||
let mbomHeaderObjid: string;
|
||||
let mbomNo: string;
|
||||
let inserted = 0, updated = 0, deleted = 0;
|
||||
let mode: "CREATE" | "UPDATE";
|
||||
|
||||
if (payload.is_update) {
|
||||
// ── UPDATE 분기 ──
|
||||
const exist = await client.query(
|
||||
`SELECT OBJID AS objid, MBOM_NO AS mbom_no FROM MBOM_HEADER
|
||||
WHERE PROJECT_OBJID = $1 AND STATUS = 'Y'
|
||||
ORDER BY REGDATE DESC LIMIT 1`,
|
||||
[projectObjId],
|
||||
);
|
||||
if (!exist.rows[0]) throw new Error("수정 대상 M-BOM 헤더를 찾을 수 없습니다");
|
||||
mbomHeaderObjid = exist.rows[0].objid;
|
||||
mbomNo = exist.rows[0].mbom_no;
|
||||
mode = "UPDATE";
|
||||
|
||||
// 헤더 update
|
||||
await client.query(
|
||||
`UPDATE MBOM_HEADER SET
|
||||
PART_NO = $1, PART_NAME = $2,
|
||||
EDITER = $3, EDIT_DATE = NOW()
|
||||
WHERE OBJID = $4`,
|
||||
[p.part_no ?? "", p.part_name ?? "", userId, mbomHeaderObjid],
|
||||
);
|
||||
|
||||
// 기존 detail objid 수집
|
||||
const existRes = await client.query(
|
||||
`SELECT OBJID AS objid FROM MBOM_DETAIL WHERE MBOM_HEADER_OBJID = $1`,
|
||||
[mbomHeaderObjid],
|
||||
);
|
||||
const existIds = new Set<string>(existRes.rows.map((r: any) => r.objid));
|
||||
const incomingIds = new Set<string>();
|
||||
|
||||
// UPSERT
|
||||
for (const row of payload.rows ?? []) {
|
||||
let objid = s(row.objid) ?? "";
|
||||
if (!objid) objid = createObjId();
|
||||
let childObjid = s(row.child_objid) ?? objid;
|
||||
incomingIds.add(objid);
|
||||
|
||||
if (existIds.has(objid)) {
|
||||
await client.query(DETAIL_UPDATE_SQL, detailUpdateParams({ ...row, child_objid: childObjid }, objid, userId));
|
||||
updated++;
|
||||
} else {
|
||||
await client.query(
|
||||
DETAIL_INSERT_SQL,
|
||||
detailInsertParams(row, objid, mbomHeaderObjid, childObjid, s(row.parent_objid), userId),
|
||||
);
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
// 누락 행 delete
|
||||
for (const oldId of existIds) {
|
||||
if (!incomingIds.has(oldId)) {
|
||||
await client.query(`DELETE FROM MBOM_DETAIL WHERE OBJID = $1`, [oldId]);
|
||||
deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
// history UPDATE
|
||||
await insertHistory(
|
||||
client,
|
||||
mbomHeaderObjid,
|
||||
"UPDATE",
|
||||
`${inserted + updated + deleted}개 항목 처리 (insert=${inserted}, update=${updated}, delete=${deleted})`,
|
||||
userId,
|
||||
);
|
||||
} else {
|
||||
// ── CREATE 분기 ──
|
||||
mbomHeaderObjid = createObjId();
|
||||
mbomNo = await generateMbomNo(client, sourceBomType, basePartNo);
|
||||
mode = "CREATE";
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO MBOM_HEADER (
|
||||
OBJID, MBOM_NO, SOURCE_BOM_TYPE, SOURCE_EBOM_OBJID, SOURCE_MBOM_OBJID,
|
||||
PROJECT_OBJID, PART_NO, PART_NAME, STATUS, MBOM_STATUS,
|
||||
WRITER, REGDATE
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'Y','DRAFT',$9,NOW())`,
|
||||
[
|
||||
mbomHeaderObjid, mbomNo, sourceBomType,
|
||||
sourceEbomObjid, sourceMbomObjid,
|
||||
projectObjId, p.part_no ?? "", p.part_name ?? "",
|
||||
userId,
|
||||
],
|
||||
);
|
||||
|
||||
// child_objid 재매핑 (E-BOM/MBOM 의 기존 child_objid → 새 child_objid)
|
||||
const childMap = new Map<string, string>();
|
||||
for (const row of payload.rows ?? []) {
|
||||
const oldChild = s(row.child_objid);
|
||||
if (oldChild && !childMap.has(oldChild)) childMap.set(oldChild, createObjId());
|
||||
}
|
||||
|
||||
for (const row of payload.rows ?? []) {
|
||||
const oldChild = s(row.child_objid);
|
||||
const oldParent = s(row.parent_objid);
|
||||
const newChild = (oldChild && childMap.get(oldChild)) || createObjId();
|
||||
const newParent = oldParent ? (childMap.get(oldParent) ?? oldParent) : null;
|
||||
|
||||
await client.query(
|
||||
DETAIL_INSERT_SQL,
|
||||
detailInsertParams(row, newChild, mbomHeaderObjid, newChild, newParent, userId),
|
||||
);
|
||||
inserted++;
|
||||
}
|
||||
|
||||
// history CREATE
|
||||
await insertHistory(
|
||||
client,
|
||||
mbomHeaderObjid,
|
||||
"CREATE",
|
||||
`M-BOM 신규 저장 (${inserted}개 항목)`,
|
||||
userId,
|
||||
);
|
||||
|
||||
// PROJECT_MGMT.MBOM_STATUS = 'Y'
|
||||
await client.query(
|
||||
`UPDATE PROJECT_MGMT SET MBOM_STATUS = 'Y', MBOM_WRITER = $1, MBOM_REGDATE = NOW()
|
||||
WHERE OBJID::VARCHAR = $2`,
|
||||
[userId, projectObjId],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return { mode, mbom_header_objid: mbomHeaderObjid, mbom_no: mbomNo, inserted, updated, deleted };
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function insertHistory(
|
||||
client: any,
|
||||
mbomHeaderObjid: string,
|
||||
changeType: "CREATE" | "UPDATE",
|
||||
description: string,
|
||||
userId: string,
|
||||
) {
|
||||
await client.query(
|
||||
`INSERT INTO MBOM_HISTORY (
|
||||
OBJID, MBOM_HEADER_OBJID, CHANGE_TYPE, CHANGE_DESCRIPTION,
|
||||
BEFORE_DATA, AFTER_DATA, CHANGE_USER, CHANGE_DATE
|
||||
) VALUES ($1, $2, $3, $4, NULL, NULL, $5, NOW())`,
|
||||
[createObjId(), mbomHeaderObjid, changeType, description, userId],
|
||||
);
|
||||
}
|
||||
|
||||
// 매퍼 partMng.getBOMTreeList search_type='working' 1:1.
|
||||
// E-BOM 호환 — part_no 컬럼명 충돌 회피 위해 운영판처럼 V.PART_NO 는 PART_OBJID 로 alias.
|
||||
const EBOM_WORKING_TREE_SQL = `
|
||||
@@ -844,9 +1288,9 @@ SELECT
|
||||
P.REMARK AS part_remark,
|
||||
P.THICKNESS, P.WIDTH, P.HEIGHT, P.OUT_DIAMETER, P.IN_DIAMETER, P.LENGTH,
|
||||
P.SOURCING_CODE, P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD')) AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD')) AS cu03_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD')) AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD')) AS cu03_cnt,
|
||||
-- E-BOM 분기는 생산정보가 없으므로 NULL 로 채워 SAVED 와 동일한 키셋 유지
|
||||
NULL::text AS supply_type, NULL::text AS make_or_buy,
|
||||
NULL::text AS raw_material_no, NULL::text AS raw_material_spec,
|
||||
@@ -861,6 +1305,6 @@ SELECT
|
||||
NULL::int AS lead_time, NULL::numeric AS min_order_qty,
|
||||
NULL::text AS writer, NULL::text AS regdate, NULL::text AS editer, NULL::text AS edit_date, NULL::text AS remark
|
||||
FROM VIEW_BOM V
|
||||
INNER JOIN PART_MNG P ON P.OBJID = COALESCE(V.LAST_PART_OBJID, V.PART_NO)
|
||||
INNER JOIN PART_MNG P ON P.OBJID::varchar = COALESCE(V.LAST_PART_OBJID, V.PART_NO)
|
||||
ORDER BY V.PATH2
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user