# 생산관리 이식 (wace_plm → vexplor_rps)

> 작성: 2026-05-15 / 작성자: 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` 패턴)
- **스키마 정책**: wace_plm 도메인 테이블(`mbom_header`/`mbom_detail`/`mbom_history`/`sales_request_master`/`project_mgmt`/`contract_mgmt`/`part_bom_report` 등) **그대로 이식**. 마스터(`customer_mng`/`item_info`/`user_info` 등)는 RPS 기존 사용.
- **이식 대상 메뉴 = 5개** (2026-05-15 사용자 확정):
  1. M-BOM 관리 (`/productionplanning/mBomMgmtList.do`)
  2. 생산계획&실적관리 (`/productionplanning/prodPlanResultMgmtList.do`)
  3. 생산계획&실적관리(장비) (`/productionplanning/prodPlanResultMgmtEquipList.do`)
  4. 반제품소요량 (`/productionplanning/semiProductRequirementList.do`)
  5. 원자재소요량 (`/productionplanning/rawMaterialRequirementList.do`)
- ⚠️ `production/{bom,plan-management,process-info,result,work-instruction}` 디렉토리는 **vexplor 잔재** — wace_plm 매뉴얼 5개에 없음, 이식 대상 아님.
- **메뉴 노출**: M-BOM 관리는 생산관리 + 구매관리 트리 양쪽 진입 허용 (`menu_info` 100016 + 100032 동시 활성). 구매관리 페이지는 production/mbom re-export.

## 1. 메뉴 매핑표

| # | 메뉴명 | wace_plm URL | wace_plm JSP | wace_plm Controller | RPS 신규 위치 |
|---|---|---|---|---|---|
| 1 | M-BOM 관리 | `/productionplanning/mBomMgmtList.do` | `productionplanning/mBomMgmtList.jsp` | `ProductionPlanningController` | `app/(main)/COMPANY_16/production/mbom/page.tsx` + `backend-node/src/{routes,services,controllers}/mbom*` (PR-A0~B5+) |
| 2 | 생산계획&실적관리 | `/productionplanning/prodPlanResultMgmtList.do` | `productionplanning/prodPlanResultMgmtList.jsp` | `ProductionPlanningController` | `app/(main)/COMPANY_16/production/plan-result/page.tsx` + `backend-node/src/{routes,services,controllers}/prodPlanResult*` |
| 3 | 생산계획&실적관리(장비) | `/productionplanning/prodPlanResultMgmtEquipList.do` | `productionplanning/prodPlanResultMgmtEquipList.jsp` | `ProductionPlanningController` | `app/(main)/COMPANY_16/production/plan-result-equip/page.tsx` + 위와 동일 service (listEquip 분기) |
| 4 | 반제품소요량 | `/productionplanning/semiProductRequirementList.do` | `productionplanning/semiProductRequirementList.jsp` | `ProductionPlanningController` | `app/(main)/COMPANY_16/production/semi-product-req/page.tsx` + `backend-node/src/services/mbomRequirementService.ts` (listSemi) |
| 5 | 원자재소요량 | `/productionplanning/rawMaterialRequirementList.do` | `productionplanning/rawMaterialRequirementList.jsp` | `ProductionPlanningController` | `app/(main)/COMPANY_16/production/raw-material-req/page.tsx` + 위와 동일 service (listRaw) |
| ─ | BOM 복사 다이얼로그 (M-BOM 보조) | `/partMng/structureBomCopyFormPopup.do` | `partMng/structureBomCopyFormPopup.jsp` | `PartMngController` | `components/production/BomCopyDialog.tsx` (PR-B5+) |

## 2. 도메인 테이블

이식 대상 — RPS DB에 CREATE 그대로 적용. DDL은 [ddl-extracted/](./ddl-extracted/), 동기화 SQL은 [data-sync/](./data-sync/).

| 우선순위 | 테이블 | 용도 | DDL 파일 |
|---|---|---|---|
| ★★★ | `mbom_header` | M-BOM 헤더 (project_objid 별 1+ 버전, status='Y' 가 활성) | [400_mbom.sql](./ddl-extracted/400_mbom.sql) |
| ★★★ | `mbom_detail` | M-BOM 상세 (parent/child 트리, qty/required_qty/order_qty/production_qty) | [400_mbom.sql](./ddl-extracted/400_mbom.sql) |
| ★★★ | `mbom_history` | M-BOM 변경이력 (CHANGE_TYPE = CREATE/UPDATE) | [401_mbom_dependencies.sql](./ddl-extracted/401_mbom_dependencies.sql) |
| ★★★ | `sales_request_master` | 구매리스트(R-YYYYMMDD-NNN), M-BOM 으로부터 단건 생성 | [401_mbom_dependencies.sql](./ddl-extracted/401_mbom_dependencies.sql) |
| ★★★ | `project_mgmt` | 프로젝트 헤더 (source_bom_type/source_ebom_objid/source_mbom_objid) | (sales 도메인 기존 테이블 재사용) |
| ★★★ | `contract_mgmt` / `contract_item` | 수주/계약 정보 (product != '0000928' = Machine 외 판별) | (sales 도메인 기존 테이블 재사용) |
| ★★ | `part_bom_report` / `bom_part_qty` | E-BOM 참조 (BOM 할당/복사 소스) | (개발관리 도메인 기존 테이블) |
| ★★ | `production_plan` | 생산계획(plan-result 보조) | [402_production_plan.sql](./ddl-extracted/402_production_plan.sql) |
| ★ | `attach_file_info` | 도면 다중 업로드 (PR-B5+ BomCopyDialog, doc_type='MBOM_DRAWING') | (공용 테이블) |
| ★ | `client_mng` | 고객사 (`user_name()` fn 동반) | (sales 기존) |

### 데이터 동기화 SQL ([data-sync/](./data-sync/))

| 파일 | 용도 |
|---|---|
| `01_mbom_sync.sql` | 운영 DB → RPS mbom_header/detail 데이터 이주 |
| `02_mbom_dependencies_sync.sql` | mbom_history + sales_request_master 동기화 |
| `03_mbom_menu_dedup.sql` | menu_info 중복 정리 (M-BOM 100016/100032) |
| `04_production_plan_sync.sql` | production_plan 동기화 |
| `05_mbom_menu_desc.sql` | M-BOM 관리 메뉴명 sync |

## 3. 매퍼 매핑표 (`productionplanning.xml` 1:1)

| wace 매퍼 ID | wace 라인 | RPS 함수 (mbomService) | PR |
|---|---|---|---|
| `mBomMgmtGridList` | 2874-3119 | `list()` | A1 |
| `getProjectMgmtDetail` | 3150-3218 | `getDetail()` | A2 |
| `getLatestMbomByProjectId` | 3555-3570 | `getLatestSavedMbom()` | A2 |
| `getLatestMbomTemplateByPartNo` | 3573-3591 | `getLatestTemplate()` | A2 |
| `getMbomTemplateDetails` | 3594-3794 | `getTemplateDetails()` | A2 |
| `getSavedMbomTreeList` | 4114-4359 | `getSavedTree()` | A2 |
| `getMbomStructureOnly` | 4362-4538 | `getStructureOnly()` | A2 |
| `insertMbomHeader/Detail` | 3873/3886 | `save()` CREATE | B1 |
| `updateMbomHeader/Detail` | 3917/3940 | `save()` UPDATE | B1 |
| `deleteMbomDetailByObjid` | 3934 | `save()` UPDATE 누락 행 | B1 |
| `insertMbomHistory` | 3973 | `insertHistory()` helper | B1/B4 |
| `updateProjectMbomStatus` | 3949 | `save()` CREATE | B1 |
| `getMbomHistory` | 3448-3470 | `getHistory()` | B4 |
| `partMng.getBOMTreeList` | partMng.xml 3289-3549 | `getEbomWorkingTree()` / `previewEbomTree()` | A2/B5 |
| `getEbomList` | 3221-3265 | `searchAssignableEboms()` | B5 |
| `saveBomAssignment` | 3545-3553 | `assignBom()` (EBOM/MBOM 둘 다) | B5 |
| `getLatestMbomByPartNo` | 3426-3445 | `getLatestMbomByPartNo()` | B5+ |
| `getMbomListForSelect2` | 4007-4014 | `searchAssignableMboms()` | B5+ |
| `salesMng.insertSalesRequestMasterFromMBom` | ~3975 | `createSalesRequest()` | B3 |
| `prodPlanResultMgmtGridList` | 4550~ | `prodPlanResultService.listGeneral()` | — |
| `prodPlanResultMgmtEquipGridList` | 4887~ | `prodPlanResultService.listEquip()` | — |
| `semiProductRequirementList` (Java LinkedHashMap) | ~5252 | `mbomRequirementService.listSemi()` | — |
| `rawMaterialRequirementList` (Java LinkedHashMap) | — | `mbomRequirementService.listRaw()` | — |

## 4. PR 진행 흐름

| PR | 내용 | 대표 커밋 |
|---|---|---|
| A0 | 의존 테이블 (mbom_history/sales_request_master/client_mng + user_name fn) | `7af366c5` |
| A0' | M-BOM 테이블 신설 (mbom_header/detail + 운영 sample) | `04cfac6e` |
| A1 | 메인 그리드/검색 (mBomMgmtGridList 1:1) | `66cee22b` |
| A2 | 단건 상세 + read-only 트리 4분기 (SAVED/ASSIGNED_EBOM/ASSIGNED_MBOM/TEMPLATE/NONE) | `dd88dc6e` |
| B1 | 본 편집 + 폴더 컬럼 + DataGrid 서버 페이지네이션 + bigint=varchar fix | `7a7f4f03` |
| B2 | 본 편집 행 추가/삭제 (팝업 방식, MbomAddPartDialog, temp-objid remap) | `dee03f60` |
| B3 | 구매리스트 생성 (createPurchaseListFromMBom 1:1) | `b38f5957` |
| B4 | 변경이력 다이얼로그 (getMbomHistory 1:1) | `8dd5f184` |
| B5 | BOM 할당 베이스 (mBomEbomSelectPopup 진입점) | `b38f5957` |
| B5 | BOM 할당 운영판 1:1 재구성 (카드+토글+미리보기 트리+제품구분/날짜 검색) | `bd47ca80` |
| 보정 | 4개 메뉴 디렉토리 rename(URL 일치) + 그리드 헤더 보정 + 소요량 numeric 캐스팅 fix | `c83a73a1` |
| **B5+** | **BOM 복사 다이얼로그 (structureBomCopyFormPopup 1:1) + 5메뉴 1:1 검증** | `63682587` |

## 5. 5개 메뉴 1:1 검증 결과 (2026-05-15)

Agent 5병렬 (read-only) 검증.

| 메뉴 | 판정 | 비고 |
|---|---|---|
| M-BOM 관리 | PASS (B5+ 후) | 검색 폼 / 그리드 16컬럼 / SQL 100% 일치. 품번/품명만 wace `Select2-part`(AJAX) → Input text. **B5+ 로 누락된 [BOM 복사] 보완**. |
| 생산계획&실적관리 | MINOR_DIFF | 검색 13필드 / 그리드 18컬럼 / SQL 100% 일치. **[생산계획 생성]·[생산실적 등록]** 액션 모달은 미구현(프로토타입). 품번/품명 Select2-part → Input text. |
| 생산계획&실적관리(장비) | PASS | 검색 9필드 / 그리드 14컬럼 / SQL 100% 일치. WBS할당 모달은 의도적 미구현 (주석 명시). |
| 반제품소요량 | MINOR_DIFF | 입력 폼 / 결과 4컬럼 / SQL / 누적 로직 100% 일치. RPS만 단위/소재/규격 3컬럼 추가 노출 (UX 향상). c83a73a1 에서 numeric 캐스팅 fix. |
| 원자재소요량 | PASS | 입력 / 8컬럼 / 구매품·원소재 SQL / Math.ceil 누적 / 숫자 포맷 모두 100% 일치. |

## 6. PR-B5+ BOM 복사 다이얼로그 (2026-05-15)

운영판 `partMng/structureBomCopyFormPopup.jsp` (774 lines) 1:1.

**진입**: 메인 그리드 [BOM 복사] 버튼 — 체크박스 단건 선택 + Machine(0000928) 외 동일 partNo 매칭 시 기존 M-BOM 자동 추천(`getLatestMbomByPartNo` 호출).

**레이아웃**:
- 상단: 품번 / 품명 readonly + [저장] [닫기]
- 중단: E-BOM 셀렉트 / M-BOM 셀렉트 (상호배타, 한쪽 선택 시 다른쪽 disable)
- 하단: 트리 미리보기 + **도면 다중 업로드** (공용 `AttachFileDropZone`, target=projectObjid, docType=`MBOM_DRAWING`, accept=`.stp,.step,.dwg,.dxf,.pdf`)

**저장**: `assignBom` 재사용 (PR-B5 매퍼 `saveBomAssignment` 1:1). 백엔드는 EBOM/MBOM 분기 이미 지원, 이번에 MBOM UI 진입점도 첫 노출.

**도면 업로드 차이**: 운영판 `fn_uploadDrawingFiles`는 placeholder ("구현 예정" 토스트). RPS는 공용 `AttachFileDropZone` 재사용해 wace 보다 앞서 실구현.

**신규 산출물**:

| 위치 | 역할 |
|---|---|
| `backend-node/src/services/mbomService.ts` | `searchAssignableMboms` / `previewMbomTree` / `getLatestMbomByPartNo` 3 함수 |
| `backend-node/src/controllers/mbomController.ts` | 3 핸들러 추가 |
| `backend-node/src/routes/productionMbomRoutes.ts` | `GET /assignable-mboms`, `GET /mbom-preview/:objid`, `GET /latest-mbom-by-partno/:partNo` |
| `frontend/lib/api/mbom.ts` | 3 메서드 + `AssignableMbomRow` / `LatestMbomByPartNoRow` 타입 |
| `frontend/components/production/BomCopyDialog.tsx` | 신규 다이얼로그 |
| `frontend/app/(main)/COMPANY_16/production/mbom/page.tsx` | [BOM 복사] 버튼 + Dialog 연결 |

## 7. 백엔드 산출물 ([backend-node/src/](../../../backend-node/src/))

| 파일 | 역할 |
|---|---|
| `services/mbomService.ts` | list / getDetail / getTree(4분기) / save / getHistory / searchAssignableEboms / previewEbomTree / assignBom / searchAssignableMboms / previewMbomTree / getLatestMbomByPartNo / createSalesRequest |
| `controllers/mbomController.ts` | 위 모든 핸들러 |
| `routes/productionMbomRoutes.ts` | 위 모든 라우트 |
| `services/prodPlanResultService.ts` | listGeneral (plan-result) + listEquip (plan-result-equip), buildWhere 공유 |
| `controllers/prodPlanResultController.ts` | 위 2 핸들러 |
| `routes/productionPlanResultRoutes.ts` | GET /list, GET /equip/list |
| `services/mbomRequirementService.ts` | listSemi (반제품) + listRaw (원자재), getOptions (M-BOM 셀렉트 + 품명) |

## 8. 프론트엔드 산출물 ([frontend/](../../../frontend/))

| 파일 | 역할 |
|---|---|
| `lib/api/mbom.ts` | 타입 + mbomApi 14 메서드 |
| `lib/api/prodPlanResult.ts` | 일반/장비 list 타입 |
| `components/production/MbomDetailDialog.tsx` | 단건 상세 + 트리 + 편집 + toolbar(변경이력/BOM 할당/본 편집/행 add-del) |
| `components/production/MbomHistoryDialog.tsx` | 변경이력 그리드 |
| `components/production/MbomAddPartDialog.tsx` | 행 추가 시 PART 검색 |
| `components/production/MbomAssignDialog.tsx` | BOM 할당 (운영 mBomEbomSelectPopup 1:1) |
| `components/production/BomCopyDialog.tsx` | **BOM 복사 (PR-B5+ 신규)** |
| `app/(main)/COMPANY_16/production/mbom/page.tsx` | 메인 리스트 + 폴더 컬럼 + 서버 페이지네이션 + [구매리스트 생성] + [BOM 복사] |
| `app/(main)/COMPANY_16/purchase/mbom/page.tsx` | re-export (구매관리 트리 노출) |
| `app/(main)/COMPANY_16/production/plan-result/page.tsx` | 생산계획&실적 |
| `app/(main)/COMPANY_16/production/plan-result-equip/page.tsx` | 장비 |
| `app/(main)/COMPANY_16/production/semi-product-req/page.tsx` | 반제품 소요량 |
| `app/(main)/COMPANY_16/production/raw-material-req/page.tsx` | 원자재 소요량 |

## 9. 작업 원칙 (도메인 공통)

- 운영판 1:1 정확 일치 — `waceplm.esgrin.com` 진실의 기준
- JSP/매퍼/Java `/* */` · `<!-- -->` · `//` 비활성 보존 — 이식 대상 아님
- bigint=varchar 함정: `ATTACH_FILE_INFO` 서브쿼리는 `P.OBJID::varchar = F.TARGET_OBJID` 캐스트 필수
- numeric 캐스팅: RPS `mbom_detail.qty/required_qty` 는 `numeric(15,4)`. wace 의 `NULLIF(x,'')::INTEGER` 패턴 적용 시 "invalid input syntax" 발생 → `COALESCE(x, 0)::INTEGER` 패턴 사용 (c83a73a1)
- 메뉴 등록은 menu_info 직접 UPDATE (data-sync 스크립트 동반)
- 다른 세션 작업물은 따로 커밋 — working tree 에 보여도 묶지 말 것
- 공용 컴포넌트 의무: PageHeader + CompactFilterBar + SmartSelect/CustomerSelect + DataGrid

## 10. 다음 작업 후보

1. **plan-result 액션 모달** — [생산계획 생성](`prodPlanFormPopup.jsp`) / [생산실적 등록](`prodResultFormPopup.jsp`) 1:1 이식
2. **공통 PartSelect 컴포넌트** — wace `Select2-part`(AJAX 자동완성) 복원, 4개 메뉴 공통 적용
3. **PR-B6** — Excel Upload/Download (운영판 mBomFormPopup 의 Excel 버튼 2종)
4. **PR-B7** — 행 순서 변경 (up/down) + 부모 변경 (drag drop)
5. **품질관리 후속** — chpark 가 베이스만 짠 4개 메뉴(incoming/process/semi-product inspection) 상세화 (별 PR)
6. **자재관리 도메인 진입** — 별 PR
