b38f5957f2
구매관리 (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>
1567 lines
64 KiB
TypeScript
1567 lines
64 KiB
TypeScript
// ============================================================
|
|
// 생산관리 > M-BOM 관리 — wace productionplanning.xml 1:1 이식.
|
|
//
|
|
// 매퍼 매핑 (원본: 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)
|
|
//
|
|
// 트리 분기 (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";
|
|
import { createObjId } from "../utils/objidUtil";
|
|
|
|
// ─── 필터/페이지 타입 ──────────────────────────────────────────
|
|
|
|
export interface MbomListFilter {
|
|
search_category_cd?: string;
|
|
search_product_cd?: string;
|
|
search_area_cd?: string;
|
|
search_customer_objid?: string;
|
|
search_paid_type?: string;
|
|
search_serial_no?: string;
|
|
search_part_no?: string;
|
|
search_part_name?: string;
|
|
search_receipt_date_from?: string;
|
|
search_receipt_date_to?: string;
|
|
search_req_del_date_from?: string;
|
|
search_req_del_date_to?: string;
|
|
page?: number;
|
|
page_size?: number;
|
|
}
|
|
|
|
function paginate(filter: { page?: number; page_size?: number }) {
|
|
const page = Math.max(1, Number(filter.page) || 1);
|
|
const pageSize = Math.min(500, Math.max(1, Number(filter.page_size) || 50));
|
|
return { limit: pageSize, offset: (page - 1) * pageSize, page, pageSize };
|
|
}
|
|
|
|
// ─── WHERE 절 빌더 (매퍼 mBomMgmtGridList <if> 조건 1:1) ──────
|
|
|
|
function buildWhere(filter: MbomListFilter, startIdx: number) {
|
|
const params: any[] = [];
|
|
const conds: string[] = [];
|
|
let idx = startIdx;
|
|
|
|
if (filter.search_category_cd) {
|
|
conds.push(`CM.CATEGORY_CD = $${idx++}`);
|
|
params.push(filter.search_category_cd);
|
|
}
|
|
if (filter.search_product_cd) {
|
|
conds.push(`CM.PRODUCT = $${idx++}`);
|
|
params.push(filter.search_product_cd);
|
|
}
|
|
if (filter.search_area_cd) {
|
|
// 운영판: CODE_NAME(CM.AREA_CD) = '국내'/'해외' (사람이 보는 값으로 검색)
|
|
conds.push(`(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.AREA_CD LIMIT 1) = $${idx++}`);
|
|
params.push(filter.search_area_cd);
|
|
}
|
|
if (filter.search_receipt_date_from) {
|
|
conds.push(`PM.REGDATE >= TO_DATE($${idx++}, 'YYYY-MM-DD')`);
|
|
params.push(filter.search_receipt_date_from);
|
|
}
|
|
if (filter.search_receipt_date_to) {
|
|
conds.push(`PM.REGDATE < TO_DATE($${idx++}, 'YYYY-MM-DD') + INTERVAL '1 day'`);
|
|
params.push(filter.search_receipt_date_to);
|
|
}
|
|
if (filter.search_customer_objid) {
|
|
// 운영판 3-way 매칭 (C_xxx 양방향) 1:1
|
|
conds.push(
|
|
`(CM.CUSTOMER_OBJID = $${idx} OR CM.CUSTOMER_OBJID = REPLACE($${idx}, 'C_', '') OR 'C_' || CM.CUSTOMER_OBJID = $${idx})`
|
|
);
|
|
params.push(filter.search_customer_objid);
|
|
idx++;
|
|
}
|
|
if (filter.search_paid_type) {
|
|
conds.push(`CM.PAID_TYPE = $${idx++}`);
|
|
params.push(filter.search_paid_type);
|
|
}
|
|
if (filter.search_serial_no) {
|
|
conds.push(
|
|
`EXISTS (SELECT 1 FROM CONTRACT_ITEM_SERIAL CIS
|
|
WHERE CIS.ITEM_OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID
|
|
AND UPPER(CIS.STATUS) = 'ACTIVE'
|
|
AND UPPER(CIS.SERIAL_NO) LIKE '%' || UPPER($${idx++}) || '%')`
|
|
);
|
|
params.push(filter.search_serial_no);
|
|
}
|
|
if (filter.search_req_del_date_from) {
|
|
conds.push(`COALESCE(CI.DUE_DATE, PM.DUE_DATE, CM.REQ_DEL_DATE) >= $${idx++}`);
|
|
params.push(filter.search_req_del_date_from);
|
|
}
|
|
if (filter.search_req_del_date_to) {
|
|
conds.push(`COALESCE(CI.DUE_DATE, PM.DUE_DATE, CM.REQ_DEL_DATE) <= $${idx++}`);
|
|
params.push(filter.search_req_del_date_to);
|
|
}
|
|
if (filter.search_part_no) {
|
|
conds.push(
|
|
`(UPPER(PM.PART_NO) LIKE '%' || UPPER($${idx}) || '%' OR UPPER(CI.PART_NO) LIKE '%' || UPPER($${idx}) || '%')`
|
|
);
|
|
params.push(filter.search_part_no);
|
|
idx++;
|
|
}
|
|
if (filter.search_part_name) {
|
|
conds.push(
|
|
`(UPPER(PM.PART_NAME) LIKE '%' || UPPER($${idx}) || '%' OR UPPER(CI.PART_NAME) LIKE '%' || UPPER($${idx}) || '%')`
|
|
);
|
|
params.push(filter.search_part_name);
|
|
idx++;
|
|
}
|
|
|
|
return { sql: conds.length ? "AND " + conds.join(" AND ") : "", params };
|
|
}
|
|
|
|
// ─── M-BOM 관리 그리드 ──────────────────────────────────────────
|
|
//
|
|
// 매퍼 productionplanning.xml mBomMgmtGridList (라인 2874~3119) 1:1 이식.
|
|
|
|
export async function list(filter: MbomListFilter) {
|
|
const { limit, offset, page, pageSize } = paginate(filter);
|
|
const where = buildWhere(filter, 1);
|
|
const pool = getPool();
|
|
|
|
const baseSql = `
|
|
FROM PROJECT_MGMT PM
|
|
LEFT JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID
|
|
LEFT OUTER JOIN CONTRACT_ITEM CI ON (
|
|
CASE
|
|
WHEN PM.CONTRACT_ITEM_OBJID IS NOT NULL THEN CI.OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID
|
|
ELSE CI.CONTRACT_OBJID = PM.CONTRACT_OBJID AND CI.PART_OBJID = PM.PART_OBJID
|
|
END
|
|
)
|
|
AND CI.STATUS = 'ACTIVE'
|
|
WHERE 1=1
|
|
AND PM.PROJECT_NO IS NOT NULL
|
|
AND PM.PROJECT_NO != ''
|
|
${where.sql}
|
|
`;
|
|
|
|
const dataSql = `
|
|
SELECT
|
|
PM.OBJID,
|
|
PM.CONTRACT_OBJID,
|
|
PM.PROJECT_NO,
|
|
CM.CATEGORY_CD,
|
|
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.CATEGORY_CD LIMIT 1), '') AS CATEGORY_NAME,
|
|
CM.PRODUCT,
|
|
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.PRODUCT LIMIT 1), '') AS PRODUCT_NAME,
|
|
CM.AREA_CD,
|
|
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.AREA_CD LIMIT 1), '') AS AREA_NAME,
|
|
TO_CHAR(PM.REGDATE, 'YYYY-MM-DD') AS RECEIPT_DATE,
|
|
(SELECT user_name(MH.WRITER)
|
|
FROM MBOM_HEADER MH
|
|
WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR
|
|
AND MH.STATUS = 'Y'
|
|
ORDER BY MH.REGDATE DESC
|
|
LIMIT 1) AS WRITER_NAME,
|
|
CM.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,
|
|
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.PAID_TYPE LIMIT 1),
|
|
CASE
|
|
WHEN CM.PAID_TYPE = 'paid' THEN '유상'
|
|
WHEN CM.PAID_TYPE = 'free' THEN '무상'
|
|
ELSE '' END
|
|
) AS PAID_TYPE_NAME,
|
|
-- 품목 정보: CONTRACT_ITEM 우선, 없으면 PM
|
|
COALESCE(CI.PART_NO, PM.PART_NO, '') AS PART_NO,
|
|
COALESCE(CI.PART_NAME, PM.PART_NAME, '') AS PART_NAME,
|
|
CI.PART_OBJID,
|
|
-- S/N 표시: "첫S/N 외 N건"
|
|
(SELECT
|
|
CASE
|
|
WHEN COUNT(*) = 0 THEN ''
|
|
WHEN COUNT(*) = 1 THEN MIN(CIS.SERIAL_NO)
|
|
ELSE MIN(CIS.SERIAL_NO) || ' 외 ' || (COUNT(*) - 1)::TEXT || '건' END
|
|
FROM CONTRACT_ITEM_SERIAL CIS
|
|
WHERE CIS.ITEM_OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID
|
|
AND UPPER(CIS.STATUS) = 'ACTIVE'
|
|
AND CIS.SERIAL_NO IS NOT NULL
|
|
AND CIS.SERIAL_NO != ''
|
|
) AS SERIAL_NO,
|
|
-- S/N 전체 (콤마 리스트) — 팝업용
|
|
(SELECT STRING_AGG(CIS.SERIAL_NO, ', ' ORDER BY CIS.SERIAL_NO)
|
|
FROM CONTRACT_ITEM_SERIAL CIS
|
|
WHERE CIS.ITEM_OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID
|
|
AND UPPER(CIS.STATUS) = 'ACTIVE'
|
|
AND CIS.SERIAL_NO IS NOT NULL
|
|
AND CIS.SERIAL_NO != ''
|
|
) AS SERIAL_NO_LIST,
|
|
COALESCE(NULLIF(PM.QUANTITY, '')::numeric, NULLIF(CI.ORDER_QUANTITY, '')::numeric, 0) AS QUANTITY,
|
|
COALESCE(CI.DUE_DATE, PM.DUE_DATE, CM.REQ_DEL_DATE) AS REQ_DEL_DATE,
|
|
COALESCE(CI.CUSTOMER_REQUEST, '') AS CUSTOMER_REQUEST,
|
|
-- E-BOM 정보
|
|
COALESCE(CI.PART_OBJID, PM.PART_OBJID) AS BOM_REPORT_OBJID,
|
|
COALESCE((SELECT PBR.STATUS FROM PART_BOM_REPORT PBR
|
|
WHERE PBR.OBJID::VARCHAR = COALESCE(CI.PART_OBJID, PM.PART_OBJID) LIMIT 1), '') AS EBOM_STATUS,
|
|
COALESCE((SELECT TO_CHAR(PBR.REGDATE, 'YYYY-MM-DD') FROM PART_BOM_REPORT PBR
|
|
WHERE PBR.OBJID::VARCHAR = COALESCE(CI.PART_OBJID, PM.PART_OBJID) LIMIT 1), '') AS EBOM_REGDATE,
|
|
-- M-BOM HEADER OBJID
|
|
(SELECT MH.OBJID::VARCHAR FROM MBOM_HEADER MH
|
|
WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
|
|
ORDER BY MH.REGDATE DESC LIMIT 1) AS MBOM_HEADER_OBJID,
|
|
-- 구매리스트 OBJID + 생성일
|
|
(SELECT SRM.OBJID::VARCHAR FROM SALES_REQUEST_MASTER SRM
|
|
WHERE SRM.MBOM_HEADER_OBJID = (
|
|
SELECT MH.OBJID::VARCHAR FROM MBOM_HEADER MH
|
|
WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
|
|
ORDER BY MH.REGDATE DESC LIMIT 1
|
|
) LIMIT 1) AS PURCHASE_LIST_OBJID,
|
|
(SELECT TO_CHAR(SRM.REGDATE, 'YYYY-MM-DD') FROM SALES_REQUEST_MASTER SRM
|
|
WHERE SRM.MBOM_HEADER_OBJID = (
|
|
SELECT MH.OBJID::VARCHAR FROM MBOM_HEADER MH
|
|
WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
|
|
ORDER BY MH.REGDATE DESC LIMIT 1
|
|
) LIMIT 1) AS PURCHASE_LIST_DATE,
|
|
-- M-BOM 상태 ('Y' 있으면 Y, 아니면 PM.mbom_status raw)
|
|
COALESCE(
|
|
(SELECT CASE WHEN COUNT(*) > 0 THEN 'Y' ELSE COALESCE(PM.MBOM_STATUS, '') END
|
|
FROM MBOM_HEADER MH
|
|
WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y' LIMIT 1),
|
|
COALESCE(PM.MBOM_STATUS, '')
|
|
) AS MBOM_STATUS,
|
|
-- M-BOM 품번
|
|
COALESCE(
|
|
(SELECT MH.MBOM_NO FROM MBOM_HEADER MH
|
|
WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
|
|
ORDER BY MH.REGDATE DESC LIMIT 1), ''
|
|
) AS MBOM_PART_NO,
|
|
-- M-BOM 저장일 (수정일 우선, 없으면 등록일)
|
|
(SELECT TO_CHAR(COALESCE(MH.EDIT_DATE, MH.REGDATE), 'YYYY-MM-DD')
|
|
FROM MBOM_HEADER MH
|
|
WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
|
|
ORDER BY COALESCE(MH.EDIT_DATE, MH.REGDATE) DESC LIMIT 1) AS MBOM_REGDATE,
|
|
-- M-BOM 작성자/수정자
|
|
(SELECT user_name(COALESCE(MH.EDITER, MH.WRITER))
|
|
FROM MBOM_HEADER MH
|
|
WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
|
|
ORDER BY COALESCE(MH.EDITER, MH.WRITER) DESC LIMIT 1) AS MBOM_EDITOR,
|
|
-- M-BOM 변경이력 카운트 (0이면 NULL)
|
|
NULLIF(
|
|
(SELECT COUNT(1)::INTEGER FROM MBOM_HISTORY MHI
|
|
WHERE MHI.MBOM_HEADER_OBJID = (
|
|
SELECT MH.OBJID::VARCHAR FROM MBOM_HEADER MH
|
|
WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
|
|
ORDER BY MH.REGDATE DESC LIMIT 1
|
|
)), 0
|
|
) AS MBOM_VERSION
|
|
${baseSql}
|
|
ORDER BY PM.REGDATE DESC, CI.PART_NO
|
|
LIMIT $${where.params.length + 1} OFFSET $${where.params.length + 2}
|
|
`;
|
|
|
|
const countSql = `SELECT COUNT(*)::int AS cnt ${baseSql}`;
|
|
|
|
const dataParams = [...where.params, limit, offset];
|
|
|
|
const [dataRes, countRes] = await Promise.all([
|
|
pool.query(dataSql, dataParams),
|
|
pool.query(countSql, where.params),
|
|
]);
|
|
|
|
return {
|
|
rows: dataRes.rows,
|
|
totalCount: countRes.rows[0]?.cnt ?? 0,
|
|
page,
|
|
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::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
|
|
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::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
|
|
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::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
|
|
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>();
|
|
|
|
// 신규 행의 client temp- child_objid → 서버 발급 createObjId 매핑
|
|
// (객체 ID 안정성 + DB 에 temp- 잔존 방지)
|
|
const tempChildMap = new Map<string, string>();
|
|
for (const row of payload.rows ?? []) {
|
|
if (!s(row.objid)) {
|
|
const newId = createObjId();
|
|
const tempChild = s(row.child_objid);
|
|
if (tempChild) tempChildMap.set(tempChild, newId);
|
|
// 임시로 row 자체에 새 objid 부여 — 이어지는 루프에서 다시 읽지 않도록
|
|
(row as any).__newObjid = newId;
|
|
}
|
|
}
|
|
|
|
// UPSERT
|
|
for (const row of payload.rows ?? []) {
|
|
let objid = s(row.objid) ?? "";
|
|
const isNew = !objid;
|
|
if (isNew) objid = (row as any).__newObjid as string;
|
|
// 새 행이면 child_objid 도 새 ID 로 통일, 기존 행이면 그대로
|
|
let childObjid = isNew ? objid : (s(row.child_objid) ?? objid);
|
|
// parent_objid: temp- 참조면 신규 발급 ID 로 remap, 아니면 그대로
|
|
const rawParent = s(row.parent_objid);
|
|
const parentObjid = rawParent && tempChildMap.has(rawParent)
|
|
? tempChildMap.get(rawParent)!
|
|
: rawParent;
|
|
incomingIds.add(objid);
|
|
|
|
if (existIds.has(objid)) {
|
|
await client.query(DETAIL_UPDATE_SQL, detailUpdateParams({ ...row, parent_objid: parentObjid }, objid, userId));
|
|
updated++;
|
|
} else {
|
|
await client.query(
|
|
DETAIL_INSERT_SQL,
|
|
detailInsertParams(row, objid, mbomHeaderObjid, childObjid, parentObjid, 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();
|
|
}
|
|
}
|
|
|
|
// ─── BOM 할당 (PR-B5) ───────────────────────────────────────
|
|
//
|
|
// 매퍼 productionplanning.getEbomList (3221~3265) 1:1 — 할당 가능한 E-BOM 검색.
|
|
// 매퍼 productionplanning.saveBomAssignment (3545~3553) 1:1 — project_mgmt.source_bom_type/source_*_objid 업데이트.
|
|
//
|
|
// 운영판 흐름 (mBomEbomSelectPopup.do + assignEbomToMbom.do):
|
|
// 1) 사용자가 BOM 할당 다이얼로그 오픈 → E-BOM 검색 (part_bom_report)
|
|
// 2) 한 건 선택 → assign(projectObjid, 'EBOM', bomReportObjid) 호출
|
|
// 3) project_mgmt.source_bom_type='EBOM' + source_ebom_objid 저장
|
|
// 4) M-BOM 다이얼로그 재조회 → ASSIGNED_EBOM 트리 자동 표시
|
|
// (M-BOM 할당은 PR-B5 v2 — 우선 E-BOM 만)
|
|
|
|
export interface AssignableEbomFilter {
|
|
search_part_no?: string;
|
|
search_part_name?: string;
|
|
search_material?: string;
|
|
search_supplier?: string;
|
|
limit?: number;
|
|
}
|
|
|
|
export interface AssignableEbomRow {
|
|
objid: string;
|
|
product_cd: string | null;
|
|
product_name: string | null;
|
|
part_no: string | null;
|
|
part_name: string | null;
|
|
status: string | null;
|
|
revision: string | null;
|
|
reg_date: string | null;
|
|
writer_name: string | null;
|
|
dept_name: string | null;
|
|
material: string | null;
|
|
supplier: string | null;
|
|
}
|
|
|
|
export async function searchAssignableEboms(filter: AssignableEbomFilter): Promise<AssignableEbomRow[]> {
|
|
const pool = getPool();
|
|
const conds: string[] = [];
|
|
const params: any[] = [];
|
|
let idx = 1;
|
|
if (filter.search_part_no) {
|
|
conds.push(`UPPER(T.PART_NO) LIKE '%' || UPPER($${idx++}) || '%'`);
|
|
params.push(filter.search_part_no);
|
|
}
|
|
if (filter.search_part_name) {
|
|
conds.push(`UPPER(T.PART_NAME) LIKE '%' || UPPER($${idx++}) || '%'`);
|
|
params.push(filter.search_part_name);
|
|
}
|
|
if (filter.search_material) {
|
|
conds.push(`UPPER(PM.MATERIAL) LIKE '%' || UPPER($${idx++}) || '%'`);
|
|
params.push(filter.search_material);
|
|
}
|
|
if (filter.search_supplier) {
|
|
conds.push(`UPPER(PM.MAKER) LIKE '%' || UPPER($${idx++}) || '%'`);
|
|
params.push(filter.search_supplier);
|
|
}
|
|
const limit = Math.min(500, Math.max(1, Number(filter.limit) || 100));
|
|
const sql = `
|
|
SELECT
|
|
T.OBJID::VARCHAR AS objid,
|
|
T.PRODUCT_CD AS product_cd,
|
|
COALESCE((SELECT CODE_NAME FROM COMM_CODE
|
|
WHERE CODE_ID = T.PRODUCT_CD LIMIT 1), '') AS product_name,
|
|
T.PART_NO AS part_no,
|
|
T.PART_NAME AS part_name,
|
|
T.STATUS AS status,
|
|
T.REVISION AS revision,
|
|
TO_CHAR(T.REGDATE, 'YYYY-MM-DD') AS reg_date,
|
|
UI.USER_NAME AS writer_name,
|
|
UI.DEPT_NAME AS dept_name,
|
|
COALESCE(PM.MATERIAL, '') AS material,
|
|
COALESCE(PM.MAKER, '') AS supplier
|
|
FROM PART_BOM_REPORT T
|
|
LEFT JOIN USER_INFO UI ON UI.USER_ID = T.WRITER
|
|
LEFT JOIN PART_MNG PM ON PM.PART_NO = T.PART_NO AND PM.STATUS = 'release'
|
|
WHERE 1=1
|
|
AND T.PART_NO IS NOT NULL AND TRIM(T.PART_NO) != ''
|
|
AND T.PART_NAME IS NOT NULL AND TRIM(T.PART_NAME) != ''
|
|
${conds.length ? "AND " + conds.join(" AND ") : ""}
|
|
ORDER BY T.REGDATE DESC
|
|
LIMIT ${limit}
|
|
`;
|
|
const r = await pool.query(sql, params);
|
|
return r.rows;
|
|
}
|
|
|
|
export type AssignSourceType = "EBOM" | "MBOM";
|
|
|
|
export async function assignBom(
|
|
projectObjId: string,
|
|
sourceBomType: AssignSourceType,
|
|
sourceBomObjId: string,
|
|
_userId: string,
|
|
): Promise<{ success: boolean; source_bom_type: string; source_obj_id: string }> {
|
|
const pool = getPool();
|
|
// 매퍼 saveBomAssignment 1:1.
|
|
// EBOM 이면 source_ebom_objid, MBOM 이면 source_mbom_objid 만 세팅 (다른 쪽 NULL).
|
|
const sql = `
|
|
UPDATE PROJECT_MGMT
|
|
SET SOURCE_BOM_TYPE = $1,
|
|
SOURCE_EBOM_OBJID = CASE WHEN $1 = 'EBOM' THEN $2 ELSE NULL END,
|
|
SOURCE_MBOM_OBJID = CASE WHEN $1 = 'MBOM' THEN $2 ELSE NULL END
|
|
WHERE OBJID::VARCHAR = $3
|
|
`;
|
|
const r = await pool.query(sql, [sourceBomType, sourceBomObjId, projectObjId]);
|
|
if (r.rowCount === 0) throw new Error("프로젝트를 찾을 수 없습니다");
|
|
return { success: true, source_bom_type: sourceBomType, source_obj_id: sourceBomObjId };
|
|
}
|
|
|
|
// ─── 변경이력 조회 (PR-B4) ──────────────────────────────────
|
|
//
|
|
// 매퍼 productionplanning.getMbomHistory (3448~3470) 1:1.
|
|
// project_objid 로 그 프로젝트의 모든 mbom_header 변경이력 시간순 (최신 우선).
|
|
|
|
export interface MbomHistoryRow {
|
|
objid: string;
|
|
mbom_header_objid: string;
|
|
change_type: string;
|
|
change_description: string | null;
|
|
change_user: string | null;
|
|
change_user_name: string | null;
|
|
change_date: string;
|
|
mbom_part_no: string | null;
|
|
mbom_regdate: string | null;
|
|
}
|
|
|
|
export async function getHistory(projectObjid: string): Promise<MbomHistoryRow[]> {
|
|
const pool = getPool();
|
|
const sql = `
|
|
SELECT
|
|
MH.OBJID AS objid,
|
|
MH.MBOM_HEADER_OBJID AS mbom_header_objid,
|
|
MH.CHANGE_TYPE AS change_type,
|
|
MH.CHANGE_DESCRIPTION AS change_description,
|
|
MH.CHANGE_USER AS change_user,
|
|
COALESCE(
|
|
(SELECT USER_NAME FROM USER_INFO WHERE USER_ID = MH.CHANGE_USER LIMIT 1),
|
|
MH.CHANGE_USER
|
|
) AS change_user_name,
|
|
TO_CHAR(MH.CHANGE_DATE, 'YYYY-MM-DD HH24:MI:SS') AS change_date,
|
|
MHD.MBOM_NO AS mbom_part_no,
|
|
TO_CHAR(MHD.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS mbom_regdate
|
|
FROM MBOM_HISTORY MH
|
|
INNER JOIN MBOM_HEADER MHD ON MH.MBOM_HEADER_OBJID = MHD.OBJID
|
|
WHERE MHD.PROJECT_OBJID = $1
|
|
ORDER BY MH.CHANGE_DATE DESC
|
|
`;
|
|
const r = await pool.query(sql, [projectObjid]);
|
|
return r.rows;
|
|
}
|
|
|
|
// ─── 구매리스트 생성 (PR-B3) ────────────────────────────────
|
|
//
|
|
// wace 매퍼 salesMng.xml `getNextRequestMngNo` + `insertSalesRequestMasterFromMBom` 1:1
|
|
// (서비스 SalesMngService.createPurchaseListFromMBom, 매퍼 라인 3960~3997).
|
|
//
|
|
// 검증: M-BOM 헤더 존재 + 동일 mbom_header_objid 로 이미 생성된 sales_request_master 없음.
|
|
// 입력: mbomHeaderObjid (mbom_header.objid), projectMgmtObjid (project_mgmt.objid).
|
|
// 처리: 단일 INSERT, BEGIN/COMMIT 트랜잭션. mbom_header 역링크 컬럼 없음 (운영 동일).
|
|
|
|
export interface CreateSalesRequestResult {
|
|
objid: string;
|
|
request_mng_no: string;
|
|
mbom_header_objid: string;
|
|
}
|
|
|
|
export async function createSalesRequest(
|
|
mbomHeaderObjid: string,
|
|
projectMgmtObjid: string,
|
|
sessionUserId: string,
|
|
): Promise<CreateSalesRequestResult> {
|
|
const pool = getPool();
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query("BEGIN");
|
|
|
|
const mbom = String(mbomHeaderObjid ?? "").trim();
|
|
const proj = String(projectMgmtObjid ?? "").trim();
|
|
if (!mbom) throw new Error("MBOM_HEADER_OBJID 누락");
|
|
if (!proj) throw new Error("PROJECT_MGMT_OBJID 누락");
|
|
const userId = String(sessionUserId ?? "").trim() || "system";
|
|
|
|
// 1) M-BOM 헤더 존재 확인
|
|
const mh = await client.query(
|
|
`SELECT OBJID FROM MBOM_HEADER WHERE OBJID::VARCHAR = $1 AND STATUS = 'Y' LIMIT 1`,
|
|
[mbom],
|
|
);
|
|
if (!mh.rows[0]) throw new Error("저장된 M-BOM 헤더를 찾을 수 없습니다");
|
|
|
|
// 2) 동일 MBOM_HEADER 로 이미 생성된 구매리스트 차단 (운영판 sweetalert 가드 1:1)
|
|
const dup = await client.query(
|
|
`SELECT OBJID, REQUEST_MNG_NO FROM SALES_REQUEST_MASTER
|
|
WHERE MBOM_HEADER_OBJID = $1 LIMIT 1`,
|
|
[mbom],
|
|
);
|
|
if (dup.rows[0]) {
|
|
throw new Error(`이미 생성된 구매리스트가 있습니다 (${dup.rows[0].request_mng_no})`);
|
|
}
|
|
|
|
// 3) 채번: R + YYYYMMDD + - + 3자리 (운영 getNextRequestMngNo 1:1)
|
|
const seqRes = await client.query(
|
|
`SELECT 'R' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' ||
|
|
LPAD(
|
|
(COALESCE(MAX(SUBSTR(REQUEST_MNG_NO, 11, 13)), '0')::INTEGER + 1)::TEXT,
|
|
3, '0'
|
|
) AS request_mng_no
|
|
FROM SALES_REQUEST_MASTER
|
|
WHERE DOC_TYPE IN ('PURCHASE_REQUEST', 'PURCHASE_REG') OR DOC_TYPE IS NULL`,
|
|
);
|
|
const requestMngNo: string = seqRes.rows[0]?.request_mng_no;
|
|
if (!requestMngNo) throw new Error("REQUEST_MNG_NO 채번 실패");
|
|
|
|
// 4) INSERT (운영 insertSalesRequestMasterFromMBom 1:1, PROJECT_NO 컬럼에 PROJECT_MGMT.OBJID 저장)
|
|
const newObjid = createObjId();
|
|
await client.query(
|
|
`INSERT INTO SALES_REQUEST_MASTER (
|
|
OBJID, REQUEST_MNG_NO, PROJECT_NO, MBOM_HEADER_OBJID,
|
|
REQUEST_USER_ID, STATUS, WRITER, REGDATE, DOC_TYPE
|
|
) VALUES ($1, $2, $3, $4, $5, 'create', $6, NOW(), 'PURCHASE_REQUEST')`,
|
|
[newObjid, requestMngNo, proj, mbom, userId, userId],
|
|
);
|
|
|
|
await client.query("COMMIT");
|
|
return {
|
|
objid: newObjid,
|
|
request_mng_no: requestMngNo,
|
|
mbom_header_objid: mbom,
|
|
};
|
|
} 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 = `
|
|
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::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,
|
|
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::varchar = COALESCE(V.LAST_PART_OBJID, V.PART_NO)
|
|
ORDER BY V.PATH2
|
|
`;
|