개발관리>E-BOM 등록·조회 메뉴 신설 (PR-B) — wace partMng 1:1 이식
backend (M3+M4): - devBomService: list/getByObjid/updateStatus/removeMany/ascending/descending - M3 그리드 SQL (getBOMStandardStructureGridList 1:1) - customer_mng 매핑 (wace SUPPLY_MNG → vexplor customer_mng.customer_code) - PRODUCT_NAME LEFT JOIN comm_code (CODE_NAME 함수 대체) - M3 다중 삭제 트랜잭션 (bom_part_qty + part_bom_report CASCADE) - M4 정/역전개 재귀 CTE (bom_part_qty 트리 + part_mng JOIN) - vexplor 적응: M4 product_mgmt_spec/upg/vc 분기 제거 (스키마 단순화) - PG 재귀 CTE 타입 일치: ARRAY[BP.objid::varchar] 명시 cast frontend (M3+M4): - ebom-regist (M3): 9셀 그리드 (제품구분·품번·품명·E-BOM·등록자·등록일·확정일·Version·상태) - ebom-search (M4): 동적 LEVEL 컬럼 + 정/역전개 토글 - BomReportStatusDialog: 상태 변경 (create/changeDesign/deploy + version) - AdminPageRenderer dynamic 임포트 2건 + menu_info URL spec 정렬 본 PR 제외 (별 PR): E-BOM Excel Import, 정/역전개 Excel Download, BOM_PART_QTY 수량 편집 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -178,6 +178,7 @@ import salesCommonRoutes from "./routes/salesCommonRoutes"; // 영업관리 4개
|
||||
import projectMgmtRoutes from "./routes/projectMgmtRoutes"; // 프로젝트관리>진행관리 (wace_plm 도메인 이식)
|
||||
import wbsTemplateRoutes from "./routes/wbsTemplateRoutes"; // 프로젝트관리>제품구분_WBS관리 (wace_plm 도메인 이식)
|
||||
import devPartRoutes from "./routes/devPartRoutes"; // 개발관리>PART 등록/조회 (wace_plm 도메인 이식)
|
||||
import devBomRoutes from "./routes/devBomRoutes"; // 개발관리>E-BOM 등록/조회 (wace_plm 도메인 이식)
|
||||
import erpSyncRoutes from "./routes/erpSyncRoutes"; // ERP 마스터 동기화 (사원/부서/창고/거래처/계정과목)
|
||||
import ecrMngRoutes from "./routes/ecrMngRoutes"; // ECR(Engineering Change Request) 관리
|
||||
import customerCsRoutes from "./routes/customerCsRoutes"; // 고객 CS 관리
|
||||
@@ -425,6 +426,7 @@ app.use("/api/sales", salesCommonRoutes); // 영업관리 공통 옵션 (codes/p
|
||||
app.use("/api/project/progress", projectMgmtRoutes); // 프로젝트관리>진행관리 (wace_plm 도메인)
|
||||
app.use("/api/project/wbs-template", wbsTemplateRoutes); // 프로젝트관리>제품구분_WBS관리 (wace_plm 도메인)
|
||||
app.use("/api/development", devPartRoutes); // 개발관리>PART 등록/조회 (wace_plm 도메인)
|
||||
app.use("/api/development", devBomRoutes); // 개발관리>E-BOM 등록/조회 (wace_plm 도메인)
|
||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// ============================================================
|
||||
// 개발관리 E-BOM (M3 등록 / M4 조회) 컨트롤러.
|
||||
// 라우트:
|
||||
// GET /api/development/ebom/list (M3 그리드)
|
||||
// GET /api/development/ebom/:objid (M3 단건)
|
||||
// PUT /api/development/ebom/:objid/status (M3 상태변경)
|
||||
// DELETE /api/development/ebom (M3 다중 삭제, body: { objids })
|
||||
// GET /api/development/ebom-tree/ascending (M4 정전개)
|
||||
// GET /api/development/ebom-tree/descending (M4 역전개)
|
||||
// ============================================================
|
||||
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import * as svc from "../services/devBomService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
function parseListFilter(q: Record<string, any>): svc.BomReportListFilter {
|
||||
const filter: svc.BomReportListFilter = { ...q };
|
||||
if (q.page) filter.page = Number(q.page);
|
||||
if (q.page_size) filter.page_size = Number(q.page_size);
|
||||
return filter;
|
||||
}
|
||||
|
||||
// ─── M3 그리드 ──────────────────────────────────────────────
|
||||
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.list(parseListFilter(req.query as Record<string, any>));
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("E-BOM(M3) 목록 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── M3 단건 ────────────────────────────────────────────────
|
||||
|
||||
export async function getByObjid(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { objid } = req.params;
|
||||
const row = await svc.getByObjid(objid);
|
||||
if (!row) return res.status(404).json({ success: false, message: "not_found" });
|
||||
return res.json({ success: true, data: row });
|
||||
} catch (e: any) {
|
||||
logger.error("E-BOM 상세 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── M3 상태 변경 ──────────────────────────────────────────
|
||||
|
||||
export async function updateStatus(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const { objid } = req.params;
|
||||
const rowCount = await svc.updateStatus(userId, objid, req.body);
|
||||
if (rowCount === 0) return res.status(404).json({ success: false, message: "not_found" });
|
||||
return res.json({ success: true, message: "상태가 변경되었습니다." });
|
||||
} catch (e: any) {
|
||||
logger.error("E-BOM 상태 변경 실패", { error: e.message });
|
||||
return res.status(400).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── M3 다중 삭제 ──────────────────────────────────────────
|
||||
|
||||
export async function removeMany(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const objids = Array.isArray(req.body?.objids) ? (req.body.objids as any[]).map(String) : [];
|
||||
if (objids.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "objids가 비어있습니다." });
|
||||
}
|
||||
const removed = await svc.removeMany(objids);
|
||||
return res.json({ success: true, data: { removed }, message: `${removed}건이 삭제되었습니다.` });
|
||||
} catch (e: any) {
|
||||
logger.error("E-BOM 삭제 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── M4 정전개 ─────────────────────────────────────────────
|
||||
|
||||
export async function ascending(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.ascending(req.query as svc.BomTreeFilter);
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("E-BOM 정전개 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── M4 역전개 ─────────────────────────────────────────────
|
||||
|
||||
export async function descending(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.descending(req.query as svc.BomTreeFilter);
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("E-BOM 역전개 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// ============================================================
|
||||
// 개발관리 E-BOM (M3 등록 / M4 조회) 라우트.
|
||||
// app.ts: app.use("/api/development", devBomRoutes) — devPart 라우터와 prefix 공유.
|
||||
// ============================================================
|
||||
|
||||
import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import * as ctrl from "../controllers/devBomController";
|
||||
|
||||
const router = Router();
|
||||
router.use(authenticateToken);
|
||||
|
||||
// M4 — 트리 (정/역전개) — /ebom-tree prefix (라우트 충돌 방지: /:objid 위)
|
||||
router.get("/ebom-tree/ascending", ctrl.ascending);
|
||||
router.get("/ebom-tree/descending", ctrl.descending);
|
||||
|
||||
// M3 — 그리드 + CRUD
|
||||
router.get("/ebom/list", ctrl.getList);
|
||||
router.delete("/ebom", ctrl.removeMany);
|
||||
router.put("/ebom/:objid/status", ctrl.updateStatus);
|
||||
router.get("/ebom/:objid", ctrl.getByObjid);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,341 @@
|
||||
// ============================================================
|
||||
// 개발관리 E-BOM (M3 등록 / M4 조회) — wace partMng.xml 1:1 이식.
|
||||
//
|
||||
// 매퍼 매핑 (원본: wace_plm/src/com/pms/mapper/partMng.xml):
|
||||
// getBOMStandardStructureGridList → list() (M3 그리드, part_bom_report)
|
||||
// updateStructureStatus → updateStatus() (M3 상태변경)
|
||||
// deleteBomReport + deleteBomQty → removeMany() (M3 다중 삭제 트랜잭션)
|
||||
// structureAscendingList → ascending() (M4 정전개, 재귀 CTE)
|
||||
// selectStructureDescendingList → descending() (M4 역전개, 재귀 CTE)
|
||||
//
|
||||
// vexplor_rps 적응:
|
||||
// · customer_mng 매핑: wace SUPPLY_MNG.OBJID → vexplor customer_mng.customer_code
|
||||
// · PRODUCT_NAME: wace CODE_NAME() → vexplor LEFT JOIN comm_code CC_PRD
|
||||
// · M4 product_mgmt_spec/upg/vc 분기 제거 (vexplor part_bom_report 단순화)
|
||||
// ============================================================
|
||||
|
||||
import { PoolClient } from "pg";
|
||||
import { getPool, transaction } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// ─── 필터/바디 타입 ──────────────────────────────────────────
|
||||
|
||||
export interface BomReportListFilter {
|
||||
customer_cd?: string;
|
||||
project_name?: string;
|
||||
unit_code?: string;
|
||||
search_unit_name?: string;
|
||||
search_writer?: string;
|
||||
product_cd?: string;
|
||||
search_part_no?: string;
|
||||
search_part_name?: string;
|
||||
search_from_date?: string;
|
||||
search_to_date?: string;
|
||||
status?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export interface BomReportStatusBody {
|
||||
product_cd?: string;
|
||||
part_no?: string;
|
||||
part_name?: string;
|
||||
version?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface BomTreeFilter {
|
||||
bom_report_objid?: string;
|
||||
project_name?: string; // part_bom_report.contract_objid
|
||||
unit_code?: string;
|
||||
search_part_no?: string;
|
||||
search_part_name?: string;
|
||||
}
|
||||
|
||||
// ─── 공용 파라미터 빌더 ────────────────────────────────────
|
||||
|
||||
function buildListWhere(filter: BomReportListFilter, startIdx: number) {
|
||||
const params: any[] = [];
|
||||
const conds: string[] = [];
|
||||
let idx = startIdx;
|
||||
|
||||
if (filter.customer_cd) { conds.push(`T.CUSTOMER_OBJID = $${idx++}`); params.push(filter.customer_cd); }
|
||||
if (filter.project_name) { conds.push(`T.CONTRACT_OBJID = $${idx++}`); params.push(filter.project_name); }
|
||||
if (filter.unit_code) { conds.push(`T.UNIT_CODE = $${idx++}`); params.push(filter.unit_code); }
|
||||
if (filter.search_unit_name) {
|
||||
conds.push(`EXISTS (SELECT 1 FROM PMS_WBS_TASK W
|
||||
WHERE W.OBJID = T.UNIT_CODE
|
||||
AND (UPPER(W.TASK_NAME) LIKE UPPER($${idx}) OR UPPER(W.UNIT_NO) LIKE UPPER($${idx})))`);
|
||||
params.push(`%${filter.search_unit_name}%`); idx++;
|
||||
}
|
||||
if (filter.search_writer) { conds.push(`T.WRITER = $${idx++}`); params.push(filter.search_writer); }
|
||||
if (filter.product_cd) { conds.push(`T.PRODUCT_CD = $${idx++}`); params.push(filter.product_cd); }
|
||||
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_from_date) { conds.push(`T.REGDATE::date >= TO_DATE($${idx++}, 'YYYY-MM-DD')`); params.push(filter.search_from_date); }
|
||||
if (filter.search_to_date) { conds.push(`T.REGDATE::date <= TO_DATE($${idx++}, 'YYYY-MM-DD')`); params.push(filter.search_to_date); }
|
||||
if (filter.status) { conds.push(`T.STATUS = $${idx++}`); params.push(filter.status); }
|
||||
|
||||
return { sql: conds.length ? conds.join(" AND ") : "1=1", params };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
// ─── M3 그리드 ──────────────────────────────────────────────
|
||||
|
||||
export async function list(filter: BomReportListFilter) {
|
||||
const { limit, offset, page, pageSize } = paginate(filter);
|
||||
const where = buildListWhere(filter, 1);
|
||||
const pool = getPool();
|
||||
|
||||
const baseSql = `
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (ORDER BY T.REGDATE DESC) AS NUM,
|
||||
T.OBJID, T.CUSTOMER_OBJID, SM.customer_name AS CUSTOMER_NAME,
|
||||
T.CONTRACT_OBJID, PM.CUSTOMER_PROJECT_NAME, PM.PROJECT_NO,
|
||||
T.UNIT_CODE, COALESCE(WT.UNIT_NO || '-' || WT.TASK_NAME, '') AS UNIT_NAME,
|
||||
T.STATUS,
|
||||
CASE UPPER(T.STATUS)
|
||||
WHEN 'CREATE' THEN '등록중'
|
||||
WHEN 'CHANGEDESIGN' THEN '설계변경미배포'
|
||||
WHEN 'DEPLOY' THEN '배포완료'
|
||||
ELSE '' END AS STATUS_TITLE,
|
||||
T.WRITER, UI.dept_name AS DEPT_NAME, UI.user_name AS USER_NAME,
|
||||
COALESCE(UI.dept_name || '/' || UI.user_name, '') AS DEPT_USER_NAME,
|
||||
T.REGDATE, TO_CHAR(T.REGDATE, 'YYYY-MM-DD') AS REG_DATE,
|
||||
T.DEPLOY_DATE, T.REVISION,
|
||||
EO_DATA.EO_NO, EO_DATA.EO_DATE,
|
||||
T.NOTE, T.MULTI_YN, T.MULTI_MASTER_YN, T.MULTI_BREAK_YN, T.MULTI_MASTER_OBJID,
|
||||
COALESCE(EO_DATA.BOM_CNT, 0)::int AS BOM_CNT,
|
||||
T.PRODUCT_CD, CC_PRD.code_name AS PRODUCT_NAME,
|
||||
T.PART_NO, T.PART_NAME
|
||||
FROM PART_BOM_REPORT T
|
||||
LEFT JOIN customer_mng SM ON SM.customer_code = T.CUSTOMER_OBJID
|
||||
LEFT JOIN PROJECT_MGMT PM ON PM.OBJID = T.CONTRACT_OBJID
|
||||
LEFT JOIN PMS_WBS_TASK WT ON WT.OBJID = T.UNIT_CODE
|
||||
LEFT JOIN user_info UI ON UI.user_id = T.WRITER
|
||||
LEFT JOIN COMM_CODE CC_PRD ON CC_PRD.code_id = T.PRODUCT_CD AND CC_PRD.status = 'active'
|
||||
LEFT JOIN (
|
||||
SELECT BP.BOM_REPORT_OBJID,
|
||||
MAX(PM2.EO_NO) AS EO_NO,
|
||||
MAX(PM2.EO_DATE) AS EO_DATE,
|
||||
COUNT(*) AS BOM_CNT
|
||||
FROM BOM_PART_QTY BP
|
||||
LEFT JOIN PART_MNG PM2 ON BP.PART_NO = PM2.OBJID::varchar
|
||||
GROUP BY BP.BOM_REPORT_OBJID
|
||||
) EO_DATA ON EO_DATA.BOM_REPORT_OBJID = T.OBJID
|
||||
WHERE ${where.sql}
|
||||
`;
|
||||
|
||||
const dataSql = `${baseSql} ORDER BY T.REGDATE DESC NULLS LAST LIMIT $${where.params.length + 1} OFFSET $${where.params.length + 2}`;
|
||||
const countSql = `SELECT COUNT(*)::int AS total FROM (${baseSql}) X`;
|
||||
|
||||
const [dataRes, countRes] = await Promise.all([
|
||||
pool.query(dataSql, [...where.params, limit, offset]),
|
||||
pool.query(countSql, where.params),
|
||||
]);
|
||||
|
||||
return { rows: dataRes.rows, total: countRes.rows[0]?.total ?? 0, page, pageSize };
|
||||
}
|
||||
|
||||
// ─── M3 단건 ────────────────────────────────────────────────
|
||||
|
||||
export async function getByObjid(objid: string) {
|
||||
const sql = `
|
||||
SELECT T.*,
|
||||
CC_PRD.code_name AS PRODUCT_NAME,
|
||||
(SELECT COUNT(*) FROM BOM_PART_QTY Q WHERE Q.BOM_REPORT_OBJID = T.OBJID) AS BOM_CNT
|
||||
FROM PART_BOM_REPORT T
|
||||
LEFT JOIN COMM_CODE CC_PRD ON CC_PRD.code_id = T.PRODUCT_CD AND CC_PRD.status='active'
|
||||
WHERE T.OBJID = $1
|
||||
`;
|
||||
const r = await getPool().query(sql, [objid]);
|
||||
return r.rows[0] ?? null;
|
||||
}
|
||||
|
||||
// ─── M3 상태 변경 ──────────────────────────────────────────
|
||||
|
||||
export async function updateStatus(userId: string, objid: string, body: BomReportStatusBody) {
|
||||
if (!body.status) throw new Error("status는 필수입니다.");
|
||||
const sql = `
|
||||
UPDATE PART_BOM_REPORT
|
||||
SET PRODUCT_CD = COALESCE($1, PRODUCT_CD),
|
||||
PART_NO = COALESCE($2, PART_NO),
|
||||
PART_NAME = COALESCE($3, PART_NAME),
|
||||
REVISION = COALESCE($4, REVISION),
|
||||
STATUS = $5,
|
||||
editer = $6,
|
||||
edit_date = NOW()
|
||||
WHERE OBJID = $7
|
||||
`;
|
||||
const r = await getPool().query(sql, [
|
||||
body.product_cd ?? null,
|
||||
body.part_no ?? null,
|
||||
body.part_name ?? null,
|
||||
body.version ?? null,
|
||||
body.status,
|
||||
userId,
|
||||
objid,
|
||||
]);
|
||||
return r.rowCount ?? 0;
|
||||
}
|
||||
|
||||
// ─── M3 다중 삭제 (트랜잭션) ───────────────────────────────
|
||||
|
||||
export async function removeMany(objids: string[]): Promise<number> {
|
||||
if (!objids || objids.length === 0) return 0;
|
||||
let removed = 0;
|
||||
await transaction(async (client: PoolClient) => {
|
||||
// 1) 자식 트리
|
||||
await client.query(
|
||||
`DELETE FROM BOM_PART_QTY WHERE BOM_REPORT_OBJID = ANY($1::varchar[])`,
|
||||
[objids]
|
||||
);
|
||||
// 2) 메인
|
||||
const r = await client.query(
|
||||
`DELETE FROM PART_BOM_REPORT WHERE OBJID = ANY($1::varchar[])`,
|
||||
[objids]
|
||||
);
|
||||
removed = r.rowCount ?? 0;
|
||||
});
|
||||
logger.info("E-BOM 삭제 완료", { removed });
|
||||
return removed;
|
||||
}
|
||||
|
||||
// ─── M4 정전개 (재귀 CTE) ──────────────────────────────────
|
||||
|
||||
export async function ascending(filter: BomTreeFilter) {
|
||||
const pool = getPool();
|
||||
const params: any[] = [];
|
||||
const conds: string[] = [];
|
||||
let idx = 1;
|
||||
|
||||
// 시작점 필터: 명시적 bom_report_objid 또는 part_bom_report 필터로 좁힘
|
||||
if (filter.bom_report_objid) {
|
||||
conds.push(`BP.bom_report_objid = $${idx++}`);
|
||||
params.push(filter.bom_report_objid);
|
||||
} else if (filter.project_name || filter.unit_code) {
|
||||
const subConds: string[] = [];
|
||||
if (filter.project_name) { subConds.push(`contract_objid = $${idx++}`); params.push(filter.project_name); }
|
||||
if (filter.unit_code) { subConds.push(`unit_code = $${idx++}`); params.push(filter.unit_code); }
|
||||
conds.push(`BP.bom_report_objid IN (SELECT objid FROM part_bom_report WHERE ${subConds.join(" AND ")})`);
|
||||
}
|
||||
const startWhere = conds.length ? conds.join(" AND ") : "1=1";
|
||||
|
||||
// PART 검색 필터는 결과 단계 적용
|
||||
const finalConds: string[] = [];
|
||||
if (filter.search_part_no) {
|
||||
finalConds.push(`UPPER(PM.part_no) LIKE UPPER($${idx++})`);
|
||||
params.push(`%${filter.search_part_no}%`);
|
||||
}
|
||||
if (filter.search_part_name) {
|
||||
finalConds.push(`UPPER(PM.part_name) LIKE UPPER($${idx++})`);
|
||||
params.push(`%${filter.search_part_name}%`);
|
||||
}
|
||||
const finalWhere = finalConds.length ? `WHERE ${finalConds.join(" AND ")}` : "";
|
||||
|
||||
const sql = `
|
||||
WITH RECURSIVE TREE(bom_report_objid, objid, parent_objid, child_objid, part_no, qty, seq, status, lev, path, cycle) AS (
|
||||
SELECT BP.bom_report_objid, BP.objid, BP.parent_objid, BP.child_objid,
|
||||
BP.part_no, BP.qty, BP.seq, BP.status,
|
||||
1, ARRAY[BP.objid::varchar], FALSE
|
||||
FROM bom_part_qty BP
|
||||
WHERE (BP.parent_objid IS NULL OR BP.parent_objid = '')
|
||||
AND ${startWhere}
|
||||
UNION ALL
|
||||
SELECT B.bom_report_objid, B.objid, B.parent_objid, B.child_objid,
|
||||
B.part_no, B.qty, B.seq, B.status,
|
||||
T.lev + 1, T.path || B.objid::varchar, B.objid::varchar = ANY(T.path)
|
||||
FROM bom_part_qty B
|
||||
JOIN TREE T ON B.parent_objid = T.objid AND NOT T.cycle
|
||||
)
|
||||
SELECT T.bom_report_objid, T.objid, T.parent_objid, T.child_objid, T.part_no, T.qty, T.seq, T.status,
|
||||
T.lev, T.path,
|
||||
PM.part_no AS pm_part_no,
|
||||
PM.part_name AS pm_part_name,
|
||||
PM.spec, PM.material, PM.weight, PM.remark,
|
||||
PM.edit_date,
|
||||
PM.eo_no, PM.revision,
|
||||
(SELECT COUNT(1) FROM attach_file_info F WHERE F.target_objid = PM.objid::varchar AND F.doc_type='3D_CAD') AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM attach_file_info F WHERE F.target_objid = PM.objid::varchar AND F.doc_type='2D_DRAWING_CAD') AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM attach_file_info F WHERE F.target_objid = PM.objid::varchar AND F.doc_type='2D_PDF_CAD') AS cu03_cnt,
|
||||
(SELECT COALESCE(MAX(lev), 0) FROM TREE) AS max_level
|
||||
FROM TREE T
|
||||
LEFT JOIN part_mng PM ON T.part_no = PM.objid::varchar
|
||||
${finalWhere}
|
||||
ORDER BY T.path
|
||||
`;
|
||||
const r = await pool.query(sql, params);
|
||||
const max_level = r.rows[0]?.max_level ?? 0;
|
||||
return { rows: r.rows, max_level };
|
||||
}
|
||||
|
||||
// ─── M4 역전개 (재귀 CTE — parent 방향) ────────────────────
|
||||
|
||||
export async function descending(filter: BomTreeFilter) {
|
||||
const pool = getPool();
|
||||
const params: any[] = [];
|
||||
const anchorConds: string[] = [];
|
||||
let idx = 1;
|
||||
|
||||
// anchor: PART 검색 필터로 leaf 후보 선택. 없으면 전체 (잎-가지가 자식 없는 행)
|
||||
if (filter.search_part_no) {
|
||||
anchorConds.push(`EXISTS (SELECT 1 FROM part_mng PMA WHERE PMA.objid::varchar = BP.part_no AND UPPER(PMA.part_no) LIKE UPPER($${idx++}))`);
|
||||
params.push(`%${filter.search_part_no}%`);
|
||||
}
|
||||
if (filter.search_part_name) {
|
||||
anchorConds.push(`EXISTS (SELECT 1 FROM part_mng PMA WHERE PMA.objid::varchar = BP.part_no AND UPPER(PMA.part_name) LIKE UPPER($${idx++}))`);
|
||||
params.push(`%${filter.search_part_name}%`);
|
||||
}
|
||||
if (filter.bom_report_objid) {
|
||||
anchorConds.push(`BP.bom_report_objid = $${idx++}`);
|
||||
params.push(filter.bom_report_objid);
|
||||
} else if (filter.project_name || filter.unit_code) {
|
||||
const subConds: string[] = [];
|
||||
if (filter.project_name) { subConds.push(`contract_objid = $${idx++}`); params.push(filter.project_name); }
|
||||
if (filter.unit_code) { subConds.push(`unit_code = $${idx++}`); params.push(filter.unit_code); }
|
||||
anchorConds.push(`BP.bom_report_objid IN (SELECT objid FROM part_bom_report WHERE ${subConds.join(" AND ")})`);
|
||||
}
|
||||
if (anchorConds.length === 0) {
|
||||
// 검색 조건이 전혀 없으면 빈 결과 반환 (역전개는 통상 PART 한정 조회)
|
||||
return { rows: [], max_level: 0 };
|
||||
}
|
||||
const anchorWhere = anchorConds.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
WITH RECURSIVE TREE(bom_report_objid, objid, parent_objid, child_objid, part_no, qty, seq, status, lev, path, cycle) AS (
|
||||
SELECT BP.bom_report_objid, BP.objid, BP.parent_objid, BP.child_objid,
|
||||
BP.part_no, BP.qty, BP.seq, BP.status,
|
||||
1, ARRAY[BP.objid::varchar], FALSE
|
||||
FROM bom_part_qty BP
|
||||
WHERE ${anchorWhere}
|
||||
UNION ALL
|
||||
SELECT B.bom_report_objid, B.objid, B.parent_objid, B.child_objid,
|
||||
B.part_no, B.qty, B.seq, B.status,
|
||||
T.lev + 1, T.path || B.objid::varchar, B.objid::varchar = ANY(T.path)
|
||||
FROM bom_part_qty B
|
||||
JOIN TREE T ON B.objid = T.parent_objid AND NOT T.cycle
|
||||
)
|
||||
SELECT T.bom_report_objid, T.objid, T.parent_objid, T.child_objid, T.part_no, T.qty, T.seq, T.status,
|
||||
T.lev, T.path,
|
||||
PM.part_no AS pm_part_no,
|
||||
PM.part_name AS pm_part_name,
|
||||
PM.spec, PM.material, PM.weight, PM.remark,
|
||||
PM.edit_date,
|
||||
PM.eo_no, PM.revision,
|
||||
(SELECT COUNT(1) FROM attach_file_info F WHERE F.target_objid = PM.objid::varchar AND F.doc_type='3D_CAD') AS cu01_cnt,
|
||||
(SELECT COUNT(1) FROM attach_file_info F WHERE F.target_objid = PM.objid::varchar AND F.doc_type='2D_DRAWING_CAD') AS cu02_cnt,
|
||||
(SELECT COUNT(1) FROM attach_file_info F WHERE F.target_objid = PM.objid::varchar AND F.doc_type='2D_PDF_CAD') AS cu03_cnt,
|
||||
(SELECT COALESCE(MAX(lev), 0) FROM TREE) AS max_level
|
||||
FROM TREE T
|
||||
LEFT JOIN part_mng PM ON T.part_no = PM.objid::varchar
|
||||
ORDER BY T.path
|
||||
`;
|
||||
const r = await pool.query(sql, params);
|
||||
const max_level = r.rows[0]?.max_level ?? 0;
|
||||
return { rows: r.rows, max_level };
|
||||
}
|
||||
Reference in New Issue
Block a user