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

21 KiB

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 (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) — 메인 그리드

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) — 팝업 헤더

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) — 팝업 트리

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

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

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

DELETE FROM PMS_WBS_TASK_STANDARD WHERE PARENT_OBJID = #{parent_objid}

deleteWBSTemplateMaster (5726) / deleteWBSTemplateMasterTask (5734)

DELETE FROM PMS_WBS_TEMPLATE WHERE OBJID IN (...)
DELETE FROM PMS_WBS_TASK_STANDARD WHERE PARENT_OBJID IN (...)

→ 헤더 다건 삭제 + cascade. 트랜잭션 1개.

getWBSTemplateProductList (5576) — 중복 체크

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 모드 분기

isEditMode = (templateObjId !== "")
if isEditMode:
  loadExistingTasks()   // AJAX → /project/getWBSTemplateTaskList.do
  title.value = masterInfo.TITLE
  product.value = masterInfo.PRODUCT_OBJID
else:
  addTotalRow()         // 빈 트리에 TOTAL 행 1개만

5.2 트리 행 구조

<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건만 있어 화면 검증 단순:

-- 메인 그리드 조회 (운영 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. 산출물 체크리스트

  • DDL: docs/migration/project/ddl-extracted/200_pms_wbs.sql (8개 테이블)
  • DDL README: docs/migration/project/ddl-extracted/README.md
  • 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 등록/수정/삭제/엑셀 임포트 동작