# 영업관리 이식 (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 그대로 사용)
- **이식 대상 메뉴 6개** (초기 4개 + 2026-05-15 확장 2개):
  1. 견적관리 (`/contractMgmt/estimateList_new.do`)
  2. 주문서관리 (`/contractMgmt/orderMgmtList.do`)
  3. 판매관리 (`/contractMgmt/salesMgmtList.do` → SalesNcollect로 위임)
  4. 매출관리 (`/revenueMgmt/revenueList.do`)
  5. **구매요청서관리** (`/salesMng/purchaseRequestRegList.do`) — 2026-05-15 추가
  6. **품의서관리(영업)** (`/salesMng/purchaseRegProposalMngList.do`) — 2026-05-15 추가

## 1. 메뉴 매핑표

| # | 메뉴명 | wace_plm URL | wace_plm JSP | wace_plm Controller / Service | vexplor_rps 신규 위치 (제안) | 상세 문서 |
|---|---|---|---|---|---|---|
| 1 | 견적관리 | `/contractMgmt/estimateList_new.do` | `contractMgmt/estimateList_new.jsp` (47KB) | `ContractMgmtController` (1629–2510 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` (2504–3169 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) |
| 5 | 구매요청서관리 | `/salesMng/purchaseRequestRegList.do` | `salesMng/purchaseRequestRegList.jsp` (728줄) | `SalesMngController` (210~258) / `salesMng.getSalesRequestMasterGridList` (DOC_TYPE_FILTER='PURCHASE_REG') | `app/(main)/COMPANY_16/purchase-request/request/page.tsx` + `backend-node/src/{routes,services}/salesPurchaseRequestRoutes.ts` | [09-purchase-request.md](./09-purchase-request.md) |
| 6 | 품의서관리 (영업) | `/salesMng/purchaseRegProposalMngList.do` | `salesMng/purchaseRegProposalMngList.jsp` (313줄) | `SalesMngController` (1363~1389) / `salesMng.getPurchaseRegProposalMngGridList` | `app/(main)/COMPANY_16/purchase-request/proposal/page.tsx` + `backend-node/src/{routes,services}/salesPurchaseRequestRoutes.ts` | [09-purchase-request.md](./09-purchase-request.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 추출~~ 완료 (2026-05-07)
2. ~~01~04 상세 매핑 + 1차 이식~~ 완료 (2026-05-08)
3. ~~[00-gap.md](./00-gap.md) PR-A/B/C 흐름 + G7~G11 결재상신~~ 완료 (2026-05-11)
4. ~~구매요청서관리·품의서관리(영업) 1차 스캐폴드~~ 완료 (2026-05-15, 커밋 `7e7c6a0a`, [09-purchase-request.md](./09-purchase-request.md))
5. **다음**: 구매요청서작성 다이얼로그 + 품의서생성 액션 + 영업>품의서 Amaranth 결재상신(target_type='PROPOSAL', formId='1163'). sales_request_part 운영DB DDL 추출 선행.

## 8. 공통 UX 규칙 (검색 폼 / 영업관리 4개 메뉴 동일 적용)

### 8.1 버튼 영역

상단 우측 버튼 영역에 다음 순서로 배치 — **모든 메뉴 공통**:

1. `조회` (`variant="outline"` + `<Search>` 아이콘)
2. 메뉴 고유 액션 버튼들 (등록/수정/삭제/확정 등)
3. **`초기화`** (`variant="ghost"`) — `searchForm` state를 모든 키 빈 값으로 reset

```tsx
<Button size="sm" variant="ghost"
        onClick={() => setSearchForm({ /* 모든 키 "" */ })}>
  초기화
</Button>
```

### 8.2 date input

- `<Input type="date">` 사용 시 별도 처리 불필요. 다음은 `Input` 컴포넌트 + `globals.css`에 자동 반영:
  - 빈 값일 때 `'YYYY/MM/DD'` 자리표시 텍스트 숨김 (data-empty="true" 자동)
  - 캘린더 아이콘 숨김 (`::-webkit-calendar-picker-indicator { display: none }`)
  - input 영역 어디 클릭해도 picker 자동 표시 (`showPicker()`)
- 위 동작은 `frontend/components/ui/input.tsx` + `frontend/app/globals.css`에서 일괄 처리.

### 8.3 검색 폼 그리드

- `grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-x-2 gap-y-1.5 p-2 border rounded-md bg-muted/30`
- 라벨: `text-[11px] mb-0.5 block text-muted-foreground`
- 입력: `h-8 text-xs` (date는 `px-1 flex-1 min-w-0` 추가)

### 8.4 wace JSP 주석 함정

검색 폼 추출 시 JSP 끝부분 `<!-- 주석처리된 검색필터 - 필요시 활성화 -->` 블록은 **이식 대상 아님**. 활성/비활성 분리 표로 문서화. 자세한 내용은 메모리 [feedback_wace_jsp_columns](../../../../.claude/projects/-Users-jhj-vexplor-rps/memory/feedback_wace_jsp_columns.md).
