0872199b30
backend (M3+M4): - devBomService: list/getByObjid/updateStatus/removeMany/ascending/descending - M3 그리드 SQL (getBOMStandardStructureGridList 1:1) - customer_mng 매핑 (wace SUPPLY_MNG → vexplor customer_mng.customer_code) - PRODUCT_NAME LEFT JOIN comm_code (CODE_NAME 함수 대체) - M3 다중 삭제 트랜잭션 (bom_part_qty + part_bom_report CASCADE) - M4 정/역전개 재귀 CTE (bom_part_qty 트리 + part_mng JOIN) - vexplor 적응: M4 product_mgmt_spec/upg/vc 분기 제거 (스키마 단순화) - PG 재귀 CTE 타입 일치: ARRAY[BP.objid::varchar] 명시 cast frontend (M3+M4): - ebom-regist (M3): 9셀 그리드 (제품구분·품번·품명·E-BOM·등록자·등록일·확정일·Version·상태) - ebom-search (M4): 동적 LEVEL 컬럼 + 정/역전개 토글 - BomReportStatusDialog: 상태 변경 (create/changeDesign/deploy + version) - AdminPageRenderer dynamic 임포트 2건 + menu_info URL spec 정렬 본 PR 제외 (별 PR): E-BOM Excel Import, 정/역전개 Excel Download, BOM_PART_QTY 수량 편집 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
283 lines
12 KiB
Markdown
283 lines
12 KiB
Markdown
# PR-B : E-BOM 등록·조회 묶음 구현 명세 (M3+M4)
|
|
|
|
> 작성: 2026-05-12 / 범위: 개발관리 M3(E-BOM 등록) + M4(E-BOM 조회) — `part_bom_report` 메인, `bom_part_qty` 트리.
|
|
|
|
---
|
|
|
|
## 1. 매퍼 쿼리 1:1 매핑
|
|
|
|
원본 `wace_plm/src/com/pms/mapper/partMng.xml`:
|
|
|
|
| Query id | Line | 본 PR 매핑 | 용도 |
|
|
|---|---:|---|---|
|
|
| `getBOMStandardStructureGridList` | 2,859 | `GET /api/development/ebom/list` | M3 그리드 (PART_BOM_REPORT + 집계) |
|
|
| `updateStructureStatus` | 8,027 | `PUT /api/development/ebom/status` | M3 상태변경 (PRODUCT_CD/PART_NO/NAME/VERSION/STATUS) |
|
|
| `deleteBomReport` | 6,838 | `DELETE /api/development/ebom` (body `objids`) | M3 다중 삭제 + BOM_PART_QTY CASCADE |
|
|
| `deleteBomQty` | 6,847 | (deleteBomReport 내부) | M3 삭제 시 자식 트리 동시 삭제 |
|
|
| `structureAscendingList` | 7,361 | `GET /api/development/ebom/ascending` | M4 정전개 (root → leaf) |
|
|
| `selectStructureDescendingList` | 6,582 | `GET /api/development/ebom/descending` | M4 역전개 (leaf → root) |
|
|
|
|
---
|
|
|
|
## 2. API 엔드포인트 명세
|
|
|
|
### 2.1 M3 그리드 — `GET /api/development/ebom/list`
|
|
|
|
**Query**:
|
|
```
|
|
customer_cd?: string // part_bom_report.customer_objid
|
|
project_name?: string // part_bom_report.contract_objid (project_mgmt.objid)
|
|
unit_code?: string // pms_wbs_task.objid
|
|
search_unit_name?: string // pms_wbs_task.unit_no/task_name LIKE
|
|
search_writer?: string // part_bom_report.writer
|
|
product_cd?: string // wace 'product_code' 검색 (part_bom_report.product_cd)
|
|
search_part_no?: string // part_bom_report.part_no LIKE
|
|
search_part_name?: string // part_bom_report.part_name LIKE
|
|
search_from_date?: string // regdate from
|
|
search_to_date?: string // regdate to
|
|
status?: string // part_bom_report.status (create/changeDesign/deploy)
|
|
page?, page_size?
|
|
```
|
|
|
|
**SQL** (vexplor_rps part_bom_report 스키마 적응 — wace `getBOMStandardStructureGridList` 의 PRODUCT_CD/PART_NO/PART_NAME 분기 활성, MULTI_* 컬럼 그대로):
|
|
```sql
|
|
SELECT
|
|
ROW_NUMBER() OVER(ORDER BY T.REGDATE DESC) AS NUM,
|
|
T.OBJID, T.CUSTOMER_OBJID, SM.SUPPLY_NAME AS CUSTOMER_NAME,
|
|
T.CONTRACT_OBJID, PM.CUSTOMER_PROJECT_NAME, PM.PROJECT_NO,
|
|
T.UNIT_CODE, COALESCE(WT.UNIT_NO || '-' || WT.TASK_NAME, '') AS UNIT_NAME,
|
|
T.STATUS,
|
|
CASE UPPER(T.STATUS)
|
|
WHEN 'CREATE' THEN '등록중'
|
|
WHEN 'CHANGEDESIGN' THEN '설계변경미배포'
|
|
WHEN 'DEPLOY' THEN '배포완료'
|
|
ELSE '' END AS STATUS_TITLE,
|
|
T.WRITER, UI.DEPT_NAME, UI.USER_NAME,
|
|
COALESCE(UI.DEPT_NAME || '/' || UI.USER_NAME, '') AS DEPT_USER_NAME,
|
|
T.REGDATE, TO_CHAR(T.REGDATE, 'YYYY-MM-DD') AS REG_DATE,
|
|
T.DEPLOY_DATE, T.REVISION,
|
|
EO_DATA.EO_NO, EO_DATA.EO_DATE,
|
|
T.NOTE, T.MULTI_YN, T.MULTI_MASTER_YN, T.MULTI_BREAK_YN, T.MULTI_MASTER_OBJID,
|
|
COALESCE(EO_DATA.BOM_CNT, 0) AS BOM_CNT,
|
|
T.PRODUCT_CD, CC_PRD.code_name AS PRODUCT_NAME,
|
|
T.PART_NO, T.PART_NAME
|
|
FROM PART_BOM_REPORT T
|
|
LEFT JOIN customer_mng SM ON SM.customer_code = T.CUSTOMER_OBJID -- vexplor 매핑
|
|
LEFT JOIN PROJECT_MGMT PM ON PM.OBJID = T.CONTRACT_OBJID
|
|
LEFT JOIN PMS_WBS_TASK WT ON WT.OBJID = T.UNIT_CODE
|
|
LEFT JOIN USER_INFO UI ON UI.USER_ID = T.WRITER
|
|
LEFT JOIN COMM_CODE CC_PRD ON CC_PRD.code_id = T.PRODUCT_CD AND CC_PRD.status = 'active'
|
|
LEFT JOIN (
|
|
SELECT BP.BOM_REPORT_OBJID,
|
|
MAX(PM2.EO_NO) AS EO_NO,
|
|
MAX(PM2.EO_DATE) AS EO_DATE,
|
|
COUNT(*) AS BOM_CNT
|
|
FROM BOM_PART_QTY BP
|
|
LEFT JOIN PART_MNG PM2 ON BP.PART_NO = PM2.OBJID::varchar
|
|
GROUP BY BP.BOM_REPORT_OBJID
|
|
) EO_DATA ON EO_DATA.BOM_REPORT_OBJID = T.OBJID
|
|
WHERE 1=1 + 동적 (위 11 필터)
|
|
```
|
|
|
|
**Response**: `{ rows: BomReportRow[], total, page, pageSize }`
|
|
|
|
### 2.2 M3 단건 상세 — `GET /api/development/ebom/:objid`
|
|
|
|
`SELECT T.* FROM PART_BOM_REPORT T WHERE T.OBJID = $1` + (옵션) BOM_PART_QTY 카운트.
|
|
편집 다이얼로그 진입용.
|
|
|
|
### 2.3 M3 상태 변경 — `PUT /api/development/ebom/:objid/status`
|
|
|
|
**Body**: `{ product_cd?, part_no?, part_name?, version?, status }` — wace `updateStructureStatus` 1:1.
|
|
`STATUS` 만 변경하는 단순 케이스도 지원 (다른 필드 NULL 허용).
|
|
|
|
```sql
|
|
UPDATE PART_BOM_REPORT
|
|
SET PRODUCT_CD = COALESCE($1, PRODUCT_CD),
|
|
PART_NO = COALESCE($2, PART_NO),
|
|
PART_NAME = COALESCE($3, PART_NAME),
|
|
REVISION = COALESCE($4, REVISION),
|
|
STATUS = $5,
|
|
EDITER = $6,
|
|
EDIT_DATE = NOW()
|
|
WHERE OBJID = $7
|
|
```
|
|
|
|
### 2.4 M3 다중 삭제 — `DELETE /api/development/ebom` (body: `{ objids: string[] }`)
|
|
|
|
**트랜잭션**:
|
|
1. `DELETE FROM BOM_PART_QTY WHERE BOM_REPORT_OBJID = ANY($1)` (자식 트리)
|
|
2. `DELETE FROM PART_BOM_REPORT WHERE OBJID = ANY($1)` (메인)
|
|
|
|
wace는 part_mng도 정리(`deleteBomQtyPart`, status='create'만)하지만 본 PR에서는 part_mng 보존 (M1·M2와 결합 안 됨).
|
|
|
|
### 2.5 M4 정전개 — `GET /api/development/ebom/ascending`
|
|
|
|
**Query**:
|
|
```
|
|
bom_report_objid?: string // 단일 BOM 한정 조회
|
|
project_name?: string // PART_BOM_REPORT.contract_objid
|
|
unit_code?: string
|
|
search_part_no?: string
|
|
search_part_name?: string
|
|
```
|
|
|
|
**SQL** (재귀 CTE — wace `structureAscendingList` 의 BOM_PART_QTY 트리 1:1):
|
|
```sql
|
|
WITH RECURSIVE TREE(bom_report_objid, objid, parent_objid, child_objid, part_no, qty, lev, path, cycle) AS (
|
|
SELECT BP.bom_report_objid, BP.objid, BP.parent_objid, BP.child_objid,
|
|
BP.part_no, BP.qty, 1, ARRAY[BP.objid], FALSE
|
|
FROM bom_part_qty BP
|
|
WHERE (BP.parent_objid IS NULL OR BP.parent_objid = '')
|
|
AND BP.bom_report_objid = $bom_report_objid /* 또는 필터 적용된 PART_BOM_REPORT 서브쿼리 */
|
|
UNION ALL
|
|
SELECT B.bom_report_objid, B.objid, B.parent_objid, B.child_objid,
|
|
B.part_no, B.qty, T.lev + 1, T.path || B.objid, B.objid = ANY(T.path)
|
|
FROM bom_part_qty B
|
|
JOIN TREE T ON B.parent_objid = T.objid AND NOT T.cycle
|
|
)
|
|
SELECT T.*,
|
|
PM.part_no AS pm_part_no,
|
|
PM.part_name AS pm_part_name,
|
|
PM.spec, PM.material, PM.weight, PM.remark,
|
|
PM.edit_date,
|
|
(SELECT COUNT(1) FROM attach_file_info F WHERE F.target_objid = PM.objid::varchar AND F.doc_type='3D_CAD') AS cu01_cnt,
|
|
(SELECT COUNT(1) FROM attach_file_info F WHERE F.target_objid = PM.objid::varchar AND F.doc_type='2D_DRAWING_CAD') AS cu02_cnt,
|
|
(SELECT COUNT(1) FROM attach_file_info F WHERE F.target_objid = PM.objid::varchar AND F.doc_type='2D_PDF_CAD') AS cu03_cnt,
|
|
(SELECT MAX(lev) FROM TREE) AS max_level
|
|
FROM TREE T
|
|
LEFT JOIN part_mng PM ON T.part_no = PM.objid::varchar
|
|
ORDER BY T.path
|
|
```
|
|
|
|
**Response**: `{ rows: AscRow[], max_level: number }`
|
|
|
|
### 2.6 M4 역전개 — `GET /api/development/ebom/descending`
|
|
|
|
같은 Query 파라미터. 재귀 방향 반대:
|
|
- 시작점: 리프(`child_objid` 가 다른 행의 `parent_objid` 가 아닌 행) 또는 사용자가 지정한 PART
|
|
- 트리 부모 방향으로 traverse
|
|
|
|
**SQL** (역전개 — wace `selectStructureDescendingList` 단순 매핑):
|
|
```sql
|
|
WITH RECURSIVE TREE(...) AS (
|
|
/* 1. anchor: 조건 매칭 BOM 또는 leaf part */
|
|
SELECT BP.* , 1 AS lev, ARRAY[BP.objid] AS path, FALSE AS cycle
|
|
FROM bom_part_qty BP
|
|
WHERE ... /* PART_NO 매칭 등 */
|
|
UNION ALL
|
|
/* 2. parent 방향 traverse */
|
|
SELECT B.*, T.lev + 1, T.path || B.objid, B.objid = ANY(T.path)
|
|
FROM bom_part_qty B
|
|
JOIN TREE T ON B.objid = T.parent_objid AND NOT T.cycle
|
|
)
|
|
SELECT ... (정전개와 동일 part_mng / attach_file_info JOIN)
|
|
```
|
|
|
|
**Response**: `{ rows: DescRow[], max_level: number }`
|
|
|
|
---
|
|
|
|
## 3. Backend 파일 구조
|
|
|
|
```
|
|
backend-node/src/
|
|
routes/
|
|
devBomRoutes.ts // 6 endpoint
|
|
controllers/
|
|
devBomController.ts
|
|
services/
|
|
devBomService.ts // list/getById/updateStatus/removeMany/ascending/descending
|
|
```
|
|
|
|
`app.ts`: `app.use("/api/development", devBomRoutes)` (devPart 라우터와 prefix 공유 — Express 중복 등록 안전, 경로 충돌 없음).
|
|
|
|
---
|
|
|
|
## 4. Frontend 파일 구조
|
|
|
|
```
|
|
frontend/
|
|
app/(main)/COMPANY_16/development/
|
|
ebom-regist/page.tsx // M3 그리드 + 검색 + 액션
|
|
ebom-search/page.tsx // M4 정/역전개 (동적 LEVEL 컬럼)
|
|
components/development/
|
|
BomReportStatusDialog.tsx // M3 상태 변경 다이얼로그 (status select)
|
|
lib/api/
|
|
devBom.ts // 6 endpoint 호출 + 타입
|
|
```
|
|
|
|
### 4.1 M3 그리드 (9 셀, wace structureList.jsp:185~215 1:1)
|
|
|
|
| key | 라벨 | 정렬 | 너비 |
|
|
|---|---|---|---:|
|
|
| product_name | 제품구분 | center | 160 |
|
|
| part_no | 품번 | left | 210 |
|
|
| part_name | 품명 | left | flex |
|
|
| bom_cnt | E-BOM (folder click) | center | 150 |
|
|
| dept_user_name | 등록자 | center | 120 |
|
|
| reg_date | 등록일 | center | 130 |
|
|
| deploy_date | 확정일 | center | 100 |
|
|
| revision | Version | center | 110 |
|
|
| status_title | 상태 | center | 110 |
|
|
|
|
### 4.2 M3 검색 폼 (wace 1:1 — 9 필드)
|
|
|
|
customer_cd · project_name · unit_code · SEARCH_UNIT_NAME · SEARCH_WRITER · product_cd · SEARCH_PART_NO · SEARCH_PART_NAME · search_fromDate~toDate · status
|
|
|
|
본 PR 1차: PRODUCT_CD · SEARCH_PART_NO · SEARCH_PART_NAME · STATUS 4필드로 시작. 나머지 추후 보강.
|
|
|
|
### 4.3 M3 액션 버튼 (wace 1:1)
|
|
|
|
- 조회 / 삭제 / E-BOM등록(Excel Import — 별 PR) / 상태변경
|
|
|
|
본 PR 포함: 조회 · 삭제 · 상태변경. **E-BOM등록(Excel Import)은 별 PR**.
|
|
|
|
### 4.4 M4 동적 LEVEL 컬럼
|
|
|
|
backend response의 `max_level` 값에 따라 컬럼을 동적 생성:
|
|
- LEVEL 1..max_level: 각 레벨 컬럼은 `row.lev === i` 인 행의 `part_no` 표시 (트리 들여쓰기 효과)
|
|
- 고정 컬럼: 품번 · 품명 · 3D/2D/PDF · 수량 · 변경일 · 규격 · 재질 · 중량 · 비고
|
|
|
|
DataGrid의 컬럼 배열을 fetch 결과 도착 시 동적 생성 (state).
|
|
|
|
### 4.5 M4 액션
|
|
|
|
- 정전개 조회 (default)
|
|
- 역전개 조회
|
|
- (Excel Download — 별 PR)
|
|
|
|
---
|
|
|
|
## 5. 본 PR 제외 항목
|
|
|
|
| 항목 | 사유 / 후속 |
|
|
|---|---|
|
|
| E-BOM 등록 (Excel Import) | `openBomReportExcelImportPopUp.jsp` — 별 PR |
|
|
| 정/역전개 Excel Download | `structureAscendingListExcel`/`structureDescendingExcelList` — 별 PR |
|
|
| `BOM_PART_QTY` 직접 편집 (수량 변경) | `structureQtySave` — wace 운영판에서도 별 화면 |
|
|
| 다중 BOM(MULTI_*) 분기 처리 | 현재 vexplor 데이터 없음 — 기본 1:1만 |
|
|
| wace_plm `product_mgmt_spec/upg/vc` 분기 | vexplor 스키마는 product_cd 단순 — 운영판 1:1 적응 |
|
|
|
|
---
|
|
|
|
## 6. 검증 시나리오 (verify.md 기준)
|
|
|
|
1. M3 페이지 진입 → part_bom_report 그리드 (현재 0건, schema/UI 동작 확인)
|
|
2. M3 상태변경 다이얼로그 → status='deploy' → DB 반영 확인
|
|
3. M3 다중 삭제 → bom_part_qty CASCADE 확인
|
|
4. M4 페이지 진입 → 정전개 (`/ascending`) 0건 응답 → 페이지 정상 표시
|
|
5. M4 역전개 토글 → `/descending` 응답
|
|
6. (시드 후) MAX_LEVEL=3 트리에서 동적 컬럼 3개 생성 확인
|
|
|
|
---
|
|
|
|
## 7. 적응 사항 (운영판 대비 변경점)
|
|
|
|
| # | 항목 | 변경 |
|
|
|---|---|---|
|
|
| 1 | `customer_mng` 매핑 | wace `SUPPLY_MNG.OBJID::VARCHAR = T.CUSTOMER_OBJID` → vexplor `customer_mng.customer_code = T.CUSTOMER_OBJID` |
|
|
| 2 | `PRODUCT_NAME` lookup | wace `CODE_NAME(PRODUCT_CD)` → vexplor `LEFT JOIN comm_code` (`CC_PRD`) |
|
|
| 3 | M4 `PRODUCT_MGMT_*` 분기 제거 | vexplor part_bom_report 는 product_cd/version 단순화 — wace `product_mgmt_spec/upg/vc` 컬럼 없음 → JOIN 생략, 정전개는 BOM_PART_QTY 트리만 |
|
|
| 4 | 다중 삭제 트랜잭션 | wace 두 매퍼(`deleteBomQty` + `deleteBomReport`) 호출 → backend `transaction()` 한 번 |
|