# 영업관리 이식 GAP 분석 (원본 wace_plm 대비)

> 작성: 2026-05-08 / 작성자: hjjeong
> 목적: vexplor_rps에 이식된 영업관리 4개 메뉴가 wace_plm 원본 흐름과 어디서 어긋나는지 정리하고, 다음 PR 우선순위를 합의하기 위한 단일 문서.
> 참고: [01-estimate.md](./01-estimate.md), [02-order.md](./02-order.md), [feedback_wace_jsp_columns](../../../../.claude/projects/-Users-jhj-vexplor-rps/memory/feedback_wace_jsp_columns.md)

## 0. 한 문장 요약

견적/주문 list와 SQL은 잘 이식됐지만 **상태 전이 트리거**(수주확정 → 프로젝트 자동생성)와 **직접등록 통합폼**, **결재 자동판정**, **PDF·SMTP 실작업**이 통째로 빠져 있어, 사용자가 영업 흐름을 끝까지 돌릴 수 없는 상태.

## 0.1 이식 원칙 (모든 GAP 작업 공통)

> **JSP/Java/매퍼XML 안의 주석 블록(`/* */`, `<!-- -->`, `//`)은 비활성 옛 로직 보존 영역이다 — 절대 이식 대상이 아니다. 활성 코드만, 한 줄 한 줄 직접 따라가서 그대로 이식한다.**

- **운영 화면이 진실의 기준**: waceplm.esgrin.com 운영 화면에 실제 보이는 항목/동작이 활성. 코드만 보면 활성/비활성 구분이 흐려짐.
- **컬럼 정의(`var columns = [...]`)**: `/* 주석처리된 컬럼 - 필요시 활성화 */` 블록 이하는 무시.
- **검색 폼(`#plmSearchZon`)**: `<!-- 주석처리된 검색필터 - 필요시 활성화 -->` 블록 이하는 무시.
- **서비스 메서드**: 주석된 옛 SQL/분기 무시. 호출 그래프(controller → service → mapper)를 한 줄씩 따라가서 활성 경로만 추출.
- **매퍼 XML**: `<!-- ... -->` 블록 안의 SQL fragment는 무시. `<select id="...">`/`<insert id="...">` 단위로 호출되는 것만 사용.
- 자세한 함정 사례: 메모리 [feedback_wace_jsp_columns](../../../../.claude/projects/-Users-jhj-vexplor-rps/memory/feedback_wace_jsp_columns.md) (2026-05-08 영업관리 4개 메뉴 검색폼 사고 기록).

## 1. 원본 견적·주문 흐름 (10단계)

| # | 단계 | wace_plm endpoint | 핵심 테이블 변경 |
|---|---|---|---|
| 1 | 견적 list 조회 | `POST /contractMgmt/estimateGridList.do` | `contract_mgmt`(read, `IS_DIRECT_ORDER!='Y'` 필터) |
| 2 | 견적요청 등록 | `POST /contractMgmt/saveContractMgmtInfo.do` | `contract_mgmt` + `contract_item`(N) + `contract_item_serial`(N) INSERT |
| 3 | 직접등록 통합폼 (견적 없이 주문) | `POST /contractMgmt/saveEstimateAndOrderInfo.do` | 위와 동일 + `IS_DIRECT_ORDER='Y'` 강제 세팅 |
| 4 | 견적요청 → 견적작성 | (식별자 `contract_objid`로 묶임. 명시적 컬럼 복사 없음) | (없음) |
| 5 | 견적 작성 (일반/장비) | `POST /contractMgmt/saveEstimate.do | saveEstimate2.do` | `estimate_template` + `estimate_template_item` INSERT/UPDATE (N건 가능) |
| 6 | 결재 사전판정 | `POST /contractMgmt/checkApprovalRequired.do` | (read only — 신규수주/가격인하 로직) |
| 6a | 결재불필요 자동처리 | `POST /contractMgmt/setApprovalNotRequired.do` | `contract_mgmt.APPROVAL_REQUIRED='N'` UPDATE |
| 6b | 아마란스 결재상신 | `POST /approval/getAmaranthSsoUrl.do` | `amaranth_approval` INSERT (외부 SSO) |
| 7 | 메일 발송 | `POST /contractMgmt/sendEstimateMail.do` | `mail_log` INSERT + 실제 SMTP `MailUtil.sendMailWithAttachFileUTF8` |
| 8 | 주문서 list 조회 | `POST /contractMgmt/contractGridList.do` | `contract_mgmt`(read, 모든 행) — 견적 list와 동일 테이블 |
| 9 | **수주확정 (전이)** | `POST /contractMgmt/updateOrderStatus.do` | `contract_mgmt.CONTRACT_RESULT` UPDATE만 (새 INSERT 없음) |
| 10 | **프로젝트 자동생성** (9의 부수효과) | (9의 service 내부에서 호출) | `project_mgmt` INSERT × N (라인별, Machine은 수량만큼) + `project_no` 채번 |

> 견적관리·주문서관리·판매관리·매출관리 4개 list는 **모두 같은 `contract_mgmt` 행**을 단계별 필터로 보여주는 구조. 단계 전이는 `CONTRACT_RESULT` 코드 변경으로 일어남.

## 2. GAP 매트릭스 (우선순위 순)

> 🔴 = 사용자 흐름 차단 / 🟠 = 핵심 기능 빠짐 / 🟡 = 보완 필요 / 🟢 = 백로그

| # | 우선 | 항목 | 원본 위치 | 이식본 현재 상태 | 권장 작업 |
|---|---|---|---|---|---|
| G1 | 🔴 | **수주확정 시 프로젝트 자동생성** | `ContractMgmtService.updateOrderStatus` (라인 2987~3113) + `project.xml` (7518~7581) | [salesOrderMgmtService.ts:521](../../../backend-node/src/services/salesOrderMgmtService.ts#L521) `updateStatus`는 `CONTRACT_RESULT` UPDATE만 — 프로젝트 생성 호출 없음 | `updateStatus` 트랜잭션 내에서: contract_item 루프 → PRODUCT='0000928'(Machine)이면 quantity만큼 N회, 아니면 1회 → `project_no` 채번 (`{주문유형}-{제품구분}-{YYMMDD}-{순번3자리}`) → `project_mgmt` INSERT |
| G2 | 🔴 | **직접등록 통합폼** (`estimateAndOrderRegistFormPopup`) | `ContractMgmtService.saveEstimateAndOrderInfo` (라인 2664) | endpoint 자체 부재. 주문관리 화면 "신규" 버튼이 견적 없이 주문 등록하는 흐름 미지원 | `POST /api/sales/order/direct` 신설 — `IS_DIRECT_ORDER='Y'` 강제, `contract_mgmt` UPSERT + `contract_item` UPSERT + `contract_item_serial` 다중 INSERT |
| G3 | 🟠 | 견적요청등록 시 contract_item 다중 INSERT | `ContractMgmtService.saveContractMgmtInfo` (라인 544) | [salesEstimateService.ts](../../../backend-node/src/services/salesEstimateService.ts)는 헤더만 INSERT, 라인 입력 누락 | save 트랜잭션에 `contract_item` 다중 UPSERT + `contract_item_serial` 처리 추가 |
| G4 | 🟡 | 결재 자동판정 (`checkApprovalRequired`) | (별도 컨트롤러, 신규수주/가격인하 룰) | 미구현. APPROVAL_REQUIRED='N' 라벨 표시만 | 룰 분석 후 endpoint 신설 (사전판정 룰만 — SSO 흐름은 G11에서 처리됨) |
| G5 | 🟠 | 견적템플릿 일반/장비 분기 + PDF | `ContractMgmtService.saveEstimateTemplate/2` (라인 1501/1591) + SmartEditor `uploadPdfChunk` | 미이식. 추가견적 카운트(시연 시드)만 표시 | template1/template2 popup 라우트 + `puppeteer` 또는 `react-pdf` PDF 생성 → `attach_file_info doc_type='estimate02'` INSERT |
| G6 | 🟠 | SMTP 실제 발송 | `ContractMgmtService.sendEstimateMail` (라인 1774-1968), `MailUtil.sendMailWithAttachFileUTF8` (라인 1925) | [salesEstimateService.ts:618](../../../backend-node/src/services/salesEstimateService.ts#L618)는 mail_log INSERT만 | `mailSendSimpleService`(nodemailer) 통합 + HTML 본문 생성기(`makeEstimateMailContents`) 포팅 + 첨부 결합 |
| G7 | 🟡 | 주문서 수정 시 contract_item UPSERT (OBJID 유지) | `mapper.upsertContractItemWithOrder` (UPSERT 패턴) | 이식본은 단순 UPDATE — 라인 변경 시 OBJID 유지 보장 안 됨 | UPSERT(ON CONFLICT) 패턴 적용 + 삭제된 라인 처리 분기 |
| G8 | 🟡 | 프로젝트 존재 시 견적·주문 삭제 방지 | `ContractMgmtService.deleteContractMngInfo` (라인 794~808) | [salesOrderMgmtService.ts](../../../backend-node/src/services/salesOrderMgmtService.ts) delete는 사전 체크 없음 | delete 전에 `project_mgmt WHERE contract_objid=$1 LIMIT 1` 체크 → 있으면 거부 |
| G9 | ✅ | **수주복사** (헤더 + contract_item + 시리얼 통째로 복제, 새 영업번호 채번) | `ContractMgmtService.copyEstimateAndOrderInfo` (라인 2601) + 매퍼 `copyContractMgmt`/`copyContractItems`/`copyContractItemSerials`/`getNextContractNo` | **완료 (2026-05-11)** — `salesOrderMgmtService.copyOrder` + `POST /sales/order-mgmt/:id/copy` + 주문관리 그리드 "수주복사" 버튼. 검증: [06-copy-order-verify.md](./06-copy-order-verify.md) |
| ~~G10~~ | ❌ | ~~환율 마스터 + EXCHANGE_RATE 자동 변환~~ | (영업관리 GAP 아님 — Admin 도메인) | wace 영업관리 화면도 `exchange_rate`는 사용자 직접 입력. `COMM_EXCHANGE_RATE` 테이블·환율관리 화면은 wace AdminController(`4898~4993`) + `admin.xml(8191~8336)` 소속 | 영업관리 GAP에서 제외. Admin 메뉴 이식 시점에 별도로 다룸 |
| G11 | ✅ | **수주 결재상신 (Amaranth 직행)** | wace `ApprovalService.getAmaranthSsoUrl` (라인 1782~1909) + `orderMgmtList.btnApproval` (132~175) | chpark의 `amaranthApprovalClient`(HMAC-SHA256 + AES-128-CBC) 기반. `amaranth_approval` 테이블만 사용, 자체 approval 미경유 (wace 패턴 동일). target_type=`CONTRACT_ORDER`, formId=`1161`, compSeq=`1000` | **완료 (2026-05-11)** — `salesOrderMgmtService.startOrderApproval` + `POST /sales/order-mgmt/:id/amaranth-approval` + 주문관리 "결재상신" 버튼 + 결재상태 컬럼(작성중/결재중/결재완료/반려). DB ALTER: `amaranth_approval.target_objid` → VARCHAR. 검증: [07-amaranth-approval-verify.md](./07-amaranth-approval-verify.md). 백로그: 첨부파일 원챔버 업로드 |
| G11E | ✅ | **견적 결재상신 (Amaranth 직행, G11 동일 패턴)** | wace `estimateList_new.jsp:154~270, 868~916` + `ApprovalService.getAmaranthSsoUrl` CONTRACT_ESTIMATE 분기 | G11과 동일 흐름. 차이점: target_type=`CONTRACT_ESTIMATE`, target_objid=`estimate_template.objid`(최신 차수), formId=`1162`. 사전판정(`checkApprovalRequired`)은 G4 영역으로 분리 | **완료 (2026-05-11)** — `salesEstimateService.startEstimateApproval` + `POST /sales/estimate/:id/amaranth-approval` + 견적관리 "결재상신" 버튼 + 견적 list SQL `LEFT JOIN amaranth_approval` + 결재상태 4단계 라벨 + `notRequired`/`approval_required='N'` fallback. 검증: [08-estimate-approval-verify.md](./08-estimate-approval-verify.md). 백로그: 첨부파일 원챔버 업로드, 사전판정(G4) |

## 3. 코드/SQL 정합성 메모

### 3.1 견적·주문은 같은 `contract_mgmt` 행
원본은 **상태 전이형 모델**: 같은 `contract_mgmt` 행이 견적단계(`IS_DIRECT_ORDER='N'`) → 수주(`CONTRACT_RESULT='0000964'`) → FCST(`CONTRACT_RESULT='0000968'`) 로 진행. 새 INSERT는 §3 통합폼과 §10 프로젝트 생성에서만 일어남. 우리 이식본은 이 단계 모델에 맞춰져 있어 list 단의 SQL은 정합성 OK.

### 3.2 project_no 채번 룰 (G1의 핵심)
```
형식: {주문유형}-{제품구분}-{YYMMDD}-{순번3자리}
예: R-CT-260507-001

주문유형 매핑 (CATEGORY_CD → 1글자):
  오버홀=O, 개조=M, 개발=D, 견적=Q, 수리=R, 판매=S, 기타=T

제품구분 매핑 (PRODUCT → 2글자):
  Machine=MC, A/S=AS, D/S=DS, B/S=BS, C/T=CT, A/C=AC, W/M=WM, 기타=원문

순번:
  같은 (주문유형 + 제품구분 + 날짜) 조합 내 MAX(순번)+1
  없으면 001부터
```
출처: `wace_plm/src/com/pms/projectmgmt/mapper/project.xml:7518-7581`.
RPS의 `project_mgmt` 89건이 모두 이 룰로 채번되어 있으니, 이식 시 **새 행도 같은 룰**을 따라야 일관성 유지됨.

### 3.3 Machine 분기
contract_item 1라인의 PRODUCT가 `0000928`(Machine)이면 **수량만큼 N회** project_mgmt INSERT (각 quantity=1). 그 외(A/S·D/S·C/T 등)는 1회 INSERT (수주수량 그대로). 이 분기 누락 시 시리얼 단위 추적이 깨짐.

### 3.4 첨부 파일 doc_type
| 단계 | doc_type | 그리드 컬럼 |
|---|---|---|
| 견적 PDF | `estimate01` (메인 견적서) | (없음) |
| 추가 견적 | `estimate02` | "추가견적" |
| 주문서 첨부 | `ORDER_DOC` 또는 `FTC_ORDER` | "주문서첨부" (CU01_CNT) |

`02-order.md`에 주문서 첨부 컬럼이 미정의되어 있음 — 보완 필요.

## 4. 다음 PR 후보 (3개로 묶기)

### PR-A: 수주확정 → 프로젝트 자동생성 (G1)
- 단독 PR. SQL·트랜잭션 위주, UI 변경 거의 없음.
- 작업: `salesOrderMgmtService.updateStatus` 안에 project 생성 로직 + project_no 채번 helper + Machine 분기.
- 검증: `0000964`/`0000968` 코드로 상태 변경 시 project_mgmt에 새 행 생기는지, project_no 형식 일치하는지.

### PR-B: 직접등록 통합폼 (G2 + G3)
- 주문관리 화면 "신규" 버튼이 통합 다이얼로그 띄움 → `IS_DIRECT_ORDER='Y'` + contract_item 다중 입력.
- G3(견적요청등록 라인 입력)도 같은 라인 입력 컴포넌트 재사용 가능 → 묶어서.

### PR-C: 결재·메일·PDF (G4 + G5 + G6)
- 분량 큼. RPS 결재 정책 결정(G11) 선행 필요.
- 단계: SMTP(G6) → PDF(G5) → 결재(G4) 순서로 사이즈 작은 것부터.

### 백로그
- G7~G11은 위 3개 끝난 뒤 평가.

## 5. 검증 체크리스트 (PR마다)

- [ ] 원본 endpoint와 이식 endpoint 매핑표 갱신 (이 문서 §1)
- [ ] 영향 받는 테이블의 BEFORE/AFTER row count 기록
- [ ] wace 운영 화면(waceplm.esgrin.com)과 동일 시나리오 비교 스크린샷
- [ ] `project_mgmt` 새 행이 89건 + N 으로 늘어나는지 (G1)
- [ ] `IS_DIRECT_ORDER='Y'` 행 신규 생성 확인 (G2)
