"use client"; // 개발관리 PART Excel Import 다이얼로그 // wace partMng/openPartExcelImportPopUp.jsp 1:1 // - Template Download / Drag & Drop / 파일선택 → 백엔드 파싱 + 검증 // - 검증 그리드 22컬럼 + NOTE (에러는 빨강) — wace expenseDetailGrid 1:1 // - NOTE 있는 행이 하나라도 있으면 저장 차단 (wace fn_save 1:1) import React, { useCallback, useMemo, useRef, useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Download, Upload, Save, Loader2, FileX } from "lucide-react"; import { toast } from "sonner"; import { devPartApi, PartExcelRow } from "@/lib/api/devPart"; import { cn } from "@/lib/utils"; const TEMPLATE_DOWNLOAD_URL = "/templates/PART_EXCEL_IMPORT_TEMPLATE.xlsx"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; onSaved: () => void; } interface Column { key: keyof PartExcelRow; label: string; width: string; align?: "left" | "center" | "right"; showNameFor?: keyof PartExcelRow; } const COLUMNS: Column[] = [ { key: "NOTE", label: "결과", width: "min-w-[200px]", align: "left" }, { key: "PART_NO", label: "품번", width: "min-w-[140px]", align: "center" }, { key: "PART_NAME", label: "품명", width: "min-w-[200px]", align: "left" }, { key: "MATERIAL", label: "재료", width: "min-w-[100px]" }, { key: "HEAT_TREATMENT_HARDNESS", label: "열처리경도", width: "min-w-[110px]" }, { key: "HEAT_TREATMENT_METHOD", label: "열처리방법", width: "min-w-[110px]" }, { key: "SURFACE_TREATMENT", label: "표면처리", width: "min-w-[100px]" }, { key: "MAKER", label: "메이커", width: "min-w-[110px]" }, { key: "PART_TYPE", label: "범주", width: "min-w-[90px]", align: "center", showNameFor: "PART_TYPE_NAME" }, { key: "SPEC", label: "규격", width: "min-w-[100px]" }, { key: "ACCTFG", label: "계정구분", width: "min-w-[90px]", align: "center", showNameFor: "ACCTFG_NAME" }, { key: "ODRFG", label: "조달구분", width: "min-w-[90px]", align: "center", showNameFor: "ODRFG_NAME" }, { key: "UNIT_DC", label: "재고단위", width: "min-w-[80px]", align: "center", showNameFor: "UNIT_DC_NAME" }, { key: "UNITMANG_DC", label: "관리단위", width: "min-w-[80px]", align: "center", showNameFor: "UNITMANG_DC_NAME" }, { key: "UNITCHNG_NB", label: "환산수량", width: "min-w-[80px]", align: "right" }, { key: "LOT_FG", label: "LOT구분", width: "min-w-[80px]", align: "center" }, { key: "USE_YN", label: "사용여부", width: "min-w-[80px]", align: "center" }, { key: "QC_FG", label: "검사여부", width: "min-w-[80px]", align: "center" }, { key: "SETITEM_FG", label: "SET품여부", width: "min-w-[90px]", align: "center" }, { key: "REQ_FG", label: "의뢰여부", width: "min-w-[80px]", align: "center" }, { key: "UNIT_LENGTH", label: "개당길이", width: "min-w-[80px]", align: "right" }, { key: "UNIT_QTY", label: "개당소요량", width: "min-w-[90px]", align: "right" }, { key: "REMARK", label: "비고", width: "min-w-[130px]", align: "left" }, ]; const LABEL_LOT = { "0": "미사용", "1": "사용" } as Record; const LABEL_USE = { "0": "미사용", "1": "사용" } as Record; const LABEL_QC = { "0": "무검사", "1": "검사" } as Record; const LABEL_YN = { "0": "부", "1": "여" } as Record; function displayValue(r: PartExcelRow, col: Column): string { if (col.showNameFor) { const name = r[col.showNameFor]; if (name) return String(name); return String(r[col.key] ?? ""); } const v = String(r[col.key] ?? ""); if (col.key === "LOT_FG") return LABEL_LOT[v] ?? v; if (col.key === "USE_YN") return LABEL_USE[v] ?? v; if (col.key === "QC_FG") return LABEL_QC[v] ?? v; if (col.key === "SETITEM_FG" || col.key === "REQ_FG") return LABEL_YN[v] ?? v; return v; } export function PartExcelImportDialog({ open, onOpenChange, onSaved }: Props) { const fileInputRef = useRef(null); const [parsedRows, setParsedRows] = useState([]); const [hasError, setHasError] = useState(false); const [fileName, setFileName] = useState(""); const [parsing, setParsing] = useState(false); const [saving, setSaving] = useState(false); const [dragOver, setDragOver] = useState(false); const reset = useCallback(() => { setParsedRows([]); setHasError(false); setFileName(""); }, []); const handleDialogChange = (v: boolean) => { if (!v) reset(); onOpenChange(v); }; const parseFile = useCallback(async (file: File) => { if (!/\.xlsx?$/i.test(file.name)) { toast.error("xlsx 또는 xls 파일만 업로드 가능합니다."); return; } setParsing(true); setFileName(file.name); try { const data = await devPartApi.excelParse(file); setParsedRows(data.rows ?? []); setHasError(!!data.hasError); if (!data.rows || data.rows.length === 0) { toast.warning("파싱된 데이터가 없습니다. 템플릿 형식을 확인해 주세요."); } else if (data.hasError) { toast.error("엑셀파일 로딩결과가 유효하지 않습니다. 결과메세지를 확인해 주세요."); } else { toast.success(`${data.rows.length}건 파싱 완료`); } } catch (e: any) { toast.error(e?.response?.data?.message ?? e?.message ?? "엑셀 파싱 실패"); reset(); } finally { setParsing(false); } }, [reset]); const handleFileInput = (e: React.ChangeEvent) => { const f = e.target.files?.[0]; if (f) parseFile(f); e.target.value = ""; }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); const f = e.dataTransfer.files?.[0]; if (f) parseFile(f); }; const handleSave = async () => { if (parsedRows.length === 0) { toast.error("저장할 데이터가 없습니다."); return; } if (hasError) { toast.error("엑셀파일 로딩결과가 유효하지 않습니다. 결과메세지를 확인해 주세요."); return; } if (!confirm("저장 하시겠습니까?")) return; setSaving(true); try { const res = await devPartApi.excelSave(parsedRows); toast.success(`${res.inserted}건이 저장되었습니다.${res.skipped > 0 ? ` (중복 ${res.skipped}건 건너뜀)` : ""}`); onSaved(); handleDialogChange(false); } catch (e: any) { toast.error(e?.response?.data?.message ?? e?.message ?? "저장 실패"); } finally { setSaving(false); } }; const errorCount = useMemo(() => parsedRows.filter((r) => r.NOTE).length, [parsedRows]); return ( PART 등록 Excel upload
{fileName && ( {fileName} )} {parsedRows.length > 0 && ( )}
총 {parsedRows.length}건 {errorCount > 0 && 에러 {errorCount}건}
{/* Drop Zone — 파싱 전에만 노출 */} {parsedRows.length === 0 && !parsing && (
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} onClick={() => fileInputRef.current?.click()} >
Drag & Drop 또는 클릭하여 엑셀 템플릿 업로드 (.xlsx, .xls)
)} {parsing && (
파싱 중...
)} {/* 결과 그리드 */} {parsedRows.length > 0 && !parsing && (
{COLUMNS.map((c) => ( ))} {parsedRows.map((r, i) => ( {COLUMNS.map((c) => { const value = displayValue(r, c); const isNote = c.key === "NOTE"; return ( ); })} ))}
# {c.label}
{i + 1} {value}
)}
); }