구매관리 발주서관리 통합 + 폼 GET API + PageHeader 컨벤션
발주서관리 리스트:
- /purchase/order-wace 임시 라우트 → /purchase/order 통합 (기존 vexplor
변형판 대체). order-wace 폴더 삭제.
- 백엔드 라우트 /order-wace → /order-list, 함수 listPurchaseOrderWace →
listPurchaseOrderList, API 클라이언트 listOrderWace → listOrder.
발주서 폼 (general 양식) GET API:
- services/purchaseOrderFormService.ts 신규 (getPurchaseOrderFormInit,
getPurchaseOrderForm). 품의서 자동채움 = salesMng.getProposalPartList
매퍼 1:1 → 발주 그리드 형식 변환. 발주번호 채번 RPS{YY}-{MMDD}-{NN}.
- 컨트롤러/라우트: GET /api/purchase/order-form/init?proposal_objid=...
+ /api/purchase/order-form/:objid.
- RPS는 OBJID가 varchar라 wace numeric 캐스트 모두 제거.
PageHeader 컨벤션 일괄 변경:
- 자동매칭이 매칭된 menu의 parent_obj_id로 부모를 찾아
"{부모}_{자식}" 형식 표기 (wace 컨벤션). 부모가 루트 그룹이면 자식만.
- description prop과 렌더링 완전 제거 (사용처 없음 확인).
- 모든 메뉴 페이지에 일괄 적용.
DB(별도): menu_info 9857401373575 + rel_menu_auth 3건 제거.
저장/삭제 API + 프론트 다이얼로그는 다음 세션.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import * as svc from "../services/purchaseService";
|
||||
import * as formSvc from "../services/purchaseOrderFormService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
function parseFilter(q: Record<string, any>): svc.PurchaseListFilter {
|
||||
@@ -36,7 +37,41 @@ export const getInbound = (req: AuthenticatedRequest, res: Response)
|
||||
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 const getPurchaseOrderWace = (req: AuthenticatedRequest, res: Response) => runList(svc.listPurchaseOrderWace, req, res, "발주서관리");
|
||||
export const getPurchaseOrderList = (req: AuthenticatedRequest, res: Response) => runList(svc.listPurchaseOrderList, req, res, "발주서관리");
|
||||
|
||||
// ─── 발주서 폼 (general 양식) ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /api/purchase/order-form/init?proposal_objid=...
|
||||
* 품의서에서 발주서 등록 폼 데이터 자동 채움.
|
||||
*/
|
||||
export async function getPurchaseOrderFormInit(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const proposalObjid = String(req.query.proposal_objid ?? "").trim();
|
||||
const data = await formSvc.getPurchaseOrderFormInit(proposalObjid);
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("발주서 폼 초기화 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/purchase/order-form/:objid
|
||||
* 발주서 마스터 + 파트 조회 (수정/조회 모드).
|
||||
*/
|
||||
export async function getPurchaseOrderForm(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const objid = String(req.params.objid ?? "").trim();
|
||||
if (!objid) return res.status(400).json({ success: false, message: "objid required" });
|
||||
const data = await formSvc.getPurchaseOrderForm(objid);
|
||||
if (!data) return res.status(404).json({ success: false, message: "발주서를 찾을 수 없어요" });
|
||||
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 getSuppliers(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
|
||||
@@ -18,7 +18,11 @@ 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("/order-wace", ctrl.getPurchaseOrderWace); // 발주서관리 (wace 1:1, 기존 /order 보존)
|
||||
router.get("/order-list", ctrl.getPurchaseOrderList); // 발주서관리 (wace purchaseOrderMasterList_new 1:1)
|
||||
|
||||
// 발주서 폼 (general 양식, wace purchaseOrderFormPopup_general.do 1:1)
|
||||
router.get("/order-form/init", ctrl.getPurchaseOrderFormInit); // 품의서에서 자동 채움
|
||||
router.get("/order-form/:objid", ctrl.getPurchaseOrderForm); // 수정/조회
|
||||
|
||||
// 공통 옵션
|
||||
router.get("/options/suppliers", ctrl.getSuppliers);
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
// ============================================================
|
||||
// 발주서관리 — 등록/수정 폼 (general 양식) 서비스
|
||||
//
|
||||
// wace_plm 1:1 이식 베이스:
|
||||
// - controller: purchaseOrder/purchaseOrderFormPopup_general.do
|
||||
// purchaseOrder/purchaseOrderFormPopup_generalSave.do
|
||||
// - service: PurchaseOrderService.savePurchaseOrder_new (1472-1817)
|
||||
// - mapper: purchaseOrder.xml mergePurchaseOrderMaster (530-714) +
|
||||
// mergePurchaseOrderPartInfo (1205-1325) +
|
||||
// getPurchaseOrderMasterInfo (1343-1556) +
|
||||
// getPURCHASE_ORDER_PART
|
||||
// salesMng.xml getProposalPartList (5012-5125) +
|
||||
// getProposalInfo (4919-)
|
||||
//
|
||||
// 운영 핵심 흐름: 품의서(/purchase/proposal)에서 "발주서생성" → general 다이얼로그 →
|
||||
// 품의서 품목 자동 채움 → 마스터 입력 → 저장.
|
||||
//
|
||||
// 본 모듈은 form-init / form-get 두 GET 엔드포인트를 제공.
|
||||
// (save / delete 는 다음 단계)
|
||||
// ============================================================
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export interface OrderFormInitResult {
|
||||
master: Record<string, any>;
|
||||
parts: Record<string, any>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/purchase/order-form/init?proposal_objid=...
|
||||
*
|
||||
* 품의서(sales_request_master)에서 발주서 등록 폼을 채울 데이터를 반환.
|
||||
* wace controller `purchaseOrderFormPopup_general.do` 의 신규 등록 분기와 1:1.
|
||||
*
|
||||
* master 기본값:
|
||||
* - PROPOSAL_OBJID / SALES_REQUEST_OBJID = proposal_objid
|
||||
* - CONTRACT_MGMT_OBJID = proposal.PROJECT_NO
|
||||
* - PURCHASE_ORDER_NO = "RPS{YY}-{MMDD}-{NN}" (NN: 당일 발주 카운트+1)
|
||||
* - PURCHASE_DATE / ORDER_DATE = 오늘
|
||||
* - STATUS = "create" / FORM_TYPE = "general"
|
||||
* parts: salesMng.getProposalPartList SQL 1:1 → 발주서 그리드 형식 변환
|
||||
* (ORDER_QTY=QTY, PARTNER_PRICE=UNIT_PRICE, SUPPLY_UNIT_PRICE=QTY*UNIT_PRICE 등)
|
||||
*/
|
||||
export async function getPurchaseOrderFormInit(proposalObjid: string): Promise<OrderFormInitResult> {
|
||||
const pool = getPool();
|
||||
|
||||
// 1) 품의서 마스터 정보 (PROJECT_NO 등)
|
||||
let proposal: Record<string, any> | null = null;
|
||||
if (proposalObjid) {
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT OBJID, PROJECT_NO, MBOM_HEADER_OBJID, TITLE, REQUEST_USER_ID, PURCHASE_TYPE
|
||||
FROM SALES_REQUEST_MASTER WHERE OBJID = $1`,
|
||||
[proposalObjid],
|
||||
);
|
||||
proposal = r.rows[0] ?? null;
|
||||
} catch (e: any) {
|
||||
logger.warn("getProposalInfo 실패", { error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 발주번호 채번 (wace mergePurchaseOrderMaster INSERT 절 1:1)
|
||||
let purchaseOrderNo = "";
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT 'RPS' || TO_CHAR(NOW(),'YY') || '-' || TO_CHAR(NOW(),'MMDD') || '-' ||
|
||||
LPAD((COALESCE(MAX(CASE WHEN PURCHASE_ORDER_NO LIKE 'RPS' || TO_CHAR(NOW(),'YY-MMDD') || '-%'
|
||||
THEN SPLIT_PART(PURCHASE_ORDER_NO, '-', 3) ELSE '0' END)::INTEGER, 0) + 1)::TEXT, 2, '0')
|
||||
AS po_no
|
||||
FROM PURCHASE_ORDER_MASTER`,
|
||||
);
|
||||
purchaseOrderNo = r.rows[0]?.po_no ?? "";
|
||||
} catch (e: any) {
|
||||
logger.warn("발주번호 채번 실패", { error: e.message });
|
||||
}
|
||||
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
const master: Record<string, any> = {
|
||||
objid: "", // 신규 — 클라이언트가 채워 보내거나 save 시 채번
|
||||
purchase_order_no: purchaseOrderNo,
|
||||
purchase_date: todayIso, // 발주일
|
||||
order_date_kor: formatKorDate(new Date()), // 발주일자 한글 표기 (wace ORDER_DATE)
|
||||
status: "create",
|
||||
form_type: "general",
|
||||
sales_request_objid: proposalObjid || "",
|
||||
proposal_objid: proposalObjid || "",
|
||||
contract_mgmt_objid: proposal?.project_no ?? "",
|
||||
title: proposal?.title ?? "",
|
||||
// wace controller _general 기본 담당자 (RPS 운영 고정값)
|
||||
manager_name: "안동윤",
|
||||
manager_position: "팀장",
|
||||
manager_phone: "010-2313-2702",
|
||||
manager_email: "ady1225@rps-korea.com",
|
||||
manager_name2: "서동민",
|
||||
manager_position2: "주임",
|
||||
manager_phone2: "010-9538-9513",
|
||||
manager_email2: "sdm0927@rps-korea.com",
|
||||
};
|
||||
|
||||
// 3) 품의서 품목 → 발주서 파트 변환 (salesMng.getProposalPartList 1:1)
|
||||
const parts: Record<string, any>[] = [];
|
||||
if (proposalObjid) {
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT
|
||||
ROW_NUMBER() OVER(ORDER BY SRP.REGDATE) AS rnum,
|
||||
SRP.OBJID AS srp_objid,
|
||||
SRP.PART_OBJID AS part_objid,
|
||||
PM.PART_NO AS part_no,
|
||||
PM.PART_NAME AS part_name,
|
||||
PM.SPEC AS spec,
|
||||
PM.MATERIAL AS material,
|
||||
COALESCE(NULLIF(SRP.UNIT, ''), PM.UNIT_DC) AS unit,
|
||||
COALESCE(
|
||||
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = NULLIF(SRP.UNIT, '')),
|
||||
(SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = PM.UNIT_DC)
|
||||
) AS unit_title,
|
||||
SRP.QTY AS qty,
|
||||
COALESCE(SRP.UNIT_PRICE, 0) AS unit_price,
|
||||
CASE WHEN COALESCE(SRP.TOTAL_PRICE::NUMERIC, 0) > 0
|
||||
THEN SRP.TOTAL_PRICE::NUMERIC
|
||||
ELSE COALESCE(SRP.QTY::NUMERIC, 0) * COALESCE(SRP.UNIT_PRICE::NUMERIC, 0)
|
||||
END AS total_price,
|
||||
SRP.VENDOR_PM AS vendor_pm,
|
||||
(SELECT CLIENT_NM FROM CLIENT_MNG
|
||||
WHERE OBJID::VARCHAR = SRP.VENDOR_PM) AS vendor_name,
|
||||
SRP.REMARK AS remark,
|
||||
SRP.DELIVERY_REQUEST_DATE AS delivery_request_date,
|
||||
COALESCE(SRP.MATERIAL_YN, 'N') AS material_yn,
|
||||
(SELECT PJ.PART_NAME FROM PROJECT_MGMT PJ
|
||||
WHERE PJ.OBJID::VARCHAR = SRM.PROJECT_NO) AS project_product_name,
|
||||
PM.PART_NAME AS component_part_name,
|
||||
SRP.CURRENCY AS currency,
|
||||
(SELECT CC.CODE_NAME FROM COMM_CODE CC
|
||||
WHERE CC.CODE_ID = NULLIF(SRP.CURRENCY, '')) AS currency_name
|
||||
FROM SALES_REQUEST_PART SRP
|
||||
LEFT JOIN PART_MNG PM
|
||||
ON SRP.PART_OBJID::VARCHAR = PM.OBJID::VARCHAR
|
||||
LEFT JOIN SALES_REQUEST_MASTER SRM
|
||||
ON SRP.SALES_REQUEST_MASTER_OBJID = SRM.OBJID
|
||||
WHERE SRP.SALES_REQUEST_MASTER_OBJID = $1
|
||||
ORDER BY SRP.REGDATE`,
|
||||
[proposalObjid],
|
||||
);
|
||||
|
||||
for (const row of r.rows) {
|
||||
const qty = toNum(row.qty);
|
||||
const unitPrice = toNum(row.unit_price);
|
||||
const projectName = (row.project_product_name ?? "").toString().trim();
|
||||
const componentName = (row.component_part_name ?? "").toString().trim();
|
||||
const remark = projectName && componentName ? `${projectName} / ${componentName}`
|
||||
: projectName || componentName || (row.remark ?? "");
|
||||
|
||||
parts.push({
|
||||
// 발주서 그리드 형식 (wace controller _general 변환 1:1)
|
||||
objid: "", // 신규 row
|
||||
part_objid: row.part_objid ?? "",
|
||||
row_num: row.rnum,
|
||||
part_no: row.part_no ?? "",
|
||||
part_name: row.part_name ?? "",
|
||||
spec: row.spec ?? "",
|
||||
material: row.material ?? "",
|
||||
order_qty: qty,
|
||||
unit: row.unit || "0001400", // wace 기본값 EA
|
||||
unit_title: row.unit_title ?? "",
|
||||
part_delivery_place: "RPS",
|
||||
partner_price: unitPrice,
|
||||
supply_unit_price: qty * unitPrice,
|
||||
remark,
|
||||
delivery_request_date: row.delivery_request_date ?? "",
|
||||
currency: row.currency ?? "",
|
||||
currency_name: row.currency_name ?? "",
|
||||
// 추적용 (저장 시 신규 row 임을 구분)
|
||||
_src: "proposal",
|
||||
_src_objid: row.srp_objid,
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error("getProposalPartList 실패", { error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
return { master, parts };
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/purchase/order-form/:objid
|
||||
*
|
||||
* 발주서 마스터 + 파트 조회 (수정/조회 모드).
|
||||
* wace `getPurchaseOrderMasterInfo` (1343-1556) + `getPURCHASE_ORDER_PART` 의 RPS 압축판.
|
||||
*/
|
||||
export async function getPurchaseOrderForm(objid: string): Promise<OrderFormInitResult | null> {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const m = await pool.query(
|
||||
`SELECT POM.*,
|
||||
(SELECT CLIENT_NM FROM CLIENT_MNG
|
||||
WHERE OBJID::VARCHAR = POM.PARTNER_OBJID) AS partner_name,
|
||||
(SELECT USER_NAME FROM USER_INFO
|
||||
WHERE USER_ID = POM.SALES_MNG_USER_ID) AS sales_mng_user_name,
|
||||
(SELECT USER_NAME FROM USER_INFO
|
||||
WHERE USER_ID = POM.WRITER) AS writer_name,
|
||||
CM.PROJECT_NO AS project_no,
|
||||
SRM.REQUEST_MNG_NO AS proposal_no
|
||||
FROM PURCHASE_ORDER_MASTER POM
|
||||
LEFT JOIN PROJECT_MGMT CM ON POM.CONTRACT_MGMT_OBJID = CM.OBJID
|
||||
LEFT JOIN SALES_REQUEST_MASTER SRM ON POM.SALES_REQUEST_OBJID = SRM.OBJID
|
||||
WHERE POM.OBJID = $1`,
|
||||
[objid],
|
||||
);
|
||||
if (m.rows.length === 0) return null;
|
||||
|
||||
const p = await pool.query(
|
||||
`SELECT POP.*,
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = POP.UNIT) AS unit_title,
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = POP.CURRENCY) AS currency_name
|
||||
FROM PURCHASE_ORDER_PART POP
|
||||
WHERE POP.PURCHASE_ORDER_MASTER_OBJID = $1
|
||||
ORDER BY POP.REGDATE`,
|
||||
[objid],
|
||||
);
|
||||
|
||||
return { master: m.rows[0], parts: p.rows };
|
||||
} catch (e: any) {
|
||||
logger.error("getPurchaseOrderForm 실패", { error: e.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function toNum(v: any): number {
|
||||
if (v == null || v === "") return 0;
|
||||
const s = String(v).replace(/,/g, "");
|
||||
const n = Number(s);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
function formatKorDate(d: Date): string {
|
||||
return `${d.getFullYear()}년 ${String(d.getMonth() + 1).padStart(2, "0")}월 ${String(d.getDate()).padStart(2, "0")}일`;
|
||||
}
|
||||
@@ -873,13 +873,13 @@ export async function listProjectStatus(filter: PurchaseListFilter): Promise<Lis
|
||||
//
|
||||
// 매퍼 본문: wace_plm/src/com/pms/mapper/purchaseOrder.xml:3295-3589
|
||||
// 화면: wace_plm/.../purchaseOrder/purchaseOrderList_new.jsp (1085 lines)
|
||||
// 라우트: /api/purchase-wace/order-list (기존 /purchase/order page.tsx 보존 + 새 라우트)
|
||||
// 라우트: GET /api/purchase/order-list
|
||||
//
|
||||
// 컬럼: 품의서No · 발주서No · 프로젝트번호 · 구매유형 · 주문유형 · 제품구분 ·
|
||||
// 품번 · 품명 · 공급업체 · 환종 · 총액 · 메일발송 · 발주일 · 구매담당자 · 작성일
|
||||
// 검색: 년도/고객사/프로젝트(CSV)/발주No/공급업체/품번/품명/입고요청일/발주일/
|
||||
// 주문유형/제품구분/구매유형/구매담당자/메일발송
|
||||
export async function listPurchaseOrderWace(filter: PurchaseListFilter): Promise<ListResult<any>> {
|
||||
export async function listPurchaseOrderList(filter: PurchaseListFilter): Promise<ListResult<any>> {
|
||||
const pool = getPool();
|
||||
const { limit, offset, page, pageSize } = clampPaging(filter);
|
||||
|
||||
@@ -1054,7 +1054,7 @@ export async function listPurchaseOrderWace(filter: PurchaseListFilter): Promise
|
||||
]);
|
||||
return { rows: d.rows, totalCount: c.rows[0]?.cnt ?? 0, page, pageSize };
|
||||
} catch (e: any) {
|
||||
logger.error("listPurchaseOrderWace 실패", { error: e.message });
|
||||
logger.error("listPurchaseOrderList 실패", { error: e.message });
|
||||
return { rows: [], totalCount: 0, page, pageSize };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user