ERP-node/frontend/components/common/ExcelUploadModal.tsx

945 lines
35 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 { 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";
import { findMappingByColumns, saveMappingTemplate } from "@/lib/api/excelMapping";
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<ExcelUploadModalProps> = ({
open,
onOpenChange,
tableName,
uploadMode = "insert",
keyColumn,
onSuccess,
userId = "guest",
}) => {
const [currentStep, setCurrentStep] = useState(1);
// 1단계: 파일 선택
const [file, setFile] = useState<File | null>(null);
const [sheetNames, setSheetNames] = useState<string[]>([]);
const [selectedSheet, setSelectedSheet] = useState<string>("");
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 2단계: 범위 지정
// (더 이상 사용하지 않는 상태들 - 3단계로 이동)
// 3단계: 컬럼 매핑 + 매핑 템플릿 자동 적용
const [isAutoMappingLoaded, setIsAutoMappingLoaded] = useState(false);
const [detectedRange, setDetectedRange] = useState<string>("");
const [previewData, setPreviewData] = useState<Record<string, any>[]>([]);
const [allData, setAllData] = useState<Record<string, any>[]>([]);
const [displayData, setDisplayData] = useState<Record<string, any>[]>([]);
// 3단계: 컬럼 매핑
const [excelColumns, setExcelColumns] = useState<string[]>([]);
const [systemColumns, setSystemColumns] = useState<TableColumn[]>([]);
const [columnMappings, setColumnMappings] = useState<ColumnMapping[]>([]);
// 4단계: 확인
const [isUploading, setIsUploading] = useState(false);
// 파일 선택 핸들러
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (!selectedFile) return;
await processFile(selectedFile);
};
// 파일 처리 공통 함수 (파일 선택 및 드래그 앤 드롭에서 공유)
const processFile = async (selectedFile: File) => {
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 handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(true);
};
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
};
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
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) {
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<string, any> = {};
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 AUTO_GENERATED_COLUMNS = [
"id", // ID
"created_date", // 생성일시
"updated_date", // 수정일시
"writer", // 작성자
"company_code", // 회사코드
];
const loadTableSchema = async () => {
try {
console.log("🔍 테이블 스키마 로드 시작:", { tableName });
const response = await getTableSchema(tableName);
console.log("📊 테이블 스키마 응답:", response);
if (response.success && response.data) {
// 자동 생성 컬럼 제외
const filteredColumns = response.data.columns.filter(
(col) => !AUTO_GENERATED_COLUMNS.includes(col.name.toLowerCase())
);
console.log("✅ 시스템 컬럼 로드 완료 (자동 생성 컬럼 제외):", filteredColumns);
setSystemColumns(filteredColumns);
// 기존 매핑 템플릿 조회
console.log("🔍 매핑 템플릿 조회 중...", { tableName, excelColumns });
const mappingResponse = await findMappingByColumns(tableName, excelColumns);
if (mappingResponse.success && mappingResponse.data) {
// 저장된 매핑 템플릿이 있으면 자동 적용
console.log("✅ 기존 매핑 템플릿 발견:", mappingResponse.data);
const savedMappings = mappingResponse.data.columnMappings;
const appliedMappings: ColumnMapping[] = excelColumns.map((col) => ({
excelColumn: col,
systemColumn: savedMappings[col] || null,
}));
setColumnMappings(appliedMappings);
setIsAutoMappingLoaded(true);
const matchedCount = appliedMappings.filter((m) => m.systemColumn).length;
toast.success(`이전 매핑 템플릿이 적용되었습니다. (${matchedCount}개 컬럼)`);
} else {
// 매핑 템플릿이 없으면 초기 상태로 설정
console.log(" 매핑 템플릿 없음 - 새 엑셀 구조");
const initialMappings: ColumnMapping[] = excelColumns.map((col) => ({
excelColumn: col,
systemColumn: null,
}));
setColumnMappings(initialMappings);
setIsAutoMappingLoaded(false);
}
} 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<string, any> = {};
columnMappings.forEach((mapping) => {
if (mapping.systemColumn) {
mappedRow[mapping.systemColumn] = row[mapping.excelColumn];
}
});
return mappedRow;
});
// 빈 행 필터링: 모든 값이 비어있거나 undefined/null인 행 제외
const filteredData = mappedData.filter((row) => {
const values = Object.values(row);
// 하나라도 유효한 값이 있는지 확인
return values.some((value) => {
if (value === undefined || value === null) return false;
if (typeof value === "string" && value.trim() === "") return false;
return true;
});
});
console.log(`📊 엑셀 업로드: 전체 ${mappedData.length}행 중 유효한 ${filteredData.length}`);
let successCount = 0;
let failCount = 0;
for (const row of filteredData) {
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}개)` : ""}`
);
// 매핑 템플릿 저장 (UPSERT - 자동 저장)
try {
const mappingsToSave: Record<string, string | null> = {};
columnMappings.forEach((mapping) => {
mappingsToSave[mapping.excelColumn] = mapping.systemColumn;
});
console.log("💾 매핑 템플릿 저장 중...", { tableName, excelColumns, mappingsToSave });
const saveResult = await saveMappingTemplate(tableName, excelColumns, mappingsToSave);
if (saveResult.success) {
console.log("✅ 매핑 템플릿 저장 완료:", saveResult.data);
} else {
console.warn("⚠️ 매핑 템플릿 저장 실패:", saveResult.error);
}
} catch (error) {
console.warn("⚠️ 매핑 템플릿 저장 중 오류:", error);
// 매핑 템플릿 저장 실패해도 업로드는 성공이므로 에러 표시 안함
}
onSuccess?.();
} else {
toast.error("업로드에 실패했습니다.");
}
} catch (error) {
console.error("❌ 엑셀 업로드 실패:", error);
toast.error("엑셀 업로드 중 오류가 발생했습니다.");
} finally {
setIsUploading(false);
}
};
// 모달 닫기 시 초기화
useEffect(() => {
if (!open) {
setCurrentStep(1);
setFile(null);
setSheetNames([]);
setSelectedSheet("");
setIsAutoMappingLoaded(false);
setDetectedRange("");
setPreviewData([]);
setAllData([]);
setDisplayData([]);
setExcelColumns([]);
setSystemColumns([]);
setColumnMappings([]);
}
}, [open]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-h-[95vh] max-w-[95vw] sm:max-w-[1200px]"
style={{
width: "1000px",
height: "700px",
minWidth: "700px",
minHeight: "500px",
maxWidth: "1400px",
maxHeight: "900px",
}}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
<FileSpreadsheet className="h-5 w-5" />
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
. .
</DialogDescription>
</DialogHeader>
{/* 스텝 인디케이터 */}
<div className="flex items-center justify-between">
{[
{ num: 1, label: "파일 선택" },
{ num: 2, label: "범위 지정" },
{ num: 3, label: "컬럼 매핑" },
{ num: 4, label: "확인" },
].map((step, index) => (
<React.Fragment key={step.num}>
<div className="flex flex-col items-center gap-1">
<div
className={cn(
"flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium transition-colors sm:h-10 sm:w-10",
currentStep === step.num
? "bg-primary text-primary-foreground"
: currentStep > step.num
? "bg-success text-white"
: "bg-muted text-muted-foreground"
)}
>
{currentStep > step.num ? (
<CheckCircle2 className="h-4 w-4 sm:h-5 sm:w-5" />
) : (
step.num
)}
</div>
<span
className={cn(
"text-[10px] font-medium sm:text-xs",
currentStep === step.num
? "text-primary"
: "text-muted-foreground"
)}
>
{step.label}
</span>
</div>
{index < 3 && (
<div
className={cn(
"h-0.5 flex-1 transition-colors",
currentStep > step.num ? "bg-success" : "bg-muted"
)}
/>
)}
</React.Fragment>
))}
</div>
{/* 스텝별 컨텐츠 */}
<div className="max-h-[calc(95vh-200px)] space-y-4 overflow-y-auto">
{/* 1단계: 파일 선택 */}
{currentStep === 1 && (
<div className="space-y-4">
<div>
<Label htmlFor="file-upload" className="text-xs sm:text-sm">
*
</Label>
{/* 드래그 앤 드롭 영역 */}
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={cn(
"mt-2 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-6 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 ? (
<>
<FileSpreadsheet className="mb-2 h-10 w-10 text-green-600" />
<p className="text-sm font-medium text-green-700">{file.name}</p>
<p className="mt-1 text-xs text-muted-foreground">
</p>
</>
) : (
<>
<Upload className={cn(
"mb-2 h-10 w-10",
isDragOver ? "text-primary" : "text-muted-foreground"
)} />
<p className={cn(
"text-sm font-medium",
isDragOver ? "text-primary" : "text-muted-foreground"
)}>
{isDragOver ? "파일을 놓으세요" : "파일을 드래그하거나 클릭하여 선택"}
</p>
<p className="mt-1 text-xs text-muted-foreground">
형식: .xlsx, .xls, .csv
</p>
</>
)}
<input
ref={fileInputRef}
id="file-upload"
type="file"
accept=".xlsx,.xls,.csv"
onChange={handleFileChange}
className="hidden"
/>
</div>
</div>
{sheetNames.length > 0 && (
<div>
<Label htmlFor="sheet-select" className="text-xs sm:text-sm">
</Label>
<Select value={selectedSheet} onValueChange={handleSheetChange}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="시트를 선택하세요" />
</SelectTrigger>
<SelectContent>
{sheetNames.map((sheetName) => (
<SelectItem
key={sheetName}
value={sheetName}
className="text-xs sm:text-sm"
>
<FileSpreadsheet className="mr-2 inline h-4 w-4" />
{sheetName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
)}
{/* 2단계: 범위 지정 */}
{currentStep === 2 && (
<div className="space-y-3">
{/* 상단: 시트 선택 + 버튼들 */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2">
<Label className="text-xs text-muted-foreground sm:text-sm">:</Label>
<Select value={selectedSheet} onValueChange={handleSheetChange}>
<SelectTrigger className="h-8 w-[140px] text-xs sm:h-10 sm:w-[180px] sm:text-sm">
<SelectValue placeholder="Sheet1" />
</SelectTrigger>
<SelectContent>
{sheetNames.map((sheetName) => (
<SelectItem key={sheetName} value={sheetName} className="text-xs sm:text-sm">
{sheetName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="ml-auto flex flex-wrap gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddRow}
className="h-7 text-xs sm:h-8 sm:text-sm"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddColumn}
className="h-7 text-xs sm:h-8 sm:text-sm"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleRemoveRow}
className="h-7 text-xs sm:h-8 sm:text-sm"
>
<Minus className="mr-1 h-3 w-3" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleRemoveColumn}
className="h-7 text-xs sm:h-8 sm:text-sm"
>
<Minus className="mr-1 h-3 w-3" />
</Button>
</div>
</div>
{/* 하단: 감지된 범위 + 테이블 */}
<div className="text-xs text-muted-foreground sm:text-sm">
: <span className="font-medium">{detectedRange}</span>
<span className="ml-2 text-[10px] sm:text-xs">
,
</span>
</div>
{displayData.length > 0 && (
<div className="max-h-[250px] overflow-auto rounded-md border border-border">
<table className="min-w-full text-[10px] sm:text-xs">
<thead className="sticky top-0 bg-muted">
<tr>
<th className="whitespace-nowrap border-b border-r border-border bg-primary/10 px-2 py-1 text-center font-medium">
</th>
{excelColumns.map((col, index) => (
<th
key={col}
className="whitespace-nowrap border-b border-r border-border bg-primary/10 px-2 py-1 text-center font-medium"
>
{String.fromCharCode(65 + index)}
</th>
))}
</tr>
</thead>
<tbody>
<tr className="bg-primary/5">
<td className="whitespace-nowrap border-b border-r border-border bg-primary/10 px-2 py-1 text-center font-medium">
1
</td>
{excelColumns.map((col) => (
<td
key={col}
className="whitespace-nowrap border-b border-r border-border px-2 py-1 font-medium text-primary"
>
{col}
</td>
))}
</tr>
{displayData.map((row, rowIndex) => (
<tr key={rowIndex} className="border-b border-border last:border-0">
<td className="whitespace-nowrap border-r border-border bg-muted/50 px-2 py-1 text-center font-medium text-muted-foreground">
{rowIndex + 2}
</td>
{excelColumns.map((col) => (
<td
key={col}
className="max-w-[150px] truncate whitespace-nowrap border-r border-border px-2 py-1"
title={String(row[col])}
>
{String(row[col] || "")}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* 3단계: 컬럼 매핑 */}
{currentStep === 3 && (
<div className="space-y-4">
{/* 상단: 제목 + 자동 매핑 버튼 */}
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold sm:text-base"> </h3>
<Button
type="button"
variant="default"
size="sm"
onClick={handleAutoMapping}
className="h-8 text-xs sm:h-9 sm:text-sm"
>
<Zap className="mr-2 h-4 w-4" />
</Button>
</div>
{/* 매핑 리스트 */}
<div className="space-y-2">
<div className="grid grid-cols-[1fr_auto_1fr] gap-2 text-[10px] font-medium text-muted-foreground sm:text-xs">
<div> </div>
<div></div>
<div> </div>
</div>
<div className="max-h-[400px] space-y-2 overflow-y-auto">
{columnMappings.map((mapping, index) => (
<div key={index} className="grid grid-cols-[1fr_auto_1fr] items-center gap-2">
<div className="rounded-md border border-border bg-muted px-3 py-2 text-xs font-medium sm:text-sm">
{mapping.excelColumn}
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
<Select
value={mapping.systemColumn || "none"}
onValueChange={(value) =>
handleMappingChange(
mapping.excelColumn,
value === "none" ? null : value
)
}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="매핑 안함">
{mapping.systemColumn
? (() => {
const col = systemColumns.find(c => c.name === mapping.systemColumn);
return col?.label || mapping.systemColumn;
})()
: "매핑 안함"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="none" className="text-xs sm:text-sm">
</SelectItem>
{systemColumns.map((col) => (
<SelectItem
key={col.name}
value={col.name}
className="text-xs sm:text-sm"
>
{col.label || col.name} ({col.type})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
))}
</div>
</div>
{/* 매핑 자동 저장 안내 */}
{isAutoMappingLoaded ? (
<div className="mt-4 rounded-md border border-success bg-success/10 p-3">
<div className="flex items-start gap-2">
<CheckCircle2 className="mt-0.5 h-4 w-4 text-success" />
<div className="text-[10px] text-success sm:text-xs">
<p className="font-medium"> </p>
<p className="mt-1">
.
.
</p>
</div>
</div>
</div>
) : (
<div className="mt-4 rounded-md border border-muted bg-muted/30 p-3">
<div className="flex items-start gap-2">
<Zap className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="text-[10px] text-muted-foreground sm:text-xs">
<p className="font-medium"> </p>
<p className="mt-1">
.
.
</p>
</div>
</div>
</div>
)}
</div>
)}
{/* 4단계: 확인 */}
{currentStep === 4 && (
<div className="space-y-4">
<div className="rounded-md border border-border bg-muted/50 p-4">
<h3 className="text-sm font-medium sm:text-base"> </h3>
<div className="mt-2 space-y-1 text-[10px] text-muted-foreground sm:text-xs">
<p>
<span className="font-medium">:</span> {file?.name}
</p>
<p>
<span className="font-medium">:</span> {selectedSheet}
</p>
<p>
<span className="font-medium"> :</span> {allData.length}
</p>
<p>
<span className="font-medium">:</span> {tableName}
</p>
<p>
<span className="font-medium">:</span>{" "}
{uploadMode === "insert"
? "삽입"
: uploadMode === "update"
? "업데이트"
: "Upsert"}
</p>
</div>
</div>
<div className="rounded-md border border-border bg-muted/50 p-4">
<h3 className="text-sm font-medium sm:text-base"> </h3>
<div className="mt-2 space-y-1 text-[10px] text-muted-foreground sm:text-xs">
{columnMappings
.filter((m) => m.systemColumn)
.map((mapping, index) => (
<p key={index}>
<span className="font-medium">{mapping.excelColumn}</span> {" "}
{mapping.systemColumn}
</p>
))}
{columnMappings.filter((m) => m.systemColumn).length === 0 && (
<p className="text-destructive"> .</p>
)}
</div>
</div>
<div className="rounded-md border border-warning bg-warning/10 p-3">
<div className="flex items-start gap-2">
<AlertCircle className="mt-0.5 h-4 w-4 text-warning" />
<div className="text-[10px] text-warning sm:text-xs">
<p className="font-medium"></p>
<p className="mt-1">
. ?
</p>
</div>
</div>
</div>
</div>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={currentStep === 1 ? () => onOpenChange(false) : handlePrevious}
disabled={isUploading}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{currentStep === 1 ? "취소" : "이전"}
</Button>
{currentStep < 4 ? (
<Button
onClick={handleNext}
disabled={isUploading}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
) : (
<Button
onClick={handleUpload}
disabled={isUploading || columnMappings.filter((m) => m.systemColumn).length === 0}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{isUploading ? "업로드 중..." : "다음"}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
};