영업관리 4개 메뉴(견적/주문/판매/매출) 1차 이식 + 마스터 매핑

- wace_plm contract_mgmt/contract_item/contract_item_serial/contract_mgmt_option/estimate_template/estimate_template_item/mail_log/sales_registration/shipment_log 9개 테이블 DDL을 vexplor_rps에 적재, 운영 데이터 복사
- 거래처: Wehago/Amaranth ERP api16S11 INBOUND 동기화 결과(customer_code) 기준 LEFT JOIN으로 변경, 25/25 매칭
- 품목: wace part_mng 8,179건을 item_info(varchar id)에 wace objid 그대로 INSERT, contract_item 72/72 매칭
- 공통코드: wace comm_code 847건 복제 + backend SQL에 5종 LEFT JOIN
- DataGrid에 formatMoney(천단위콤마+소수점2자리) / formatNumber 자동 우측정렬 분리
- adminService.getUserMenuList company_code 분기 제거(RPS 단독), useMenu.buildMenuTree root 식별 보강

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-07 15:39:03 +09:00
parent c123fd01ff
commit 1760045634
30 changed files with 4885 additions and 1901 deletions
+184
View File
@@ -0,0 +1,184 @@
# 01. 견적관리 이식 상세
> 원본: `/contractMgmt/estimateList_new.do` (estimateList_new.jsp, 47KB)
> 대상: `app/(main)/COMPANY_16/sales/estimate/page.tsx`
## 1. 화면 구조 (wace_plm 원본)
### 1.1 검색 폼 (`#plmSearchZon`)
| 필드 | name/id | 타입 | 비고 |
|---|---|---|---|
| 주문유형 | `category_cd` | select2 (공통코드) | |
| 고객사 | `customer_objid` | select2 (`SUPPLY_MNG`+`CLIENT_MNG`) | `'C_'` 접두사로 두 테이블 구분 |
| 품번 | `search_partNo` | select2-part (검색어 → API) | hidden `search_partObjId` 동행 |
| 품명 | `search_partName` | select2-part | |
| 시리얼 | `search_serialNo` | text | |
| 결재상태 | `appr_status` | select2 (대기/상신중/완료/반려/불필요) | |
| 접수일 from~to | `receipt_start_date` / `receipt_end_date` | date_icon | |
| 제품 | `product` | select2 (공통코드) | |
| 지역 | `area_cd` | select2 (공통코드) | |
| 유/무상 | `paid_type` | select2 (공통코드) | |
| 요청납기 from~to | `due_start_date` / `due_end_date` | date_icon | |
### 1.2 버튼 영역 (`.plm_btn_wrap`)
| 버튼 | id | 동작 |
|---|---|---|
| 조회 | `btnSearch` | `fn_search()``/contractMgmt/estimateGridList.do` |
| 삭제 | `btnDelete` | 선택행 → `/contractMgmt/deleteEstimateMgmtInfo.do` |
| 견적요청등록 | `.btnRegist` | 팝업: `/contractMgmt/estimateRegistFormPopup.do?actionType=regist` |
| 견적작성 | `btnEstimate` | 일반/장비 견적서 분기 → `fn_openEstimateTemplate(objId, "1"|"2")` |
| 결재상신 | `btnApproval` | 결재불필요 분기 → `/contractMgmt/checkApprovalRequired.do``fn_openAmaranthApproval` |
| 메일발송 | `btnMail` | 결재완료/불필요만 → `/contractMgmt/estimateMailFormPopup.do` |
### 1.3 그리드 (`_tabulGrid` = Tabulator)
데이터 소스: `POST /contractMgmt/estimateGridList.do` (검색 폼 직렬화)
| # | 컬럼 | field | 정렬 | 비고 |
|---|---|---|---|---|
| 0 | EST_OBJID (hidden) | EST_OBJID | | |
| 1 | 영업번호 | CONTRACT_NO | center | 클릭 → `fn_projectConceptDetail(OBJID)` |
| 2 | 주문유형 | CATEGORY_NAME | center | |
| 3 | 접수일 | RECEIPT_DATE | center | |
| 4 | 요청납기 | EARLIEST_DUE_DATE | center | "{date} 외 {n}건" 포맷 |
| 5 | 고객사 | CUSTOMER_NAME | left | |
| 6 | 품명 | ITEM_SUMMARY | left | "{itemName} 외 {n}건" 포맷 |
| 7 | 견적수량 | ESTIMATE_QUANTITY | right | toLocaleString |
| 8 | 유/무상 | PAID_TYPE | center | |
| 9 | 공급가액 | EST_TOTAL_AMOUNT | right | 통화기호 변환 (CONTRACT_CURRENCY_NAME) |
| 10 | 원화환산공급가액 | EST_TOTAL_AMOUNT_KRW | right | |
| 11 | 견적현황 | EST_STATUS | center | 클릭 → `fn_showEstimateList(OBJID)` |
| 11-1 | 추가견적(PDF 첨부수) | ADD_EST_CNT | center | 📎 클릭 → `fn_openAddEstimatePdf` |
| 12 | AMARANTH_STATUS (hidden) | AMARANTH_STATUS | | 아마란스 결재상태 |
| 13 | 결재상태 | APPR_STATUS | center | 색상 표시 |
> 16개 더 있을 수 있음 — 559라인까지 컬럼 정의 이어짐. 구현 시 [estimateList_new.jsp:342-559](../../../wace_plm/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp#L342) 직접 참고.
## 2. 백엔드 endpoint 매핑 (wace_plm → vexplor_rps)
| wace_plm endpoint | 메서드 | 용도 | vexplor_rps 신규 endpoint (제안) |
|---|---|---|---|
| `/contractMgmt/estimateList_new.do` | GET | 페이지 진입 (JSP 렌더) | (없음 — Next.js 페이지) |
| `/contractMgmt/estimateGridList.do` | POST | 그리드 데이터 조회 | `GET /api/sales/estimate/list?...` |
| `/contractMgmt/estimateRegistFormPopup.do` | GET | 견적 등록/수정 팝업 | (Next.js Dialog 컴포넌트) |
| `/contractMgmt/saveEstimateMgmtInfo.do` | POST | 견적 메타 저장 | `POST /api/sales/estimate` / `PUT /api/sales/estimate/:id` |
| `/contractMgmt/deleteEstimateMgmtInfo.do` | POST | 견적 삭제 | `DELETE /api/sales/estimate/:id` |
| `/contractMgmt/createEstimateMgmtInfo.do` | POST | 견적 신규 생성 | `POST /api/sales/estimate` 와 통합 |
| `/contractMgmt/estimateTemplate1.do` | GET | 일반 견적서 작성 화면 | `app/(pop)/sales/estimate/template1/page.tsx` |
| `/contractMgmt/estimateTemplate2.do` | GET | 장비 견적서 작성 화면 | `app/(pop)/sales/estimate/template2/page.tsx` |
| `/contractMgmt/getEstimateTemplateList.do` | POST | 템플릿 목록 | `GET /api/sales/estimate/:id/templates` |
| `/contractMgmt/getEstimateTemplateDataByObjId.do` | POST | 템플릿 단건 | `GET /api/sales/estimate/templates/:templateId` |
| `/contractMgmt/saveEstimate.do` | POST | 일반 견적서 저장 | `POST /api/sales/estimate/templates` |
| `/contractMgmt/saveEstimate2.do` | POST | 장비 견적서 저장 | 위와 통합 (type 파라미터) |
| `/contractMgmt/uploadPdfChunk.do` | POST | PDF 청크 업로드 | `POST /api/sales/estimate/templates/:id/pdf-chunk` |
| `/contractMgmt/sendEstimateMail.do` | POST | 견적 메일 발송 | `POST /api/sales/estimate/:id/mail` |
| `/contractMgmt/sendEstimateMailCustom.do` | POST | 견적 메일 발송(커스텀) | 위와 옵션 통합 |
| `/contractMgmt/estimateMailFormPopup.do` | GET | 메일 발송 팝업 | (Next.js Dialog) |
| `/contractMgmt/getContractInfoForMail.do` | POST | 메일 발송용 데이터 | `GET /api/sales/estimate/:id/mail-context` |
| `/contractMgmt/getCustomerContactInfo.do` | POST | 고객사 연락처 | `GET /api/customers/:id/contacts` (마스터 공용) |
| `/contractMgmt/getEstimateDetail.do` | POST | 견적 단건 상세 | `GET /api/sales/estimate/:id` |
| `/contractMgmt/checkApprovalRequired.do` | POST | 결재 필요 여부 체크 | `GET /api/sales/estimate/:id/approval-check` |
| `/contractMgmt/setApprovalNotRequired.do` | POST | 결재불필요 처리 | `POST /api/sales/estimate/:id/approval-skip` |
| `/contractMgmt/checkProjectExists.do` | POST | 프로젝트 존재 체크 | `GET /api/sales/estimate/check-project?...` |
| `/contractMgmt/searchPartList.do` | POST | 품목 검색 (select2) | `GET /api/items/search?q=...` (마스터 공용) |
| `/contractMgmt/itemPriceCompareList.do` | GET | 가격비교 화면 | `app/(main)/COMPANY_16/sales/estimate/price-compare/page.tsx` |
| `/contractMgmt/getItemPriceCompareGridList.do` | GET | 가격비교 그리드 | `GET /api/sales/item-price-compare?...` |
| `/contractMgmt/FileRegistPopup.do` | GET | 첨부파일 등록 팝업 | (Next.js Dialog + 파일 업로드 컴포넌트) |
| `/contractMgmt/addEstimatePdfPopup.do` | GET | 추가견적 PDF 첨부 팝업 | (Next.js Dialog) |
## 3. DB 테이블 (이식 대상)
### 3.1 핵심 테이블
| 테이블 | 역할 | DDL 출처 |
|---|---|---|
| `estimate_mgmt` | 견적 헤더 (구버전) | `db/dbexport.pgsql:4340` |
| `estimate_template` | 견적 템플릿 / **신규 견적 헤더** | dbexport에 없음 — 운영 DB에서 추출 필요 |
| `estimate_template_item` | 견적 라인 | dbexport에 없음 — 운영 DB에서 추출 필요 |
| `contract_mgmt` | 영업번호(CONTRACT_NO)와 연결되는 영업/계약 헤더 | `db/dbexport.pgsql:2488` |
| `contract_item` | 견적/계약 품목 라인 | `database/contract_item_tables.sql` |
| `contract_item_serial` | 라인별 시리얼 | 위 SQL |
| `contract_mgmt_option` | 옵션 | `db/dbexport.pgsql:2899` |
| `attach_file_info` | 첨부파일 (`doc_type='contractMgmt01'`, `addEstimatePdf` 등) | `db/dbexport.pgsql:1387` |
| `approval` / `amaranth_approval` | 결재 (자체 + 아마란스) | `db/dbexport.pgsql:507` |
| `mail_log` | 견적 메일 이력 | (필요 시 추출) |
### 3.2 마스터 의존 (vexplor_rps에 매핑)
- `SUPPLY_MNG` (objid) + `CLIENT_MNG` (objid, `'C_'` prefix) → vexplor_rps `customer_mng.id`
- 매핑 테이블: `legacy_customer_map (legacy_objid VARCHAR PK, customer_mng_id INT)`
- `PART_MGMT` (objid) → vexplor_rps `item_info.id`
- `USER_INFO` (user_id) → vexplor_rps `user_info.user_id`
- `DEPT_INFO` (dept_code) → 확인 필요
### 3.3 estimate_mgmt 컬럼 (`db/dbexport.pgsql:4340-4371`)
```sql
CREATE TABLE public.estimate_mgmt (
objid integer NOT NULL,
customer_objid character varying, -- → customer_mng.id (변환)
category_cd character varying, -- 공통코드
product_group character varying,
product character varying DEFAULT '0',
product_std character varying,
qty character varying,
warranty character varying,
product_price integer,
other_price integer,
total_price integer,
contract_user_id character varying, -- → user_info.user_id
contract_date character varying,
contract_phone character varying,
contract_email character varying,
contract_office_no character varying,
contract_fax_no character varying,
est_release_date character varying,
regdate timestamp without time zone,
writer character varying,
estimate_no character varying, -- 채번 (EST-YYYYMMDD-NNN)
contract_product_price integer,
sale character varying,
final_total_price integer,
contract_type character varying,
note character varying,
cus_request_date character varying,
delivery_place character varying,
product_code character varying,
status character varying
);
-- 시퀀스: estimate_mgmt_seq
```
⚠️ **현재 신규 견적은 `estimate_mgmt`보다 `estimate_template`을 메인으로 쓰는 것으로 보임** (`estimateList_new.jsp` 분석 결과). 운영 DB DDL 확인이 우선.
## 4. 구현 순서
1. **운영 DB에서 누락 DDL 추출**
```bash
pg_dump -h 211.115.91.141 -p 11133 -U postgres -d waceplm \
--schema-only --table=estimate_template --table=estimate_template_item \
--table=mail_log > /tmp/estimate_missing_ddl.sql
```
2. **마이그레이션 파일 작성**: `db/migrations/100_create_estimate_tables.sql` (estimate_mgmt + estimate_template + estimate_template_item + 매핑 테이블)
3. **백엔드 작성**:
- `backend-node/src/routes/estimateRoutes.ts`
- `backend-node/src/controllers/estimateController.ts`
- `backend-node/src/services/estimateService.ts`
4. **마스터 매핑 유틸**: `backend-node/src/utils/legacyIdMap.ts` (customer/part objid 변환)
5. **프론트엔드 작성**:
- `frontend/app/(main)/COMPANY_16/sales/estimate/page.tsx` (목록 + 검색 + 그리드)
- 견적 등록/수정 Dialog 컴포넌트 (`components/sales/EstimateRegistDialog.tsx`)
- 일반/장비 견적서 작성 페이지 (popup): `frontend/app/(pop)/sales/estimate/template[1|2]/page.tsx`
- 메일 발송 Dialog
6. **메뉴/권한**: vexplor_rps 메뉴 관리에서 등록 + 권한 매핑
7. **테스트**: 견적 생성 → 일반/장비 견적서 작성 → 결재 → 메일 발송 → 추가견적 PDF 첨부
## 5. 주의사항 / 결정 보류 항목
- **결재 시스템**: wace_plm은 자체 `approval` + 외부 `amaranth_approval` 두 채널. RPS에서 둘 다 살릴지, 자체만 쓸지 결정 필요. (관리자 측에서 amaranth 연동 진행 중이라면 활용 가능)
- **PDF 생성/메일 발송**: wace_plm은 SmartEditor 기반. Next.js에선 `puppeteer` 또는 react-pdf 등으로 대체 검토.
- **`fn_projectConceptDetail`** (영업번호 클릭): 프로젝트 모듈 종속 → 프로젝트관리 이식 후 연결.
- **품목 검색 select2**: vexplor_rps의 `item_info` 마스터에 wace_plm `PART_MGMT` 데이터를 마이그레이션해 두어야 select2 동작.
- **통화/환율**: `CONTRACT_CURRENCY_NAME`, `EST_TOTAL_AMOUNT_KRW` 컬럼 — 환율 마스터 또는 환율 API 필요.
+187
View File
@@ -0,0 +1,187 @@
# 02. 주문서관리 이식 상세
> 원본: `/contractMgmt/orderMgmtList.do` (orderMgmtList.jsp, 45KB)
> 대상: `app/(main)/COMPANY_16/sales/order/page.tsx` (기존 골격 대체 또는 신규 경로)
## 1. 화면 구조 (wace_plm 원본)
### 1.1 검색 폼 (`#plmSearchZon`)
| 필드 | name | 타입 | 비고 |
|---|---|---|---|
| 주문유형 | `category_cd` | select2 (공통코드) | |
| 발주번호 | `search_poNo` | text | |
| 고객사 | `customer_objid` | select2 (`SUPPLY_MNG`+`CLIENT_MNG`) | `'C_'` 접두사 |
| 품번 | `search_partNo` | select2-part | hidden `search_partObjId` 동행 |
| 품명 | `search_partName` | select2-part | |
| 시리얼 | `search_serialNo` | text | |
| 수주상태 | `contract_result` | select2 (공통코드) | |
| 발주일 from~to | `order_start_date` / `order_end_date` | date_icon | |
| 요청납기 from~to | `due_start_date` / `due_end_date` | date_icon | |
| 제품 | `product` | select2 | |
| 지역 | `area_cd` | select2 | |
| 유/무상 | `paid_type` | select2 | |
| 환종 | `contract_currency` | select2 | |
### 1.2 버튼 영역
| 버튼 | id | 동작 |
|---|---|---|
| 조회 | `btnSearch` | `fn_search()``/contractMgmt/contractGridList.do` |
| 수주복사 | `btnCopy` | `/contractMgmt/copyEstimateAndOrderInfo.do` |
| 수주입력 | `.btnRegist` | 팝업: `/contractMgmt/estimateAndOrderRegistFormPopup.do` 또는 `orderRegistFormPopup.do` |
| 수주확정 | `btnOrderConfirm` | (녹색) — 견적 → 수주 확정 처리 |
| 수주취소 | `btnOrderCancel` | (빨강) — `/contractMgmt/saveOrderCancelQty.do` (수량별 취소) |
| 결재상신 | `btnApproval` | 결재 워크플로우 |
### 1.3 그리드 (`_tabulGrid` = Tabulator)
데이터 소스: `POST /contractMgmt/contractGridList.do`
| # | 컬럼 | field | 정렬 | 비고 |
|---|---|---|---|---|
| 1 | 영업번호 | CONTRACT_NO | center | frozen, anchor → `fn_projectConceptDetail(OBJID)` |
| 2 | 주문유형 | CATEGORY_NAME | center | |
| 3 | 발주일 | ORDER_DATE | center | |
| 4 | 발주번호 | PO_NO | center | |
| 5 | 요청납기 | EARLIEST_DUE_DATE | center | "{date} 외 {n}건" 포맷 |
| 6 | 고객사 | CUSTOMER_NAME | left | |
| 7 | 품명 | ITEM_SUMMARY | left | "{itemName} 외 {n}건" |
| 8 | 수주수량 | ORDER_QUANTITY | right | |
| 9 | 수주취소 | CANCEL_QTY_SUM | right | |
| 10 | 유/무상 | PAID_TYPE | center | |
| 11 | 수주상태 | CONTRACT_RESULT_NAME | left | |
| 12 | 공급가액 | ORDER_SUPPLY_PRICE_SUM | right | 통화기호 |
| 13 | 부가세 | ORDER_VAT_SUM | right | |
| 14 | 총액 | ORDER_TOTAL_AMOUNT_SUM | right | |
| 15 | 원화총액 | ORDER_TOTAL_AMOUNT_KRW | right | |
| 16 | 주문서첨부 | CU01_CNT | center | 📎 클릭 → `/contractMgmt/FileRegistPopup.do` |
| 17 | 주문서 | HAS_ORDER_DATA | center | 클릭 → `/contractMgmt/orderFormView.do?objId=...` |
| 18 | 고객사요청사항 | CUSTOMER_REQUEST | left | |
| 19 | 결재상태 | ORDER_APPR_STATUS | center | |
| 20 | 환종 | CONTRACT_CURRENCY_NAME | center | |
| 21 | 환율 | EXCHANGE_RATE | right | |
| 22 | S/N | SERIAL_NO | center | |
| 23 | 품번 | PART_NO | center | |
| 24 | 작성자 | WRITER_NAME | center | |
| 25 | 제품구분 | PRODUCT_NAME | center | |
| 26 | 국내/해외 | AREA_NAME | center | |
| 27 | 접수일 | RECEIPT_DATE | center | |
> 일부 셀 클릭은 `contractList.jsp`로 form submit되어 다른 화면 이동 (line 194 참고).
## 2. 백엔드 endpoint 매핑
| wace_plm endpoint | 메서드 | 용도 | vexplor_rps 신규 endpoint |
|---|---|---|---|
| `/contractMgmt/orderMgmtList.do` | GET | 페이지 진입 | (Next.js page) |
| `/contractMgmt/contractGridList.do` | POST | 그리드 데이터 | `GET /api/sales/order/list?...` |
| `/contractMgmt/getPagingContractList.do` | POST | 페이징 조회 (legacy) | 위와 통합 |
| `/contractMgmt/orderRegistFormPopup.do` | GET | 수주 등록/수정 팝업 | (Next.js Dialog) |
| `/contractMgmt/estimateAndOrderRegistFormPopup.do` | GET | 견적+수주 통합 팝업 | (Next.js Dialog with mode) |
| `/contractMgmt/saveOrderInfo.do` | POST | 수주 저장 | `POST /api/sales/order` / `PUT /api/sales/order/:id` |
| `/contractMgmt/saveEstimateAndOrderInfo.do` | POST | 견적+수주 통합 저장 | 위와 통합 (mode 파라미터) |
| `/contractMgmt/copyEstimateAndOrderInfo.do` | POST | 수주 복사 | `POST /api/sales/order/:id/copy` |
| `/contractMgmt/updateOrderStatus.do` | POST | 수주 상태 변경(확정/취소 등) | `PATCH /api/sales/order/:id/status` |
| `/contractMgmt/saveOrderCancelQty.do` | POST | 라인별 취소 수량 저장 | `POST /api/sales/order/:id/cancel-qty` |
| `/contractMgmt/getContractItems.do` | POST | 수주 라인 조회 | `GET /api/sales/order/:id/items` |
| `/contractMgmt/getAllSerialNumbers.do` | POST | 라인 전체 시리얼 조회 | `GET /api/sales/order/:id/serials` |
| `/contractMgmt/orderFormView.do` | GET | 주문서 양식 화면 | `app/(pop)/sales/order/[id]/form/page.tsx` |
| `/contractMgmt/getOrderFormData.do` | POST | 주문서 양식 데이터 | `GET /api/sales/order/:id/form-data` |
| `/contractMgmt/getOrderTotalAmount.do` | POST | 합계 계산 | `GET /api/sales/order/:id/totals` |
| `/contractMgmt/orderMgmtGrodList.do` (typo) | POST | (구버전 그리드) | (사용 안함) |
| `/contractMgmt/getContractItemList.do` | POST | 계약 라인 조회 | 위 `/items`와 통합 |
| `/contractMgmt/contracMgmtReviewFormPopup.do` (typo) | GET | 계약 리뷰 팝업 | (Next.js Dialog) |
| `/contractMgmt/saveContractMgmtReviewInfo.do` | POST | 리뷰 정보 저장 | `POST /api/sales/order/:id/review` |
| `/contractMgmt/overlapOrder.do` | POST | 중복 수주 체크 | `GET /api/sales/order/check-duplicate?...` |
| `/contractMgmt/checkApprovalRequired.do` | POST | 결재 필요 여부 | (견적과 공유) |
| `/contractMgmt/deleteContractMngInfo.do` | POST | 계약(수주) 삭제 | `DELETE /api/sales/order/:id` |
| `/contractMgmt/FileRegistPopup.do` | GET | 첨부파일 팝업 | (공유 컴포넌트) |
| `/contractMgmt/getCustomerManagerList.do` | POST | 고객사 담당자 목록 | `GET /api/customers/:id/contacts` |
| `/contractMgmt/checkProjectExists.do` | POST | 프로젝트 존재 체크 | (견적과 공유) |
> `OrdersMgmtController` (다른 컨트롤러, `/ordersMgmt/*`)는 별개 모듈로 보임 (입출고/미납 관리 위주). 본 메뉴와는 무관.
## 3. DB 테이블 (이식 대상)
| 테이블 | 역할 | DDL 출처 |
|---|---|---|
| `contract_mgmt` | 수주(계약) 헤더 — **주문서의 메인 테이블** | `db/dbexport.pgsql:2488` |
| `contract_item` | 수주 라인 (품번/수량/단가/공급가/부가세/총액 + ORDER_* 컬럼들) | `database/contract_item_tables.sql` + `database/add_order_columns_to_contract_item.sql` |
| `contract_item_serial` | 라인별 S/N | `database/contract_item_tables.sql` |
| `contract_mgmt_option` | 수주 옵션 | `db/dbexport.pgsql:2899` |
| `contract_base_data` | 수주 기준 데이터 (공통 마스터) | (운영 DB 추출 필요) |
| `attach_file_info` | 주문서 첨부 (`doc_type='contractMgmt01'`) | `db/dbexport.pgsql:1387` |
| `approval` | 결재 | `db/dbexport.pgsql:507` |
### 3.1 contract_item 핵심 컬럼 (DDL 합본)
```sql
CREATE TABLE CONTRACT_ITEM (
OBJID VARCHAR(50) PRIMARY KEY,
CONTRACT_OBJID VARCHAR(50) NOT NULL REFERENCES CONTRACT_MGMT(OBJID) ON DELETE CASCADE,
SEQ INTEGER NOT NULL,
PART_NO VARCHAR(100) NOT NULL, -- → vexplor_rps item_info 매핑
PART_NAME VARCHAR(200) NOT NULL,
QUANTITY INTEGER NOT NULL DEFAULT 1, -- 견적 수량
DUE_DATE VARCHAR(10),
CUSTOMER_REQUEST TEXT,
-- 수주 컬럼 (add_order_columns 추가)
ORDER_QUANTITY VARCHAR(50),
ORDER_UNIT_PRICE VARCHAR(50),
ORDER_SUPPLY_PRICE VARCHAR(50),
ORDER_VAT VARCHAR(50),
ORDER_TOTAL_AMOUNT VARCHAR(50),
-- 공통
REGDATE TIMESTAMP NOT NULL DEFAULT NOW(),
WRITER VARCHAR(50),
CHGDATE TIMESTAMP,
CHG_USER_ID VARCHAR(50),
STATUS VARCHAR(20) DEFAULT 'ACTIVE'
);
CREATE TABLE CONTRACT_ITEM_SERIAL (
OBJID VARCHAR(50) PRIMARY KEY,
ITEM_OBJID VARCHAR(50) NOT NULL REFERENCES CONTRACT_ITEM(OBJID) ON DELETE CASCADE,
SEQ INTEGER NOT NULL,
SERIAL_NO VARCHAR(200) NOT NULL,
REGDATE TIMESTAMP NOT NULL DEFAULT NOW(),
WRITER VARCHAR(50),
STATUS VARCHAR(20) DEFAULT 'ACTIVE',
UNIQUE (ITEM_OBJID, SERIAL_NO)
);
```
### 3.2 contract_mgmt 주요 컬럼 (참고)
`db/dbexport.pgsql:2488-2540` 참조. 주문서에서 자주 쓰는 컬럼:
- `objid` (PK), `contract_no` (영업번호 = CONTRACT_NO)
- `customer_objid` (고객사 — `'C_'` prefix로 client_mng 분기)
- `category_cd`, `area_cd`, `product`, `paid_type` (공통코드)
- `po_no` (발주번호), `contract_date` (발주일), `req_del_date` (요청납기)
- `contract_currency`, `contract_currency_name`, `exchange_rate`(?)
- `result_cd``contract_result_name` (수주상태)
- `pm_user_id`, `salesman` (담당자)
- `regdate`, `writer`, `chg_user_id`
## 4. 구현 순서
1. **마이그레이션 SQL 작성**: `db/migrations/101_create_order_tables.sql` (contract_mgmt + contract_item + contract_item_serial + contract_mgmt_option + contract_base_data)
2. **백엔드**:
- `backend-node/src/routes/orderRoutes.ts`
- `backend-node/src/controllers/orderController.ts`
- `backend-node/src/services/orderService.ts` (트랜잭션 필수: 헤더 + 라인 + 시리얼 동시 저장)
3. **프론트엔드**:
- `frontend/app/(main)/COMPANY_16/sales/order/page.tsx` (기존 파일 백업 후 신규 작성, 또는 새 라우트 `sales/contract-order`)
- 수주 등록/수정 Dialog (`components/sales/OrderRegistDialog.tsx`)
- 수주확정/취소 모달 (수량 입력)
- 라인별 시리얼 관리 모달
- 주문서 양식 출력 페이지 (PDF/인쇄)
## 5. 주의사항
- **견적 → 수주 전환**: `estimateAndOrderRegistFormPopup`은 견적 OBJID를 받아 수주 헤더를 생성하면서 라인을 복사. 트랜잭션 + 라인 컬럼 매핑(견적 QUANTITY → ORDER_QUANTITY) 필요.
- **라인별 부분 취소**: `saveOrderCancelQty`는 라인의 ORDER_QUANTITY 일부만 취소하는 시나리오. UI에서 취소 수량 입력 → DB는 별도 취소이력 테이블 사용 가능성 (확인 필요).
- **주문서 양식 (orderFormView)**: 인쇄용 별도 화면. PDF 다운로드 또는 인쇄 다이얼로그.
- **계약 리뷰 (Review)**: 사내 검토 단계 — 별도 결재 또는 메모/체크리스트. `contracMgmtReviewFormPopup` (typo) 분석 필요.
- **vexplor_rps 기존 `sales/order/page.tsx`** ([order/page.tsx](../../../frontend/app/(main)/COMPANY_16/sales/order/page.tsx))는 이 도메인이 아닐 가능성. 백업 후 신규 작성 권장.
+153
View File
@@ -0,0 +1,153 @@
# 03. 판매관리 이식 상세
> 원본: `/contractMgmt/salesMgmtList.do` (위임) → `/salesNcollectMgmt/sales.do` 또는 `/salesMgmt/salesMgmt`
> JSP: `salesmgmt/salesMgmt/salesMgmtList.jsp` (34KB)
> Controller: `SalesNcollectMgmtController` (line 763~)
> 대상: `app/(main)/COMPANY_16/sales/sale/page.tsx` (신규)
## 1. 화면 구조 (wace_plm 원본)
수주된 항목을 출하지시 → 판매등록 → 거래명세서 발행까지 처리.
### 1.1 검색 폼
| 필드 | name | 타입 | 비고 |
|---|---|---|---|
| 주문유형 | `orderType` | select2 (공통코드) | |
| 발주번호 | `poNo` | text | |
| 고객사 | `customer_objid` | select2 | |
| 품번 | `search_partNo` | select2-part | hidden `search_partObjId` |
| 품명 | `search_partName` | select2-part | |
| 시리얼 | `serialNo` | text | |
| 출하상태 | `shippingStatus` | select2 | |
| 발주일 from~to | `orderDateFrom` / `orderDateTo` | date_icon | |
| 출하일 from~to | `shippingDateFrom` / `shippingDateTo` | date_icon | |
| 판매상태 | `salesStatus` | select2 multiple | 다중 선택 |
### 1.2 버튼
| 버튼 | id | 동작 |
|---|---|---|
| 조회 | `btnSearch` | `/salesMgmt/salesMgmtGridList.do` |
| 출하지시/판매등록 | `btnBulkRegister` | (녹색) → `/salesMgmt/salesRegForm.do` 새창 |
| ~~거래명세서 생성~~ | (주석) | 매출관리에서 사용 |
| ~~분할출하~~ | (주석) | `/salesMgmt/splitShipmentForm.do` |
### 1.3 그리드 컬럼
데이터 소스: `POST /salesMgmt/salesMgmtGridList.do`
| # | 컬럼 | field | 정렬 |
|---|---|---|---|
| 1 | 프로젝트번호 | PROJECT_NO | center, frozen |
| 2 | 주문유형 | ORDER_TYPE | center |
| 3 | 발주일 | ORDER_DATE | center |
| 4 | 발주번호 | PO_NO | center |
| 5 | 요청납기 | REQUEST_DATE | center |
| 6 | 출하일 | SHIPPING_DATE_WITH_COUNT | center |
| 7 | 고객사 | CUSTOMER | left |
| 8 | 품명 | PRODUCT_NAME | left |
| 9 | 수주수량 | ORDER_QUANTITY | right |
| 10 | 판매수량 | SALES_QUANTITY | right |
| 11 | 잔량 | REMAINING_QUANTITY | right |
| 12 | 판매단가 | SALES_UNIT_PRICE | right |
| 13 | 판매공급가액 | SALES_SUPPLY_PRICE | right |
| 14 | 부가세 | SALES_VAT | right |
| 15 | 판매총액 | SALES_TOTAL_AMOUNT | right |
| 16 | 판매원화총액 | SALES_TOTAL_AMOUNT_KRW | right |
| 17 | 잔량원화총액 | REMAINING_AMOUNT_KRW | right |
| 18 | 수주상태 | ORDER_STATUS | center |
| 19 | 판매상태 | SALES_STATUS | center |
| 20 | 생산상태 | PRODUCTION_STATUS | center |
| 21 | 출하지시상태 | SHIPPING_ORDER_STATUS | center |
| 22 | 유/무상 | PAYMENT_TYPE | center |
| 23 | 환종 | SALES_CURRENCY_NAME | center |
| 24 | 환율 | SALES_EXCHANGE_RATE | right |
| 25 | S/N | SERIAL_NO | left |
| 26 | 분할S/N | SPLIT_SERIAL_NO | left |
| 27 | 품번 | PRODUCT_NO | left |
| 28 | 제품구분 | PRODUCT_TYPE | center |
| 29 | 국내/해외 | NATION | center |
| 30 | 접수일 | RECEIPT_DATE | center |
| 31 | 고객사요청사항 | CUSTOMER_REQUEST | left |
| 32 | 주문서첨부 | CU01_CNT | center |
| 33 | 출하방법 | SHIPPING_METHOD | center |
| 34 | 담당자 | MANAGER | center |
| 35 | 인도조건 | INCOTERMS | center |
| 36 | 거래명세서 | TRANSACTION_STATEMENT | center |
## 2. 백엔드 endpoint 매핑
| wace_plm endpoint | 메서드 | 용도 | vexplor_rps 신규 endpoint |
|---|---|---|---|
| `/contractMgmt/salesMgmtList.do` | GET | (위임) → `/salesmgmt/salesMgmt/salesMgmtList` 렌더 | (Next.js page) |
| `/salesNcollectMgmt/sales.do` | GET | 판매 화면 직접 진입 | (Next.js page) |
| `/salesMgmt/salesMgmtGridList.do` | POST | 그리드 데이터 | `GET /api/sales/sale/list?...` |
| `/salesMgmt/salesRegForm.do` | GET | 출하지시/판매등록 폼 | `app/(pop)/sales/sale/register/page.tsx` (43KB JSP) |
| `/salesMgmt/saveSales.do` | POST | 판매 저장 | `POST /api/sales/sale` |
| `/salesMgmt/splitShipmentForm.do` | GET | 분할출하 폼 | `app/(pop)/sales/sale/split/page.tsx` (23KB JSP) |
| `/salesMgmt/saveSplitShipment.do` | POST | 분할출하 저장 | `POST /api/sales/sale/split` |
| `/salesMgmt/shippingDetailPopup.do` | GET | 출하 상세 팝업 | (Next.js Dialog) |
| `/salesMgmt/transactionStatementForm.do` | GET | 거래명세서 폼 | `app/(pop)/sales/sale/transaction/page.tsx` (31KB JSP) |
| `/salesMgmt/getTransactionStatementData.do` | POST | 거래명세서 데이터 | `GET /api/sales/sale/transaction-statement?...` |
| `/salesMgmt/saveTransactionStatement.do` | POST | 거래명세서 저장 | `POST /api/sales/sale/transaction-statement` |
| `/salesMgmt/getSavedTransactionStatement.do` | POST | 저장된 거래명세서 조회 | `GET /api/sales/sale/transaction-statement/:id` |
| `/salesMgmt/getAllSerialNumbers.do` | POST | 시리얼 번호 일괄 조회 | (주문과 공유 가능) |
| `/salesNcollectMgmt/contractList.do` | GET | (대시보드 화면) | TBD |
| `/salesNcollectMgmt/salesMgmtFormPopup.do` | GET | 판매 폼 팝업 (구버전) | (사용 안할 듯) |
| `/salesNcollectMgmt/saveSalesMgmt.do` | POST | 판매 저장 (구버전) | 위와 통합 |
| `/salesNcollectMgmt/deleteSalesMgmt.do` | POST | 판매 삭제 | `DELETE /api/sales/sale/:id` |
| `/salesNcollectMgmt/salesDeadlineConfirm.do` | POST | 매출 마감 확인 | (매출관리에서 호출) |
## 3. DB 테이블
> **2026-05-07 운영 DB 직접 확인 결과**: `transaction_statement_*` 테이블은 존재하지 않음. 거래명세서는 별도 테이블 없이 화면 폼에서 출력만 하거나, attach_file_info에 PDF로 저장하는 방식으로 보임 (정확한 위치는 `getSavedTransactionStatement.do` SQL 분석 필요).
| 테이블 | 역할 | DDL |
|---|---|---|
| `sales_registration` | 판매 등록 헤더 (프로젝트당 1건, UNIQUE `project_no`) — **메인** | [ddl-extracted/101_create_sales_registration.sql](./ddl-extracted/101_create_sales_registration.sql) |
| `shipment_log` | 분할출하 이력 라인 (FK `parent_sale_no` → sales_registration) | 위 |
| `contract_mgmt` | 수주 헤더 (조인 필수, target_objid로 연결) | `db/dbexport.pgsql:2488` |
| `contract_item` | 수주 라인 (조인 필수) | `database/contract_item_tables.sql` |
| `contract_item_serial` | 시리얼 (분할출하 시 분리) | 위 |
| `production_result` | 생산 결과 (조인 — `PRODUCTION_STATUS`) | (참고만) |
### 핵심 관계
```
contract_mgmt (수주 헤더)
└─ contract_item (수주 라인)
└─ contract_item_serial (라인별 S/N)
contract_mgmt.objid = sales_registration.project_no? (정확한 매칭 키 확인 필요)
= shipment_log.target_objid
sales_registration (판매 헤더, 프로젝트당 1건)
↑ parent_sale_no
└─ shipment_log (분할출하 + 매출마감 이력 N건)
```
> ⚠️ `sales_registration.project_no`와 `contract_mgmt.objid`(또는 `contract_no`)의 매칭 키는 운영 SQL에서 재확인 필요. 칼럼명은 `project_no`이지만 실제로는 contract_mgmt를 가리킬 가능성이 큼.
## 4. 구현 순서
1. **운영 DB DDL 추출** (sales_registration, shipment_log, transaction_statement)
2. **마이그레이션 SQL**: `db/migrations/102_create_sale_tables.sql`
3. **백엔드**:
- `backend-node/src/routes/saleRoutes.ts`
- `backend-node/src/services/saleService.ts` (그리드 SQL은 contract_mgmt + contract_item + sales_registration JOIN, 잔량 계산 = ORDER_QUANTITY - SUM(SALES_QUANTITY))
- 분할출하 트랜잭션 (라인 분리 + 시리얼 재할당)
4. **프론트엔드**:
- `frontend/app/(main)/COMPANY_16/sales/sale/page.tsx` (목록)
- `app/(pop)/sales/sale/register/page.tsx` (출하지시/판매등록 폼)
- `app/(pop)/sales/sale/split/page.tsx` (분할출하)
- `app/(pop)/sales/sale/transaction/page.tsx` (거래명세서)
## 5. 주의사항
- **잔량(REMAINING_QUANTITY) 계산**: `ORDER_QUANTITY - SUM(sales_registration.SALES_QUANTITY where contract_item_objid = ?)`. 그리드는 라인 단위 또는 헤더 단위 표시 모드가 있을 수 있음 — 원본 SQL 확인 필요.
- **분할출하**: 한 라인의 시리얼을 여러 출하건으로 나누는 시나리오. UI 복잡도 높음.
- **거래명세서**: 여러 판매건을 묶어 1장으로 발행. `projectNos` 파라미터로 다건 처리.
- **상태 컬럼들**: ORDER_STATUS / SALES_STATUS / PRODUCTION_STATUS / SHIPPING_ORDER_STATUS 4개를 동시 표시 — 각 상태 머신 정의 필요.
- **환율 컬럼 분리**: 수주(`exchange_rate`)와 판매(`sales_exchange_rate`)를 별도 보유 — 시점 환율을 잠그는 정책.
- **거래명세서 PDF**: 한국어 양식, 워터마크/도장 처리 필요할 수 있음.
+137
View File
@@ -0,0 +1,137 @@
# 04. 매출관리 이식 상세
> 원본: `/revenueMgmt/revenueList.do`
> JSP: `salesmgmt/salesMgmt/revenueMgmtList.jsp` (38KB)
> Controller: `SalesNcollectMgmtController` (line 103, 214)
> 대상: `app/(main)/COMPANY_16/sales/revenue/page.tsx` (신규)
## 1. 화면 구조 (wace_plm 원본)
판매관리에서 출하/판매 등록된 데이터를 **매출 관점**(마감일/세금계산서/원장)으로 조회·확정하는 화면. `revenueMode=Y` 플래그로 판매관리와 동일 폼/그리드를 다른 모드로 표시.
### 1.1 검색 폼 (판매와 유사하나 매출 항목 추가)
| 필드 | name | 타입 | 비고 |
|---|---|---|---|
| 주문유형 | `orderType` | select2 | |
| 발주번호 | `poNo` | text | |
| 고객사 | `customer_objid` | select2 | |
| 제품구분 | `productType` | select2 | |
| 품번 | `search_partNo` | select2-part | |
| 품명 | `search_partName` | select2-part | |
| 국내/해외 | `nation` | select2 | |
| 시리얼 | `serialNo` | text | |
| **매출마감 기간** | `salesDeadlineFrom` / `salesDeadlineTo` | date_icon | |
| 발주일 from~to | `orderDateFrom` / `orderDateTo` | date_icon | |
| 출하일 from~to | `shippingDateFrom` / `shippingDateTo` | date_icon | |
| 유/무상 | `paymentType` | select2 | |
| 수주상태 | `orderStatus` | select2 | |
| 요청납기 from~to | `requestDateFrom` / `requestDateTo` | date_icon | |
| 출하상태 | `shippingStatus` | select2 | |
| 출하방법 | `shippingMethod` | select2 | |
| 담당자 | `manager` | select2 | |
| 인도조건 | `incoterms` | select2 | |
Hidden: `revenueMode=Y` (shipment_log 기반 조회 모드)
### 1.2 버튼
| 버튼 | id | 동작 |
|---|---|---|
| 조회 | `btnSearch` | `/revenueMgmt/revenueGridList.do` |
| 거래명세서 생성 | `btnTransactionStatement` | `/salesMgmt/transactionStatementForm.do` (판매관리 공유) |
| 마감정보입력 | `btnDeadlineInfo` | `/revenueMgmt/saveDeadlineInfo.do` (세금계산서/수출신고 등 입력) |
| 매출마감 | `btnDeadline` | `/salesNcollectMgmt/salesDeadlineConfirm.do` (마감 처리) |
### 1.3 그리드 컬럼 (`POST /revenueMgmt/revenueGridList.do`)
| # | 컬럼 | field | 비고 |
|---|---|---|---|
| 1 | 프로젝트번호 | PROJECT_NO | frozen |
| 2 | 주문유형 | ORDER_TYPE | |
| 3 | **매출마감일** | SALES_DEADLINE_DATE | |
| 4 | 발주일 | ORDER_DATE | |
| 5 | 발주번호 | PO_NO | |
| 6 | 고객사 | CUSTOMER | |
| 7 | 제품구분 | PRODUCT_TYPE | |
| 8 | 품명 | PRODUCT_NAME | |
| 9 | 수량 | SALES_QUANTITY | right |
| 10 | 단가 | SALES_UNIT_PRICE | right |
| 11 | 공급가액 | SALES_SUPPLY_PRICE | right |
| 12 | 부가세 | SALES_VAT | right |
| 13 | 총액 | SALES_TOTAL_AMOUNT | right |
| 14 | 원화총액 | SALES_TOTAL_AMOUNT_KRW | right |
| 15 | 출하일 | SHIPPING_DATE | |
| 16 | 국내/해외 | NATION | |
| 17 | 환종 | SALES_CURRENCY_NAME | |
| 18 | 환율 | SALES_EXCHANGE_RATE | right |
| 19 | S/N | SERIAL_NO | left |
| 20 | 분할S/N | SPLIT_SERIAL_NO | left |
| 21 | 품번 | PRODUCT_NO | |
| 22 | **과세구분** | TAX_TYPE_NAME | |
| 23 | **세금계산서발행일** | TAX_INVOICE_DATE | |
| 24 | **수출신고필증번호** | EXPORT_DECL_NO | |
| 25 | **선적일자** | LOADING_DATE | |
매출관리 추가 컬럼: 매출마감일, 과세구분, 세금계산서발행일, 수출신고필증번호, 선적일자.
## 2. 백엔드 endpoint 매핑
| wace_plm endpoint | 메서드 | 용도 | vexplor_rps 신규 endpoint |
|---|---|---|---|
| `/revenueMgmt/revenueList.do` | GET | 페이지 진입 | (Next.js page) |
| `/revenueMgmt/revenueGridList.do` | POST | 그리드 데이터 (shipment_log 기반) | `GET /api/sales/revenue/list?...` |
| `/revenueMgmt/getDeadlineInfo.do` | POST | 마감정보 조회 | `GET /api/sales/revenue/:id/deadline` |
| `/revenueMgmt/saveDeadlineInfo.do` | POST | 마감정보 저장 (세금계산서/수출신고/선적일자) | `PUT /api/sales/revenue/:id/deadline` |
| `/salesNcollectMgmt/salesDeadlineConfirm.do` | POST | 매출마감 확정 | `POST /api/sales/revenue/deadline-confirm` |
| `/salesMgmt/transactionStatementForm.do` | GET | 거래명세서 (판매와 공유) | `app/(pop)/sales/sale/transaction/page.tsx` |
| `/salesMgmt/getSavedTransactionStatement.do` | POST | 저장된 거래명세서 조회 | (판매와 공유) |
## 3. DB 테이블
> **2026-05-07 운영 DB 직접 확인 결과 정정**: `final_data`, `end_count`, `tax_invoice_*`, `transaction_statement_*`는 **존재하지 않음**. 매출 마감/세금계산서/수출신고 정보는 모두 **`shipment_log`의 컬럼**으로 저장됨.
| 테이블 | 역할 | DDL |
|---|---|---|
| `shipment_log` | 분할출하 이력 + **매출 마감 정보** (컬럼: `sales_deadline_date`, `tax_type`, `tax_invoice_date`, `export_decl_no`, `loading_date`, `sales_slip_date`, `sales_slip_menu_sq`) | [ddl-extracted/101_create_sales_registration.sql](./ddl-extracted/101_create_sales_registration.sql) |
| `sales_registration` | 판매 등록 헤더 (프로젝트당 1건, UNIQUE project_no) | 위 |
| `contract_mgmt` / `contract_item` | 수주 헤더/라인 (조인) | 기존 (db/dbexport.pgsql) |
| `mail_log` | 거래명세서 메일 발송 이력 (전 시스템 공용, 7,805건) | [ddl-extracted/102_create_mail_log.sql](./ddl-extracted/102_create_mail_log.sql) |
### 매출 컬럼 참조 (shipment_log)
```sql
sales_deadline_date VARCHAR(10), -- 매출 마감일
tax_type VARCHAR(20), -- 과세구분
tax_invoice_date VARCHAR(10), -- 세금계산서 발행일
export_decl_no VARCHAR(100), -- 수출신고필증 번호
loading_date VARCHAR(10), -- 선적일자
sales_slip_date VARCHAR(10), -- 매출전표일
sales_slip_menu_sq INTEGER, -- 매출전표 menu_sq
```
→ 매출관리 화면의 "마감정보입력" / "매출마감"은 **`shipment_log` 행을 INSERT/UPDATE**.
## 4. 구현 순서
1. **운영 DB DDL 추출** (shipment_log, sales_registration, final_data, end_count) — 03-sale.md와 함께 진행
2. **마이그레이션 SQL**: `db/migrations/103_create_revenue_tables.sql` (또는 sale과 통합)
3. **백엔드**:
- `backend-node/src/routes/revenueRoutes.ts`
- `backend-node/src/services/revenueService.ts` (그리드 SQL은 shipment_log 또는 sales_registration JOIN)
- 매출마감 트랜잭션 (다건 마감일 일괄 업데이트 + 마감 이력 기록)
4. **프론트엔드**:
- `frontend/app/(main)/COMPANY_16/sales/revenue/page.tsx` (목록)
- 마감정보 입력 Dialog (`components/sales/RevenueDeadlineInfoDialog.tsx`) — 세금계산서/수출신고/선적일자/과세구분
- 매출마감 확정 모달
## 5. 주의사항
- **매출 vs 판매**: 같은 데이터(sales_registration / shipment_log)를 다른 시점/관점으로 봄.
- 판매관리 = "출하 + 판매 등록까지의 진행 현황"
- 매출관리 = "마감일/세금계산서/수출신고 등 회계·세무 처리"
- **`revenueMode=Y`**: 동일 컨트롤러(`SalesNcollectMgmtController`)가 판매/매출 두 화면을 분기. Next.js에선 그냥 별도 라우트.
- **세금계산서 연동**: 외부 시스템(국세청/이지로보) 연동 가능성 — 본 메뉴에선 단순 발행일·번호 기록만 우선 구현.
- **수출 케이스**: NATION='해외'일 때 수출신고필증번호/선적일자/통화/환율 별도. 환율 마스터 또는 외부 API 필요.
- **거래명세서 공유**: 판매관리(03-sale.md)와 동일한 화면을 호출. 한 곳에 구현 후 양쪽에서 호출.
- **매출마감 후 수정 잠금**: 마감 확정된 라인은 판매관리에서 수정 불가능해야 함 — 권한/상태 가드.
+196
View File
@@ -0,0 +1,196 @@
# 05. 마스터 데이터 매핑 (wace_plm → vexplor_rps)
> 도메인 테이블(견적/주문/판매/매출)이 참조하는 마스터를 vexplor_rps 기존 마스터와 어떻게 연결할지 정의.
## 1. 매핑 정책
**하이브리드 정책** (사용자 확정):
- **공용 마스터** (거래처, 품목, 사용자, 부서 등): vexplor_rps 기존 테이블(`customer_mng`, `item_info`, `user_info` 등) 사용
- **도메인 테이블** (`contract_mgmt`, `estimate_mgmt`, `sales_registration` 등): wace_plm 스키마 그대로 이식
→ 도메인 테이블의 외래 컬럼이 wace_plm objid를 가리키는 문자열이므로, **매핑 테이블** 또는 **마이그레이션 시 inline 변환** 둘 중 하나의 전략 필요.
## 2. 핵심 매핑 표 (2026-05-07 vexplor_rps DB 실측 반영)
| wace_plm 테이블 | wace_plm PK | wace_plm 키 컬럼 | vexplor_rps 테이블 | vexplor_rps PK | 매핑 방식 |
|---|---|---|---|---|---|
| `SUPPLY_MNG` | `objid` (numeric) | `customer_objid` (varchar) | `customer_mng` | `id` (**integer** auto seq) | 매핑 테이블 필수 |
| `CLIENT_MNG` | `objid` (numeric) | `customer_objid` = `'C_'` + objid | `customer_mng` | `id` (integer) | 매핑 테이블 (prefix로 분기) |
| `PART_MGMT` | `objid` (varchar) | `part_objid` 등 | `item_info` | `id` (**varchar 500**) | **objid 그대로 사용 가능** (매핑 테이블 불필요) |
| `PRODUCT_MGMT` | `objid` | (별도 컬럼) | `item_info` 또는 별도 | `id` (varchar) | 검토 필요 |
| `USER_INFO` | `user_id` (varchar) | `writer`, `pm_user_id`, `salesman` 등 | `user_info` | `user_id` (varchar 1024) | 동일 키 사용 가능 (사용자 매핑 검증 필요) |
| `DEPT_INFO` | `dept_code` | `deptcd` | `user_info.dept_code` (별도 부서 마스터 미확인) | — | 일단 dept_code 그대로 가져오고 부서 마스터는 추후 |
| `COMM_CODE` | `code_id` + `code_value` | 다양한 `*_cd` 컬럼 | (vexplor_rps `code_master*` 추정) | (별도 확인 필요) | 코드 시드 작업 |
| `ATTACH_FILE_INFO` | `objid` | `target_objid` + `doc_type` | (vexplor_rps 파일 시스템) | (별도 확인 필요) | TBD |
| `OEM_MNG` | `objid` | (차량 정보) | — | — | RPS에 불필요 |
### 실측한 vexplor_rps 마스터 핵심 컬럼
#### `customer_mng` (PK `id` integer)
주요 컬럼: `customer_code(varchar 20)`, `customer_name(varchar 100)`, `division(varchar 50)`, `business_number(varchar 20)`, `ceo_name(varchar 120)`, `address(text)`, `phone`, `fax_no(varchar 40)`, `hp_no(varchar 40)`, `charge_name(varchar 120)`, `charge_tel(varchar 40)`, `charge_email(varchar 200)`, `nation_code(varchar 20)`, `currency_code(varchar 20)`, `bank_name`, `bank_account`, `account_owner`, `corp_number(varchar 32)`, `short_name(varchar 120)`, `biz_condition`, `biz_item`, `zip_code`, `address_detail`, `customer_type(varchar 20)`, `internal_manager(varchar 500)`, `delivery_location(varchar 500)`, `use_yn(varchar 1, default 'Y')`, `company_code(varchar 20)`, `created_at`, `updated_at`
#### `item_info` (PK `id` **varchar 500**)
주요 컬럼: `id(varchar 500)`, `item_name(varchar 500)`, `item_number(varchar 500)`, `division(varchar 500)`, `type(varchar 500)`, `unit(varchar 500)`, `inventory_unit`, `material`, `size`, `weight`, `volum`, `specific_gravity`, `selling_price`, `standard_price`, `currency_code`, `lead_time`, `width(numeric)`, `height(numeric)`, `thickness(numeric)`, `area(numeric)`, `mold_number`, `use_packaging`, `use_insert`, `image`, `image_path`, `drawing_path`, `expiry_years`, `expiry_months`, `expiry_days`, `meno(text 500, 오타: memo가 아님)`, `status`, `writer`, `company_code`, `created_date`, `updated_date`
#### `user_info` (PK `user_id` varchar 1024)
주요 컬럼: `user_id`, `sabun`, `user_password`, `user_name`, `user_name_eng`, `user_name_cn`, `dept_code`, `dept_name`, `position_code`, `position_name`, `rank_code`, `rank_name`, `email`, `out_email`, `tel`, `cell_phone`, `fax_no`, `user_type`, `user_type_name`, `data_type`, `signup_type`, `branch_name`, `department_history`, `gender_fg`, `join_date`, `retire_date`, `work_status`, `partner_objid`, `license_number`, `vehicle_number`, `emp_seq`, `status`, `end_date`, `locale`, `company_code`, `regdate`, `token_version`
## 3. 매핑 테이블 설계 (제안)
```sql
-- wace_plm objid → vexplor_rps id 변환
CREATE TABLE legacy_id_map (
legacy_table VARCHAR(50) NOT NULL, -- 'supply_mng', 'client_mng', 'part_mgmt', ...
legacy_objid VARCHAR(100) NOT NULL, -- 'C_' prefix 포함 가능
target_table VARCHAR(50) NOT NULL, -- 'customer_mng', 'item_info'
target_id INTEGER NOT NULL,
company_code VARCHAR(20) DEFAULT 'COMPANY_16',
migrated_at TIMESTAMP DEFAULT NOW(),
note TEXT,
PRIMARY KEY (legacy_table, legacy_objid)
);
CREATE INDEX idx_legacy_id_map_target ON legacy_id_map(target_table, target_id);
```
## 4. 마이그레이션 절차
### 4.1 마스터 이주 (도메인 이주 전 선행 필수)
#### 거래처 (`SUPPLY_MNG` + `CLIENT_MNG` → `customer_mng`)
```sql
-- 1. SUPPLY_MNG → customer_mng (공급/일반 고객사)
INSERT INTO customer_mng (
customer_code, customer_name, division, business_number,
address, contact_person, contact_phone, email,
company_code, writer, created_date
)
SELECT
'CUST-' || lpad(objid::text, 6, '0'), -- 임시 코드, 추후 채번 룰로 대체
supply_name,
code_name(area_cd), -- 또는 별도 코드 매핑
bus_reg_no,
address,
charge_user_name,
charge_phone, -- 컬럼명 확인 필요
charge_email,
'COMPANY_16',
'migration',
CURRENT_TIMESTAMP
FROM wace_plm_dump.supply_mng
ON CONFLICT DO NOTHING;
-- 2. 매핑 테이블 기록
INSERT INTO legacy_id_map (legacy_table, legacy_objid, target_table, target_id)
SELECT 'supply_mng', s.objid::text, 'customer_mng', c.id
FROM wace_plm_dump.supply_mng s
JOIN customer_mng c ON c.customer_name = s.supply_name AND c.business_number = s.bus_reg_no;
-- 3. CLIENT_MNG → customer_mng (일반 거래처)
INSERT INTO customer_mng (
customer_code, customer_name, division, business_number,
address, contact_person, contact_phone, email,
company_code, writer, created_date
)
SELECT
'CUST-' || lpad((objid + 1000000)::text, 7, '0'), -- supply와 충돌 방지
client_nm,
'general_client',
bus_reg_no,
address, -- 컬럼명 확인
ceo_nm,
NULL, NULL,
'COMPANY_16',
'migration',
CURRENT_TIMESTAMP
FROM wace_plm_dump.client_mng;
-- 4. CLIENT_MNG 매핑 (legacy_objid는 'C_' prefix)
INSERT INTO legacy_id_map (legacy_table, legacy_objid, target_table, target_id)
SELECT 'client_mng', 'C_' || cl.objid::text, 'customer_mng', cn.id
FROM wace_plm_dump.client_mng cl
JOIN customer_mng cn ON cn.customer_name = cl.client_nm
AND cn.division = 'general_client';
```
#### 품목 (`PART_MGMT` → `item_info`)
vexplor_rps `item_info` 스키마 확인 후 동일 패턴 적용. PRODUCT_MGMT가 별도 도메인이면 합치거나 분리.
### 4.2 도메인 이주 시 변환 패턴
```sql
-- contract_mgmt 이주 예시
INSERT INTO contract_mgmt (
objid, contract_no,
customer_objid_legacy, -- 원본 보존
customer_id, -- vexplor_rps 매핑된 customer_mng.id
category_cd, area_cd, ...
)
SELECT
cm.objid,
cm.contract_no,
cm.customer_objid, -- 원본
m.target_id, -- 매핑된 id
cm.category_cd, cm.area_cd, ...
FROM wace_plm_dump.contract_mgmt cm
LEFT JOIN legacy_id_map m
ON m.legacy_objid = cm.customer_objid
AND m.legacy_table = CASE WHEN cm.customer_objid LIKE 'C_%' THEN 'client_mng' ELSE 'supply_mng' END
AND m.target_table = 'customer_mng';
```
> 권장: 원본 컬럼(`customer_objid`)을 그대로 남기고 + 신규 `customer_id` 컬럼을 추가해서 디버깅·역추적 쉽게 함. SQL 조회 시엔 `customer_id` 사용.
### 4.3 코드 마스터 매핑
wace_plm `COMM_CODE``(code_id, code_value, code_name)` 구조.
vexplor_rps의 코드 시스템 확인 후:
- 동일 키로 매핑 가능하면 그대로 INSERT
- 다르면 코드 매핑 시드 작성
자주 쓰는 그룹:
- `category_cd` (주문유형)
- `area_cd` (지역/국내해외)
- `paid_type` (유/무상)
- `contract_currency` (환종)
- `contract_result` (수주상태)
- `appr_status` (결재상태)
- `est_status` (견적상태)
### 4.4 첨부파일
`attach_file_info`는 파일 메타. 실제 파일은 `/data_storage` 등 디스크에 있음.
이주 시:
1. 메타데이터 INSERT (target_objid는 그대로 유지)
2. 실제 파일 복사 (또는 vexplor_rps 파일 시스템에 맞게 경로 변환)
## 5. 사용자(USER_INFO) 매핑
wace_plm `USER_INFO.user_id` 와 vexplor_rps `user_info.user_id`가 동일 사람을 가리킨다는 보장이 없음.
**확인 사항**:
- 두 시스템 사용자 ID 체계가 통합되어 있는가?
- 통합되어 있지 않다면, `legacy_user_map (legacy_user_id, target_user_id)` 별도 필요
**확인 방법**: 양쪽 USER_INFO 테이블의 user_id, user_name, dept_code, email 비교.
## 6. 마이그레이션 실행 체크리스트
- [ ] vexplor_rps `customer_mng`, `item_info`, `user_info`, `code_*` 스키마 확인 후 본 문서 컬럼명 보정
- [ ] wace_plm 운영 DB에서 마스터 4종 (`supply_mng`, `client_mng`, `part_mgmt`, `user_info`) schema-only dump 추출
- [ ] 데이터 dump 추출 (또는 staging DB에 운영 데이터 복제)
- [ ] `legacy_id_map` 테이블 생성
- [ ] 거래처 이주 + 매핑 기록
- [ ] 품목 이주 + 매핑 기록
- [ ] 사용자 매핑 검토 (이주 또는 별도 매핑)
- [ ] 코드 마스터 매핑 (시드 SQL)
- [ ] 도메인 테이블 이주 (각 메뉴별 01–04 문서 순서로)
- [ ] 검증 SQL: 도메인 테이블 행 수, NULL 매핑 행 (legacy_id_map JOIN 실패) 카운트
## 7. 주의 / 결정 보류
- **vexplor_rps 마스터 컬럼명** 본 문서 추정치임. 실제 컬럼명/타입은 `customer_mng` 정의([docs/customer-management-tables.md](../../customer-management-tables.md)) 와 `item_info`, `user_info` 정의 보고 보정.
- **PRODUCT_MGMT** vs PART_MGMT: wace_plm에서 둘 다 사용되며 다른 도메인일 가능성. 추후 분석 필요.
- **OEM_MNG**: 자동차 OEM 정보. RPS 도메인에선 불필요해 보이지만 contract_mgmt에 일부 컬럼이 참조할 수 있음 — 확인 필요.
- **운영 데이터 vs 신규 데이터**: 기존 운영 데이터를 모두 이주할 것인지, 신규 데이터만 RPS에 쌓을 것인지 결정 필요. 전자라면 본 문서대로, 후자라면 매핑 테이블 없이 단순 도메인 스키마 이식만으로 충분.
+146
View File
@@ -0,0 +1,146 @@
# 영업관리 이식 (wace_plm → vexplor_rps)
> 작성: 2026-05-07 / 작성자: hjjeong
> 대상: vexplor_rps (RPS 전용 분기, COMPANY_16 단독 운영)
> 원본: wace_plm (Java 7 / Spring 3.2.4 / JSP / MyBatis)
## 0. 정책 (사용자 확정 사항)
- **이식 방식**: JSP → Next.js 리라이트 (백엔드도 Java→Node 재작성, vexplor_rps `backend-node` 패턴 채택)
- **스키마 정책**: **하이브리드** — 도메인 테이블(`contract_mgmt`, `estimate_mgmt`, `sales_registration` 등)은 wace_plm 원본 스키마를 그대로 가져오고, 거래처/품목 등 **마스터는 vexplor_rps 기존 테이블(`customer_mng`, `item_info`)에 매핑**
- **관리자 메뉴**: 이식 대상 아님 (vexplor 그대로 사용)
- **이식 대상 메뉴 4개**:
1. 견적관리 (`/contractMgmt/estimateList_new.do`)
2. 주문서관리 (`/contractMgmt/orderMgmtList.do`)
3. 판매관리 (`/contractMgmt/salesMgmtList.do` → SalesNcollect로 위임)
4. 매출관리 (`/revenueMgmt/revenueList.do`)
## 1. 메뉴 매핑표
| # | 메뉴명 | wace_plm URL | wace_plm JSP | wace_plm Controller / Service | vexplor_rps 신규 위치 (제안) | 상세 문서 |
|---|---|---|---|---|---|---|
| 1 | 견적관리 | `/contractMgmt/estimateList_new.do` | `contractMgmt/estimateList_new.jsp` (47KB) | `ContractMgmtController` (16292510 line) / `ContractMgmtService` | `app/(main)/COMPANY_16/sales/estimate/page.tsx` + `backend-node/src/{routes,services}/estimateRoutes.ts` | [01-estimate.md](./01-estimate.md) |
| 2 | 주문서관리 | `/contractMgmt/orderMgmtList.do` | `contractMgmt/orderMgmtList.jsp` (45KB) | `ContractMgmtController` (25043169 line) / `ContractMgmtService` | `app/(main)/COMPANY_16/sales/order/page.tsx` (재작성) + `backend-node/src/{routes,services}/orderMgmtRoutes.ts` | [02-order.md](./02-order.md) |
| 3 | 판매관리 | `/contractMgmt/salesMgmtList.do` (위임) → `/salesNcollectMgmt/sales.do` | `salesmgmt/salesMgmt/*.jsp` | `SalesNcollectMgmtController` (line 763~) / `SalesNcollectMgmtService` | `app/(main)/COMPANY_16/sales/sale/page.tsx` + `backend-node/src/{routes,services}/saleRoutes.ts` | [03-sale.md](./03-sale.md) |
| 4 | 매출관리 | `/revenueMgmt/revenueList.do` | `salesmgmt/salesMgmt/revenueMgmtList.jsp` | `SalesNcollectMgmtController` (line 103, 214) / `SalesNcollectMgmtService` | `app/(main)/COMPANY_16/sales/revenue/page.tsx` + `backend-node/src/{routes,services}/revenueRoutes.ts` | [04-revenue.md](./04-revenue.md) |
| ─ | 마스터 매핑 | (전 메뉴 공통) | — | — | — | [05-master-mapping.md](./05-master-mapping.md) |
> ⚠️ vexplor_rps의 기존 [sales/quote](../../../frontend/app/(main)/COMPANY_16/sales/quote/page.tsx)/[sales/order](../../../frontend/app/(main)/COMPANY_16/sales/order/page.tsx) 페이지는 별도 도메인(`quote_mng`/`quote_detail`)으로 만들어져 있음. 이식 후 **사용 중지** 또는 **별도 모듈로 이름 변경** 검토 필요. 신규 페이지는 `estimate/`, `order/` 신규 경로로 작성하는 것을 권장.
## 2. 도메인 테이블 (wace_plm → vexplor_rps 그대로 이식)
이식 대상 테이블. 새 vexplor_rps DB에 **CREATE TABLE 그대로 적용** (컬럼명/타입 유지).
| 우선순위 | 테이블 | 용도 | dbexport.pgsql line |
|---|---|---|---|
| ★★★ | `contract_mgmt` | 계약/주문서 헤더 | 2488 |
| ★★★ | `contract_item` | 계약/주문서 상세 라인 | (DDL: `database/contract_item_tables.sql`) |
| ★★★ | `estimate_mgmt` | 견적 헤더 (구버전) | 4340 |
| ★★★ | `estimate_template` | 견적 템플릿 = **새 견적의 헤더**(estimateList_new가 사용) | (CREATE 미발견, 운영 DB 추출 필요) |
| ★★★ | `estimate_template_item` | 견적 템플릿 라인 = **새 견적의 상세** | (CREATE 미발견, 운영 DB 추출 필요) |
| ★★ | `counselingmgmt` | 상담관리 — **이식 대상 아님** (사용자 확정) | 2989 |
| ★★ | `contract_mgmt_option` | 주문 옵션 | 2899 |
| ★★ | `contract_item_serial` | 주문 라인 시리얼 | (DDL: `database/contract_item_tables.sql`) |
| ★★ | `sales_registration` | 판매 등록 (= 판매·매출 집계 원장) | (dbexport에서 위치 확인 필요) |
| ★ | `attach_file_info` | 첨부파일 (PDF/이미지) | 1387 |
| ★ | `approval` | 결재 | 507 |
| ★ | `mail_log` | 메일 발송 로그 (견적 메일링) | (필요 시) |
| ★ | `pms_pjt_year_goal` | 연도 목표 (대시보드용) | (필요 시) |
### ✅ 운영 DB DDL 추출 완료 (2026-05-07)
운영 DB(`211.115.91.141:11133/waceplm` PG 16.8)에서 누락된 5개 테이블을 추출하여 [ddl-extracted/](./ddl-extracted/)에 정리. 주요 발견:
- `estimate_template` / `estimate_template_item` 발견 → [100_create_estimate_template.sql](./ddl-extracted/100_create_estimate_template.sql)
- `sales_registration` / `shipment_log` 발견 → [101_create_sales_registration.sql](./ddl-extracted/101_create_sales_registration.sql)
- `mail_log` 발견 → [102_create_mail_log.sql](./ddl-extracted/102_create_mail_log.sql)
- `final_data`, `end_count`, `transaction_statement_*`, `tax_invoice_*`**존재하지 않음**
- 매출관리의 마감/세금계산서/수출신고 컬럼은 모두 `shipment_log`에 통합되어 있음
- 운영 데이터 카운트: 견적 5건, 견적라인 7건, 판매 10건, 분할출하 0건, 메일로그 7,805건 → **도메인은 사실상 신규 시스템, 데이터 이주 부담 거의 없음**
자세한 내용은 [ddl-extracted/README.md](./ddl-extracted/README.md).
## 3. 마스터 매핑 (wace_plm 마스터 → vexplor_rps 마스터로 변환)
| wace_plm 테이블 | wace_plm 키 컬럼 | vexplor_rps 테이블 | vexplor_rps 키 | 변환 규칙 |
|---|---|---|---|---|
| `SUPPLY_MNG` (공급/고객사 통합) | `objid` (numeric) | `customer_mng` | `id` (integer) | 양방향 매핑 테이블 필요 (`legacy_supply_objid → customer_mng.id`) |
| `CLIENT_MNG` (일반 거래처) | `objid` (numeric) | `customer_mng` | `id` | 위와 동일 (wace_plm은 `customer_objid``'C_'` 접두사로 두 테이블 구분) |
| `USER_INFO` | `user_id` | `user_info` | `user_id` | 동일 키 사용 가능 (있다면 그대로) |
| `DEPT_INFO` | `dept_code` | (vexplor_rps `department`) | (확인 필요) | TBD |
| `PART_MGMT` / `PRODUCT_MGMT` | `objid` | `item_info` | `id` | 매핑 테이블 필요 |
| `ATTACH_FILE_INFO` | `target_objid` + `doc_type` | (vexplor_rps 파일 시스템) | (확인 필요) | TBD |
### 마스터 마이그레이션 절차 (제안)
1. wace_plm `SUPPLY_MNG` + `CLIENT_MNG``customer_mng`로 INSERT (legacy_objid 컬럼 추가)
2. wace_plm `PART_MGMT` (+ `PRODUCT_MGMT`?) → `item_info`로 INSERT (legacy_part_objid 컬럼 추가)
3. 도메인 테이블(`contract_mgmt`, `estimate_mgmt` 등) 이식 시 외래 컬럼은 wace_plm objid를 그대로 가져오고, 별도 매핑 테이블(`legacy_id_map`)을 통해 vexplor_rps 마스터 id로 변환
## 4. 백엔드 패턴 (vexplor_rps `backend-node`)
### 라우트 (예: `quoteRoutes.ts` 패턴)
```ts
import { Router } from "express";
import { authenticateToken } from "../middleware/authMiddleware";
import * as ctrl from "../controllers/<feature>Controller";
const router = Router();
router.use(authenticateToken);
router.get("/list", ctrl.getList);
router.get("/generate-number", ctrl.generateNumber);
router.get("/:id", ctrl.getById);
router.post("/", ctrl.create);
router.put("/:id", ctrl.update);
router.delete("/:id", ctrl.remove);
export default router;
```
### 서비스 (예: `quoteService.ts` 패턴)
- `getPool()` (PG raw)
- companyCode 멀티테넌시: `WHERE company_code = $1` (RPS는 `COMPANY_16` 고정)
- 소프트삭제: `use_yn = 'Y'`
- 트랜잭션: `pool.connect()``BEGIN/COMMIT/ROLLBACK`
- 자동 채번: `generateNumber()` (예: `EST-YYYYMMDD-001`)
### 컨트롤러
- 인증: `req.user.companyCode`, `req.user.userId` 사용
- 응답: `res.json({ data, totalCount })` 또는 `res.json({ success, message })`
## 5. 프론트엔드 패턴 (vexplor_rps Next.js)
```tsx
"use client";
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
// ... shadcn/ui Button, Dialog, Select, Input, Label ...
```
- 검색 필터: `DynamicSearchFilter` (테이블 카테고리/공통코드 자동 바인딩)
- 그리드: `DataGrid` (선택, 정렬, 포맷팅)
- 채번 / 단일 조회 / 저장 / 삭제: `apiClient.get/post/put/delete`
- 권한: `useCurrent2ndLevelMenuObjid` + 관리자에서 권한 매핑
## 6. 진행 체크리스트 (메뉴별)
각 메뉴 1개 이식 = 다음 9단계.
- [ ] (a) 운영 DB에서 해당 테이블 schema-only dump 추출
- [ ] (b) `db/migrations/``NNN_create_<feature>_tables.sql` 작성
- [ ] (c) wace_plm Controller endpoint 목록 추출 → `backend-node/src/routes/<feature>Routes.ts` 매핑표 작성
- [ ] (d) `backend-node/src/services/<feature>Service.ts` 작성 (raw SQL)
- [ ] (e) `backend-node/src/controllers/<feature>Controller.ts` 작성
- [ ] (f) `backend-node/src/server.ts` 또는 라우트 등록 위치에 마운트
- [ ] (g) `frontend/app/(main)/COMPANY_16/sales/<feature>/page.tsx` 작성 (DataGrid + Filter + Modal)
- [ ] (h) 메뉴 트리에 등록 (vexplor_rps 메뉴 관리 화면 또는 메뉴 시드)
- [ ] (i) 권한 매핑 + 수동 테스트
## 7. 다음 작업
1. **운영 DB 접속해서 누락 테이블 DDL 추출** (`estimate_template`, `estimate_template_item`, `sales_registration`)
2. [01-estimate.md](./01-estimate.md) 견적관리 상세 매핑 작성 → 코드 시작
3. 마스터 매핑 테이블 설계 (`legacy_id_map`)
@@ -0,0 +1,78 @@
-- ====================================================================
-- estimate_template / estimate_template_item
-- ====================================================================
-- 출처: wace_plm 운영 DB 211.115.91.141:11133/waceplm (PG 16.8)
-- 추출일: 2026-05-07
-- 견적관리(estimateList_new)의 메인 테이블 — 신규 견적의 헤더+라인을 담당.
-- contract_objid 로 contract_mgmt에 묶여 1 contract → N templates (template_type별).
-- ====================================================================
CREATE TABLE IF NOT EXISTS estimate_template (
objid VARCHAR(50) PRIMARY KEY,
contract_objid VARCHAR(50) NOT NULL, -- → contract_mgmt(objid)
template_type VARCHAR(10) NOT NULL, -- '1'=일반, '2'=장비
executor VARCHAR(200), -- 발주자
recipient VARCHAR(200), -- 수신자
estimate_no VARCHAR(100), -- 견적번호
contact_person VARCHAR(100), -- 담당자
greeting_text TEXT, -- 인사말
model_name VARCHAR(200),
model_code VARCHAR(100),
executor_date VARCHAR(50),
note1 VARCHAR(500),
note2 VARCHAR(500),
note3 VARCHAR(500),
note4 VARCHAR(500),
categories_json TEXT, -- 그룹/소그룹 구조 JSON
writer VARCHAR(50),
regdate TIMESTAMP DEFAULT now(),
chg_user_id VARCHAR(50),
chgdate TIMESTAMP DEFAULT now(),
notes_content TEXT, -- 본문 비고
validity_period VARCHAR(50), -- 유효기간
total_amount VARCHAR(50),
total_amount_krw VARCHAR(50),
manager_name VARCHAR(100), -- 담당자 이름
manager_contact VARCHAR(100), -- 담당자 연락처
note_remarks TEXT, -- 테이블 내 비고 (품목 하단)
show_total_row VARCHAR(1) DEFAULT 'Y', -- 계 행 표시 여부 (Y/N)
group1_subtotal VARCHAR(50),
part_name VARCHAR(200),
part_objid VARCHAR(50) -- → item_info(legacy mapping)
);
CREATE INDEX IF NOT EXISTS idx_estimate_contract ON estimate_template(contract_objid);
CREATE INDEX IF NOT EXISTS idx_estimate_type ON estimate_template(template_type);
COMMENT ON TABLE estimate_template IS '견적 템플릿 (estimateList_new의 메인)';
COMMENT ON COLUMN estimate_template.manager_name IS '담당자 이름';
COMMENT ON COLUMN estimate_template.manager_contact IS '담당자 연락처';
COMMENT ON COLUMN estimate_template.note_remarks IS '테이블 내 비고 (품목 하단)';
COMMENT ON COLUMN estimate_template.show_total_row IS '계 행 표시 여부 (Y: 표시, N: 숨김)';
-- --------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS estimate_template_item_objid_seq;
CREATE TABLE IF NOT EXISTS estimate_template_item (
objid INTEGER PRIMARY KEY DEFAULT nextval('estimate_template_item_objid_seq'),
template_objid VARCHAR(50) NOT NULL, -- → estimate_template(objid)
seq INTEGER NOT NULL,
category VARCHAR(100), -- 그룹명
description VARCHAR(500), -- 품명
specification TEXT, -- 규격
quantity VARCHAR(50),
unit VARCHAR(50),
unit_price VARCHAR(50),
amount VARCHAR(50),
note VARCHAR(500),
remark VARCHAR(500),
part_objid VARCHAR
);
ALTER SEQUENCE estimate_template_item_objid_seq OWNED BY estimate_template_item.objid;
CREATE INDEX IF NOT EXISTS idx_item_template ON estimate_template_item(template_objid);
CREATE INDEX IF NOT EXISTS idx_item_seq ON estimate_template_item(template_objid, seq);
COMMENT ON TABLE estimate_template_item IS '견적 템플릿 라인';
@@ -0,0 +1,105 @@
-- ====================================================================
-- sales_registration / shipment_log
-- ====================================================================
-- 출처: wace_plm 운영 DB 211.115.91.141:11133/waceplm (PG 16.8)
-- 추출일: 2026-05-07
-- 판매관리(sales) + 매출관리(revenue)의 메인 테이블.
-- sales_registration: 프로젝트별 1건 (UNIQUE project_no) — 헤더성격
-- shipment_log: 분할출하/매출 마감 이력 — 라인성격 + 매출 추가 컬럼
-- ====================================================================
CREATE SEQUENCE IF NOT EXISTS sales_registration_sale_no_seq;
CREATE TABLE IF NOT EXISTS sales_registration (
sale_no INTEGER PRIMARY KEY DEFAULT nextval('sales_registration_sale_no_seq'),
project_no VARCHAR(50) NOT NULL UNIQUE,
shipping_order_status VARCHAR(50), -- 출하지시 상태
serial_no TEXT, -- S/N (쉼표 구분)
sales_quantity INTEGER,
sales_unit_price NUMERIC,
sales_supply_price NUMERIC,
sales_vat NUMERIC,
sales_total_amount NUMERIC,
sales_currency VARCHAR(10),
sales_exchange_rate NUMERIC,
shipping_date DATE,
shipping_method VARCHAR(50),
manager_user_id VARCHAR(50),
incoterms VARCHAR(20),
reg_date TIMESTAMP DEFAULT now(),
reg_user_id VARCHAR(50),
upd_date TIMESTAMP,
upd_user_id VARCHAR(50),
has_split_shipment BOOLEAN DEFAULT false -- true: 분할출하 있음
);
ALTER SEQUENCE sales_registration_sale_no_seq OWNED BY sales_registration.sale_no;
CREATE INDEX IF NOT EXISTS idx_sales_reg_project ON sales_registration(project_no);
CREATE INDEX IF NOT EXISTS idx_sales_reg_manager ON sales_registration(manager_user_id);
CREATE INDEX IF NOT EXISTS idx_sales_reg_date ON sales_registration(reg_date DESC);
COMMENT ON TABLE sales_registration IS '판매 등록 정보 - 프로젝트별 판매/출하 데이터 관리';
COMMENT ON COLUMN sales_registration.sale_no IS '판매 일련번호 (PK)';
COMMENT ON COLUMN sales_registration.project_no IS '프로젝트 번호 (UNIQUE)';
COMMENT ON COLUMN sales_registration.shipping_order_status IS '출하지시 상태';
COMMENT ON COLUMN sales_registration.serial_no IS 'S/N (쉼표 구분)';
COMMENT ON COLUMN sales_registration.has_split_shipment IS '분할출하 여부 (true: 분할출하 있음)';
-- --------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS shipment_log_log_id_seq;
CREATE TABLE IF NOT EXISTS shipment_log (
log_id INTEGER PRIMARY KEY DEFAULT nextval('shipment_log_log_id_seq'),
target_objid VARCHAR(50) NOT NULL, -- 대상 계약 OBJID (→ contract_mgmt)
log_type VARCHAR(50) NOT NULL, -- SPLIT_SHIPMENT 등
log_message TEXT,
split_quantity INTEGER, -- 분할 수량
original_quantity INTEGER, -- 원본 수량
remaining_quantity INTEGER, -- 잔여 수량
reg_date TIMESTAMP DEFAULT now(),
reg_user_id VARCHAR(50),
-- 출하 정보
shipping_status VARCHAR(20) DEFAULT 'PENDING', -- PENDING/COMPLETED/CANCELLED
shipping_date DATE,
shipping_method VARCHAR(50), -- DIRECT/PARCEL
-- 판매 금액
sales_unit_price NUMERIC(15,2),
sales_supply_price NUMERIC(15,2),
sales_vat NUMERIC(15,2),
sales_total_amount NUMERIC(15,2),
sales_currency VARCHAR(10),
sales_exchange_rate NUMERIC(15,4),
manager_user_id VARCHAR(50),
incoterms VARCHAR(20),
remark TEXT,
-- 분할 추적
is_split_record BOOLEAN DEFAULT true, -- 분할출하 레코드 여부
serial_no TEXT,
parent_sale_no INTEGER, -- → sales_registration(sale_no)
-- ★ 매출관리 전용 컬럼들 (revenueMode=Y에서 입력) ★
sales_deadline_date VARCHAR(10), -- 매출 마감일
tax_type VARCHAR(20), -- 과세구분
tax_invoice_date VARCHAR(10), -- 세금계산서 발행일
export_decl_no VARCHAR(100), -- 수출신고필증 번호
loading_date VARCHAR(10), -- 선적일자
sales_slip_date VARCHAR(10), -- 매출전표일
sales_slip_menu_sq INTEGER, -- 매출전표 menu_sq
split_serial_no TEXT -- 분할 S/N (콤마 구분)
);
ALTER SEQUENCE shipment_log_log_id_seq OWNED BY shipment_log.log_id;
CREATE INDEX IF NOT EXISTS idx_shipment_log_objid ON shipment_log(target_objid);
CREATE INDEX IF NOT EXISTS idx_shipment_log_type ON shipment_log(log_type);
CREATE INDEX IF NOT EXISTS idx_shipment_log_date ON shipment_log(reg_date DESC);
CREATE INDEX IF NOT EXISTS idx_shipment_log_parent ON shipment_log(parent_sale_no);
COMMENT ON TABLE shipment_log IS '분할출하 이력 로그';
COMMENT ON COLUMN shipment_log.log_id IS '로그 ID';
COMMENT ON COLUMN shipment_log.target_objid IS '대상 계약 OBJID';
COMMENT ON COLUMN shipment_log.log_type IS '로그 유형 (SPLIT_SHIPMENT 등)';
COMMENT ON COLUMN shipment_log.shipping_status IS '출하상태 (PENDING, COMPLETED, CANCELLED)';
COMMENT ON COLUMN shipment_log.parent_sale_no IS '원본 판매번호 (sales_registration.sale_no)';
COMMENT ON COLUMN shipment_log.split_serial_no IS '분할S/N - 해당 출하건에서 선택한 S/N 목록 (콤마 구분)';
@@ -0,0 +1,30 @@
-- ====================================================================
-- mail_log
-- ====================================================================
-- 출처: wace_plm 운영 DB 211.115.91.141:11133/waceplm (PG 16.8)
-- 추출일: 2026-05-07
-- 메일 발송 이력 (견적서 메일링 등 전 시스템 공용).
-- 운영: 7,805건 (시스템 전반에서 사용 중)
-- 주의: PK 없음. 신규 마이그레이션 시 BIGSERIAL 추가 권장.
-- ====================================================================
CREATE TABLE IF NOT EXISTS mail_log (
objid VARCHAR, -- (원본 PK 없음)
system_name VARCHAR(32), -- 발송 시스템명
send_user_id VARCHAR(64), -- 발신자 ID
from_addr VARCHAR(256), -- 발신 주소
reception_user_id VARCHAR(64), -- 수신자 ID
receiver_to VARCHAR(256), -- 수신 주소
title VARCHAR(512), -- 제목
contents TEXT, -- 본문
log_time TIMESTAMP, -- 발송 시각
is_send VARCHAR(8), -- 발송 결과 ('Y'/'N')
mail_type VARCHAR(32), -- 메일 종류 (estimate 등)
error_log TEXT -- 실패 시 에러 로그
);
CREATE INDEX IF NOT EXISTS idx_mail_log_time ON mail_log(log_time DESC);
CREATE INDEX IF NOT EXISTS idx_mail_log_type ON mail_log(mail_type);
CREATE INDEX IF NOT EXISTS idx_mail_log_send_user ON mail_log(send_user_id);
COMMENT ON TABLE mail_log IS '메일 발송 이력 (전 시스템 공용)';
@@ -0,0 +1,163 @@
-- ====================================================================
-- contract_mgmt / contract_item / contract_item_serial / contract_mgmt_option
-- ====================================================================
-- 출처: wace_plm 운영 DB 211.115.91.141:11133/waceplm (PG 16.8) 2026-05-07 추출
-- 영업관리 4개 메뉴(견적/주문/판매/매출) 모두의 헤더 테이블.
-- contract_mgmt 가 영업번호(contract_no) 단위 헤더, contract_item이 라인.
-- ⚠️ part_objid 는 vexplor_rps item_info.id (varchar 500) 와 직접 매칭 가능.
-- ====================================================================
CREATE TABLE IF NOT EXISTS contract_mgmt (
objid VARCHAR(50) PRIMARY KEY,
contract_no VARCHAR, -- 영업번호 (CONTRACT_NO 채번)
-- 분류/마스터 참조
category_cd VARCHAR, -- 주문유형 (공통코드)
customer_objid VARCHAR, -- 거래처 ('C_'+id 또는 id::text)
product VARCHAR, -- 제품구분
customer_project_name VARCHAR,
project_no VARCHAR, -- 프로젝트 번호
project_name VARCHAR,
target_project_no VARCHAR,
target_project_no_direct VARCHAR,
customer_production_no VARCHAR,
-- 상태
status_cd VARCHAR,
result_cd VARCHAR,
contract_result VARCHAR,
spec_result_cd VARCHAR,
est_result_cd VARCHAR,
sales_closure_status VARCHAR,
production_status VARCHAR(50),
-- 일자 (모두 varchar)
due_date VARCHAR,
plan_date VARCHAR,
complete_date VARCHAR,
spec_plan_date VARCHAR,
spec_comp_date VARCHAR,
est_plan_date VARCHAR,
est_comp_date VARCHAR,
contract_date VARCHAR, -- 발주일
contract_del_date VARCHAR,
req_del_date VARCHAR, -- 요청납기
receipt_date VARCHAR, -- 접수일
order_date VARCHAR,
-- 사람
chg_user_id VARCHAR,
spec_user_id VARCHAR,
est_user_id VARCHAR,
pm_user_id VARCHAR,
writer VARCHAR,
regdate TIMESTAMP,
-- 위치/시설/장비
location VARCHAR,
setup VARCHAR,
facility VARCHAR,
facility_qty VARCHAR,
facility_type VARCHAR,
facility_depth VARCHAR,
production_no VARCHAR,
bus_cal_cd VARCHAR,
category1_cd VARCHAR,
customer_equip_name VARCHAR,
contract_company VARCHAR,
manufacture_plant VARCHAR,
mechanical_type VARCHAR,
overhaul_order VARCHAR,
area_cd VARCHAR, -- 지역/국내해외
paid_type VARCHAR, -- paid/free
-- 가격
contract_price VARCHAR,
contract_price_currency VARCHAR,
contract_currency VARCHAR,
exchange_rate VARCHAR,
est_price VARCHAR,
est_supply_price VARCHAR,
order_unit_price VARCHAR,
order_supply_price VARCHAR,
order_vat VARCHAR,
order_total_amount VARCHAR,
-- 발주
po_no VARCHAR, -- 발주번호
-- 라인 헤더 카피
part_no VARCHAR,
part_name VARCHAR,
serial_no VARCHAR,
quantity VARCHAR,
customer_request VARCHAR,
-- 출하/배송
shipping_method VARCHAR,
incoterms VARCHAR,
-- 분기/플래그
is_direct_order VARCHAR,
approval_required VARCHAR(1) DEFAULT 'N',
split_from_objid VARCHAR(50)
);
CREATE INDEX IF NOT EXISTS idx_contract_split_from ON contract_mgmt(split_from_objid);
COMMENT ON TABLE contract_mgmt IS '영업/계약 헤더 (견적/주문/판매/매출 공통)';
-- --------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS contract_item (
objid VARCHAR(50) PRIMARY KEY,
contract_objid VARCHAR(50) NOT NULL REFERENCES contract_mgmt(objid) ON DELETE CASCADE,
seq INTEGER NOT NULL,
part_objid VARCHAR(50) NOT NULL, -- → item_info(id)
part_no VARCHAR(100) NOT NULL,
part_name VARCHAR(200) NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
due_date VARCHAR(10),
customer_request TEXT,
-- 수주 컬럼 (database/add_order_columns_to_contract_item.sql)
order_quantity VARCHAR(50),
order_unit_price VARCHAR(50),
order_supply_price VARCHAR(50),
order_vat VARCHAR(50),
order_total_amount VARCHAR(50),
cancel_qty VARCHAR(50),
return_reason VARCHAR,
product VARCHAR(20),
-- 공통
regdate TIMESTAMP NOT NULL DEFAULT NOW(),
writer VARCHAR(50),
chgdate TIMESTAMP,
chg_user_id VARCHAR(50),
status VARCHAR(20) DEFAULT 'ACTIVE'
);
CREATE INDEX IF NOT EXISTS idx_contract_item_contract ON contract_item(contract_objid);
CREATE INDEX IF NOT EXISTS idx_contract_item_seq ON contract_item(contract_objid, seq);
COMMENT ON TABLE contract_item IS '영업/계약 라인 (견적/주문 공통)';
-- --------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS contract_item_serial (
objid VARCHAR(50) PRIMARY KEY,
item_objid VARCHAR(50) NOT NULL REFERENCES contract_item(objid) ON DELETE CASCADE,
seq INTEGER NOT NULL,
serial_no VARCHAR(200) NOT NULL,
regdate TIMESTAMP NOT NULL DEFAULT NOW(),
writer VARCHAR(50),
status VARCHAR(20) DEFAULT 'ACTIVE',
UNIQUE (item_objid, serial_no)
);
CREATE INDEX IF NOT EXISTS idx_contract_item_serial_item ON contract_item_serial(item_objid);
CREATE INDEX IF NOT EXISTS idx_contract_item_serial_seq ON contract_item_serial(item_objid, seq);
COMMENT ON TABLE contract_item_serial IS '계약 라인별 시리얼번호';
-- --------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS contract_mgmt_option (
objid INTEGER NOT NULL,
contract_objid INTEGER NOT NULL,
option_objid INTEGER,
option_qty INTEGER,
price INTEGER,
note VARCHAR
);
COMMENT ON TABLE contract_mgmt_option IS '계약 옵션';
@@ -0,0 +1,90 @@
# 추출된 DDL (wace_plm 운영 DB)
> 추출일: 2026-05-07
> 출처: `211.115.91.141:11133/waceplm` (PostgreSQL 16.8)
> 추출 방법: information_schema 쿼리 (pg_dump 14↔16 버전 불일치로 직접 추출 불가)
`db/dbexport.pgsql`(2026-03-26 시점)에 누락된 테이블을 운영 DB에서 직접 추출해 재구성.
## 파일 목록
| 파일 | 테이블 | 비고 |
|---|---|---|
| `100_create_estimate_template.sql` | `estimate_template`, `estimate_template_item` | 견적관리(estimateList_new) 메인. 운영 5건 |
| `101_create_sales_registration.sql` | `sales_registration`, `shipment_log` | 판매·매출관리 메인. 운영 10건 / 0건 |
| `102_create_mail_log.sql` | `mail_log` | 메일 이력. 운영 7,805건. **PK 없음** |
| `103_create_contract_mgmt.sql` | `contract_mgmt`, `contract_item`, `contract_item_serial`, `contract_mgmt_option` | 영업/계약 헤더+라인+시리얼+옵션 (견적/주문/판매/매출 공통 헤더) |
## 운영 데이터 카운트 (참고)
```
estimate_template : 5
estimate_template_item : 7
sales_registration : 10
shipment_log : 0
mail_log : 7,805
```
→ 견적/판매 도메인은 사실상 **신규 시스템**. 데이터 이주 부담 거의 없음.
## 핵심 발견
### 1. 매출관리(revenue)는 별도 테이블이 아님
기존 04-revenue.md에서는 `final_data`, `end_count`, `tax_invoice_*`, `transaction_statement_*` 같은 별도 테이블을 가정했으나, 실제로는 모두 **`shipment_log` 컬럼**으로 존재:
| 매출관리 항목 | 위치 |
|---|---|
| 매출마감일 | `shipment_log.sales_deadline_date` (varchar 10) |
| 과세구분 | `shipment_log.tax_type` |
| 세금계산서 발행일 | `shipment_log.tax_invoice_date` |
| 수출신고필증 번호 | `shipment_log.export_decl_no` |
| 선적일자 | `shipment_log.loading_date` |
| 매출전표일 | `shipment_log.sales_slip_date` |
| 매출전표 menu_sq | `shipment_log.sales_slip_menu_sq` |
→ 마감/세금계산서/수출신고는 **`shipment_log` 행 INSERT/UPDATE**로 처리.
### 2. sales_registration ↔ shipment_log 관계
- `sales_registration` (PK `sale_no` BIGSERIAL, UNIQUE `project_no`) — **프로젝트당 1건** 헤더
- `shipment_log` (PK `log_id` BIGSERIAL, FK `parent_sale_no` → sales_registration.sale_no) — 분할출하·매출 이력 라인
- `is_split_record` 기본값 `true` — 즉 shipment_log는 기본이 분할 레코드 가정
- `target_objid``contract_mgmt.objid` (FK처럼 사용)
### 3. estimate_template은 contract_objid로 묶임
- 1 contract → N templates (`template_type='1'`=일반, `'2'`=장비, 추가가능)
- `categories_json` 컬럼에 그룹/소그룹 구조 저장 (JSON)
- `manager_name`, `manager_contact`, `note_remarks`, `show_total_row` 등은 `database/add_manager_info_to_estimate_template.sql`에 별도 ALTER로 추가됐던 컬럼들 — DDL에 통합됨
### 4. mail_log는 PK 없음
운영에서 PK 없이 사용 중. vexplor_rps에는 마이그레이션 시 `id BIGSERIAL PRIMARY KEY` 추가 권장.
## 사용
```bash
# vexplor_rps DB에 적용 (테스트 환경 먼저 권장)
psql -h <vexplor_rps_host> -U <user> -d <db> -f 100_create_estimate_template.sql
psql -h <vexplor_rps_host> -U <user> -d <db> -f 101_create_sales_registration.sql
psql -h <vexplor_rps_host> -U <user> -d <db> -f 102_create_mail_log.sql
```
`db/migrations/` 정식 위치로 옮길 때는 vexplor_rps 마이그레이션 번호 체계에 맞춰 파일명 변경.
## 추출 명령 재현
서버가 PG 16, 로컬 pg_dump가 PG 14인 경우 pg_dump가 실패하므로 information_schema 쿼리로 추출:
```bash
PGPASSWORD='<pwd>' psql -h 211.115.91.141 -p 11133 -U postgres -d waceplm -t -A -F '|' -c "
SELECT table_name||'|'||column_name||'|'||data_type||'|'||COALESCE(character_maximum_length::text,'')||'|'||is_nullable||'|'||COALESCE(column_default,'')
FROM information_schema.columns
WHERE table_schema='public' AND table_name IN ('estimate_template','estimate_template_item','sales_registration','shipment_log','mail_log')
ORDER BY table_name, ordinal_position;"
```
PK/UNIQUE는 `information_schema.table_constraints` + `key_column_usage`,
인덱스는 `pg_indexes`, 시퀀스는 `information_schema.sequences`,
컬럼 코멘트는 `pg_description` JOIN.