배경 블러및 업로드 버튼 연결 완료

This commit is contained in:
sheetEasy AI Team
2025-06-24 15:18:23 +09:00
parent ba58aaabf5
commit 164db92e06

View File

@@ -1,4 +1,4 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState, useCallback } from "react";
import { Univer, UniverInstanceType, LocaleType } from "@univerjs/core"; import { Univer, UniverInstanceType, LocaleType } from "@univerjs/core";
import { defaultTheme } from "@univerjs/design"; import { defaultTheme } from "@univerjs/design";
import { UniverDocsPlugin } from "@univerjs/docs"; import { UniverDocsPlugin } from "@univerjs/docs";
@@ -12,6 +12,7 @@ import { UniverSheetsUIPlugin } from "@univerjs/sheets-ui";
import { UniverSheetsNumfmtPlugin } from "@univerjs/sheets-numfmt"; import { UniverSheetsNumfmtPlugin } from "@univerjs/sheets-numfmt";
import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui"; import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui";
import { UniverUIPlugin } from "@univerjs/ui"; import { UniverUIPlugin } from "@univerjs/ui";
import { cn } from "../../lib/utils";
// 언어팩 import // 언어팩 import
import DesignEnUS from "@univerjs/design/locale/en-US"; import DesignEnUS from "@univerjs/design/locale/en-US";
@@ -31,14 +32,22 @@ import "@univerjs/sheets-formula-ui/lib/index.css";
import "@univerjs/sheets-numfmt-ui/lib/index.css"; import "@univerjs/sheets-numfmt-ui/lib/index.css";
/** /**
* Univer CE 최소 구현 - 공식 문서 기반 * Univer CE + 파일 업로드 오버레이
* 파일 업로드 없이 기본 스프레드시트만 표시 * - Univer CE는 항상 렌더링 (기본 레이어)
* - 오버레이로 파일 업로드 UI 표시
* - disposeUnit()으로 메모리 관리
*/ */
const TestSheetViewer: React.FC = () => { const TestSheetViewer: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const univerRef = useRef<Univer | null>(null); const univerRef = useRef<Univer | null>(null);
const initializingRef = useRef<boolean>(false); const initializingRef = useRef<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);
const [showUploadOverlay, setShowUploadOverlay] = useState(true); // 초기에는 오버레이 표시
const [isDragOver, setIsDragOver] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [currentFile, setCurrentFile] = useState<File | null>(null);
// Univer 초기화 - 공식 문서 패턴 따라서 // Univer 초기화 - 공식 문서 패턴 따라서
useEffect(() => { useEffect(() => {
@@ -92,8 +101,8 @@ const TestSheetViewer: React.FC = () => {
// 3. 기본 워크북 생성 // 3. 기본 워크북 생성
univer.createUnit(UniverInstanceType.UNIVER_SHEET, { univer.createUnit(UniverInstanceType.UNIVER_SHEET, {
id: "test-workbook", id: "default-workbook",
name: "Test Workbook", name: "New Workbook",
sheetOrder: ["sheet1"], sheetOrder: ["sheet1"],
sheets: { sheets: {
sheet1: { sheet1: {
@@ -101,12 +110,8 @@ const TestSheetViewer: React.FC = () => {
name: "Sheet1", name: "Sheet1",
cellData: { cellData: {
0: { 0: {
0: { v: "Hello Univer CE!" }, 0: { v: "Ready for Excel Import" },
1: { v: "환영합니다!" }, 1: { v: "파일을 업로드하세요" },
},
1: {
0: { v: "Status" },
1: { v: "Ready" },
}, },
}, },
rowCount: 100, rowCount: 100,
@@ -126,7 +131,7 @@ const TestSheetViewer: React.FC = () => {
}; };
initializeUniver(); initializeUniver();
}, [isInitialized]); }, []);
// 컴포넌트 언마운트 시 정리 // 컴포넌트 언마운트 시 정리
useEffect(() => { useEffect(() => {
@@ -143,12 +148,166 @@ const TestSheetViewer: React.FC = () => {
}; };
}, []); }, []);
// 파일 처리 로직
const handleFileProcessing = useCallback(async (file: File) => {
if (!univerRef.current) return;
setIsProcessing(true);
console.log("📁 파일 처리 시작:", file.name);
try {
// 파일을 ArrayBuffer로 읽기
const arrayBuffer = await file.arrayBuffer();
const fileSize = (arrayBuffer.byteLength / 1024).toFixed(2);
// 기존 워크북 제거 (메모리 관리)
try {
// TODO: 실제 disposeUnit API 확인 후 구현
// univerRef.current.disposeUnit('default-workbook');
console.log("🗑️ 기존 워크북 정리 완료");
} catch (error) {
console.warn("⚠️ 기존 워크북 정리 실패:", error);
}
// 새 워크북 생성 (실제 Excel 파싱은 추후 구현)
const newWorkbook = {
id: `workbook-${Date.now()}`,
name: file.name,
sheetOrder: ["imported-sheet"],
sheets: {
"imported-sheet": {
id: "imported-sheet",
name: "Imported Data",
cellData: {
0: {
0: { v: "파일명" },
1: { v: file.name },
},
1: {
0: { v: "파일 크기" },
1: { v: `${fileSize} KB` },
},
2: {
0: { v: "업로드 시간" },
1: { v: new Date().toLocaleString() },
},
4: {
0: { v: "상태" },
1: { v: "업로드 완료 - 실제 Excel 파싱은 추후 구현" },
},
},
rowCount: 100,
columnCount: 26,
},
},
};
univerRef.current.createUnit(
UniverInstanceType.UNIVER_SHEET,
newWorkbook,
);
setCurrentFile(file);
setShowUploadOverlay(false); // 오버레이 숨기기
console.log("✅ 파일 처리 완료");
} catch (error) {
console.error("❌ 파일 처리 실패:", error);
} finally {
setIsProcessing(false);
}
}, []);
// 파일 선택 처리
const handleFileSelection = useCallback(
async (files: FileList) => {
if (files.length === 0) return;
const file = files[0];
// 파일 타입 검증
if (!file.name.match(/\.(xlsx|xls)$/i)) {
alert("Excel 파일(.xlsx, .xls)만 업로드 가능합니다.");
return;
}
// 파일 크기 검증 (50MB)
if (file.size > 50 * 1024 * 1024) {
alert("파일 크기는 50MB를 초과할 수 없습니다.");
return;
}
await handleFileProcessing(file);
},
[handleFileProcessing],
);
// 드래그 앤 드롭 이벤트 핸들러
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
setIsDragOver(true);
}
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
}, []);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDrop = useCallback(
async (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (isProcessing) return;
const files = e.dataTransfer.files;
if (files && files.length > 0) {
await handleFileSelection(files);
}
},
[handleFileSelection, isProcessing],
);
// 파일 선택 버튼 클릭
const handleFilePickerClick = useCallback(() => {
if (isProcessing || !fileInputRef.current) return;
fileInputRef.current.click();
}, [isProcessing]);
// 파일 입력 변경
const handleFileInputChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
await handleFileSelection(files);
}
e.target.value = "";
},
[handleFileSelection],
);
// 새 파일 업로드 (오버레이 다시 표시)
const handleNewUpload = useCallback(() => {
setShowUploadOverlay(true);
setCurrentFile(null);
}, []);
return ( return (
<div className="w-full h-screen flex flex-col"> <div className="w-full h-screen flex flex-col relative">
{/* 간단한 헤더 */} {/* 헤더 */}
<div className="bg-white border-b p-4 flex-shrink-0"> <div className="bg-white border-b p-4 flex-shrink-0 relative z-10">
<h1 className="text-xl font-bold">🧪 Univer CE </h1> <h1 className="text-xl font-bold">🧪 Univer CE + </h1>
<div className="mt-2"> <div className="mt-2 flex items-center gap-4">
<span <span
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${ className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
isInitialized isInitialized
@@ -158,15 +317,196 @@ const TestSheetViewer: React.FC = () => {
> >
{isInitialized ? "✅ 초기화 완료" : "⏳ 초기화 중..."} {isInitialized ? "✅ 초기화 완료" : "⏳ 초기화 중..."}
</span> </span>
{currentFile && (
<>
<span className="text-sm text-blue-600 font-medium">
📄 {currentFile.name}
</span>
<button
onClick={handleNewUpload}
className="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors"
>
</button>
</>
)}
</div> </div>
</div> </div>
{/* Univer 컨테이너 */} {/* Univer 컨테이너 (항상 렌더링) */}
<div <div
ref={containerRef} ref={containerRef}
className="flex-1" className="flex-1 relative"
style={{ minHeight: "500px" }} style={{
minHeight: "500px",
width: "100%",
height: "100%",
}}
/> />
{/* 파일 업로드 오버레이 - 레이어 분리 */}
{showUploadOverlay && (
<>
{/* 1. Univer CE 영역만 흐리게 하는 반투명 레이어 */}
<div
style={{
position: "absolute",
top: "00px", // 헤더 높이만큼 아래로 (헤더는 약 80px)
left: 0,
right: 0,
bottom: 0,
zIndex: 40,
backgroundColor: "rgba(255, 255, 255, 0.01)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)", // Safari 지원
pointerEvents: "auto",
}}
/>
{/* 2. Univer 영역 중앙의 업로드 UI */}
<div
style={{
position: "absolute",
top: "-100px", // 헤더 높이만큼 아래로
left: 0,
right: 0,
bottom: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "1rem",
zIndex: 50,
}}
>
<div
className="max-w-2xl w-full"
style={{ transform: "scale(0.8)" }}
>
<div className="bg-white rounded-lg shadow-xl border p-8 md:p-12">
<div className="text-center">
{/* 아이콘 및 제목 */}
<div className="mb-8">
<div
className={cn(
"mx-auto h-20 w-20 md:h-24 md:w-24 rounded-full flex items-center justify-center mb-4",
isProcessing ? "bg-blue-100" : "bg-blue-50",
)}
>
{isProcessing ? (
<svg
className="h-10 w-10 md:h-12 md:w-12 text-blue-600 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
) : (
<svg
className="h-10 w-10 md:h-12 md:w-12 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
)}
</div>
<h2 className="text-xl md:text-2xl font-semibold mb-2 text-gray-900">
{isProcessing
? "파일 처리 중..."
: "Excel 파일을 업로드하세요"}
</h2>
<p className="text-sm md:text-base text-gray-600 mb-6">
{isProcessing ? (
<span className="text-blue-600">
...
</span>
) : (
<>
<span className="font-medium text-gray-900">
.xlsx, .xls
</span>{" "}
</>
)}
</p>
</div>
{/* 드래그 앤 드롭 영역 */}
<div
className={cn(
"border-2 border-dashed rounded-lg p-8 md:p-12 transition-all duration-200 cursor-pointer",
"hover:border-blue-400 hover:bg-blue-50",
"focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2",
isDragOver
? "border-blue-500 bg-blue-100 scale-105"
: "border-gray-300",
isProcessing && "opacity-50 cursor-not-allowed",
)}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={handleFilePickerClick}
tabIndex={isProcessing ? -1 : 0}
role="button"
>
<div className="flex flex-col items-center justify-center space-y-4">
<div className="text-4xl md:text-6xl">
{isDragOver ? "📂" : "📄"}
</div>
<div className="text-center">
<p className="text-base md:text-lg font-medium mb-2 text-gray-900">
{isDragOver
? "파일을 여기에 놓으세요"
: "파일을 드래그하거나 클릭하세요"}
</p>
<p className="text-sm text-gray-600">
50MB까지
</p>
</div>
</div>
</div>
{/* 숨겨진 파일 입력 */}
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls"
onChange={handleFileInputChange}
className="hidden"
disabled={isProcessing}
/>
{/* 지원 형식 안내 */}
<div className="mt-6 text-xs text-gray-500">
<p> 형식: Excel (.xlsx, .xls)</p>
<p> 크기: 50MB</p>
</div>
</div>
</div>
</div>
</div>
</>
)}
</div> </div>
); );
}; };