"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 { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { toast } from "sonner"; import { Upload, FileSpreadsheet, AlertCircle, CheckCircle2, Plus, Minus, ArrowRight, Zap, } from "lucide-react"; import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport"; import { DynamicFormApi } from "@/lib/api/dynamicForm"; import { getTableSchema, TableColumn } from "@/lib/api/tableSchema"; import { cn } from "@/lib/utils"; export interface ExcelUploadModalProps { open: boolean; onOpenChange: (open: boolean) => void; tableName: string; uploadMode?: "insert" | "update" | "upsert"; keyColumn?: string; onSuccess?: () => void; userId?: string; } interface ColumnMapping { excelColumn: string; systemColumn: string | null; } export const ExcelUploadModal: React.FC = ({ open, onOpenChange, tableName, uploadMode = "insert", keyColumn, onSuccess, userId = "guest", }) => { const [currentStep, setCurrentStep] = useState(1); // 1단계: 파일 선택 const [file, setFile] = useState(null); const [sheetNames, setSheetNames] = useState([]); const [selectedSheet, setSelectedSheet] = useState(""); const fileInputRef = useRef(null); // 2단계: 범위 지정 const [autoCreateColumn, setAutoCreateColumn] = useState(false); const [selectedCompany, setSelectedCompany] = useState(""); const [selectedDataType, setSelectedDataType] = useState(""); const [detectedRange, setDetectedRange] = useState(""); const [previewData, setPreviewData] = useState[]>([]); const [allData, setAllData] = useState[]>([]); const [displayData, setDisplayData] = useState[]>([]); // 3단계: 컬럼 매핑 const [excelColumns, setExcelColumns] = useState([]); const [systemColumns, setSystemColumns] = useState([]); const [columnMappings, setColumnMappings] = useState([]); // 4단계: 확인 const [isUploading, setIsUploading] = useState(false); // 파일 선택 핸들러 const handleFileChange = async (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (!selectedFile) return; const fileExtension = selectedFile.name.split(".").pop()?.toLowerCase(); if (!["xlsx", "xls", "csv"].includes(fileExtension || "")) { 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) { const columns = Object.keys(data[0]); const lastCol = String.fromCharCode(64 + columns.length); setDetectedRange(`A1:${lastCol}${data.length + 1}`); setExcelColumns(columns); } toast.success(`파일이 선택되었습니다: ${selectedFile.name}`); } catch (error) { console.error("파일 읽기 오류:", error); toast.error("파일을 읽는 중 오류가 발생했습니다."); setFile(null); } }; // 시트 변경 핸들러 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) { const columns = Object.keys(data[0]); const lastCol = String.fromCharCode(64 + columns.length); setDetectedRange(`A1:${lastCol}${data.length + 1}`); setExcelColumns(columns); } } catch (error) { console.error("시트 읽기 오류:", error); toast.error("시트를 읽는 중 오류가 발생했습니다."); } }; // 행 추가 const handleAddRow = () => { const newRow: Record = {}; excelColumns.forEach((col) => { newRow[col] = ""; }); setDisplayData([...displayData, newRow]); toast.success("행이 추가되었습니다."); }; // 행 삭제 const handleRemoveRow = () => { if (displayData.length > 1) { setDisplayData(displayData.slice(0, -1)); toast.success("마지막 행이 삭제되었습니다."); } else { toast.error("최소 1개의 행이 필요합니다."); } }; // 열 추가 const handleAddColumn = () => { const newColName = `Column${excelColumns.length + 1}`; setExcelColumns([...excelColumns, newColName]); setDisplayData( displayData.map((row) => ({ ...row, [newColName]: "", })) ); toast.success("열이 추가되었습니다."); }; // 열 삭제 const handleRemoveColumn = () => { if (excelColumns.length > 1) { const lastCol = excelColumns[excelColumns.length - 1]; setExcelColumns(excelColumns.slice(0, -1)); setDisplayData( displayData.map((row) => { const { [lastCol]: removed, ...rest } = row; return rest; }) ); toast.success("마지막 열이 삭제되었습니다."); } else { toast.error("최소 1개의 열이 필요합니다."); } }; // 테이블 스키마 가져오기 useEffect(() => { if (currentStep === 3 && tableName) { loadTableSchema(); } }, [currentStep, tableName]); const loadTableSchema = async () => { try { console.log("🔍 테이블 스키마 로드 시작:", { tableName }); const response = await getTableSchema(tableName); console.log("📊 테이블 스키마 응답:", response); if (response.success && response.data) { console.log("✅ 시스템 컬럼 로드 완료:", response.data.columns); setSystemColumns(response.data.columns); const initialMappings: ColumnMapping[] = excelColumns.map((col) => ({ excelColumn: col, systemColumn: null, })); setColumnMappings(initialMappings); } else { console.error("❌ 테이블 스키마 로드 실패:", response); } } catch (error) { console.error("❌ 테이블 스키마 로드 실패:", error); toast.error("테이블 스키마를 불러올 수 없습니다."); } }; // 자동 매핑 - 컬럼명과 라벨 모두 비교 const handleAutoMapping = () => { const newMappings = excelColumns.map((excelCol) => { const normalizedExcelCol = excelCol.toLowerCase().trim(); // 1. 먼저 라벨로 매칭 시도 let matchedSystemCol = systemColumns.find( (sysCol) => sysCol.label && sysCol.label.toLowerCase().trim() === normalizedExcelCol ); // 2. 라벨로 매칭되지 않으면 컬럼명으로 매칭 시도 if (!matchedSystemCol) { matchedSystemCol = systemColumns.find( (sysCol) => sysCol.name.toLowerCase().trim() === normalizedExcelCol ); } return { excelColumn: excelCol, systemColumn: matchedSystemCol ? matchedSystemCol.name : null, }; }); setColumnMappings(newMappings); const matchedCount = newMappings.filter((m) => m.systemColumn).length; toast.success(`${matchedCount}개 컬럼이 자동 매핑되었습니다.`); }; // 컬럼 매핑 변경 const handleMappingChange = (excelColumn: string, systemColumn: string | null) => { setColumnMappings((prev) => prev.map((mapping) => mapping.excelColumn === excelColumn ? { ...mapping, systemColumn } : mapping ) ); }; // 다음 단계 const handleNext = () => { if (currentStep === 1 && !file) { toast.error("파일을 선택해주세요."); return; } if (currentStep === 2 && displayData.length === 0) { toast.error("데이터가 없습니다."); return; } setCurrentStep((prev) => Math.min(prev + 1, 4)); }; // 이전 단계 const handlePrevious = () => { setCurrentStep((prev) => Math.max(prev - 1, 1)); }; // 업로드 핸들러 const handleUpload = async () => { if (!file || !tableName) { toast.error("필수 정보가 누락되었습니다."); return; } setIsUploading(true); try { // allData를 사용하여 전체 데이터 업로드 (displayData는 미리보기용 10개만) const mappedData = allData.map((row) => { const mappedRow: Record = {}; columnMappings.forEach((mapping) => { if (mapping.systemColumn) { mappedRow[mapping.systemColumn] = row[mapping.excelColumn]; } }); return mappedRow; }); let successCount = 0; let failCount = 0; for (const row of mappedData) { try { if (uploadMode === "insert") { const formData = { screenId: 0, tableName, data: row }; const result = await DynamicFormApi.saveFormData(formData); if (result.success) { successCount++; } else { failCount++; } } } catch (error) { failCount++; } } if (successCount > 0) { toast.success( `${successCount}개 행이 업로드되었습니다.${failCount > 0 ? ` (실패: ${failCount}개)` : ""}` ); onSuccess?.(); } else { toast.error("업로드에 실패했습니다."); } } catch (error) { console.error("❌ 엑셀 업로드 실패:", error); toast.error("엑셀 업로드 중 오류가 발생했습니다."); } finally { setIsUploading(false); } }; // 모달 닫기 시 초기화 useEffect(() => { if (!open) { setCurrentStep(1); setFile(null); setSheetNames([]); setSelectedSheet(""); setAutoCreateColumn(false); setSelectedCompany(""); setSelectedDataType(""); setDetectedRange(""); setPreviewData([]); setAllData([]); setDisplayData([]); setExcelColumns([]); setSystemColumns([]); setColumnMappings([]); } }, [open]); return ( 엑셀 데이터 업로드 엑셀 파일을 선택하고 컬럼을 매핑하여 데이터를 업로드하세요. 모달 테두리를 드래그하여 크기를 조절할 수 있습니다. {/* 스텝 인디케이터 */}
{[ { num: 1, label: "파일 선택" }, { num: 2, label: "범위 지정" }, { num: 3, label: "컬럼 매핑" }, { num: 4, label: "확인" }, ].map((step, index) => (
step.num ? "bg-success text-white" : "bg-muted text-muted-foreground" )} > {currentStep > step.num ? ( ) : ( step.num )}
{step.label}
{index < 3 && (
step.num ? "bg-success" : "bg-muted" )} /> )} ))}
{/* 스텝별 컨텐츠 */}
{/* 1단계: 파일 선택 */} {currentStep === 1 && (

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

{sheetNames.length > 0 && (
)}
)} {/* 2단계: 범위 지정 */} {currentStep === 2 && (
{/* 상단: 3개 드롭다운 가로 배치 */}
{/* 중간: 체크박스 + 버튼들 한 줄 배치 */}
setAutoCreateColumn(checked as boolean)} />
{/* 하단: 감지된 범위 + 테이블 */}
감지된 범위: {detectedRange} 첫 행이 컬럼명, 데이터는 자동 감지됩니다
{displayData.length > 0 && (
{excelColumns.map((col, index) => ( ))} {excelColumns.map((col) => ( ))} {displayData.map((row, rowIndex) => ( {excelColumns.map((col) => ( ))} ))}
{String.fromCharCode(65 + index)}
1 {col}
{rowIndex + 2} {String(row[col] || "")}
)}
)} {/* 3단계: 컬럼 매핑 */} {currentStep === 3 && (
{/* 상단: 제목 + 자동 매핑 버튼 */}

컬럼 매핑 설정

{/* 매핑 리스트 */}
엑셀 컬럼
시스템 컬럼
{columnMappings.map((mapping, index) => (
{mapping.excelColumn}
))}
)} {/* 4단계: 확인 */} {currentStep === 4 && (

업로드 요약

파일: {file?.name}

시트: {selectedSheet}

데이터 행: {allData.length}개

테이블: {tableName}

모드:{" "} {uploadMode === "insert" ? "삽입" : uploadMode === "update" ? "업데이트" : "Upsert"}

컬럼 매핑

{columnMappings .filter((m) => m.systemColumn) .map((mapping, index) => (

{mapping.excelColumn} →{" "} {mapping.systemColumn}

))} {columnMappings.filter((m) => m.systemColumn).length === 0 && (

매핑된 컬럼이 없습니다.

)}

주의사항

업로드를 진행하면 데이터가 데이터베이스에 저장됩니다. 계속하시겠습니까?

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