Files
wace_rps/docs/migration/development/01-part.md
T
hjjeong ea6606da0c 개발관리>PART 등록·조회 메뉴 신설 (PR-A) — wace partMng 1:1 이식
backend (M1+M2):
- devPartService: listTemp/listRelease/getByObjid/create/update/deploy/removeMany
- partMngBaseSimple SELECT + 추가 15컬럼(acctfg/odrfg/unit_dc/unitmang_dc/lot_fg 등) 라벨/CASE
- deploy 트랜잭션 3단계 (isLastInit → part_mng_history INSERT → partMngDeploy + EO_NO 채번)
- EO_NO 분기: is_longd='1'→EOB{yy}-{seq} / else EO{yy}-{seq}
- objidUtil: wace CommonUtils.createObjId() 1:1 (bigint objid 채번)
- DDL: 9 신규 테이블 + part_mng 15컬럼 ALTER (운영판 1:1 추출)

frontend (M1+M2):
- part-regist (M1) / part-search (M2): 23셀 그리드 + 검색폼 + 액션
- PartFormDialog: 등록/수정 통합 (mode prop, 4 섹션)
- PartDetailDialog: 읽기 전용 + "수정" dispatch
- AdminPageRenderer dynamic 임포트 2건 + menu_info URL spec 정렬

본 PR 제외 (별 PR): 도면 다중 업로드, ERP 업로드, Excel Import, BOM_PART_QTY R/W

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:14:10 +09:00

314 lines
12 KiB
Markdown

# PR-A : PART 등록·조회 묶음 구현 명세
> 작성: 2026-05-12 / 범위: 개발관리 M1(PART 등록) + M2(PART 조회) — 같은 `part_mng` 테이블 R/W, 매퍼 공유.
---
## 1. 매퍼 쿼리 1:1 매핑
원본 `wace_plm/src/com/pms/mapper/partMng.xml`:
| Query id | Line | 본 PR 매핑 | 용도 |
|---|---:|---|---|
| `partMngBaseSimple` (sql) | 78 | (서비스 측 공통 SELECT fragment) | PART_MNG 메인 88+ 컬럼 SELECT |
| `partMngTempGridList` | 2,354 | `GET /api/development/part-temp/list` | M1 그리드 (status != 'release') + ORDER_SPEC_MNG·ADMIN_SUPPLY_MNG JOIN |
| `partMngGridList` | 1,903 | `GET /api/development/part/list` | M2 그리드 (status = 'release' 고정) |
| `partMngInfo` | 2,699 | `GET /api/development/part/:objid` | 상세 단건 (편집 팝업 진입) |
| `insertpartInfo` | 7,625 | `POST /api/development/part` | 신규 등록 (38 컬럼 INSERT) |
| `updatePartDetail` | 2,711 | `PUT /api/development/part/:objid` | 상세 수정 (21 컬럼 UPDATE) |
| `partMngDeploy` | 4,190 | `POST /api/development/part-temp/deploy` | 확정 (M1→M2) STATUS='release', EO_NO 채번 |
| `partMngIsLastInit` | 4,230 | (deploy 트랜잭션 내부) | 동일 PART_NO 이전 IS_LAST='0' |
| `insertPartMngHistory` | 4,244 | (deploy 트랜잭션 내부) | PART_MNG_HISTORY 이력 INSERT |
| `partMngDelete` | 4,486 | `DELETE /api/development/part` (body: `objids: string[]`) | 다중 삭제 |
`partMngBaseSimple` SELECT 핵심: `PART_MNG P` + `COMM_CODE CC_UNIT`(UNIT) + `COMM_CODE CC_PART`(PART_TYPE) + `admin_supply_mng SUP`(SUPPLY_CODE) + LATERAL `BOM_PART_QTY`(LAST_PART_OBJID·status='deploy'·최신 1행) + LATERAL `COMM_CODE`(CHANGE_OPTION 다중 라벨) + `ATTACH_FILE_INFO`(3D/2D/PDF 파일 카운트). 23개 그리드 컬럼 + CODE_NAME 라벨 + Y/N flag CASE 변환 자체 처리.
---
## 2. API 엔드포인트 명세
### 2.1 M1 그리드 — `GET /api/development/part-temp/list`
**Query**:
```
search_part_no?: string
search_part_name?: string
search_material?: string
search_spec?: string
search_part_type?: string (PART_TYPE_CODE comm_code id)
writer?: string
status?: string // 단일: 'create'/'changing'/'editing'
status_arr?: string[] // 다중 (둘 중 하나만 사용)
product_code?: string
upg_no?: string
page?: number // 기본 1
page_size?: number // 기본 20
```
**SQL** (요약):
```sql
SELECT T.*, SORT (REVISION), O.PARTNER_TITLE, Q.OBJID/CHILD_OBJID/QTY/QTY_TEMP, Q_QTY (CASE),
(SELECT PART_NO FROM PART_MNG SP WHERE SP.OBJID = Q.PARENT_PART_NO) PARENT_PART_INFO
FROM <partMngBaseSimple> T
LEFT JOIN (ORDER_SPEC_MNG OSM JOIN ADMIN_SUPPLY_MNG SUP) O ON T.OBJID::VARCHAR = O.PART_OBJID::VARCHAR
LEFT JOIN BOM_PART_QTY Q ON (
T.OBJID IN (SELECT DISTINCT PM1.OBJID FROM PART_MNG PM1, PART_MNG PM2
WHERE PM1.STATUS='changing' AND PM2.STATUS!='changing'
AND PM2.OBJID = Q.PART_NO AND PM1.PART_NO = PM2.PART_NO)
AND Q.STATUS = 'beforeEdit'
)
WHERE 1=1 + (SEARCH_PART_NO/NAME/MATERIAL/SPEC/PART_TYPE, WRITER, STATUS, STATUS_ARR)
ORDER BY PARENT_PART_INFO, T.PART_NO
```
**Response**:
```ts
{
rows: PartTempRow[]; // 그리드 23셀 + 추가 필드
total: number;
page: number;
pageSize: number;
}
```
### 2.2 M2 그리드 — `GET /api/development/part/list`
**Query**: 위 + `search_year?` `search_hardness?` `search_method?` `search_surface?` `customer_objid?` `customer_cd?` `project_name?` `unit_code?` `search_design_date_from?` `search_design_date_to?` `is_last?` `eo?`
**SQL** (요약):
```sql
SELECT NUM (ROW_NUMBER), T.*,
DECODE(PART_TYPE, '0000063', '1',
(SELECT SUM(...) FROM BOM_PART_QTY Q WHERE Q.LAST_PART_OBJID=T.OBJID)::CHARACTER) BOM_QTY
FROM <partMngBaseSimple> T
WHERE 1=1 AND T.status='release' -- M1 vs M2 핵심 차이
+ (M1 + 5)
```
**Response**: `{ rows: PartRow[]; total, page, pageSize }`
### 2.3 단건 상세 — `GET /api/development/part/:objid`
```sql
SELECT T.* FROM <partMngBaseSimple> T WHERE T.OBJID = #{OBJID}
```
`PartRow` 단일 반환. 404 시 `{ error: 'not_found' }`.
### 2.4 신규 등록 — `POST /api/development/part`
**Body** (38 컬럼, 핵심):
```ts
{
part_objid: string; // numeric, 클라이언트 채번 (nanoid-based) 또는 서버 채번
part_no: string;
part_name: string;
unit?: string; // comm_code
qty?: string;
spec?: string;
material?: string;
thickness?: string; width?: string; height?: string;
out_diameter?: string; in_diameter?: string; length?: string;
remark?: string;
part_type: string; // comm_code (PART_TYPE_CODE)
product_mgmt_objid?: string;
supply_code?: string;
maker?: string;
contract_objid?: string;
post_processing?: string;
heat_treatment_hardness?: string;
heat_treatment_method?: string;
surface_treatment?: string;
acctfg?: string; // comm_code 계정구분
odrfg?: string; // 0=구매/1=생산/8=Phantom
unit_dc?: string; unitmang_dc?: string;
unitchng_nb?: string;
lot_fg?: '0'|'1';
use_yn?: '0'|'1';
qc_fg?: '0'|'1';
setitem_fg?: '0'|'1';
req_fg?: '0'|'1';
unit_length?: string;
unit_qty?: string;
}
```
**SQL**: `insertpartInfo` (7,625) 그대로. `STATUS='create'`, `REG_DATE=now()`, `IS_LAST='1'`, `WRITER=#{CONNECTUSERID}` (서버에서 `req.user.user_id` 주입).
**채번 정책**: `part_mng.objid`**`bigint`** 타입(다른 영업관리 테이블 `contract_mgmt.objid` 등은 varchar — `genObjid("CM")` 패턴 사용). bigint 컬럼은 prefix-string 못 쓰므로 **wace `CommonUtils.createObjId()` 1:1 구현** 사용:
```typescript
// backend-node/src/utils/objidUtil.ts (신규)
import { randomUUID } from 'crypto';
function javaStringHashCode(s: string): number {
let h = 0;
for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
return h;
}
/** wace CommonUtils.createObjId() 1:1 — UUID v4 → 하이픈 제거(32 hex) → Java String.hashCode (int32) → String. 결과: -2,147,483,648 ~ 2,147,483,647. */
export function createObjId(): string {
return String(javaStringHashCode(randomUUID().replaceAll('-', '')));
}
```
INSERT 시 `body.part_objid` 가 비어 있으면 서버에서 `createObjId()` 호출(클라이언트 채번도 허용하되 권장 X).
### 2.5 상세 수정 — `PUT /api/development/part/:objid`
**Body** (21 컬럼, `updatePartDetail` 1:1):
`part_name, material, heat_treatment_hardness, heat_treatment_method, surface_treatment, maker, part_type, acctfg, odrfg, spec, unit_dc, unitmang_dc, unitchng_nb, lot_fg, use_yn, qc_fg, setitem_fg, req_fg, unit_length, unit_qty, remark`
`EDIT_DATE = NOW()` 자동.
### 2.6 확정 — `POST /api/development/part-temp/deploy`
**Body**: `{ objids: string[] }` — 다중 선택 확정.
**트랜잭션 (각 objid에 대해 순차 처리)**:
1. `partMngIsLastInit`: 같은 PART_NO 모든 행 `IS_LAST='0'`
2. `insertPartMngHistory`: 현재 행을 `PART_MNG_HISTORY`로 복사 (이력 보존)
3. `partMngDeploy`: 본 행 `IS_LAST='1'`, `STATUS='release'`, `DEPLOY_DATE=NOW()`, `REVISION=COALESCE(REVISION,'RE')`, `EO_DATE=...`, `EO_NO=` 채번 (IS_LONGD에 따라 `EOB{yy}-{seq}` or `EO{yy}-{seq}`)
**EO_NO 채번 SQL** (wace 운영판 그대로):
```sql
CASE WHEN P.IS_LONGD = '1' THEN
'EOB' || TO_CHAR(NOW(),'yy') || '-' || LPAD(
(SELECT COALESCE(SUBSTR(MAX(EO_NO),7,8)::INTEGER+1, 1)
FROM PART_MNG SP
WHERE SP.EO_NO LIKE 'EOB' || TO_CHAR(NOW(),'yy') || '-%'
AND SP.PART_NO != P.PART_NO
AND SP.REVISION != P.REVISION
)||'', 4, '0')
ELSE
'EO' || TO_CHAR(NOW(),'yy') || '-' || LPAD(... 'EO{yy}-{seq}' ...)
END
```
**Response**: `{ deployed: number, eo_nos: Record<objid, eo_no> }`
### 2.7 다중 삭제 — `DELETE /api/development/part`
**Body**: `{ objids: string[] }`
**SQL** (wace 그대로 POSITION 트릭):
```sql
DELETE FROM PART_MNG WHERE POSITION(OBJID||',' IN #{checkArr}||',') > 0
```
→ backend-node에서는 PostgreSQL 표준인 `WHERE OBJID = ANY($1::numeric[])` 로 정리(동일 효과 + 인덱스 활용 가능).
---
## 3. Backend 파일 구조
```
backend-node/src/
routes/
devPartRoutes.ts // Express Router — 7 endpoint
controllers/
devPartController.ts // req/res 처리, validation
services/
devPartService.ts // SQL 실행 (pg 트랜잭션 처리 포함)
devPartSqlFragments.ts // partMngBaseSimple SELECT fragment 재사용
```
`app.ts``app.use('/api/development', devPartRoutes)` 추가 (또는 메뉴 묶음 라우터 도입 시 그쪽).
---
## 4. Frontend 파일 구조
```
frontend/
app/(main)/COMPANY_16/development/
part-regist/
page.tsx // M1 그리드 + 상단 액션 + 페이징
part-search/
page.tsx // M2 그리드 + 상단 액션 + 페이징
components/development/
PartFormDialog.tsx // 신규/수정 통합 (mode prop)
PartDetailDialog.tsx // 읽기 전용 상세
lib/api/
devPart.ts // 7 endpoint 호출 함수 + 타입
```
### 4.1 그리드 23셀 (M1·M2 공통)
| key | 라벨 | 정렬 | 너비 |
|---|---|---|---:|
| part_no | 품번 | left | 140 |
| part_name | 품명 | left | 220 |
| cu01_cnt | 3D | right | 60 |
| cu02_cnt | 2D | right | 60 |
| cu03_cnt | PDF | right | 60 |
| material | 재료 | left | 100 |
| heat_treatment_hardness | 열처리경도 | left | 110 |
| heat_treatment_method | 열처리방법 | left | 110 |
| surface_treatment | 표면처리 | left | 100 |
| maker | 메이커 | left | 100 |
| part_type_title | 범주이름 | left | 100 |
| spec | 규격 | left | 140 |
| acctfg_nm | 계정구분 | center | 80 |
| odrfg_nm | 조달구분 | center | 80 |
| unit_dc_nm | 재고단위 | center | 80 |
| unitmang_dc_nm | 관리단위 | center | 80 |
| unitchng_nb | 환산수량 | right | 90 |
| lot_fg_nm | LOT구분 | center | 80 |
| use_yn_nm | 사용여부 | center | 80 |
| qc_fg_nm | 검사여부 | center | 80 |
| setitem_fg_nm | SET품여부 | center | 90 |
| req_fg_nm | 의뢰여부 | center | 80 |
| unit_length / unit_qty | 개당길이/수량 | right | 100 |
추가 (M1만): `partner_title`, `q_qty`, `parent_part_info`
추가 (M2만): `bom_qty`
### 4.2 검색 폼
**M1 (PART 등록)** — 2 필드: SEARCH_PART_NO · SEARCH_PART_NAME (둘 다 PartSelect autocomplete)
**M2 (PART 조회)** — 메인 조회 화면 (별도 검색 폼 없음, 그리드 헤더 inline 필터로 처리하거나 상단 간소화 검색바 1줄로 통합 — 본 PR 우선 `<Input>` 2개로 시작, 추후 보강)
### 4.3 액션 버튼 (각 page 상단)
**M1**: 등록 · 수정 · 삭제 · 확정 · 조회
**M2**: 등록 · 수정 · 삭제 · 조회 (도면연동/ERP업로드/Excel은 본 PR 제외)
### 4.4 PartFormDialog (신규/수정 통합)
- mode: `'create' | 'edit'`
- 38 필드 — `<Input>` + `<CommCodeSelect>` 조합
- 검증: part_no/part_name 필수, comm_code 필드는 SmartSelect
- 신규: POST → 신규 행 추가
- 수정: PUT → 21 필드만 전송 (insertpartInfo는 38, updatePartDetail는 21 — wace 그대로)
### 4.5 PartDetailDialog (읽기 전용)
행 더블클릭 시 진입. 모든 필드 disabled. "수정" 버튼 → PartFormDialog(mode='edit') 전환.
---
## 5. 본 PR 제외 항목
| 항목 | 사유 / 후속 |
|---|---|
| 도면 다중 업로드 (M1) | `ATTACH_FILE_INFO` 다파일 업로드 — 별 PR |
| ERP 업로드 (M2) | wace 외부 시스템 연동 — 별 PR |
| Excel Upload (M1·M2) | `openPartExcelImportPopUp.jsp` 별도 — 별 PR |
| BOM_PART_QTY R/W (M3 영역) | PR-B 에서 다룸 |
| EO_NO 채번 분기 일부 (`IS_LONGD` flag) | 본 PR 포함 — 운영판 그대로 |
---
## 6. 검증 시나리오 (verify.md 기준)
1. M1 페이지 진입 → 그리드 표시(status != 'release') 확인
2. "등록" → PartFormDialog 신규 → POST → M1 그리드에 새 행
3. M1 행 선택 → "확정" → POST deploy → STATUS='release', EO_NO 채번 확인
4. M2 페이지 진입 → deploy된 행이 M2 그리드에 표시
5. M2 행 선택 → "수정" → PartFormDialog 수정 → PUT
6. M2 행 다중 선택 → "삭제" → DELETE → 그리드에서 제거
7. 검색 (SEARCH_PART_NO/NAME) → 필터 적용 확인
8. 운영DB 11133/waceplm 의 동일 SQL 결과와 vexplor_rps 결과 행 수 비교 (sanity)