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