구매관리 발주서관리 통합 + 폼 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 { Response } from "express";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import * as svc from "../services/purchaseService";
|
import * as svc from "../services/purchaseService";
|
||||||
|
import * as formSvc from "../services/purchaseOrderFormService";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
function parseFilter(q: Record<string, any>): svc.PurchaseListFilter {
|
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 getInboundByItem = (req: AuthenticatedRequest, res: Response) => runList(svc.listInboundByItem, req, res, "품목별 입고관리");
|
||||||
export const getInboundByDate = (req: AuthenticatedRequest, res: Response) => runList(svc.listInboundByDate, 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 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) {
|
export async function getSuppliers(_req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ router.get("/inbound", ctrl.getInbound); // 입고관리
|
|||||||
router.get("/inbound-by-item", ctrl.getInboundByItem); // 품목별 입고관리
|
router.get("/inbound-by-item", ctrl.getInboundByItem); // 품목별 입고관리
|
||||||
router.get("/inbound-by-date", ctrl.getInboundByDate); // 입고일별 입고관리
|
router.get("/inbound-by-date", ctrl.getInboundByDate); // 입고일별 입고관리
|
||||||
router.get("/project-status", ctrl.getProjectStatus); // 프로젝트별 발주/입고 현황
|
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);
|
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/src/com/pms/mapper/purchaseOrder.xml:3295-3589
|
||||||
// 화면: wace_plm/.../purchaseOrder/purchaseOrderList_new.jsp (1085 lines)
|
// 화면: wace_plm/.../purchaseOrder/purchaseOrderList_new.jsp (1085 lines)
|
||||||
// 라우트: /api/purchase-wace/order-list (기존 /purchase/order page.tsx 보존 + 새 라우트)
|
// 라우트: GET /api/purchase/order-list
|
||||||
//
|
//
|
||||||
// 컬럼: 품의서No · 발주서No · 프로젝트번호 · 구매유형 · 주문유형 · 제품구분 ·
|
// 컬럼: 품의서No · 발주서No · 프로젝트번호 · 구매유형 · 주문유형 · 제품구분 ·
|
||||||
// 품번 · 품명 · 공급업체 · 환종 · 총액 · 메일발송 · 발주일 · 구매담당자 · 작성일
|
// 품번 · 품명 · 공급업체 · 환종 · 총액 · 메일발송 · 발주일 · 구매담당자 · 작성일
|
||||||
// 검색: 년도/고객사/프로젝트(CSV)/발주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 pool = getPool();
|
||||||
const { limit, offset, page, pageSize } = clampPaging(filter);
|
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 };
|
return { rows: d.rows, totalCount: c.rows[0]?.cnt ?? 0, page, pageSize };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error("listPurchaseOrderWace 실패", { error: e.message });
|
logger.error("listPurchaseOrderList 실패", { error: e.message });
|
||||||
return { rows: [], totalCount: 0, page, pageSize };
|
return { rows: [], totalCount: 0, page, pageSize };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
// 구매관리 > 발주서관리 — wace_plm purchaseOrder/purchaseOrderList_new.jsp 1:1 이식
|
|
||||||
// 매퍼: wace_plm/src/com/pms/mapper/purchaseOrder.xml purchaseOrderMasterList_new
|
|
||||||
// 검색: 년도/고객사/프로젝트번호/발주No/공급업체/품번/품명 (1행) +
|
|
||||||
// 입고요청일/발주일/주문유형/제품구분/구매유형/구매담당자/메일발송 (2행)
|
|
||||||
// 그리드: 품의서No / 발주서No / 프로젝트번호 / 구매유형 / 주문유형 / 제품구분 /
|
|
||||||
// 품번 / 품명 / 공급업체 / 환종 / 총액 / 메일발송 / 발주일 / 구매담당자 / 작성일
|
|
||||||
// 액션: 조회만 (이번 세션 — 등록/수정/메일/인쇄는 후속)
|
|
||||||
// 기존 /purchase/order(page.tsx)는 보존, 신규 라우트로 분리
|
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
|
|
||||||
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
|
|
||||||
import { CustomerSelect } from "@/components/common/CustomerSelect";
|
|
||||||
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
|
|
||||||
import { PageHeader } from "@/components/common/PageHeader";
|
|
||||||
import { purchaseApi, PurchaseListFilter, OptionItem, getYearOptions } from "@/lib/api/purchase";
|
|
||||||
import { apiClient } from "@/lib/api/client";
|
|
||||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
|
||||||
|
|
||||||
const MAIL_SEND_OPTS: SmartSelectOption[] = [
|
|
||||||
{ code: "Y", label: "발송완료" },
|
|
||||||
{ code: "N", label: "미발송" },
|
|
||||||
{ code: "orderCancel", label: "발주취소" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const EMPTY_FILTER: PurchaseListFilter = {
|
|
||||||
year: String(new Date().getFullYear()),
|
|
||||||
customer_cd: "", project_no: "", purchase_order_no: "",
|
|
||||||
partner_objid: "", part_no: "", part_name: "",
|
|
||||||
delivery_start_date: "", delivery_end_date: "",
|
|
||||||
reg_start_date: "", reg_end_date: "",
|
|
||||||
category_cd: "", product_cd: "", purchase_type: "", writer: "",
|
|
||||||
mail_send_yn: "",
|
|
||||||
page: 1, page_size: 50,
|
|
||||||
};
|
|
||||||
|
|
||||||
// wace 코드그룹 ID (PurchaseOrderController.matermgmtList 기준)
|
|
||||||
const CODE_GROUP = {
|
|
||||||
CATEGORY: "0000167", // 주문유형
|
|
||||||
PRODUCT: "0000001", // 제품구분
|
|
||||||
PURCHASE: "0001814", // 구매유형
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
async function loadCodes(groupId: string): Promise<OptionItem[]> {
|
|
||||||
try {
|
|
||||||
const r = await apiClient.get(`/sales/codes/${groupId}`);
|
|
||||||
return (r.data?.data ?? []) as OptionItem[];
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PurchaseOrderWacePage() {
|
|
||||||
const [rows, setRows] = useState<any[]>([]);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [filter, setFilter] = useState<PurchaseListFilter>(EMPTY_FILTER);
|
|
||||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const [vendorOpts, setVendorOpts] = useState<OptionItem[]>([]);
|
|
||||||
const [userOpts, setUserOpts] = useState<OptionItem[]>([]);
|
|
||||||
const [categoryOpts, setCategoryOpts] = useState<OptionItem[]>([]);
|
|
||||||
const [productOpts, setProductOpts] = useState<OptionItem[]>([]);
|
|
||||||
const [purchaseOpts, setPurchaseOpts] = useState<OptionItem[]>([]);
|
|
||||||
|
|
||||||
const yearOpts = useMemo(() => getYearOptions(), []);
|
|
||||||
|
|
||||||
const fetchList = useCallback(async (override?: Partial<PurchaseListFilter>) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const f = { ...filter, ...override };
|
|
||||||
const res = await purchaseApi.listOrderWace(f);
|
|
||||||
setRows(res.rows ?? []);
|
|
||||||
setTotal(res.totalCount ?? 0);
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [filter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let dead = false;
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const [v, u, cat, prod, pur] = await Promise.all([
|
|
||||||
purchaseApi.listVendors(),
|
|
||||||
purchaseApi.listUsers(),
|
|
||||||
loadCodes(CODE_GROUP.CATEGORY),
|
|
||||||
loadCodes(CODE_GROUP.PRODUCT),
|
|
||||||
loadCodes(CODE_GROUP.PURCHASE),
|
|
||||||
]);
|
|
||||||
if (dead) return;
|
|
||||||
setVendorOpts(v);
|
|
||||||
setUserOpts(u);
|
|
||||||
setCategoryOpts(cat);
|
|
||||||
setProductOpts(prod);
|
|
||||||
setPurchaseOpts(pur);
|
|
||||||
} catch { /* skip */ }
|
|
||||||
})();
|
|
||||||
fetchList(EMPTY_FILTER);
|
|
||||||
return () => { dead = true; };
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const gridRows = useMemo(
|
|
||||||
() => rows.map((r, i) => ({ ...r, id: r.objid ?? `r_${i}` })),
|
|
||||||
[rows],
|
|
||||||
);
|
|
||||||
|
|
||||||
const GRID_COLUMNS: DataGridColumn[] = useMemo(() => ([
|
|
||||||
{ key: "proposal_no", label: "품의서 No", width: "w-[125px]", align: "center" },
|
|
||||||
{ key: "purchase_order_no", label: "발주서 No", width: "w-[125px]", align: "center" },
|
|
||||||
{ key: "project_no", label: "프로젝트번호", width: "w-[140px]", align: "center" },
|
|
||||||
{ key: "purchase_type_name", label: "구매유형", width: "w-[110px]", align: "center" },
|
|
||||||
{ key: "category_name", label: "주문유형", width: "w-[110px]", align: "center" },
|
|
||||||
{ key: "product_name", label: "제품구분", width: "w-[110px]", align: "center" },
|
|
||||||
{ key: "part_no", label: "품번", width: "w-[170px]" },
|
|
||||||
{ key: "part_name", label: "품명", minWidth: "min-w-[280px]" },
|
|
||||||
{ key: "partner_name", label: "공급업체", minWidth: "min-w-[170px]" },
|
|
||||||
{ key: "currency_name", label: "환종", width: "w-[80px]", align: "center" },
|
|
||||||
{ key: "total_supply_price", label: "총액", width: "w-[140px]", align: "right", formatMoney: true },
|
|
||||||
{ key: "mail_send_yn", label: "메일발송", width: "w-[100px]", align: "center" },
|
|
||||||
{ key: "mail_send_date", label: "발주일", width: "w-[115px]", align: "center" },
|
|
||||||
{ key: "writer_name", label: "구매담당자", width: "w-[110px]", align: "center" },
|
|
||||||
{ key: "regdate", label: "작성일", width: "w-[115px]", align: "center" },
|
|
||||||
]), []);
|
|
||||||
|
|
||||||
const summary = useMemo(() => [
|
|
||||||
{ label: "전체 건수", value: total.toLocaleString(), suffix: "건" },
|
|
||||||
{ label: "선택", value: checkedIds.length.toLocaleString(), suffix: "건" },
|
|
||||||
], [total, checkedIds]);
|
|
||||||
|
|
||||||
const handleSearch = () => { setFilter(f => ({ ...f, page: 1 })); fetchList({ page: 1 }); };
|
|
||||||
const handleReset = () => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
|
||||||
<PageHeader loading={loading} onSearch={handleSearch} onReset={handleReset} />
|
|
||||||
|
|
||||||
<CompactFilterBar totalText={<>총 {total.toLocaleString()}건</>}>
|
|
||||||
<CompactFilterField label="년도" width={100}>
|
|
||||||
<SmartSelect options={yearOpts} value={filter.year ?? ""}
|
|
||||||
onValueChange={(v) => setFilter({ ...filter, year: v })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="고객사" width={170}>
|
|
||||||
<CustomerSelect value={filter.customer_cd ?? ""}
|
|
||||||
onValueChange={(v) => setFilter({ ...filter, customer_cd: v })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="프로젝트번호" width={160}>
|
|
||||||
<Input value={filter.project_no ?? ""}
|
|
||||||
onChange={(e) => setFilter({ ...filter, project_no: e.target.value })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="발주No" width={140}>
|
|
||||||
<Input value={filter.purchase_order_no ?? ""}
|
|
||||||
onChange={(e) => setFilter({ ...filter, purchase_order_no: e.target.value })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="공급업체" width={180}>
|
|
||||||
<SmartSelect options={vendorOpts} value={filter.partner_objid ?? ""}
|
|
||||||
onValueChange={(v) => setFilter({ ...filter, partner_objid: v })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="품번" width={140}>
|
|
||||||
<Input value={filter.part_no ?? ""}
|
|
||||||
onChange={(e) => setFilter({ ...filter, part_no: e.target.value })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="품명" width={150}>
|
|
||||||
<Input value={filter.part_name ?? ""}
|
|
||||||
onChange={(e) => setFilter({ ...filter, part_name: e.target.value })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="입고요청일" width={280}>
|
|
||||||
<CompactDateRange
|
|
||||||
from={filter.delivery_start_date ?? ""} setFrom={(v) => setFilter({ ...filter, delivery_start_date: v })}
|
|
||||||
to={filter.delivery_end_date ?? ""} setTo={(v) => setFilter({ ...filter, delivery_end_date: v })}
|
|
||||||
/>
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="발주일" width={280}>
|
|
||||||
<CompactDateRange
|
|
||||||
from={filter.reg_start_date ?? ""} setFrom={(v) => setFilter({ ...filter, reg_start_date: v })}
|
|
||||||
to={filter.reg_end_date ?? ""} setTo={(v) => setFilter({ ...filter, reg_end_date: v })}
|
|
||||||
/>
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="주문유형" width={130}>
|
|
||||||
<SmartSelect options={categoryOpts} value={filter.category_cd ?? ""}
|
|
||||||
onValueChange={(v) => setFilter({ ...filter, category_cd: v })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="제품구분" width={130}>
|
|
||||||
<SmartSelect options={productOpts} value={filter.product_cd ?? ""}
|
|
||||||
onValueChange={(v) => setFilter({ ...filter, product_cd: v })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="구매유형" width={130}>
|
|
||||||
<SmartSelect options={purchaseOpts} value={filter.purchase_type ?? ""}
|
|
||||||
onValueChange={(v) => setFilter({ ...filter, purchase_type: v })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="구매담당자" width={150}>
|
|
||||||
<SmartSelect options={userOpts} value={filter.writer ?? ""}
|
|
||||||
onValueChange={(v) => setFilter({ ...filter, writer: v })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
<CompactFilterField label="메일발송" width={130}>
|
|
||||||
<SmartSelect options={MAIL_SEND_OPTS} value={filter.mail_send_yn ?? ""}
|
|
||||||
onValueChange={(v) => setFilter({ ...filter, mail_send_yn: v })} />
|
|
||||||
</CompactFilterField>
|
|
||||||
</CompactFilterBar>
|
|
||||||
|
|
||||||
<DataGrid
|
|
||||||
columns={GRID_COLUMNS}
|
|
||||||
data={gridRows}
|
|
||||||
loading={loading}
|
|
||||||
showCheckbox
|
|
||||||
checkedIds={checkedIds}
|
|
||||||
onCheckedChange={setCheckedIds}
|
|
||||||
emptyMessage={loading ? "조회 중..." : "데이터가 없습니다"}
|
|
||||||
gridId="purchase-order-wace"
|
|
||||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
|
||||||
paginationStyle="range"
|
|
||||||
serverPaging
|
|
||||||
serverPage={filter.page ?? 1}
|
|
||||||
serverPageSize={filter.page_size ?? 50}
|
|
||||||
serverTotalItems={total}
|
|
||||||
onPageChange={(p) => { setFilter(f => ({ ...f, page: p })); fetchList({ page: p }); }}
|
|
||||||
onPageSizeChange={(n) => { setFilter(f => ({ ...f, page: 1, page_size: n })); fetchList({ page: 1, page_size: n }); }}
|
|
||||||
showColumnSettings
|
|
||||||
summaryStats={summary}
|
|
||||||
onRefresh={() => fetchList()}
|
|
||||||
onDownload={() => {
|
|
||||||
if (gridRows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
|
||||||
const exportRows = gridRows.map((r: any) => {
|
|
||||||
const out: Record<string, any> = {};
|
|
||||||
GRID_COLUMNS.forEach((c) => { out[c.label] = r[c.key] ?? ""; });
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
exportToExcel(exportRows, "발주서관리.xlsx", "발주서");
|
|
||||||
}}
|
|
||||||
showChart
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PageHeader — 페이지 상단 메뉴명 + 설명 + 액션 슬롯.
|
* PageHeader — 페이지 상단 "대메뉴_중메뉴" 제목 + 액션/검색 슬롯.
|
||||||
*
|
*
|
||||||
* customer-cs/cs 페이지 패턴 1:1 추출. 모든 RPS 메뉴 페이지의 상단에 의무 배치.
|
* 모든 RPS 메뉴 페이지의 상단에 의무 배치.
|
||||||
*
|
*
|
||||||
* 자동 매칭 (탭 시스템 대응):
|
* 자동 매칭 (탭 시스템 대응):
|
||||||
* - RPS 는 탭 기반이라 usePathname() 이 /main 으로 고정됨.
|
* - RPS 는 탭 기반이라 usePathname() 이 /main 으로 고정됨.
|
||||||
* - useTabStore 의 활성 탭 adminUrl → /COMPANY_NN prefix 제거 → menu_info.menu_url 매칭.
|
* - useTabStore 의 활성 탭 adminUrl → /COMPANY_NN prefix 제거 → menu_info.menu_url 매칭.
|
||||||
* - useCurrent2ndLevelMenuObjid 와 동일 패턴.
|
* - 매칭된 menu 의 parent_obj_id 로 부모 메뉴를 찾아 "{부모}_{자식}" 으로 표기 (wace 컨벤션).
|
||||||
|
* - 루트 그룹(parent_obj_id 가 0뎁스)이면 자식만 단독 표기.
|
||||||
*
|
*
|
||||||
* 명시 지정:
|
* 명시 지정:
|
||||||
* <PageHeader title="M-BOM 관리" description="생산용 BOM 트리" actions={...} />
|
* <PageHeader title="M-BOM 관리" actions={...} />
|
||||||
*
|
*
|
||||||
* 원칙:
|
* 원칙: menu_info 에 등록만 되어 있으면 props 없이도 자동 매칭.
|
||||||
* - 모든 page.tsx 의 최상위 자식으로 <PageHeader /> 를 배치한다.
|
|
||||||
* - menu_info 에 등록만 되어 있으면 props 없이도 자동 매칭.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -29,7 +28,6 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
interface PageHeaderProps {
|
interface PageHeaderProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
|
||||||
/** 업무 액션 슬롯 (등록/삭제/상신 등). 검색·초기화는 onSearch/onReset 로 전달. */
|
/** 업무 액션 슬롯 (등록/삭제/상신 등). 검색·초기화는 onSearch/onReset 로 전달. */
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
/** 검색 핸들러. 지정 시 우측에 검색 버튼 자동 렌더. */
|
/** 검색 핸들러. 지정 시 우측에 검색 버튼 자동 렌더. */
|
||||||
@@ -47,6 +45,17 @@ function stripCompanyPrefix(p: string): string {
|
|||||||
return p.replace(/^\/COMPANY_\d+/, "") || "/";
|
return p.replace(/^\/COMPANY_\d+/, "") || "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findParentMenu(menus: MenuItem[], menu: MenuItem | null): MenuItem | null {
|
||||||
|
if (!menu) return null;
|
||||||
|
const pid = menu.parent_obj_id ?? menu.PARENT_OBJ_ID;
|
||||||
|
if (!pid) return null;
|
||||||
|
for (const m of menus) {
|
||||||
|
const oid = m.objid ?? m.OBJID;
|
||||||
|
if (oid && String(oid) === String(pid)) return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function findByUrl(menus: MenuItem[], strippedUrl: string): MenuItem | null {
|
function findByUrl(menus: MenuItem[], strippedUrl: string): MenuItem | null {
|
||||||
// menu_info.menu_url 이 /COMPANY_16/... 으로 저장되어 있으므로 양쪽 비교
|
// menu_info.menu_url 이 /COMPANY_16/... 으로 저장되어 있으므로 양쪽 비교
|
||||||
for (const m of menus) {
|
for (const m of menus) {
|
||||||
@@ -68,7 +77,7 @@ function findByUrl(menus: MenuItem[], strippedUrl: string): MenuItem | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PageHeader({
|
export function PageHeader({
|
||||||
title, description, actions, onSearch, onReset, loading,
|
title, actions, onSearch, onReset, loading,
|
||||||
searchLabel = "검색", resetLabel = "초기화", className,
|
searchLabel = "검색", resetLabel = "초기화", className,
|
||||||
}: PageHeaderProps) {
|
}: PageHeaderProps) {
|
||||||
const pathname = usePathname() ?? "";
|
const pathname = usePathname() ?? "";
|
||||||
@@ -76,6 +85,7 @@ export function PageHeader({
|
|||||||
const activeTabId = useTabStore(selectActiveTabId);
|
const activeTabId = useTabStore(selectActiveTabId);
|
||||||
|
|
||||||
let menu: MenuItem | null = null;
|
let menu: MenuItem | null = null;
|
||||||
|
let parentMenu: MenuItem | null = null;
|
||||||
try {
|
try {
|
||||||
const { userMenus, adminMenus } = useMenu();
|
const { userMenus, adminMenus } = useMenu();
|
||||||
// RPS 탭 시스템: pathname=/main 이면 활성 탭의 adminUrl 사용
|
// RPS 탭 시스템: pathname=/main 이면 활성 탭의 adminUrl 사용
|
||||||
@@ -87,25 +97,31 @@ export function PageHeader({
|
|||||||
targetUrl = stripCompanyPrefix(activeTab.adminUrl);
|
targetUrl = stripCompanyPrefix(activeTab.adminUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const allMenus = [...(userMenus as MenuItem[]), ...(adminMenus as MenuItem[])];
|
||||||
menu = findByUrl(userMenus as MenuItem[], targetUrl) ?? findByUrl(adminMenus as MenuItem[], targetUrl);
|
menu = findByUrl(userMenus as MenuItem[], targetUrl) ?? findByUrl(adminMenus as MenuItem[], targetUrl);
|
||||||
|
parentMenu = findParentMenu(allMenus, menu);
|
||||||
} catch {
|
} catch {
|
||||||
/* Provider 밖 — 자동 매칭 생략 */
|
/* Provider 밖 — 자동 매칭 생략 */
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedTitle = title ?? menu?.menu_name_kor ?? "";
|
// wace 컨벤션: "대메뉴_중메뉴" (parent_obj_id 가 루트 그룹이면 단독 표기)
|
||||||
const resolvedDesc = description ?? menu?.menu_desc ?? "";
|
const parentName = parentMenu?.menu_name_kor ?? parentMenu?.MENU_NAME_KOR ?? "";
|
||||||
|
const ownName = menu?.menu_name_kor ?? menu?.MENU_NAME_KOR ?? "";
|
||||||
|
const parentParentPid = parentMenu?.parent_obj_id ?? parentMenu?.PARENT_OBJ_ID;
|
||||||
|
// 부모의 부모가 있어야 (즉, 부모가 1뎁스 그룹) "부모_자식" 표기. 부모 없거나 부모가 루트이면 자식만.
|
||||||
|
const autoTitle = parentName && parentParentPid && ownName
|
||||||
|
? `${parentName}_${ownName}`
|
||||||
|
: ownName;
|
||||||
|
const resolvedTitle = title ?? autoTitle;
|
||||||
|
|
||||||
const hasSearchButtons = !!(onSearch || onReset);
|
const hasSearchButtons = !!(onSearch || onReset);
|
||||||
if (!resolvedTitle && !resolvedDesc && !actions && !hasSearchButtons) return null;
|
if (!resolvedTitle && !actions && !hasSearchButtons) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-shrink-0 items-end justify-between gap-3 border-b pb-3", className)}>
|
<div className={cn("flex flex-shrink-0 items-center justify-between gap-3 border-b pb-2", className)}>
|
||||||
<div>
|
<div>
|
||||||
{resolvedTitle && (
|
{resolvedTitle && (
|
||||||
<h1 className="text-xl font-bold tracking-tight">{resolvedTitle}</h1>
|
<h1 className="text-lg font-bold tracking-tight">{resolvedTitle}</h1>
|
||||||
)}
|
|
||||||
{resolvedDesc && (
|
|
||||||
<p className="text-xs text-muted-foreground">{resolvedDesc}</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(actions || hasSearchButtons) && (
|
{(actions || hasSearchButtons) && (
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const purchaseApi = {
|
|||||||
listInboundByItem: (f: PurchaseListFilter = {}) => getList("inbound-by-item", f),
|
listInboundByItem: (f: PurchaseListFilter = {}) => getList("inbound-by-item", f),
|
||||||
listInboundByDate: (f: PurchaseListFilter = {}) => getList("inbound-by-date", f),
|
listInboundByDate: (f: PurchaseListFilter = {}) => getList("inbound-by-date", f),
|
||||||
listProjectStatus: (f: PurchaseListFilter = {}) => getList("project-status", f),
|
listProjectStatus: (f: PurchaseListFilter = {}) => getList("project-status", f),
|
||||||
listOrderWace: (f: PurchaseListFilter = {}) => getList("order-wace", f),
|
listOrder: (f: PurchaseListFilter = {}) => getList("order-list", f),
|
||||||
|
|
||||||
// 공통 옵션
|
// 공통 옵션
|
||||||
async listSuppliers(): Promise<OptionItem[]> {
|
async listSuppliers(): Promise<OptionItem[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user