ea6606da0c
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>
314 lines
12 KiB
Markdown
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)
|