Files
sheeteasyAI/src/components/sheet/EditSheetViewer.tsx
sheetEasy AI Team e5ee01553a 히스토리 패널 UI 작업 완료
- T-008 태스크 완료: 히스토리 패널 UI 마크업 구현 (슬라이드 인)
- 히스토리, UNDO, 전송하기 버튼을 세로로 균등 간격 배치
- Tailwind CSS v3/v4 버전 충돌 문제 해결
  - v4 패키지 완전 제거 (@tailwindcss/postcss 등)
  - PostCSS 설정을 v3 방식으로 수정
  - CSS 파일에서 수동 클래스 정의 제거
  - Vite 캐시 완전 삭제로 설정 변경 반영
- 히스토리 패널 기능 개선
  - 우측 슬라이드 인 애니메이션
  - 파일 업로드 시에만 표시
  - 상태별 아이콘과 시간순 로그 리스트
  - 재적용 및 전체 삭제 기능
- 새로운 rule 파일 생성: tailwind-css-management.mdc
2025-06-26 18:25:25 +09:00

964 lines
31 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useEffect, useState, useCallback } from "react";
// 공식 문서 권장: presets 패키지 사용으로 간소화
import {
createUniver,
defaultTheme,
LocaleType,
merge,
} from "@univerjs/presets";
import { UniverSheetsCorePreset } from "@univerjs/presets/preset-sheets-core";
import UniverPresetSheetsCoreEnUS from "@univerjs/presets/preset-sheets-core/locales/en-US";
import { UniverInstanceType } from "@univerjs/core";
// Presets CSS import
import "@univerjs/presets/lib/styles/preset-sheets-core.css";
import { cn } from "../../lib/utils";
import LuckyExcel from "@zwight/luckyexcel";
import PromptInput from "./PromptInput";
import HistoryPanel from "../ui/historyPanel";
import { useAppStore } from "../../stores/useAppStore";
import { rangeToAddress } from "../../utils/cellUtils";
import { CellSelectionHandler } from "../../utils/cellSelectionHandler";
import { aiProcessor } from "../../utils/aiProcessor";
import type { HistoryEntry } from "../../types/ai";
// 전역 고유 키 생성
const GLOBAL_UNIVER_KEY = "__GLOBAL_UNIVER_INSTANCE__";
const GLOBAL_STATE_KEY = "__GLOBAL_UNIVER_STATE__";
// 전역 상태 인터페이스
interface GlobalUniverState {
instance: any | null; // createUniver 반환 타입
univerAPI: any | null; // univerAPI 별도 저장
isInitializing: boolean;
isDisposing: boolean;
initializationPromise: Promise<any> | null;
lastContainerId: string | null;
}
// Window 객체에 전역 상태 확장
declare global {
interface Window {
[GLOBAL_UNIVER_KEY]: any | null;
[GLOBAL_STATE_KEY]: GlobalUniverState;
__UNIVER_DEBUG__: {
getGlobalUniver: () => any | null;
getGlobalState: () => GlobalUniverState;
clearGlobalState: () => void;
forceReset: () => void;
completeCleanup: () => void;
};
}
}
// 전역 상태 초기화 함수
const initializeGlobalState = (): GlobalUniverState => {
if (!window[GLOBAL_STATE_KEY]) {
window[GLOBAL_STATE_KEY] = {
instance: null,
univerAPI: null,
isInitializing: false,
isDisposing: false,
initializationPromise: null,
lastContainerId: null,
};
}
return window[GLOBAL_STATE_KEY];
};
// 전역 상태 가져오기
const getGlobalState = (): GlobalUniverState => {
return window[GLOBAL_STATE_KEY] || initializeGlobalState();
};
/**
* Presets 기반 강화된 전역 Univer 관리자
* 공식 문서 권장사항에 따라 createUniver 사용
*/
const UniverseManager = {
// 전역 인스턴스 생성 (완전 단일 인스턴스 보장)
async createInstance(container: HTMLElement): Promise<any> {
const state = getGlobalState();
const containerId = container.id || `container-${Date.now()}`;
console.log(`🚀 Univer 인스턴스 생성 요청 - Container: ${containerId}`);
// 이미 존재하는 인스턴스가 있고 같은 컨테이너면 재사용
if (
state.instance &&
state.univerAPI &&
state.lastContainerId === containerId
) {
console.log("✅ 기존 전역 Univer 인스턴스와 univerAPI 재사용");
return { univer: state.instance, univerAPI: state.univerAPI };
}
// 초기화가 진행 중이면 대기
if (state.isInitializing && state.initializationPromise) {
console.log("⏳ 기존 초기화 프로세스 대기 중...");
return state.initializationPromise;
}
// 새로운 초기화 시작
state.isInitializing = true;
console.log("🔄 새로운 Univer 인스턴스 생성 시작");
// 기존 인스턴스 정리
if (state.instance) {
try {
console.log("🗑️ 기존 인스턴스 정리 중...");
await state.instance.dispose();
state.instance = null;
window[GLOBAL_UNIVER_KEY] = null;
} catch (error) {
console.warn("⚠️ 기존 인스턴스 정리 중 오류:", error);
}
}
// 초기화 Promise 생성
state.initializationPromise = (async () => {
try {
// 컨테이너 ID 설정
if (!container.id) {
container.id = containerId;
}
console.log("🛠️ Presets 기반 Univer 인스턴스 생성 중...");
// 공식 문서 권장: createUniver 사용으로 대폭 간소화
const { univer, univerAPI } = createUniver({
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: merge({}, UniverPresetSheetsCoreEnUS),
},
theme: defaultTheme,
presets: [
UniverSheetsCorePreset({
container: container,
}),
],
});
// 전역 상태 업데이트 (univerAPI도 함께 저장)
state.instance = univer;
state.univerAPI = univerAPI;
state.lastContainerId = containerId;
window[GLOBAL_UNIVER_KEY] = univer;
console.log("✅ Presets 기반 Univer 인스턴스와 univerAPI 생성 완료");
return { univer, univerAPI };
} catch (error) {
console.error("❌ Univer 인스턴스 생성 실패:", error);
throw error;
} finally {
state.isInitializing = false;
state.initializationPromise = null;
}
})();
return state.initializationPromise;
},
// 전역 인스턴스 정리
async disposeInstance(): Promise<void> {
const state = getGlobalState();
if (state.isDisposing) {
console.log("🔄 이미 dispose 진행 중...");
return;
}
if (!state.instance) {
console.log(" 정리할 인스턴스가 없음");
return;
}
state.isDisposing = true;
try {
console.log("🗑️ 전역 Univer 인스턴스 dispose 시작");
await state.instance.dispose();
state.instance = null;
state.univerAPI = null;
state.lastContainerId = null;
window[GLOBAL_UNIVER_KEY] = null;
console.log("✅ 전역 Univer 인스턴스 dispose 완료");
} catch (error) {
console.error("❌ dispose 실패:", error);
} finally {
state.isDisposing = false;
}
},
// 현재 인스턴스 반환
getInstance(): any | null {
const state = getGlobalState();
return state.instance || window[GLOBAL_UNIVER_KEY] || null;
},
// 상태 확인 메서드들
isInitializing(): boolean {
return getGlobalState().isInitializing || false;
},
isDisposing(): boolean {
return getGlobalState().isDisposing || false;
},
// 강제 리셋 (디버깅용)
forceReset(): void {
const state = getGlobalState();
if (state.instance) {
try {
state.instance.dispose();
} catch (error) {
console.warn("강제 리셋 중 dispose 오류:", error);
}
}
// REDI 전역 상태 완전 정리 시도
try {
// 브라우저의 전역 REDI 상태 정리 (가능한 경우)
if (typeof window !== "undefined") {
// REDI 관련 전역 객체들 정리
const globalKeys = Object.keys(window).filter(
(key) =>
key.includes("redi") ||
key.includes("REDI") ||
key.includes("univerjs") ||
key.includes("univer"),
);
globalKeys.forEach((key) => {
try {
delete (window as any)[key];
} catch (e) {
console.warn(`전역 키 ${key} 정리 실패:`, e);
}
});
console.log("🧹 REDI 전역 상태 정리 시도 완료");
}
} catch (error) {
console.warn("⚠️ REDI 전역 상태 정리 중 오류:", error);
}
state.instance = null;
state.univerAPI = null;
state.isInitializing = false;
state.isDisposing = false;
state.initializationPromise = null;
state.lastContainerId = null;
window[GLOBAL_UNIVER_KEY] = null;
console.log("🔄 전역 Univer 상태 강제 리셋 완료 (REDI 정리 포함)");
},
// 완전한 정리 메서드 (REDI 포함)
async completeCleanup(): Promise<void> {
const state = getGlobalState();
if (state.isDisposing) {
console.log("🔄 이미 정리 진행 중...");
return;
}
state.isDisposing = true;
try {
console.log("🗑️ 완전한 정리 시작 (REDI 포함)");
if (state.instance) {
await state.instance.dispose();
}
// 약간의 대기 후 REDI 상태 정리
await new Promise((resolve) => setTimeout(resolve, 100));
// REDI 전역 상태 정리 시도
this.forceReset();
console.log("✅ 완전한 정리 완료");
} catch (error) {
console.error("❌ 완전한 정리 실패:", error);
} finally {
state.isDisposing = false;
}
},
};
// 전역 디버그 객체 설정을 위한 헬퍼 함수 (모듈 레벨 실행 방지)
const setupDebugTools = (): void => {
if (typeof window !== "undefined" && !window.__UNIVER_DEBUG__) {
// 전역 상태 초기화 (필요한 경우에만)
initializeGlobalState();
// 디버그 객체 설정
window.__UNIVER_DEBUG__ = {
getGlobalUniver: () => UniverseManager.getInstance(),
getGlobalState: () => getGlobalState(),
clearGlobalState: () => {
const state = getGlobalState();
Object.assign(state, {
instance: null,
univerAPI: null,
isInitializing: false,
isDisposing: false,
initializationPromise: null,
lastContainerId: null,
});
window[GLOBAL_UNIVER_KEY] = null;
console.log("🧹 전역 상태 정리 완료");
},
forceReset: () => UniverseManager.forceReset(),
completeCleanup: () => UniverseManager.completeCleanup(),
};
console.log("🐛 디버그 객체 설정 완료: window.__UNIVER_DEBUG__");
}
};
/**
* Univer CE + 파일 업로드 오버레이
* Window 객체 기반 완전한 단일 인스턴스 관리
*/
const TestSheetViewer: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const mountedRef = useRef<boolean>(false);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const [showUploadOverlay, setShowUploadOverlay] = useState(true);
const [isDragOver, setIsDragOver] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [currentFile, setCurrentFile] = useState<File | null>(null);
const [prompt, setPrompt] = useState("");
const appStore = useAppStore();
// CellSelectionHandler 인스턴스 생성
const cellSelectionHandler = useRef(new CellSelectionHandler());
// 히스토리 관련 상태 추가
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [history, setHistory] = useState<HistoryEntry[]>([]);
// 히스토리 관련 핸들러
const handleHistoryToggle = () => {
console.log("🔄 히스토리 토글:", !isHistoryOpen);
setIsHistoryOpen(!isHistoryOpen);
};
const handleHistoryClear = () => {
if (window.confirm("모든 히스토리를 삭제하시겠습니까?")) {
setHistory([]);
}
};
const handleHistoryReapply = (entry: HistoryEntry) => {
// 히스토리 항목 재적용 로직
setPrompt(entry.prompt);
console.log("🔄 히스토리 재적용:", entry);
// TODO: 실제 액션 재실행 로직 구현
};
// 새 히스토리 항목 추가 함수
const addHistoryEntry = (
prompt: string,
range: string,
sheetName: string,
actions: any[],
status: "success" | "error" | "pending",
error?: string,
) => {
const newEntry: HistoryEntry = {
id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date(),
prompt,
range,
sheetName,
actions,
status,
error,
};
setHistory((prev) => [newEntry, ...prev]); // 최신 항목을 맨 위에
};
// Univer 초기화 함수 (Presets 기반)
const initializeUniver = useCallback(
async (workbookData?: any) => {
if (!containerRef.current || !mountedRef.current) {
console.error("❌ 컨테이너가 준비되지 않았습니다!");
return;
}
try {
console.log("🚀 Presets 기반 Univer 초기화 시작");
// 전역 인스턴스 생성 또는 재사용 (Presets 사용)
const { univer, univerAPI } = await UniverseManager.createInstance(
containerRef.current,
);
// 기본 워크북 데이터
const defaultWorkbook = {
id: `workbook-${Date.now()}`,
locale: LocaleType.EN_US,
name: "Sample Workbook",
sheetOrder: ["sheet-01"],
sheets: {
"sheet-01": {
type: 0,
id: "sheet-01",
name: "Sheet1",
tabColor: "",
hidden: 0,
rowCount: 100,
columnCount: 20,
zoomRatio: 1,
scrollTop: 0,
scrollLeft: 0,
defaultColumnWidth: 93,
defaultRowHeight: 27,
cellData: {},
rowData: {},
columnData: {},
showGridlines: 1,
rowHeader: { width: 46, hidden: 0 },
columnHeader: { height: 20, hidden: 0 },
selections: ["A1"],
rightToLeft: 0,
},
},
};
const workbookToUse = workbookData || defaultWorkbook;
// Presets에서는 univerAPI가 자동으로 제공됨
try {
if (univerAPI) {
console.log("✅ Presets 기반 univerAPI 초기화 완료");
// 새 워크북 생성 (presets univerAPI 방식)
const workbook = univerAPI.createWorkbook(workbookToUse);
console.log("✅ Presets 기반 워크북 생성 완료:", workbook?.getId());
// 셀 선택 핸들러 초기화 - SRP에 맞춰 별도 클래스로 분리
cellSelectionHandler.current.initialize(univer);
setIsInitialized(true);
} else {
console.warn("⚠️ univerAPI가 제공되지 않음");
setIsInitialized(false);
}
} catch (error) {
console.error("❌ Presets 기반 워크북 생성 오류:", error);
setIsInitialized(false);
}
} catch (error) {
console.error("❌ Presets 기반 Univer 초기화 실패:", error);
setIsInitialized(false);
}
},
[appStore],
);
// 파일 처리 함수
const processFile = useCallback(
async (file: File) => {
// 강화된 상태 확인으로 중복 실행 완전 차단
if (
isProcessing ||
UniverseManager.isInitializing() ||
UniverseManager.isDisposing()
) {
console.log("⏸️ 처리 중이거나 상태 변경 중입니다. 현재 상태:", {
isProcessing,
isInitializing: UniverseManager.isInitializing(),
isDisposing: UniverseManager.isDisposing(),
});
return;
}
if (!file.name.toLowerCase().endsWith(".xlsx")) {
throw new Error("XLSX 파일만 지원됩니다.");
}
setIsProcessing(true);
console.log("📁 파일 처리 시작:", file.name);
try {
// LuckyExcel을 사용하여 Excel 파일을 Univer 데이터로 변환
await new Promise<void>((resolve, reject) => {
LuckyExcel.transformExcelToUniver(
file,
async (exportJson: any) => {
try {
console.log("📊 LuckyExcel 변환 완료:", exportJson);
// 변환된 데이터로 워크북 업데이트
if (exportJson && typeof exportJson === "object") {
await initializeUniver(exportJson);
} else {
console.warn(
"⚠️ 변환된 데이터가 유효하지 않음, 기본 워크북 사용",
);
await initializeUniver();
}
setCurrentFile(file);
setShowUploadOverlay(false);
resolve();
} catch (error) {
console.error("❌ 워크북 업데이트 오류:", error);
try {
await initializeUniver();
setCurrentFile(file);
setShowUploadOverlay(false);
resolve();
} catch (fallbackError) {
reject(fallbackError);
}
}
},
(error: any) => {
console.error("❌ LuckyExcel 변환 오류:", error);
reject(new Error("파일 변환 중 오류가 발생했습니다."));
},
);
});
} catch (error) {
console.error("❌ 파일 처리 오류:", error);
throw error;
} finally {
setIsProcessing(false);
console.log("✅ 파일 처리 완료");
}
},
[initializeUniver],
);
// 파일 입력 변경 처리
const handleFileInputChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
await processFile(file);
} catch (error) {
console.error("❌ 파일 처리 오류:", error);
alert(
error instanceof Error
? error.message
: "파일 처리 중 오류가 발생했습니다.",
);
} finally {
// 파일 입력 초기화로 중복 실행 방지
if (event.target) {
event.target.value = "";
}
}
},
[processFile],
);
// 파일 피커 클릭
const handleFilePickerClick = useCallback(() => {
if (isProcessing || UniverseManager.isInitializing()) return;
fileInputRef.current?.click();
}, [isProcessing]);
// 새 업로드 처리
const handleNewUpload = useCallback(() => {
if (isProcessing || UniverseManager.isInitializing()) return;
setShowUploadOverlay(true);
setCurrentFile(null);
}, [isProcessing]);
// 드래그 앤 드롭 이벤트 핸들러
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
}, []);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(false);
}, []);
const handleDrop = useCallback(
async (e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(false);
const files = e.dataTransfer.files;
if (files.length === 0) return;
const file = files[0];
if (!file.name.toLowerCase().endsWith(".xlsx")) {
alert("XLSX 파일만 업로드 가능합니다.");
return;
}
if (file.size > 50 * 1024 * 1024) {
alert("파일 크기는 50MB를 초과할 수 없습니다.");
return;
}
try {
await processFile(file);
} catch (error) {
console.error("❌ 파일 처리 오류:", error);
alert(
error instanceof Error
? error.message
: "파일 처리 중 오류가 발생했습니다.",
);
}
},
[processFile],
);
// 컴포넌트 마운트 시 초기화
useEffect(() => {
mountedRef.current = true;
console.log("🎯 컴포넌트 마운트됨");
// 디버그 도구 설정 (컴포넌트 마운트 시에만)
setupDebugTools();
// 강화된 기존 인스턴스 확인 및 재사용
const existingUniver = UniverseManager.getInstance();
const state = getGlobalState();
// 기존 인스턴스가 있고 정상 상태면 재사용
if (
existingUniver &&
state.univerAPI &&
!UniverseManager.isInitializing() &&
!UniverseManager.isDisposing()
) {
console.log("♻️ 기존 전역 Univer 인스턴스와 univerAPI 재사용");
setIsInitialized(true);
// 기존 인스턴스의 컨테이너가 현재 컨테이너와 다른 경우 갱신
if (
containerRef.current &&
state.lastContainerId !== containerRef.current.id
) {
console.log("🔄 컨테이너 정보 갱신");
state.lastContainerId = containerRef.current.id;
}
return;
}
// 초기화 중이거나 정리 중인 경우 대기
if (UniverseManager.isInitializing() || UniverseManager.isDisposing()) {
console.log("⏳ 초기화/정리 중이므로 대기");
const waitTimer = setInterval(() => {
if (
!UniverseManager.isInitializing() &&
!UniverseManager.isDisposing()
) {
clearInterval(waitTimer);
const currentUniver = UniverseManager.getInstance();
if (currentUniver && getGlobalState().univerAPI) {
console.log("✅ 대기 후 기존 인스턴스 재사용");
setIsInitialized(true);
} else if (containerRef.current && mountedRef.current) {
console.log("🚀 대기 후 새 인스턴스 초기화");
initializeUniver();
}
}
}, 100);
return () => clearInterval(waitTimer);
}
// 완전히 새로운 초기화가 필요한 경우만 진행
const initTimer = setTimeout(() => {
// 마운트 상태와 컨테이너 재확인
if (
containerRef.current &&
mountedRef.current &&
!UniverseManager.isInitializing()
) {
console.log("🚀 컴포넌트 마운트 시 새 Univer 초기화");
initializeUniver();
}
}, 100); // DOM 완전 준비 보장
return () => {
clearTimeout(initTimer);
mountedRef.current = false;
console.log("👋 컴포넌트 언마운트됨");
};
}, []); // 의존성 배열을 빈 배열로 변경하여 한 번만 실행
// 컴포넌트 언마운트 시 정리 (전역 인스턴스는 유지)
useEffect(() => {
return () => {
// 전역 인스턴스는 앱 종료 시에만 정리
// 여기서는 로컬 상태만 초기화
setIsInitialized(false);
console.log("🧹 로컬 상태 정리 완료");
};
}, []);
// 컴포넌트 언마운트 시 리소스 정리
useEffect(() => {
return () => {
// 셀 선택 핸들러 정리
if (cellSelectionHandler.current.isActive()) {
cellSelectionHandler.current.dispose();
}
};
}, []);
return (
<div className="w-full h-screen flex flex-col relative">
{/* 헤더 */}
<div className="bg-white border-b p-4 flex-shrink-0 relative z-10">
<div
className="mt-2 flex items-center gap-4"
style={{ position: "relative" }}
>
{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"
style={{
position: "absolute",
right: "0%",
top: "50%",
transform: "translateY(-50%)",
}}
>
</button>
</div>
</div>
{/* Univer 컨테이너 (항상 렌더링) */}
<div
ref={containerRef}
className="flex-1 relative"
style={{
minHeight: "0",
width: "100%",
height: "100%",
flexGrow: 0.85,
}}
/>
{/* Univer와 입력창 사이 여백 */}
<div style={{ height: "1rem" }} />
{/* 프롬프트 입력창 - Univer 하단에 이어서 */}
<PromptInput
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onHistoryToggle={handleHistoryToggle}
historyCount={history.length}
/>
{/* 히스토리 패널 - 파일이 업로드된 후에만 표시 */}
{!showUploadOverlay && (
<HistoryPanel
isOpen={isHistoryOpen}
onClose={() => setIsHistoryOpen(false)}
history={history}
onReapply={handleHistoryReapply}
onClear={handleHistoryClear}
/>
)}
{/* 파일 업로드 오버레이 - 레이어 분리 */}
{showUploadOverlay && (
<>
{/* 1. Univer CE 영역만 흐리게 하는 반투명 레이어 */}
<div
style={{
position: "absolute",
top: "0px", // 헤더 높이만큼 아래로 (헤더는 약 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"
style={{
backgroundColor: "#ffffff",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
}}
>
<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
</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"
onChange={handleFileInputChange}
className="hidden"
disabled={isProcessing}
/>
{/* 지원 형식 안내 */}
<div className="mt-6 text-xs text-gray-500">
<p> 형식: Excel (.xlsx)</p>
<p> 크기: 50MB</p>
<p className="mt-2 text-blue-600">
💡 window.__UNIVER_DEBUG__
</p>
</div>
</div>
</div>
</div>
</div>
</>
)}
</div>
);
};
export default TestSheetViewer;