"use client"; import React, { useState, useRef, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { toast } from "sonner"; import { Upload, FileSpreadsheet, AlertCircle, CheckCircle2, ArrowRight, Zap, Download, Loader2, } from "lucide-react"; import { importFromExcel, getExcelSheetNames, exportToExcel } from "@/lib/utils/excelExport"; import { cn } from "@/lib/utils"; import { EditableSpreadsheet } from "./EditableSpreadsheet"; import { TableChainConfig, uploadMultiTableExcel, } from "@/lib/api/multiTableExcel"; export interface MultiTableExcelUploadModalProps { open: boolean; onOpenChange: (open: boolean) => void; config: TableChainConfig; onSuccess?: () => void; } interface ColumnMapping { excelColumn: string; targetColumn: string | null; } export const MultiTableExcelUploadModal: React.FC = ({ open, onOpenChange, config, onSuccess, }) => { // 스텝: 1=모드선택+파일, 2=컬럼매핑, 3=확인 const [currentStep, setCurrentStep] = useState(1); // 모드 선택 const [selectedModeId, setSelectedModeId] = useState( config.uploadModes[0]?.id || "" ); // 파일 const [file, setFile] = useState(null); const [sheetNames, setSheetNames] = useState([]); const [selectedSheet, setSelectedSheet] = useState(""); const [isDragOver, setIsDragOver] = useState(false); const fileInputRef = useRef(null); const [allData, setAllData] = useState[]>([]); const [displayData, setDisplayData] = useState[]>([]); const [excelColumns, setExcelColumns] = useState([]); // 매핑 const [columnMappings, setColumnMappings] = useState([]); // 업로드 const [isUploading, setIsUploading] = useState(false); const selectedMode = config.uploadModes.find((m) => m.id === selectedModeId); // 선택된 모드에서 활성화되는 컬럼 목록 const activeColumns = React.useMemo(() => { if (!selectedMode) return []; const cols: Array<{ dbColumn: string; excelHeader: string; required: boolean; levelLabel: string }> = []; for (const levelIdx of selectedMode.activeLevels) { const level = config.levels[levelIdx]; if (!level) continue; for (const col of level.columns) { cols.push({ ...col, levelLabel: level.label, }); } } return cols; }, [selectedMode, config.levels]); // 템플릿 다운로드 const handleDownloadTemplate = () => { if (!selectedMode) return; const headers: string[] = []; const sampleRow: Record = {}; const sampleRow2: Record = {}; for (const levelIdx of selectedMode.activeLevels) { const level = config.levels[levelIdx]; if (!level) continue; for (const col of level.columns) { headers.push(col.excelHeader); sampleRow[col.excelHeader] = col.required ? "(필수)" : ""; sampleRow2[col.excelHeader] = ""; } } // 예시 데이터 생성 (config에 맞춰) exportToExcel( [sampleRow, sampleRow2], `${config.name}_${selectedMode.label}_템플릿.xlsx`, "Sheet1" ); toast.success("템플릿 파일이 다운로드되었습니다."); }; // 파일 처리 const processFile = async (selectedFile: File) => { const ext = selectedFile.name.split(".").pop()?.toLowerCase(); if (!["xlsx", "xls", "csv"].includes(ext || "")) { toast.error("엑셀 파일만 업로드 가능합니다. (.xlsx, .xls, .csv)"); return; } setFile(selectedFile); try { const sheets = await getExcelSheetNames(selectedFile); setSheetNames(sheets); setSelectedSheet(sheets[0] || ""); const data = await importFromExcel(selectedFile, sheets[0]); setAllData(data); setDisplayData(data); if (data.length > 0) { setExcelColumns(Object.keys(data[0])); } toast.success(`파일 선택 완료: ${selectedFile.name}`); } catch (error) { console.error("파일 읽기 오류:", error); toast.error("파일을 읽는 중 오류가 발생했습니다."); setFile(null); } }; const handleFileChange = async (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (selectedFile) await processFile(selectedFile); }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }; const handleDrop = async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); const droppedFile = e.dataTransfer.files?.[0]; if (droppedFile) await processFile(droppedFile); }; const handleSheetChange = async (sheetName: string) => { setSelectedSheet(sheetName); if (!file) return; try { const data = await importFromExcel(file, sheetName); setAllData(data); setDisplayData(data); if (data.length > 0) { setExcelColumns(Object.keys(data[0])); } } catch (error) { console.error("시트 읽기 오류:", error); toast.error("시트를 읽는 중 오류가 발생했습니다."); } }; // 2단계 진입 시 자동 매핑 시도 useEffect(() => { if (currentStep === 2 && excelColumns.length > 0) { performAutoMapping(); } }, [currentStep]); const performAutoMapping = () => { const newMappings: ColumnMapping[] = excelColumns.map((excelCol) => { const normalizedExcel = excelCol.toLowerCase().trim(); const matched = activeColumns.find((ac) => { return ( ac.excelHeader.toLowerCase().trim() === normalizedExcel || ac.dbColumn.toLowerCase().trim() === normalizedExcel ); }); return { excelColumn: excelCol, targetColumn: matched ? matched.excelHeader : null, }; }); setColumnMappings(newMappings); const matchedCount = newMappings.filter((m) => m.targetColumn).length; if (matchedCount > 0) { toast.success(`${matchedCount}개 컬럼이 자동 매핑되었습니다.`); } }; const handleMappingChange = (excelColumn: string, targetColumn: string | null) => { setColumnMappings((prev) => prev.map((m) => m.excelColumn === excelColumn ? { ...m, targetColumn } : m ) ); }; // 업로드 실행 const handleUpload = async () => { if (!file || !selectedMode) return; setIsUploading(true); try { // 엑셀 데이터를 excelHeader 기준으로 변환 const mappedRows = allData.map((row) => { const mappedRow: Record = {}; columnMappings.forEach((mapping) => { if (mapping.targetColumn) { mappedRow[mapping.targetColumn] = row[mapping.excelColumn]; } }); return mappedRow; }); // 빈 행 필터링 const filteredRows = mappedRows.filter((row) => Object.values(row).some( (v) => v !== undefined && v !== null && (typeof v !== "string" || v.trim() !== "") ) ); console.log(`다중 테이블 업로드: ${filteredRows.length}행`); const result = await uploadMultiTableExcel({ config, modeId: selectedModeId, rows: filteredRows, }); if (result.success && result.data) { const { results, errors } = result.data; const summaryParts = results .filter((r) => r.inserted + r.updated > 0) .map((r) => { const parts: string[] = []; if (r.inserted > 0) parts.push(`신규 ${r.inserted}건`); if (r.updated > 0) parts.push(`수정 ${r.updated}건`); return `${r.tableName}: ${parts.join(", ")}`; }); const msg = summaryParts.join(" / "); const errorMsg = errors.length > 0 ? ` (오류: ${errors.length}건)` : ""; toast.success(`업로드 완료: ${msg}${errorMsg}`); if (errors.length > 0) { console.warn("업로드 오류 목록:", errors); } onSuccess?.(); onOpenChange(false); } else { toast.error(result.message || "업로드에 실패했습니다."); } } catch (error) { console.error("다중 테이블 업로드 실패:", error); toast.error("업로드 중 오류가 발생했습니다."); } finally { setIsUploading(false); } }; // 다음/이전 단계 const handleNext = () => { if (currentStep === 1) { if (!file) { toast.error("파일을 선택해주세요."); return; } if (displayData.length === 0) { toast.error("데이터가 없습니다."); return; } } if (currentStep === 2) { // 필수 컬럼 매핑 확인 const mappedTargets = new Set( columnMappings.filter((m) => m.targetColumn).map((m) => m.targetColumn) ); const unmappedRequired = activeColumns .filter((ac) => ac.required && !mappedTargets.has(ac.excelHeader)) .map((ac) => `${ac.excelHeader}`); if (unmappedRequired.length > 0) { toast.error(`필수 컬럼이 매핑되지 않았습니다: ${unmappedRequired.join(", ")}`); return; } } setCurrentStep((prev) => Math.min(prev + 1, 3)); }; const handlePrevious = () => { setCurrentStep((prev) => Math.max(prev - 1, 1)); }; // 모달 닫기 시 초기화 useEffect(() => { if (!open) { setCurrentStep(1); setSelectedModeId(config.uploadModes[0]?.id || ""); setFile(null); setSheetNames([]); setSelectedSheet(""); setAllData([]); setDisplayData([]); setExcelColumns([]); setColumnMappings([]); } }, [open, config.uploadModes]); return ( {config.name} - 엑셀 업로드 다중 테이블 {config.description} {/* 스텝 인디케이터 */}
{[ { num: 1, label: "모드 선택 / 파일" }, { num: 2, label: "컬럼 매핑" }, { num: 3, label: "확인" }, ].map((step, index) => (
step.num ? "bg-success text-white" : "bg-muted text-muted-foreground" )} > {currentStep > step.num ? ( ) : ( step.num )}
{step.label}
{index < 2 && (
step.num ? "bg-success" : "bg-muted" )} /> )} ))}
{/* 스텝별 컨텐츠 */}
{/* 1단계: 모드 선택 + 파일 선택 */} {currentStep === 1 && (
{/* 업로드 모드 선택 */}
{config.uploadModes.map((mode) => ( ))}
{/* 템플릿 다운로드 */}
선택한 모드에 맞는 엑셀 양식을 다운로드하세요
{/* 파일 선택 */}
fileInputRef.current?.click()} className={cn( "mt-2 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-4 transition-colors", isDragOver ? "border-primary bg-primary/5" : file ? "border-green-500 bg-green-50" : "border-muted-foreground/25 hover:border-primary hover:bg-muted/50" )} > {file ? (

{file.name}

클릭하여 다른 파일 선택

) : ( <>

{isDragOver ? "파일을 놓으세요" : "파일을 드래그하거나 클릭하여 선택"}

지원 형식: .xlsx, .xls, .csv

)}
{/* 미리보기 */} {file && displayData.length > 0 && ( <>
{displayData.length}개 행
{ setDisplayData(newData); setAllData(newData); }} maxHeight="250px" /> )}
)} {/* 2단계: 컬럼 매핑 */} {currentStep === 2 && (

컬럼 매핑 설정

엑셀 컬럼
시스템 컬럼
{columnMappings.map((mapping, index) => (
{mapping.excelColumn}
))}
{/* 미매핑 필수 컬럼 경고 */} {(() => { const mappedTargets = new Set( columnMappings.filter((m) => m.targetColumn).map((m) => m.targetColumn) ); const missing = activeColumns.filter( (ac) => ac.required && !mappedTargets.has(ac.excelHeader) ); if (missing.length === 0) return null; return (

필수 컬럼이 매핑되지 않았습니다:

{missing.map((m) => `[${m.levelLabel}] ${m.excelHeader}`).join(", ")}

); })()} {/* 모드 정보 */} {selectedMode && (

모드: {selectedMode.label}

대상 테이블:{" "} {selectedMode.activeLevels .map((i) => config.levels[i]?.label) .filter(Boolean) .join(" → ")}

)}
)} {/* 3단계: 확인 */} {currentStep === 3 && (

업로드 요약

파일: {file?.name}

시트: {selectedSheet}

데이터 행: {allData.length}개

모드: {selectedMode?.label}

대상 테이블:{" "} {selectedMode?.activeLevels .map((i) => { const level = config.levels[i]; return level ? `${level.label}(${level.tableName})` : ""; }) .filter(Boolean) .join(" → ")}

컬럼 매핑

{columnMappings .filter((m) => m.targetColumn) .map((mapping, idx) => { const ac = activeColumns.find( (c) => c.excelHeader === mapping.targetColumn ); return (

{mapping.excelColumn}{" "} → [{ac?.levelLabel}] {mapping.targetColumn}

); })}

주의사항

업로드를 진행하면 데이터가 데이터베이스에 저장됩니다. 같은 키 값의 기존 데이터는 업데이트됩니다.

)}
{currentStep < 3 ? ( ) : ( )}
); };