영업관리 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:
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user