구매관리 7메뉴 신규 + M-BOM PR-B3·B5 + 발주관리 DataGrid 통일 + 생산계획&실적 라우트
구매관리 (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>
This commit is contained in:
@@ -120,6 +120,8 @@ import tableHistoryRoutes from "./routes/tableHistoryRoutes"; // 테이블 변
|
||||
import bomRoutes from "./routes/bomRoutes"; // BOM 이력/버전 관리
|
||||
import productionRoutes from "./routes/productionRoutes"; // 생산계획 관리
|
||||
import productionMbomRoutes from "./routes/productionMbomRoutes"; // 생산관리>M-BOM 관리 (wace_plm 도메인)
|
||||
import { planResultRouter as productionPlanResultRoutes, mbomReqRouter as productionMbomRequirementRoutes } from "./routes/productionPlanResultRoutes"; // 생산관리 4개 메뉴 (생산계획&실적, 소요량)
|
||||
import purchaseRoutes from "./routes/purchaseRoutes"; // 구매관리 7개 메뉴 (wace_plm 도메인)
|
||||
import itemInspectionRoutes from "./routes/itemInspectionRoutes"; // 품목검사정보
|
||||
import crawlRoutes from "./routes/crawlRoutes"; // 웹 크롤링
|
||||
import roleRoutes from "./routes/roleRoutes"; // 권한 그룹 관리
|
||||
@@ -382,6 +384,9 @@ app.use("/api/table-history", tableHistoryRoutes); // 테이블 변경 이력
|
||||
app.use("/api/bom", bomRoutes); // BOM 이력/버전 관리
|
||||
app.use("/api/production", productionRoutes); // 생산계획 관리
|
||||
app.use("/api/production/mbom", productionMbomRoutes); // 생산관리>M-BOM 관리 (wace_plm 도메인)
|
||||
app.use("/api/production/plan-result", productionPlanResultRoutes); // 생산계획&실적관리 (메뉴1+2)
|
||||
app.use("/api/production/mbom-requirement", productionMbomRequirementRoutes); // 반제품/원자재 소요량 (메뉴3+4)
|
||||
app.use("/api/purchase", purchaseRoutes); // 구매관리 7개 메뉴 (wace_plm 도메인)
|
||||
app.use("/api/item-inspection", itemInspectionRoutes); // 품목검사정보 (그룹 페이징)
|
||||
app.use("/api/crawl", crawlRoutes); // 웹 크롤링
|
||||
app.use("/api/material-status", materialStatusRoutes); // 자재현황
|
||||
|
||||
@@ -53,6 +53,44 @@ export async function getTree(req: AuthenticatedRequest, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
// PR-B5 — 할당 가능한 E-BOM 검색 (운영 mBomEbomSelectPopup.do getEbomList 1:1)
|
||||
export async function searchAssignableEboms(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const q = req.query as Record<string, any>;
|
||||
const data = await svc.searchAssignableEboms({
|
||||
search_part_no: String(q.search_part_no ?? "").trim() || undefined,
|
||||
search_part_name: String(q.search_part_name ?? "").trim() || undefined,
|
||||
search_material: String(q.search_material ?? "").trim() || undefined,
|
||||
search_supplier: String(q.search_supplier ?? "").trim() || undefined,
|
||||
limit: q.limit ? Number(q.limit) : undefined,
|
||||
});
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
// PR-B5 — BOM 할당 (운영 saveBomAssignment.do 1:1)
|
||||
export async function assignBom(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const projectObjId = String(req.body?.project_obj_id ?? req.body?.projectObjId ?? "").trim();
|
||||
const sourceBomType = String(req.body?.source_bom_type ?? req.body?.sourceBomType ?? "").trim().toUpperCase();
|
||||
const sourceBomObjId = String(req.body?.source_bom_obj_id ?? req.body?.sourceBomObjId ?? "").trim();
|
||||
if (!projectObjId) return res.status(400).json({ success: false, message: "project_obj_id 누락" });
|
||||
if (sourceBomType !== "EBOM" && sourceBomType !== "MBOM") {
|
||||
return res.status(400).json({ success: false, message: "source_bom_type 은 EBOM 또는 MBOM" });
|
||||
}
|
||||
if (!sourceBomObjId) return res.status(400).json({ success: false, message: "source_bom_obj_id 누락" });
|
||||
const userId = req.user?.userId ?? "system";
|
||||
const data = await svc.assignBom(projectObjId, sourceBomType as svc.AssignSourceType, sourceBomObjId, userId);
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("BOM 할당 실패", { error: e.message });
|
||||
return res.status(400).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// PR-B4 — 변경이력 조회 (운영 getMbomHistory.do 1:1)
|
||||
export async function getHistory(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
@@ -66,6 +104,30 @@ export async function getHistory(req: AuthenticatedRequest, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
// PR-B3 — 구매리스트(SALES_REQUEST_MASTER) 생성 (운영 createPurchaseListFromMBom.do 1:1)
|
||||
export async function createSalesRequest(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const mbomHeaderObjid = String(
|
||||
req.body?.mbom_header_objid ?? req.body?.MBOM_HEADER_OBJID ?? "",
|
||||
).trim();
|
||||
const projectMgmtObjid = String(
|
||||
req.body?.project_mgmt_objid ?? req.body?.PROJECT_MGMT_OBJID ?? "",
|
||||
).trim();
|
||||
if (!mbomHeaderObjid) {
|
||||
return res.status(400).json({ success: false, message: "mbom_header_objid 누락" });
|
||||
}
|
||||
if (!projectMgmtObjid) {
|
||||
return res.status(400).json({ success: false, message: "project_mgmt_objid 누락" });
|
||||
}
|
||||
const userId = req.user?.userId ?? "system";
|
||||
const data = await svc.createSalesRequest(mbomHeaderObjid, projectMgmtObjid, userId);
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("구매리스트 생성 실패", { error: e.message });
|
||||
return res.status(400).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// PR-B1 — 본 편집 저장 (운영 saveMbom.do 1:1)
|
||||
export async function save(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// ============================================================
|
||||
// 생산관리 > 생산계획&실적관리 (일반/장비) + 반제품·원자재 소요량.
|
||||
// wace productionplanning.xml 1:1.
|
||||
//
|
||||
// 라우트:
|
||||
// GET /api/production/plan-result/list (메뉴1 — 일반)
|
||||
// GET /api/production/plan-result/list-equip (메뉴2 — 장비)
|
||||
// GET /api/production/plan-result/project-options (검색 multiple — 프로젝트번호)
|
||||
// GET /api/production/plan-result/writer-options (검색 — 등록자)
|
||||
//
|
||||
// GET /api/production/mbom-requirement/options (M-BOM 옵션 + 품명 매핑)
|
||||
// POST /api/production/mbom-requirement/semi (메뉴3 — 반제품)
|
||||
// POST /api/production/mbom-requirement/raw (메뉴4 — 원자재)
|
||||
// ============================================================
|
||||
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import * as ppr from "../services/prodPlanResultService";
|
||||
import * as req from "../services/mbomRequirementService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
function parseFilter(q: Record<string, any>): ppr.ProdPlanResultFilter {
|
||||
const f: ppr.ProdPlanResultFilter = { ...q };
|
||||
if (q.page) f.page = Number(q.page);
|
||||
if (q.page_size) f.page_size = Number(q.page_size);
|
||||
return f;
|
||||
}
|
||||
|
||||
// ─── 메뉴 1: 생산계획&실적관리 (일반) ─────────────────────────
|
||||
|
||||
export async function getList(reqMsg: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await ppr.listGeneral(parseFilter(reqMsg.query as Record<string, any>));
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("생산계획&실적관리 목록 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 메뉴 2: 생산계획&실적관리 (장비) ─────────────────────────
|
||||
|
||||
export async function getListEquip(reqMsg: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await ppr.listEquip(parseFilter(reqMsg.query as Record<string, any>));
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("생산계획&실적관리(장비) 목록 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 검색 옵션 ────────────────────────────────────────────────
|
||||
|
||||
export async function getProjectOptions(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await ppr.getProjectNoOptions();
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("프로젝트번호 옵션 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWriterOptions(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await ppr.getWriterOptions();
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("등록자 옵션 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 메뉴 3 & 4: M-BOM 소요량 ─────────────────────────────────
|
||||
|
||||
export async function getMbomOptions(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await req.getMbomOptions();
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("M-BOM 옵션 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSemiRequirement(reqMsg: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const items = Array.isArray(reqMsg.body?.mbomItems) ? reqMsg.body.mbomItems : [];
|
||||
const data = await req.getSemiRequirement(items);
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("반제품 소요량 조회 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRawRequirement(reqMsg: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const items = Array.isArray(reqMsg.body?.mbomItems) ? reqMsg.body.mbomItems : [];
|
||||
const data = await req.getRawRequirement(items);
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("원자재 소요량 조회 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// ============================================================
|
||||
// 구매관리 — 7개 메뉴 그리드 컨트롤러 (purchaseService.ts orchestrator)
|
||||
// ============================================================
|
||||
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import * as svc from "../services/purchaseService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
function parseFilter(q: Record<string, any>): svc.PurchaseListFilter {
|
||||
const f: svc.PurchaseListFilter = { ...q };
|
||||
if (q.page) f.page = Number(q.page);
|
||||
if (q.page_size) f.page_size = Number(q.page_size);
|
||||
return f;
|
||||
}
|
||||
|
||||
async function runList(
|
||||
fn: (f: svc.PurchaseListFilter) => Promise<any>,
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
name: string,
|
||||
) {
|
||||
try {
|
||||
const data = await fn(parseFilter(req.query as Record<string, any>));
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error(`${name} 실패`, { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
export const getPurchaseRequest = (req: AuthenticatedRequest, res: Response) => runList(svc.listPurchaseRequest, req, res, "구매리스트관리");
|
||||
export const getQuotationRequest = (req: AuthenticatedRequest, res: Response) => runList(svc.listQuotationRequest, req, res, "견적요청서관리");
|
||||
export const getProposal = (req: AuthenticatedRequest, res: Response) => runList(svc.listProposal, req, res, "품의서관리");
|
||||
export const getInbound = (req: AuthenticatedRequest, res: Response) => runList(svc.listInbound, req, res, "입고관리");
|
||||
export const getInboundByItem = (req: AuthenticatedRequest, res: Response) => runList(svc.listInboundByItem, req, res, "품목별 입고관리");
|
||||
export const getInboundByDate = (req: AuthenticatedRequest, res: Response) => runList(svc.listInboundByDate, req, res, "입고일별 입고관리");
|
||||
export const getProjectStatus = (req: AuthenticatedRequest, res: Response) => runList(svc.listProjectStatus, req, res, "프로젝트별 발주/입고 현황");
|
||||
|
||||
export async function getSuppliers(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.listSupplierOptions();
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("공급업체 옵션 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUsers(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.listUserOptions();
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("작성자 옵션 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProjects(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.listProjectOptions();
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("프로젝트 옵션 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,8 @@ router.get("/detail/:objid", ctrl.getDetail);
|
||||
router.get("/tree/:objid", ctrl.getTree);
|
||||
router.post("/save", ctrl.save); // PR-B1 본 편집 저장
|
||||
router.get("/history/:projectObjid", ctrl.getHistory); // PR-B4 변경이력 조회
|
||||
router.post("/sales-request", ctrl.createSalesRequest); // PR-B3 구매리스트 생성
|
||||
router.get("/assignable-eboms", ctrl.searchAssignableEboms); // PR-B5 할당 가능 E-BOM 검색
|
||||
router.post("/assign", ctrl.assignBom); // PR-B5 BOM 할당
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// ============================================================
|
||||
// 생산관리 > 생산계획&실적 + 소요량 라우트.
|
||||
// app.ts:
|
||||
// app.use("/api/production/plan-result", productionPlanResultRoutes)
|
||||
// app.use("/api/production/mbom-requirement", productionMbomRequirementRoutes)
|
||||
// 각 라우트는 동일 컨트롤러를 공유하므로 한 파일에서 두 Router 를 export.
|
||||
// ============================================================
|
||||
|
||||
import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import * as ctrl from "../controllers/prodPlanResultController";
|
||||
|
||||
const planResultRouter = Router();
|
||||
planResultRouter.use(authenticateToken);
|
||||
|
||||
planResultRouter.get("/list", ctrl.getList); // 메뉴1
|
||||
planResultRouter.get("/list-equip", ctrl.getListEquip); // 메뉴2
|
||||
planResultRouter.get("/project-options", ctrl.getProjectOptions); // 공용 검색 옵션
|
||||
planResultRouter.get("/writer-options", ctrl.getWriterOptions); // 메뉴1 등록자
|
||||
|
||||
const mbomReqRouter = Router();
|
||||
mbomReqRouter.use(authenticateToken);
|
||||
|
||||
mbomReqRouter.get("/options", ctrl.getMbomOptions); // 메뉴3+4 공용 M-BOM 옵션
|
||||
mbomReqRouter.post("/semi", ctrl.getSemiRequirement); // 메뉴3 반제품
|
||||
mbomReqRouter.post("/raw", ctrl.getRawRequirement); // 메뉴4 원자재
|
||||
|
||||
export { planResultRouter, mbomReqRouter };
|
||||
@@ -0,0 +1,27 @@
|
||||
// ============================================================
|
||||
// 구매관리 — 7개 메뉴 + 공통 옵션 라우트.
|
||||
// app.ts: app.use("/api/purchase", purchaseRoutes)
|
||||
// ============================================================
|
||||
|
||||
import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import * as ctrl from "../controllers/purchaseController";
|
||||
|
||||
const router = Router();
|
||||
router.use(authenticateToken);
|
||||
|
||||
// 그리드 7종
|
||||
router.get("/purchase-request", ctrl.getPurchaseRequest); // 구매리스트관리
|
||||
router.get("/quotation-request", ctrl.getQuotationRequest); // 견적요청서관리
|
||||
router.get("/proposal", ctrl.getProposal); // 품의서관리
|
||||
router.get("/inbound", ctrl.getInbound); // 입고관리
|
||||
router.get("/inbound-by-item", ctrl.getInboundByItem); // 품목별 입고관리
|
||||
router.get("/inbound-by-date", ctrl.getInboundByDate); // 입고일별 입고관리
|
||||
router.get("/project-status", ctrl.getProjectStatus); // 프로젝트별 발주/입고 현황
|
||||
|
||||
// 공통 옵션
|
||||
router.get("/options/suppliers", ctrl.getSuppliers);
|
||||
router.get("/options/users", ctrl.getUsers);
|
||||
router.get("/options/projects", ctrl.getProjects);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,259 @@
|
||||
// ============================================================
|
||||
// 생산관리 > 반제품소요량 + 원자재소요량 — wace productionplanning.xml 1:1.
|
||||
//
|
||||
// 매퍼 매핑:
|
||||
// getMbomListWithPartName → getMbomOptions() (4017)
|
||||
// getMbomSemiProductItems → getSemiRequirement() (5252)
|
||||
// getMbomRawMaterialItems → getRawRequirement() (5273) 1차 — 구매품
|
||||
// getMbomRawSourceItems → getRawRequirement() (5298) 2차 — 원소재
|
||||
//
|
||||
// 서비스 로직(ProductionPlanningService.java:2030~2270) 의 LinkedHashMap 합산을
|
||||
// 자바스크립트 Map 으로 1:1 이식.
|
||||
// ============================================================
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
|
||||
// ─── 입력 / 출력 ────────────────────────────────────────────
|
||||
|
||||
export interface MbomRequirementInputItem {
|
||||
mbomObjid: string;
|
||||
qty: number | string;
|
||||
}
|
||||
|
||||
export interface SemiRequirementRow {
|
||||
PART_NO: string;
|
||||
PART_NAME: string;
|
||||
CATEGORY_NAME: string;
|
||||
UNIT: string;
|
||||
MATERIAL: string;
|
||||
SPEC: string;
|
||||
REQUIRED_QTY: number;
|
||||
}
|
||||
|
||||
export interface RawRequirementRow {
|
||||
PART_NO: string;
|
||||
PART_NAME: string;
|
||||
CATEGORY_NAME: string; // '구매품' | '원소재'
|
||||
UNIT: string;
|
||||
MATERIAL: string;
|
||||
SPEC: string;
|
||||
REQUIRED_QTY: number | string;
|
||||
RAW_MATERIAL: string;
|
||||
RAW_MATERIAL_SIZE: string;
|
||||
MATERIAL_PART_NO: string;
|
||||
MATERIAL_REQUIRED_QTY: number | string;
|
||||
}
|
||||
|
||||
function toInt(v: any): number {
|
||||
if (v == null) return 0;
|
||||
if (typeof v === "number") return Math.trunc(v);
|
||||
const n = Number(String(v));
|
||||
return Number.isFinite(n) ? Math.trunc(n) : 0;
|
||||
}
|
||||
function toNum(v: any): number {
|
||||
if (v == null) return 0;
|
||||
if (typeof v === "number") return v;
|
||||
const n = Number(String(v));
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
// ─── M-BOM 옵션 조회 (셀렉트박스용 + 품명 매핑) ────────────────
|
||||
|
||||
export async function getMbomOptions(): Promise<Array<{ objid: string; mbom_no: string; part_name: string }>> {
|
||||
const pool = getPool();
|
||||
const r = await pool.query(`
|
||||
SELECT OBJID::VARCHAR AS objid,
|
||||
COALESCE(MBOM_NO, '') AS mbom_no,
|
||||
COALESCE(PART_NAME, '') AS part_name
|
||||
FROM MBOM_HEADER
|
||||
WHERE STATUS = 'Y'
|
||||
ORDER BY REGDATE DESC, MBOM_NO
|
||||
`);
|
||||
return r.rows;
|
||||
}
|
||||
|
||||
// ─── 메뉴 3: 반제품 소요량 ──────────────────────────────────
|
||||
//
|
||||
// 매퍼: getMbomSemiProductItems (5252~5270)
|
||||
// 조건: MBOM_DETAIL × PART_MNG, PART_TYPE IN ('0001812', '0001813'), 1레벨 제외.
|
||||
// 자바 서비스: 동일 PART_NO 합산 (입력수량 × 항목수량).
|
||||
|
||||
export async function getSemiRequirement(items: MbomRequirementInputItem[]): Promise<SemiRequirementRow[]> {
|
||||
if (!Array.isArray(items) || items.length === 0) return [];
|
||||
const pool = getPool();
|
||||
|
||||
// PART_NO 기준 LinkedHashMap (자바 LinkedHashMap 동일 보장)
|
||||
const partMap = new Map<string, SemiRequirementRow>();
|
||||
|
||||
for (const it of items) {
|
||||
const mbomObjid = String(it?.mbomObjid ?? "").trim();
|
||||
const inputQty = toInt(it?.qty);
|
||||
if (!mbomObjid || inputQty <= 0) continue;
|
||||
|
||||
const r = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
MD.PART_NO,
|
||||
MD.PART_NAME,
|
||||
COALESCE(NULLIF(MD.QTY, '')::INTEGER, 1) AS ITEM_QTY,
|
||||
P.PART_TYPE,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = P.PART_TYPE LIMIT 1), '') AS CATEGORY_NAME,
|
||||
COALESCE(P.UNIT, '') AS UNIT,
|
||||
COALESCE(P.MATERIAL, '') AS MATERIAL,
|
||||
COALESCE(P.SPEC, '') AS SPEC
|
||||
FROM MBOM_DETAIL MD
|
||||
INNER JOIN PART_MNG P ON P.OBJID::VARCHAR = MD.PART_OBJID
|
||||
WHERE MD.MBOM_HEADER_OBJID = $1
|
||||
AND MD.STATUS = 'ACTIVE'
|
||||
AND (MD.PARENT_OBJID IS NOT NULL AND MD.PARENT_OBJID != '')
|
||||
AND P.PART_TYPE IN ('0001812', '0001813')
|
||||
ORDER BY P.PART_TYPE, MD.PART_NO
|
||||
`,
|
||||
[mbomObjid],
|
||||
);
|
||||
|
||||
for (const row of r.rows) {
|
||||
const partNo = String(row.part_no ?? "").trim();
|
||||
if (!partNo) continue;
|
||||
const itemQty = toInt(row.item_qty);
|
||||
const required = inputQty * itemQty;
|
||||
const existing = partMap.get(partNo);
|
||||
if (existing) {
|
||||
existing.REQUIRED_QTY += required;
|
||||
} else {
|
||||
partMap.set(partNo, {
|
||||
PART_NO: partNo,
|
||||
PART_NAME: row.part_name ?? "",
|
||||
CATEGORY_NAME: row.category_name ?? "",
|
||||
UNIT: row.unit ?? "",
|
||||
MATERIAL: row.material ?? "",
|
||||
SPEC: row.spec ?? "",
|
||||
REQUIRED_QTY: required,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(partMap.values());
|
||||
}
|
||||
|
||||
// ─── 메뉴 4: 원자재 소요량 ──────────────────────────────────
|
||||
//
|
||||
// 매퍼: getMbomRawMaterialItems (5273~5295) + getMbomRawSourceItems (5298~5318)
|
||||
// 자바 서비스: 구매품/원소재 두 LinkedHashMap. 원소재는 소수점 합산 후 올림.
|
||||
|
||||
export async function getRawRequirement(items: MbomRequirementInputItem[]): Promise<RawRequirementRow[]> {
|
||||
if (!Array.isArray(items) || items.length === 0) return [];
|
||||
const pool = getPool();
|
||||
|
||||
// 운영판 1:1 — 두 갈래 LinkedHashMap (구매품 / 원소재)
|
||||
const purchaseMap = new Map<string, RawRequirementRow>();
|
||||
// 원소재는 소수점 합산을 위해 임시 number 보관 후 마지막에 올림 처리
|
||||
const rawSourceMap = new Map<string, RawRequirementRow & { __rawSum: number }>();
|
||||
|
||||
for (const it of items) {
|
||||
const mbomObjid = String(it?.mbomObjid ?? "").trim();
|
||||
const inputQty = toInt(it?.qty);
|
||||
if (!mbomObjid || inputQty <= 0) continue;
|
||||
|
||||
// 1) 구매품 (PART_TYPE = '0000063')
|
||||
const r1 = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
MD.PART_NO,
|
||||
MD.PART_NAME,
|
||||
COALESCE(NULLIF(MD.QTY, '')::INTEGER, 1) AS ITEM_QTY,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = P.PART_TYPE LIMIT 1), '') AS CATEGORY_NAME,
|
||||
COALESCE(P.UNIT, '') AS UNIT
|
||||
FROM MBOM_DETAIL MD
|
||||
INNER JOIN PART_MNG P ON P.OBJID::VARCHAR = MD.PART_OBJID
|
||||
WHERE MD.MBOM_HEADER_OBJID = $1
|
||||
AND MD.STATUS = 'ACTIVE'
|
||||
AND (MD.PARENT_OBJID IS NOT NULL AND MD.PARENT_OBJID != '')
|
||||
AND P.PART_TYPE = '0000063'
|
||||
ORDER BY MD.PART_NO
|
||||
`,
|
||||
[mbomObjid],
|
||||
);
|
||||
|
||||
for (const row of r1.rows) {
|
||||
const partNo = String(row.part_no ?? "").trim();
|
||||
if (!partNo) continue;
|
||||
const itemQty = toInt(row.item_qty);
|
||||
const required = inputQty * itemQty;
|
||||
const existing = purchaseMap.get(partNo);
|
||||
if (existing) {
|
||||
existing.REQUIRED_QTY = toInt(existing.REQUIRED_QTY) + required;
|
||||
} else {
|
||||
purchaseMap.set(partNo, {
|
||||
PART_NO: partNo,
|
||||
PART_NAME: row.part_name ?? "",
|
||||
CATEGORY_NAME: row.category_name ?? "",
|
||||
UNIT: row.unit ?? "",
|
||||
MATERIAL: "",
|
||||
SPEC: "",
|
||||
REQUIRED_QTY: required,
|
||||
RAW_MATERIAL: "",
|
||||
RAW_MATERIAL_SIZE: "",
|
||||
MATERIAL_PART_NO: "",
|
||||
MATERIAL_REQUIRED_QTY: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 원소재 (RAW_MATERIAL_PART_NO 가 있는 항목)
|
||||
const r2 = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
MD.RAW_MATERIAL_PART_NO AS PART_NO,
|
||||
MD.RAW_MATERIAL AS PART_NAME,
|
||||
COALESCE(NULLIF(MD.REQUIRED_QTY, '')::NUMERIC, 0) AS ITEM_QTY,
|
||||
MD.RAW_MATERIAL,
|
||||
MD.RAW_MATERIAL_SIZE
|
||||
FROM MBOM_DETAIL MD
|
||||
WHERE MD.MBOM_HEADER_OBJID = $1
|
||||
AND MD.STATUS = 'ACTIVE'
|
||||
AND MD.RAW_MATERIAL_PART_NO IS NOT NULL
|
||||
AND MD.RAW_MATERIAL_PART_NO != ''
|
||||
ORDER BY MD.RAW_MATERIAL_PART_NO
|
||||
`,
|
||||
[mbomObjid],
|
||||
);
|
||||
|
||||
for (const row of r2.rows) {
|
||||
const materialPartNo = String(row.part_no ?? "").trim();
|
||||
if (!materialPartNo) continue;
|
||||
const itemQty = toNum(row.item_qty);
|
||||
const required = inputQty * itemQty;
|
||||
const existing = rawSourceMap.get(materialPartNo);
|
||||
if (existing) {
|
||||
existing.__rawSum += required;
|
||||
} else {
|
||||
rawSourceMap.set(materialPartNo, {
|
||||
PART_NO: "",
|
||||
PART_NAME: "",
|
||||
CATEGORY_NAME: "원소재",
|
||||
UNIT: "",
|
||||
MATERIAL: row.raw_material ?? "",
|
||||
SPEC: row.raw_material_size ?? "",
|
||||
REQUIRED_QTY: "",
|
||||
RAW_MATERIAL: row.raw_material ?? "",
|
||||
RAW_MATERIAL_SIZE: row.raw_material_size ?? "",
|
||||
MATERIAL_PART_NO: materialPartNo,
|
||||
MATERIAL_REQUIRED_QTY: "",
|
||||
__rawSum: required,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 구매품 먼저, 원소재(올림) 뒤
|
||||
const result: RawRequirementRow[] = [];
|
||||
for (const v of purchaseMap.values()) result.push(v);
|
||||
for (const v of rawSourceMap.values()) {
|
||||
const ceilQty = Math.ceil(v.__rawSum);
|
||||
const { __rawSum, ...rest } = v;
|
||||
result.push({ ...rest, MATERIAL_REQUIRED_QTY: ceilQty });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1228,6 +1228,115 @@ export async function save(payload: MbomSavePayload, sessionUserId: string): Pro
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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.
|
||||
@@ -1270,6 +1379,91 @@ export async function getHistory(projectObjid: string): Promise<MbomHistoryRow[]
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
// ============================================================
|
||||
// 생산관리 > 생산계획&실적관리 + 생산계획&실적관리(장비) — wace productionplanning.xml 1:1.
|
||||
//
|
||||
// 매퍼 매핑:
|
||||
// prodPlanResultMgmtGridList → listGeneral() (productionplanning.xml:4550)
|
||||
// prodPlanResultMgmtEquipGridList → listEquip() (productionplanning.xml:4887)
|
||||
//
|
||||
// 두 메뉴는 동일 베이스(PROJECT_MGMT + CONTRACT_MGMT + CONTRACT_ITEM + PRODUCTION_PLAN +
|
||||
// PRODUCTION_RESULT 합산) 위에서 PRODUCT 코드(0000928=장비) 기준으로 갈라진다.
|
||||
// ============================================================
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
|
||||
// ─── 필터 ────────────────────────────────────────────────────
|
||||
|
||||
export interface ProdPlanResultFilter {
|
||||
search_project_nos?: string; // "OBJID,OBJID,..." (multiple select)
|
||||
search_product_code?: string;
|
||||
search_category_code?: string;
|
||||
search_production_type?: string;
|
||||
search_customer_objid?: string;
|
||||
search_req_del_date_from?: string;
|
||||
search_req_del_date_to?: string;
|
||||
search_part_no?: string;
|
||||
search_part_name?: string;
|
||||
search_serial_no?: string;
|
||||
search_writer?: string;
|
||||
search_regdate_from?: string;
|
||||
search_regdate_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 빌더 (매퍼 <if> 1:1) ──────────────────────────
|
||||
//
|
||||
// 매퍼: 4684~4741 / 4985~5022. 두 메뉴 동일.
|
||||
// (Equip 은 search_writer / search_regdate 없지만, 빌더는 동일 — 안 쓰면 됨.)
|
||||
|
||||
function buildWhere(filter: ProdPlanResultFilter, startIdx: number, includeSerial: boolean) {
|
||||
const params: any[] = [];
|
||||
const conds: string[] = [];
|
||||
let idx = startIdx;
|
||||
|
||||
if (filter.search_project_nos) {
|
||||
const ids = filter.search_project_nos.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
if (ids.length > 0) {
|
||||
const placeholders = ids.map(() => `$${idx++}`).join(", ");
|
||||
conds.push(`T.OBJID::VARCHAR IN (${placeholders})`);
|
||||
params.push(...ids);
|
||||
}
|
||||
}
|
||||
if (filter.search_product_code) {
|
||||
conds.push(`T.PRODUCT = $${idx++}`);
|
||||
params.push(filter.search_product_code);
|
||||
}
|
||||
if (filter.search_category_code) {
|
||||
conds.push(`T.CATEGORY_CODE = $${idx++}`);
|
||||
params.push(filter.search_category_code);
|
||||
}
|
||||
if (filter.search_production_type) {
|
||||
conds.push(`T.PRODUCTION_TYPE = $${idx++}`);
|
||||
params.push(filter.search_production_type);
|
||||
}
|
||||
if (filter.search_customer_objid) {
|
||||
conds.push(
|
||||
`(T.CUSTOMER_OBJID = $${idx} OR T.CUSTOMER_OBJID = REPLACE($${idx}, 'C_', '') OR REPLACE(T.CUSTOMER_OBJID, 'C_', '') = REPLACE($${idx}, 'C_', ''))`
|
||||
);
|
||||
params.push(filter.search_customer_objid);
|
||||
idx++;
|
||||
}
|
||||
if (filter.search_req_del_date_from) {
|
||||
conds.push(`T.REQ_DEL_DATE >= $${idx++}`);
|
||||
params.push(filter.search_req_del_date_from);
|
||||
}
|
||||
if (filter.search_req_del_date_to) {
|
||||
conds.push(`T.REQ_DEL_DATE <= $${idx++}`);
|
||||
params.push(filter.search_req_del_date_to);
|
||||
}
|
||||
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_serial_no) {
|
||||
// 일반: SERIAL_NO_LIST 전체 매칭 / 장비: SERIAL_NO 단일 (둘 다 동일 컬럼 alias 로 처리)
|
||||
const col = includeSerial ? "T.SERIAL_NO_LIST" : "T.SERIAL_NO";
|
||||
conds.push(`UPPER(${col}) LIKE '%' || UPPER($${idx++}) || '%'`);
|
||||
params.push(filter.search_serial_no);
|
||||
}
|
||||
if (filter.search_writer) {
|
||||
conds.push(`T.WRITER = $${idx++}`);
|
||||
params.push(filter.search_writer);
|
||||
}
|
||||
if (filter.search_regdate_from) {
|
||||
conds.push(`T.REGDATE >= TO_DATE($${idx++}, 'YYYY-MM-DD')`);
|
||||
params.push(filter.search_regdate_from);
|
||||
}
|
||||
if (filter.search_regdate_to) {
|
||||
conds.push(`T.REGDATE <= TO_DATE($${idx++}, 'YYYY-MM-DD')`);
|
||||
params.push(filter.search_regdate_to);
|
||||
}
|
||||
|
||||
return { sql: conds.length ? "AND " + conds.join(" AND ") : "", params };
|
||||
}
|
||||
|
||||
// ─── 메뉴 1: 생산계획&실적관리 (일반) ─────────────────────────
|
||||
//
|
||||
// 매퍼 prodPlanResultMgmtGridList (4550~4743) 1:1.
|
||||
// UNION ALL 두 갈래:
|
||||
// 1) PROJECT_MGMT 기반 — PM.PRODUCT != '0000928' (장비 제외)
|
||||
// 2) PRODUCTION_PLAN 단독 — PROJECT_OBJID 비어있는 독립 계획
|
||||
|
||||
const SQL_GENERAL_INNER = `
|
||||
SELECT * FROM (
|
||||
-- 1) 프로젝트 기반
|
||||
SELECT
|
||||
PM.OBJID::VARCHAR AS OBJID,
|
||||
PM.PROJECT_NO,
|
||||
CM.PRODUCT,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.PRODUCT LIMIT 1), '') AS PRODUCT_NAME,
|
||||
CM.CATEGORY_CD AS CATEGORY_CODE,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.CATEGORY_CD LIMIT 1), CM.CATEGORY_CD) AS CATEGORY_CODE_NAME,
|
||||
COALESCE(PP.PRODUCTION_TYPE, '') AS PRODUCTION_TYPE,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = PP.PRODUCTION_TYPE LIMIT 1), '') AS PRODUCTION_TYPE_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,
|
||||
COALESCE(CI.DUE_DATE, PM.DUE_DATE, CM.REQ_DEL_DATE) AS REQ_DEL_DATE,
|
||||
COALESCE(CI.CUSTOMER_REQUEST, '') AS CUSTOMER_REQUEST,
|
||||
COALESCE(CI.PART_NO, PM.PART_NO, '') AS PART_NO,
|
||||
COALESCE(CI.PART_NAME, PM.PART_NAME, '') AS PART_NAME,
|
||||
(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,
|
||||
(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(NULLIF(PP.EXTRA_PROD_QTY, '')::numeric, 0) AS EXTRA_PROD_QTY,
|
||||
COALESCE(NULLIF(PM.QUANTITY, '')::numeric, NULLIF(CI.ORDER_QUANTITY, '')::numeric, 0)
|
||||
+ COALESCE(NULLIF(PP.EXTRA_PROD_QTY, '')::numeric, 0) AS TOTAL_PROD_QTY,
|
||||
COALESCE((SELECT SUM(RESULT_QTY) FROM PRODUCTION_RESULT PR
|
||||
WHERE PR.PROJECT_OBJID = PM.OBJID::VARCHAR AND PR.RESULT_TYPE = 'ASSEMBLY' AND PR.STATUS = 'active'), 0) AS ASSEMBLY_QTY,
|
||||
COALESCE((SELECT SUM(RESULT_QTY) FROM PRODUCTION_RESULT PR
|
||||
WHERE PR.PROJECT_OBJID = PM.OBJID::VARCHAR AND PR.RESULT_TYPE = 'INSPECTION' AND PR.STATUS = 'active'), 0) AS INSPECTION_QTY,
|
||||
COALESCE((SELECT SUM(RESULT_QTY) FROM PRODUCTION_RESULT PR
|
||||
WHERE PR.PROJECT_OBJID = PM.OBJID::VARCHAR AND PR.RESULT_TYPE = 'SHIP_WAIT' AND PR.STATUS = 'active'), 0) AS SHIP_WAIT_QTY,
|
||||
PP.OBJID::VARCHAR AS PROD_PLAN_OBJID,
|
||||
PM.REGDATE AS SORT_DATE,
|
||||
PP.WRITER,
|
||||
COALESCE(user_name(PP.WRITER), '') AS WRITER_NAME,
|
||||
TO_CHAR(PP.REGDATE, 'YYYY-MM-DD') AS REGDATE_TITLE,
|
||||
PP.REGDATE
|
||||
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'
|
||||
LEFT OUTER JOIN PRODUCTION_PLAN PP ON PP.PROJECT_OBJID = PM.OBJID::VARCHAR AND PP.STATUS = 'active'
|
||||
WHERE PM.PROJECT_NO IS NOT NULL AND PM.PROJECT_NO != ''
|
||||
AND (CM.PRODUCT IS NULL OR CM.PRODUCT != '0000928')
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- 2) 프로젝트 없이 등록한 생산계획
|
||||
SELECT
|
||||
PP.OBJID::VARCHAR AS OBJID,
|
||||
'' AS PROJECT_NO,
|
||||
PP.PRODUCT_CODE AS PRODUCT,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = PP.PRODUCT_CODE LIMIT 1), '') AS PRODUCT_NAME,
|
||||
PP.CATEGORY_CODE AS CATEGORY_CODE,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = PP.CATEGORY_CODE LIMIT 1), '') AS CATEGORY_CODE_NAME,
|
||||
COALESCE(PP.PRODUCTION_TYPE, '') AS PRODUCTION_TYPE,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = PP.PRODUCTION_TYPE LIMIT 1), '') AS PRODUCTION_TYPE_NAME,
|
||||
PP.CUSTOMER_OBJID,
|
||||
COALESCE(
|
||||
CASE WHEN PP.CUSTOMER_OBJID LIKE 'C_%'
|
||||
THEN (SELECT CLIENT_NM FROM CLIENT_MNG AS C WHERE 'C_' || C.OBJID::VARCHAR = PP.CUSTOMER_OBJID LIMIT 1)
|
||||
ELSE (SELECT CLIENT_NM FROM CLIENT_MNG WHERE OBJID::VARCHAR = PP.CUSTOMER_OBJID LIMIT 1) END,
|
||||
''
|
||||
) AS CUSTOMER_NAME,
|
||||
PP.REQ_DEL_DATE,
|
||||
COALESCE(PP.CUSTOMER_REQUEST, '') AS CUSTOMER_REQUEST,
|
||||
COALESCE(PP.PART_NO, '') AS PART_NO,
|
||||
COALESCE(PP.PART_NAME, '') AS PART_NAME,
|
||||
COALESCE(PP.SERIAL_NO, '') AS SERIAL_NO,
|
||||
COALESCE(PP.SERIAL_NO, '') AS SERIAL_NO_LIST,
|
||||
COALESCE(NULLIF(PP.ORDER_QTY, '')::numeric, 0) AS QUANTITY,
|
||||
COALESCE(NULLIF(PP.EXTRA_PROD_QTY, '')::numeric, 0) AS EXTRA_PROD_QTY,
|
||||
COALESCE(NULLIF(PP.TOTAL_PROD_QTY, '')::numeric, 0) AS TOTAL_PROD_QTY,
|
||||
COALESCE((SELECT SUM(RESULT_QTY) FROM PRODUCTION_RESULT PR
|
||||
WHERE PR.PROJECT_OBJID = PP.OBJID::VARCHAR AND PR.RESULT_TYPE = 'ASSEMBLY' AND PR.STATUS = 'active'), 0) AS ASSEMBLY_QTY,
|
||||
COALESCE((SELECT SUM(RESULT_QTY) FROM PRODUCTION_RESULT PR
|
||||
WHERE PR.PROJECT_OBJID = PP.OBJID::VARCHAR AND PR.RESULT_TYPE = 'INSPECTION' AND PR.STATUS = 'active'), 0) AS INSPECTION_QTY,
|
||||
COALESCE((SELECT SUM(RESULT_QTY) FROM PRODUCTION_RESULT PR
|
||||
WHERE PR.PROJECT_OBJID = PP.OBJID::VARCHAR AND PR.RESULT_TYPE = 'SHIP_WAIT' AND PR.STATUS = 'active'), 0) AS SHIP_WAIT_QTY,
|
||||
PP.OBJID::VARCHAR AS PROD_PLAN_OBJID,
|
||||
PP.REGDATE AS SORT_DATE,
|
||||
PP.WRITER,
|
||||
COALESCE(user_name(PP.WRITER), '') AS WRITER_NAME,
|
||||
TO_CHAR(PP.REGDATE, 'YYYY-MM-DD') AS REGDATE_TITLE,
|
||||
PP.REGDATE
|
||||
FROM PRODUCTION_PLAN PP
|
||||
WHERE PP.STATUS = 'active'
|
||||
AND (PP.PROJECT_OBJID IS NULL OR PP.PROJECT_OBJID = '')
|
||||
) T
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
export async function listGeneral(filter: ProdPlanResultFilter) {
|
||||
const { limit, offset, page, pageSize } = paginate(filter);
|
||||
const where = buildWhere(filter, 1, true);
|
||||
const pool = getPool();
|
||||
|
||||
const dataSql = `
|
||||
SELECT * FROM (${SQL_GENERAL_INNER} ${where.sql}) X
|
||||
ORDER BY X.SORT_DATE DESC NULLS LAST, X.PROJECT_NO DESC
|
||||
LIMIT $${where.params.length + 1} OFFSET $${where.params.length + 2}
|
||||
`;
|
||||
const countSql = `SELECT COUNT(*)::int AS cnt FROM (${SQL_GENERAL_INNER} ${where.sql}) X`;
|
||||
|
||||
const [dataRes, countRes] = await Promise.all([
|
||||
pool.query(dataSql, [...where.params, limit, offset]),
|
||||
pool.query(countSql, where.params),
|
||||
]);
|
||||
|
||||
return {
|
||||
rows: dataRes.rows,
|
||||
totalCount: countRes.rows[0]?.cnt ?? 0,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 메뉴 2: 생산계획&실적관리(장비) ────────────────────────
|
||||
//
|
||||
// 매퍼 prodPlanResultMgmtEquipGridList (4887~5024) 1:1.
|
||||
// PM.PRODUCT = '0000928' (장비) 만. PMS_WBS_TASK 기반 진척율.
|
||||
// (운영판 CONTRACT_ITEM 조인 중복 라인은 정리 — 동일 ON 조건이 2번 들어있음)
|
||||
|
||||
const SQL_EQUIP_INNER = `
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
PM.OBJID::VARCHAR AS OBJID,
|
||||
PM.PROJECT_NO,
|
||||
CM.PRODUCT,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.PRODUCT LIMIT 1), '') AS PRODUCT_NAME,
|
||||
CM.CATEGORY_CD AS CATEGORY_CODE,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.CATEGORY_CD LIMIT 1), CM.CATEGORY_CD) AS CATEGORY_CODE_NAME,
|
||||
COALESCE(PP.PRODUCTION_TYPE, '') AS PRODUCTION_TYPE,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = PP.PRODUCTION_TYPE LIMIT 1), '') AS PRODUCTION_TYPE_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,
|
||||
COALESCE(CI.DUE_DATE, PM.DUE_DATE, CM.REQ_DEL_DATE) AS REQ_DEL_DATE,
|
||||
COALESCE(CI.CUSTOMER_REQUEST, '') AS CUSTOMER_REQUEST,
|
||||
COALESCE(CI.PART_NO, PM.PART_NO, '') AS PART_NO,
|
||||
COALESCE(CI.PART_NAME, PM.PART_NAME, '') AS PART_NAME,
|
||||
(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 = CI.OBJID
|
||||
AND UPPER(CIS.STATUS) = 'ACTIVE'
|
||||
AND CIS.SERIAL_NO IS NOT NULL) AS SERIAL_NO,
|
||||
-- 생산WBS 건수 (WBS_TYPE='PRODUCE')
|
||||
(SELECT COUNT(1)::int FROM PMS_WBS_TASK O
|
||||
WHERE O.CONTRACT_OBJID = PM.OBJID::VARCHAR
|
||||
AND O.WBS_TYPE = 'PRODUCE') AS PROD_WBS_CNT,
|
||||
-- 생산진척율: Level1 PROGRESS 평균
|
||||
CASE
|
||||
WHEN (SELECT COUNT(1) FROM PMS_WBS_TASK O
|
||||
WHERE O.CONTRACT_OBJID = PM.OBJID::VARCHAR
|
||||
AND O.WBS_TYPE = 'PRODUCE'
|
||||
AND O.TASK_LEVEL = '1') = 0 THEN 0
|
||||
ELSE ROUND(
|
||||
(SELECT COALESCE(AVG(CASE WHEN O.PROGRESS IS NOT NULL AND O.PROGRESS != ''
|
||||
THEN CAST(O.PROGRESS AS numeric) ELSE 0 END), 0)
|
||||
FROM PMS_WBS_TASK O
|
||||
WHERE O.CONTRACT_OBJID = PM.OBJID::VARCHAR
|
||||
AND O.WBS_TYPE = 'PRODUCE'
|
||||
AND O.TASK_LEVEL = '1'), 1)
|
||||
END AS PROD_PROGRESS_RATE,
|
||||
-- 납품WBS 건수 (WBS_TYPE='SHIP')
|
||||
(SELECT COUNT(1)::int FROM PMS_WBS_TASK O
|
||||
WHERE O.CONTRACT_OBJID = PM.OBJID::VARCHAR
|
||||
AND O.WBS_TYPE = 'SHIP') AS DELV_WBS_CNT,
|
||||
-- 납품진척율: Level1 PROGRESS 평균
|
||||
CASE
|
||||
WHEN (SELECT COUNT(1) FROM PMS_WBS_TASK O
|
||||
WHERE O.CONTRACT_OBJID = PM.OBJID::VARCHAR
|
||||
AND O.WBS_TYPE = 'SHIP'
|
||||
AND O.TASK_LEVEL = '1') = 0 THEN 0
|
||||
ELSE ROUND(
|
||||
(SELECT COALESCE(AVG(CASE WHEN O.PROGRESS IS NOT NULL AND O.PROGRESS != ''
|
||||
THEN CAST(O.PROGRESS AS numeric) ELSE 0 END), 0)
|
||||
FROM PMS_WBS_TASK O
|
||||
WHERE O.CONTRACT_OBJID = PM.OBJID::VARCHAR
|
||||
AND O.WBS_TYPE = 'SHIP'
|
||||
AND O.TASK_LEVEL = '1'), 1)
|
||||
END AS DELV_PROGRESS_RATE,
|
||||
PM.REGDATE AS SORT_DATE
|
||||
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'
|
||||
LEFT OUTER JOIN PRODUCTION_PLAN PP ON PP.PROJECT_OBJID = PM.OBJID::VARCHAR AND PP.STATUS = 'active'
|
||||
WHERE PM.PROJECT_NO IS NOT NULL AND PM.PROJECT_NO != ''
|
||||
AND CM.PRODUCT = '0000928'
|
||||
) T
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
export async function listEquip(filter: ProdPlanResultFilter) {
|
||||
const { limit, offset, page, pageSize } = paginate(filter);
|
||||
const where = buildWhere(filter, 1, false);
|
||||
const pool = getPool();
|
||||
|
||||
const dataSql = `
|
||||
SELECT * FROM (${SQL_EQUIP_INNER} ${where.sql}) X
|
||||
ORDER BY X.SORT_DATE DESC NULLS LAST, X.PROJECT_NO DESC
|
||||
LIMIT $${where.params.length + 1} OFFSET $${where.params.length + 2}
|
||||
`;
|
||||
const countSql = `SELECT COUNT(*)::int AS cnt FROM (${SQL_EQUIP_INNER} ${where.sql}) X`;
|
||||
|
||||
const [dataRes, countRes] = await Promise.all([
|
||||
pool.query(dataSql, [...where.params, limit, offset]),
|
||||
pool.query(countSql, where.params),
|
||||
]);
|
||||
|
||||
return {
|
||||
rows: dataRes.rows,
|
||||
totalCount: countRes.rows[0]?.cnt ?? 0,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 검색 옵션 — 프로젝트번호 / 등록자 ──────────────────────
|
||||
//
|
||||
// 운영판 페이지 진입 시 모델에 담아주던 code_map.project_no / code_map.writer 대체.
|
||||
|
||||
export async function getProjectNoOptions() {
|
||||
const pool = getPool();
|
||||
const r = await pool.query(`
|
||||
SELECT PM.OBJID::VARCHAR AS code, PM.PROJECT_NO AS label
|
||||
FROM PROJECT_MGMT PM
|
||||
WHERE PM.PROJECT_NO IS NOT NULL AND PM.PROJECT_NO != ''
|
||||
ORDER BY PM.REGDATE DESC, PM.PROJECT_NO DESC
|
||||
LIMIT 1000
|
||||
`);
|
||||
return r.rows;
|
||||
}
|
||||
|
||||
export async function getWriterOptions() {
|
||||
const pool = getPool();
|
||||
// PRODUCTION_PLAN.WRITER 와 PROJECT_MGMT.REGISTER 모두 user_info 의 user_id.
|
||||
// 4개 메뉴 중 메뉴1 의 search_writer 만 사용. 활성 등록자만.
|
||||
const r = await pool.query(`
|
||||
SELECT DISTINCT PP.WRITER AS code,
|
||||
COALESCE(user_name(PP.WRITER), PP.WRITER) AS label
|
||||
FROM PRODUCTION_PLAN PP
|
||||
WHERE PP.WRITER IS NOT NULL AND PP.WRITER != ''
|
||||
AND PP.STATUS = 'active'
|
||||
ORDER BY label
|
||||
`);
|
||||
return r.rows;
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
// ============================================================
|
||||
// 구매관리 — 7개 메뉴 그리드/옵션 서비스
|
||||
//
|
||||
// wace_plm 1:1 이식 베이스. 마스터 테이블만 RPS 에 존재 (2026-05-14 현재):
|
||||
// ✓ sales_request_master / mbom_header / mbom_detail
|
||||
// ✓ purchase_order_master (56 cols, 0 rows)
|
||||
// ✓ client_mng / supply_mng / admin_supply_mng / part_mng / project_mgmt / contract_mgmt
|
||||
//
|
||||
// 누락 (운영DB 추출 후 신설 필요):
|
||||
// ✗ sales_request_part / sales_request_detail
|
||||
// ✗ quotation_request_master / quotation_received
|
||||
// ✗ purchase_order_part
|
||||
// ✗ arrival_plan
|
||||
// ✗ inventory_mgmt / inventory_mgmt_in
|
||||
// ✗ incoming_inspection / incoming_inspection_detail
|
||||
//
|
||||
// 정책: 누락 테이블 의존 SELECT 는 빈 그리드 + 콘솔 warning 으로 처리.
|
||||
// 마스터 단독 데이터는 정상 노출 (구매리스트관리 / 품의서관리 / 발주서관리).
|
||||
// ============================================================
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export interface PurchaseListFilter {
|
||||
year?: string;
|
||||
customer_objid?: string;
|
||||
customer_cd?: string;
|
||||
project_no?: string;
|
||||
part_no?: string;
|
||||
part_name?: string;
|
||||
part_spec?: string;
|
||||
partner_objid?: string;
|
||||
purchase_order_no?: string;
|
||||
proposal_no?: string;
|
||||
search_status?: string;
|
||||
writer?: string;
|
||||
request_user?: string;
|
||||
purchase_type?: string;
|
||||
part_type?: string;
|
||||
product_cd?: string;
|
||||
paid_type?: string;
|
||||
mail_send_yn?: string;
|
||||
delivery_status?: string;
|
||||
close_status?: string;
|
||||
sales_mng_user_id?: string;
|
||||
regdate_start?: string;
|
||||
regdate_end?: string;
|
||||
receipt_date_start?: string;
|
||||
receipt_date_end?: string;
|
||||
delivery_start_date?: string;
|
||||
delivery_end_date?: string;
|
||||
reg_start_date?: string;
|
||||
reg_end_date?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
interface ListResult<T> {
|
||||
rows: T[];
|
||||
totalCount: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
function clampPaging(filter: PurchaseListFilter): { limit: number; offset: number; page: number; pageSize: number } {
|
||||
const page = Math.max(1, Number(filter.page ?? 1));
|
||||
const pageSize = Math.max(1, Math.min(500, Number(filter.page_size ?? 50)));
|
||||
return { limit: pageSize, offset: (page - 1) * pageSize, page, pageSize };
|
||||
}
|
||||
|
||||
// ─── 1) 구매리스트관리 (wace salesMng.xml salesRequestMngRegList 매퍼 1:1 베이스) ──
|
||||
//
|
||||
// sales_request_master + (sales_request_part 누락 → PART_NO/PART_NAME 빈값) +
|
||||
// project_mgmt + contract_mgmt + comm_code + client_mng (customer 분기).
|
||||
//
|
||||
// WHERE: doc_type = 'PURCHASE_REQUEST' (또는 NULL) + 동적 필터.
|
||||
// ORDER: regdate DESC.
|
||||
|
||||
export async function listPurchaseRequest(filter: PurchaseListFilter): Promise<ListResult<any>> {
|
||||
const pool = getPool();
|
||||
const { limit, offset, page, pageSize } = clampPaging(filter);
|
||||
|
||||
const where: string[] = [`(SRM.DOC_TYPE = 'PURCHASE_REQUEST' OR SRM.DOC_TYPE IS NULL)`];
|
||||
const params: any[] = [];
|
||||
const addParam = (val: any) => { params.push(val); return `$${params.length}`; };
|
||||
|
||||
if (filter.customer_cd) where.push(`SRM.CUSTOMER_OBJID = ${addParam(filter.customer_cd)}`);
|
||||
if (filter.project_no) where.push(`PM.PROJECT_NO ILIKE ${addParam(`%${filter.project_no}%`)}`);
|
||||
if (filter.request_user) where.push(`SRM.REQUEST_USER_ID = ${addParam(filter.request_user)}`);
|
||||
if (filter.part_type) where.push(`SRM.PRODUCT_NAME = ${addParam(filter.part_type)}`);
|
||||
if (filter.regdate_start) where.push(`SRM.REGDATE::DATE >= ${addParam(filter.regdate_start)}::DATE`);
|
||||
if (filter.regdate_end) where.push(`SRM.REGDATE::DATE <= ${addParam(filter.regdate_end)}::DATE`);
|
||||
if (filter.part_no) where.push(`EXISTS (SELECT 1 FROM MBOM_DETAIL MD JOIN PART_MNG PP ON MD.PART_OBJID::VARCHAR=PP.OBJID::VARCHAR WHERE MD.MBOM_HEADER_OBJID = SRM.MBOM_HEADER_OBJID AND PP.PART_NO ILIKE ${addParam(`%${filter.part_no}%`)})`);
|
||||
if (filter.part_name) where.push(`EXISTS (SELECT 1 FROM MBOM_DETAIL MD JOIN PART_MNG PP ON MD.PART_OBJID::VARCHAR=PP.OBJID::VARCHAR WHERE MD.MBOM_HEADER_OBJID = SRM.MBOM_HEADER_OBJID AND PP.PART_NAME ILIKE ${addParam(`%${filter.part_name}%`)})`);
|
||||
|
||||
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
||||
|
||||
const dataSql = `
|
||||
SELECT
|
||||
SRM.OBJID AS objid,
|
||||
SRM.REQUEST_MNG_NO AS request_mng_no,
|
||||
SRM.DOC_TYPE AS doc_type,
|
||||
SRM.STATUS AS status,
|
||||
CASE SRM.STATUS
|
||||
WHEN 'create' THEN '작성중'
|
||||
WHEN 'approvalRequest' THEN '결재중'
|
||||
WHEN 'approvalComplete' THEN '결재완료'
|
||||
WHEN 'reject' THEN '반려'
|
||||
WHEN 'release' THEN '진행중'
|
||||
ELSE COALESCE(SRM.STATUS, '')
|
||||
END AS status_title,
|
||||
COALESCE(
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = SRM.PURCHASE_TYPE LIMIT 1), ''
|
||||
) AS purchase_type_name,
|
||||
COALESCE(
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = SRM.ORDER_TYPE LIMIT 1), ''
|
||||
) AS order_type_name,
|
||||
COALESCE(
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = SRM.PRODUCT_NAME LIMIT 1), ''
|
||||
) AS product_name_full,
|
||||
CASE WHEN SRM.PAID_TYPE = 'paid' THEN '유상'
|
||||
WHEN SRM.PAID_TYPE = 'free' THEN '무상'
|
||||
ELSE COALESCE(SRM.PAID_TYPE, '')
|
||||
END AS paid_type_name,
|
||||
PM.PROJECT_NO AS project_number,
|
||||
-- 고객사 (wace 동일 — 프로젝트.contract_mgmt.customer_objid 우선)
|
||||
COALESCE(
|
||||
(SELECT CM2.CLIENT_NM FROM CLIENT_MNG CM2
|
||||
WHERE 'C_' || CM2.OBJID::VARCHAR = CTR.CUSTOMER_OBJID LIMIT 1),
|
||||
(SELECT SUPPLY_NAME FROM SUPPLY_MNG SM
|
||||
WHERE SM.OBJID::VARCHAR = CTR.CUSTOMER_OBJID LIMIT 1),
|
||||
''
|
||||
) AS customer_name,
|
||||
-- 품번/품명 (MBOM_DETAIL → PART_MNG, 다중이면 "외 N건")
|
||||
COALESCE((SELECT PP.PART_NO FROM MBOM_DETAIL MD
|
||||
JOIN PART_MNG PP ON MD.PART_OBJID::VARCHAR = PP.OBJID::VARCHAR
|
||||
WHERE MD.MBOM_HEADER_OBJID = SRM.MBOM_HEADER_OBJID
|
||||
ORDER BY MD.REGDATE LIMIT 1), '') AS part_no,
|
||||
COALESCE((SELECT PP.PART_NAME FROM MBOM_DETAIL MD
|
||||
JOIN PART_MNG PP ON MD.PART_OBJID::VARCHAR = PP.OBJID::VARCHAR
|
||||
WHERE MD.MBOM_HEADER_OBJID = SRM.MBOM_HEADER_OBJID
|
||||
ORDER BY MD.REGDATE LIMIT 1), '') AS part_name,
|
||||
(SELECT COUNT(DISTINCT PP.PART_NO)::int - 1
|
||||
FROM MBOM_DETAIL MD
|
||||
JOIN PART_MNG PP ON MD.PART_OBJID::VARCHAR = PP.OBJID::VARCHAR
|
||||
WHERE MD.MBOM_HEADER_OBJID = SRM.MBOM_HEADER_OBJID) AS part_extra_count,
|
||||
-- 견적요청서 존재여부 (quotation_request_master 누락 → 일괄 'N')
|
||||
'N' AS has_quotation_request,
|
||||
SRM.REQUEST_USER_ID AS request_user,
|
||||
COALESCE(user_name(SRM.REQUEST_USER_ID), SRM.REQUEST_USER_ID, '') AS request_user_name,
|
||||
SRM.DELIVERY_REQUEST_DATE AS delivery_request_date,
|
||||
TO_CHAR(SRM.REGDATE, 'YYYY-MM-DD') AS regdate_title,
|
||||
SRM.MBOM_HEADER_OBJID AS mbom_header_objid
|
||||
FROM SALES_REQUEST_MASTER SRM
|
||||
LEFT JOIN PROJECT_MGMT PM ON PM.OBJID::VARCHAR = SRM.PROJECT_NO
|
||||
LEFT JOIN CONTRACT_MGMT CTR ON CTR.OBJID = PM.CONTRACT_OBJID
|
||||
${whereSql}
|
||||
ORDER BY SRM.REGDATE DESC
|
||||
LIMIT ${addParam(limit)} OFFSET ${addParam(offset)}
|
||||
`;
|
||||
const countSql = `
|
||||
SELECT COUNT(*)::int AS cnt
|
||||
FROM SALES_REQUEST_MASTER SRM
|
||||
LEFT JOIN PROJECT_MGMT PM ON PM.OBJID::VARCHAR = SRM.PROJECT_NO
|
||||
LEFT JOIN CONTRACT_MGMT CTR ON CTR.OBJID = PM.CONTRACT_OBJID
|
||||
${whereSql}
|
||||
`;
|
||||
try {
|
||||
const [d, c] = await Promise.all([
|
||||
pool.query(dataSql, params),
|
||||
pool.query(countSql, params.slice(0, params.length - 2)),
|
||||
]);
|
||||
return { rows: d.rows, totalCount: c.rows[0]?.cnt ?? 0, page, pageSize };
|
||||
} catch (e: any) {
|
||||
logger.error("listPurchaseRequest 실패", { error: e.message });
|
||||
return { rows: [], totalCount: 0, page, pageSize };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 2) 견적요청서관리 (wace salesMng.xml quotationRequestList) ──
|
||||
// quotation_request_master 누락 → 빈 그리드.
|
||||
export async function listQuotationRequest(filter: PurchaseListFilter): Promise<ListResult<any>> {
|
||||
const { page, pageSize } = clampPaging(filter);
|
||||
logger.warn("listQuotationRequest: quotation_request_master 테이블 미존재 — 빈 응답");
|
||||
return { rows: [], totalCount: 0, page, pageSize };
|
||||
}
|
||||
|
||||
// ─── 3) 품의서관리 (wace salesMng.xml proposalMngList) ──
|
||||
// sales_request_master.doc_type='PROPOSAL'.
|
||||
// 결재 우선순위: AMR.STATUS > APPROVAL.APPR_STATUS > SRM.STATUS('create'→'등록중')
|
||||
export async function listProposal(filter: PurchaseListFilter): Promise<ListResult<any>> {
|
||||
const pool = getPool();
|
||||
const { limit, offset, page, pageSize } = clampPaging(filter);
|
||||
|
||||
const where: string[] = [
|
||||
`SRM.STATUS IN ('create','approvalRequest','approvalComplete','reject')`,
|
||||
`(SRM.DOC_TYPE = 'PROPOSAL' OR SRM.DOC_TYPE = 'PURCHASE_REG_PROPOSAL')`,
|
||||
];
|
||||
const params: any[] = [];
|
||||
const addParam = (val: any) => { params.push(val); return `$${params.length}`; };
|
||||
|
||||
if (filter.proposal_no) where.push(`SRM.REQUEST_MNG_NO ILIKE ${addParam(`%${filter.proposal_no}%`)}`);
|
||||
if (filter.project_no) where.push(`EXISTS (SELECT 1 FROM PROJECT_MGMT PMX WHERE PMX.OBJID::VARCHAR = SRM.PROJECT_NO AND PMX.PROJECT_NO ILIKE ${addParam(`%${filter.project_no}%`)})`);
|
||||
if (filter.search_status) where.push(`SRM.STATUS = ${addParam(filter.search_status)}`);
|
||||
if (filter.regdate_start) where.push(`SRM.REGDATE::DATE >= ${addParam(filter.regdate_start)}::DATE`);
|
||||
if (filter.regdate_end) where.push(`SRM.REGDATE::DATE <= ${addParam(filter.regdate_end)}::DATE`);
|
||||
if (filter.purchase_type) where.push(`SRM.PURCHASE_TYPE = ${addParam(filter.purchase_type)}`);
|
||||
if (filter.writer) where.push(`SRM.WRITER = ${addParam(filter.writer)}`);
|
||||
if (filter.part_type) where.push(`SRM.PRODUCT_NAME = ${addParam(filter.part_type)}`);
|
||||
|
||||
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
||||
|
||||
const dataSql = `
|
||||
SELECT
|
||||
SRM.OBJID AS objid,
|
||||
SRM.REQUEST_MNG_NO AS proposal_no,
|
||||
SRM.STATUS AS status,
|
||||
-- AMARANTH_STATUS 컬럼 RPS 미존재 → NULL (wace 1순위 결재상태 우선순위 향후 보완)
|
||||
NULL::text AS amaranth_status,
|
||||
PM.PROJECT_NO AS project_number,
|
||||
COALESCE(
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = SRM.PURCHASE_TYPE LIMIT 1), ''
|
||||
) AS purchase_type_name,
|
||||
COALESCE(
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = SRM.ORDER_TYPE LIMIT 1), ''
|
||||
) AS order_type_name,
|
||||
COALESCE(
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = SRM.PRODUCT_NAME LIMIT 1), ''
|
||||
) AS product_name_title,
|
||||
COALESCE((SELECT PP.PART_NO FROM MBOM_DETAIL MD
|
||||
JOIN PART_MNG PP ON MD.PART_OBJID::VARCHAR = PP.OBJID::VARCHAR
|
||||
WHERE MD.MBOM_HEADER_OBJID = SRM.MBOM_HEADER_OBJID
|
||||
ORDER BY MD.REGDATE LIMIT 1), '') AS part_no,
|
||||
COALESCE((SELECT PP.PART_NAME FROM MBOM_DETAIL MD
|
||||
JOIN PART_MNG PP ON MD.PART_OBJID::VARCHAR = PP.OBJID::VARCHAR
|
||||
WHERE MD.MBOM_HEADER_OBJID = SRM.MBOM_HEADER_OBJID
|
||||
ORDER BY MD.REGDATE LIMIT 1), '') AS part_name,
|
||||
(SELECT COUNT(DISTINCT PP.PART_NO)::int - 1
|
||||
FROM MBOM_DETAIL MD
|
||||
JOIN PART_MNG PP ON MD.PART_OBJID::VARCHAR = PP.OBJID::VARCHAR
|
||||
WHERE MD.MBOM_HEADER_OBJID = SRM.MBOM_HEADER_OBJID) AS part_extra_count,
|
||||
CASE SRM.STATUS
|
||||
WHEN 'create' THEN '작성중'
|
||||
WHEN 'approvalRequest' THEN '결재중'
|
||||
WHEN 'approvalComplete' THEN '결재완료'
|
||||
WHEN 'reject' THEN '반려'
|
||||
ELSE COALESCE(SRM.STATUS, '')
|
||||
END AS status_title,
|
||||
TO_CHAR(SRM.REGDATE, 'YYYY-MM-DD') AS regdate_title,
|
||||
SRM.WRITER AS writer,
|
||||
COALESCE(user_name(SRM.WRITER), SRM.WRITER, '') AS writer_name,
|
||||
SRM.MBOM_HEADER_OBJID AS mbom_header_objid
|
||||
FROM SALES_REQUEST_MASTER SRM
|
||||
LEFT JOIN PROJECT_MGMT PM ON PM.OBJID::VARCHAR = SRM.PROJECT_NO
|
||||
${whereSql}
|
||||
ORDER BY SRM.REGDATE DESC
|
||||
LIMIT ${addParam(limit)} OFFSET ${addParam(offset)}
|
||||
`;
|
||||
const countSql = `
|
||||
SELECT COUNT(*)::int AS cnt
|
||||
FROM SALES_REQUEST_MASTER SRM
|
||||
LEFT JOIN PROJECT_MGMT PM ON PM.OBJID::VARCHAR = SRM.PROJECT_NO
|
||||
${whereSql}
|
||||
`;
|
||||
try {
|
||||
const [d, c] = await Promise.all([
|
||||
pool.query(dataSql, params),
|
||||
pool.query(countSql, params.slice(0, params.length - 2)),
|
||||
]);
|
||||
return { rows: d.rows, totalCount: c.rows[0]?.cnt ?? 0, page, pageSize };
|
||||
} catch (e: any) {
|
||||
logger.error("listProposal 실패", { error: e.message });
|
||||
return { rows: [], totalCount: 0, page, pageSize };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 4) 입고관리 (wace purchaseOrder.xml deliveryMngAcceptanceList) ──
|
||||
// purchase_order_master + purchase_order_part(누락) + arrival_plan(누락).
|
||||
// 누락 의존 — purchase_order_master 단독으로 빈 그리드 처리.
|
||||
export async function listInbound(filter: PurchaseListFilter): Promise<ListResult<any>> {
|
||||
const { page, pageSize } = clampPaging(filter);
|
||||
logger.warn("listInbound: purchase_order_part / arrival_plan 미존재 — 빈 응답");
|
||||
return { rows: [], totalCount: 0, page, pageSize };
|
||||
}
|
||||
|
||||
// ─── 5) 품목별 입고관리 (wace deliveryMngPartList) ──
|
||||
export async function listInboundByItem(filter: PurchaseListFilter): Promise<ListResult<any>> {
|
||||
const { page, pageSize } = clampPaging(filter);
|
||||
logger.warn("listInboundByItem: purchase_order_part / arrival_plan 미존재 — 빈 응답");
|
||||
return { rows: [], totalCount: 0, page, pageSize };
|
||||
}
|
||||
|
||||
// ─── 6) 입고일별 입고관리 (wace purchaseCloseList) ──
|
||||
export async function listInboundByDate(filter: PurchaseListFilter): Promise<ListResult<any>> {
|
||||
const { page, pageSize } = clampPaging(filter);
|
||||
logger.warn("listInboundByDate: arrival_plan / purchase_order_part 미존재 — 빈 응답");
|
||||
return { rows: [], totalCount: 0, page, pageSize };
|
||||
}
|
||||
|
||||
// ─── 7) 프로젝트별 발주/입고 현황 (wace projectPurchaseDeliveryStatus) ──
|
||||
// contract_mgmt + mbom_header/detail (전체수량/품목수) + purchase_order_master/part (발주현황) +
|
||||
// arrival_plan (입고현황). 발주/입고는 누락 테이블 의존 → 0 표시.
|
||||
export async function listProjectStatus(filter: PurchaseListFilter): Promise<ListResult<any>> {
|
||||
const pool = getPool();
|
||||
const { limit, offset, page, pageSize } = clampPaging(filter);
|
||||
|
||||
// wace 운영판 WHERE CTR.MAIL_SEND_DATE IS NOT NULL — RPS contract_mgmt 미존재 컬럼이라 생략 (전체 노출)
|
||||
const where: string[] = [];
|
||||
const params: any[] = [];
|
||||
const addParam = (val: any) => { params.push(val); return `$${params.length}`; };
|
||||
|
||||
if (filter.customer_objid) where.push(`CTR.CUSTOMER_OBJID = ${addParam(filter.customer_objid)}`);
|
||||
if (filter.project_no) where.push(`PM.PROJECT_NO ILIKE ${addParam(`%${filter.project_no}%`)}`);
|
||||
if (filter.product_cd) where.push(`CTR.PRODUCT = ${addParam(filter.product_cd)}`);
|
||||
if (filter.part_no) where.push(`PM.PART_NO ILIKE ${addParam(`%${filter.part_no}%`)}`);
|
||||
if (filter.part_name) where.push(`PM.PART_NAME ILIKE ${addParam(`%${filter.part_name}%`)}`);
|
||||
if (filter.year) where.push(`EXTRACT(YEAR FROM PM.REGDATE) = ${addParam(Number(filter.year))}`);
|
||||
|
||||
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
||||
|
||||
const dataSql = `
|
||||
SELECT
|
||||
PM.OBJID AS objid,
|
||||
PM.PROJECT_NO AS project_no,
|
||||
COALESCE(
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CTR.PRODUCT LIMIT 1), ''
|
||||
) AS product_name,
|
||||
PM.PART_NO AS part_no,
|
||||
PM.PART_NAME AS part_name,
|
||||
COALESCE(
|
||||
(SELECT CLIENT_NM FROM CLIENT_MNG CL
|
||||
WHERE 'C_' || CL.OBJID::VARCHAR = CTR.CUSTOMER_OBJID LIMIT 1),
|
||||
(SELECT SUPPLY_NAME FROM SUPPLY_MNG SM
|
||||
WHERE SM.OBJID::VARCHAR = CTR.CUSTOMER_OBJID LIMIT 1),
|
||||
''
|
||||
) AS customer_name,
|
||||
-- BOM 기준 (mbom_detail)
|
||||
COALESCE((SELECT COUNT(DISTINCT MD.PART_OBJID)::int
|
||||
FROM MBOM_DETAIL MD
|
||||
JOIN MBOM_HEADER MH2 ON MD.MBOM_HEADER_OBJID = MH2.OBJID
|
||||
WHERE MH2.PROJECT_OBJID = PM.OBJID::VARCHAR
|
||||
AND MH2.STATUS = 'Y'), 0) AS total_item_cnt,
|
||||
COALESCE((SELECT SUM(NULLIF(MD.QTY, '')::numeric)
|
||||
FROM MBOM_DETAIL MD
|
||||
JOIN MBOM_HEADER MH2 ON MD.MBOM_HEADER_OBJID = MH2.OBJID
|
||||
WHERE MH2.PROJECT_OBJID = PM.OBJID::VARCHAR
|
||||
AND MH2.STATUS = 'Y'), 0) AS total_qty,
|
||||
-- 발주/입고/미발주/미입고 — purchase_order_part / arrival_plan 누락이라 모두 0
|
||||
0::int AS po_item_cnt,
|
||||
0::numeric AS po_qty,
|
||||
COALESCE((SELECT COUNT(DISTINCT MD.PART_OBJID)::int
|
||||
FROM MBOM_DETAIL MD
|
||||
JOIN MBOM_HEADER MH2 ON MD.MBOM_HEADER_OBJID = MH2.OBJID
|
||||
WHERE MH2.PROJECT_OBJID = PM.OBJID::VARCHAR
|
||||
AND MH2.STATUS = 'Y'), 0) AS non_po_item_cnt,
|
||||
COALESCE((SELECT SUM(NULLIF(MD.QTY, '')::numeric)
|
||||
FROM MBOM_DETAIL MD
|
||||
JOIN MBOM_HEADER MH2 ON MD.MBOM_HEADER_OBJID = MH2.OBJID
|
||||
WHERE MH2.PROJECT_OBJID = PM.OBJID::VARCHAR
|
||||
AND MH2.STATUS = 'Y'), 0) AS non_po_qty,
|
||||
0::int AS dlv_item_cnt,
|
||||
0::numeric AS dlv_qty,
|
||||
0::int AS non_dlv_item_cnt,
|
||||
0::numeric AS non_dlv_qty
|
||||
FROM PROJECT_MGMT PM
|
||||
LEFT JOIN CONTRACT_MGMT CTR ON CTR.OBJID = PM.CONTRACT_OBJID
|
||||
${whereSql}
|
||||
ORDER BY PM.REGDATE DESC
|
||||
LIMIT ${addParam(limit)} OFFSET ${addParam(offset)}
|
||||
`;
|
||||
const countSql = `
|
||||
SELECT COUNT(*)::int AS cnt
|
||||
FROM PROJECT_MGMT PM
|
||||
LEFT JOIN CONTRACT_MGMT CTR ON CTR.OBJID = PM.CONTRACT_OBJID
|
||||
${whereSql}
|
||||
`;
|
||||
try {
|
||||
const [d, c] = await Promise.all([
|
||||
pool.query(dataSql, params),
|
||||
pool.query(countSql, params.slice(0, params.length - 2)),
|
||||
]);
|
||||
return { rows: d.rows, totalCount: c.rows[0]?.cnt ?? 0, page, pageSize };
|
||||
} catch (e: any) {
|
||||
logger.error("listProjectStatus 실패", { error: e.message });
|
||||
return { rows: [], totalCount: 0, page, pageSize };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 옵션 — 공급업체 / 작성자 (구매메뉴 공용) ──────────────────
|
||||
|
||||
export async function listSupplierOptions(): Promise<{ code: string; label: string }[]> {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT OBJID::VARCHAR AS code, SUPPLY_NAME AS label
|
||||
FROM SUPPLY_MNG
|
||||
WHERE COALESCE(STATUS, 'active') IN ('active', '활성')
|
||||
AND SUPPLY_NAME IS NOT NULL AND SUPPLY_NAME <> ''
|
||||
ORDER BY SUPPLY_NAME`,
|
||||
);
|
||||
return r.rows;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function listUserOptions(): Promise<{ code: string; label: string }[]> {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT USER_ID AS code,
|
||||
USER_NAME || COALESCE(' (' || DEPT_NAME || ')', '') AS label
|
||||
FROM USER_INFO
|
||||
WHERE COALESCE(STATUS, 'active') IN ('active', '활성', 'ACTIVE')
|
||||
AND USER_NAME IS NOT NULL AND USER_NAME <> ''
|
||||
ORDER BY USER_NAME`,
|
||||
);
|
||||
return r.rows;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function listProjectOptions(): Promise<{ code: string; label: string }[]> {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT OBJID::VARCHAR AS code, PROJECT_NO AS label
|
||||
FROM PROJECT_MGMT
|
||||
WHERE PROJECT_NO IS NOT NULL AND PROJECT_NO <> ''
|
||||
ORDER BY PROJECT_NO DESC
|
||||
LIMIT 500`,
|
||||
);
|
||||
return r.rows;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user