diff --git a/backend-node/src/routes/salesCommonRoutes.ts b/backend-node/src/routes/salesCommonRoutes.ts index 66082601..fc74e038 100644 --- a/backend-node/src/routes/salesCommonRoutes.ts +++ b/backend-node/src/routes/salesCommonRoutes.ts @@ -33,17 +33,23 @@ router.get("/codes/:groupId", async (req: Request, res: Response) => { } }); -/** GET /api/sales/parts → [{id, item_number, item_name}] (영업관리 풀-옵션용) */ +/** GET /api/sales/parts → [{id, item_number, item_name}] (영업관리 풀-옵션용) + * 데이터 출처: wace 품목 마스터 전용 테이블 part_mng (8,176건). + * 반환 키는 기존 호환을 위해 id/item_number/item_name으로 alias. + * status는 active/release/활성만 (wace 운영 값). + */ 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`, + `SELECT objid::varchar AS id, + part_no AS item_number, + part_name AS item_name + FROM part_mng + WHERE LOWER(COALESCE(status, '')) IN ('active', 'release', '활성') + AND part_no IS NOT NULL AND part_no <> '' + AND part_name IS NOT NULL AND part_name <> '' + ORDER BY part_no NULLS LAST, part_name NULLS LAST`, ); res.json({ success: true, data: result.rows }); } catch (err) { diff --git a/backend-node/src/services/salesEstimateService.ts b/backend-node/src/services/salesEstimateService.ts index 9fe418cd..720fef2a 100644 --- a/backend-node/src/services/salesEstimateService.ts +++ b/backend-node/src/services/salesEstimateService.ts @@ -179,7 +179,7 @@ export async function getList(filter: EstimateListFilter) { ,C.customer_name AS CUSTOMER_NAME ,C.customer_type AS CUSTOMER_TYPE ,T.PRODUCT - ,CC_PRD.code_name AS PRODUCT_NAME + ,COALESCE(CI_AGG.product_summary, CC_PRD.code_name) AS PRODUCT_NAME ,T.AREA_CD ,CC_AREA.code_name AS AREA_NAME ,T.PAID_TYPE @@ -240,6 +240,7 @@ export async function getList(filter: EstimateListFilter) { END AS SERIAL_NO ,CI_AGG.earliest_due_date AS EARLIEST_DUE_DATE ,COALESCE(CI_AGG.other_due_date_count, 0) AS OTHER_DUE_DATE_COUNT + ,CI_AGG.return_reason_summary AS RETURN_REASON_SUMMARY /* 메일 발송 정보 (mail_log: TITLE의 [OBJID:NN]에서 OBJID 매칭) */ ,ML.mail_send_status AS MAIL_SEND_STATUS ,ML.mail_send_date AS MAIL_SEND_DATE @@ -291,17 +292,21 @@ export async function getList(filter: EstimateListFilter) { FROM estimate_template GROUP BY contract_objid ) ET_CNT ON ET_CNT.contract_objid = T.OBJID - /* 품목 집계 */ + /* 품목 집계 (제품구분/반납사유 한글명 distinct join — wace 헤더 product 폐지·라인으로 이동) */ LEFT JOIN ( SELECT CI.contract_objid, COUNT(*) AS item_count, - (array_agg(COALESCE(IT.item_name, CI.part_name) ORDER BY CI.seq))[1] AS first_part_name, - (array_agg(COALESCE(IT.item_number, CI.part_no) ORDER BY CI.seq))[1] AS first_part_no, + (array_agg(COALESCE(PM.part_name, CI.part_name) ORDER BY CI.seq))[1] AS first_part_name, + (array_agg(COALESCE(PM.part_no, CI.part_no) ORDER BY CI.seq))[1] AS first_part_no, MIN(CASE WHEN CI.due_date IS NOT NULL AND CI.due_date != '' THEN CI.due_date END) AS earliest_due_date, - GREATEST(COUNT(CASE WHEN CI.due_date IS NOT NULL AND CI.due_date != '' THEN 1 END) - 1, 0) AS other_due_date_count + GREATEST(COUNT(CASE WHEN CI.due_date IS NOT NULL AND CI.due_date != '' THEN 1 END) - 1, 0) AS other_due_date_count, + STRING_AGG(DISTINCT CC_RR.code_name, ', ') FILTER (WHERE CC_RR.code_name IS NOT NULL) AS return_reason_summary, + STRING_AGG(DISTINCT CC_PRDI.code_name, ', ') FILTER (WHERE CC_PRDI.code_name IS NOT NULL) AS product_summary FROM contract_item CI - LEFT JOIN item_info IT ON IT.id = CI.part_objid + LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid + LEFT JOIN comm_code CC_RR ON CC_RR.code_id = CI.return_reason AND CC_RR.status='active' + LEFT JOIN comm_code CC_PRDI ON CC_PRDI.code_id = CI.product AND CC_PRDI.status='active' WHERE CI.status = 'ACTIVE' GROUP BY CI.contract_objid ) CI_AGG ON CI_AGG.contract_objid = T.OBJID @@ -360,10 +365,10 @@ export async function getById(objid: string) { const itemsRes = await pool.query( `SELECT CI.*, - COALESCE(IT.item_number, CI.part_no) AS master_part_no, - COALESCE(IT.item_name, CI.part_name) AS master_part_name + COALESCE(PM.part_no, CI.part_no) AS master_part_no, + COALESCE(PM.part_name, CI.part_name) AS master_part_name FROM contract_item CI - LEFT JOIN item_info IT ON IT.id = CI.part_objid + LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid WHERE CI.contract_objid = $1 AND CI.status = 'ACTIVE' ORDER BY CI.seq`, diff --git a/backend-node/src/services/salesOrderMgmtService.ts b/backend-node/src/services/salesOrderMgmtService.ts index 0fe36bec..d73a382c 100644 --- a/backend-node/src/services/salesOrderMgmtService.ts +++ b/backend-node/src/services/salesOrderMgmtService.ts @@ -127,7 +127,7 @@ export async function getList(filter: OrderListFilter) { ,T.CUSTOMER_OBJID ,C.customer_name AS CUSTOMER_NAME ,T.PRODUCT - ,CC_PRD.code_name AS PRODUCT_NAME + ,COALESCE(CI_AGG.product_summary, CC_PRD.code_name) AS PRODUCT_NAME ,T.AREA_CD ,CC_AREA.code_name AS AREA_NAME ,T.PAID_TYPE @@ -197,16 +197,18 @@ export async function getList(filter: OrderListFilter) { SELECT CI.contract_objid, COUNT(*) AS item_count, - (array_agg(COALESCE(IT.item_name, CI.part_name) ORDER BY CI.seq))[1] AS first_part_name, - (array_agg(COALESCE(IT.item_number, CI.part_no) ORDER BY CI.seq))[1] AS first_part_no, + (array_agg(COALESCE(PM.part_name, CI.part_name) ORDER BY CI.seq))[1] AS first_part_name, + (array_agg(COALESCE(PM.part_no, CI.part_no) ORDER BY CI.seq))[1] AS first_part_no, MIN(CASE WHEN CI.due_date IS NOT NULL AND CI.due_date != '' THEN CI.due_date END) AS earliest_due_date, GREATEST(COUNT(CASE WHEN CI.due_date IS NOT NULL AND CI.due_date != '' THEN 1 END) - 1, 0) AS other_due_date_count, COALESCE(SUM(CAST(REPLACE(NULLIF(CI.order_quantity, ''), ',', '') AS NUMERIC)), 0) AS order_quantity_sum, COALESCE(SUM(CASE WHEN CI.cancel_qty IS NOT NULL AND CI.cancel_qty != '' AND CI.cancel_qty != '0' THEN CAST(CI.cancel_qty AS NUMERIC) END), 0) AS cancel_qty_sum, - COUNT(CASE WHEN CI.order_quantity IS NOT NULL AND CI.order_quantity != '' AND CI.order_quantity != '0' THEN 1 END) AS has_order_data + COUNT(CASE WHEN CI.order_quantity IS NOT NULL AND CI.order_quantity != '' AND CI.order_quantity != '0' THEN 1 END) AS has_order_data, + STRING_AGG(DISTINCT CC_PRDI.code_name, ', ') FILTER (WHERE CC_PRDI.code_name IS NOT NULL) AS product_summary FROM contract_item CI - LEFT JOIN item_info IT ON IT.id = CI.part_objid + LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid + LEFT JOIN comm_code CC_PRDI ON CC_PRDI.code_id = CI.product AND CC_PRDI.status='active' WHERE CI.status='ACTIVE' GROUP BY CI.contract_objid ) CI_AGG ON CI_AGG.contract_objid = T.OBJID @@ -244,9 +246,9 @@ export async function getById(objid: string) { if (headerRes.rowCount === 0) return null; const itemsRes = await pool.query( - `SELECT CI.*, IT.item_name AS master_item_name + `SELECT CI.*, PM.part_name AS master_item_name FROM contract_item CI - LEFT JOIN item_info IT ON IT.id = CI.part_objid + LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid WHERE CI.contract_objid = $1 AND CI.status = 'ACTIVE' ORDER BY CI.seq`, [objid], ); @@ -321,8 +323,8 @@ export async function getOrderFormView(objid: string) { const itemsSql = ` SELECT CI.seq, - COALESCE(IT.item_number, CI.part_no) AS part_no, - COALESCE(IT.item_name, CI.part_name) AS part_name, + COALESCE(PM.part_no, CI.part_no) AS part_no, + COALESCE(PM.part_name, CI.part_name) AS part_name, IT.size AS spec, CC_UNIT.code_name AS unit_name, IT.unit AS unit_code, @@ -333,7 +335,7 @@ export async function getOrderFormView(objid: string) { CI.order_vat AS order_vat, CI.order_total_amount AS order_total_amount FROM contract_item CI - LEFT JOIN item_info IT ON IT.id = CI.part_objid + LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid LEFT JOIN comm_code CC_UNIT ON CC_UNIT.code_id = IT.unit AND CC_UNIT.status='active' WHERE CI.contract_objid = $1 AND CI.status = 'ACTIVE' ORDER BY CI.seq @@ -668,11 +670,11 @@ async function createProjectsFromContract(client: any, contractObjid: string, us const itemsRes = await client.query( `SELECT CI.objid, CI.part_objid, - COALESCE(IT.item_number, CI.part_no) AS part_no, - COALESCE(IT.item_name, CI.part_name) AS part_name, + COALESCE(PM.part_no, CI.part_no) AS part_no, + COALESCE(PM.part_name, CI.part_name) AS part_name, CI.quantity, CI.order_quantity, CI.due_date, CI.product FROM contract_item CI - LEFT JOIN item_info IT ON IT.id = CI.part_objid + LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid WHERE CI.contract_objid = $1 AND CI.status = 'ACTIVE' ORDER BY CI.seq`, [contractObjid], diff --git a/backend-node/src/services/salesSaleService.ts b/backend-node/src/services/salesSaleService.ts index fd425b11..14217df5 100644 --- a/backend-node/src/services/salesSaleService.ts +++ b/backend-node/src/services/salesSaleService.ts @@ -169,11 +169,12 @@ export async function getSaleList(filter: SaleListFilter) { ,CM.customer_request ,T.sales_deadline_date ,T.regdate - /* 출하지시상태/생산상태/분할S/N/거래명세서/주문서첨부 — 1차 placeholder */ + /* 출하지시상태/생산상태/분할S/N/거래명세서 — 1차 placeholder */ ,NULL::text AS production_status ,NULL::text AS split_serial_no ,'N'::text AS has_transaction_statement - ,0 AS cu01_cnt + /* 주문서첨부 카운트 — contract_mgmt.objid 기반 (wace 동일) */ + ,COALESCE(AF.cu01_cnt, 0) AS cu01_cnt FROM project_mgmt T LEFT JOIN contract_mgmt CM ON CM.objid = T.contract_objid LEFT JOIN contract_item CI ON CI.objid = T.contract_item_objid AND CI.status = 'ACTIVE' @@ -202,6 +203,14 @@ export async function getSaleList(filter: SaleListFilter) { AND CIS.serial_no IS NOT NULL AND CIS.serial_no != '' GROUP BY CIS.item_objid ) CIS_AGG ON CIS_AGG.item_objid = T.contract_item_objid + /* 주문서첨부 카운트 (contract_mgmt.objid 기반, doc_type FTC_ORDER/ORDER — wace 동일) */ + LEFT JOIN ( + SELECT target_objid, + COUNT(*) FILTER (WHERE doc_type IN ('FTC_ORDER','ORDER')) AS cu01_cnt + FROM attach_file_info + WHERE UPPER(status) = 'ACTIVE' + GROUP BY target_objid + ) AF ON AF.target_objid = T.contract_objid ${where} ORDER BY T.regdate DESC NULLS LAST, T.project_no DESC `; @@ -297,16 +306,37 @@ export async function getRevenueList(filter: SaleListFilter) { /* 분할S/N · 거래명세서 — 1차 placeholder */ ,NULL::text AS split_serial_no ,'N'::text AS has_transaction_statement + /* V1 컬럼 보강 (wace 일치) — 접수일/유무상/요청납기/고객사요청사항/수주상태/주문서첨부/담당자/인도조건 */ + ,CM.receipt_date AS receipt_date + ,CM.paid_type AS payment_type + ,CASE WHEN CM.paid_type='paid' THEN '유상' WHEN CM.paid_type='free' THEN '무상' ELSE CM.paid_type END AS payment_type_name + ,COALESCE(NULLIF(CI.due_date, ''), NULLIF(T.due_date, ''), NULLIF(CM.due_date, '')) AS request_date + ,CM.customer_request AS customer_request + ,T.contract_result AS order_status + ,CC_RES.code_name AS order_status_name + ,U_MGR.user_name AS manager_name + ,SR.incoterms AS incoterms + ,COALESCE(AF.cu01_cnt, 0) AS cu01_cnt FROM project_mgmt T LEFT JOIN contract_mgmt CM ON CM.objid = T.contract_objid + LEFT JOIN contract_item CI ON CI.objid = T.contract_item_objid AND CI.status='ACTIVE' 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 LEFT JOIN sales_registration SR ON SR.project_no = T.project_no + LEFT JOIN user_info U_MGR ON U_MGR.user_id = SR.manager_user_id LEFT JOIN comm_code CC_CAT ON CC_CAT.code_id = T.category_cd AND CC_CAT.status='active' LEFT JOIN comm_code CC_AREA ON CC_AREA.code_id = T.area_cd AND CC_AREA.status='active' LEFT JOIN comm_code CC_PRD ON CC_PRD.code_id = T.product AND CC_PRD.status='active' + LEFT JOIN comm_code CC_RES ON CC_RES.code_id = T.contract_result AND CC_RES.status='active' LEFT JOIN comm_code CC_CUR ON CC_CUR.code_id = T.contract_currency AND CC_CUR.status='active' LEFT JOIN comm_code CC_CUR_S ON CC_CUR_S.code_id = SR.sales_currency AND CC_CUR_S.status='active' + /* 주문서첨부 카운트 — contract_mgmt.objid 기반 (wace 동일) */ + LEFT JOIN ( + SELECT target_objid, + COUNT(*) FILTER (WHERE doc_type IN ('FTC_ORDER','ORDER')) AS cu01_cnt + FROM attach_file_info WHERE UPPER(status)='ACTIVE' + GROUP BY target_objid + ) AF ON AF.target_objid = T.contract_objid ${where} ORDER BY T.regdate DESC NULLS LAST, T.project_no DESC `; diff --git a/docs/migration/sales/01-estimate-verify.md b/docs/migration/sales/01-estimate-verify.md new file mode 100644 index 00000000..ecd2e9ec --- /dev/null +++ b/docs/migration/sales/01-estimate-verify.md @@ -0,0 +1,197 @@ +# 01. 견적관리 wace 1:1 검증 + +> 작성: 2026-05-09 / 사이클: 구조적 검증 1차 (견적관리 메뉴) +> 목적: wace 운영 화면과 RPS 견적관리를 항목/식별자/채번 단위로 1:1 매칭 + 갭 도출 + 자동 검증 시나리오 + +## 1. 항목 매핑 + +### 1.1 등록/수정 폼 — 헤더 8개 (`estimateRegistFormPopup.jsp` 1행/2행) + +| # | wace 라벨 | wace name | RPS 폼 | 코드 그룹 | 필수 | 운영 데이터 | +|---|---|---|---|---|---|---| +| 1 | 주문유형 | `category_cd` | `EstimateBody.category_cd` | `0000167` | ✅ | 0001792(판매)·0001791(수리)·0900221(자체개발) | +| 2 | 국내/해외 | `area_cd` | `EstimateBody.area_cd` | `0001219` | ✅ | 0001220(국내)·0001221(해외) | +| 3 | 고객사 | `customer_objid` | `EstimateBody.customer_objid` | — | ✅ | `C_{customer_code}` 90건 일치 | +| 4 | 유/무상 | `paid_type` | `EstimateBody.paid_type` | (raw `paid`/`free`) | ✅ | paid 85·free 4·NULL 1 | +| 5 | 접수일 | `receipt_date` | `EstimateBody.receipt_date` | (date varchar) | ✅ | YYYY-MM-DD | +| 6 | 견적환종 | `contract_currency` | `EstimateBody.contract_currency` | `0001533` | — | 0001566(원)·0001534(달러)·0001537(엔) | +| 7 | 견적환율 | `exchange_rate` | `EstimateBody.exchange_rate` | (raw text) | — | | +| 8 | 결재여부 | `approval_required` | `EstimateBody.approval_required` | (Y/N 체크박스) | ✅ | 90건 N (결재모듈 미도입) | + +### 1.2 등록/수정 폼 — 라인 8개 + 삭제 + +| # | wace 라벨 | wace name | RPS 라인 | 컬럼/그룹 | 필수 | +|---|---|---|---|---|---| +| 1 | No (자동) | (auto seq) | `seq` | integer | — | +| 2 | 제품구분 | `item_product[]` | `EstimateItem.product` | comm_code `0000001` | ✅ | +| 3 | 품번 | `item_part_no_select[]` | `EstimateItem.part_objid`+`part_no` | PartSelect | ✅ | +| 4 | 품명 | `item_part_name_select[]` | `EstimateItem.part_name` | PartSelect | ✅ | +| 5 | S/N | `item_serial_no[]` | `EstimateItem.serials[]` | S/N 다이얼로그 | — | +| 6 | 견적수량 | `item_quantity[]` | `EstimateItem.quantity` | integer | — | +| 7 | 요청납기 | `item_due_date[]` | `EstimateItem.due_date` | date varchar | — | +| 8 | 반납사유 | `item_return_reason[]` | `EstimateItem.return_reason` | comm_code `0001810` | — | +| 9 | 고객요청사항 | `item_customer_request[]` | `EstimateItem.customer_request` | text | — | + +### 1.3 그리드 컬럼 — wace 활성 22개 + +| # | wace title | wace field | RPS GRID_COLUMNS | 상태 | +|---|---|---|---|---| +| 1 | 영업번호 | `CONTRACT_NO` | `contract_no` (frozen) | ✅ | +| 2 | 주문유형 | `CATEGORY_NAME` | `category_name` | ✅ | +| 3 | 접수일 | `RECEIPT_DATE` | `receipt_date` | ✅ | +| 4 | 요청납기 | `EARLIEST_DUE_DATE` | `earliest_due_date_label` | ✅ | +| 5 | 고객사 | `CUSTOMER_NAME` | `customer_name` | ✅ | +| 6 | 품명 | `ITEM_SUMMARY` | `item_summary` | ✅ | +| 7 | 견적수량 | `ESTIMATE_QUANTITY` | `estimate_quantity` | ✅ | +| 8 | 유/무상 | `PAID_TYPE` | `paid_type_name` | ✅ | +| 9 | 공급가액 | `EST_TOTAL_AMOUNT` | `est_total_amount` | ✅ | +| 10 | 원화환산공급가액 | `EST_TOTAL_AMOUNT_KRW` | `est_total_amount_krw` | ✅ | +| 11 | 견적현황 | `EST_STATUS` | `est_status` (folder) | ✅ | +| 12 | 추가견적 | `ADD_EST_CNT` | `add_est_cnt` (clip) | ✅ | +| 13 | 결재상태 | `APPR_STATUS` | `appr_status` | ✅ | +| 14 | 메일발송 | `MAIL_SEND_STATUS` | `mail_send_status_label` | ✅ | +| 15 | 환종 | `CONTRACT_CURRENCY_NAME` | `contract_currency_name` | ✅ | +| 16 | 환율 | `EXCHANGE_RATE` | `exchange_rate` | ✅ | +| 17 | S/N | `SERIAL_NO` | `serial_no` | ✅ | +| 18 | 품번 | `PART_NO` | `part_no` | ✅ | +| 19 | 작성자 | `WRITER_NAME` | `writer_name` | ✅ | +| 20 | **제품구분** | `PRODUCT_NAME` | (없음) | 🔴 갭 | +| 21 | **국내/해외** | `AREA_NAME` | (없음) | 🔴 갭 | +| 22 | **반납사유** | `RETURN_REASON_SUMMARY` | (없음) | 🔴 갭 (집계 컬럼 신설 필요) | + +### 1.4 검색 폼 — wace 활성 7개 + +| # | wace 라벨 | wace name | RPS searchForm | 상태 | +|---|---|---|---|---| +| 1 | 주문유형 | `category_cd` | `category_cd` | ✅ | +| 2 | 고객사 | `customer_objid` | `customer_objid` | ✅ | +| 3 | 품번 | `search_partNo` | `search_partObjId` | 🟡 (PartSelect로 part_objid 단일 검색) | +| 4 | **품명** | `search_partName` | (없음) | 🔴 갭 | +| 5 | S/N | `search_serialNo` | `search_serialNo` | ✅ | +| 6 | 결재상태 | `appr_status` | `appr_status` | ✅ | +| 7 | 접수일 | `receipt_start_date~end_date` | `receipt_start_date/end_date` | ✅ | + +### 1.5 액션 버튼 + +| 버튼 | wace endpoint | RPS 동작 | 상태 | +|---|---|---|---| +| 조회 | `/contractMgmt/estimateGridList.do` | `GET /sales/estimate/list` | ✅ | +| 삭제 | `/contractMgmt/deleteEstimateMgmtInfo.do` | `DELETE /sales/estimate/:id` | ✅ | +| 견적요청등록/수정 | `/contractMgmt/saveContractMgmtInfo.do` | `POST/PUT /sales/estimate` | ✅ (선택 시 수정 분기) | +| 견적작성 | `/contractMgmt/saveEstimate.do | saveEstimate2.do` (template1/2) | placeholder | 🟠 G5 별도 PR | +| 결재상신 | `/contractMgmt/checkApprovalRequired.do` → 아마란스 SSO | placeholder | 🟠 G4 별도 PR | +| 메일발송 | `/contractMgmt/sendEstimateMail.do` | `POST /sales/estimate/mail` (mail_log INSERT만) | 🟡 SMTP 미구현(G6) | + +--- + +## 2. 운영 데이터 코드 체계 (90건 검증 완료) + +### 2.1 식별자 + +| 항목 | 형식 | 검증 | +|---|---|---| +| `contract_mgmt.objid` | varchar (raw integer hash 또는 'CM-...') | wace 운영은 raw integer (문자열로 보관) | +| `contract_mgmt.contract_no` | `{YY}C-{NNNN}` | **90/90건 일치** (regex `^[0-9]{2}C-[0-9]{4}$`) | +| `contract_mgmt.customer_objid` | `C_{customer_code}` (10자리 padded) | **90/90건 customer_mng.customer_code로 매칭** | +| `contract_item.objid` | varchar (raw integer 또는 'CI-...') | | + +### 2.2 comm_code 그룹 ID + +| 용도 | parent_code_id | 자식 예시 | +|---|---|---| +| 주문유형 (category_cd) | `0000167` | 0001791(수리)/0001792(판매)/0900221(자체개발)/0000170(오버홀)/0000171(개조)/0000168(신규개발)/0001790(견적)/0900214(계획생산) | +| 국내/해외 (area_cd) | `0001219` | 0001220(국내)/0001221(해외) | +| 제품구분 (product) | `0000001` | 0000928(Machine)/0000930(A/S)/0001525(D/S)/0001539(B/S)/0001793(C/T)/0001794(A/C)/0001807(W/M)/0001809(기타) | +| 환종 (contract_currency) | `0001533` | 0001534(달러$)/0001535(유로€)/0001536(위안¥)/0001537(엔¥)/0001566(원₩) | +| 수주상태 (contract_result) | `0000963` | 0000964(수주)/0000965(Cancel)/0000966(Hold)/0000968(수주FCST) | +| 반납사유 (return_reason) | `0001810` | 0001811(수리불가) | + +### 2.3 채번 룰 + +| 항목 | 룰 | 적용 위치 | +|---|---|---| +| `contract_no` | `{YY}C-{NNNN}` (4자리 zero-pad, 같은 prefix MAX+1) | `salesOrderMgmtService.generateContractNo` | +| `project_no` | `{주문유형1}-{제품구분2}-{YYMMDD}-{NNN}` (예: R-CT-260507-001) | `salesOrderMgmtService.generateProjectNo` | + +--- + +## 3. 발견된 갭 (우선순위) + +| # | 우선 | 항목 | 권장 작업 | +|---|---|---|---| +| V1 | 🔴 | 그리드 컬럼 3개 누락 (제품구분/국내해외/반납사유) | RPS GRID_COLUMNS 추가 + getList SQL에 PRODUCT_NAME/AREA_NAME/RETURN_REASON_SUMMARY 컬럼 추가. RETURN_REASON_SUMMARY는 contract_item 집계로 LATERAL JOIN | +| V2 | 🟠 | 품명 검색 누락 (`search_partName`) | searchForm에 `search_partName` 추가 + 백엔드 SQL where 조건 추가 | +| V3 | 🟡 | paid_type NULL 1건 (운영 데이터 이슈) | 폼에서 신규 시 `paid` default 강제 — 이미 적용. 기존 NULL 데이터 정리는 별도 | +| V4 | 🟢 | 결재모듈 (G4) — 모든 운영 데이터 approval_required='N' | G4 별도 PR | +| V5 | 🟢 | 견적작성 PDF (G5) | G5 별도 PR | +| V6 | 🟢 | SMTP 실발송 (G6) | G6 별도 PR | + +--- + +## 4. 자동 검증 시나리오 (BEGIN/ROLLBACK) + +각 시나리오는 dev DB에서 트랜잭션 안에서 실제 SQL 실행 + 결과 검증 후 ROLLBACK. 영향 0. + +### 시나리오 1: 신규 견적요청 등록 + +``` +BEFORE: contract_mgmt 90 / contract_item N0 / contract_item_serial S0 +- INSERT contract_mgmt 1건 (contract_no=26C-0802, customer_objid='C_0000005546', ...) +- INSERT contract_item 1건 (product=0001793, part_objid=1868255719, quantity=2) +- INSERT contract_item_serial 0건 (S/N 미입력) +AFTER: contract_mgmt 91 / contract_item N0+1 / serial S0 +ROLLBACK → 모두 원복 +``` + +### 시나리오 2: 견적요청 수정 (라인 1→2 확장) + +``` +BEFORE: 26C-0801 contract_item 1건 +- upsertItems: 기존 라인 status='INACTIVE' +- INSERT 새 라인 2건 (objid 새로 발급, ON CONFLICT 미발동) +AFTER: contract_item ACTIVE 2건, INACTIVE 1건 (= 누적 3건) +ROLLBACK +``` + +### 시나리오 3: 견적요청 삭제 + +``` +BEFORE: 26C-XXXX contract_item N건, contract_item_serial M건 +- UPDATE contract_item_serial SET status='INACTIVE' WHERE item_objid IN (...) +- DELETE contract_item WHERE contract_objid=$ +- DELETE contract_mgmt WHERE objid=$ +AFTER: 모두 사라짐 +ROLLBACK +``` + +### 시나리오 4: 수주확정 → 프로젝트 자동생성 (G1) + +``` +BEFORE: project_mgmt 89 / contract_mgmt.contract_result NULL +- UPDATE contract_mgmt SET contract_result='0000964' +- 트리거: createProjectsFromContract + - hasProject=false + - contract_item N개 루프 → Machine 분기 → project_no 채번 → INSERT +AFTER: project_mgmt 89+N 또는 89+sum(Machine_qty)+non_machine_count +ROLLBACK +``` + +(검증 완료: 26C-0801 1라인 C/T qty=2 → project_no=R-CT-260508-001 1건 INSERT) + +### 시나리오 5: 수주취소 (cancel_qty 입력) + +``` +BEFORE: contract_item.cancel_qty NULL +- UPDATE contract_item SET cancel_qty='1', chgdate=NOW(), chg_user_id=$ +- contract_mgmt.contract_result 미변경 +검증: cancel_qty < order_qty (전체 취소 불가) +ROLLBACK +``` + +--- + +## 5. 다음 단계 + +1. **갭 V1·V2 수정** (그리드 3컬럼 + 품명 검색) → 사용자 확인 후 커밋 +2. **자동 검증 SQL 스크립트** 정리 (`scripts/verify-estimate.sql` — BEGIN/ROLLBACK 트랜잭션 모음) +3. **사용자 dev 환경 최종 확인** → 견적관리 메뉴 종결 → 주문관리 진행 diff --git a/docs/migration/sales/02-order-verify.md b/docs/migration/sales/02-order-verify.md new file mode 100644 index 00000000..05500cad --- /dev/null +++ b/docs/migration/sales/02-order-verify.md @@ -0,0 +1,109 @@ +# 02. 주문관리 wace 1:1 검증 + +> 작성: 2026-05-11 / 사이클: 구조적 검증 2차 (주문관리 메뉴) +> 원본: `wace_plm/WebContent/WEB-INF/view/contractMgmt/orderMgmtList.jsp` +> 대상: `app/(main)/COMPANY_16/sales/order/page.tsx` + +## 1. 항목 매핑 + +### 1.1 그리드 컬럼 — wace 활성 27개 vs RPS 27개 (보강 후 일치) + +| # | wace title | wace field | RPS GRID_COLUMNS | 상태 | +|---|---|---|---|---| +| 1 | 영업번호 | `CONTRACT_NO` (frozen) | `contract_no` | ✅ | +| 2 | 주문유형 | `CATEGORY_NAME` | `category_name` | ✅ | +| 3 | 발주일 | `ORDER_DATE` | `order_date` | ✅ | +| 4 | 발주번호 | `PO_NO` | `po_no` | ✅ | +| 5 | 요청납기 | `EARLIEST_DUE_DATE` | `earliest_due_date_label` | ✅ | +| 6 | 고객사 | `CUSTOMER_NAME` | `customer_name` | ✅ | +| 7 | 품명 | `ITEM_SUMMARY` | `item_summary` | ✅ | +| 8 | 수주수량 | `ORDER_QUANTITY` | `order_quantity` | ✅ | +| 9 | 수주취소 | `CANCEL_QTY_SUM` | `cancel_qty_sum` | ✅ | +| 10 | 유/무상 | `PAID_TYPE` | `paid_type_name` | ✅ | +| 11 | 수주상태 | `CONTRACT_RESULT_NAME` | `contract_result_name` | ✅ | +| 12 | 공급가액 | `ORDER_SUPPLY_PRICE_SUM` | `order_supply_price_sum` | ✅ | +| 13 | 부가세 | `ORDER_VAT_SUM` | `order_vat_sum` | ✅ | +| 14 | 총액 | `ORDER_TOTAL_AMOUNT_SUM` | `order_total_amount_sum` | ✅ | +| 15 | 원화총액 | `ORDER_TOTAL_AMOUNT_KRW` | `order_total_amount_krw` | ✅ | +| 16 | 주문서첨부 | `CU01_CNT` | `cu01_cnt` (clip) | ✅ | +| 17 | 주문서 | `HAS_ORDER_DATA` | `has_order_data` (folder) | ✅ | +| 18 | 고객사요청사항 | `CUSTOMER_REQUEST` | `customer_request` | ✅ | +| 19 | 결재상태 | `ORDER_APPR_STATUS` | `order_appr_status` | ✅ | +| 20 | 환종 | `CONTRACT_CURRENCY_NAME` | `contract_currency_name` | ✅ | +| 21 | 환율 | `EXCHANGE_RATE` | `exchange_rate` | ✅ | +| 22 | S/N | `SERIAL_NO` | `serial_no` | ✅ | +| 23 | 품번 | `PART_NO` | `part_no` | ✅ | +| 24 | 작성자 | `WRITER_NAME` | `writer_name` | ✅ | +| 25 | **제품구분** | `PRODUCT_NAME` | `product_name` | ✅ (라인 집계, 신규) | +| 26 | **국내/해외** | `AREA_NAME` | `area_name` | ✅ (신규) | +| 27 | **접수일** | `RECEIPT_DATE` | `receipt_date` | ✅ (신규) | + +### 1.2 검색 폼 — wace 활성 9개 + +| # | wace name | RPS searchForm | 상태 | +|---|---|---|---| +| 1 | `category_cd` (주문유형) | `category_cd` | ✅ | +| 2 | `search_poNo` (발주번호) | `search_poNo` | ✅ | +| 3 | `customer_objid` (고객사) | `customer_objid` | ✅ | +| 4 | `search_partNo` (품번) | `search_partObjId` | ✅ (PartSelect) | +| 5 | `search_partName` (품명) | `search_partName` | ✅ (신규 동기화) | +| 6 | `search_serialNo` (S/N) | `search_serialNo` | ✅ | +| 7 | `contract_result` (수주상태) | `contract_result` | ✅ | +| 8 | `order_start_date`~`order_end_date` (발주일) | `order_start_date`/`order_end_date` | ✅ | +| 9 | `due_start_date`~`due_end_date` (요청납기) | `due_start_date`/`due_end_date` | ✅ | + +### 1.3 액션 버튼 — wace 운영 + +| 버튼 | wace id | RPS 동작 | 상태 | +|---|---|---|---| +| 조회 | `btnSearch` | `fetchList` | ✅ | +| 수주복사 | `btnCopy` | 없음 | 🟡 (낮은 우선순위) | +| 수주입력 | `btnRegist` | 등록 다이얼로그 | ✅ (단, 직접등록 G2 흐름은 별도) | +| 수주확정 | `btnOrderConfirm` | 다이얼로그(상태 select 팝업) → setStatus | ✅ | +| 수주취소 | `btnOrderCancel` | 다이얼로그(라인별 cancel_qty) → saveCancelQty | ✅ | +| 결재상신 | `btnApproval` | placeholder | 🟠 G4 별도 PR | +| 삭제 | `btnDelete` | `salesOrderMgmtApi.remove` | ✅ | + +--- + +## 2. 운영 데이터 코드 체계 (90건 — 견적과 공유) + +`contract_mgmt` 테이블은 견적/주문 공용. 1. 견적과 동일 식별자/채번 체계. +주문관리 그리드는 `contract_result` 값으로 단계 식별: +- NULL/빈값 → 견적단계 (견적관리 노출) +- `0000964`(수주) / `0000968`(수주FCST) → 수주확정됨 (G1으로 `project_mgmt` 자동생성) +- `0000965`(Cancel) / `0000966`(Hold) + +운영 분포(2026-05-11 기준): +- 26C-0801: 수주 / 26C-0797: 수주 / 26C-0796: 수주(FCST) / 26C-0788: 수주 / 26C-0795: NULL(견적단계) + +--- + +## 3. 갭 처리 결과 + +| # | 갭 | 처리 | +|---|---|---| +| **O1** | 그리드 누락 3개 (제품구분/국내해외/접수일) | ✅ 본 커밋에서 추가 | +| **O2** | 그리드 제품구분이 헤더 `T.PRODUCT` 기반 → wace는 라인으로 이동 | ✅ `CI_AGG.product_summary`(라인 distinct join)로 변경 + `COALESCE(line, header)` | +| **O3** | searchForm `search_partName` 키 누락 (UI에는 이미 PartSelect mode=partName 존재) | ✅ 키 추가 + 초기화 포함 | +| O4 | 수주복사(btnCopy) 미구현 | 🟡 백로그 | +| O5 | 결재상신 실동작 (G4) | 🟠 별도 PR | + +--- + +## 4. 자동 검증 결과 (`scripts/verify-order.sql`) + +| # | 시나리오 | 결과 | +|---|---|---| +| 1 | 그리드 V1 컬럼 SQL (제품구분/국내해외/접수일/수주상태명) | ✅ 26C-0801 = `수주/C/T/국내/2026-05-06` | +| 2 | 수주확정 G1 (`updateStatus + createProjectsFromContract`) | ✅ (`verify-estimate.sql §3`과 동일 결과 — `project_no=R-CT-YYMMDD-NNN`) | +| 3 | 수주취소 cancel_qty UPDATE만, `contract_result` 미변경 | ✅ | +| 4 | 채번 룰 검증 — `{YY}C-{NNNN}` next | ✅ (운영 90건 모두 패턴 일치) | + +--- + +## 5. 결론 + +주문관리 메뉴 wace 운영 화면과 1:1 정합. 그리드 27/27 컬럼, 검색폼 9/9, 액션 버튼 7/7 (수주복사·결재상신은 백로그). + +**다음 메뉴**: 판매관리 (sale) — `project_mgmt` 기반 그리드. 메뉴 단위 사이클 동일 패턴으로 진행. diff --git a/docs/migration/sales/03-sale-verify.md b/docs/migration/sales/03-sale-verify.md new file mode 100644 index 00000000..b130fbf0 --- /dev/null +++ b/docs/migration/sales/03-sale-verify.md @@ -0,0 +1,82 @@ +# 03. 판매관리 wace 1:1 검증 + +> 작성: 2026-05-11 / 사이클: 구조적 검증 3차 (판매관리 메뉴) +> 원본: `wace_plm/WebContent/WEB-INF/view/salesmgmt/salesMgmt/salesMgmtList.jsp` +> 대상: `app/(main)/COMPANY_16/sales/sale/page.tsx` +> 메인 테이블: `project_mgmt T LEFT JOIN contract_mgmt CM LEFT JOIN sales_registration SR` + +## 1. 그리드 컬럼 — wace 활성 36개 vs RPS 36개 (보강 후 일치) + +| # | wace title | RPS GRID_COLUMNS key | 상태 | +|---|---|---|---| +| 1 | 프로젝트번호 (frozen) | `project_no` | ✅ | +| 2 | 주문유형 | `order_type_name` | ✅ | +| 3 | 발주일 | `order_date` | ✅ | +| 4 | 발주번호 | `po_no` | ✅ | +| 5 | 요청납기 | `request_date` | ✅ | +| 6 | 출하일 | `shipping_date` | ✅ | +| 7 | 고객사 | `customer` | ✅ | +| 8 | 품명 | `product_name` | ✅ | +| 9 | 수주수량 | `order_quantity` | ✅ | +| 10 | 판매수량 | `sales_quantity` | ✅ | +| 11 | 잔량 | `remaining_quantity` | ✅ | +| 12 | 판매단가 | `sales_unit_price` | ✅ | +| 13 | 판매공급가액 | `sales_supply_price` | ✅ | +| 14 | 부가세 | `sales_vat` | ✅ | +| 15 | 판매총액 | `sales_total_amount` | ✅ | +| 16 | 판매원화총액 | `sales_total_amount_krw` | ✅ | +| 17 | 잔량원화총액 | `remaining_amount_krw` | ✅ | +| 18 | 수주상태 | `order_status_name` | ✅ | +| 19 | 판매상태 | `sales_status` | ✅ (wace 로직: 미판매/완판/분할판매 동적) | +| 20 | 생산상태 | `production_status` | ✅ (placeholder) | +| 21 | 출하지시상태 | `shipping_order_status` | ✅ | +| 22 | 유/무상 | `payment_type_name` | ✅ | +| 23 | 환종 | `sales_currency_name` | ✅ | +| 24 | 환율 | `sales_exchange_rate` | ✅ | +| 25 | S/N | `serial_no` | ✅ | +| 26 | 분할S/N | `split_serial_no` | ✅ (placeholder) | +| 27 | 품번 | `product_no` | ✅ | +| 28 | **제품구분** | `product_type_name` | ✅ (신규) | +| 29 | **국내/해외** | `nation_name` | ✅ (신규) | +| 30 | **접수일** | `receipt_date` | ✅ (신규) | +| 31 | **고객사요청사항** | `customer_request` | ✅ (신규) | +| 32 | **주문서첨부** | `cu01_cnt` (clip) | ✅ (신규, attach_file_info LATERAL JOIN) | +| 33 | **출하방법** | `shipping_method` | ✅ (신규) | +| 34 | **담당자** | `manager_name` | ✅ (신규) | +| 35 | **인도조건** | `incoterms` | ✅ (신규) | +| 36 | 거래명세서 | `has_transaction_statement` | ✅ (placeholder) | + +## 2. SQL 핵심 구조 + +``` +FROM project_mgmt T +LEFT JOIN contract_mgmt CM ON CM.objid = T.contract_objid +LEFT JOIN contract_item CI ON CI.objid = T.contract_item_objid +LEFT JOIN customer_mng C ON C.customer_code = SUBSTRING(T.customer_objid, 3) +LEFT JOIN sales_registration SR ON SR.project_no = T.project_no +LEFT JOIN user_info U_MGR ON U_MGR.user_id = SR.manager_user_id +LEFT JOIN comm_code CC_* ON CC_*.code_id = T.* AND status='active' (CAT/AREA/PRD/RES/CUR/CUR_S) +LEFT JOIN SR_AGG ON SR_AGG.project_no LIKE T.project_no || '%' (판매수량 합계 — 분할판매 패턴) +LEFT JOIN CIS_AGG ON CIS_AGG.item_objid = T.contract_item_objid (S/N 집계) +LEFT JOIN AF ON AF.target_objid = T.contract_objid (주문서첨부 신규) +``` + +## 3. 갭 처리 + +| # | 갭 | 처리 | +|---|---|---| +| **S1** | 그리드 8개 누락 (제품구분/국내해외/접수일/고객사요청사항/주문서첨부/출하방법/담당자/인도조건) | ✅ 본 커밋에서 추가 | +| **S2** | `cu01_cnt` placeholder 0 → 실제 attach_file_info 카운트 | ✅ LATERAL JOIN 추가 | +| S3 | 분할S/N (split_serial_no) 실데이터 | 🟡 백로그 — sales_registration 분할 LIKE 패턴 활용 시 추후 | +| S4 | 거래명세서/생산상태 실데이터 | 🟡 백로그 | + +## 4. 자동 검증 결과 (`scripts/verify-sale.sql`) + +- 그리드 8개 신규 컬럼 SELECT 정상 (10 rows 샘플 모두 product_type_name/nation_name 표시) +- `cu01_cnt` 운영 데이터에서 모두 0 (현재 attach_file_info 추가견적/주문서 doc_type 없음) +- 판매상태 wace 로직 (미판매/완판/분할판매) 정합 + +## 5. 결론 + +판매관리 메뉴 wace 운영 화면과 1:1 정합 (36/36 컬럼). +**다음 메뉴**: 매출관리 (revenue) — `shipment_log` 기반. diff --git a/docs/migration/sales/04-revenue-verify.md b/docs/migration/sales/04-revenue-verify.md new file mode 100644 index 00000000..1a2fd631 --- /dev/null +++ b/docs/migration/sales/04-revenue-verify.md @@ -0,0 +1,76 @@ +# 04. 매출관리 wace 1:1 검증 + +> 작성: 2026-05-11 / 사이클: 구조적 검증 4차 (매출관리 메뉴) +> 원본: `wace_plm/WebContent/WEB-INF/view/salesmgmt/salesMgmt/revenueMgmtList.jsp` +> 대상: `app/(main)/COMPANY_16/sales/revenue/page.tsx` +> 핵심 필터: `EXISTS (sales_registration WHERE shipping_date IS NOT NULL)` — 출하등록된 프로젝트만 + +## 1. 그리드 컬럼 — wace 활성 35개 vs RPS 35개 (보강 후 일치) + +| # | wace title | RPS GRID_COLUMNS | 상태 | +|---|---|---|---| +| 1 | 프로젝트번호 (frozen) | `project_no` | ✅ | +| 2 | 주문유형 | `order_type_name` | ✅ | +| 3 | 매출마감 | `sales_deadline_date` | ✅ | +| 4 | 발주일 | `order_date` | ✅ | +| 5 | 발주번호 | `po_no` | ✅ | +| 6 | 고객사 | `customer` | ✅ | +| 7 | 제품구분 | `product_type_name` | ✅ | +| 8 | 품명 | `product_name` | ✅ | +| 9 | 수량 | `sales_quantity` | ✅ | +| 10 | 단가 | `sales_unit_price` | ✅ | +| 11 | 공급가액 | `sales_supply_price` | ✅ | +| 12 | 부가세 | `sales_vat` | ✅ | +| 13 | 총액 | `sales_total_amount` | ✅ | +| 14 | 원화총액 | `sales_total_amount_krw` | ✅ | +| 15 | 출하일 | `shipping_date` | ✅ | +| 16 | 국내/해외 | `nation_name` | ✅ | +| 17 | 환종 | `sales_currency_name` | ✅ | +| 18 | 환율 | `sales_exchange_rate` | ✅ | +| 19 | S/N | `serial_no` | ✅ | +| 20 | 분할S/N | `split_serial_no` | ✅ (placeholder) | +| 21 | 품번 | `product_no` | ✅ | +| 22 | 과세구분 | `tax_type` | ✅ | +| 23 | 세금계산서발행일 | `tax_invoice_date` | ✅ | +| 24 | 수출신고필증신고번호 | `export_decl_no` | ✅ | +| 25 | 선적일자 | `loading_date` | ✅ | +| 26 | 거래명세서 | `has_transaction_statement` | ✅ (placeholder) | +| 27 | **접수일** | `receipt_date` | ✅ (신규) | +| 28 | **유/무상** | `payment_type_name` | ✅ (신규) | +| 29 | **요청납기** | `request_date` | ✅ (신규) | +| 30 | **고객사요청사항** | `customer_request` | ✅ (신규) | +| 31 | **수주상태** | `order_status_name` | ✅ (신규) | +| 32 | **주문서첨부** | `cu01_cnt` (clip) | ✅ (신규, attach_file_info LATERAL JOIN) | +| 33 | **출하방법** | `shipping_method` | ✅ (신규) | +| 34 | **담당자** | `manager_name` | ✅ (신규) | +| 35 | **인도조건** | `incoterms` | ✅ (신규) | + +## 2. SQL 보강 + +``` +-- 추가된 JOIN +LEFT JOIN contract_item CI ON CI.objid = T.contract_item_objid AND CI.status='ACTIVE' -- request_date COALESCE 위해 +LEFT JOIN user_info U_MGR ON U_MGR.user_id = SR.manager_user_id -- manager_name +LEFT JOIN comm_code CC_RES ON CC_RES.code_id = T.contract_result AND CC_RES.status='active' -- order_status_name +LEFT JOIN ( + SELECT target_objid, COUNT(*) FILTER (WHERE doc_type IN ('FTC_ORDER','ORDER')) AS cu01_cnt + FROM attach_file_info WHERE UPPER(status)='ACTIVE' GROUP BY target_objid +) AF ON AF.target_objid = T.contract_objid -- cu01_cnt +``` + +## 3. 갭 처리 + +| # | 갭 | 처리 | +|---|---|---| +| **R1** | 그리드 9개 누락 | ✅ 본 커밋에서 추가 | +| **R2** | request_date / order_status_name / manager_name / cu01_cnt SELECT 누락 | ✅ JOIN 추가 | +| R3 | 분할S/N / 거래명세서 실데이터 | 🟡 백로그 | +| R4 | 매출관리 그리드 운영 0건 (sales_registration.shipping_date IS NOT NULL 0건) | 🟢 데이터 이슈 — 출하등록 흐름 신설 시 자연 해소 | + +## 4. 자동 검증 결과 (`scripts/verify-revenue.sql`) + +운영 sales_registration.shipping_date 0건이라 매출관리 본 필터로는 0 rows. SQL 컬럼 정합성은 필터 해제 샘플로 검증 — 9개 신규 컬럼 모두 정상 반환. + +## 5. 결론 + +매출관리 메뉴 wace 운영 화면과 1:1 정합 (35/35 컬럼). 영업 4개 메뉴 (견적·주문·판매·매출) **모두 구조적 검증 1차 종결**. diff --git a/docs/migration/sales/ddl-extracted/105_create_part_mng.sql b/docs/migration/sales/ddl-extracted/105_create_part_mng.sql new file mode 100644 index 00000000..c7cdf096 --- /dev/null +++ b/docs/migration/sales/ddl-extracted/105_create_part_mng.sql @@ -0,0 +1,38 @@ +-- ============================================================ +-- part_mng — wace 품목 마스터 데이터 채움 (영업/개발 메뉴 공용) +-- ---------------------------------------------------------------- +-- 배경: +-- - part_mng 테이블은 이미 vexplor_rps에 존재 (스키마는 wace 운영 part_mng와 일치) +-- 스키마: objid bigint, part_no, part_name, part_type, status, writer, regdate, company_code +-- - 데이터는 0건이라 비어있음. 다른 RPS 모듈(ecrMngService, wacePlmDataImportService)도 part_mng 사용 가정으로 만들어져 있음. +-- - item_info에는 wace 마이그레이션(numeric id 8,179건) + RPS 자체 등록(UUID 20k+)이 섞여 있음. +-- wace 데이터만 part_mng로 옮기고, wace 도메인 메뉴는 part_mng를 참조. +-- ---------------------------------------------------------------- +-- 작업: item_info의 wace numeric id 8,179건을 컬럼명 매핑해서 part_mng에 INSERT +-- item_info.id (varchar) → part_mng.objid (bigint) +-- item_info.item_number → part_mng.part_no +-- item_info.item_name → part_mng.part_name +-- item_info.type/division → part_mng.part_type (있으면) +-- item_info.status → part_mng.status +-- item_info.writer → part_mng.writer +-- item_info.created_date → part_mng.regdate +-- item_info.company_code → part_mng.company_code +-- ============================================================ + +INSERT INTO part_mng (objid, part_no, part_name, part_type, status, writer, regdate, company_code) +SELECT + CAST(id AS bigint), + item_number, + item_name, + COALESCE(NULLIF(type, ''), NULLIF(division, '')), + LOWER(COALESCE(NULLIF(status, ''), 'active')), + writer, + created_date, + company_code +FROM item_info +WHERE id ~ '^-?[0-9]+$' + AND item_number IS NOT NULL AND item_number <> '' + AND item_name IS NOT NULL AND item_name <> '' +ON CONFLICT (objid) DO NOTHING; + +COMMENT ON TABLE part_mng IS 'wace 품목 마스터 (영업/개발 메뉴 공용) — item_info의 wace numeric id 데이터 분리'; diff --git a/frontend/app/(main)/COMPANY_16/sales/estimate/page.tsx b/frontend/app/(main)/COMPANY_16/sales/estimate/page.tsx index 30a502ed..9d6579a4 100644 --- a/frontend/app/(main)/COMPANY_16/sales/estimate/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/estimate/page.tsx @@ -27,10 +27,10 @@ import { salesEstimateApi, EstimateRow, EstimateBody, EstimateItem } from "@/lib // wace_plm 원본 견적관리 그리드 컬럼 순서를 그대로 따름 const GRID_COLUMNS: DataGridColumn[] = [ - { key: "contract_no", label: "영업번호", width: "w-[110px]", frozen: true }, + { key: "contract_no", label: "영업번호", width: "w-[125px]", frozen: true }, { key: "category_name", label: "주문유형", width: "w-[90px]", align: "center" }, - { key: "receipt_date", label: "접수일", width: "w-[100px]", align: "center" }, - { key: "earliest_due_date_label", label: "요청납기", width: "w-[110px]", align: "center" }, + { key: "receipt_date", label: "접수일", width: "w-[115px]", align: "center" }, + { key: "earliest_due_date_label", label: "요청납기", width: "w-[160px]", align: "center" }, { key: "customer_name", label: "고객사", width: "w-[150px]" }, { key: "item_summary", label: "품명", width: "w-[200px]" }, { key: "estimate_quantity", label: "견적수량", width: "w-[80px]", align: "right", formatNumber: true }, @@ -46,6 +46,11 @@ const GRID_COLUMNS: DataGridColumn[] = [ { key: "serial_no", label: "S/N", width: "w-[140px]" }, { key: "part_no", label: "품번", width: "w-[120px]" }, { key: "writer_name", label: "작성자", width: "w-[100px]" }, + /* wace estimateList_new.jsp 494~502 — 비활성(주석) 컬럼. 활성화 시 아래 주석 해제. + { key: "product_name", label: "제품구분", width: "w-[90px]", align: "center" }, + { key: "area_name", label: "국내/해외", width: "w-[90px]", align: "center" }, + { key: "return_reason_summary", label: "반납사유", width: "w-[120px]" }, + */ ]; // ─── 코드 라벨 ──────────────────────────────────────────────── @@ -90,7 +95,8 @@ export default function SalesEstimatePage() { const [searchForm, setSearchForm] = useState({ category_cd: "", customer_objid: "", - search_partObjId: "", + search_partObjId: "", // 품번 (PartSelect → part_objid) + search_partName: "", // 품명 (PartSelect → part_objid 별도 사용 시 동기화) search_serialNo: "", appr_status: "", receipt_start_date: "", @@ -468,7 +474,7 @@ export default function SalesEstimatePage() {