311 lines
11 KiB
TypeScript
311 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { FileInfo } from "./types";
|
|
import { Download, X, AlertTriangle, FileText, Image as ImageIcon } from "lucide-react";
|
|
import { formatFileSize } from "@/lib/utils";
|
|
|
|
interface FileViewerModalProps {
|
|
file: FileInfo | null;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onDownload?: (file: FileInfo) => void;
|
|
}
|
|
|
|
/**
|
|
* 파일 뷰어 모달 컴포넌트
|
|
* 다양한 파일 타입에 대한 미리보기 기능 제공
|
|
*/
|
|
export const FileViewerModal: React.FC<FileViewerModalProps> = ({
|
|
file,
|
|
isOpen,
|
|
onClose,
|
|
onDownload,
|
|
}) => {
|
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
const [previewError, setPreviewError] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// 파일이 변경될 때마다 미리보기 URL 생성
|
|
useEffect(() => {
|
|
if (!file || !isOpen) {
|
|
setPreviewUrl(null);
|
|
setPreviewError(null);
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setPreviewError(null);
|
|
|
|
// 로컬 파일인 경우
|
|
if (file._file) {
|
|
const url = URL.createObjectURL(file._file);
|
|
setPreviewUrl(url);
|
|
setIsLoading(false);
|
|
|
|
return () => URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// 서버 파일인 경우 - 미리보기 API 호출
|
|
const generatePreviewUrl = async () => {
|
|
try {
|
|
const fileExt = file.fileExt.toLowerCase();
|
|
|
|
// 미리보기 지원 파일 타입 정의
|
|
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'];
|
|
|
|
const supportedExtensions = [
|
|
...imageExtensions,
|
|
...documentExtensions,
|
|
...textExtensions,
|
|
...mediaExtensions
|
|
];
|
|
|
|
if (supportedExtensions.includes(fileExt)) {
|
|
// 실제 환경에서는 파일 서빙 API 엔드포인트 사용
|
|
const url = `/api/files/preview/${file.objid}`;
|
|
setPreviewUrl(url);
|
|
} else {
|
|
// 지원하지 않는 파일 타입
|
|
setPreviewError(`${file.fileExt.toUpperCase()} 파일은 미리보기를 지원하지 않습니다.`);
|
|
}
|
|
} catch (error) {
|
|
console.error('미리보기 URL 생성 오류:', error);
|
|
setPreviewError('미리보기를 불러오는데 실패했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
generatePreviewUrl();
|
|
}, [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 (
|
|
<div className="flex flex-col items-center justify-center h-96 text-gray-500">
|
|
<AlertTriangle className="w-16 h-16 mb-4" />
|
|
<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();
|
|
|
|
// 이미지 파일
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(fileExt)) {
|
|
return (
|
|
<div className="flex items-center justify-center max-h-96 overflow-hidden">
|
|
<img
|
|
src={previewUrl || ''}
|
|
alt={file.realFileName}
|
|
className="max-w-full max-h-full object-contain rounded-lg"
|
|
onError={() => setPreviewError('이미지를 불러올 수 없습니다.')}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 텍스트 파일
|
|
if (['txt', 'md', 'json', 'xml', 'csv'].includes(fileExt)) {
|
|
return (
|
|
<div className="h-96 overflow-auto">
|
|
<iframe
|
|
src={previewUrl || ''}
|
|
className="w-full h-full border rounded-lg"
|
|
title={`${file.realFileName} 미리보기`}
|
|
onError={() => setPreviewError('텍스트 파일을 불러올 수 없습니다.')}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// PDF 파일
|
|
if (fileExt === 'pdf') {
|
|
return (
|
|
<div className="h-96">
|
|
<iframe
|
|
src={previewUrl || ''}
|
|
className="w-full h-full border rounded-lg"
|
|
title={`${file.realFileName} 미리보기`}
|
|
onError={() => setPreviewError('PDF 파일을 불러올 수 없습니다.')}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Microsoft Office, 한컴오피스, Apple iWork 문서 파일
|
|
if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'hwp', 'hwpx', 'hwpml', 'hcdt', 'hpt', 'pages', 'numbers', 'keynote'].includes(fileExt)) {
|
|
// Office 파일은 Google Docs Viewer 또는 Microsoft Office Online을 통해 미리보기
|
|
const officeViewerUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(previewUrl || '')}`;
|
|
|
|
return (
|
|
<div className="h-96">
|
|
<iframe
|
|
src={officeViewerUrl}
|
|
className="w-full h-full border rounded-lg"
|
|
title={`${file.realFileName} 미리보기`}
|
|
onError={() => setPreviewError('Office 문서를 불러올 수 없습니다. 파일을 다운로드하여 확인해주세요.')}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 기타 문서 파일 (RTF, ODT 등)
|
|
if (['rtf', 'odt', 'ods', 'odp'].includes(fileExt)) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-96 text-gray-500">
|
|
<FileText className="w-16 h-16 mb-4 text-blue-500" />
|
|
<p className="text-lg font-medium mb-2">{file.fileExt.toUpperCase()} 문서</p>
|
|
<p className="text-sm text-center mb-4">
|
|
{file.realFileName}
|
|
</p>
|
|
<div className="flex flex-col items-center space-y-2 text-xs text-gray-400">
|
|
<p>파일 크기: {formatFileSize(file.fileSize)}</p>
|
|
<p>문서 타입: {file.docTypeName || '일반 문서'}</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => onDownload?.(file)}
|
|
className="mt-4"
|
|
>
|
|
<Download className="w-4 h-4 mr-2" />
|
|
파일 다운로드
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 비디오 파일
|
|
if (['mp4', 'webm', 'ogg'].includes(fileExt)) {
|
|
return (
|
|
<div className="flex items-center justify-center">
|
|
<video
|
|
controls
|
|
className="max-w-full max-h-96 rounded-lg"
|
|
onError={() => setPreviewError('비디오를 재생할 수 없습니다.')}
|
|
>
|
|
<source src={previewUrl || ''} type={`video/${fileExt}`} />
|
|
브라우저가 비디오 재생을 지원하지 않습니다.
|
|
</video>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 오디오 파일
|
|
if (['mp3', 'wav', 'ogg'].includes(fileExt)) {
|
|
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">
|
|
<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" />
|
|
</svg>
|
|
</div>
|
|
<audio
|
|
controls
|
|
className="w-full max-w-md"
|
|
onError={() => setPreviewError('오디오를 재생할 수 없습니다.')}
|
|
>
|
|
<source src={previewUrl || ''} type={`audio/${fileExt}`} />
|
|
브라우저가 오디오 재생을 지원하지 않습니다.
|
|
</audio>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 기타 파일 타입
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-96 text-gray-500">
|
|
<FileText className="w-16 h-16 mb-4" />
|
|
<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 (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden">
|
|
<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 className="flex items-center space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onDownload?.(file)}
|
|
>
|
|
<Download className="w-4 h-4 mr-2" />
|
|
다운로드
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onClose}
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 파일 정보 */}
|
|
<div className="flex items-center space-x-4 text-sm text-gray-500 mt-2">
|
|
<span>크기: {formatFileSize(file.fileSize)}</span>
|
|
{file.uploadedAt && (
|
|
<span>업로드: {new Date(file.uploadedAt).toLocaleString()}</span>
|
|
)}
|
|
{file.writer && <span>작성자: {file.writer}</span>}
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
{/* 파일 미리보기 영역 */}
|
|
<div className="flex-1 overflow-auto py-4">
|
|
{renderPreview()}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|