엑셀 업로드 단계 통합
This commit is contained in:
parent
5321ea5b80
commit
83eb92cb27
|
|
@ -18,7 +18,6 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Upload,
|
||||
|
|
@ -62,29 +61,23 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
|
||||
// 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단계: 컬럼 매핑
|
||||
// 2단계: 컬럼 매핑 + 매핑 템플릿 자동 적용
|
||||
const [isAutoMappingLoaded, setIsAutoMappingLoaded] = useState(false);
|
||||
const [excelColumns, setExcelColumns] = useState<string[]>([]);
|
||||
const [systemColumns, setSystemColumns] = useState<TableColumn[]>([]);
|
||||
const [columnMappings, setColumnMappings] = useState<ColumnMapping[]>([]);
|
||||
|
||||
// 4단계: 확인
|
||||
// 3단계: 확인
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
// 파일 선택 핸들러
|
||||
|
|
@ -160,7 +153,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
try {
|
||||
const data = await importFromExcel(file, sheetName);
|
||||
setAllData(data);
|
||||
setDisplayData(data); // 전체 데이터를 미리보기에 표시 (스크롤 가능)
|
||||
setDisplayData(data);
|
||||
|
||||
if (data.length > 0) {
|
||||
const columns = Object.keys(data[0]);
|
||||
|
|
@ -224,30 +217,30 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// 테이블 스키마 가져오기
|
||||
// 테이블 스키마 가져오기 (2단계 진입 시)
|
||||
useEffect(() => {
|
||||
if (currentStep === 3 && tableName) {
|
||||
if (currentStep === 2 && tableName) {
|
||||
loadTableSchema();
|
||||
}
|
||||
}, [currentStep, tableName]);
|
||||
|
||||
// 테이블 생성 시 자동 생성되는 시스템 컬럼 (매핑에서 제외)
|
||||
const AUTO_GENERATED_COLUMNS = [
|
||||
"id", // ID
|
||||
"created_date", // 생성일시
|
||||
"updated_date", // 수정일시
|
||||
"writer", // 작성자
|
||||
"company_code", // 회사코드
|
||||
"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(
|
||||
|
|
@ -259,19 +252,19 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
// 기존 매핑 템플릿 조회
|
||||
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 {
|
||||
|
|
@ -297,10 +290,11 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
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
|
||||
(sysCol) =>
|
||||
sysCol.label && sysCol.label.toLowerCase().trim() === normalizedExcelCol
|
||||
);
|
||||
|
||||
// 2. 라벨로 매칭되지 않으면 컬럼명으로 매칭 시도
|
||||
|
|
@ -325,9 +319,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
const handleMappingChange = (excelColumn: string, systemColumn: string | null) => {
|
||||
setColumnMappings((prev) =>
|
||||
prev.map((mapping) =>
|
||||
mapping.excelColumn === excelColumn
|
||||
? { ...mapping, systemColumn }
|
||||
: mapping
|
||||
mapping.excelColumn === excelColumn ? { ...mapping, systemColumn } : mapping
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
@ -339,12 +331,12 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
if (currentStep === 2 && displayData.length === 0) {
|
||||
if (currentStep === 1 && displayData.length === 0) {
|
||||
toast.error("데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentStep((prev) => Math.min(prev + 1, 4));
|
||||
setCurrentStep((prev) => Math.min(prev + 1, 3));
|
||||
};
|
||||
|
||||
// 이전 단계
|
||||
|
|
@ -362,7 +354,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
// allData를 사용하여 전체 데이터 업로드 (displayData는 미리보기용 10개만)
|
||||
// allData를 사용하여 전체 데이터 업로드
|
||||
const mappedData = allData.map((row) => {
|
||||
const mappedRow: Record<string, any> = {};
|
||||
columnMappings.forEach((mapping) => {
|
||||
|
|
@ -376,7 +368,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
// 빈 행 필터링: 모든 값이 비어있거나 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;
|
||||
|
|
@ -384,7 +375,9 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
});
|
||||
});
|
||||
|
||||
console.log(`📊 엑셀 업로드: 전체 ${mappedData.length}행 중 유효한 ${filteredData.length}행`);
|
||||
console.log(
|
||||
`📊 엑셀 업로드: 전체 ${mappedData.length}행 중 유효한 ${filteredData.length}행`
|
||||
);
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
|
@ -416,10 +409,18 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
columnMappings.forEach((mapping) => {
|
||||
mappingsToSave[mapping.excelColumn] = mapping.systemColumn;
|
||||
});
|
||||
|
||||
console.log("💾 매핑 템플릿 저장 중...", { tableName, excelColumns, mappingsToSave });
|
||||
const saveResult = await saveMappingTemplate(tableName, excelColumns, mappingsToSave);
|
||||
|
||||
|
||||
console.log("💾 매핑 템플릿 저장 중...", {
|
||||
tableName,
|
||||
excelColumns,
|
||||
mappingsToSave,
|
||||
});
|
||||
const saveResult = await saveMappingTemplate(
|
||||
tableName,
|
||||
excelColumns,
|
||||
mappingsToSave
|
||||
);
|
||||
|
||||
if (saveResult.success) {
|
||||
console.log("✅ 매핑 템플릿 저장 완료:", saveResult.data);
|
||||
} else {
|
||||
|
|
@ -427,7 +428,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
}
|
||||
} catch (error) {
|
||||
console.warn("⚠️ 매핑 템플릿 저장 중 오류:", error);
|
||||
// 매핑 템플릿 저장 실패해도 업로드는 성공이므로 에러 표시 안함
|
||||
}
|
||||
|
||||
onSuccess?.();
|
||||
|
|
@ -451,7 +451,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
setSelectedSheet("");
|
||||
setIsAutoMappingLoaded(false);
|
||||
setDetectedRange("");
|
||||
setPreviewData([]);
|
||||
setAllData([]);
|
||||
setDisplayData([]);
|
||||
setExcelColumns([]);
|
||||
|
|
@ -479,17 +478,16 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
엑셀 데이터 업로드
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
엑셀 파일을 선택하고 컬럼을 매핑하여 데이터를 업로드하세요. 모달 테두리를 드래그하여 크기를 조절할 수 있습니다.
|
||||
엑셀 파일을 선택하고 컬럼을 매핑하여 데이터를 업로드하세요.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 스텝 인디케이터 */}
|
||||
{/* 스텝 인디케이터 (3단계) */}
|
||||
<div className="flex items-center justify-between">
|
||||
{[
|
||||
{ num: 1, label: "파일 선택" },
|
||||
{ num: 2, label: "범위 지정" },
|
||||
{ num: 3, label: "컬럼 매핑" },
|
||||
{ num: 4, label: "확인" },
|
||||
{ num: 2, label: "컬럼 매핑" },
|
||||
{ num: 3, label: "확인" },
|
||||
].map((step, index) => (
|
||||
<React.Fragment key={step.num}>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
|
|
@ -512,15 +510,13 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
<span
|
||||
className={cn(
|
||||
"text-[10px] font-medium sm:text-xs",
|
||||
currentStep === step.num
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
currentStep === step.num ? "text-primary" : "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
</div>
|
||||
{index < 3 && (
|
||||
{index < 2 && (
|
||||
<div
|
||||
className={cn(
|
||||
"h-0.5 flex-1 transition-colors",
|
||||
|
|
@ -534,21 +530,21 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
|
||||
{/* 스텝별 컨텐츠 */}
|
||||
<div className="max-h-[calc(95vh-200px)] space-y-4 overflow-y-auto">
|
||||
{/* 1단계: 파일 선택 */}
|
||||
{/* 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",
|
||||
"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
|
||||
|
|
@ -557,24 +553,32 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
)}
|
||||
>
|
||||
{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>
|
||||
</>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileSpreadsheet className="h-8 w-8 text-green-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-700">{file.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
클릭하여 다른 파일 선택
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<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 ? "파일을 놓으세요" : "파일을 드래그하거나 클릭하여 선택"}
|
||||
<Upload
|
||||
className={cn(
|
||||
"mb-2 h-8 w-8",
|
||||
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
|
||||
|
|
@ -592,163 +596,148 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
</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>
|
||||
)}
|
||||
{/* 파일이 선택된 경우에만 미리보기 표시 */}
|
||||
{file && displayData.length > 0 && (
|
||||
<>
|
||||
{/* 시트 선택 + 행/열 편집 버튼 */}
|
||||
<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-9 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>
|
||||
|
||||
{/* 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-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddRow}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />행
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRemoveRow}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Minus className="mr-1 h-3 w-3" />행
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddColumn}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />열
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRemoveColumn}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Minus className="mr-1 h-3 w-3" />열
|
||||
</Button>
|
||||
</div>
|
||||
</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">
|
||||
감지된 범위: <span className="font-medium">{detectedRange}</span>
|
||||
<span className="ml-2">({displayData.length}개 행)</span>
|
||||
</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}
|
||||
{/* 데이터 미리보기 테이블 */}
|
||||
<div className="max-h-[280px] 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="max-w-[150px] truncate whitespace-nowrap border-r border-border px-2 py-1"
|
||||
title={String(row[col])}
|
||||
className="whitespace-nowrap border-b border-r border-border px-2 py-1 font-medium text-primary"
|
||||
>
|
||||
{String(row[col] || "")}
|
||||
{col}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{displayData.slice(0, 10).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>
|
||||
))}
|
||||
{displayData.length > 10 && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={excelColumns.length + 1}
|
||||
className="bg-muted/30 px-2 py-1 text-center text-muted-foreground"
|
||||
>
|
||||
... 외 {displayData.length - 10}개 행
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3단계: 컬럼 매핑 */}
|
||||
{currentStep === 3 && (
|
||||
{/* 2단계: 컬럼 매핑 */}
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-4">
|
||||
{/* 상단: 제목 + 자동 매핑 버튼 */}
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -773,9 +762,12 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
<div>시스템 컬럼</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[400px] space-y-2 overflow-y-auto">
|
||||
<div className="max-h-[350px] 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
|
||||
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>
|
||||
|
|
@ -793,7 +785,9 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
<SelectValue placeholder="매핑 안함">
|
||||
{mapping.systemColumn
|
||||
? (() => {
|
||||
const col = systemColumns.find(c => c.name === mapping.systemColumn);
|
||||
const col = systemColumns.find(
|
||||
(c) => c.name === mapping.systemColumn
|
||||
);
|
||||
return col?.label || mapping.systemColumn;
|
||||
})()
|
||||
: "매핑 안함"}
|
||||
|
|
@ -821,27 +815,27 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
|
||||
{/* 매핑 자동 저장 안내 */}
|
||||
{isAutoMappingLoaded ? (
|
||||
<div className="mt-4 rounded-md border border-success bg-success/10 p-3">
|
||||
<div className="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="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>
|
||||
|
|
@ -850,8 +844,8 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 4단계: 확인 */}
|
||||
{currentStep === 4 && (
|
||||
{/* 3단계: 확인 */}
|
||||
{currentStep === 3 && (
|
||||
<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>
|
||||
|
|
@ -871,7 +865,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
<p>
|
||||
<span className="font-medium">모드:</span>{" "}
|
||||
{uploadMode === "insert"
|
||||
? "삽입"
|
||||
? "신규 등록"
|
||||
: uploadMode === "update"
|
||||
? "업데이트"
|
||||
: "Upsert"}
|
||||
|
|
@ -884,12 +878,17 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
<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>
|
||||
))}
|
||||
.map((mapping, index) => {
|
||||
const col = systemColumns.find(
|
||||
(c) => c.name === mapping.systemColumn
|
||||
);
|
||||
return (
|
||||
<p key={index}>
|
||||
<span className="font-medium">{mapping.excelColumn}</span> →{" "}
|
||||
{col?.label || mapping.systemColumn}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
{columnMappings.filter((m) => m.systemColumn).length === 0 && (
|
||||
<p className="text-destructive">매핑된 컬럼이 없습니다.</p>
|
||||
)}
|
||||
|
|
@ -902,7 +901,8 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
<div className="text-[10px] text-warning sm:text-xs">
|
||||
<p className="font-medium">주의사항</p>
|
||||
<p className="mt-1">
|
||||
업로드를 진행하면 데이터가 데이터베이스에 저장됩니다. 계속하시겠습니까?
|
||||
업로드를 진행하면 데이터가 데이터베이스에 저장됩니다.
|
||||
계속하시겠습니까?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -920,10 +920,10 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
>
|
||||
{currentStep === 1 ? "취소" : "이전"}
|
||||
</Button>
|
||||
{currentStep < 4 ? (
|
||||
{currentStep < 3 ? (
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={isUploading}
|
||||
disabled={isUploading || (currentStep === 1 && !file)}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
다음
|
||||
|
|
@ -931,10 +931,12 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
) : (
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={isUploading || columnMappings.filter((m) => m.systemColumn).length === 0}
|
||||
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 ? "업로드 중..." : "다음"}
|
||||
{isUploading ? "업로드 중..." : "업로드"}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
|
|
|||
Loading…
Reference in New Issue