Files
wace_rps/frontend/components/development/PartDetailDialog.tsx
T
hjjeong 119f0f3f2e 개발관리>PART 도면 다중 업로드 (DEV-7) — 공통 AttachFileDropZone 신설 + CAD Data 활성
- 공통 컴포넌트: frontend/components/common/AttachFileDropZone.tsx
  · wace fnc_setFileDropZone + fn_fileCallback2 + fileDelete 1:1
  · /api/files (upload·list·delete·download) attach_file_info 기반
  · readOnly 옵션 (Detail 다이얼로그용), accept 옵션, dragenter+dropEffect=copy
  · 도메인 무관 — ERP/ECR/생산실적 등 어디서나 재사용

- 프론트 채번 유틸: frontend/lib/utils/objidUtil.ts
  · backend objidUtil 1:1 (UUID v4 → Java String.hashCode int32)
  · 신규 등록 시 다이얼로그 진입 시점에 part_mng.objid 선채번
    (wace partMngFormPopUp resultMap.OBJID 패턴)

- PartFormDialog (M1 신규/수정): CAD Data placeholder 제거,
  AttachFileDropZone 3종(3D_CAD / 2D_DRAWING_CAD / 2D_PDF_CAD) 활성.
  신규 모드는 createObjId 로 선채번 후 part_objid 로 백엔드 전달.

- PartDetailDialog: CadCount 제거, AttachFileDropZone readOnly 로 교체
  (목록·다운로드만, 드롭존/삭제 숨김).

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

223 lines
9.6 KiB
TypeScript

"use client";
// 개발관리 > PART 상세 다이얼로그 — wace partMng/partMngDetailPopUp.jsp 1:1
//
// 운영판은 form 과 동일 화면을 disabled 로 표시 후 "수정" 클릭 시 활성화.
// RPS 에서는 PartFormDialog 와 분리 유지 (호환). 본 다이얼로그는 Form 레이아웃 readonly +
// 부속 정보 행 추가 (EO_NO / EO_DATE / EO구분(CHANGE_TYPE) / EO사유(CHANGE_OPTION)) +
// CAD Data 영역 (3D / 2D(Drawing) / 2D(PDF)) — AttachFileDropZone readonly (목록·다운로드).
//
// "수정" 버튼: 부모가 본 다이얼로그를 닫고 PartFormDialog(mode='edit') 오픈하도록 onEdit 콜백 호출.
import React, { useEffect, useState } from "react";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Loader2, Pencil } from "lucide-react";
import { toast } from "sonner";
import { devPartApi, PartRow } from "@/lib/api/devPart";
import { cn } from "@/lib/utils";
import { AttachFileDropZone } from "@/components/common/AttachFileDropZone";
const LABEL_ODRFG: Record<string, string> = { "0": "구매", "1": "생산", "8": "Phantom" };
const LABEL_LOT_FG: Record<string, string> = { "0": "미사용", "1": "사용" };
const LABEL_USE_YN: Record<string, string> = { "0": "미사용", "1": "사용" };
const LABEL_QC_FG: Record<string, string> = { "0": "무검사", "1": "검사" };
const LABEL_YESNO: Record<string, string> = { "0": "부", "1": "여" };
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
objid: string | null;
/** "수정" 버튼 클릭 시 호출 — 부모는 본 다이얼로그 닫고 PartFormDialog(mode='edit') 오픈 */
onEdit?: (objid: string) => void;
}
export function PartDetailDialog({ open, onOpenChange, objid, onEdit }: Props) {
const [row, setRow] = useState<PartRow | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!open || !objid) return;
let alive = true;
setLoading(true);
devPartApi.detail(objid)
.then((data) => { if (alive) setRow(data); })
.catch((e: any) => {
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
onOpenChange(false);
})
.finally(() => { if (alive) setLoading(false); });
return () => { alive = false; };
}, [open, objid, onOpenChange]);
if (!open) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<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"> </DialogTitle>
</DialogHeader>
{loading || !row ? (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="flex-1 overflow-y-auto px-4 py-3">
{/* 운영판 colgroup 1:1 (12% / 12% / 25% / 12% / *) */}
<table className="w-full border-collapse text-sm table-fixed">
<colgroup>
<col style={{ width: "12%" }} />
<col style={{ width: "12%" }} />
<col style={{ width: "25%" }} />
<col style={{ width: "12%" }} />
<col />
</colgroup>
<tbody>
<Tr>
<Th></Th><Td colSpan={2}><Ro>{row.part_no}</Ro></Td>
<Th></Th><Td><Ro>{row.part_name}</Ro></Td>
</Tr>
<Tr>
<Th></Th><Td colSpan={2}><Ro>{row.material}</Ro></Td>
<Th></Th><Td><Ro>{row.heat_treatment_hardness}</Ro></Td>
</Tr>
<Tr>
<Th></Th><Td colSpan={2}><Ro>{row.heat_treatment_method}</Ro></Td>
<Th></Th><Td><Ro>{row.surface_treatment}</Ro></Td>
</Tr>
<Tr>
<Th></Th><Td colSpan={2}><Ro>{row.maker}</Ro></Td>
<Th> </Th><Td><Ro>{row.part_type_title}</Ro></Td>
</Tr>
<Tr>
<Th></Th><Td colSpan={4}><Ro>{row.spec}</Ro></Td>
</Tr>
<Tr>
<Th></Th><Td colSpan={2}><Ro>{row.acctfg_nm}</Ro></Td>
<Th></Th><Td><Ro>{LABEL_ODRFG[row.odrfg ?? ""] ?? row.odrfg_nm ?? ""}</Ro></Td>
</Tr>
<Tr>
<Th></Th><Td colSpan={2}><Ro>{row.unit_dc_nm}</Ro></Td>
<Th></Th><Td><Ro>{row.unitmang_dc_nm}</Ro></Td>
</Tr>
<Tr>
<Th></Th>
<Td colSpan={2}><Ro align="right">{row.unitchng_nb != null && row.unitchng_nb !== "" ? String(row.unitchng_nb) : ""}</Ro></Td>
<Th>LOT구분</Th><Td><Ro>{LABEL_LOT_FG[row.lot_fg ?? ""] ?? row.lot_fg_nm ?? ""}</Ro></Td>
</Tr>
<Tr>
<Th></Th><Td colSpan={2}><Ro>{LABEL_USE_YN[row.use_yn ?? ""] ?? row.use_yn_nm ?? ""}</Ro></Td>
<Th></Th><Td><Ro>{LABEL_QC_FG[row.qc_fg ?? ""] ?? row.qc_fg_nm ?? ""}</Ro></Td>
</Tr>
<Tr>
<Th>SET품여부</Th><Td colSpan={2}><Ro>{LABEL_YESNO[row.setitem_fg ?? ""] ?? row.setitem_fg_nm ?? ""}</Ro></Td>
<Th></Th><Td><Ro>{LABEL_YESNO[row.req_fg ?? ""] ?? row.req_fg_nm ?? ""}</Ro></Td>
</Tr>
<Tr>
<Th></Th><Td colSpan={2}><Ro align="right">{row.unit_length}</Ro></Td>
<Th></Th><Td><Ro align="right">{row.unit_qty}</Ro></Td>
</Tr>
<Tr>
<Th></Th><Td colSpan={4}><Ro>{row.remark}</Ro></Td>
</Tr>
{/* 부속 — wace detail 만 표시 */}
<Tr>
<Th>EO No</Th><Td colSpan={2}><Ro>{row.eo_no}</Ro></Td>
<Th>EO Date</Th><Td><Ro>{row.eo_date}</Ro></Td>
</Tr>
<Tr>
<Th>EO구분</Th><Td colSpan={2}><Ro>{row.change_type}</Ro></Td>
<Th>EO사유</Th><Td><Ro>{row.change_option_name ?? row.change_option}</Ro></Td>
</Tr>
{/* CAD Data — readonly (목록·다운로드만) */}
<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">3D</th>
<td className="border px-3 py-2" colSpan={3}>
<AttachFileDropZone
targetObjid={row.objid}
docType="3D_CAD"
docTypeName="3D CAD 첨부파일"
readOnly
/>
</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={3}>
<AttachFileDropZone
targetObjid={row.objid}
docType="2D_DRAWING_CAD"
docTypeName="2D(Drawing) CAD 첨부파일"
readOnly
/>
</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={3}>
<AttachFileDropZone
targetObjid={row.objid}
docType="2D_PDF_CAD"
docTypeName="2D(PDF) CAD 첨부파일"
readOnly
/>
</td>
</tr>
</tbody>
</table>
</div>
)}
<DialogFooter className="border-t bg-muted/20 px-4 py-3 sm:justify-center">
{row && onEdit && (
<Button onClick={() => onEdit(row.objid)}>
<Pencil className="h-4 w-4" /><span className="ml-1"></span>
</Button>
)}
<Button variant="outline" onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
// ─── 보조 컴포넌트 ──────────────────────────────────────────
function Tr({ children }: { children: React.ReactNode }) {
return <tr>{children}</tr>;
}
function Th({ children }: { children: React.ReactNode }) {
return (
<th className="border bg-muted/30 px-3 py-2 text-left align-middle font-medium">
{children}
</th>
);
}
function Td({ children, colSpan }: { children: React.ReactNode; colSpan?: number }) {
return <td className="border px-3 py-1.5" colSpan={colSpan}>{children}</td>;
}
// 운영판 disabled input 의 readonly 박스
function Ro({ children, align }: { children: React.ReactNode; align?: "left" | "center" | "right" }) {
const cls = align === "right" ? "text-right" : align === "center" ? "text-center" : "text-left";
const empty = children == null || children === "";
return (
<div className={cn(
"min-h-9 rounded border bg-muted/30 px-2 py-1.5",
cls,
)}>
{empty ? <span className="text-muted-foreground"></span> : children}
</div>
);
}