diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index 97214a2a..01c39351 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -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 = ({ }) => { const [currentStep, setCurrentStep] = useState(1); - // 1단계: 파일 선택 + // 1단계: 파일 선택 & 미리보기 const [file, setFile] = useState(null); const [sheetNames, setSheetNames] = useState([]); const [selectedSheet, setSelectedSheet] = useState(""); const [isDragOver, setIsDragOver] = useState(false); const fileInputRef = useRef(null); - - // 2단계: 범위 지정 - // (더 이상 사용하지 않는 상태들 - 3단계로 이동) - - // 3단계: 컬럼 매핑 + 매핑 템플릿 자동 적용 - const [isAutoMappingLoaded, setIsAutoMappingLoaded] = useState(false); const [detectedRange, setDetectedRange] = useState(""); - const [previewData, setPreviewData] = useState[]>([]); const [allData, setAllData] = useState[]>([]); const [displayData, setDisplayData] = useState[]>([]); - // 3단계: 컬럼 매핑 + // 2단계: 컬럼 매핑 + 매핑 템플릿 자동 적용 + const [isAutoMappingLoaded, setIsAutoMappingLoaded] = useState(false); const [excelColumns, setExcelColumns] = useState([]); const [systemColumns, setSystemColumns] = useState([]); const [columnMappings, setColumnMappings] = useState([]); - // 4단계: 확인 + // 3단계: 확인 const [isUploading, setIsUploading] = useState(false); // 파일 선택 핸들러 @@ -160,7 +153,7 @@ export const ExcelUploadModal: React.FC = ({ 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 = ({ } }; - // 테이블 스키마 가져오기 + // 테이블 스키마 가져오기 (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 = ({ // 기존 매핑 템플릿 조회 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 = ({ 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 = ({ 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 = ({ 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 = ({ setIsUploading(true); try { - // allData를 사용하여 전체 데이터 업로드 (displayData는 미리보기용 10개만) + // allData를 사용하여 전체 데이터 업로드 const mappedData = allData.map((row) => { const mappedRow: Record = {}; columnMappings.forEach((mapping) => { @@ -376,7 +368,6 @@ export const ExcelUploadModal: React.FC = ({ // 빈 행 필터링: 모든 값이 비어있거나 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 = ({ }); }); - 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 = ({ 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 = ({ } } catch (error) { console.warn("⚠️ 매핑 템플릿 저장 중 오류:", error); - // 매핑 템플릿 저장 실패해도 업로드는 성공이므로 에러 표시 안함 } onSuccess?.(); @@ -451,7 +451,6 @@ export const ExcelUploadModal: React.FC = ({ setSelectedSheet(""); setIsAutoMappingLoaded(false); setDetectedRange(""); - setPreviewData([]); setAllData([]); setDisplayData([]); setExcelColumns([]); @@ -479,17 +478,16 @@ export const ExcelUploadModal: React.FC = ({ 엑셀 데이터 업로드 - 엑셀 파일을 선택하고 컬럼을 매핑하여 데이터를 업로드하세요. 모달 테두리를 드래그하여 크기를 조절할 수 있습니다. + 엑셀 파일을 선택하고 컬럼을 매핑하여 데이터를 업로드하세요. - {/* 스텝 인디케이터 */} + {/* 스텝 인디케이터 (3단계) */}
{[ { num: 1, label: "파일 선택" }, - { num: 2, label: "범위 지정" }, - { num: 3, label: "컬럼 매핑" }, - { num: 4, label: "확인" }, + { num: 2, label: "컬럼 매핑" }, + { num: 3, label: "확인" }, ].map((step, index) => (
@@ -512,15 +510,13 @@ export const ExcelUploadModal: React.FC = ({ {step.label}
- {index < 3 && ( + {index < 2 && (
= ({ {/* 스텝별 컨텐츠 */}
- {/* 1단계: 파일 선택 */} + {/* 1단계: 파일 선택 & 미리보기 (통합) */} {currentStep === 1 && (
+ {/* 파일 선택 영역 */}
- {/* 드래그 앤 드롭 영역 */}
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 = ({ )} > {file ? ( - <> - -

{file.name}

-

- 클릭하여 다른 파일 선택 -

- +
+ +
+

{file.name}

+

+ 클릭하여 다른 파일 선택 +

+
+
) : ( <> - -

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

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

지원 형식: .xlsx, .xls, .csv @@ -592,163 +596,148 @@ export const ExcelUploadModal: React.FC = ({

- {sheetNames.length > 0 && ( -
- - -
- )} -
- )} + {/* 파일이 선택된 경우에만 미리보기 표시 */} + {file && displayData.length > 0 && ( + <> + {/* 시트 선택 + 행/열 편집 버튼 */} +
+
+ + +
- {/* 2단계: 범위 지정 */} - {currentStep === 2 && ( -
- {/* 상단: 시트 선택 + 버튼들 */} -
-
- - -
+
+ + + + +
+
-
- - - - -
-
+ {/* 감지된 범위 */} +
+ 감지된 범위: {detectedRange} + ({displayData.length}개 행) +
- {/* 하단: 감지된 범위 + 테이블 */} -
- 감지된 범위: {detectedRange} - - 첫 행이 컬럼명, 데이터는 자동 감지됩니다 - -
- - {displayData.length > 0 && ( -
- - - - - {excelColumns.map((col, index) => ( - - ))} - - - - - - {excelColumns.map((col) => ( - - ))} - - {displayData.map((row, rowIndex) => ( - - + + {excelColumns.map((col) => ( + + ))} + + ))} + {displayData.length > 10 && ( + + + + )} + +
- - - {String.fromCharCode(65 + index)} -
- 1 - - {col} -
- {rowIndex + 2} + {/* 데이터 미리보기 테이블 */} +
+ + + + + {excelColumns.map((col, index) => ( + + ))} + + + + + {excelColumns.map((col) => ( ))} - ))} - -
+ {String.fromCharCode(65 + index)} +
+ 1 - {String(row[col] || "")} + {col}
-
+ {displayData.slice(0, 10).map((row, rowIndex) => ( +
+ {rowIndex + 2} + + {String(row[col] || "")} +
+ ... 외 {displayData.length - 10}개 행 +
+
+ )}
)} - {/* 3단계: 컬럼 매핑 */} - {currentStep === 3 && ( + {/* 2단계: 컬럼 매핑 */} + {currentStep === 2 && (
{/* 상단: 제목 + 자동 매핑 버튼 */}
@@ -773,9 +762,12 @@ export const ExcelUploadModal: React.FC = ({
시스템 컬럼
-
+
{columnMappings.map((mapping, index) => ( -
+
{mapping.excelColumn}
@@ -793,7 +785,9 @@ export const ExcelUploadModal: React.FC = ({ {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 = ({ {/* 매핑 자동 저장 안내 */} {isAutoMappingLoaded ? ( -
+

이전 매핑이 자동 적용됨

동일한 엑셀 구조가 감지되어 이전에 저장된 매핑이 적용되었습니다. - 필요시 수정하면 업로드 시 자동으로 저장됩니다. + 수정하면 업로드 시 자동 저장됩니다.

) : ( -
+

새로운 엑셀 구조

- 이 엑셀 구조는 처음입니다. 컬럼 매핑을 설정하면 업로드 시 자동으로 저장되어 - 다음에 같은 구조의 엑셀을 업로드할 때 자동 적용됩니다. + 이 엑셀 구조는 처음입니다. 매핑을 설정하면 다음에 같은 구조의 + 엑셀에 자동 적용됩니다.

@@ -850,8 +844,8 @@ export const ExcelUploadModal: React.FC = ({
)} - {/* 4단계: 확인 */} - {currentStep === 4 && ( + {/* 3단계: 확인 */} + {currentStep === 3 && (

업로드 요약

@@ -871,7 +865,7 @@ export const ExcelUploadModal: React.FC = ({

모드:{" "} {uploadMode === "insert" - ? "삽입" + ? "신규 등록" : uploadMode === "update" ? "업데이트" : "Upsert"} @@ -884,12 +878,17 @@ export const ExcelUploadModal: React.FC = ({

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

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

- ))} + .map((mapping, index) => { + const col = systemColumns.find( + (c) => c.name === mapping.systemColumn + ); + return ( +

+ {mapping.excelColumn} →{" "} + {col?.label || mapping.systemColumn} +

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

매핑된 컬럼이 없습니다.

)} @@ -902,7 +901,8 @@ export const ExcelUploadModal: React.FC = ({

주의사항

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

@@ -920,10 +920,10 @@ export const ExcelUploadModal: React.FC = ({ > {currentStep === 1 ? "취소" : "이전"} - {currentStep < 4 ? ( + {currentStep < 3 ? ( )}