생산관리>M-BOM 관리 — PR-A2 단건 상세 + read-only 트리 4분기 (wace mBomPopupLeft.do 1:1)

행 더블클릭 → MbomDetailDialog (헤더 메타 + 동적 LEVEL × 19컬럼 트리 그리드).
운영판 ProductionPlanningController:1113~1276 의 4분기 자동 판별을 백엔드에서 처리:
  1) SAVED         mbom_header.status='Y' 우선 → getSavedMbomTreeList CTE
  2) ASSIGNED_EBOM source_bom_type='EBOM' → partMng.getBOMTreeList(working) CTE
  3) ASSIGNED_MBOM source_bom_type='MBOM' → getMbomStructureOnly CTE
  4) TEMPLATE      Machine 이외 + 동일 part_no → mbom_header 템플릿 CTE
  5) NONE          빈 트리

backend:
  - mbomService.getDetail (getProjectMgmtDetail 1:1, TOTAL_PROD_QTY = production_plan 우선)
  - mbomService.getTree   (4분기 orchestrator + 매퍼 4종 CTE 1:1)
  - GET /api/production/mbom/detail/:objid
  - GET /api/production/mbom/tree/:objid

frontend:
  - lib/api/mbom.ts  : MbomDetail / MbomTreeRow / MbomBomDataType / getDetail / getTree
  - components/production/MbomDetailDialog.tsx (max-w-1600px, 헤더 14필드 + 트리 그리드)
  - page.tsx 행 더블클릭 핸들러

검증: O-RING (593315995) SAVED 분기 5행 정상. TOTAL_PROD_QTY production_plan=5 / QUANTITY=2 fallback 확인.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-13 16:29:52 +09:00
parent 55239547d6
commit dd88dc6e8c
6 changed files with 992 additions and 17 deletions
+29 -2
View File
@@ -1,7 +1,9 @@
// ============================================================
// 생산관리 > M-BOM 관리 (PR-A1) — wace productionplanning.xml 1:1 이식.
// 생산관리 > M-BOM 관리 — wace productionplanning.xml 1:1 이식.
// 라우트:
// GET /api/production/mbom/list M-BOM 관리 그리드 (PROJECT_MGMT × CONTRACT_ITEM 펼침)
// GET /api/production/mbom/list 그리드 (PROJECT_MGMT × CONTRACT_ITEM 펼침)
// GET /api/production/mbom/detail/:objid 단건 상세 (mBomHeaderPopup.do 1:1)
// GET /api/production/mbom/tree/:objid read-only 트리 4분기 자동 판별 (mBomPopupLeft.do 1:1)
// ============================================================
import { Response } from "express";
@@ -25,3 +27,28 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
return res.status(500).json({ success: false, message: e.message });
}
}
export async function getDetail(req: AuthenticatedRequest, res: Response) {
try {
const objid = String(req.params.objid ?? "").trim();
if (!objid) return res.status(400).json({ success: false, message: "objid 누락" });
const data = await svc.getDetail(objid);
if (!data) return res.status(404).json({ success: false, message: "프로젝트를 찾을 수 없습니다" });
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 });
}
}
export async function getTree(req: AuthenticatedRequest, res: Response) {
try {
const objid = String(req.params.objid ?? "").trim();
if (!objid) return res.status(400).json({ success: false, message: "objid 누락" });
const data = await svc.getTree(objid);
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 });
}
}
@@ -1,5 +1,5 @@
// ============================================================
// 생산관리 > M-BOM 관리 (PR-A1) 라우트.
// 생산관리 > M-BOM 관리 라우트.
// app.ts: app.use("/api/production/mbom", productionMbomRoutes)
// ============================================================
@@ -10,6 +10,8 @@ import * as ctrl from "../controllers/mbomController";
const router = Router();
router.use(authenticateToken);
router.get("/list", ctrl.getList);
router.get("/list", ctrl.getList);
router.get("/detail/:objid", ctrl.getDetail);
router.get("/tree/:objid", ctrl.getTree);
export default router;
+597 -8
View File
@@ -1,14 +1,23 @@
// ============================================================
// 생산관리 > M-BOM 관리 (PR-A1) — wace productionplanning.xml 1:1 이식.
// 생산관리 > M-BOM 관리 — wace productionplanning.xml 1:1 이식.
//
// 매퍼 매핑 (원본: wace_plm/src/com/pms/mapper/productionplanning.xml):
// mBomMgmtGridList → list() (라인 2874~3119, PROJECT_MGMT × CONTRACT_ITEM 펼침)
// 매퍼 매핑 (원본: wace_plm/src/com/pms/mapper/productionplanning.xml,
// wace_plm/src/com/pms/mapper/partMng.xml):
// mBomMgmtGridList → list() (PR-A1, ~3119)
// getProjectMgmtDetail → getDetail() (PR-A2, ~3218)
// getLatestMbomByProjectId → getLatestSavedMbom() (PR-A2, ~3570)
// getLatestMbomTemplateByPartNo → getLatestTemplate() (PR-A2, ~3591)
// getSavedMbomTreeList → getSavedTree() (PR-A2, ~4359)
// getMbomStructureOnly → getStructureOnly() (PR-A2, ~4538)
// getMbomTemplateDetails → getTemplateDetails() (PR-A2, ~3794)
// partMng.getBOMTreeList → getEbomWorkingTree() (PR-A2, partMng.xml ~3549)
//
// 그리드 베이스: PROJECT_MGMT × CONTRACT_ITEM 펼침 (1 프로젝트 = 1+ 행).
// 9 검색 필터 + 30+ 출력 컬럼 (M-BOM 상태/저장일/작성자 + 구매리스트 매칭).
// vexplor_rps 의존: project_mgmt / contract_mgmt / contract_item / contract_item_serial
// / mbom_header (PR-A0) / mbom_history / sales_request_master / client_mng
// / supply_mng / part_bom_report / comm_code / user_info / user_name() fn
// 트리 분기 (mBomPopupLeft.do 1:1):
// 1) SAVED — mbom_header 에 status='Y' 가 있으면 그 트리
// 2) ASSIGNED_EBOM — project_mgmt.source_bom_type='EBOM' + source_ebom_objid
// 3) ASSIGNED_MBOM — project_mgmt.source_bom_type='MBOM' + source_mbom_objid
// 4) TEMPLATE — Machine 이외(product != '0000928') + part_no 있으면 동일 품번 mbom_header
// 5) NONE — 빈 트리
// ============================================================
import { getPool } from "../database/db";
@@ -275,3 +284,583 @@ export async function list(filter: MbomListFilter) {
pageSize,
};
}
// ─── 단건 상세 (getProjectMgmtDetail) ──────────────────────────
//
// 매퍼 productionplanning.xml getProjectMgmtDetail (라인 3150~3218) 1:1.
// TOTAL_PROD_QTY = production_plan 우선 → PM.QUANTITY fallback.
export async function getDetail(objid: string) {
const pool = getPool();
const sql = `
SELECT
PM.OBJID::VARCHAR AS objid,
PM.CONTRACT_OBJID AS contract_objid,
PM.PROJECT_NO AS project_no,
PM.BOM_REPORT_OBJID AS bom_report_objid,
PM.PART_OBJID AS part_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,
PM.QUANTITY AS quantity,
COALESCE(
(SELECT NULLIF(PP.TOTAL_PROD_QTY, '')::numeric
FROM PRODUCTION_PLAN PP
WHERE PP.PROJECT_OBJID = PM.OBJID::VARCHAR
AND UPPER(PP.STATUS) = 'ACTIVE'
LIMIT 1),
COALESCE(NULLIF(PM.QUANTITY, '')::numeric, 0)
) AS total_prod_qty,
COALESCE(
(SELECT PBR.PART_NO FROM PART_BOM_REPORT PBR
WHERE PBR.OBJID::VARCHAR = PM.BOM_REPORT_OBJID
AND PM.MBOM_STATUS = 'Y'
LIMIT 1), ''
) AS mbom_part_no,
CM.CATEGORY_CD AS category_cd,
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.CATEGORY_CD LIMIT 1), '') AS category_name,
CM.PRODUCT AS product,
CM.PRODUCT AS product_code,
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.PRODUCT LIMIT 1), '') AS product_name,
CM.AREA_CD AS area_cd,
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.AREA_CD LIMIT 1), '') AS area_name,
CM.CUSTOMER_OBJID AS customer_objid,
COALESCE(
CASE WHEN CM.CUSTOMER_OBJID LIKE 'C_%'
THEN (SELECT CLIENT_NM FROM CLIENT_MNG AS C
WHERE 'C_' || C.OBJID::VARCHAR = CM.CUSTOMER_OBJID LIMIT 1)
ELSE (SELECT SUPPLY_NAME FROM SUPPLY_MNG
WHERE OBJID::VARCHAR = CM.CUSTOMER_OBJID::VARCHAR LIMIT 1) END,
''
) AS customer_name,
CM.PAID_TYPE AS paid_type,
CM.REQ_DEL_DATE AS req_del_date,
TO_CHAR(PM.REGDATE, 'YYYY-MM-DD') AS receipt_date,
COALESCE(
(SELECT TO_CHAR(PBR.REGDATE, 'YYYY-MM-DD')
FROM PART_BOM_REPORT PBR
WHERE PBR.OBJID::VARCHAR = PM.BOM_REPORT_OBJID
AND PM.MBOM_STATUS = 'Y'
LIMIT 1),
TO_CHAR(PM.REGDATE, 'YYYY-MM-DD')
) AS mbom_regdate
FROM PROJECT_MGMT PM
INNER JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID
WHERE PM.OBJID::VARCHAR = $1
LIMIT 1
`;
const r = await pool.query(sql, [objid]);
return r.rows[0] ?? null;
}
// ─── 분기 진입점 (mBomPopupLeft.do 1:1) ────────────────────────
//
// 운영판 ProductionPlanningController:1113~1276 의 분기 로직:
// 1) 저장된 M-BOM (mbom_header.status='Y') 우선
// 2) 없으면 source_bom_type 으로 EBOM/MBOM 분기
// 3) 그래도 없으면 Machine 이외 + part_no 매칭으로 템플릿
// 4) 모두 없으면 빈 트리
export type BomDataType = "SAVED" | "ASSIGNED_EBOM" | "ASSIGNED_MBOM" | "TEMPLATE" | "NONE";
export interface MbomTreeResult {
bom_data_type: BomDataType;
bom_report_objid: string | null;
max_level: number;
rows: any[];
}
export async function getTree(objid: string): Promise<MbomTreeResult> {
const detail = await getDetail(objid);
if (!detail) {
return { bom_data_type: "NONE", bom_report_objid: null, max_level: 1, rows: [] };
}
// 1) SAVED — getLatestMbomByProjectId
const saved = await getLatestSavedMbom(objid);
if (saved && saved.objid) {
const rows = await getSavedTree(saved.objid);
return finalize("SAVED", saved.objid, rows);
}
// 2) ASSIGNED_EBOM
if (detail.source_bom_type === "EBOM" && detail.source_ebom_objid) {
const rows = await getEbomWorkingTree(detail.source_ebom_objid);
return finalize("ASSIGNED_EBOM", detail.source_ebom_objid, rows);
}
// 3) ASSIGNED_MBOM
if (detail.source_bom_type === "MBOM" && detail.source_mbom_objid) {
const rows = await getStructureOnly(detail.source_mbom_objid);
return finalize("ASSIGNED_MBOM", detail.source_mbom_objid, rows);
}
// 4) TEMPLATE — Machine 이외 + part_no
if (detail.product_code !== "0000928" && detail.part_no) {
const tpl = await getLatestTemplate(detail.part_no);
if (tpl && tpl.template_header_objid) {
const rows = await getTemplateDetails(tpl.template_header_objid);
return finalize("TEMPLATE", tpl.template_header_objid, rows);
}
}
// 5) NONE
return { bom_data_type: "NONE", bom_report_objid: detail.bom_report_objid ?? null, max_level: 1, rows: [] };
}
function finalize(type: BomDataType, bomReportObjid: string, rows: any[]): MbomTreeResult {
let maxLevel = 1;
for (const r of rows) {
const lv = Number(r.level ?? r.LEVEL ?? 1);
if (lv > maxLevel) maxLevel = lv;
}
return { bom_data_type: type, bom_report_objid: bomReportObjid, max_level: maxLevel, rows };
}
// ─── 분기 1) SAVED 진입 ──────────────────────────────────────
//
// 매퍼 getLatestMbomByProjectId (productionplanning.xml:3555~3570) 1:1.
async function getLatestSavedMbom(projectObjId: string) {
const pool = getPool();
const r = await pool.query(
`SELECT OBJID::VARCHAR AS objid, MBOM_NO AS mbom_no,
SOURCE_BOM_TYPE AS source_bom_type,
SOURCE_EBOM_OBJID AS source_ebom_objid,
SOURCE_MBOM_OBJID AS source_mbom_objid,
PROJECT_OBJID AS project_objid, STATUS AS status, REGDATE AS regdate
FROM MBOM_HEADER
WHERE PROJECT_OBJID = $1 AND STATUS = 'Y'
ORDER BY REGDATE DESC LIMIT 1`,
[projectObjId],
);
return r.rows[0] ?? null;
}
// ─── 분기 4) TEMPLATE 진입 ───────────────────────────────────
//
// 매퍼 getLatestMbomTemplateByPartNo (productionplanning.xml:3573~3591) 1:1.
async function getLatestTemplate(partNo: string) {
const pool = getPool();
const r = await pool.query(
`SELECT MH.OBJID::VARCHAR AS template_header_objid,
MH.MBOM_NO AS template_mbom_no,
MH.PART_NO AS part_no,
MH.PART_NAME AS part_name,
MH.SOURCE_BOM_TYPE AS source_bom_type,
MH.SOURCE_EBOM_OBJID AS source_ebom_objid,
MH.SOURCE_MBOM_OBJID AS source_mbom_objid,
TO_CHAR(MH.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS regdate
FROM MBOM_HEADER MH
INNER JOIN PROJECT_MGMT PM ON MH.PROJECT_OBJID = PM.OBJID::VARCHAR
INNER JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID
WHERE MH.PART_NO = $1
AND MH.STATUS = 'Y'
AND CM.PRODUCT != '0000928'
ORDER BY MH.REGDATE DESC LIMIT 1`,
[partNo],
);
return r.rows[0] ?? null;
}
// ─── 분기 1-SAVED 트리 ───────────────────────────────────────
//
// 매퍼 getSavedMbomTreeList (productionplanning.xml:4114~4359) 1:1.
// RECURSIVE CTE + PART_MNG 조인 + ATTACH_FILE_INFO 카운트 (CU01/02/03_CNT) + 소재소요량.
async function getSavedTree(mbomHeaderObjid: string) {
const pool = getPool();
const r = await pool.query(SAVED_TREE_SQL, [mbomHeaderObjid]);
return r.rows;
}
// ─── 분기 3-ASSIGNED_MBOM 구조만 ─────────────────────────────
//
// 매퍼 getMbomStructureOnly (productionplanning.xml:4362~4538) 1:1.
// 생산 정보는 NULL — 구조만 표시.
async function getStructureOnly(mbomHeaderObjid: string) {
const pool = getPool();
const r = await pool.query(STRUCTURE_ONLY_SQL, [mbomHeaderObjid]);
return r.rows;
}
// ─── 분기 4-TEMPLATE 트리 ────────────────────────────────────
//
// 매퍼 getMbomTemplateDetails (productionplanning.xml:3594~3794) 1:1.
// ORDER_QTY/PRODUCTION_QTY 가 빠진 점만 SAVED 와 다름.
async function getTemplateDetails(mbomHeaderObjid: string) {
const pool = getPool();
const r = await pool.query(TEMPLATE_TREE_SQL, [mbomHeaderObjid]);
return r.rows;
}
// ─── 분기 2-ASSIGNED_EBOM 트리 ───────────────────────────────
//
// 매퍼 partMng.getBOMTreeList (partMng.xml:3289~3549) - search_type='working' 1:1.
// bom_part_qty RECURSIVE CTE + PART_MNG 조인.
async function getEbomWorkingTree(bomReportObjid: string) {
const pool = getPool();
const r = await pool.query(EBOM_WORKING_TREE_SQL, [bomReportObjid]);
return r.rows;
}
// ─── 트리 SELECT 본문 (매퍼 4종 1:1, lowercase alias) ──────
const SAVED_TREE_SQL = `
WITH RECURSIVE VIEW_BOM(
MBOM_HEADER_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
PART_OBJID, PART_NO, PART_NAME, QTY, ITEM_QTY, QTY_TEMP,
REGDATE, SEQ, STATUS, LEV, PATH, PATH2, CYCLE,
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, WRITER, EDITER, EDIT_DATE, REMARK
) AS (
SELECT A.MBOM_HEADER_OBJID, A.OBJID, A.PARENT_OBJID, A.CHILD_OBJID,
A.PART_OBJID, A.PART_NO, A.PART_NAME, A.QTY, A.ITEM_QTY, A.QTY,
A.REGDATE, A.SEQ, A.STATUS, 1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.UNIT, A.SUPPLY_TYPE, A.MAKE_OR_BUY,
A.RAW_MATERIAL_PART_NO, A.RAW_MATERIAL_SPEC, A.RAW_MATERIAL, A.RAW_MATERIAL_SIZE,
A.PROCESSING_VENDOR, A.PROCESSING_DEADLINE, A.GRINDING_DEADLINE,
A.REQUIRED_QTY, A.ORDER_QTY, A.PRODUCTION_QTY, A.STOCK_QTY, A.SHORTAGE_QTY,
A.VENDOR, A.UNIT_PRICE, A.PROCESSING_UNIT_PRICE, A.TOTAL_PRICE, A.CURRENCY,
A.LEAD_TIME, A.MIN_ORDER_QTY, A.WRITER, A.EDITER, A.EDIT_DATE, A.REMARK
FROM MBOM_DETAIL A
WHERE 1=1
AND (A.PARENT_OBJID IS NULL OR A.PARENT_OBJID = '')
AND A.MBOM_HEADER_OBJID = $1
AND A.STATUS = 'ACTIVE'
UNION ALL
SELECT B.MBOM_HEADER_OBJID, B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID,
B.PART_OBJID, B.PART_NO, B.PART_NAME, B.QTY, B.ITEM_QTY, B.QTY,
B.REGDATE, B.SEQ, B.STATUS, LEV + 1,
PATH || B.CHILD_OBJID::TEXT,
PATH2 || LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH),
B.UNIT, B.SUPPLY_TYPE, B.MAKE_OR_BUY,
B.RAW_MATERIAL_PART_NO, B.RAW_MATERIAL_SPEC, B.RAW_MATERIAL, B.RAW_MATERIAL_SIZE,
B.PROCESSING_VENDOR, B.PROCESSING_DEADLINE, B.GRINDING_DEADLINE,
B.REQUIRED_QTY, B.ORDER_QTY, B.PRODUCTION_QTY, B.STOCK_QTY, B.SHORTAGE_QTY,
B.VENDOR, B.UNIT_PRICE, B.PROCESSING_UNIT_PRICE, B.TOTAL_PRICE, B.CURRENCY,
B.LEAD_TIME, B.MIN_ORDER_QTY, B.WRITER, B.EDITER, B.EDIT_DATE, B.REMARK
FROM MBOM_DETAIL B
JOIN VIEW_BOM ON B.PARENT_OBJID = VIEW_BOM.CHILD_OBJID
AND VIEW_BOM.MBOM_HEADER_OBJID = B.MBOM_HEADER_OBJID
AND B.STATUS = 'ACTIVE'
)
SELECT
V.MBOM_HEADER_OBJID AS bom_report_objid,
V.OBJID AS objid,
V.PARENT_OBJID AS parent_objid,
V.CHILD_OBJID AS child_objid,
V.PART_OBJID AS part_objid,
V.PART_NO AS part_no,
V.PART_NAME AS part_name,
V.QTY AS qty,
V.ITEM_QTY AS item_qty,
V.QTY_TEMP AS qty_temp,
V.LEV AS level,
(SELECT COUNT(*) FROM MBOM_DETAIL WHERE PARENT_OBJID = V.CHILD_OBJID) AS sub_part_cnt,
V.SEQ AS seq,
V.STATUS AS status,
V.UNIT AS unit,
V.SUPPLY_TYPE AS supply_type,
V.MAKE_OR_BUY AS make_or_buy,
V.RAW_MATERIAL_PART_NO AS raw_material_no,
V.RAW_MATERIAL_SPEC AS raw_material_spec,
V.RAW_MATERIAL AS raw_material,
V.RAW_MATERIAL_SIZE AS size,
V.PROCESSING_VENDOR AS processing_vendor,
(SELECT CLIENT_NM FROM CLIENT_MNG WHERE OBJID::VARCHAR = V.PROCESSING_VENDOR) AS processing_vendor_name,
V.PROCESSING_DEADLINE AS processing_deadline,
V.GRINDING_DEADLINE AS grinding_deadline,
V.REQUIRED_QTY AS required_qty,
V.ORDER_QTY AS order_qty,
V.PRODUCTION_QTY AS production_qty,
V.STOCK_QTY AS stock_qty,
V.SHORTAGE_QTY AS shortage_qty,
V.VENDOR AS vendor,
(SELECT CLIENT_NM FROM CLIENT_MNG WHERE OBJID::VARCHAR = V.VENDOR) AS vendor_name,
V.UNIT_PRICE AS unit_price,
V.PROCESSING_UNIT_PRICE AS processing_unit_price,
V.TOTAL_PRICE AS total_price,
V.CURRENCY AS currency,
V.LEAD_TIME AS lead_time,
V.MIN_ORDER_QTY AS min_order_qty,
V.WRITER AS writer,
TO_CHAR(V.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS regdate,
V.EDITER AS editer,
CASE WHEN V.EDIT_DATE IS NOT NULL THEN TO_CHAR(V.EDIT_DATE, 'YYYY-MM-DD HH24:MI:SS') END AS edit_date,
V.REMARK AS remark,
CASE WHEN V.LEV = 1 THEN V.OBJID END AS root_objid,
CASE WHEN V.LEV = 1 THEN V.OBJID END AS sub_root_objid,
1 AS leaf,
P.SPEC, P.MATERIAL, P.WEIGHT, P.PART_TYPE, P.REVISION, P.MAKER,
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 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,
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
INNER JOIN PART_MNG P ON P.OBJID = V.PART_OBJID
ORDER BY V.PATH2
`;
const STRUCTURE_ONLY_SQL = `
WITH RECURSIVE VIEW_BOM(
MBOM_HEADER_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
PART_OBJID, PART_NO, PART_NAME, QTY, ITEM_QTY, QTY_TEMP,
REGDATE, SEQ, STATUS, LEV, PATH, PATH2, CYCLE,
UNIT, WRITER, RAW_MATERIAL_PART_NO
) AS (
SELECT A.MBOM_HEADER_OBJID, A.OBJID, A.PARENT_OBJID, A.CHILD_OBJID,
A.PART_OBJID, A.PART_NO, A.PART_NAME, A.QTY, A.QTY, A.QTY,
A.REGDATE, A.SEQ, A.STATUS, 1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.UNIT, A.WRITER, A.RAW_MATERIAL_PART_NO
FROM MBOM_DETAIL A
WHERE 1=1
AND (A.PARENT_OBJID IS NULL OR A.PARENT_OBJID = '')
AND A.MBOM_HEADER_OBJID = $1
AND A.STATUS = 'ACTIVE'
UNION ALL
SELECT B.MBOM_HEADER_OBJID, B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID,
B.PART_OBJID, B.PART_NO, B.PART_NAME, B.QTY, B.QTY, B.QTY,
B.REGDATE, B.SEQ, B.STATUS, LEV + 1,
PATH || B.CHILD_OBJID::TEXT,
PATH2 || LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH),
B.UNIT, B.WRITER, B.RAW_MATERIAL_PART_NO
FROM MBOM_DETAIL B
JOIN VIEW_BOM ON B.PARENT_OBJID = VIEW_BOM.CHILD_OBJID
AND VIEW_BOM.MBOM_HEADER_OBJID = B.MBOM_HEADER_OBJID
AND B.STATUS = 'ACTIVE'
)
SELECT
V.MBOM_HEADER_OBJID AS bom_report_objid,
V.OBJID AS objid,
V.PARENT_OBJID AS parent_objid,
V.CHILD_OBJID AS child_objid,
V.PART_OBJID AS part_objid,
V.PART_NO AS part_no,
V.PART_NAME AS part_name,
V.QTY AS qty,
V.ITEM_QTY AS item_qty,
V.QTY_TEMP AS qty_temp,
V.LEV AS level,
(SELECT COUNT(*) FROM MBOM_DETAIL WHERE PARENT_OBJID = V.CHILD_OBJID) AS sub_part_cnt,
V.SEQ, V.STATUS, V.UNIT, V.WRITER,
TO_CHAR(V.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS regdate,
NULL::text AS supply_type, NULL::text AS make_or_buy,
NULL::text AS raw_material_no, NULL::text AS raw_material_spec,
NULL::text AS raw_material, NULL::text AS size,
NULL::text AS processing_vendor, NULL::text AS processing_deadline, NULL::text AS grinding_deadline,
NULL::numeric AS required_qty, NULL::numeric AS order_qty, NULL::numeric AS production_qty,
NULL::numeric AS stock_qty, NULL::numeric AS shortage_qty,
NULL::text AS vendor, NULL::numeric AS unit_price, NULL::numeric AS processing_unit_price,
NULL::numeric AS total_price, NULL::text AS currency,
NULL::int AS lead_time, NULL::numeric AS min_order_qty,
NULL::text AS editer, NULL::text AS edit_date, NULL::text AS remark,
CASE WHEN V.LEV = 1 THEN V.OBJID END AS root_objid,
CASE WHEN V.LEV = 1 THEN V.OBJID END AS sub_root_objid,
1 AS leaf,
P.SPEC, P.MATERIAL, P.WEIGHT, P.PART_TYPE, P.REVISION, P.MAKER,
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 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,
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
INNER JOIN PART_MNG P ON P.OBJID = V.PART_OBJID
ORDER BY V.PATH2
`;
// TEMPLATE 트리는 SAVED 와 동일한 CTE — 운영판도 mbom_detail 한 테이블에서 가져옴.
// 차이: TEMPLATE 은 ORDER_QTY/PRODUCTION_QTY 를 결과에서 표시하지 않음 (재계산 대상).
const TEMPLATE_TREE_SQL = `
WITH RECURSIVE VIEW_BOM(
MBOM_HEADER_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
PART_OBJID, PART_NO, PART_NAME, QTY, ITEM_QTY, QTY_TEMP,
REGDATE, SEQ, STATUS, LEV, PATH, PATH2, CYCLE,
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, WRITER, EDITER, EDIT_DATE, REMARK
) AS (
SELECT A.MBOM_HEADER_OBJID, A.OBJID, A.PARENT_OBJID, A.CHILD_OBJID,
A.PART_OBJID, A.PART_NO, A.PART_NAME, A.QTY, A.QTY, A.QTY,
A.REGDATE, A.SEQ, A.STATUS, 1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE,
A.UNIT, A.SUPPLY_TYPE, A.MAKE_OR_BUY,
A.RAW_MATERIAL_PART_NO, A.RAW_MATERIAL_SPEC, A.RAW_MATERIAL, A.RAW_MATERIAL_SIZE,
A.PROCESSING_VENDOR, A.PROCESSING_DEADLINE, A.GRINDING_DEADLINE,
A.REQUIRED_QTY, A.WRITER, A.EDITER, A.EDIT_DATE, A.REMARK
FROM MBOM_DETAIL A
WHERE 1=1
AND (A.PARENT_OBJID IS NULL OR A.PARENT_OBJID = '')
AND A.MBOM_HEADER_OBJID = $1
AND A.STATUS = 'ACTIVE'
UNION ALL
SELECT B.MBOM_HEADER_OBJID, B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID,
B.PART_OBJID, B.PART_NO, B.PART_NAME, B.QTY, B.QTY, B.QTY,
B.REGDATE, B.SEQ, B.STATUS, LEV + 1,
PATH || B.CHILD_OBJID::TEXT,
PATH2 || LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH),
B.UNIT, B.SUPPLY_TYPE, B.MAKE_OR_BUY,
B.RAW_MATERIAL_PART_NO, B.RAW_MATERIAL_SPEC, B.RAW_MATERIAL, B.RAW_MATERIAL_SIZE,
B.PROCESSING_VENDOR, B.PROCESSING_DEADLINE, B.GRINDING_DEADLINE,
B.REQUIRED_QTY, B.WRITER, B.EDITER, B.EDIT_DATE, B.REMARK
FROM MBOM_DETAIL B
JOIN VIEW_BOM ON B.PARENT_OBJID = VIEW_BOM.CHILD_OBJID
AND VIEW_BOM.MBOM_HEADER_OBJID = B.MBOM_HEADER_OBJID
AND B.STATUS = 'ACTIVE'
)
SELECT
V.MBOM_HEADER_OBJID AS bom_report_objid,
V.OBJID, V.PARENT_OBJID AS parent_objid, V.CHILD_OBJID AS child_objid,
V.PART_OBJID AS part_objid, V.PART_NO AS part_no, V.PART_NAME AS part_name,
V.QTY AS qty, V.ITEM_QTY AS item_qty, V.QTY_TEMP AS qty_temp,
V.LEV AS level,
(SELECT COUNT(*) FROM MBOM_DETAIL WHERE PARENT_OBJID = V.CHILD_OBJID) AS sub_part_cnt,
V.SEQ AS seq, V.STATUS AS status,
V.UNIT, V.SUPPLY_TYPE AS supply_type, V.MAKE_OR_BUY AS make_or_buy,
V.RAW_MATERIAL_PART_NO AS raw_material_no,
V.RAW_MATERIAL_SPEC AS raw_material_spec,
V.RAW_MATERIAL AS raw_material,
V.RAW_MATERIAL_SIZE AS size,
V.PROCESSING_VENDOR AS processing_vendor,
V.PROCESSING_DEADLINE AS processing_deadline,
V.GRINDING_DEADLINE AS grinding_deadline,
V.REQUIRED_QTY AS required_qty,
NULL::numeric AS order_qty, NULL::numeric AS production_qty,
V.WRITER AS writer,
TO_CHAR(V.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS regdate,
V.EDITER AS editer,
CASE WHEN V.EDIT_DATE IS NOT NULL THEN TO_CHAR(V.EDIT_DATE, 'YYYY-MM-DD HH24:MI:SS') END AS edit_date,
V.REMARK AS remark,
CASE WHEN V.LEV = 1 THEN V.OBJID END AS root_objid,
CASE WHEN V.LEV = 1 THEN V.OBJID END AS sub_root_objid,
1 AS leaf,
P.SPEC, P.MATERIAL, P.WEIGHT, P.PART_TYPE, P.REVISION, P.MAKER,
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 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,
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
LEFT JOIN PART_MNG P ON V.PART_OBJID = P.OBJID
ORDER BY V.PATH2
`;
// 매퍼 partMng.getBOMTreeList search_type='working' 1:1.
// E-BOM 호환 — part_no 컬럼명 충돌 회피 위해 운영판처럼 V.PART_NO 는 PART_OBJID 로 alias.
const EBOM_WORKING_TREE_SQL = `
WITH RECURSIVE VIEW_BOM(
BOM_REPORT_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
PARENT_PART_NO, PART_NO, LAST_PART_OBJID,
QTY, ITEM_QTY, QTY_TEMP, REGDATE, SEQ, STATUS,
PART_MNG_NO, PARENT_PART_MNG_NO, LEV, PATH, PATH2, CYCLE
) AS (
SELECT A.BOM_REPORT_OBJID, A.OBJID, A.PARENT_OBJID, A.CHILD_OBJID,
A.PARENT_PART_NO, A.PART_NO, A.LAST_PART_OBJID,
A.QTY, A.ITEM_QTY, A.QTY_TEMP, A.REGDATE, A.SEQ, A.STATUS,
(SELECT PART_NO FROM PART_MNG P WHERE P.OBJID::varchar = A.PART_NO) AS PART_MNG_NO,
(SELECT PART_NO FROM PART_MNG P WHERE P.OBJID::varchar = A.PARENT_PART_NO) AS PARENT_PART_MNG_NO,
1,
ARRAY [A.CHILD_OBJID::TEXT],
ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
FALSE
FROM BOM_PART_QTY A
WHERE 1=1
AND (A.PARENT_OBJID IS NULL OR A.PARENT_OBJID = '')
AND A.BOM_REPORT_OBJID = $1
AND (A.STATUS NOT IN ('deleting', 'deleted') OR A.STATUS IS NULL)
UNION ALL
SELECT B.BOM_REPORT_OBJID, B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID,
B.PARENT_PART_NO, B.PART_NO, B.LAST_PART_OBJID,
B.QTY, B.ITEM_QTY, B.QTY_TEMP, B.REGDATE, B.SEQ, B.STATUS,
(SELECT PART_NO FROM PART_MNG P WHERE P.OBJID::varchar = B.PART_NO) AS PART_MNG_NO,
(SELECT PART_NO FROM PART_MNG P WHERE P.OBJID::varchar = B.PARENT_PART_NO) AS PARENT_PART_MNG_NO,
LEV + 1,
PATH || B.CHILD_OBJID::TEXT,
PATH2 || LPAD(B.SEQ::TEXT, 10, '0'),
B.PARENT_OBJID = ANY(PATH)
FROM BOM_PART_QTY B
JOIN VIEW_BOM ON B.PARENT_OBJID = VIEW_BOM.CHILD_OBJID
AND VIEW_BOM.BOM_REPORT_OBJID = B.BOM_REPORT_OBJID
AND (B.STATUS NOT IN ('deleting', 'deleted') OR B.STATUS IS NULL)
)
SELECT
V.BOM_REPORT_OBJID AS bom_report_objid,
V.OBJID AS objid,
V.PARENT_OBJID AS parent_objid,
V.CHILD_OBJID AS child_objid,
V.PARENT_PART_NO AS parent_part_no,
V.PART_NO AS part_objid,
V.LAST_PART_OBJID AS bom_last_part_objid,
V.QTY AS qty,
V.ITEM_QTY AS item_qty,
(CASE WHEN V.STATUS = 'deploy' THEN V.QTY
WHEN V.STATUS = 'beforeEdit' THEN V.QTY
WHEN V.STATUS != 'editing' AND (V.QTY_TEMP IS NULL OR V.QTY_TEMP = '') THEN V.QTY
ELSE COALESCE(V.QTY_TEMP, V.QTY) END) AS qty_temp,
V.LEV AS level,
(SELECT COUNT(*) FROM BOM_PART_QTY WHERE PARENT_OBJID = V.CHILD_OBJID) AS sub_part_cnt,
V.SEQ AS seq, V.STATUS AS status,
P.OBJID AS last_part_objid,
P.PART_NAME AS part_name,
P.PART_NO AS part_no,
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CODE_ID = P.UNIT) AS unit_title,
P.SPEC, P.MATERIAL, P.WEIGHT, P.REVISION, P.MAKER,
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CODE_ID = P.PART_TYPE) AS part_type_title,
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,
-- 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,
NULL::text AS raw_material, NULL::text AS size,
NULL::text AS processing_vendor, NULL::text AS processing_vendor_name,
NULL::text AS processing_deadline, NULL::text AS grinding_deadline,
NULL::numeric AS required_qty, NULL::numeric AS order_qty, NULL::numeric AS production_qty,
NULL::numeric AS stock_qty, NULL::numeric AS shortage_qty,
NULL::text AS vendor, NULL::text AS vendor_name,
NULL::numeric AS unit_price, NULL::numeric AS processing_unit_price,
NULL::numeric AS total_price, NULL::text AS currency,
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)
ORDER BY V.PATH2
`;