'use client'; import React, { useState, useCallback, useRef } from 'react'; import { Upload, FileSpreadsheet, Download, CheckCircle2, XCircle, AlertCircle, Loader2, } from 'lucide-react'; export interface BatchResult { totalCount: number; successCount: number; failCount: number; failures: { row: number; reason: string }[]; data?: any[]; } export interface ExcelUploaderProps { title: string; description?: string; templateUrl?: string; uploadUrl: string; onSuccess?: (result: BatchResult) => void; accept?: string; } interface PreviewData { headers: string[]; rows: string[][]; } export default function ExcelUploader({ title, description, templateUrl, uploadUrl, onSuccess, accept = '.xlsx', }: ExcelUploaderProps) { const [file, setFile] = useState(null); const [preview, setPreview] = useState(null); const [rowCount, setRowCount] = useState(0); const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [showFailures, setShowFailures] = useState(false); const fileInputRef = useRef(null); const resetState = useCallback(() => { setFile(null); setPreview(null); setRowCount(0); setResult(null); setError(null); setShowFailures(false); if (fileInputRef.current) { fileInputRef.current.value = ''; } }, []); const handleFile = useCallback((selectedFile: File) => { if (!selectedFile.name.endsWith('.xlsx')) { setError('지원하지 않는 파일 형식입니다. .xlsx 파일을 선택해주세요.'); return; } setError(null); setResult(null); setFile(selectedFile); setShowFailures(false); // Parse file for preview (filename + row count estimation) const reader = new FileReader(); reader.onload = (e) => { try { // Simple row count from binary content (approximate) // In production, you'd use xlsx library for proper parsing const content = e.target?.result; if (content) { // For preview purposes, we'll just show the filename // A full xlsx parser would be needed for actual cell data setRowCount(0); // Will be updated by server response setPreview(null); // Preview requires xlsx library } } catch { // Ignore parse errors for preview } }; reader.readAsArrayBuffer(selectedFile); }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const droppedFile = e.dataTransfer.files[0]; if (droppedFile) { handleFile(droppedFile); } }, [handleFile]); const handleFileSelect = useCallback((e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (selectedFile) { handleFile(selectedFile); } }, [handleFile]); const handleUpload = useCallback(async () => { if (!file) return; setIsUploading(true); setError(null); setResult(null); try { const formData = new FormData(); formData.append('file', file); const response = await fetch(uploadUrl, { method: 'POST', body: formData, }); const json = await response.json(); if (!response.ok || !json.success) { throw new Error(json.message || '업로드 중 오류가 발생했습니다.'); } const batchResult: BatchResult = json.data; setResult(batchResult); setRowCount(batchResult.totalCount); if (batchResult.failCount === 0 && onSuccess) { onSuccess(batchResult); } } catch (err) { const message = err instanceof Error ? err.message : '알 수 없는 오류가 발생했습니다.'; setError(message); } finally { setIsUploading(false); } }, [file, uploadUrl, onSuccess]); const handleDownloadTemplate = useCallback(() => { if (templateUrl) { window.open(templateUrl, '_blank'); } }, [templateUrl]); return (
{/* Header */}

{title}

{description && (

{description}

)}
{/* Drop Zone */}
fileInputRef.current?.click()} className={` relative border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-colors duration-200 ${isDragging ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300 hover:border-indigo-400 hover:bg-gray-50' } `} >

엑셀 파일을 드래그하거나 [파일 선택]

지원 형식: {accept}

{/* Template Download */} {templateUrl && (
)} {/* File Info */} {file && (

{file.name}

{rowCount > 0 ? `${rowCount}행` : '행 수 분석 중...'}

)} {/* Preview Table */} {preview && (

미리보기

{preview.headers.map((header, i) => ( ))} {preview.rows.slice(0, 5).map((row, i) => ( {row.map((cell, j) => ( ))} ))}
{header}
{cell}
{preview.rows.length > 5 && (

상위 5행만 표시됩니다 (총 {preview.rows.length}행)

)}
)} {/* Upload Button */} {file && !result && (
)} {/* Error State */} {error && (

오류

{error}

)} {/* Results */} {result && (
{/* Summary */}
{result.failCount === 0 ? ( ) : ( )}

{result.totalCount}건 중 {result.successCount}건 성공 {result.failCount > 0 && `, ${result.failCount}건 실패`}

{/* Failure Details */} {result.failCount > 0 && (
{showFailures && (
{result.failures.map((failure, i) => (
Row {failure.row}

{failure.reason}

))}
)}
)} {/* Reset Button */}
)}
); }