"use client"; // 개발관리 E-BOM 등록 CSV Import 다이얼로그 // wace partMng/openBomReportExcelImportPopUp.jsp 1:1 // // 운영판 흐름: // · Drop Zone: Drag & Drop CSV 템플릿 (fnc_setFileDropZone(..., "csv")) // · 파싱: parsingExcelFile.do 의 .csv 분기 → parsingCsvFile (수준 기반 부모 자동 매핑) // · 저장: partBomApplySave.do → savePartBomMaster // // CSV 컬럼 (11개, 헤더 1줄 후 데이터): // 0:수준 1:품번 2:품명 3:수량 4:항목수량 5:재료 6:열처리경도 7:열처리방법 // 8:표면처리 9:공급업체(MAKER) 10:범주이름(PART_TYPE) import React, { useCallback, useEffect, useMemo, useRef, 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 { CommCodeSelect } from "@/components/common/CommCodeSelect"; import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect"; import { Download, Upload, Save, Loader2, FileX, Copy } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { devBomApi, BomCsvRow, BomCopySourceRow } from "@/lib/api/devBom"; const PRODUCT_GROUP = "0000001"; const TEMPLATE_DOWNLOAD_URL = "/templates/BOM_REPORT_CSV_IMPORT_TEMPLATE.csv"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; editObjid?: string | null; initialProductCd?: string; onSaved: () => void; } interface Column { key: keyof BomCsvRow; label: string; width: string; align?: "left" | "center" | "right"; showNameFor?: keyof BomCsvRow; } // 그리드 컬럼: 화면 표시는 핵심 + 자동 채움 컬럼 (운영 그리드 25컬럼 중 CSV 11컬럼 + 자동 ACCTFG/ODRFG) const COLUMNS: Column[] = [ { key: "NOTE", label: "결과", width: "min-w-[200px]", align: "left" }, { key: "LEVEL", label: "수준", width: "min-w-[60px]", align: "center" }, { key: "PARENT_PART_NO", label: "모품번 (자동)", width: "min-w-[140px]", align: "center" }, { key: "PART_NO", label: "품번", width: "min-w-[140px]", align: "center" }, { key: "PART_NAME", label: "품명", width: "min-w-[200px]", align: "left" }, { key: "QTY", label: "수량", width: "min-w-[70px]", align: "right" }, { key: "ITEM_QTY", label: "항목수량", width: "min-w-[80px]", align: "right" }, { key: "MATERIAL", label: "재료", width: "min-w-[100px]" }, { key: "HEAT_TREATMENT_HARDNESS", label: "열처리경도", width: "min-w-[100px]" }, { key: "HEAT_TREATMENT_METHOD", label: "열처리방법", width: "min-w-[100px]" }, { 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: "ACCTFG", label: "계정구분(자동)", width: "min-w-[90px]", align: "center" }, { key: "ODRFG", label: "조달구분(자동)", width: "min-w-[90px]", align: "center" }, ]; const ACCTFG_LABEL: Record = { "4": "반제품", "7": "비용" }; const ODRFG_LABEL: Record = { "0": "구매", "1": "생산", "8": "Phantom" }; function displayValue(r: BomCsvRow, col: Column): string { if (col.key === "ACCTFG") { const v = String(r.ACCTFG ?? ""); return ACCTFG_LABEL[v] ?? v; } if (col.key === "ODRFG") { const v = String(r.ODRFG ?? ""); return ODRFG_LABEL[v] ?? v; } if (col.showNameFor) { const name = r[col.showNameFor]; if (name) return String(name); } return String(r[col.key] ?? ""); } export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, initialProductCd, onSaved }: Props) { const fileInputRef = useRef(null); const [productCd, setProductCd] = useState(""); const [bomPartNo, setBomPartNo] = useState(""); const [bomPartName, setBomPartName] = useState(""); const [version, setVersion] = useState(""); const [copyOptions, setCopyOptions] = useState([]); const [copySelect, setCopySelect] = useState(""); const [rows, setRows] = useState([]); const [hasError, setHasError] = useState(false); const [fileName, setFileName] = useState(""); const [encoding, setEncoding] = useState(""); const [parsing, setParsing] = useState(false); const [saving, setSaving] = useState(false); const [copying, setCopying] = useState(false); const [dragOver, setDragOver] = useState(false); const reset = useCallback(() => { setProductCd(initialProductCd ?? ""); setBomPartNo(""); setBomPartName(""); setVersion(""); setRows([]); setHasError(false); setFileName(""); setEncoding(""); setCopySelect(""); }, [initialProductCd]); useEffect(() => { if (!open) return; reset(); devBomApi.excelCopySource().then(setCopyOptions).catch(() => setCopyOptions([])); }, [open, reset]); const handleDialogChange = (v: boolean) => { if (!v) reset(); onOpenChange(v); }; const applyFirstLevelToHeader = (first: { part_no: string; part_name: string } | null) => { if (!first) return; if (first.part_no) setBomPartNo(first.part_no); if (first.part_name) setBomPartName(first.part_name); }; const parseFile = useCallback(async (file: File) => { if (!/\.csv$/i.test(file.name)) { toast.error("CSV(.csv) 파일만 업로드 가능합니다."); return; } setParsing(true); setFileName(file.name); try { const data = await devBomApi.excelParse(file); setRows(data.rows ?? []); setHasError(!!data.hasError); setEncoding(data.encoding ?? ""); applyFirstLevelToHeader(data.firstLevel); if (!data.rows || data.rows.length === 0) { toast.warning("파싱된 데이터가 없습니다. CSV 형식을 확인해 주세요."); } else if (data.hasError) { toast.error("CSV 파일 로딩결과가 유효하지 않습니다. 결과 메시지를 확인해 주세요."); } else { toast.success(`${data.rows.length}건 파싱 완료 (인코딩: ${data.encoding})`); } } catch (e: any) { toast.error(e?.response?.data?.message ?? e?.message ?? "CSV 파싱 실패"); setRows([]); setHasError(false); setFileName(""); setEncoding(""); } finally { setParsing(false); } }, []); 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 handleCopy = async () => { if (!copySelect) { toast.error("복사할 BOM을 선택하세요."); return; } setCopying(true); try { const copied = await devBomApi.excelCopy(copySelect); setRows(copied); setHasError(false); const first = copied.find((r) => !r.PARENT_PART_NO); if (first) applyFirstLevelToHeader({ part_no: first.PART_NO, part_name: first.PART_NAME }); toast.success(`BOM 데이터 ${copied.length}건 불러왔습니다.`); } catch (e: any) { toast.error(e?.response?.data?.message ?? e?.message ?? "BOM 복사 실패"); } finally { setCopying(false); } }; const handleSave = async () => { if (!productCd) { toast.error("제품구분을 선택해 주세요."); return; } if (!bomPartNo) { toast.error("품번을 입력해 주세요."); return; } if (!bomPartName){ toast.error("품명을 입력해 주세요."); return; } if (hasError) { toast.error("CSV 파일 로딩결과가 유효하지 않습니다. 결과 메시지를 확인해 주세요."); return; } try { const dup = await devBomApi.excelCheckDuplicate(bomPartNo, editObjid ?? undefined); if (dup) { toast.error("입력한 품번이 이미 존재합니다. 다른 품번을 입력해주세요."); return; } } catch { /* 비차단 */ } const confirmMsg = rows.length > 0 ? "저장 하시겠습니까?" : "품번, 품명으로 빈 E-BOM을 생성하시겠습니까?"; if (!confirm(confirmMsg)) return; setSaving(true); try { const result = await devBomApi.excelSave({ bomReportObjid: editObjid ?? undefined, productCd, partNo: bomPartNo, partName: bomPartName, version, rows, }); toast.success(`${result.mode === "create" ? "등록" : "수정"} 완료 — BOM ${result.bomRows}건 (PART 신규 ${result.insertedParts} / 수정 ${result.updatedParts})`); onSaved(); handleDialogChange(false); } catch (e: any) { toast.error(e?.response?.data?.message ?? e?.message ?? "저장 실패"); } finally { setSaving(false); } }; const errorCount = useMemo(() => rows.filter((r) => r.NOTE).length, [rows]); return ( PART 및 구조등록 CSV upload {/* 헤더 */}
setVersion(e.target.value)} placeholder="REV 등" />
{/* E-BOM 복사 + 액션 버튼 */}
((o) => ({ code: o.objid, label: `${o.part_no} / ${o.part_name}${o.revision ? ` (v${o.revision})` : ""}${o.regdate ? ` - ${o.regdate}` : ""}`, }))} value={copySelect} onValueChange={setCopySelect} placeholder="선택" />
{rows.length > 0 && ( )}
{fileName && {fileName}} {encoding && 인코딩: {encoding}} 총 {rows.length}건 {errorCount > 0 && 에러 {errorCount}건}
{/* Drop Zone */} {rows.length === 0 && !parsing && (
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} onClick={() => fileInputRef.current?.click()} >
Drag & Drop 또는 클릭하여 CSV 템플릿 업로드 (.csv)
컬럼: 수준 / 품번 / 품명 / 수량 / 항목수량 / 재료 / 열처리경도 / 열처리방법 / 표면처리 / 공급업체 / 범주이름
)} {parsing && (
파싱 중...
)} {/* 결과 그리드 */} {rows.length > 0 && !parsing && (
{COLUMNS.map((c) => ( ))} {rows.map((r, i) => ( {COLUMNS.map((c) => { const value = displayValue(r, c); const isNote = c.key === "NOTE"; return ( ); })} ))}
# {c.label}
{i + 1} {value}
)}
); }