배경 블러및 업로드 버튼 연결 완료
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 { 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user