개발관리>PART 등록 폼 — wace partMngFormPopUp.jsp 1:1 재작성

사용자 지적으로 재발견: 기존 PartFormDialog 는 임의로 "기본정보/크기·형상/분류·단위/Y-N
플래그" 섹션 분리 + 운영판에 없는 필드(두께/너비/높이/외경/내경/길이/수량/단위/후가공/
공급업체코드/제품OBJID) 추가 + Y/N 라디오로 작성되어 있었음. 운영판과 완전 다른 화면.

운영판 1:1 정정:
- 단일 4-col table 레이아웃 (라벨 / input / 라벨 / input)
- 필드 22개만 (운영판 그대로):
  ① 품번 | 품명
  ② 재료 | 열처리경도
  ③ 열처리방법 | 표면처리
  ④ 메이커 | 범주이름* (PART_TYPE, comm_code 0000062)
  ⑤ 규격 (colspan=3)
  ⑥ 계정구분*(comm_code 0900213) | 조달구분*(0=구매/1=생산/8=Phantom 하드)
  ⑦ 재고단위*(comm_code 0001399) | 관리단위*(동일)
  ⑧ 환산수량*(숫자) | LOT구분*(0=미사용/1=사용)
  ⑨ 사용여부*(0=미사용/1=사용) | 검사여부*(0=무검사/1=검사)
  ⑩ SET품여부*(0=부/1=여) | 의뢰여부*(0=부/1=여)
  ⑪ 개당길이 | 개당소요량
  ⑫ 비고 (colspan=3)
  ⑬ CAD Data : 3D / 2D(Drawing) / 2D(PDF) Drag&Drop placeholder
     (실제 업로드 기능은 DEV-7 별 PR — UI 영역만 추가)

- LOT/USE_YN/QC_FG/SETITEM_FG/REQ_FG : Y/N 라디오 → 운영판 select 옵션 그대로 (4개씩 한글값)
- 필수 검증 11개 추가 (wace fn_save 1:1) : 범주이름·계정구분·조달구분·재고단위·관리단위·
  환산수량·LOT구분·사용여부·검사여부·SET품여부·의뢰여부
- DialogTitle "품목 등록" / "품목 수정" (운영판 헤더 그대로) + 파란색 헤더 바
- 저장 / 닫기 버튼 중앙 배치 (운영판 plm_btn_wrap_center)

PartDetailDialog 도 동일하게 운영판 부적합 — 다음 단계에서 별도 재작성 예정.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-12 18:18:39 +09:00
parent 7d73d2ee57
commit 82a253ec6a
+286 -302
View File
@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl">
<DialogHeader>
<DialogTitle>{titleText}</DialogTitle>
<DialogContent className="max-w-[1100px] w-[95vw] max-h-[92vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="bg-blue-600 px-4 py-3">
<DialogTitle className="text-white">{titleText}</DialogTitle>
</DialogHeader>
{loading ? (
@@ -233,179 +225,182 @@ export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }:
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="max-h-[70vh] space-y-4 overflow-y-auto px-1 py-2">
{/* ① 기본정보 */}
<Section title="기본정보">
<Row>
<Field label="품번" required>
<Input value={form.part_no} disabled={isEdit}
onChange={(e) => setField("part_no", e.target.value)} />
</Field>
<Field label="품명" required>
<Input value={form.part_name}
onChange={(e) => setField("part_name", e.target.value)} />
</Field>
<Field label="범주(PART TYPE)" required>
<CommCodeSelect groupId={GROUP_PART_TYPE} withAll={false}
value={form.part_type}
onValueChange={(v) => setField("part_type", v)} />
</Field>
</Row>
<Row>
<Field label="단위">
<CommCodeSelect groupId={GROUP_UNIT} withAll={false}
value={form.unit}
onValueChange={(v) => setField("unit", v)} />
</Field>
<Field label="수량">
<Input value={form.qty} className="text-right"
onChange={(e) => setField("qty", e.target.value)} />
</Field>
<Field label="규격">
<Input value={form.spec}
onChange={(e) => setField("spec", e.target.value)} />
</Field>
</Row>
<Row>
<Field label="재료">
<Input value={form.material}
onChange={(e) => setField("material", e.target.value)} />
</Field>
<Field label="메이커">
<Input value={form.maker}
onChange={(e) => setField("maker", e.target.value)} />
</Field>
<Field label="비고">
<Input value={form.remark}
onChange={(e) => setField("remark", e.target.value)} />
</Field>
</Row>
</Section>
<div className="flex-1 overflow-y-auto px-4 py-3">
<table className="w-full border-collapse text-sm">
<colgroup>
<col className="w-[110px]" />
<col />
<col className="w-[110px]" />
<col />
</colgroup>
<tbody>
{/* ① */}
<Tr>
<Th></Th>
<Td><Input value={form.part_no} disabled={isEdit}
onChange={(e) => setField("part_no", e.target.value)} /></Td>
<Th></Th>
<Td><Input value={form.part_name}
onChange={(e) => setField("part_name", e.target.value)} /></Td>
</Tr>
{/* ② 크기/형상 */}
<Section title="크기 / 형상">
<Row>
<Field label="두께"><Input value={form.thickness} className="text-right"
onChange={(e) => setField("thickness", e.target.value)} /></Field>
<Field label="너비(W)"><Input value={form.width} className="text-right"
onChange={(e) => setField("width", e.target.value)} /></Field>
<Field label="높이(H)"><Input value={form.height} className="text-right"
onChange={(e) => setField("height", e.target.value)} /></Field>
</Row>
<Row>
<Field label="외경"><Input value={form.out_diameter} className="text-right"
onChange={(e) => setField("out_diameter", e.target.value)} /></Field>
<Field label="내경"><Input value={form.in_diameter} className="text-right"
onChange={(e) => setField("in_diameter", e.target.value)} /></Field>
<Field label="길이(L)"><Input value={form.length} className="text-right"
onChange={(e) => setField("length", e.target.value)} /></Field>
</Row>
</Section>
{/* ② */}
<Tr>
<Th></Th>
<Td><Input value={form.material}
onChange={(e) => setField("material", e.target.value)} /></Td>
<Th></Th>
<Td><Input value={form.heat_treatment_hardness}
onChange={(e) => setField("heat_treatment_hardness", e.target.value)} /></Td>
</Tr>
{/* ③ 분류 / 단위 */}
<Section title="분류 / 단위">
<Row>
<Field label="계정구분">
<CommCodeSelect groupId={GROUP_ACCTFG} withAll={false}
value={form.acctfg}
onValueChange={(v) => setField("acctfg", v)} />
</Field>
<Field label="조달구분">
<select className="h-9 w-full rounded-md border bg-background px-2 text-sm"
value={form.odrfg}
onChange={(e) => setField("odrfg", e.target.value)}>
<option value=""></option>
{ODRFG_OPTIONS.map((o) =>
<option key={o.code} value={o.code}>{o.label}</option>)}
</select>
</Field>
<Field label="환산수량(UNITCHNG_NB)">
<Input value={form.unitchng_nb} className="text-right"
onChange={(e) => setField("unitchng_nb", e.target.value)} />
</Field>
</Row>
<Row>
<Field label="재고단위">
<CommCodeSelect groupId={GROUP_UNIT} withAll={false}
value={form.unit_dc}
onValueChange={(v) => setField("unit_dc", v)} />
</Field>
<Field label="관리단위">
<CommCodeSelect groupId={GROUP_UNIT} withAll={false}
value={form.unitmang_dc}
onValueChange={(v) => setField("unitmang_dc", v)} />
</Field>
<Field label="개당길이 / 개당수량">
<div className="flex items-center gap-1">
<Input value={form.unit_length} className="text-right"
placeholder="개당길이"
onChange={(e) => setField("unit_length", e.target.value)} />
<span className="text-xs text-muted-foreground">/</span>
<Input value={form.unit_qty} className="text-right"
placeholder="개당수량"
onChange={(e) => setField("unit_qty", e.target.value)} />
</div>
</Field>
</Row>
<Row>
<Field label="열처리경도">
<Input value={form.heat_treatment_hardness}
onChange={(e) => setField("heat_treatment_hardness", e.target.value)} />
</Field>
<Field label="열처리방법">
<Input value={form.heat_treatment_method}
onChange={(e) => setField("heat_treatment_method", e.target.value)} />
</Field>
<Field label="표면처리">
<Input value={form.surface_treatment}
onChange={(e) => setField("surface_treatment", e.target.value)} />
</Field>
</Row>
<Row>
<Field label="후가공">
<Input value={form.post_processing}
onChange={(e) => setField("post_processing", e.target.value)} />
</Field>
{!isEdit && (
<Field label="공급업체 코드">
<Input value={form.supply_code}
onChange={(e) => setField("supply_code", e.target.value)}
placeholder="admin_supply_mng.objid" />
</Field>
)}
{!isEdit && (
<Field label="제품 OBJID">
<Input value={form.product_mgmt_objid}
onChange={(e) => setField("product_mgmt_objid", e.target.value)}
placeholder="product_mgmt.objid" />
</Field>
)}
</Row>
</Section>
{/* ③ */}
<Tr>
<Th></Th>
<Td><Input value={form.heat_treatment_method}
onChange={(e) => setField("heat_treatment_method", e.target.value)} /></Td>
<Th></Th>
<Td><Input value={form.surface_treatment}
onChange={(e) => setField("surface_treatment", e.target.value)} /></Td>
</Tr>
{/* ④ Y/N 플래그 */}
<Section title="Y/N 플래그">
<Row>
<Field label="LOT구분"><YNRadio value={form.lot_fg} onChange={(v) => setField("lot_fg", v)} /></Field>
<Field label="사용여부"><YNRadio value={form.use_yn} onChange={(v) => setField("use_yn", v)} /></Field>
<Field label="검사여부"><YNRadio value={form.qc_fg} onChange={(v) => setField("qc_fg", v)} /></Field>
</Row>
<Row>
<Field label="SET품여부"><YNRadio value={form.setitem_fg} onChange={(v) => setField("setitem_fg", v)} /></Field>
<Field label="의뢰여부"><YNRadio value={form.req_fg} onChange={(v) => setField("req_fg", v)} /></Field>
<Field label="" />
</Row>
</Section>
{/* ④ */}
<Tr>
<Th></Th>
<Td><Input value={form.maker}
onChange={(e) => setField("maker", e.target.value)} /></Td>
<Th></Th>
<Td>
<CommCodeSelect groupId={GROUP_PART_TYPE} withAll={false}
value={form.part_type}
onValueChange={(v) => setField("part_type", v)} />
</Td>
</Tr>
{/* ⑤ 규격 (colspan=3) */}
<Tr>
<Th></Th>
<Td colSpan={3}><Input value={form.spec} maxLength={100}
onChange={(e) => setField("spec", e.target.value)} /></Td>
</Tr>
{/* ⑥ */}
<Tr>
<Th><Req /></Th>
<Td>
<CommCodeSelect groupId={GROUP_ACCTFG} withAll={false}
value={form.acctfg}
onValueChange={(v) => setField("acctfg", v)} />
</Td>
<Th><Req /></Th>
<Td><BasicSelect value={form.odrfg} options={OPT_ODRFG}
onChange={(v) => setField("odrfg", v)} /></Td>
</Tr>
{/* ⑦ */}
<Tr>
<Th><Req /></Th>
<Td>
<CommCodeSelect groupId={GROUP_UNIT_DC} withAll={false}
value={form.unit_dc}
onValueChange={(v) => setField("unit_dc", v)} />
</Td>
<Th><Req /></Th>
<Td>
<CommCodeSelect groupId={GROUP_UNIT_DC} withAll={false}
value={form.unitmang_dc}
onValueChange={(v) => setField("unitmang_dc", v)} />
</Td>
</Tr>
{/* ⑧ */}
<Tr>
<Th><Req /></Th>
<Td><Input value={form.unitchng_nb} className="text-right"
inputMode="decimal"
onChange={(e) => setField("unitchng_nb", e.target.value.replace(/[^0-9.]/g, ""))} /></Td>
<Th>LOT구분<Req /></Th>
<Td><BasicSelect value={form.lot_fg} options={OPT_LOT_FG}
onChange={(v) => setField("lot_fg", v)} /></Td>
</Tr>
{/* ⑨ */}
<Tr>
<Th><Req /></Th>
<Td><BasicSelect value={form.use_yn} options={OPT_USE_YN}
onChange={(v) => setField("use_yn", v)} /></Td>
<Th><Req /></Th>
<Td><BasicSelect value={form.qc_fg} options={OPT_QC_FG}
onChange={(v) => setField("qc_fg", v)} /></Td>
</Tr>
{/* ⑩ */}
<Tr>
<Th>SET품여부<Req /></Th>
<Td><BasicSelect value={form.setitem_fg} options={OPT_YESNO}
onChange={(v) => setField("setitem_fg", v)} /></Td>
<Th><Req /></Th>
<Td><BasicSelect value={form.req_fg} options={OPT_YESNO}
onChange={(v) => setField("req_fg", v)} /></Td>
</Tr>
{/* ⑪ */}
<Tr>
<Th></Th>
<Td><Input value={form.unit_length} className="text-right"
inputMode="decimal" maxLength={20}
onChange={(e) => setField("unit_length", e.target.value.replace(/[^0-9.]/g, ""))} /></Td>
<Th></Th>
<Td><Input value={form.unit_qty} className="text-right"
inputMode="decimal" maxLength={20}
onChange={(e) => setField("unit_qty", e.target.value.replace(/[^0-9.]/g, ""))} /></Td>
</Tr>
{/* ⑫ 비고 (colspan=3) */}
<Tr>
<Th></Th>
<Td colSpan={3}><Input value={form.remark}
onChange={(e) => setField("remark", e.target.value)} /></Td>
</Tr>
{/* ⑬ CAD Data — placeholder (DEV-7 도면업로드 별 PR) */}
<tr>
<th className="border bg-muted/30 px-3 py-2 text-left align-top font-medium" rowSpan={3}>
CAD Data
</th>
<th className="border bg-muted/30 px-3 py-2 text-left font-medium w-[110px]">3D</th>
<td className="border px-3 py-2" colSpan={2}>
<DropPlaceholder label="3D" />
</td>
</tr>
<tr>
<th className="border bg-muted/30 px-3 py-2 text-left font-medium">2D(Drawing)</th>
<td className="border px-3 py-2" colSpan={2}>
<DropPlaceholder label="2D(Drawing)" />
</td>
</tr>
<tr>
<th className="border bg-muted/30 px-3 py-2 text-left font-medium">2D(PDF)</th>
<td className="border px-3 py-2" colSpan={2}>
<DropPlaceholder label="2D(PDF)" />
</td>
</tr>
</tbody>
</table>
<div className="mt-2 text-[11px] text-muted-foreground">
CAD Data DEV-7 () PR .
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
</Button>
<DialogFooter className="border-t bg-muted/20 px-4 py-3 sm:justify-center">
<Button onClick={handleSave} disabled={saving || loading}>
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
<span className="ml-1">{isEdit ? "수정" : "등록"}</span>
<span className="ml-1"></span>
</Button>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
</Button>
</DialogFooter>
</DialogContent>
@@ -415,85 +410,74 @@ export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }:
// ─── 보조 컴포넌트 ──────────────────────────────────────────
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="rounded-md border bg-card p-3">
<div className="mb-2 text-sm font-semibold text-foreground">{title}</div>
<div className="space-y-2">{children}</div>
</div>
);
function Tr({ children }: { children: React.ReactNode }) {
return <tr>{children}</tr>;
}
function Row({ children }: { children: React.ReactNode }) {
return <div className="grid grid-cols-3 gap-3">{children}</div>;
}
function Field({ label, required, children }: { label: string; required?: boolean; children?: React.ReactNode }) {
function Th({ children }: { children: React.ReactNode }) {
return (
<div>
{label && (
<Label className="mb-1 block text-xs text-muted-foreground">
{label}
{required && <span className="ml-1 text-red-500">*</span>}
</Label>
)}
<th className="border bg-muted/30 px-3 py-2 text-left align-middle font-medium w-[110px]">
{children}
</div>
</th>
);
}
function YNRadio({ value, onChange }: { value: string; onChange: (v: string) => void }) {
function Td({ children, colSpan }: { children: React.ReactNode; colSpan?: number }) {
return <td className="border px-3 py-1.5" colSpan={colSpan}>{children}</td>;
}
function Req() {
return <span className="ml-1 text-red-500">*</span>;
}
function BasicSelect({
value, options, onChange,
}: {
value: string;
options: { v: string; t: string }[];
onChange: (v: string) => void;
}) {
return (
<div className="flex h-9 items-center gap-3 rounded-md border bg-background px-3 text-sm">
<label className="flex items-center gap-1">
<input type="radio" checked={value === "1"} onChange={() => onChange("1")} />
<span></span>
</label>
<label className="flex items-center gap-1">
<input type="radio" checked={value === "0"} onChange={() => onChange("0")} />
<span></span>
</label>
<select
className={cn("h-9 w-full rounded-md border bg-background px-2 text-sm")}
value={value}
onChange={(e) => onChange(e.target.value)}
>
<option value=""></option>
{options.map((o) => <option key={o.v} value={o.v}>{o.t}</option>)}
</select>
);
}
function DropPlaceholder({ label }: { label: string }) {
return (
<div className="border-2 border-dashed border-muted-foreground/30 rounded text-muted-foreground py-6 text-center text-xs">
<Upload className="h-5 w-5 mx-auto mb-1" />
Drag &amp; Drop Files Here ({label})
</div>
);
}
// ─── 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 ?? "",
};
}