구매관리 견적요청서 빈 그리드 채움 (quotation_request_master/detail 신설)

- DDL: quotation_request_master(14 cols) + quotation_request_detail(15 cols)
  운영 → RPS 타입 차이: numeric objid → varchar(64), detail.part_objid bigint(part_mng FK)
- 데이터: 운영 sample master 4건 / detail 4건 (sales_request_part 미존재 → NULL fallback)
- 백엔드 listQuotationRequest — wace salesMng.xml:5248-5349 매퍼 1:1
  (vendor → client_mng JOIN, attach_file_info QUOTATION_RECEIVED 카운트)
- listVendorOptions(client_mng) 신규 — 발주서 vendor 옵션이 supply_mng 와 분리됨
- listPurchaseRequest.has_quotation_request 분기 활성화
- quote-request page.tsx UI 문자열 내부 참조 제거, vendor 옵션 client_mng 로 교체
This commit is contained in:
hjjeong
2026-05-15 15:32:05 +09:00
parent 30a891f4b8
commit 6b029e20f9
7 changed files with 274 additions and 11 deletions
@@ -47,6 +47,16 @@ export async function getSuppliers(_req: AuthenticatedRequest, res: Response) {
}
}
export async function getVendors(_req: AuthenticatedRequest, res: Response) {
try {
const data = await svc.listVendorOptions();
return res.json({ success: true, data });
} catch (e: any) {
logger.error("공급업체(client_mng) 옵션 실패", { 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();
@@ -21,6 +21,7 @@ router.get("/project-status", ctrl.getProjectStatus); // 프로젝트별
// 공통 옵션
router.get("/options/suppliers", ctrl.getSuppliers);
router.get("/options/vendors", ctrl.getVendors); // wace client_mng 기반
router.get("/options/users", ctrl.getUsers);
router.get("/options/projects", ctrl.getProjects);
+131 -7
View File
@@ -144,8 +144,11 @@ export async function listPurchaseRequest(filter: PurchaseListFilter): Promise<L
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,
-- 견적요청서 존재여부 (quotation_request_master 와 sales_request_master_objid 매칭)
CASE WHEN EXISTS (
SELECT 1 FROM QUOTATION_REQUEST_MASTER QRM
WHERE QRM.SALES_REQUEST_MASTER_OBJID = SRM.OBJID
) THEN 'Y' ELSE 'N' END 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,
@@ -177,12 +180,115 @@ export async function listPurchaseRequest(filter: PurchaseListFilter): Promise<L
}
}
// ─── 2) 견적요청서관리 (wace salesMng.xml quotationRequestList) ──
// quotation_request_master 누락 → 빈 그리드.
// ─── 2) 견적요청서관리 (wace salesMng.xml getQuotationRequestList 매퍼 1:1) ──
//
// quotation_request_master + quotation_request_detail + sales_request_master +
// project_mgmt + contract_mgmt + client_mng (vendor) + comm_code + user_info +
// attach_file_info (DOC_TYPE='QUOTATION_RECEIVED').
//
// 매퍼 본문: wace_plm/src/com/pms/mapper/salesMng.xml:5248-5349
// 검색: year / project_no / quotation_request_no / vendor / mail_send_yn / writer / product_cd
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 };
const pool = getPool();
const { limit, offset, page, pageSize } = clampPaging(filter);
const where: string[] = [];
const params: any[] = [];
const addParam = (val: any) => { params.push(val); return `$${params.length}`; };
if (filter.year) where.push(`EXTRACT(YEAR FROM QRM.REG_DATE) = ${addParam(Number(filter.year))}`);
if (filter.project_no) where.push(`PM.PROJECT_NO ILIKE ${addParam(`%${filter.project_no}%`)}`);
if (filter.proposal_no) where.push(`QRM.QUOTATION_REQUEST_NO ILIKE ${addParam(`%${filter.proposal_no}%`)}`);
if (filter.partner_objid) where.push(`QRM.VENDOR_OBJID = ${addParam(filter.partner_objid)}`);
if (filter.mail_send_yn) where.push(`COALESCE(QRM.MAIL_SEND_YN, 'N') = ${addParam(filter.mail_send_yn)}`);
if (filter.writer) where.push(`QRM.WRITER = ${addParam(filter.writer)}`);
if (filter.product_cd) where.push(`COALESCE(CTM.PRODUCT, SRM.PRODUCT_NAME) = ${addParam(filter.product_cd)}`);
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
const dataSql = `
SELECT
QRM.OBJID AS objid,
QRM.QUOTATION_REQUEST_NO AS quotation_request_no,
QRM.SALES_REQUEST_MASTER_OBJID AS sales_request_master_objid,
QRM.PROJECT_MGMT_OBJID AS project_mgmt_objid,
QRM.VENDOR_OBJID AS vendor_objid,
QRM.VENDOR_TYPE AS vendor_type,
QRM.STATUS AS status,
CASE QRM.STATUS
WHEN 'create' THEN '작성중'
WHEN 'sent' THEN '발송완료'
WHEN 'received' THEN '견적수신'
WHEN 'completed' THEN '완료'
ELSE COALESCE(QRM.STATUS, '')
END AS status_name,
QRM.MAIL_SEND_DATE AS mail_send_date,
TO_CHAR(QRM.MAIL_SEND_DATE, 'YYYY-MM-DD') AS mail_send_date_title,
COALESCE(QRM.MAIL_SEND_YN, 'N') AS mail_send_yn,
QRM.DUE_DATE AS due_date,
TO_CHAR(QRM.DUE_DATE, 'YYYY-MM-DD') AS due_date_title,
QRM.REMARK AS remark,
QRM.WRITER AS writer,
COALESCE((SELECT USER_NAME FROM USER_INFO WHERE USER_ID = QRM.WRITER LIMIT 1), QRM.WRITER, '') AS writer_name,
QRM.REG_DATE AS reg_date,
TO_CHAR(QRM.REG_DATE, 'YYYY-MM-DD') AS reg_date_title,
SRM.REQUEST_MNG_NO AS request_mng_no,
SRM.PURCHASE_TYPE AS purchase_type,
COALESCE(
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = SRM.PURCHASE_TYPE LIMIT 1), ''
) AS purchase_type_name,
COALESCE(PM.CATEGORY_CD, SRM.ORDER_TYPE) AS order_type,
COALESCE(
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = COALESCE(PM.CATEGORY_CD, SRM.ORDER_TYPE) LIMIT 1), ''
) AS order_type_name,
COALESCE(
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CTM.PRODUCT LIMIT 1),
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = SRM.PRODUCT_NAME LIMIT 1),
''
) AS product_name_title,
PM.PROJECT_NO AS project_number,
CM.CLIENT_NM AS vendor_name,
(SELECT QRD.PART_NO FROM QUOTATION_REQUEST_DETAIL QRD
WHERE QRD.QUOTATION_REQUEST_MASTER_OBJID = QRM.OBJID
ORDER BY QRD.OBJID LIMIT 1) AS part_no,
(SELECT QRD.PART_NAME FROM QUOTATION_REQUEST_DETAIL QRD
WHERE QRD.QUOTATION_REQUEST_MASTER_OBJID = QRM.OBJID
ORDER BY QRD.OBJID LIMIT 1) AS part_name,
(SELECT COUNT(*)::int FROM QUOTATION_REQUEST_DETAIL QRD
WHERE QRD.QUOTATION_REQUEST_MASTER_OBJID = QRM.OBJID) AS detail_count,
(SELECT COUNT(*)::int FROM ATTACH_FILE_INFO
WHERE TARGET_OBJID = QRM.OBJID
AND DOC_TYPE = 'QUOTATION_RECEIVED'
AND COALESCE(STATUS, 'Active') = 'Active') AS attach_file_cnt
FROM QUOTATION_REQUEST_MASTER QRM
LEFT JOIN SALES_REQUEST_MASTER SRM ON QRM.SALES_REQUEST_MASTER_OBJID = SRM.OBJID
LEFT JOIN PROJECT_MGMT PM ON PM.OBJID = SRM.PROJECT_NO
LEFT JOIN CONTRACT_MGMT CTM ON CTM.OBJID = PM.CONTRACT_OBJID
LEFT JOIN CLIENT_MNG CM ON QRM.VENDOR_OBJID = CM.OBJID
${whereSql}
ORDER BY QRM.REG_DATE DESC
LIMIT ${addParam(limit)} OFFSET ${addParam(offset)}
`;
const countSql = `
SELECT COUNT(*)::int AS cnt
FROM QUOTATION_REQUEST_MASTER QRM
LEFT JOIN SALES_REQUEST_MASTER SRM ON QRM.SALES_REQUEST_MASTER_OBJID = SRM.OBJID
LEFT JOIN PROJECT_MGMT PM ON PM.OBJID = SRM.PROJECT_NO
LEFT JOIN CONTRACT_MGMT CTM ON CTM.OBJID = PM.CONTRACT_OBJID
LEFT JOIN CLIENT_MNG CM ON QRM.VENDOR_OBJID = CM.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("listQuotationRequest 실패", { error: e.message });
return { rows: [], totalCount: 0, page, pageSize };
}
}
// ─── 3) 품의서관리 (wace salesMng.xml proposalMngList) ──
@@ -388,6 +494,24 @@ export async function listProjectStatus(filter: PurchaseListFilter): Promise<Lis
// ─── 옵션 — 공급업체 / 작성자 (구매메뉴 공용) ──────────────────
// 견적요청서 / 발주서 vendor — wace 매퍼는 client_mng 와 매칭 (supply_mng 아님)
export async function listVendorOptions(): Promise<{ code: string; label: string }[]> {
const pool = getPool();
try {
const r = await pool.query(
`SELECT OBJID AS code, CLIENT_NM AS label
FROM CLIENT_MNG
WHERE COALESCE(STATUS, 'Y') IN ('Y', 'active', 'ACTIVE', '활성')
AND CLIENT_NM IS NOT NULL AND CLIENT_NM <> ''
ORDER BY CLIENT_NM
LIMIT 2000`,
);
return r.rows;
} catch {
return [];
}
}
export async function listSupplierOptions(): Promise<{ code: string; label: string }[]> {
const pool = getPool();
try {