프로젝트관리>진행관리 메뉴 신설 — wace projectMgmtWbsList3 1:1 이식
· 그리드: 8그룹 18셀 평탄화 (운영판 projectMgmtWbsGridList SQL 1:1) · 검색폼: 11필드 (년도/프로젝트번호/주문유형/고객사/제품구분/요청납기/국내해외/유무상/품번/품명/S/N) · 영업관리 패턴 통일: PartSelect 단일 search_partObjId, customer_mng 단일 LEFT JOIN · client_mng/supply_mng 분기 → customer_mng로 흡수, CODE_NAME() 함수 직접 사용 · 행 클릭 동작은 P1.5 별도 결정 (영업관리 OrderRegistDialog 재사용 후보) · PMS_WBS_TASK/SETUP_WBS_TASK 의존 컬럼은 그리드 표시 컬럼에 없어 P2(WBS관리) 보류 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -175,6 +175,7 @@ import salesEstimateRoutes from "./routes/salesEstimateRoutes"; // 영업관리>
|
||||
import salesOrderMgmtRoutes from "./routes/salesOrderMgmtRoutes"; // 영업관리>주문서 (wace_plm 도메인 이식)
|
||||
import salesSaleRoutes from "./routes/salesSaleRoutes"; // 영업관리>판매+매출 (wace_plm 도메인)
|
||||
import salesCommonRoutes from "./routes/salesCommonRoutes"; // 영업관리 4개 메뉴 공통 옵션 (codes/parts/customers)
|
||||
import projectMgmtRoutes from "./routes/projectMgmtRoutes"; // 프로젝트관리>진행관리 (wace_plm 도메인 이식)
|
||||
import erpSyncRoutes from "./routes/erpSyncRoutes"; // ERP 마스터 동기화 (사원/부서/창고/거래처/계정과목)
|
||||
import ecrMngRoutes from "./routes/ecrMngRoutes"; // ECR(Engineering Change Request) 관리
|
||||
import customerCsRoutes from "./routes/customerCsRoutes"; // 고객 CS 관리
|
||||
@@ -419,6 +420,7 @@ app.use("/api/sales/estimate", salesEstimateRoutes); // 영업관리>견적 (wac
|
||||
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/project/progress", projectMgmtRoutes); // 프로젝트관리>진행관리 (wace_plm 도메인)
|
||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import * as svc from "../services/projectMgmtService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const filter = req.query as Record<string, string>;
|
||||
const data = await svc.listProgress(filter);
|
||||
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 getProjectNoOptions(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.listProjectNoOptions();
|
||||
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 getById(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = await svc.getById(id);
|
||||
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 update(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const rowCount = await svc.updateProject(id, req.body);
|
||||
if (rowCount === 0) return res.status(404).json({ success: false, message: "프로젝트를 찾을 수 없습니다." });
|
||||
return res.json({ success: true, message: "프로젝트가 수정되었습니다." });
|
||||
} catch (e: any) {
|
||||
logger.error("진행관리 수정 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import * as ctrl from "../controllers/projectMgmtController";
|
||||
|
||||
const router = Router();
|
||||
router.use(authenticateToken);
|
||||
|
||||
router.get("/list", ctrl.getList);
|
||||
router.get("/project-no-options", ctrl.getProjectNoOptions);
|
||||
router.get("/:id", ctrl.getById);
|
||||
router.put("/:id", ctrl.update);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,331 @@
|
||||
// ============================================================
|
||||
// 프로젝트관리 > 진행관리 (wace_plm 도메인 이식)
|
||||
// 메인 화면: wace `/project/projectMgmtWbsList3.do` (= 운영판 RPS 진행관리)
|
||||
// 그리드 SQL: wace_plm/src/com/pms/mapper/project.xml:3854 projectMgmtWbsGridList
|
||||
// JSP 원본: wace_plm/WebContent/WEB-INF/view/project/projectMgmtWbsList3.jsp
|
||||
//
|
||||
// 1:1 이식 + RPS 매핑 변경:
|
||||
// · wace의 CLIENT_MNG/SUPPLY_MNG CASE WHEN LIKE 'C_%' 분기 → RPS는 customer_mng 단일 LEFT JOIN
|
||||
// · CODE_NAME() 함수는 RPS DB 보유 — 그대로 사용
|
||||
// · PMS_WBS_TASK/SETUP_WBS_TASK 의존 컬럼은 그리드 표시 컬럼에 없으므로 미반영 (P2)
|
||||
// ============================================================
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export interface ProgressListFilter {
|
||||
Year?: string; // 년도 (TO_CHAR(REGDATE,'YYYY'))
|
||||
project_nos?: string; // wace project_nos (쉼표 직렬화 OBJID — 다중 또는 단일)
|
||||
category_cd?: string;
|
||||
customer_objid?: string;
|
||||
product?: string;
|
||||
status_cd?: string;
|
||||
result_cd?: string;
|
||||
contract_start_date?: string; // 요청납기 시작
|
||||
contract_end_date?: string; // 요청납기 끝
|
||||
area_cd?: string; // '국내'/'해외' 라벨로 비교 (wace 1:1)
|
||||
free_of_charge?: string; // '유상'/'무상' 라벨로 비교
|
||||
// 영업관리 패턴(search_partObjId 단일) 통일 — wace의 product_item_code/_name LIKE 두 필드는 부재화
|
||||
search_partObjId?: string; // part_mng.objid::varchar = project_mgmt.part_objid
|
||||
serial_no?: string; // contract_item_serial EXISTS
|
||||
pm_user_id?: string; // (검색폼 주석처리되어 있으나 SQL은 받음)
|
||||
location?: string;
|
||||
setup?: string;
|
||||
}
|
||||
|
||||
// ─── 목록: 운영판 projectMgmtWbsGridList 1:1 ──────────────────
|
||||
|
||||
export async function listProgress(filter: ProgressListFilter) {
|
||||
const pool = getPool();
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
// wace L4187: Year — TO_CHAR(REGDATE,'YYYY')
|
||||
if (filter.Year) {
|
||||
conditions.push(`TO_CHAR(T.REGDATE,'YYYY') = $${idx++}`);
|
||||
params.push(filter.Year);
|
||||
}
|
||||
|
||||
// wace L4195: project_nos 쉼표 split → OBJID IN
|
||||
if (filter.project_nos) {
|
||||
const list = filter.project_nos.split(",").map(s => s.trim()).filter(Boolean);
|
||||
if (list.length > 0) {
|
||||
const placeholders = list.map(() => `$${idx++}`).join(",");
|
||||
conditions.push(`T.OBJID IN (${placeholders})`);
|
||||
params.push(...list);
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.category_cd) { conditions.push(`T.CATEGORY_CD = $${idx++}`); params.push(filter.category_cd); }
|
||||
if (filter.customer_objid) { conditions.push(`T.CUSTOMER_OBJID = $${idx++}`); params.push(filter.customer_objid); }
|
||||
if (filter.product) { conditions.push(`T.PRODUCT = $${idx++}`); params.push(filter.product); }
|
||||
if (filter.status_cd) { conditions.push(`T.STATUS_CD = $${idx++}`); params.push(filter.status_cd); }
|
||||
if (filter.result_cd) { conditions.push(`T.RESULT_CD = $${idx++}`); params.push(filter.result_cd); }
|
||||
|
||||
// wace L4224-4237: 요청납기 범위 — COALESCE(contract_item.due_date, project_mgmt.due_date, contract_mgmt.due_date)
|
||||
const dueDateCoalesce = `COALESCE(
|
||||
(SELECT CI.DUE_DATE FROM contract_item CI WHERE CI.contract_objid = T.CONTRACT_OBJID AND CI.part_objid = T.PART_OBJID AND CI.status = 'ACTIVE' ORDER BY CI.objid DESC LIMIT 1),
|
||||
T.DUE_DATE,
|
||||
(SELECT CM.due_date FROM contract_mgmt CM WHERE CM.OBJID = T.CONTRACT_OBJID LIMIT 1)
|
||||
)`;
|
||||
if (filter.contract_start_date) {
|
||||
conditions.push(`TO_DATE(${dueDateCoalesce},'YYYY-MM-DD') >= TO_DATE($${idx++},'YYYY-MM-DD')`);
|
||||
params.push(filter.contract_start_date);
|
||||
}
|
||||
if (filter.contract_end_date) {
|
||||
conditions.push(`TO_DATE(${dueDateCoalesce},'YYYY-MM-DD') <= TO_DATE($${idx++},'YYYY-MM-DD')`);
|
||||
params.push(filter.contract_end_date);
|
||||
}
|
||||
|
||||
if (filter.pm_user_id) { conditions.push(`T.PM_USER_ID = $${idx++}`); params.push(filter.pm_user_id); }
|
||||
if (filter.location) { conditions.push(`UPPER(T.LOCATION) LIKE UPPER($${idx++})`); params.push(`%${filter.location}%`); }
|
||||
if (filter.setup) { conditions.push(`UPPER(T.SETUP) LIKE UPPER($${idx++})`); params.push(`%${filter.setup}%`); }
|
||||
|
||||
// wace L4249: area_cd — CODE_NAME(area_cd) 라벨 비교 ('국내'/'해외')
|
||||
if (filter.area_cd) {
|
||||
conditions.push(`CODE_NAME(T.AREA_CD) = $${idx++}`);
|
||||
params.push(filter.area_cd);
|
||||
}
|
||||
|
||||
// wace L4253: free_of_charge — contract_mgmt.paid_type → '유상'/'무상' 매핑 비교
|
||||
if (filter.free_of_charge) {
|
||||
conditions.push(`(SELECT
|
||||
CASE
|
||||
WHEN O.PAID_TYPE = 'paid' THEN '유상'
|
||||
WHEN O.PAID_TYPE = 'free' THEN '무상'
|
||||
ELSE O.PAID_TYPE
|
||||
END
|
||||
FROM contract_mgmt AS O WHERE O.OBJID = T.CONTRACT_OBJID) = $${idx++}`);
|
||||
params.push(filter.free_of_charge);
|
||||
}
|
||||
|
||||
// 영업관리 패턴 통일: search_partObjId 단일 (PartSelect의 part_mng.objid 값 매칭)
|
||||
// wace 매퍼는 product_item_code/_name LIKE 텍스트 검색이지만 RPS는 part_objid 직접 매칭.
|
||||
if (filter.search_partObjId) {
|
||||
conditions.push(`T.PART_OBJID = $${idx++}`);
|
||||
params.push(filter.search_partObjId);
|
||||
}
|
||||
// wace L4271: serial_no — EXISTS contract_item_serial
|
||||
if (filter.serial_no) {
|
||||
conditions.push(`EXISTS (
|
||||
SELECT 1 FROM contract_item_serial CIS
|
||||
WHERE CIS.item_objid::varchar = T.CONTRACT_ITEM_OBJID
|
||||
AND UPPER(CIS.status) = 'ACTIVE'
|
||||
AND UPPER(CIS.serial_no) LIKE UPPER($${idx++})
|
||||
)`);
|
||||
params.push(`%${filter.serial_no}%`);
|
||||
}
|
||||
|
||||
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
// wace projectMgmtWbsGridList 활성 본문 1:1 (그리드 표시 18셀 + 필수 부속)
|
||||
const sql = `
|
||||
SELECT
|
||||
T.OBJID
|
||||
,T.CATEGORY_CD
|
||||
,CODE_NAME(T.CATEGORY_CD) AS CATEGORY_NAME
|
||||
,T.CUSTOMER_OBJID
|
||||
,C.customer_name AS CUSTOMER_NAME
|
||||
,T.PRODUCT
|
||||
,CODE_NAME(T.PRODUCT) AS PRODUCT_NAME
|
||||
,T.STATUS_CD
|
||||
,CODE_NAME(T.STATUS_CD) AS STATUS_NAME
|
||||
,T.RESULT_CD
|
||||
,CODE_NAME(T.RESULT_CD) AS RESULT_NAME
|
||||
,T.DUE_DATE
|
||||
,T.LOCATION
|
||||
,T.SETUP
|
||||
,T.FACILITY
|
||||
,CODE_NAME(T.FACILITY) AS FACILITY_NAME
|
||||
,T.FACILITY_QTY
|
||||
,T.FACILITY_TYPE
|
||||
,T.FACILITY_DEPTH
|
||||
,T.PROJECT_NO
|
||||
,T.PM_USER_ID
|
||||
,(SELECT user_name FROM user_info WHERE user_id = (SELECT pm_user_id FROM contract_mgmt WHERE OBJID = T.CONTRACT_OBJID)) AS PM_USER_NAME
|
||||
,T.CONTRACT_PRICE
|
||||
,T.CONTRACT_PRICE_CURRENCY
|
||||
,T.CONTRACT_CURRENCY
|
||||
,CODE_NAME(T.CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME
|
||||
,T.REGDATE
|
||||
,TO_CHAR(T.REGDATE,'YYYY-MM-DD') AS REG_DATE
|
||||
,T.WRITER
|
||||
,(SELECT user_name FROM user_info WHERE user_id = T.WRITER) AS WRITER_NAME
|
||||
,(SELECT COUNT(1) FROM attach_file_info WHERE target_objid = T.OBJID AND doc_type='contractMgmt01' AND UPPER(status)='ACTIVE') AS CU01_CNT
|
||||
,(SELECT COUNT(1) FROM attach_file_info WHERE target_objid = T.OBJID AND doc_type='contractMgmt02' AND UPPER(status)='ACTIVE') AS CU02_CNT
|
||||
,T.CONTRACT_NO
|
||||
,T.CUSTOMER_EQUIP_NAME
|
||||
,T.CONTRACT_DEL_DATE
|
||||
,T.CONTRACT_COMPANY
|
||||
,CODE_NAME(T.CONTRACT_COMPANY) AS CONTRACT_COMPANY_NAME
|
||||
,T.CONTRACT_DATE
|
||||
,T.PO_NO
|
||||
,T.MANUFACTURE_PLANT
|
||||
,CODE_NAME(T.MANUFACTURE_PLANT) AS MANUFACTURE_PLANT_NAME
|
||||
,T.CONTRACT_RESULT
|
||||
,CODE_NAME(T.CONTRACT_RESULT) AS CONTRACT_RESULT_NAME
|
||||
,CODE_NAME(T.AREA_CD) AS AREA_NAME
|
||||
,T.PROJECT_NAME
|
||||
|
||||
-- 운영판 그리드 핵심 컬럼들
|
||||
,(SELECT CASE
|
||||
WHEN O.PAID_TYPE = 'paid' THEN '유상'
|
||||
WHEN O.PAID_TYPE = 'free' THEN '무상'
|
||||
ELSE O.PAID_TYPE
|
||||
END
|
||||
FROM contract_mgmt AS O WHERE O.OBJID = T.CONTRACT_OBJID) AS FREE_OF_CHARGE
|
||||
,COALESCE(NULLIF(T.QUANTITY,'')::numeric, 0) AS CONTRACT_QTY
|
||||
,T.PART_NO AS PRODUCT_ITEM_CODE
|
||||
,T.PART_NAME AS PRODUCT_ITEM_NAME
|
||||
-- S/N: contract_item_serial (CIS.item_objid = T.contract_item_objid)
|
||||
,(SELECT
|
||||
CASE
|
||||
WHEN COUNT(*) = 0 THEN ''
|
||||
WHEN COUNT(*) = 1 THEN MIN(CIS.serial_no)
|
||||
ELSE MIN(CIS.serial_no) || ' 외 ' || (COUNT(*) - 1)::text || '건'
|
||||
END
|
||||
FROM contract_item_serial CIS
|
||||
WHERE CIS.item_objid::varchar = T.CONTRACT_ITEM_OBJID
|
||||
AND UPPER(CIS.status) = 'ACTIVE'
|
||||
AND CIS.serial_no IS NOT NULL
|
||||
AND CIS.serial_no != ''
|
||||
) AS SERIAL_NO
|
||||
-- 요청납기: contract_item.due_date → project_mgmt.due_date → contract_mgmt.due_date
|
||||
,COALESCE(
|
||||
(SELECT CI.due_date FROM contract_item CI
|
||||
WHERE CI.contract_objid = T.CONTRACT_OBJID
|
||||
AND CI.part_objid = T.PART_OBJID
|
||||
AND CI.status = 'ACTIVE'
|
||||
ORDER BY CI.objid DESC LIMIT 1),
|
||||
T.DUE_DATE,
|
||||
(SELECT CM.due_date FROM contract_mgmt CM WHERE CM.OBJID = T.CONTRACT_OBJID LIMIT 1)
|
||||
) AS REQ_DEL_DATE
|
||||
,T.EBOM_STATUS
|
||||
,T.MBOM_STATUS
|
||||
-- 발주일: contract_mgmt.order_date
|
||||
,(SELECT O.order_date FROM contract_mgmt AS O WHERE O.OBJID = T.CONTRACT_OBJID) AS ORDER_DATE
|
||||
,T.RECEIVING_RATE
|
||||
,T.PRODUCTION_TEAM_12
|
||||
,T.PRODUCTION_TEAM_3
|
||||
-- 출하일: sales_registration.shipping_date (project_no 매칭)
|
||||
,COALESCE(
|
||||
(SELECT TO_CHAR(SR.shipping_date, 'YYYY-MM-DD')
|
||||
FROM sales_registration SR
|
||||
WHERE SR.project_no = T.PROJECT_NO
|
||||
ORDER BY SR.sale_no DESC LIMIT 1),
|
||||
''
|
||||
) AS SHIPMENT_DATE
|
||||
,T.MECHANICAL_TYPE
|
||||
,T.OVERHAUL_ORDER
|
||||
,T.CONTRACT_OBJID
|
||||
|
||||
-- P2: PMS_WBS_TASK 의존 컬럼들은 그리드 표시엔 없으므로 자리만
|
||||
,0 AS TOTAL_RATE
|
||||
,0 AS DESIGN_RATE
|
||||
,0 AS PURCHASE_RATE
|
||||
,0 AS PRODUCE_RATE
|
||||
,0 AS SELFINS_RATE
|
||||
,0 AS FINALINS_RATE
|
||||
,0 AS SHIP_RATE
|
||||
,0 AS SETUP_RATE
|
||||
,0 AS WBS_CNT
|
||||
,NULL::text AS ASSEMBLY
|
||||
,NULL::text AS VERIFICATION
|
||||
FROM project_mgmt T
|
||||
LEFT JOIN customer_mng C
|
||||
ON C.customer_code = CASE WHEN T.CUSTOMER_OBJID LIKE 'C_%' THEN substring(T.CUSTOMER_OBJID, 3) ELSE T.CUSTOMER_OBJID END
|
||||
${where}
|
||||
ORDER BY SUBSTRING(T.PROJECT_NO, POSITION('-' IN T.PROJECT_NO)+1) DESC,
|
||||
T.OVERHAUL_ORDER DESC NULLS LAST
|
||||
`;
|
||||
|
||||
const res = await pool.query(sql, params);
|
||||
logger.info("진행관리 목록 조회", { count: res.rowCount, filter });
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
// ─── 옵션: 프로젝트번호 셀렉트박스용 ──────────────────────────
|
||||
// wace `common.getCusProjectNoList` 대응 — 진행관리 메뉴 검색폼 project_no 옵션 채움.
|
||||
|
||||
export async function listProjectNoOptions() {
|
||||
const pool = getPool();
|
||||
const sql = `
|
||||
SELECT OBJID AS value, PROJECT_NO AS label
|
||||
FROM project_mgmt
|
||||
WHERE PROJECT_NO IS NOT NULL AND PROJECT_NO != ''
|
||||
ORDER BY SUBSTRING(PROJECT_NO, POSITION('-' IN PROJECT_NO)+1) DESC
|
||||
`;
|
||||
const res = await pool.query(sql);
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
// ─── 단건/수정: P1.5에서 영업관리 OrderRegistDialog 재사용 결정 시 제거 가능 ──
|
||||
|
||||
export async function getById(objid: string) {
|
||||
const pool = getPool();
|
||||
const sql = `
|
||||
SELECT
|
||||
T.OBJID
|
||||
,T.PROJECT_NO
|
||||
,T.PROJECT_NAME
|
||||
,T.FACILITY
|
||||
,CODE_NAME(T.FACILITY) AS FACILITY_NAME
|
||||
,T.FACILITY_QTY
|
||||
,T.FACILITY_DEPTH
|
||||
,T.CONTRACT_DEL_DATE
|
||||
,T.CONTRACT_CURRENCY
|
||||
,CODE_NAME(T.CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME
|
||||
,T.CONTRACT_PRICE
|
||||
,T.CONTRACT_PRICE_CURRENCY
|
||||
FROM project_mgmt T
|
||||
WHERE T.OBJID = $1
|
||||
LIMIT 1
|
||||
`;
|
||||
const res = await pool.query(sql, [objid]);
|
||||
return res.rows[0] ?? null;
|
||||
}
|
||||
|
||||
export interface ProjectUpdateBody {
|
||||
project_no?: string;
|
||||
project_name?: string;
|
||||
facility?: string;
|
||||
facility_qty?: string;
|
||||
facility_depth?: string;
|
||||
contract_del_date?: string;
|
||||
contract_currency?: string;
|
||||
contract_price_currency?: string;
|
||||
contract_price?: string;
|
||||
}
|
||||
|
||||
export async function updateProject(objid: string, body: ProjectUpdateBody) {
|
||||
const pool = getPool();
|
||||
const sql = `
|
||||
UPDATE project_mgmt
|
||||
SET FACILITY_QTY = $1
|
||||
,PROJECT_NO = $2
|
||||
,CONTRACT_PRICE = $3
|
||||
,CONTRACT_PRICE_CURRENCY = $4
|
||||
,CONTRACT_CURRENCY = $5
|
||||
,PROJECT_NAME = $6
|
||||
,FACILITY = $7
|
||||
,FACILITY_DEPTH = $8
|
||||
,CONTRACT_DEL_DATE = $9
|
||||
WHERE OBJID = $10
|
||||
`;
|
||||
const params = [
|
||||
body.facility_qty ?? "",
|
||||
body.project_no ?? "",
|
||||
body.contract_price ?? "",
|
||||
body.contract_price_currency ?? "",
|
||||
body.contract_currency ?? "",
|
||||
body.project_name ?? "",
|
||||
body.facility ?? "",
|
||||
body.facility_depth ?? "",
|
||||
body.contract_del_date ?? "",
|
||||
objid,
|
||||
];
|
||||
const res = await pool.query(sql, params);
|
||||
logger.info("진행관리 수정", { objid, rowCount: res.rowCount });
|
||||
return res.rowCount ?? 0;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
# 프로젝트관리 이식 GAP 분석 (원본 wace_plm 대비)
|
||||
|
||||
> 작성: 2026-05-11 / 작성자: hjjeong
|
||||
> **개정 2026-05-11**: 운영판 실 화면은 `projectMgmtList.jsp`(옛 화면)가 **아니라** `projectMgmtWbsList3.jsp` + 매퍼 `projectMgmtWbsGridList`인 것이 확인되어 GAP 전면 개정.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한 문장 요약
|
||||
|
||||
운영판 진행관리는 **`/project/projectMgmtWbsList3.do`** endpoint가 반환하는 화면이고, 그리드 SQL은 매퍼 `projectMgmtWbsGridList`(project.xml:3854~4280). 그리드 컬럼은 8그룹/18셀 + 검색폼은 11필드. 의존 테이블 대부분 RPS에 보유 — `project_mgmt`/`contract_mgmt`/`contract_item`/`contract_item_serial`/`customer_mng`/`user_info`/`comm_code`/`attach_file_info`/`sales_registration`/`part_mng`. `pms_wbs_task`/`setup_wbs_task`만 부재(P2 의존, 그리드 표시 컬럼엔 영향 거의 없음). **P1에서 그리드 18셀 전부 실데이터로 채울 수 있음.**
|
||||
|
||||
## 0.1 이식 원칙
|
||||
|
||||
> JSP/Java/매퍼XML 안의 `/* */`, `<!-- -->`, `//` 주석 블록은 비활성. 활성 코드만 이식.
|
||||
|
||||
- 운영 화면(waceplm.esgrin.com) = 진실의 기준.
|
||||
- wace 컨트롤러에 `projectMgmtList.do`(옛) + `projectMgmtList1.do` + **`projectMgmtWbsList3.do`(운영판)** 셋이 동시 존재 — 메뉴가 가리키는 것이 무엇인지 확인 필수.
|
||||
- `client_mng`/`supply_mng` → RPS는 `customer_mng`로 통합. CASE WHEN LIKE 'C_%' 분기는 모두 customer_mng 단일 LEFT JOIN으로 변환.
|
||||
- `CODE_NAME()` 함수 RPS DB에 존재. 다만 영업관리 패턴(`LEFT JOIN comm_code CC_X ON CC_X.code_id=...`)으로 통일.
|
||||
- 금액 1,234.00 / 수량 1,234 / 모든 숫자 right-align.
|
||||
|
||||
---
|
||||
|
||||
## 1. 운영판 진행관리 흐름
|
||||
|
||||
### 1.1 endpoint
|
||||
|
||||
| URL | Controller | view / 결과 |
|
||||
|---|---|---|
|
||||
| `/project/projectMgmtWbsList3.do` | `projectMgmtWbsList3` (L3243) | view 반환 + 코드맵(`category_cd`/`customer_cd`/`project_no`/`product_cd`/`status_cd`/`result_cd`/`pm_user_id`) |
|
||||
| `/project/projectMgmtWbsGridList.do` | (`projectMgmtWbsGridList` 매퍼 호출) | 그리드 JSON |
|
||||
| `fn_openSaleRegPopup(orderNo)` → `/salesMgmt/salesRegForm.do?orderNo={PROJECT_NO}` | (영업관리 주문서 등록 화면) | 행 클릭 → 영업관리 주문서 등록 폼 오픈. **진행관리 자체 수정 폼 없음.** |
|
||||
|
||||
→ 본 이식의 RPS 측 라우트:
|
||||
- `GET /api/project/progress/list` — 그리드
|
||||
- 행 클릭 동작은 **P1.5에서 결정** (영업관리 `OrderRegistDialog` 재사용 vs 미연결)
|
||||
|
||||
### 1.2 그리드 컬럼 (운영판 그대로)
|
||||
|
||||
8그룹 / 18셀 (frozen 1개 포함):
|
||||
|
||||
| # | dataField | 라벨 | 의존 | P1 채움 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **PROJECT_NO** (frozen) | 프로젝트번호 | project_mgmt | ✅ |
|
||||
| 2 | CATEGORY_NAME | 주문유형 | comm_code(0000167) | ✅ |
|
||||
| 3 | PRODUCT_NAME | 제품구분 | comm_code(0000001) | ✅ |
|
||||
| 4 | AREA_NAME | 국내/해외 | comm_code(area_cd) | ✅ |
|
||||
| 5 | REG_DATE | 접수일 | TO_CHAR(regdate,'YYYY-MM-DD') | ✅ |
|
||||
| 6 | CUSTOMER_NAME | 고객사 | customer_mng | ✅ |
|
||||
| 7 | FREE_OF_CHARGE | 유/무상 | contract_mgmt.paid_type → '유상'/'무상' | ✅ |
|
||||
| 8 | PRODUCT_ITEM_CODE | 품번 | project_mgmt.part_no | ✅ |
|
||||
| 9 | PRODUCT_ITEM_NAME | 품명 | project_mgmt.part_name | ✅ |
|
||||
| 10 | SERIAL_NO | S/N | contract_item_serial (CIS.item_objid=T.contract_item_objid) | ✅ |
|
||||
| 11 | CONTRACT_QTY | 수주수량 | project_mgmt.quantity | ✅ |
|
||||
| 12 | REQ_DEL_DATE | 요청납기 | COALESCE(contract_item.due_date, project_mgmt.due_date, contract_mgmt.due_date) | ✅ |
|
||||
| 13 | EBOM_STATUS | E-BOM | project_mgmt.ebom_status | ✅ (대부분 빈값, 운영DB도 빈값) |
|
||||
| 14 | MBOM_STATUS | M-BOM | project_mgmt.mbom_status | ✅ (대부분 빈값) |
|
||||
| 15 | ORDER_DATE | 발주일 | contract_mgmt.order_date | ✅ |
|
||||
| 16 | RECEIVING_RATE | 입고율 | project_mgmt.receiving_rate | ✅ |
|
||||
| 17 | PRODUCTION_TEAM_12 | 제조1,2팀 | project_mgmt.production_team_12 | ✅ |
|
||||
| 18 | PRODUCTION_TEAM_3 | 제조3팀 | project_mgmt.production_team_3 | ✅ |
|
||||
| 19 | ASSEMBLY | 조립 | (wace SQL에 없음 — 빈값) | (빈값) |
|
||||
| 20 | VERIFICATION | 검증 | (wace SQL에 없음 — 빈값) | (빈값) |
|
||||
| 21 | SHIPMENT_DATE | 출하일 | sales_registration.shipping_date (project_no 매칭) | ✅ |
|
||||
|
||||
→ **18셀 모두 P1에서 채움 가능** (장비 조립/검증 2개는 wace 원본도 빈값). PMS_WBS_TASK 의존 컬럼들은 그리드 표시에 없음.
|
||||
|
||||
### 1.3 검색 폼 (11필드)
|
||||
|
||||
| # | name | 라벨 | 입력 | 매핑 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Year | 년도 | select(sysYear±4) | TO_CHAR(REGDATE,'YYYY') |
|
||||
| 2 | project_nos | 프로젝트번호 | multi-select | OBJID IN (쉼표 split) |
|
||||
| 3 | category_cd | 주문유형 | select(0000167) | T.CATEGORY_CD |
|
||||
| 4 | customer_objid | 고객사 | select | T.CUSTOMER_OBJID |
|
||||
| 5 | product | 제품구분 | select(0000001) | T.PRODUCT |
|
||||
| 6 | contract_start_date / contract_end_date | 요청납기일 | date range | COALESCE(contract_item.due_date, project_mgmt.due_date, contract_mgmt.due_date) |
|
||||
| 7 | area_cd | 국내/해외 | select(국내/해외 라벨) | CODE_NAME(AREA_CD) = '국내'/'해외' |
|
||||
| 8 | free_of_charge | 유/무상 | select(유상/무상 라벨) | contract_mgmt.paid_type 매핑 비교 |
|
||||
| 9 | product_item_code | 품번 | autocomplete | T.PART_NO ILIKE '%?%' |
|
||||
| 10 | product_item_name | 품명 | autocomplete | T.PART_NAME ILIKE '%?%' |
|
||||
| 11 | serial_no | S/N | text | EXISTS contract_item_serial ILIKE |
|
||||
|
||||
비활성(주석블록): location / setup / pm_user_id — 무시.
|
||||
|
||||
### 1.4 ORDER BY
|
||||
|
||||
```sql
|
||||
ORDER BY SUBSTRING(PROJECT_NO,POSITION('-' IN PROJECT_NO)+1) DESC,
|
||||
OVERHAUL_ORDER DESC NULLS LAST
|
||||
```
|
||||
|
||||
### 1.5 행 클릭 동작
|
||||
|
||||
`fn_openSaleRegPopup(PROJECT_NO)` → `/salesMgmt/salesRegForm.do?orderNo={PROJECT_NO}` (영업관리 주문서 등록 폼). **진행관리 자체 수정 폼 없음.** RPS 이식 시 P1.5 별도 결정.
|
||||
|
||||
---
|
||||
|
||||
## 2. RPS DB 보유 매트릭스 (개정)
|
||||
|
||||
| 테이블 | 보유 | 진행관리에서의 용도 | P1 |
|
||||
|---|---|---|---|
|
||||
| project_mgmt | ✅ | 메인 (90건) | ✅ |
|
||||
| contract_mgmt | ✅ | paid_type / order_date / due_date / pm_user_id | ✅ |
|
||||
| **contract_item** | ✅ | 요청납기 COALESCE 1순위 | ✅ |
|
||||
| **contract_item_serial** | ✅ | S/N 집계 | ✅ |
|
||||
| **sales_registration** | ✅ | 출하일 COALESCE | ✅ |
|
||||
| customer_mng | ✅ | 고객사명 (wace의 client_mng/supply_mng 분기 흡수) | ✅ |
|
||||
| user_info | ✅ | PM명 / writer명 / chg_user 등 | ✅ |
|
||||
| comm_code + CODE_NAME() | ✅ | 카테고리/제품/지역/통화 등 라벨 | ✅ |
|
||||
| attach_file_info | ✅ | contractMgmt01/02 카운트 (CU01_CNT/CU02_CNT) | ✅ |
|
||||
| part_mng | ✅ | (이번 P1에서는 직접 사용 안 함 — project_mgmt.part_no/part_name 직접 사용) | — |
|
||||
| purchase_order_master | ✅ | (이번 P1에서는 사용 안 함 — P2의 투입원가 컬럼에서만 사용) | — |
|
||||
| pms_wbs_task | ❌ | 진척율/연체 카운트 — 그리드 표시 컬럼엔 없음 | (P2) |
|
||||
| setup_wbs_task | ❌ | 셋업 진척율 — 그리드 표시 컬럼엔 없음 | (P2) |
|
||||
| client_mng / supply_mng | ➖ | wace 전용 (customer_mng로 흡수) | — |
|
||||
|
||||
→ **P1에서 18셀 전부 실데이터 채울 수 있음.**
|
||||
|
||||
---
|
||||
|
||||
## 3. GAP 매트릭스 (개정)
|
||||
|
||||
| # | 우선 | 항목 | 권장 작업 |
|
||||
|---|---|---|---|
|
||||
| **PRJ-1** | 🔴 | 진행관리 메뉴 자체 부재 → 운영판 화면 1:1 이식 | **본 PR (P1)** — 18셀 + 검색 11필드 |
|
||||
| **PRJ-2** | 🟠 | 행 클릭 → 영업관리 주문서 등록 폼 오픈 (`fn_openSaleRegPopup`) | **P1.5** — `OrderRegistDialog` 재사용 vs 미연결. 사용자 결정 |
|
||||
| **PRJ-3** | 🟠 | 다중 선택 프로젝트번호 검색 (multi-select) | **본 PR** — frontend SmartSelect multi 모드, backend는 `project_nos` 쉼표 split |
|
||||
| **PRJ-4** | 🟡 | 검색 코드맵 옵션 (`project_no` 다중 옵션 리스트) | wace는 코드맵에서 동적 생성. RPS는 grid 데이터 기반 옵션 또는 별도 API. **P1 보수적 처리**: 사용자가 OBJID 또는 PROJECT_NO 직접 입력 |
|
||||
| **PRJ-5** | 🟢 | 엑셀 다운로드 | wace에 없음 — 본 PR 제외 |
|
||||
| **PRJ-6** | 🟢 | 진척율(PMS_WBS_TASK 의존) — 그리드 표시 컬럼엔 영향 없으나 SQL은 계산 | **P2 (WBS 메뉴 이식 후)**. 본 P1은 진척율 컬럼 자체가 그리드에 없으므로 미반영 |
|
||||
| **PRJ-7** | 🟢 | 설계/생산 ASSEMBLY/VERIFICATION 컬럼 (운영판 빈값) | 자리만, 빈값 |
|
||||
|
||||
---
|
||||
|
||||
## 4. P1 스코프 — 본 GAP 결정사항
|
||||
|
||||
### 4.1 백엔드 갱신 (`backend-node/src/`)
|
||||
|
||||
| 파일 | 동작 |
|
||||
|---|---|
|
||||
| `services/projectMgmtService.ts` | **전면 교체** — `listProgress` SQL을 `projectMgmtWbsGridList` 1:1로 재작성. `getById`/`updateProject`는 P1.5에서 결정될 때까지 일단 유지 (사용 안 됨) |
|
||||
| `controllers/projectMgmtController.ts` | 현행 유지 |
|
||||
| `routes/projectMgmtRoutes.ts` | 현행 유지 (`GET /list` + 향후 추가 endpoint 자리) |
|
||||
| `app.ts` | 현행 유지 (이미 마운트됨) |
|
||||
|
||||
### 4.2 프론트엔드 갱신 (`frontend/`)
|
||||
|
||||
| 파일 | 동작 |
|
||||
|---|---|
|
||||
| `app/(main)/COMPANY_16/project/progress/page.tsx` | **전면 교체** — 검색폼 11필드(2행) + 그리드 18셀(8그룹 평탄화) |
|
||||
| `components/project/ProjectProgressEditDialog.tsx` | **폐기** (옛 jsp 기반). 행 클릭은 P1.5에서 결정 |
|
||||
| `lib/api/projectMgmt.ts` | 검색 타입/결과 행 타입 갱신 |
|
||||
| `components/layout/AdminPageRenderer.tsx` | 현행 유지 |
|
||||
|
||||
### 4.3 검색 옵션 컴포넌트 매핑
|
||||
|
||||
| 검색 필드 | RPS 컴포넌트 |
|
||||
|---|---|
|
||||
| Year | `<select>` (native, sysYear±4 동적) |
|
||||
| project_nos | `<Input>` (쉼표 직렬화 텍스트) — 다중 SmartSelect는 P1.5에서 보강 가능 |
|
||||
| category_cd / product | `<CommCodeSelect groupId="0000167"/"0000001">` |
|
||||
| customer_objid | `<CustomerSelect>` |
|
||||
| 요청납기 범위 | `<Input type="date">` × 2 |
|
||||
| area_cd | `<select>` 정적 옵션 ('국내'/'해외') |
|
||||
| free_of_charge | `<select>` 정적 옵션 ('유상'/'무상') |
|
||||
| product_item_code / product_item_name | `<Input>` (자동완성은 P1.5 보강) |
|
||||
| serial_no | `<Input>` |
|
||||
|
||||
### 4.4 본 PR에서 **하지 않을 것**
|
||||
- WBS관리 메뉴(별도 PR P2)
|
||||
- PMS_WBS_TASK / SETUP_WBS_TASK 의존 컬럼 (그리드 표시 컬럼엔 없으므로 영향 없음)
|
||||
- 행 클릭 → 영업관리 주문서 등록 폼 라우팅 (P1.5 별도)
|
||||
- 다중 select / 품번품명 자동완성 (P1.5 보강)
|
||||
- 엑셀 / 결재 (운영판 없음)
|
||||
|
||||
---
|
||||
|
||||
## 5. 사용자 결정 사항 (2026-05-11)
|
||||
|
||||
| # | 항목 | 결정 |
|
||||
|---|---|---|
|
||||
| 1 | URL 경로 | `/COMPANY_16/project/progress` |
|
||||
| 2 | 차종/OEM 마일스톤 | 영구 제외 |
|
||||
| 3 | 운영판 1:1 재작성 범위 | **그리드 + 검색폼 모두 운영판(projectMgmtWbsList3) 기준 재작성** |
|
||||
| 4 | 행 클릭 다이얼로그 | P1.5 — `OrderRegistDialog` 재사용 검토. P1에서는 미연결 |
|
||||
| 5 | 진척율(WBS) 컬럼 | P2에서 — 그리드 표시 컬럼엔 없으므로 P1 영향 없음 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 다음 단계
|
||||
|
||||
1. P1 운영판 1:1 재작성 → 사용자 동작 확인 (그리드 18셀이 실데이터로 채워지는지)
|
||||
2. P1.5: 행 클릭 동작 결정 (영업관리 OrderRegistDialog 재사용 / 미연결 / 신규 다이얼로그)
|
||||
3. P2: WBS관리 메뉴(`/COMPANY_16/project/wbs-template`) — PMS_WBS_* 운영DB DDL 추출 + 02-wbs.md
|
||||
@@ -0,0 +1,238 @@
|
||||
"use client";
|
||||
|
||||
// 진행관리 (wace projectMgmtWbsList3.jsp + projectMgmtWbsGridList 1:1 이식)
|
||||
// 원본:
|
||||
// - JSP: /Users/jhj/wace_plm/WebContent/WEB-INF/view/project/projectMgmtWbsList3.jsp (349줄)
|
||||
// - 매퍼: wace_plm/src/com/pms/mapper/project.xml:3854 projectMgmtWbsGridList
|
||||
// GAP: docs/migration/project/00-gap.md
|
||||
//
|
||||
// 그리드: 8그룹 18셀 평탄화 (DataGrid가 그룹 헤더 미지원 → 라벨 prefix로 표현)
|
||||
// 검색폼: 11필드 (1행 6 + 2행 5)
|
||||
// 행 클릭: P1.5에서 영업관리 OrderRegistDialog 재사용 검토 — 현재 미연결
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Search, Loader2, RotateCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
|
||||
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
|
||||
import { CustomerSelect } from "@/components/common/CustomerSelect";
|
||||
import { PartSelect } from "@/components/common/PartSelect";
|
||||
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
|
||||
import { projectMgmtApi, ProgressListFilter, ProgressRow } from "@/lib/api/projectMgmt";
|
||||
|
||||
// wace projectMgmtWbsList3.jsp 컬럼 정의 1:1 (8그룹 → 평탄화, 그룹명은 라벨 prefix)
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[160px]", frozen: true },
|
||||
// 프로젝트정보 그룹
|
||||
{ key: "category_name", label: "주문유형", width: "w-[90px]", align: "center" },
|
||||
{ key: "product_name", label: "제품구분", width: "w-[100px]", align: "left" },
|
||||
{ key: "area_name", label: "국내/해외", width: "w-[90px]", align: "center" },
|
||||
{ key: "reg_date", label: "접수일", width: "w-[110px]", align: "center" },
|
||||
{ key: "customer_name", label: "고객사", width: "w-[140px]" },
|
||||
{ key: "free_of_charge", label: "유/무상", width: "w-[80px]", align: "center" },
|
||||
{ key: "product_item_code", label: "품번", width: "w-[150px]" },
|
||||
{ key: "product_item_name", label: "품명", width: "w-[180px]" },
|
||||
{ key: "serial_no", label: "S/N", width: "w-[150px]" },
|
||||
{ key: "contract_qty", label: "수주수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "req_del_date", label: "요청납기", width: "w-[110px]", align: "center" },
|
||||
// 설계
|
||||
{ key: "ebom_status", label: "E-BOM", width: "w-[100px]", align: "center" },
|
||||
// 생산관리
|
||||
{ key: "mbom_status", label: "M-BOM", width: "w-[100px]", align: "center" },
|
||||
// 구매
|
||||
{ key: "order_date", label: "발주일", width: "w-[110px]", align: "center" },
|
||||
{ key: "receiving_rate", label: "입고율", width: "w-[90px]", align: "right" },
|
||||
// 생산
|
||||
{ key: "production_team_12", label: "제조1,2팀", width: "w-[100px]", align: "center" },
|
||||
{ key: "production_team_3", label: "제조3팀", width: "w-[100px]", align: "center" },
|
||||
// 장비
|
||||
{ key: "assembly", label: "조립", width: "w-[90px]", align: "center" },
|
||||
{ key: "verification", label: "검증", width: "w-[90px]", align: "center" },
|
||||
// 출하
|
||||
{ key: "shipment_date", label: "출하일", width: "w-[110px]", align: "center" },
|
||||
];
|
||||
|
||||
const CATEGORY_GROUP = "0000167"; // 주문유형
|
||||
const PRODUCT_GROUP = "0000001"; // 제품구분
|
||||
|
||||
// wace L229: 시스템년도 ±4 (운영판은 sysYear-4 ~ sysYear). RPS는 sysYear±4로 여유.
|
||||
const YEAR_OPTIONS = (() => {
|
||||
const cur = new Date().getFullYear();
|
||||
const arr: string[] = [];
|
||||
for (let y = cur + 4; y >= cur - 4; y--) arr.push(String(y));
|
||||
return arr;
|
||||
})();
|
||||
|
||||
const EMPTY_FILTER: ProgressListFilter = {
|
||||
Year: "", project_nos: "", category_cd: "", customer_objid: "", product: "",
|
||||
contract_start_date: "", contract_end_date: "",
|
||||
area_cd: "", free_of_charge: "",
|
||||
search_partObjId: "", serial_no: "",
|
||||
};
|
||||
|
||||
export default function ProjectProgressPage() {
|
||||
const [rows, setRows] = useState<ProgressRow[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filter, setFilter] = useState<ProgressListFilter>(EMPTY_FILTER);
|
||||
const [projectNoOptions, setProjectNoOptions] = useState<SmartSelectOption[]>([]);
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await projectMgmtApi.list(filter);
|
||||
setRows(data);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
||||
} finally { setLoading(false); }
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => { fetchList(); /* eslint-disable-next-line */ }, []);
|
||||
|
||||
// 프로젝트번호 셀렉트 옵션 (wace common.getCusProjectNoList 대응)
|
||||
useEffect(() => {
|
||||
projectMgmtApi.projectNoOptions()
|
||||
.then((opts) => setProjectNoOptions(opts.map((o) => ({ code: o.value, label: o.label }))))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const handleReset = () => {
|
||||
setFilter(EMPTY_FILTER);
|
||||
setTimeout(() => fetchList(), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* 검색폼 — wace projectMgmtWbsList3.jsp:222-313 활성 11필드 */}
|
||||
<div className="border-b bg-card px-4 py-3">
|
||||
<div className="grid grid-cols-6 gap-3 text-sm">
|
||||
{/* 1행 */}
|
||||
<Field label="년도">
|
||||
<select
|
||||
className="h-9 w-full rounded-md border bg-background px-2 text-sm"
|
||||
value={filter.Year ?? ""}
|
||||
onChange={(e) => setFilter({ ...filter, Year: e.target.value })}
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{YEAR_OPTIONS.map((y) => <option key={y} value={y}>{y}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="프로젝트번호">
|
||||
<SmartSelect
|
||||
options={projectNoOptions}
|
||||
value={filter.project_nos ?? ""}
|
||||
onValueChange={(v) => setFilter({ ...filter, project_nos: v })}
|
||||
placeholder="전체"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="주문유형">
|
||||
<CommCodeSelect
|
||||
groupId={CATEGORY_GROUP}
|
||||
value={filter.category_cd ?? ""}
|
||||
onValueChange={(v) => setFilter({ ...filter, category_cd: v })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="고객사">
|
||||
<CustomerSelect
|
||||
value={filter.customer_objid ?? ""}
|
||||
onValueChange={(v) => setFilter({ ...filter, customer_objid: v })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="제품구분">
|
||||
<CommCodeSelect
|
||||
groupId={PRODUCT_GROUP}
|
||||
value={filter.product ?? ""}
|
||||
onValueChange={(v) => setFilter({ ...filter, product: v })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="요청납기일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={filter.contract_start_date ?? ""} onChange={(e) => setFilter({ ...filter, contract_start_date: e.target.value })} />
|
||||
<span className="text-xs text-muted-foreground">~</span>
|
||||
<Input type="date" value={filter.contract_end_date ?? ""} onChange={(e) => setFilter({ ...filter, contract_end_date: e.target.value })} />
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{/* 2행 */}
|
||||
<Field label="국내/해외">
|
||||
<select
|
||||
className="h-9 w-full rounded-md border bg-background px-2 text-sm"
|
||||
value={filter.area_cd ?? ""}
|
||||
onChange={(e) => setFilter({ ...filter, area_cd: e.target.value })}
|
||||
>
|
||||
<option value="">전체</option>
|
||||
<option value="국내">국내</option>
|
||||
<option value="해외">해외</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="유/무상">
|
||||
<select
|
||||
className="h-9 w-full rounded-md border bg-background px-2 text-sm"
|
||||
value={filter.free_of_charge ?? ""}
|
||||
onChange={(e) => setFilter({ ...filter, free_of_charge: e.target.value })}
|
||||
>
|
||||
<option value="">전체</option>
|
||||
<option value="유상">유상</option>
|
||||
<option value="무상">무상</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="품번">
|
||||
<PartSelect
|
||||
mode="partNo"
|
||||
value={filter.search_partObjId ?? ""}
|
||||
onValueChange={(v) => setFilter({ ...filter, search_partObjId: v })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="품명">
|
||||
<PartSelect
|
||||
mode="partName"
|
||||
value={filter.search_partObjId ?? ""}
|
||||
onValueChange={(v) => setFilter({ ...filter, search_partObjId: v })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="S/N">
|
||||
<Input
|
||||
value={filter.serial_no ?? ""}
|
||||
onChange={(e) => setFilter({ ...filter, serial_no: e.target.value })}
|
||||
placeholder="S/N LIKE"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* 액션 */}
|
||||
<div className="flex items-end justify-end gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleReset}>
|
||||
<RotateCcw className="h-4 w-4" /><span className="ml-1">초기화</span>
|
||||
</Button>
|
||||
<Button size="sm" onClick={fetchList} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
|
||||
<span className="ml-1">조회</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 그리드 (8그룹 18셀 평탄화) */}
|
||||
<div className="flex-1 min-h-0 p-2">
|
||||
<DataGrid
|
||||
columns={GRID_COLUMNS}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
emptyMessage="조건에 맞는 프로젝트가 없습니다."
|
||||
gridId="project-progress-wbslist3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<Label className="mb-1 block text-xs text-muted-foreground">{label}</Label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -104,6 +104,7 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
"/COMPANY_16/sales/shipping-plan": dynamic(() => import("@/app/(main)/COMPANY_16/sales/shipping-plan/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/sales/claim": dynamic(() => import("@/app/(main)/COMPANY_16/sales/claim/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/sales/quote": dynamic(() => import("@/app/(main)/COMPANY_16/sales/quote/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/project/progress": dynamic(() => import("@/app/(main)/COMPANY_16/project/progress/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/production/process-info": dynamic(() => import("@/app/(main)/COMPANY_16/production/process-info/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/production/result": dynamic(() => import("@/app/(main)/COMPANY_16/production/result/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/production/work-instruction": dynamic(() => import("@/app/(main)/COMPANY_16/production/work-instruction/page"), { ssr: false, loading: LoadingFallback }),
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { apiClient } from "./client";
|
||||
|
||||
// wace projectMgmtWbsGridList 매퍼 + 영업관리 패턴 통일
|
||||
export interface ProgressListFilter {
|
||||
Year?: string;
|
||||
project_nos?: string; // OBJID (단일 또는 쉼표 다중)
|
||||
category_cd?: string;
|
||||
customer_objid?: string;
|
||||
product?: string;
|
||||
status_cd?: string;
|
||||
result_cd?: string;
|
||||
contract_start_date?: string;
|
||||
contract_end_date?: string;
|
||||
area_cd?: string; // '국내'/'해외'
|
||||
free_of_charge?: string; // '유상'/'무상'
|
||||
// 영업관리 패턴 통일 — PartSelect의 value (part_mng.objid::varchar)
|
||||
search_partObjId?: string;
|
||||
serial_no?: string;
|
||||
pm_user_id?: string;
|
||||
location?: string;
|
||||
setup?: string;
|
||||
}
|
||||
|
||||
export interface ProjectNoOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// 운영판 그리드 18셀 + 부속 필드
|
||||
export interface ProgressRow {
|
||||
objid: string;
|
||||
project_no: string | null;
|
||||
category_cd: string | null;
|
||||
category_name: string | null;
|
||||
customer_objid: string | null;
|
||||
customer_name: string | null;
|
||||
product: string | null;
|
||||
product_name: string | null;
|
||||
area_name: string | null;
|
||||
reg_date: string | null;
|
||||
free_of_charge: string | null;
|
||||
product_item_code: string | null;
|
||||
product_item_name: string | null;
|
||||
serial_no: string | null;
|
||||
contract_qty: number | string | null;
|
||||
req_del_date: string | null;
|
||||
ebom_status: string | null;
|
||||
mbom_status: string | null;
|
||||
order_date: string | null;
|
||||
receiving_rate: string | null;
|
||||
production_team_12: string | null;
|
||||
production_team_3: string | null;
|
||||
assembly: string | null;
|
||||
verification: string | null;
|
||||
shipment_date: string | null;
|
||||
// 부속
|
||||
contract_objid: string | null;
|
||||
contract_no: string | null;
|
||||
overhaul_order: string | null;
|
||||
pm_user_name: string | null;
|
||||
writer_name: string | null;
|
||||
cu01_cnt: number | null;
|
||||
cu02_cnt: number | null;
|
||||
}
|
||||
|
||||
export interface ProgressDetail {
|
||||
objid: string;
|
||||
project_no: string;
|
||||
project_name: string;
|
||||
facility: string;
|
||||
facility_name: string;
|
||||
facility_qty: string;
|
||||
facility_depth: string;
|
||||
contract_del_date: string;
|
||||
contract_currency: string;
|
||||
contract_currency_name: string;
|
||||
contract_price: string;
|
||||
contract_price_currency: string;
|
||||
}
|
||||
|
||||
export interface ProgressUpdateBody {
|
||||
project_no?: string;
|
||||
project_name?: string;
|
||||
facility?: string;
|
||||
facility_qty?: string;
|
||||
facility_depth?: string;
|
||||
contract_del_date?: string;
|
||||
contract_currency?: string;
|
||||
contract_price_currency?: string;
|
||||
contract_price?: string;
|
||||
}
|
||||
|
||||
export const projectMgmtApi = {
|
||||
async list(filter: ProgressListFilter = {}) {
|
||||
const res = await apiClient.get("/project/progress/list", { params: filter });
|
||||
return (res.data?.data ?? []) as ProgressRow[];
|
||||
},
|
||||
async projectNoOptions(): Promise<ProjectNoOption[]> {
|
||||
const res = await apiClient.get("/project/progress/project-no-options");
|
||||
return (res.data?.data ?? []) as ProjectNoOption[];
|
||||
},
|
||||
async detail(objid: string): Promise<ProgressDetail | null> {
|
||||
const res = await apiClient.get(`/project/progress/${objid}`);
|
||||
return res.data?.data ?? null;
|
||||
},
|
||||
async update(objid: string, body: ProgressUpdateBody) {
|
||||
return (await apiClient.put(`/project/progress/${objid}`, body)).data;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user