2025-09-26 13:11:34 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
2025-09-29 13:29:03 +09:00
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
2025-09-26 13:11:34 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { FileInfo } from "./types";
|
2025-09-29 13:29:03 +09:00
|
|
|
import { Download, X, AlertTriangle, FileText, Trash2, ExternalLink } from "lucide-react";
|
2025-09-26 13:11:34 +09:00
|
|
|
import { formatFileSize } from "@/lib/utils";
|
2025-09-29 13:29:03 +09:00
|
|
|
import { API_BASE_URL } from "@/lib/api/client";
|
|
|
|
|
|
|
|
|
|
// Office 문서 렌더링을 위한 CDN 라이브러리 로드
|
|
|
|
|
const loadOfficeLibrariesFromCDN = async () => {
|
|
|
|
|
if (typeof window === 'undefined') return { XLSX: null, mammoth: null };
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// XLSX 라이브러리가 이미 로드되어 있는지 확인
|
|
|
|
|
if (!(window as any).XLSX) {
|
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
|
const script = document.createElement('script');
|
|
|
|
|
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js';
|
|
|
|
|
script.onload = resolve;
|
|
|
|
|
script.onerror = reject;
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mammoth 라이브러리가 이미 로드되어 있는지 확인
|
|
|
|
|
if (!(window as any).mammoth) {
|
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
|
const script = document.createElement('script');
|
|
|
|
|
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.4.2/mammoth.browser.min.js';
|
|
|
|
|
script.onload = resolve;
|
|
|
|
|
script.onerror = reject;
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
XLSX: (window as any).XLSX,
|
|
|
|
|
mammoth: (window as any).mammoth
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Office 라이브러리 CDN 로드 실패:', error);
|
|
|
|
|
return { XLSX: null, mammoth: null };
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
interface FileViewerModalProps {
|
|
|
|
|
file: FileInfo | null;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onDownload?: (file: FileInfo) => void;
|
2025-09-29 13:29:03 +09:00
|
|
|
onDelete?: (file: FileInfo) => void;
|
2025-09-26 13:11:34 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파일 뷰어 모달 컴포넌트
|
|
|
|
|
*/
|
|
|
|
|
export const FileViewerModal: React.FC<FileViewerModalProps> = ({
|
|
|
|
|
file,
|
|
|
|
|
isOpen,
|
|
|
|
|
onClose,
|
|
|
|
|
onDownload,
|
2025-09-29 13:29:03 +09:00
|
|
|
onDelete,
|
2025-09-26 13:11:34 +09:00
|
|
|
}) => {
|
|
|
|
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
|
|
|
const [previewError, setPreviewError] = useState<string | null>(null);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
2025-09-29 13:29:03 +09:00
|
|
|
const [renderedContent, setRenderedContent] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// Office 문서를 CDN 라이브러리로 렌더링하는 함수
|
|
|
|
|
const renderOfficeDocument = async (blob: Blob, fileExt: string, fileName: string) => {
|
|
|
|
|
try {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
|
|
|
|
// CDN에서 라이브러리 로드
|
|
|
|
|
const { XLSX, mammoth } = await loadOfficeLibrariesFromCDN();
|
|
|
|
|
|
|
|
|
|
if (fileExt === "docx" && mammoth) {
|
|
|
|
|
// Word 문서 렌더링
|
|
|
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
|
|
|
const result = await mammoth.convertToHtml({ arrayBuffer });
|
|
|
|
|
|
|
|
|
|
const htmlContent = `
|
|
|
|
|
<div>
|
|
|
|
|
<h4 style="margin: 0 0 15px 0; color: #333; font-size: 16px;">📄 ${fileName}</h4>
|
|
|
|
|
<div class="word-content" style="max-height: 500px; overflow-y: auto; padding: 20px; background: white; border: 1px solid #ddd; border-radius: 5px; line-height: 1.6; font-family: 'Times New Roman', serif;">
|
|
|
|
|
${result.value || '내용을 읽을 수 없습니다.'}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
setRenderedContent(htmlContent);
|
|
|
|
|
return true;
|
|
|
|
|
} else if (["xlsx", "xls"].includes(fileExt) && XLSX) {
|
|
|
|
|
// Excel 문서 렌더링
|
|
|
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
|
|
|
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
|
|
|
|
const sheetName = workbook.SheetNames[0];
|
|
|
|
|
const worksheet = workbook.Sheets[sheetName];
|
|
|
|
|
|
|
|
|
|
const html = XLSX.utils.sheet_to_html(worksheet, {
|
|
|
|
|
table: { className: 'excel-table' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const htmlContent = `
|
|
|
|
|
<div>
|
|
|
|
|
<h4 style="margin: 0 0 10px 0; color: #333; font-size: 16px;">📊 ${fileName}</h4>
|
|
|
|
|
<p style="margin: 0 0 15px 0; color: #666; font-size: 14px;">시트: ${sheetName}</p>
|
|
|
|
|
<div style="max-height: 400px; overflow: auto; border: 1px solid #ddd; border-radius: 5px;">
|
|
|
|
|
<style>
|
|
|
|
|
.excel-table { border-collapse: collapse; width: 100%; }
|
|
|
|
|
.excel-table td, .excel-table th { border: 1px solid #ddd; padding: 8px; text-align: left; font-size: 12px; }
|
|
|
|
|
.excel-table th { background-color: #f5f5f5; font-weight: bold; }
|
|
|
|
|
</style>
|
|
|
|
|
${html}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
setRenderedContent(htmlContent);
|
|
|
|
|
return true;
|
|
|
|
|
} else if (fileExt === "doc") {
|
|
|
|
|
// .doc 파일은 .docx로 변환 안내
|
|
|
|
|
const htmlContent = `
|
|
|
|
|
<div style="text-align: center; padding: 40px;">
|
|
|
|
|
<h3 style="color: #333; margin-bottom: 15px;">📄 ${fileName}</h3>
|
|
|
|
|
<p style="color: #666; margin-bottom: 10px;">.doc 파일은 .docx로 변환 후 업로드해주세요.</p>
|
|
|
|
|
<p style="color: #666; font-size: 14px;">(.docx 파일만 미리보기 지원)</p>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
setRenderedContent(htmlContent);
|
|
|
|
|
return true;
|
|
|
|
|
} else if (["ppt", "pptx"].includes(fileExt)) {
|
|
|
|
|
// PowerPoint는 미리보기 불가 안내
|
|
|
|
|
const htmlContent = `
|
|
|
|
|
<div style="text-align: center; padding: 40px;">
|
|
|
|
|
<h3 style="color: #333; margin-bottom: 15px;">📑 ${fileName}</h3>
|
|
|
|
|
<p style="color: #666; margin-bottom: 10px;">PowerPoint 파일은 브라우저에서 미리보기할 수 없습니다.</p>
|
|
|
|
|
<p style="color: #666; font-size: 14px;">파일을 다운로드하여 확인해주세요.</p>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
setRenderedContent(htmlContent);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false; // 지원하지 않는 형식
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Office 문서 렌더링 오류:", error);
|
|
|
|
|
|
|
|
|
|
const htmlContent = `
|
|
|
|
|
<div style="color: red; text-align: center; padding: 20px;">
|
|
|
|
|
Office 문서를 읽을 수 없습니다.<br>
|
|
|
|
|
파일이 손상되었거나 지원하지 않는 형식일 수 있습니다.
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
setRenderedContent(htmlContent);
|
|
|
|
|
return true; // 오류 메시지라도 표시
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
// 파일이 변경될 때마다 미리보기 URL 생성
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!file || !isOpen) {
|
|
|
|
|
setPreviewUrl(null);
|
|
|
|
|
setPreviewError(null);
|
2025-09-29 13:29:03 +09:00
|
|
|
setRenderedContent(null);
|
2025-09-26 13:11:34 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
setPreviewError(null);
|
|
|
|
|
|
|
|
|
|
// 로컬 파일인 경우
|
|
|
|
|
if (file._file) {
|
|
|
|
|
const url = URL.createObjectURL(file._file);
|
|
|
|
|
setPreviewUrl(url);
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
|
|
|
|
return () => URL.revokeObjectURL(url);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
let cleanup: (() => void) | undefined;
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
// 서버 파일인 경우 - 미리보기 API 호출
|
|
|
|
|
const generatePreviewUrl = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const fileExt = file.fileExt.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// 미리보기 지원 파일 타입 정의
|
2025-09-29 13:29:03 +09:00
|
|
|
const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg"];
|
|
|
|
|
const documentExtensions = ["pdf","doc", "docx", "xls", "xlsx", "ppt", "pptx", "rtf", "odt", "ods", "odp", "hwp", "hwpx", "hwpml", "hcdt", "hpt", "pages", "numbers", "keynote"];
|
|
|
|
|
const textExtensions = ["txt", "md", "json", "xml", "csv"];
|
|
|
|
|
const mediaExtensions = ["mp4", "webm", "ogg", "mp3", "wav"];
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
|
|
|
const supportedExtensions = [
|
|
|
|
|
...imageExtensions,
|
|
|
|
|
...documentExtensions,
|
|
|
|
|
...textExtensions,
|
|
|
|
|
...mediaExtensions
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (supportedExtensions.includes(fileExt)) {
|
2025-09-29 13:29:03 +09:00
|
|
|
// 이미지나 PDF는 인증된 요청으로 Blob 생성
|
|
|
|
|
if (imageExtensions.includes(fileExt) || fileExt === "pdf") {
|
|
|
|
|
try {
|
|
|
|
|
// 인증된 요청으로 파일 데이터 가져오기
|
|
|
|
|
const response = await fetch(`${API_BASE_URL}/files/preview/${file.objid}`, {
|
|
|
|
|
headers: {
|
|
|
|
|
"Authorization": `Bearer ${localStorage.getItem("authToken")}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
|
|
|
setPreviewUrl(blobUrl);
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 언마운트 시 URL 정리를 위해 cleanup 함수 저장
|
|
|
|
|
cleanup = () => URL.revokeObjectURL(blobUrl);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("파일 미리보기 로드 실패:", error);
|
|
|
|
|
setPreviewError("파일을 불러올 수 없습니다. 권한을 확인해주세요.");
|
|
|
|
|
}
|
|
|
|
|
} else if (documentExtensions.includes(fileExt)) {
|
|
|
|
|
// Office 문서는 OnlyOffice 또는 안정적인 뷰어 사용
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE_URL}/files/preview/${file.objid}`, {
|
|
|
|
|
headers: {
|
|
|
|
|
'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
|
|
|
|
|
|
|
|
// Office 문서를 위한 특별한 처리 - CDN 라이브러리 사용
|
|
|
|
|
if (["doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(fileExt)) {
|
|
|
|
|
// CDN 라이브러리로 클라이언트 사이드 렌더링 시도
|
|
|
|
|
try {
|
|
|
|
|
const renderSuccess = await renderOfficeDocument(blob, fileExt, file.realFileName);
|
|
|
|
|
|
|
|
|
|
if (!renderSuccess) {
|
|
|
|
|
// 렌더링 실패 시 Blob URL 사용
|
|
|
|
|
setPreviewUrl(blobUrl);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Office 문서 렌더링 중 오류:", error);
|
|
|
|
|
// 오류 발생 시 Blob URL 사용
|
|
|
|
|
setPreviewUrl(blobUrl);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 기타 문서는 직접 Blob URL 사용
|
|
|
|
|
setPreviewUrl(blobUrl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => URL.revokeObjectURL(blobUrl); // Cleanup function
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Office 문서 로드 실패:", error);
|
|
|
|
|
// 오류 발생 시 다운로드 옵션 제공
|
|
|
|
|
setPreviewError(`${fileExt.toUpperCase()} 문서를 미리보기할 수 없습니다. 다운로드하여 확인해주세요.`);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 기타 파일은 다운로드 URL 사용
|
|
|
|
|
const url = `${API_BASE_URL.replace("/api", "")}/api/files/download/${file.objid}`;
|
|
|
|
|
setPreviewUrl(url);
|
|
|
|
|
}
|
2025-09-26 13:11:34 +09:00
|
|
|
} else {
|
|
|
|
|
// 지원하지 않는 파일 타입
|
|
|
|
|
setPreviewError(`${file.fileExt.toUpperCase()} 파일은 미리보기를 지원하지 않습니다.`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-09-29 13:29:03 +09:00
|
|
|
console.error("미리보기 URL 생성 오류:", error);
|
|
|
|
|
setPreviewError("미리보기를 불러오는데 실패했습니다.");
|
2025-09-26 13:11:34 +09:00
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
generatePreviewUrl();
|
2025-09-29 13:29:03 +09:00
|
|
|
|
|
|
|
|
// cleanup 함수 반환
|
|
|
|
|
return () => {
|
|
|
|
|
if (cleanup) {
|
|
|
|
|
cleanup();
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-09-26 13:11:34 +09:00
|
|
|
}, [file, isOpen]);
|
|
|
|
|
|
|
|
|
|
if (!file) return null;
|
|
|
|
|
|
|
|
|
|
// 파일 타입별 미리보기 컴포넌트
|
|
|
|
|
const renderPreview = () => {
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center h-96">
|
|
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (previewError) {
|
|
|
|
|
return (
|
2025-09-29 13:29:03 +09:00
|
|
|
<div className="flex flex-col items-center justify-center h-96">
|
|
|
|
|
<AlertTriangle className="w-16 h-16 mb-4 text-yellow-500" />
|
2025-09-26 13:11:34 +09:00
|
|
|
<p className="text-lg font-medium mb-2">미리보기 불가</p>
|
|
|
|
|
<p className="text-sm text-center">{previewError}</p>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onDownload?.(file)}
|
|
|
|
|
className="mt-4"
|
|
|
|
|
>
|
|
|
|
|
<Download className="w-4 h-4 mr-2" />
|
|
|
|
|
파일 다운로드
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fileExt = file.fileExt.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// 이미지 파일
|
2025-09-29 13:29:03 +09:00
|
|
|
if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(fileExt)) {
|
2025-09-26 13:11:34 +09:00
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center max-h-96 overflow-hidden">
|
|
|
|
|
<img
|
2025-09-29 13:29:03 +09:00
|
|
|
src={previewUrl || ""}
|
2025-09-26 13:11:34 +09:00
|
|
|
alt={file.realFileName}
|
2025-09-29 13:29:03 +09:00
|
|
|
className="max-w-full max-h-full object-contain rounded-lg shadow-lg"
|
|
|
|
|
onError={(e) => {
|
|
|
|
|
console.error("이미지 로드 오류:", previewUrl, e);
|
|
|
|
|
setPreviewError("이미지를 불러올 수 없습니다. 파일이 손상되었거나 서버에서 접근할 수 없습니다.");
|
|
|
|
|
}}
|
|
|
|
|
onLoad={() => {
|
|
|
|
|
console.log("이미지 로드 성공:", previewUrl);
|
|
|
|
|
}}
|
2025-09-26 13:11:34 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 텍스트 파일
|
2025-09-29 13:29:03 +09:00
|
|
|
if (["txt", "md", "json", "xml", "csv"].includes(fileExt)) {
|
2025-09-26 13:11:34 +09:00
|
|
|
return (
|
|
|
|
|
<div className="h-96 overflow-auto">
|
|
|
|
|
<iframe
|
2025-09-29 13:29:03 +09:00
|
|
|
src={previewUrl || ""}
|
2025-09-26 13:11:34 +09:00
|
|
|
className="w-full h-full border rounded-lg"
|
2025-09-29 13:29:03 +09:00
|
|
|
onError={() => setPreviewError("텍스트 파일을 불러올 수 없습니다.")}
|
2025-09-26 13:11:34 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PDF 파일
|
2025-09-29 13:29:03 +09:00
|
|
|
if (fileExt === "pdf") {
|
2025-09-26 13:11:34 +09:00
|
|
|
return (
|
2025-09-29 13:29:03 +09:00
|
|
|
<div className="h-96 overflow-auto">
|
2025-09-26 13:11:34 +09:00
|
|
|
<iframe
|
2025-09-29 13:29:03 +09:00
|
|
|
src={previewUrl || ""}
|
2025-09-26 13:11:34 +09:00
|
|
|
className="w-full h-full border rounded-lg"
|
2025-09-29 13:29:03 +09:00
|
|
|
onError={() => setPreviewError("PDF 파일을 불러올 수 없습니다.")}
|
2025-09-26 13:11:34 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
// Office 문서 (CDN 라이브러리 렌더링 또는 iframe)
|
|
|
|
|
if (
|
|
|
|
|
["doc", "docx", "xls", "xlsx", "ppt", "pptx", "hwp", "hwpx", "hwpml", "hcdt", "hpt", "pages", "numbers", "keynote"].includes(fileExt)
|
|
|
|
|
) {
|
|
|
|
|
// CDN 라이브러리로 렌더링된 콘텐츠가 있는 경우
|
|
|
|
|
if (renderedContent) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="relative h-96 overflow-auto">
|
|
|
|
|
<div
|
|
|
|
|
className="w-full h-full p-4 border rounded-lg bg-white"
|
|
|
|
|
dangerouslySetInnerHTML={{ __html: renderedContent }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-09-26 13:11:34 +09:00
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
// iframe 방식 (fallback)
|
2025-09-26 13:11:34 +09:00
|
|
|
return (
|
2025-09-29 13:29:03 +09:00
|
|
|
<div className="relative h-96 overflow-auto">
|
2025-09-26 13:11:34 +09:00
|
|
|
<iframe
|
2025-09-29 13:29:03 +09:00
|
|
|
src={previewUrl || ""}
|
2025-09-26 13:11:34 +09:00
|
|
|
className="w-full h-full border rounded-lg"
|
2025-09-29 13:29:03 +09:00
|
|
|
onError={() => {
|
|
|
|
|
console.log("iframe 오류 발생, fallback 옵션 제공");
|
|
|
|
|
setPreviewError("이 Office 문서는 브라우저에서 직접 미리보기할 수 없습니다. 다운로드하여 확인해주세요.");
|
|
|
|
|
}}
|
2025-09-26 13:11:34 +09:00
|
|
|
title={`${file.realFileName} 미리보기`}
|
2025-09-29 13:29:03 +09:00
|
|
|
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
|
|
|
|
onLoad={() => setIsLoading(false)}
|
2025-09-26 13:11:34 +09:00
|
|
|
/>
|
2025-09-29 13:29:03 +09:00
|
|
|
|
|
|
|
|
{/* 로딩 상태 */}
|
|
|
|
|
{isLoading && (
|
|
|
|
|
<div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-90">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
|
|
|
|
<p className="text-sm text-gray-600">Office 문서를 처리하는 중...</p>
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">잠시만 기다려주세요</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 오류 발생 시 fallback 옵션 */}
|
|
|
|
|
{previewError && (
|
|
|
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-white">
|
|
|
|
|
<FileText className="w-16 h-16 mb-4 text-orange-500" />
|
|
|
|
|
<p className="text-lg font-medium mb-2">미리보기 제한</p>
|
|
|
|
|
<p className="text-sm text-center mb-4 text-gray-600">
|
|
|
|
|
{previewError}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onDownload?.(file)}
|
|
|
|
|
>
|
|
|
|
|
<Download className="w-4 h-4 mr-2" />
|
|
|
|
|
다운로드
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
// 새 탭에서 파일 열기 시도
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
link.href = previewUrl || '';
|
|
|
|
|
link.target = '_blank';
|
|
|
|
|
link.click();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ExternalLink className="w-4 h-4 mr-2" />
|
|
|
|
|
새 탭에서 열기
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-26 13:11:34 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 비디오 파일
|
2025-09-29 13:29:03 +09:00
|
|
|
if (["mp4", "webm", "ogg"].includes(fileExt)) {
|
2025-09-26 13:11:34 +09:00
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center">
|
|
|
|
|
<video
|
|
|
|
|
controls
|
2025-09-29 13:29:03 +09:00
|
|
|
className="w-full max-h-96"
|
|
|
|
|
onError={() => setPreviewError("비디오를 재생할 수 없습니다.")}
|
2025-09-26 13:11:34 +09:00
|
|
|
>
|
2025-09-29 13:29:03 +09:00
|
|
|
<source src={previewUrl || ""} type={`video/${fileExt}`} />
|
2025-09-26 13:11:34 +09:00
|
|
|
</video>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 오디오 파일
|
2025-09-29 13:29:03 +09:00
|
|
|
if (["mp3", "wav", "ogg"].includes(fileExt)) {
|
2025-09-26 13:11:34 +09:00
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-96">
|
|
|
|
|
<div className="w-32 h-32 bg-gray-100 rounded-full flex items-center justify-center mb-6">
|
|
|
|
|
<svg className="w-16 h-16 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
2025-09-29 13:29:03 +09:00
|
|
|
<path
|
|
|
|
|
fillRule="evenodd"
|
|
|
|
|
d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM15.657 6.343a1 1 0 011.414 0A9.972 9.972 0 0119 12a9.972 9.972 0 01-1.929 5.657 1 1 0 11-1.414-1.414A7.971 7.971 0 0017 12c0-1.594-.471-3.078-1.343-4.343a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 12a5.984 5.984 0 01-.757 2.829 1 1 0 01-1.415-1.414A3.987 3.987 0 0013 12a3.988 3.988 0 00-.172-1.171 1 1 0 010-1.414z"
|
|
|
|
|
clipRule="evenodd"
|
|
|
|
|
/>
|
2025-09-26 13:11:34 +09:00
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<audio
|
|
|
|
|
controls
|
|
|
|
|
className="w-full max-w-md"
|
2025-09-29 13:29:03 +09:00
|
|
|
onError={() => setPreviewError("오디오를 재생할 수 없습니다.")}
|
2025-09-26 13:11:34 +09:00
|
|
|
>
|
2025-09-29 13:29:03 +09:00
|
|
|
<source src={previewUrl || ""} type={`audio/${fileExt}`} />
|
2025-09-26 13:11:34 +09:00
|
|
|
</audio>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기타 파일 타입
|
|
|
|
|
return (
|
2025-09-29 13:29:03 +09:00
|
|
|
<div className="flex flex-col items-center justify-center h-96">
|
|
|
|
|
<FileText className="w-16 h-16 mb-4 text-gray-400" />
|
2025-09-26 13:11:34 +09:00
|
|
|
<p className="text-lg font-medium mb-2">미리보기 불가</p>
|
|
|
|
|
<p className="text-sm text-center mb-4">
|
|
|
|
|
{file.fileExt.toUpperCase()} 파일은 미리보기를 지원하지 않습니다.
|
|
|
|
|
</p>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onDownload?.(file)}
|
|
|
|
|
>
|
|
|
|
|
<Download className="w-4 h-4 mr-2" />
|
|
|
|
|
파일 다운로드
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-09-29 13:29:03 +09:00
|
|
|
<Dialog open={isOpen} onOpenChange={() => {}}>
|
|
|
|
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
2025-09-26 13:11:34 +09:00
|
|
|
<DialogHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
<DialogTitle className="text-lg font-semibold truncate">
|
|
|
|
|
{file.realFileName}
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
|
|
|
{file.fileExt.toUpperCase()}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-09-29 13:29:03 +09:00
|
|
|
<DialogDescription>
|
|
|
|
|
파일 크기: {formatFileSize(file.size)} | 파일 형식: {file.fileExt.toUpperCase()}
|
|
|
|
|
</DialogDescription>
|
2025-09-26 13:11:34 +09:00
|
|
|
</DialogHeader>
|
2025-09-29 13:29:03 +09:00
|
|
|
|
|
|
|
|
<div className="flex-1 overflow-y-auto">
|
2025-09-26 13:11:34 +09:00
|
|
|
{renderPreview()}
|
|
|
|
|
</div>
|
2025-09-29 13:29:03 +09:00
|
|
|
|
|
|
|
|
{/* 파일 정보 및 액션 버튼 */}
|
|
|
|
|
<div className="flex items-center space-x-4 text-sm text-gray-500 mt-2">
|
|
|
|
|
<span>크기: {formatFileSize(file.size)}</span>
|
|
|
|
|
{file.uploadedAt && (
|
|
|
|
|
<span>업로드: {new Date(file.uploadedAt).toLocaleString()}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-end space-x-2 pt-4 border-t">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => onDownload?.(file)}
|
|
|
|
|
>
|
|
|
|
|
<Download className="w-4 h-4 mr-2" />
|
|
|
|
|
다운로드
|
|
|
|
|
</Button>
|
|
|
|
|
{onDelete && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
|
|
|
onClick={() => onDelete(file)}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
|
|
|
삭제
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4 mr-2" />
|
|
|
|
|
닫기
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-09-26 13:11:34 +09:00
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
2025-09-29 13:29:03 +09:00
|
|
|
};
|