생산관리 M-BOM PR-B5+ — BOM 복사 다이얼로그 (운영 structureBomCopyFormPopup 1:1)

5개 메뉴 1:1 검증(Agent 5병렬) 후 발견된 mbom 메인 그리드 [BOM 복사] 누락 보완.
저장 매퍼는 PR-B5 saveBomAssignment 재사용 — 백엔드는 EBOM/MBOM 분기 이미 지원,
이번에 MBOM UI 진입점도 첫 노출.

신규 컴포넌트:
 - BomCopyDialog.tsx — 품번/품명 readonly + E-BOM/M-BOM 셀렉트 상호배타 +
   트리 미리보기 + AttachFileDropZone (docType=MBOM_DRAWING, accept=.stp,.step,.dwg,.dxf,.pdf)
 - 메인 page.tsx [BOM 복사] 버튼 추가 + 체크 단건 검증 +
   Machine(0000928) 외 동일 partNo 최신 M-BOM 자동 추천

신규 백엔드 (mbomService/Controller/Routes):
 - searchAssignableMboms — 매퍼 productionplanning.getMbomListForSelect2 (4007~4014) 1:1
 - previewMbomTree     — getStructureOnly + finalize("ASSIGNED_MBOM")
 - getLatestMbomByPartNo — 매퍼 getLatestMbomByPartNo (3426~3445) 1:1, Machine 외 자동 검색
 - 라우트: GET /assignable-mboms, GET /mbom-preview/:objid, GET /latest-mbom-by-partno/:partNo

도면 업로드 차이: 운영 fn_uploadDrawingFiles 는 placeholder("구현 예정"). RPS 는
공용 AttachFileDropZone 재사용해 실구현 (target_objid=projectObjid).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-15 10:53:35 +09:00
parent 7e7c6a0ac0
commit 6368258797
6 changed files with 625 additions and 13 deletions
@@ -86,6 +86,50 @@ export async function previewEbomTree(req: AuthenticatedRequest, res: Response)
}
}
// PR-B5+ — 할당 가능한 M-BOM 검색 (운영 BOM 복사 다이얼로그 M-BOM 셀렉트 옵션)
export async function searchAssignableMboms(req: AuthenticatedRequest, res: Response) {
try {
const q = req.query as Record<string, any>;
const data = await svc.searchAssignableMboms({
search_part_no: String(q.search_part_no ?? "").trim() || undefined,
search_part_name: String(q.search_part_name ?? "").trim() || undefined,
search_from_date: String(q.search_from_date ?? "").trim() || undefined,
search_to_date: String(q.search_to_date ?? "").trim() || undefined,
limit: q.limit ? Number(q.limit) : undefined,
});
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 });
}
}
// PR-B5+ — M-BOM 미리보기 트리 (운영 BOM 복사 다이얼로그 M-BOM 선택 시)
export async function previewMbomTree(req: AuthenticatedRequest, res: Response) {
try {
const mbomHeaderObjid = String(req.params.mbomHeaderObjid ?? "").trim();
if (!mbomHeaderObjid) return res.status(400).json({ success: false, message: "mbomHeaderObjid 누락" });
const data = await svc.previewMbomTree(mbomHeaderObjid);
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 });
}
}
// PR-B5+ — 동일 partNo 의 최신 M-BOM 조회 (운영 getLatestMbomByPartNo, Machine 외 자동 추천)
export async function getLatestMbomByPartNo(req: AuthenticatedRequest, res: Response) {
try {
const partNo = String(req.params.partNo ?? "").trim();
if (!partNo) return res.status(400).json({ success: false, message: "partNo 누락" });
const data = await svc.getLatestMbomByPartNo(partNo);
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 });
}
}
// PR-B5 — BOM 할당 (운영 saveBomAssignment.do 1:1)
export async function assignBom(req: AuthenticatedRequest, res: Response) {
try {
@@ -19,5 +19,8 @@ router.post("/sales-request", ctrl.createSalesRequest); // PR-B3 구매리스트
router.get("/assignable-eboms", ctrl.searchAssignableEboms); // PR-B5 할당 가능 E-BOM 검색
router.get("/ebom-preview/:bomReportObjid", ctrl.previewEbomTree); // PR-B5 E-BOM 미리보기
router.post("/assign", ctrl.assignBom); // PR-B5 BOM 할당
router.get("/assignable-mboms", ctrl.searchAssignableMboms); // PR-B5+ BOM 복사 - M-BOM 셀렉트 옵션
router.get("/mbom-preview/:mbomHeaderObjid", ctrl.previewMbomTree); // PR-B5+ BOM 복사 - M-BOM 미리보기
router.get("/latest-mbom-by-partno/:partNo", ctrl.getLatestMbomByPartNo); // PR-B5+ Machine 외 자동 추천
export default router;
+106
View File
@@ -1357,6 +1357,112 @@ export async function assignBom(
return { success: true, source_bom_type: sourceBomType, source_obj_id: sourceBomObjId };
}
// ─── BOM 복사 다이얼로그 보조 (PR-B5+) ─────────────────────────
//
// 운영판 partMng/structureBomCopyFormPopup.jsp 진입점 보조 함수.
// 저장은 위 assignBom 재사용 (sourceBomType='EBOM'|'MBOM').
export interface AssignableMbomFilter {
search_part_no?: string;
search_part_name?: string;
search_from_date?: string;
search_to_date?: string;
limit?: number;
}
export interface AssignableMbomRow {
objid: string;
mbom_no: string | null;
part_no: string | null;
part_name: string | null;
reg_date: string | null;
}
// 매퍼 productionplanning.getMbomListForSelect2 (4007~4014) 1:1 + 검색 필터 확장.
export async function searchAssignableMboms(filter: AssignableMbomFilter = {}): Promise<AssignableMbomRow[]> {
const pool = getPool();
const conds: string[] = [];
const params: any[] = [];
let idx = 1;
if (filter.search_part_no) {
conds.push(`UPPER(MH.PART_NO) LIKE '%' || UPPER($${idx++}) || '%'`);
params.push(filter.search_part_no);
}
if (filter.search_part_name) {
conds.push(`UPPER(MH.PART_NAME) LIKE '%' || UPPER($${idx++}) || '%'`);
params.push(filter.search_part_name);
}
if (filter.search_from_date) {
conds.push(`MH.REGDATE >= TO_DATE($${idx++}, 'YYYY-MM-DD')`);
params.push(filter.search_from_date);
}
if (filter.search_to_date) {
conds.push(`MH.REGDATE < TO_DATE($${idx++}, 'YYYY-MM-DD') + INTERVAL '1 day'`);
params.push(filter.search_to_date);
}
const limit = Math.min(500, Math.max(1, Number(filter.limit) || 200));
const sql = `
SELECT
MH.OBJID::VARCHAR AS objid,
COALESCE(MH.MBOM_NO, '') AS mbom_no,
COALESCE(MH.PART_NO, '') AS part_no,
COALESCE(MH.PART_NAME, '') AS part_name,
TO_CHAR(MH.REGDATE, 'YYYY-MM-DD') AS reg_date
FROM MBOM_HEADER MH
WHERE MH.STATUS = 'Y'
${conds.length ? "AND " + conds.join(" AND ") : ""}
ORDER BY MH.REGDATE DESC, MH.MBOM_NO
LIMIT ${limit}
`;
const r = await pool.query(sql, params);
return r.rows;
}
// M-BOM 미리보기 트리 — mbom_header_objid 만으로 STRUCTURE_ONLY_SQL 호출.
// 운영판 structureBomCopyFormPopup 의 BOM 트리 미리보기 (M-BOM 선택 시).
export async function previewMbomTree(mbomHeaderObjid: string): Promise<MbomTreeResult> {
if (!mbomHeaderObjid) {
return { bom_data_type: "NONE", bom_report_objid: null, max_level: 1, rows: [] };
}
const rows = await getStructureOnly(mbomHeaderObjid);
return finalize("ASSIGNED_MBOM", mbomHeaderObjid, rows);
}
export interface LatestMbomByPartNoRow {
template_header_objid: string;
mbom_no: string | null;
project_objid: string | null;
part_no: string | null;
part_name: string | null;
save_date: string | null;
}
// 매퍼 productionplanning.getLatestMbomByPartNo (3426~3445) 1:1.
// Machine(0000928) 이외 제품 + 동일 partNo + STATUS='Y' 최신 1건.
export async function getLatestMbomByPartNo(partNo: string): Promise<LatestMbomByPartNoRow | null> {
if (!partNo || !partNo.trim()) return null;
const pool = getPool();
const sql = `
SELECT
MH.OBJID::VARCHAR AS template_header_objid,
MH.MBOM_NO AS mbom_no,
MH.PROJECT_OBJID::VARCHAR AS project_objid,
MH.PART_NO AS part_no,
MH.PART_NAME AS part_name,
TO_CHAR(MH.REGDATE, 'YYYY-MM-DD') AS save_date
FROM MBOM_HEADER MH
INNER JOIN PROJECT_MGMT PM ON MH.PROJECT_OBJID = PM.OBJID
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
`;
const r = await pool.query(sql, [partNo.trim()]);
return r.rows[0] ?? null;
}
// ─── 변경이력 조회 (PR-B4) ──────────────────────────────────
//
// 매퍼 productionplanning.getMbomHistory (3448~3470) 1:1.