영업관리 4개 메뉴 검색폼 wace 일치 + 공통 UX(초기화·date input) 정비

- 검색 폼 정합성: wace JSP `<!-- 주석처리된 검색필터 -->` 블록까지 잘못 이식했던 부분 정정
  - 견적: 11→7개 (제품구분/국내해외/유무상/요청납기 제거)
  - 주문: 13→9개 (제품구분/국내해외/유무상/견적환종 제거)
  - 매출: 10→11개 (출하지시상태 제거 + 제품구분·국내/해외 추가, JSP 순서로 재배치)
  - 판매: 변경 없음 (원본 그대로 일치)
- 매출 백엔드: SaleListFilter에 productType/nation 추가, getRevenueList에 partObjId/serialNo/orderDate/productType/nation 5개 필터 처리
- 공통 UX
  - 초기화 버튼을 4개 메뉴 동일하게 통일 (variant=ghost, 버튼 영역 끝)
  - <Input type="date">는 빈 값 placeholder 숨김 + 캘린더 아이콘 숨김 + 영역 클릭으로 picker 자동(showPicker)
- 신규 공통 컴포넌트: CommCodeSelect/CustomerSelect/CustomerSearchDialog/PartSelect/ItemSearchDialog + backend salesCommonRoutes
- 문서: 01/02/04 검색 폼 표를 활성/비활성 분리 형식으로 정정, README에 8. 공통 UX 규칙 섹션 신설

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-08 10:42:16 +09:00
parent 4c3ea194a0
commit 489fa50d11
20 changed files with 1317 additions and 146 deletions
+2
View File
@@ -174,6 +174,7 @@ import quoteRoutes from "./routes/quoteRoutes"; // 견적관리 (vexplor 자체
import salesEstimateRoutes from "./routes/salesEstimateRoutes"; // 영업관리>견적 (wace_plm 도메인 이식)
import salesOrderMgmtRoutes from "./routes/salesOrderMgmtRoutes"; // 영업관리>주문서 (wace_plm 도메인 이식)
import salesSaleRoutes from "./routes/salesSaleRoutes"; // 영업관리>판매+매출 (wace_plm 도메인)
import salesCommonRoutes from "./routes/salesCommonRoutes"; // 영업관리 4개 메뉴 공통 옵션 (codes/parts/customers)
import erpSyncRoutes from "./routes/erpSyncRoutes"; // ERP 마스터 동기화 (사원/부서/창고/거래처/계정과목)
import { BatchSchedulerService } from "./services/batchSchedulerService";
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
@@ -407,6 +408,7 @@ app.use("/api/quotes", quoteRoutes); // 견적관리 (vexplor 자체)
app.use("/api/sales/estimate", salesEstimateRoutes); // 영업관리>견적 (wace_plm 도메인)
app.use("/api/sales/order-mgmt", salesOrderMgmtRoutes); // 영업관리>주문서 (wace_plm 도메인)
app.use("/api/sales", salesSaleRoutes); // 영업관리>판매+매출 (wace_plm 도메인)
app.use("/api/sales", salesCommonRoutes); // 영업관리 공통 옵션 (codes/parts/customers)
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
@@ -0,0 +1,72 @@
import { Router, Request, Response } from "express";
import { authenticateToken } from "../middleware/authMiddleware";
import { getPool } from "../database/db";
import { logger } from "../utils/logger";
const router = Router();
router.use(authenticateToken);
/**
* 영업관리 4개 메뉴(견적/주문/판매/매출) 공통 옵션 endpoint.
* - 공통코드(comm_code)는 wace_plm parent_code_id 그룹 키로 조회
* - 품목(item_info)은 wace 마이그레이션 데이터(company_code 비어있음) +
* 현재 운영 회사 데이터 모두 노출
*/
/** GET /api/sales/codes/:groupId → [{code, label, sort}] */
router.get("/codes/:groupId", async (req: Request, res: Response) => {
try {
const { groupId } = req.params;
const pool = getPool();
const result = await pool.query(
`SELECT code_id AS code, code_name AS label, sort_order AS sort
FROM comm_code
WHERE parent_code_id = $1
AND status = 'active'
ORDER BY sort_order, code_name`,
[groupId],
);
res.json({ success: true, data: result.rows });
} catch (err) {
logger.error("salesCommon /codes error", err);
res.status(500).json({ success: false, message: (err as Error).message });
}
});
/** GET /api/sales/parts → [{id, item_number, item_name}] (영업관리 풀-옵션용) */
router.get("/parts", async (_req: Request, res: Response) => {
try {
const pool = getPool();
// wace 이식 데이터(company_code 빈 값/별표) 우선, COMPANY_16 데이터 추가
const result = await pool.query(
`SELECT id, item_number, item_name
FROM item_info
WHERE COALESCE(company_code, '') IN ('', '*', 'COMPANY_16')
AND (item_number IS NOT NULL OR item_name IS NOT NULL)
ORDER BY item_number NULLS LAST, item_name NULLS LAST`,
);
res.json({ success: true, data: result.rows });
} catch (err) {
logger.error("salesCommon /parts error", err);
res.status(500).json({ success: false, message: (err as Error).message });
}
});
/** GET /api/sales/customers → [{id, customer_name, customer_code}] */
router.get("/customers", async (_req: Request, res: Response) => {
try {
const pool = getPool();
const result = await pool.query(
`SELECT id, customer_name, customer_code
FROM customer_mng
WHERE customer_name IS NOT NULL AND customer_name <> ''
ORDER BY customer_name`,
);
res.json({ success: true, data: result.rows });
} catch (err) {
logger.error("salesCommon /customers error", err);
res.status(500).json({ success: false, message: (err as Error).message });
}
});
export default router;
@@ -6,6 +6,7 @@
import { getPool } from "../database/db";
import { logger } from "../utils/logger";
import { generateContractNo } from "./salesOrderMgmtService";
// ─── 타입 ─────────────────────────────────────────────────────
@@ -13,6 +14,7 @@ export interface EstimateListFilter {
category_cd?: string;
customer_objid?: string;
search_partObjId?: string;
search_partNo?: string;
search_partName?: string;
search_serialNo?: string;
appr_status?: string;
@@ -131,6 +133,24 @@ export async function getList(filter: EstimateListFilter) {
)`);
params.push(filter.search_partObjId);
}
if (filter.search_partNo) {
conditions.push(`EXISTS (
SELECT 1 FROM contract_item CI
WHERE CI.contract_objid = T.OBJID
AND CI.status = 'ACTIVE'
AND UPPER(CI.part_no) LIKE UPPER($${idx++})
)`);
params.push(`%${filter.search_partNo}%`);
}
if (filter.search_partName) {
conditions.push(`EXISTS (
SELECT 1 FROM contract_item CI
WHERE CI.contract_objid = T.OBJID
AND CI.status = 'ACTIVE'
AND UPPER(CI.part_name) LIKE UPPER($${idx++})
)`);
params.push(`%${filter.search_partName}%`);
}
if (filter.search_serialNo) {
conditions.push(`EXISTS (
SELECT 1 FROM contract_item CI
@@ -418,6 +438,7 @@ export async function create(userId: string, body: EstimateTemplateBody) {
if (!contractObjid) {
contractObjid = genVarcharObjid("CM");
const ctx = body.contract_context ?? {};
const contractNo = ctx.contract_no || (await generateContractNo());
await client.query(
`INSERT INTO contract_mgmt (
objid, contract_no, customer_objid, category_cd, area_cd,
@@ -426,7 +447,7 @@ export async function create(userId: string, body: EstimateTemplateBody) {
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9, NOW())`,
[
contractObjid,
ctx.contract_no || null,
contractNo,
ctx.customer_objid || null,
ctx.category_cd || null,
ctx.area_cd || null,
@@ -21,6 +21,8 @@ export interface SaleListFilter {
shippingDateFrom?: string;
shippingDateTo?: string;
salesStatus?: string; // 판매상태 (registered/cancelled 등)
productType?: string; // project_mgmt.product (제품구분)
nation?: string; // project_mgmt.area_cd (국내/해외)
// 매출관리 전용
revenueMode?: string;
salesDeadlineFrom?: string;
@@ -220,6 +222,17 @@ export async function getRevenueList(filter: SaleListFilter) {
if (filter.orderType) { conditions.push(`T.category_cd = $${idx++}`); params.push(filter.orderType); }
if (filter.poNo) { conditions.push(`T.po_no ILIKE $${idx++}`); params.push(`%${filter.poNo}%`); }
if (filter.customer_objid) { conditions.push(`T.customer_objid = $${idx++}`); params.push(filter.customer_objid); }
if (filter.productType) { conditions.push(`T.product = $${idx++}`); params.push(filter.productType); }
if (filter.nation) { conditions.push(`T.area_cd = $${idx++}`); params.push(filter.nation); }
if (filter.search_partObjId) { conditions.push(`T.part_objid = $${idx++}`); params.push(filter.search_partObjId); }
if (filter.serialNo) {
conditions.push(`EXISTS (SELECT 1 FROM contract_item_serial CIS
WHERE CIS.item_objid = T.contract_item_objid AND CIS.status='ACTIVE'
AND UPPER(CIS.serial_no) LIKE UPPER($${idx++}))`);
params.push(`%${filter.serialNo}%`);
}
if (filter.orderDateFrom) { conditions.push(`COALESCE(T.contract_date, CM.order_date) >= $${idx++}`); params.push(filter.orderDateFrom); }
if (filter.orderDateTo) { conditions.push(`COALESCE(T.contract_date, CM.order_date) <= $${idx++}`); params.push(filter.orderDateTo); }
if (filter.salesDeadlineFrom) { conditions.push(`T.sales_deadline_date >= $${idx++}`); params.push(filter.salesDeadlineFrom); }
if (filter.salesDeadlineTo) { conditions.push(`T.sales_deadline_date <= $${idx++}`); params.push(filter.salesDeadlineTo); }
if (filter.shippingDateFrom) { conditions.push(`SR.shipping_date >= $${idx++}`); params.push(filter.shippingDateFrom); }