Files
wace_rps/backend-node/src/services/purchaseService.ts
T
hjjeong b38f5957f2 구매관리 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>
2026-05-14 17:31:12 +09:00

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 [];
}
}