b38f5957f2
구매관리 (wace 1:1) - backend: services/purchaseService.ts (7 list + 옵션 3종) + controllers/purchaseController.ts + routes/purchaseRoutes.ts (/api/purchase 마운트) - frontend: lib/api/purchase.ts + 7 page.tsx (list/quote-request/proposal/inbound/inbound-by-item/inbound-by-date/project-status) - 영업관리 4메뉴 DataGrid 패턴 통일 — pageSizeOptions=[10,15,20,50,100], emptyMessage, showColumnSettings/summaryStats/onRefresh/onDownload/showChart - 마스터단독 데이터(sales_request_master, project_mgmt+mbom_detail) 노출, detail/part 누락 테이블 의존은 빈 그리드 + UI 발주관리 (purchase/order/page.tsx) - EDataTable → DataGrid 교체 + logicstudio 6종 props + 날짜/숫자 pre-format M-BOM PR-B3 — 구매리스트 생성 (wace createPurchaseListFromMBom.do 1:1) - mbomService.createSalesRequest + controller + route POST /api/production/mbom/sales-request - 단건 체크 + 1:1 강제 + R-YYYYMMDD-NNN 채번 + sales_request_master 단건 INSERT - production/mbom/page.tsx 에 [구매리스트 생성] 버튼 M-BOM PR-B5 — BOM 할당 (mBomEbomSelectPopup.do) - mbomService.searchAssignableEboms/assignBom + controller + routes - MbomAssignDialog 신규, MbomDetailDialog 통합 생산관리 4메뉴 라우트 (생산계획&실적, 소요량) - prodPlanResultService/Controller + productionPlanResultRoutes (planResult/mbomReq) - mbomRequirementService + 4 page.tsx (prod-plan-result, prod-plan-result-equip, raw-material-requirement, semi-product-requirement) - lib/api/prodPlanResult.ts Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
439 lines
21 KiB
TypeScript
439 lines
21 KiB
TypeScript
// ============================================================
|
|
// 구매관리 — 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 [];
|
|
}
|
|
}
|