Files
hjjeong 50669a66ee 프로젝트관리>제품구분_WBS관리 메뉴 신설 — wace WBS 템플릿 1:1 이식
· 메인 그리드 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>
2026-05-12 13:43:51 +09:00

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 등록/수정/삭제/엑셀 임포트 동작