50669a66ee
· 메인 그리드 5컬럼(제품구분/제목/WBS/등록자/등록일) + 통합 팝업(트리 CRUD + 엑셀 임포트 + 템플릿 다운로드) · 운영 매핑: pms_wbs_template(헤더) + pms_wbs_task_standard(트리) — 활성 갈래 확정 (_info/_standard2 갈래는 2021년 멈춘 레거시) · wace mergeExcelUploadWBS 1:1: 신규=헤더+트리 INSERT, 수정=트리 일괄 DELETE→INSERT (헤더 변경 없음) · objid 채번 gen_random_uuid()::text, 엑셀 파싱 xlsx(SheetJS), 정적 템플릿 frontend/public/templates/ · DataGrid 컬럼 단위 onClick 추가 (WBS 폴더 셀 클릭용) · DDL: 8개 테이블 162컬럼 (docs/migration/project/ddl-extracted/200_pms_wbs.sql) / GAP: docs/migration/project/02-wbs-template.md · 프로젝트 자동 복사/진행관리 연계는 wace도 미완성 — P2 범위 외 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
478 lines
21 KiB
Markdown
478 lines
21 KiB
Markdown
# P2: 프로젝트관리 > 제품구분_WBS관리 (WBS 템플릿)
|
|
|
|
> 작성일: 2026-05-12
|
|
> 원본: wace_plm `/project/wbsTemplateMngList.do` + `/project/WBSExcelImportPopUp.do` 통합 워크플로
|
|
> 운영판 URL: https://waceplm.esgrin.com/main.do → 프로젝트관리 > 제품구분_WBS관리
|
|
> 대상 DB 테이블: `pms_wbs_template`(헤더), `pms_wbs_task_standard`(트리)
|
|
|
|
---
|
|
|
|
## 1. 범위 (Scope)
|
|
|
|
### 1.1 범위 안
|
|
|
|
- **메인 그리드** (5컬럼): 제품구분 / 제목 / WBS(폴더 아이콘) / 등록자 / 등록일
|
|
- **검색**: 제품구분 단일 셀렉트
|
|
- **신규 등록**: 통합 팝업 진입 (`WBSExcelImportPopUp.do?product=...`)
|
|
- **수정**: 통합 팝업 진입 (`WBSExcelImportPopUp.do?templateObjId=...`)
|
|
- **삭제**: 다건 선택 후 헤더+트리 cascade
|
|
- **통합 팝업**: 헤더(제품구분/제목) + 트리 CRUD(추가/하위추가/삭제) + 엑셀 임포트 + 템플릿 다운로드 + 저장
|
|
|
|
### 1.2 범위 밖 (의도적 제외)
|
|
|
|
- 진행관리(P1) WBS 연계 (= 프로젝트 생성 시 템플릿 자동 복사) — wace도 미완성 영역, vexplor_rps에서도 손대지 않음
|
|
- 간트차트 / FN Task 연결 / 작업확정 / 제품별 WBS / 셋업 WBS — wace의 30+ endpoint 중 진행관리/별도 메뉴용
|
|
- `pms_wbs_task` 본체(트리) — 진행관리에서 사용 예정, 본 메뉴 미사용
|
|
|
|
---
|
|
|
|
## 2. 운영판 화면 검증 (2026-05-11 캡처)
|
|
|
|
### 2.1 메인 그리드
|
|
|
|
| 영역 | 내용 |
|
|
|---|---|
|
|
| 화면 제목 | "프로젝트관리_제품구분_WBS관리" |
|
|
| 우상단 버튼 | 삭제 / 등록 / 조회 / 초기화 / [엑셀 다운로드] |
|
|
| 검색 필터 | **제품구분** select 단일 |
|
|
| 그리드 컬럼 | 체크박스 / 제품구분 / 제목 / WBS(폴더) / 등록자 / 등록일 |
|
|
| 운영 데이터 | 1건: Machine / test 생산 / [폴더] / 경영지원팀관리자 / 2026-04-08 |
|
|
|
|
### 2.2 통합 팝업 (등록/수정)
|
|
|
|
- URL: `/project/WBSExcelImportPopUp.do?templateObjId=1120026346` (운영 확인)
|
|
- 제목: 신규 시 "WBS 템플릿 등록" / 수정 시 "WBS 템플릿 수정"
|
|
- 헤더: 제품구분(수정 시 disabled) + 제목(수정 시 disabled)
|
|
- 버튼: Template Download / 추가 / 하위추가 / 삭제 / 저장 / 닫기
|
|
- 드롭존: "Drag & Drop 엑셀 템플릿"
|
|
- 트리 그리드: 선택 / 수준(1/2/3 세 칸) / Unit Name·공정 — TOTAL 행 + 자식 5행 (운영 데이터)
|
|
|
|
### 2.3 엑셀 템플릿 (`WBS_EXCEL_IMPORT_TEMPLATE.xlsx`)
|
|
|
|
| A (수준1) | B (수준2) | C (수준3) | D (unit name /공정) |
|
|
|---|---|---|---|
|
|
| 1 | | | TASK1 |
|
|
| | 1.1 | | TASK2 |
|
|
| | | 1.1.1 | TASK3 |
|
|
| ... | | | |
|
|
|
|
- 1행 = "입력" 라벨(노란색), 2행 = "수준/unit name /공정" 헤더, **3행부터 데이터**.
|
|
- 수준 위치 = depth(1/2/3), 셀 값 = UNIT_NO ("1", "1.1", "1.1.1" 형식 자유 입력).
|
|
- TASK_NAME은 D열.
|
|
|
|
---
|
|
|
|
## 3. 데이터 모델 (운영 1:1)
|
|
|
|
### 3.1 `pms_wbs_template` (헤더, 1건 운영)
|
|
|
|
| 컬럼 | 타입 | 용도 |
|
|
|---|---|---|
|
|
| `objid` | varchar PK | 헤더 키 (Java 측 채번) |
|
|
| `product_objid` | varchar | 제품구분 코드 (CODE_NAME 함수로 라벨화) |
|
|
| `title` | varchar | 템플릿 제목 |
|
|
| `writer` | varchar | 등록자 user_id |
|
|
| `reg_date` | timestamp | 등록일 |
|
|
| `customer_product` | varchar | (미사용 — 운영 비활성 컬럼 `CUSTOMER_PRODUCT`로 흔적만) |
|
|
|
|
### 3.2 `pms_wbs_task_standard` (트리, 5건 운영, 활성 갈래)
|
|
|
|
| 컬럼 | 타입 | 용도 |
|
|
|---|---|---|
|
|
| `objid` | varchar PK | task 키 |
|
|
| `parent_objid` | varchar | **`pms_wbs_template.objid` 참조** (헤더 FK) |
|
|
| `task_name` | varchar | task 이름 (TOTAL / 사용자 입력 / 엑셀 임포트) |
|
|
| `task_seq` | varchar | 폼 제출 순서 (INTEGER 캐스팅 정렬용) |
|
|
| `task_level` | varchar | depth (0=TOTAL, 1/2/3=수준) |
|
|
| `unit_no` | varchar | "1" / "1.1" / "1.1.1" 표기 — depth와 일치 |
|
|
| `upper_task_objid` | varchar | **트리 부모 objid** (TOTAL의 objid가 depth=1의 부모, 이전 depth-1 행이 depth>1의 부모) |
|
|
| `user_id` / `writer` / `reg_date` | varchar/varchar/timestamp | 메타 |
|
|
|
|
→ vexplor_rps DDL 위치: [docs/migration/project/ddl-extracted/200_pms_wbs.sql](ddl-extracted/200_pms_wbs.sql) (8개 테이블 중 2개가 본 메뉴 대상).
|
|
|
|
### 3.3 폐기 / 미사용 (참고)
|
|
|
|
| 테이블 | 운영 카운트 | 폐기 이유 |
|
|
|---|---:|---|
|
|
| `pms_wbs_task_info` | 518 (2021 멈춤) | 매퍼 사용 없음 — 레거시 |
|
|
| `pms_wbs_task_standard2` | 74 | 매퍼 사용 없음 |
|
|
| `pms_wbs_task_confirm` | 0 | 매퍼 사용 없음 |
|
|
|
|
---
|
|
|
|
## 4. wace 1:1 매핑 카탈로그
|
|
|
|
### 4.1 ProjectController.java endpoint (P2 범위 8개)
|
|
|
|
| URL | 라인 | 호출 service | 매퍼 |
|
|
|---|---:|---|---|
|
|
| `/project/wbsTemplateMngList.do` | 2242 | (forward only) + `bizMakeOptionList('0000001')` | (코드맵만) |
|
|
| `/project/wbsTemplateMngGridList.do` | 2270 | `commonService.selectListPagingNew` | `project.wbsTemplateMngGridList` |
|
|
| `/project/WBSExcelImportPopUp.do` | 2282 | `getWBSTemplateMasterInfo` + `getWBSTemplateTaskList` (수정 시) | `getWBSTemplateMasterInfo` + `getWBSTemplateTaskList` |
|
|
| `/project/getWBSTemplateTaskList.do` | 2419 | `getWBSTemplateTaskList` (AJAX, 팝업 로드 시) | `getWBSTemplateTaskList` |
|
|
| `/project/parsingExcelFile.do` | 2319 | `parsingExcelFile` | (Apache POI 직접) |
|
|
| `/project/excelImportFileProc.do` | 2336 | `commonService.insertUploadFileInfo` | (파일 메타만) |
|
|
| `/project/checkWBSTemplateProduct.do` | 2371 | `getWBSTemplateProductList` | `getWBSTemplateProductList` |
|
|
| `/project/saveExcelUploadWBS.do` | 2379 | **`mergeExcelUploadWBS`** (통합 저장) | `deleteWBSTemplateTaskByMaster` + `saveWBSTaskTemp` + `saveWBSTemplateTaskInfo` |
|
|
| `/project/deleteWBSTemplateMaster.do` | 2474 | `deleteWBSTemplateMaster` | `deleteWBSTemplateMaster` + `deleteWBSTemplateMasterTask` |
|
|
|
|
→ 9개 endpoint, 그 중 핵심 워크플로는 4개 (`grid` / `popup-forward` / `parse` / `merge-save`).
|
|
|
|
### 4.2 project.xml 매퍼 SQL (라인번호 + 본문 핵심)
|
|
|
|
#### `wbsTemplateMngGridList` (5552) — 메인 그리드
|
|
|
|
```sql
|
|
SELECT
|
|
OBJID, PRODUCT_OBJID,
|
|
CODE_NAME(PRODUCT_OBJID) AS PRODUCT_NAME,
|
|
TITLE,
|
|
WRITER,
|
|
(SELECT DEPT_NAME || USER_NAME FROM USER_INFO WHERE USER_ID = WRITER) AS WRITER_TITLE,
|
|
REG_DATE,
|
|
TO_CHAR(REG_DATE, 'YYYY-MM-DD') AS REG_DATE_TITLE,
|
|
(SELECT COUNT(1) FROM PMS_WBS_TASK_STANDARD PWTS WHERE PWTS.PARENT_OBJID = OBJID) AS WBS_TASK_CNT,
|
|
CUSTOMER_PRODUCT
|
|
FROM PMS_WBS_TEMPLATE
|
|
WHERE 1=1
|
|
<if test="product != null and product !=''">
|
|
AND PRODUCT_OBJID = #{product}
|
|
</if>
|
|
```
|
|
|
|
→ `CODE_NAME()` 함수 호출 (RPS DB 보유, P1 진행관리에서도 사용).
|
|
|
|
#### `getWBSTemplateMasterInfo` (5647) — 팝업 헤더
|
|
|
|
```sql
|
|
SELECT OBJID, PRODUCT_OBJID, CODE_NAME(PRODUCT_OBJID) AS PRODUCT_OBJID_NAME,
|
|
TITLE, WRITER, REG_DATE, CUSTOMER_PRODUCT
|
|
FROM PMS_WBS_TEMPLATE AS T
|
|
WHERE OBJID = #{OBJID}
|
|
```
|
|
|
|
#### `getWBSTemplateTaskList` (5661) — 팝업 트리
|
|
|
|
```sql
|
|
SELECT T.OBJID, T.PARENT_OBJID, T.TASK_NAME, T.TASK_SEQ, T.TASK_LEVEL,
|
|
T.USER_ID,
|
|
(SELECT USER_NAME FROM USER_INFO WHERE USER_ID = T.USER_ID) AS USER_ID_TITLE,
|
|
T.WRITER, T.REG_DATE, T.UNIT_NO, T.UPPER_TASK_OBJID
|
|
FROM PMS_WBS_TASK_STANDARD AS T
|
|
WHERE T.PARENT_OBJID = #{OBJID}
|
|
ORDER BY CAST(T.TASK_SEQ AS INTEGER)
|
|
```
|
|
|
|
#### `saveWBSTemplateTaskInfo` (5609) — 트리 upsert
|
|
|
|
```sql
|
|
INSERT INTO PMS_WBS_TASK_STANDARD
|
|
(OBJID, PARENT_OBJID, TASK_NAME, TASK_SEQ, TASK_LEVEL, USER_ID,
|
|
WRITER, REG_DATE, UNIT_NO, UPPER_TASK_OBJID)
|
|
VALUES (#{objid}, #{parent_objid}, #{task_name}, #{task_seq}, #{task_level},
|
|
#{user_id}, #{writer}, now(), #{unit_no}, #{upper_task_objid})
|
|
ON CONFLICT (OBJID) DO UPDATE
|
|
SET TASK_NAME = #{task_name}, TASK_SEQ = #{task_seq},
|
|
TASK_LEVEL = #{task_level}, USER_ID = #{user_id},
|
|
UNIT_NO = #{unit_no}, UPPER_TASK_OBJID = #{upper_task_objid}
|
|
```
|
|
|
|
→ **upsert (INSERT … ON CONFLICT DO UPDATE)** — neon/node-postgres에서 동일 패턴 사용.
|
|
|
|
#### `saveWBSTaskTemp` (5587) — 헤더 INSERT
|
|
|
|
```sql
|
|
INSERT INTO PMS_WBS_TEMPLATE
|
|
(OBJID, PRODUCT_OBJID, TITLE, CUSTOMER_PRODUCT, WRITER, REG_DATE)
|
|
VALUES (#{objid}, #{product}, #{title}, #{customer_product}, #{writer}, now())
|
|
```
|
|
|
|
#### `deleteWBSTemplateTaskByMaster` (5643) — 수정 시 트리 cascade clear
|
|
|
|
```sql
|
|
DELETE FROM PMS_WBS_TASK_STANDARD WHERE PARENT_OBJID = #{parent_objid}
|
|
```
|
|
|
|
#### `deleteWBSTemplateMaster` (5726) / `deleteWBSTemplateMasterTask` (5734)
|
|
|
|
```sql
|
|
DELETE FROM PMS_WBS_TEMPLATE WHERE OBJID IN (...)
|
|
DELETE FROM PMS_WBS_TASK_STANDARD WHERE PARENT_OBJID IN (...)
|
|
```
|
|
|
|
→ 헤더 다건 삭제 + cascade. 트랜잭션 1개.
|
|
|
|
#### `getWBSTemplateProductList` (5576) — 중복 체크
|
|
|
|
```sql
|
|
SELECT *
|
|
FROM PMS_WBS_TEMPLATE AS T
|
|
LEFT OUTER JOIN PMS_WBS_TASK_STANDARD AS T1 ON T.OBJID = T1.PARENT_OBJID
|
|
WHERE T.PRODUCT_OBJID = #{PRODUCT}
|
|
AND T.TITLE = #{TITLE}
|
|
```
|
|
|
|
→ 신규 등록 시 동일 (제품구분 + 제목) 조합 있는지 사전 체크.
|
|
|
|
### 4.3 ProjectService 핵심 메소드
|
|
|
|
#### `mergeExcelUploadWBS` (line 2902, 통합 저장)
|
|
|
|
```
|
|
PersonBean.userId → writer
|
|
request.parameter("product"|"title"|"templateObjId"|"customer_product")
|
|
|
|
분기:
|
|
if templateObjId != "":
|
|
wbsMasterObjId = templateObjId # 헤더 재사용 (UPDATE 안 함!)
|
|
sqlSession.delete("deleteWBSTemplateTaskByMaster", {parent_objid: wbsMasterObjId})
|
|
else:
|
|
wbsMasterObjId = CommonUtils.createObjId()
|
|
sqlSession.insert("saveWBSTaskTemp", {objid, product, title, customer_product, writer})
|
|
|
|
루프 (WBS_TASK_OBJID 배열):
|
|
for i, wbsObjId in enumerate(WBS_TASK_OBJID):
|
|
{TASK_NAME, UNIT_NO, UPPER_TASK_OBJID, TASK_LEVEL} = request 각 hidden
|
|
sqlSession.insert("saveWBSTemplateTaskInfo", {
|
|
objid, task_name, task_seq=i+1, task_level, unit_no,
|
|
upper_task_objid, parent_objid=wbsMasterObjId, writer
|
|
})
|
|
|
|
sqlSession.commit()
|
|
```
|
|
|
|
**핵심 패턴**:
|
|
- 수정 모드는 **헤더 UPDATE 안 함** (product/title 변경 불가). 트리만 일괄 DELETE → INSERT.
|
|
- 신규 모드는 헤더 INSERT + 트리 INSERT.
|
|
- 트리 순서(task_seq)는 폼 제출 순서.
|
|
|
|
#### `parsingExcelFile` (line 2779)
|
|
|
|
```
|
|
fileList = commonService.getFileList({targetObjId, docType: "WBS_EXCEL_IMPORT"})
|
|
첫 파일을 XSSFWorkbook으로 읽음.
|
|
|
|
루프 rowIndex = 2 ~ end (3행부터):
|
|
열 0/1/2 → levelValues[0..2] # 수준1/2/3 셀
|
|
열 3 → taskName
|
|
|
|
unitNo = levelValues[2] or levelValues[1] or levelValues[0] # depth 3→2→1 우선
|
|
if unitNo and taskName:
|
|
result.add({WBS_OBJID = createObjId(), UNIT_NO = unitNo, TASK_NAME = taskName})
|
|
```
|
|
|
|
→ depth는 클라이언트가 unit_no의 `.` 개수로 계산 (`.match(/\./g) || []).length + 1`).
|
|
→ 파싱 결과는 클라이언트에 List 리턴, **DB 저장은 사용자가 "저장" 버튼 누를 때 mergeExcelUploadWBS에서**.
|
|
|
|
#### `deleteWBSTemplateMaster` (line 3284)
|
|
|
|
```
|
|
SqlSession (transactional)
|
|
sqlSession.delete("deleteWBSTemplateMaster", {checkArr})
|
|
sqlSession.delete("deleteWBSTemplateMasterTask", {checkArr})
|
|
commit
|
|
```
|
|
|
|
---
|
|
|
|
## 5. UI 동작 명세 (`WBSExcelImportPopUp.jsp` 1:1)
|
|
|
|
### 5.1 모드 분기
|
|
|
|
```js
|
|
isEditMode = (templateObjId !== "")
|
|
if isEditMode:
|
|
loadExistingTasks() // AJAX → /project/getWBSTemplateTaskList.do
|
|
title.value = masterInfo.TITLE
|
|
product.value = masterInfo.PRODUCT_OBJID
|
|
else:
|
|
addTotalRow() // 빈 트리에 TOTAL 행 1개만
|
|
```
|
|
|
|
### 5.2 트리 행 구조
|
|
|
|
```html
|
|
<tr id="row_{objId}" data-depth="?">
|
|
<td><input type="checkbox" name="rowCheck" value="{objId}"></td>
|
|
<input type="hidden" name="WBS_TASK_OBJID" value="{objId}">
|
|
<input type="hidden" name="UNIT_NO_{objId}" value="">
|
|
<input type="hidden" name="UPPER_TASK_OBJID_{objId}" value="">
|
|
<input type="hidden" name="TASK_LEVEL_{objId}" value="">
|
|
<td><input class="lvl_input" data-level="1"></td> <!-- 수준1 -->
|
|
<td><input class="lvl_input" data-level="2"></td> <!-- 수준2 -->
|
|
<td><input class="lvl_input" data-level="3"></td> <!-- 수준3 -->
|
|
<td><input name="TASK_NAME_{objId}" value="..."></td>
|
|
</tr>
|
|
```
|
|
|
|
- TOTAL 행은 hidden TASK_LEVEL=0 / UNIT_NO=0 / TASK_NAME=TOTAL / 체크박스 없음 / "TOTAL" 텍스트 표시.
|
|
- **수준 1/2/3은 셋 중 하나에만 값 입력 가능** (`bindLevelInput`): 한 칸 입력 시 다른 두 칸 비우고 UNIT_NO + TASK_LEVEL 동기화.
|
|
|
|
### 5.3 버튼 핸들러
|
|
|
|
| 버튼 | 함수 | 동작 |
|
|
|---|---|---|
|
|
| Template Download | `templateDownload.click` | `location.href="/template/WBS_EXCEL_IMPORT_TEMPLATE.xlsx"` (정적 파일) |
|
|
| 추가 | `addRow()` | 선택된 행 다음에 같은 depth 행 추가. 선택 없으면 마지막에 append. |
|
|
| 하위추가 | `addChildRow()` | 선택된 행의 하위(depth+1) 행을 자식 마지막 다음에 추가. depth>=3 거부. |
|
|
| 삭제 | `deleteRow()` | 선택된 행 + 하위 후손 행 일괄 삭제. cascade 확인 alert. |
|
|
| 저장 | `saveWBS()` | 검증 → calculateParentRelations() → POST `/project/saveExcelUploadWBS.do` |
|
|
| 닫기 | `self.close()` | 팝업 닫기 |
|
|
|
|
### 5.4 저장 검증 로직 (`saveWBS()`)
|
|
|
|
```
|
|
1. title 빈값 거부
|
|
2. WBS_TASK_OBJID 0개 거부 (등록할 항목 없음)
|
|
3. 각 행에서 UNIT_NO + TASK_NAME 빈값 거부 (TOTAL 행 제외)
|
|
4. 신규 모드일 때 fn_checkWBSTemplateRevision() — (PRODUCT, TITLE) 중복 거부
|
|
5. calculateParentRelations() — 모든 행 순회하며 UPPER_TASK_OBJID 채우기:
|
|
- depth=1 → totalObjId (TOTAL 행 objid)
|
|
- depth>1 → 이전 prevAll 중 depth-1인 가장 가까운 행 objid
|
|
6. POST form serialize → opener.fn_search() + self.close()
|
|
```
|
|
|
|
### 5.5 엑셀 임포트 흐름
|
|
|
|
```
|
|
사용자 파일 드롭 → fileUploadPreProc() → preFileDelete() (기존 삭제 alert)
|
|
↓
|
|
fnc_setFileDropZone POST /project/excelImportFileProc.do (Multipart)
|
|
↓ 업로드 완료 콜백 → setExcelFileArea() → setUploadTemplateFile()
|
|
↓ getFileList.do AJAX → 첨부 표시 + parsingExcelFile()
|
|
↓ parsingExcelFile() POST /project/parsingExcelFile.do
|
|
↓ 응답 List(WBS_OBJID, UNIT_NO, TASK_NAME)
|
|
↓ 각 row를 #wbsTaskList 에 append (depth는 unit_no의 '.' 개수로 계산)
|
|
```
|
|
|
|
### 5.6 그리드 행 클릭 (메인 화면)
|
|
|
|
- WBS 폴더 아이콘 클릭 → `fn_openWBSTaskListPopUp(objid)` → `/project/WBSExcelImportPopUp.do?templateObjId={objid}` 팝업 (1340x700)
|
|
- 등록 버튼 → `/project/WBSExcelImportPopUp.do?product={product}` 팝업
|
|
- 제품구분 미선택 시 alert "제품은 필수값입니다 제품을 선택해 주세요"
|
|
|
|
---
|
|
|
|
## 6. vexplor_rps 이식 매핑 (구현 계획)
|
|
|
|
### 6.1 backend-node
|
|
|
|
| wace | vexplor_rps | 비고 |
|
|
|---|---|---|
|
|
| `ProjectController` WBS template 9개 | `backend-node/src/controllers/wbsTemplateController.ts` | REST 통합 |
|
|
| `ProjectService.mergeExcelUploadWBS` | `backend-node/src/services/wbsTemplateService.ts` `saveTemplate(payload)` | upsert 패턴 그대로 |
|
|
| `ProjectService.parsingExcelFile` | `backend-node/src/services/wbsTemplateService.ts` `parseExcelFile(buffer)` | Apache POI → **`xlsx`** npm 패키지 |
|
|
| `CODE_NAME(PRODUCT_OBJID)` | DB 함수 직접 호출 (P1 진행관리와 동일) | RPS DB 보유 |
|
|
| `commonService.getFileList` | 영업관리 첨부 패턴 재사용 또는 임포트 시 메모리 처리 |
|
|
|
|
#### REST endpoint 매핑
|
|
|
|
```
|
|
GET /api/project/wbs-template — 메인 그리드 (product 필터)
|
|
GET /api/project/wbs-template/:id — 헤더 + 트리 (팝업 진입)
|
|
POST /api/project/wbs-template — 신규 저장 (헤더 + 트리)
|
|
PUT /api/project/wbs-template/:id — 수정 저장 (트리만 일괄 DELETE→INSERT)
|
|
DELETE /api/project/wbs-template — 다건 삭제 (헤더 + cascade)
|
|
POST /api/project/wbs-template/parse-excel — 엑셀 파일 multipart → 파싱 결과 JSON
|
|
GET /api/project/wbs-template/check-duplicate?product=&title= — 중복 체크
|
|
GET /api/project/wbs-template/excel-template — 엑셀 템플릿 다운로드 (`public/templates/WBS_EXCEL_IMPORT_TEMPLATE.xlsx` 정적)
|
|
```
|
|
|
|
#### objid 채번
|
|
|
|
wace는 `CommonUtils.createObjId()` (시퀀스 함수 기반 string). vexplor_rps는 `nanoid()` 또는 `crypto.randomUUID()` → 영업관리 패턴 확인 후 통일.
|
|
|
|
### 6.2 frontend
|
|
|
|
| wace | vexplor_rps | 비고 |
|
|
|---|---|---|
|
|
| `wbsTemplateMngList.jsp` | `frontend/app/(main)/COMPANY_16/project/wbs-template/page.tsx` | 메인 페이지 |
|
|
| 통합 팝업 `WBSExcelImportPopUp.jsp` | `frontend/components/project/WbsTemplateDialog.tsx` | shadcn Dialog + 트리 테이블 + 파일 드롭존 |
|
|
| 메인 그리드 | `DataGrid` 5컬럼 (영업관리 패턴 재사용) | frozen 제품구분 |
|
|
| API 클라이언트 | `frontend/lib/api/wbsTemplate.ts` | 영업관리 패턴 |
|
|
| 메뉴 | `AdminPageRenderer.tsx` dynamic 등록 | 라우트 `/COMPANY_16/project/wbs-template` |
|
|
|
|
#### 트리 UI 결정 사항
|
|
|
|
운영판은 jQuery 기반 단순 테이블 + hidden input 직렬화. vexplor_rps는 React 상태로 트리 행 관리:
|
|
- 행 배열 + depth 필드 (1~3) + `objid` 클라이언트 측 생성 (`nanoid()`)
|
|
- 추가/하위추가/삭제는 배열 조작
|
|
- 수준 1/2/3 컬럼 단일 입력 보장
|
|
- 저장 시 task_seq = index+1, upper_task_objid 자동 계산 (depth=1→TOTAL, depth>1→이전 depth-1 항목 objid)
|
|
|
|
#### 엑셀 라이브러리
|
|
|
|
- `xlsx` (SheetJS) — backend-node에서 파싱
|
|
- 정적 엑셀 템플릿 파일은 `frontend/public/templates/WBS_EXCEL_IMPORT_TEMPLATE.xlsx`에 wace 원본 그대로 배치
|
|
|
|
---
|
|
|
|
## 7. 함정 & 결정 메모
|
|
|
|
### 7.1 수정 모드 헤더 변경 불가
|
|
운영판 mergeExcelUploadWBS는 templateObjId 있으면 헤더 UPDATE를 호출하지 않음. 화면에서 product1 disabled / title은 표시만. vexplor_rps도 동일하게 disabled 처리 — **헤더 수정 기능은 추가하지 않음**.
|
|
|
|
### 7.2 트리 일괄 DELETE → INSERT
|
|
수정 시 운영은 `deleteWBSTemplateTaskByMaster`로 트리 전체 삭제 후 폼 데이터로 재삽입. vexplor_rps도 같은 패턴 (트랜잭션 1개). 부분 update/delete 분기 없음 — 간단하고 안전.
|
|
|
|
### 7.3 task_seq는 INTEGER 캐스팅 정렬
|
|
SQL `ORDER BY CAST(T.TASK_SEQ AS INTEGER)`. wace는 폼 제출 순서로 1, 2, 3, ... 매김. vexplor_rps도 동일.
|
|
|
|
### 7.4 upper_task_objid 자동 계산
|
|
클라이언트 측 `calculateParentRelations()` — 백엔드로 보내기 전 결정. depth=1 행의 부모는 TOTAL 행 objid. 운영판 그대로.
|
|
|
|
### 7.5 CUSTOMER_PRODUCT 컬럼은 비활성
|
|
운영판 메인 그리드의 "고객사_장비목적" 컬럼은 JSP 주석 처리됨. `saveWBSTemplateMasterInfo`(UPDATE)는 CUSTOMER_PRODUCT만 수정하는데 호출하는 곳이 비활성 마스터 폼뿐. vexplor_rps 초기 이식에서는 **빼고**, customer_product는 DB에 보존만 함.
|
|
|
|
### 7.6 엑셀 1행/2행 무시
|
|
파싱 루프 `for(rowIndex = 2 ; ...)` — 1행(입력 라벨) + 2행(수준/unit name 헤더) 무시, **3행부터 데이터**. vexplor_rps도 동일.
|
|
|
|
### 7.7 신규 등록 시 product 필수
|
|
`wbsTemplateMngList.jsp` 라인 53-57: 등록 버튼 클릭 시 product 비었으면 alert. 운영 UX 그대로.
|
|
|
|
### 7.8 폐기 갈래 안 건드림
|
|
`pms_wbs_task_info`, `_standard2`, `_confirm`은 DDL은 보존했지만 매퍼/서비스/UI에서 절대 사용하지 않음. 향후 진행관리 P2(WBS 진행 트리) 진입 시 `pms_wbs_task` 본체만 추가 매핑.
|
|
|
|
### 7.9 진행관리 P1과의 연결은 P2 범위 외
|
|
프로젝트(주문) 생성 시 템플릿 자동 복사 흐름은 wace도 미완성 — 사용자 명시. vexplor_rps도 추후 별도 단계로.
|
|
|
|
---
|
|
|
|
## 8. 검증 베이스라인 (운영DB)
|
|
|
|
운영DB에 1건만 있어 화면 검증 단순:
|
|
|
|
```sql
|
|
-- 메인 그리드 조회 (운영 wbsTemplateMngGridList 1:1)
|
|
SELECT OBJID, CODE_NAME(PRODUCT_OBJID) AS PRODUCT_NAME, TITLE, WRITER,
|
|
TO_CHAR(REG_DATE,'YYYY-MM-DD') AS REG_DATE_TITLE,
|
|
(SELECT COUNT(1) FROM pms_wbs_task_standard WHERE parent_objid = t.objid) AS WBS_TASK_CNT
|
|
FROM pms_wbs_template t;
|
|
-- 운영: 1건 (Machine / test 생산 / 경영지원팀관리자 / 2026-04-08 / WBS_TASK_CNT=5)
|
|
|
|
-- 트리 조회 (운영 getWBSTemplateTaskList 1:1)
|
|
SELECT objid, task_name, task_seq, task_level, unit_no, upper_task_objid
|
|
FROM pms_wbs_task_standard
|
|
WHERE parent_objid = '1120026346'
|
|
ORDER BY CAST(task_seq AS INTEGER);
|
|
-- 운영: 5건 (TOTAL + ㅁㅁ4건)
|
|
```
|
|
|
|
vexplor_rps 측은 운영 데이터 시드 없이 빈 상태에서 시작 — 화면 검증은 사용자가 직접 등록·저장·수정·삭제로.
|
|
|
|
---
|
|
|
|
## 9. 산출물 체크리스트
|
|
|
|
- [x] DDL: `docs/migration/project/ddl-extracted/200_pms_wbs.sql` (8개 테이블)
|
|
- [x] DDL README: `docs/migration/project/ddl-extracted/README.md`
|
|
- [x] GAP 문서: 본 파일
|
|
- [ ] backend service: `backend-node/src/services/wbsTemplateService.ts`
|
|
- [ ] backend controller/route: `backend-node/src/controllers/wbsTemplateController.ts` + `routes/wbsTemplateRoutes.ts`
|
|
- [ ] frontend page: `frontend/app/(main)/COMPANY_16/project/wbs-template/page.tsx`
|
|
- [ ] frontend dialog: `frontend/components/project/WbsTemplateDialog.tsx`
|
|
- [ ] frontend api: `frontend/lib/api/wbsTemplate.ts`
|
|
- [ ] AdminPageRenderer dynamic 등록
|
|
- [ ] 엑셀 템플릿 정적 파일: `frontend/public/templates/WBS_EXCEL_IMPORT_TEMPLATE.xlsx`
|
|
- [ ] 검증: 운영판 1:1 등록/수정/삭제/엑셀 임포트 동작
|