배경 블러및 업로드 버튼 연결 완료
This commit is contained in:
@@ -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 { defaultTheme } from "@univerjs/design";
|
||||
import { UniverDocsPlugin } from "@univerjs/docs";
|
||||
@@ -12,6 +12,7 @@ import { UniverSheetsUIPlugin } from "@univerjs/sheets-ui";
|
||||
import { UniverSheetsNumfmtPlugin } from "@univerjs/sheets-numfmt";
|
||||
import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui";
|
||||
import { UniverUIPlugin } from "@univerjs/ui";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
// 언어팩 import
|
||||
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";
|
||||
|
||||
/**
|
||||
* Univer CE 최소 구현 - 공식 문서 기반
|
||||
* 파일 업로드 없이 기본 스프레드시트만 표시
|
||||
* Univer CE + 파일 업로드 오버레이
|
||||
* - Univer CE는 항상 렌더링 (기본 레이어)
|
||||
* - 오버레이로 파일 업로드 UI 표시
|
||||
* - disposeUnit()으로 메모리 관리
|
||||
*/
|
||||
const TestSheetViewer: React.FC = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const univerRef = useRef<Univer | null>(null);
|
||||
const initializingRef = useRef<boolean>(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
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 초기화 - 공식 문서 패턴 따라서
|
||||
useEffect(() => {
|
||||
@@ -92,8 +101,8 @@ const TestSheetViewer: React.FC = () => {
|
||||
|
||||
// 3. 기본 워크북 생성
|
||||
univer.createUnit(UniverInstanceType.UNIVER_SHEET, {
|
||||
id: "test-workbook",
|
||||
name: "Test Workbook",
|
||||
id: "default-workbook",
|
||||
name: "New Workbook",
|
||||
sheetOrder: ["sheet1"],
|
||||
sheets: {
|
||||
sheet1: {
|
||||
@@ -101,12 +110,8 @@ const TestSheetViewer: React.FC = () => {
|
||||
name: "Sheet1",
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: "Hello Univer CE!" },
|
||||
1: { v: "환영합니다!" },
|
||||
},
|
||||
1: {
|
||||
0: { v: "Status" },
|
||||
1: { v: "Ready" },
|
||||
0: { v: "Ready for Excel Import" },
|
||||
1: { v: "파일을 업로드하세요" },
|
||||
},
|
||||
},
|
||||
rowCount: 100,
|
||||
@@ -126,7 +131,7 @@ const TestSheetViewer: React.FC = () => {
|
||||
};
|
||||
|
||||
initializeUniver();
|
||||
}, [isInitialized]);
|
||||
}, []);
|
||||
|
||||
// 컴포넌트 언마운트 시 정리
|
||||
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 (
|
||||
<div className="w-full h-screen flex flex-col">
|
||||
{/* 간단한 헤더 */}
|
||||
<div className="bg-white border-b p-4 flex-shrink-0">
|
||||
<h1 className="text-xl font-bold">🧪 Univer CE 최소 테스트</h1>
|
||||
<div className="mt-2">
|
||||
<div className="w-full h-screen flex flex-col relative">
|
||||
{/* 헤더 */}
|
||||
<div className="bg-white border-b p-4 flex-shrink-0 relative z-10">
|
||||
<h1 className="text-xl font-bold">🧪 Univer CE + 파일 업로드</h1>
|
||||
<div className="mt-2 flex items-center gap-4">
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||
isInitialized
|
||||
@@ -158,15 +317,196 @@ const TestSheetViewer: React.FC = () => {
|
||||
>
|
||||
{isInitialized ? "✅ 초기화 완료" : "⏳ 초기화 중..."}
|
||||
</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>
|
||||
|
||||
{/* Univer 컨테이너 */}
|
||||
{/* Univer 컨테이너 (항상 렌더링) */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex-1"
|
||||
style={{ minHeight: "500px" }}
|
||||
className="flex-1 relative"
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user