diff --git a/frontend/components/development/PartFormDialog.tsx b/frontend/components/development/PartFormDialog.tsx index db2224cf..bb585fd7 100644 --- a/frontend/components/development/PartFormDialog.tsx +++ b/frontend/components/development/PartFormDialog.tsx @@ -1,91 +1,82 @@ "use client"; -// 개발관리 > PART 등록/수정 통합 다이얼로그. -// wace partMngFormPopUp.jsp + partMngDetailPopUp.jsp 1:1 (mode 분기). +// 개발관리 > PART 등록/수정 다이얼로그 — wace partMng/partMngFormPopUp.jsp 1:1 // -// 신규: POST /api/development/part (38 컬럼) -// 수정: PUT /api/development/part/:objid (21 컬럼만 — wace updatePartDetail 1:1) +// 폼 필드 (운영판 그대로, 그 외 추가 없음): +// ① 품번 | 품명 +// ② 재료 | 열처리경도 +// ③ 열처리방법 | 표면처리 +// ④ 메이커 | 범주이름* (PART_TYPE, comm_code 0000062) +// ⑤ 규격 (1행) +// ⑥ 계정구분* (ACCTFG, comm_code 0900213) | 조달구분* (ODRFG, 0/1/8 하드) +// ⑦ 재고단위* (UNIT_DC, comm_code 0001399) | 관리단위* (UNITMANG_DC, 동일) +// ⑧ 환산수량* (UNITCHNG_NB, 숫자) | LOT구분* (0=미사용, 1=사용) +// ⑨ 사용여부* (0=미사용, 1=사용) | 검사여부* (0=무검사, 1=검사) +// ⑩ SET품여부* (0=부, 1=여) | 의뢰여부* (0=부, 1=여) +// ⑪ 개당길이 | 개당소요량 +// ⑫ 비고 (1행) +// ⑬ CAD Data: 3D / 2D(Drawing) / 2D(PDF) Drag&Drop — 별 PR(DEV-7) 도면업로드. 본 PR은 UI placeholder // -// 그룹: -// ① 기본정보 (필수 ★: part_no, part_name, part_type) -// ② 크기/형상 -// ③ 분류/단위 (comm_code SmartSelect) -// ④ Y/N 플래그 (radio '1'/'0') +// 신규: POST /api/development/part (운영 폼 22컬럼) +// 수정: PUT /api/development/part/:objid (wace updatePartDetail 21컬럼 1:1) -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Loader2, Save } from "lucide-react"; +import { Loader2, Save, Upload } from "lucide-react"; import { toast } from "sonner"; import { CommCodeSelect } from "@/components/common/CommCodeSelect"; import { devPartApi, PartCreateBody, PartUpdateBody, PartRow } from "@/lib/api/devPart"; +import { cn } from "@/lib/utils"; -// comm_code 그룹 ID (vexplor_rps DB 실재 그룹) -const GROUP_PART_TYPE = "0000062"; // PART TYPE (조립품/부품/구매품) -const GROUP_UNIT = "0001399"; // 단위 (m/Set/EA/식/BAG/kg/...) -const GROUP_ACCTFG = "0900213"; // 파트_계정구분 (원자재/제품/...) +// comm_code group ids (vexplor_rps DB) +const GROUP_PART_TYPE = "0000062"; +const GROUP_UNIT_DC = "0001399"; +const GROUP_ACCTFG = "0900213"; -// ODRFG: spec '0=구매/1=생산/8=Phantom' — 하드코딩 -const ODRFG_OPTIONS = [ - { code: "0", label: "구매" }, - { code: "1", label: "생산" }, - { code: "8", label: "Phantom" }, -]; +// 운영 1:1 하드코딩 옵션 +const OPT_ODRFG = [{ v: "0", t: "구매" }, { v: "1", t: "생산" }, { v: "8", t: "Phantom" }]; +const OPT_LOT_FG = [{ v: "0", t: "미사용" }, { v: "1", t: "사용" }]; +const OPT_USE_YN = [{ v: "0", t: "미사용" }, { v: "1", t: "사용" }]; +const OPT_QC_FG = [{ v: "0", t: "무검사" }, { v: "1", t: "검사" }]; +const OPT_YESNO = [{ v: "0", t: "부" }, { v: "1", t: "여" }]; interface FormState { - // 기본 part_no: string; part_name: string; - part_type: string; - unit: string; - qty: string; - spec: string; material: string; - remark: string; + heat_treatment_hardness: string; + heat_treatment_method: string; + surface_treatment: string; maker: string; - // 크기/형상 - thickness: string; - width: string; - height: string; - out_diameter: string; - in_diameter: string; - length: string; - // 분류/단위 + part_type: string; + spec: string; acctfg: string; odrfg: string; unit_dc: string; unitmang_dc: string; unitchng_nb: string; - unit_length: string; - unit_qty: string; - // 열처리/표면처리/후가공 - heat_treatment_hardness: string; - heat_treatment_method: string; - surface_treatment: string; - post_processing: string; - // 부속 (신규 시만 표시) - product_mgmt_objid: string; - supply_code: string; - contract_objid: string; - // Y/N lot_fg: string; use_yn: string; qc_fg: string; setitem_fg: string; req_fg: string; + unit_length: string; + unit_qty: string; + remark: string; } const EMPTY_FORM: FormState = { - part_no: "", part_name: "", part_type: "", unit: "", qty: "", spec: "", material: "", remark: "", maker: "", - thickness: "", width: "", height: "", out_diameter: "", in_diameter: "", length: "", - acctfg: "", odrfg: "", unit_dc: "", unitmang_dc: "", unitchng_nb: "", unit_length: "", unit_qty: "", - heat_treatment_hardness: "", heat_treatment_method: "", surface_treatment: "", post_processing: "", - product_mgmt_objid: "", supply_code: "", contract_objid: "", - lot_fg: "1", use_yn: "1", qc_fg: "0", setitem_fg: "0", req_fg: "0", + part_no: "", part_name: "", + material: "", heat_treatment_hardness: "", heat_treatment_method: "", surface_treatment: "", + maker: "", part_type: "", spec: "", + acctfg: "", odrfg: "", unit_dc: "", unitmang_dc: "", unitchng_nb: "", + lot_fg: "", use_yn: "", qc_fg: "", setitem_fg: "", req_fg: "", + unit_length: "", unit_qty: "", remark: "", }; interface Props { @@ -108,14 +99,10 @@ export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }: [] ); - // 초기화/로드 useEffect(() => { if (!open) return; - if (isEdit && editObjid) { - loadDetail(editObjid); - } else { - setForm(EMPTY_FORM); - } + if (isEdit && editObjid) loadDetail(editObjid); + else setForm(EMPTY_FORM); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); @@ -137,10 +124,27 @@ export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }: } }; + // wace fn_save 1:1 — 모든 required 검증 + const validate = (): string | null => { + if (!form.part_no.trim()) return "품번은 필수입니다."; + if (!form.part_name.trim()) return "품명은 필수입니다."; + if (!form.part_type) return "범주이름은 필수입니다."; + if (!form.acctfg) return "계정구분은 필수입니다."; + if (!form.odrfg) return "조달구분은 필수입니다."; + if (!form.unit_dc) return "재고단위는 필수입니다."; + if (!form.unitmang_dc) return "관리단위는 필수입니다."; + if (!form.unitchng_nb) return "환산수량은 필수입니다."; + if (!form.lot_fg) return "LOT구분은 필수입니다."; + if (!form.use_yn) return "사용여부는 필수입니다."; + if (!form.qc_fg) return "검사여부는 필수입니다."; + if (!form.setitem_fg) return "SET품여부는 필수입니다."; + if (!form.req_fg) return "의뢰여부는 필수입니다."; + return null; + }; + const handleSave = async () => { - if (!form.part_no.trim()) return toast.error("품번은 필수입니다."); - if (!form.part_name.trim()) return toast.error("품명은 필수입니다."); - if (!isEdit && !form.part_type.trim()) return toast.error("범주(PART TYPE)는 필수입니다."); + const err = validate(); + if (err) return toast.error(err); setSaving(true); try { @@ -175,25 +179,12 @@ export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }: part_no: form.part_no, part_name: form.part_name, part_type: form.part_type, - unit: form.unit, - qty: form.qty, - spec: form.spec, material: form.material, - thickness: form.thickness, - width: form.width, - height: form.height, - out_diameter: form.out_diameter, - in_diameter: form.in_diameter, - length: form.length, - remark: form.remark, - product_mgmt_objid: form.product_mgmt_objid, - supply_code: form.supply_code, - maker: form.maker, - contract_objid: form.contract_objid, - post_processing: form.post_processing, heat_treatment_hardness: form.heat_treatment_hardness, heat_treatment_method: form.heat_treatment_method, surface_treatment: form.surface_treatment, + maker: form.maker, + spec: form.spec, acctfg: form.acctfg, odrfg: form.odrfg, unit_dc: form.unit_dc, @@ -206,6 +197,7 @@ export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }: req_fg: form.req_fg, unit_length: form.unit_length, unit_qty: form.unit_qty, + remark: form.remark, }; await devPartApi.create(body); toast.success("PART가 등록되었습니다."); @@ -219,13 +211,13 @@ export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }: } }; - const titleText = isEdit ? "PART 수정" : "PART 신규 등록"; + const titleText = isEdit ? "품목 수정" : "품목 등록"; return ( - - - {titleText} + + + {titleText} {loading ? ( @@ -233,179 +225,182 @@ export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }: ) : ( -
- {/* ① 기본정보 */} -
- - - setField("part_no", e.target.value)} /> - - - setField("part_name", e.target.value)} /> - - - setField("part_type", v)} /> - - - - - setField("unit", v)} /> - - - setField("qty", e.target.value)} /> - - - setField("spec", e.target.value)} /> - - - - - setField("material", e.target.value)} /> - - - setField("maker", e.target.value)} /> - - - setField("remark", e.target.value)} /> - - -
+
+ + + + + + + + + {/* ① */} + + + + + + - {/* ② 크기/형상 */} -
- - setField("thickness", e.target.value)} /> - setField("width", e.target.value)} /> - setField("height", e.target.value)} /> - - - setField("out_diameter", e.target.value)} /> - setField("in_diameter", e.target.value)} /> - setField("length", e.target.value)} /> - -
+ {/* ② */} + + + + + + - {/* ③ 분류 / 단위 */} -
- - - setField("acctfg", v)} /> - - - - - - setField("unitchng_nb", e.target.value)} /> - - - - - setField("unit_dc", v)} /> - - - setField("unitmang_dc", v)} /> - - -
- setField("unit_length", e.target.value)} /> - / - setField("unit_qty", e.target.value)} /> -
-
-
- - - setField("heat_treatment_hardness", e.target.value)} /> - - - setField("heat_treatment_method", e.target.value)} /> - - - setField("surface_treatment", e.target.value)} /> - - - - - setField("post_processing", e.target.value)} /> - - {!isEdit && ( - - setField("supply_code", e.target.value)} - placeholder="admin_supply_mng.objid" /> - - )} - {!isEdit && ( - - setField("product_mgmt_objid", e.target.value)} - placeholder="product_mgmt.objid" /> - - )} - -
+ {/* ③ */} + + + + + + - {/* ④ Y/N 플래그 */} -
- - setField("lot_fg", v)} /> - setField("use_yn", v)} /> - setField("qc_fg", v)} /> - - - setField("setitem_fg", v)} /> - setField("req_fg", v)} /> - - -
+ {/* ④ */} + + + + + + + + {/* ⑤ 규격 (colspan=3) */} + + + + + + {/* ⑥ */} + + + + + + + + {/* ⑦ */} + + + + + + + + {/* ⑧ */} + + + + + + + + {/* ⑨ */} + + + + + + + + {/* ⑩ */} + + + + + + + + {/* ⑪ */} + + + + + + + + {/* ⑫ 비고 (colspan=3) */} + + + + + + {/* ⑬ CAD Data — placeholder (DEV-7 도면업로드 별 PR) */} + + + + + + + + + + + + + + +
품번 setField("part_no", e.target.value)} />품명 setField("part_name", e.target.value)} />
재료 setField("material", e.target.value)} />열처리경도 setField("heat_treatment_hardness", e.target.value)} />
열처리방법 setField("heat_treatment_method", e.target.value)} />표면처리 setField("surface_treatment", e.target.value)} />
메이커 setField("maker", e.target.value)} />범주이름 + setField("part_type", v)} /> +
규격 setField("spec", e.target.value)} />
계정구분 + setField("acctfg", v)} /> + 조달구분 setField("odrfg", v)} />
재고단위 + setField("unit_dc", v)} /> + 관리단위 + setField("unitmang_dc", v)} /> +
환산수량 setField("unitchng_nb", e.target.value.replace(/[^0-9.]/g, ""))} />LOT구분 setField("lot_fg", v)} />
사용여부 setField("use_yn", v)} />검사여부 setField("qc_fg", v)} />
SET품여부 setField("setitem_fg", v)} />의뢰여부 setField("req_fg", v)} />
개당길이 setField("unit_length", e.target.value.replace(/[^0-9.]/g, ""))} />개당소요량 setField("unit_qty", e.target.value.replace(/[^0-9.]/g, ""))} />
비고 setField("remark", e.target.value)} />
+ CAD Data + 3D + +
2D(Drawing) + +
2D(PDF) + +
+ +
+ CAD Data 첨부는 DEV-7 (도면업로드) 별 PR 에서 활성화됩니다. +
)} - - + + @@ -415,85 +410,74 @@ export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }: // ─── 보조 컴포넌트 ────────────────────────────────────────── -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-
{title}
-
{children}
-
- ); +function Tr({ children }: { children: React.ReactNode }) { + return {children}; } - -function Row({ children }: { children: React.ReactNode }) { - return
{children}
; -} - -function Field({ label, required, children }: { label: string; required?: boolean; children?: React.ReactNode }) { +function Th({ children }: { children: React.ReactNode }) { return ( -
- {label && ( - - )} + {children} -
+ ); } - -function YNRadio({ value, onChange }: { value: string; onChange: (v: string) => void }) { +function Td({ children, colSpan }: { children: React.ReactNode; colSpan?: number }) { + return {children}; +} +function Req() { + return *; +} +function BasicSelect({ + value, options, onChange, +}: { + value: string; + options: { v: string; t: string }[]; + onChange: (v: string) => void; +}) { return ( -
- - + + ); +} +function DropPlaceholder({ label }: { label: string }) { + return ( +
+ + Drag & Drop Files Here ({label})
); } -// ─── PartRow → FormState 매핑 ────────────────────────────── +// ─── PartRow → FormState ──────────────────────────────────── function rowToForm(r: PartRow): FormState { return { part_no: r.part_no ?? "", part_name: r.part_name ?? "", - part_type: r.part_type ?? "", - unit: r.unit ?? "", - qty: r.qty ?? "", - spec: r.spec ?? "", material: r.material ?? "", - remark: r.remark ?? "", + heat_treatment_hardness: r.heat_treatment_hardness ?? "", + heat_treatment_method: r.heat_treatment_method ?? "", + surface_treatment: r.surface_treatment ?? "", maker: r.maker ?? "", - thickness: r.thickness ?? "", - width: r.width ?? "", - height: r.height ?? "", - out_diameter: r.out_diameter ?? "", - in_diameter: r.in_diameter ?? "", - length: r.length ?? "", + part_type: r.part_type ?? "", + spec: r.spec ?? "", acctfg: r.acctfg ?? "", odrfg: r.odrfg ?? "", unit_dc: r.unit_dc ?? "", unitmang_dc: r.unitmang_dc ?? "", unitchng_nb: r.unitchng_nb != null ? String(r.unitchng_nb) : "", + lot_fg: r.lot_fg ?? "", + use_yn: r.use_yn ?? "", + qc_fg: r.qc_fg ?? "", + setitem_fg: r.setitem_fg ?? "", + req_fg: r.req_fg ?? "", unit_length: r.unit_length ?? "", unit_qty: r.unit_qty ?? "", - heat_treatment_hardness: r.heat_treatment_hardness ?? "", - heat_treatment_method: r.heat_treatment_method ?? "", - surface_treatment: r.surface_treatment ?? "", - post_processing: r.post_processing ?? "", - product_mgmt_objid: r.product_mgmt_objid ?? "", - supply_code: r.supply_code ?? "", - contract_objid: r.contract_objid ?? "", - lot_fg: r.lot_fg ?? "1", - use_yn: r.use_yn ?? "1", - qc_fg: r.qc_fg ?? "0", - setitem_fg: r.setitem_fg ?? "0", - req_fg: r.req_fg ?? "0", + remark: r.remark ?? "", }; }