From 4c2d4c235bd80368e31cae9c375e9a585f6a6844 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 25 Sep 2025 03:28:27 +0000 Subject: (대표님) archiver 추가, 데이터룸구현 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/file-manager/SharedFileViewer.tsx | 411 +++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 components/file-manager/SharedFileViewer.tsx (limited to 'components/file-manager/SharedFileViewer.tsx') diff --git a/components/file-manager/SharedFileViewer.tsx b/components/file-manager/SharedFileViewer.tsx new file mode 100644 index 00000000..a6e4eef5 --- /dev/null +++ b/components/file-manager/SharedFileViewer.tsx @@ -0,0 +1,411 @@ +// components/file-manager/SharedFileViewer.tsx +'use client'; + +import { useState, useEffect } from 'react'; +import { + Download, + Eye, + EyeOff, + FileText, + Image, + Film, + Music, + Archive, + Code, + File, + Lock, + AlertCircle, + Calendar, + Clock, + User +} from 'lucide-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { useToast } from '@/hooks/use-toast'; +import { cn } from '@/lib/utils'; + +interface SharedFile { + id: string; + name: string; + type: 'file' | 'folder'; + size: number; + mimeType?: string; + category: string; + createdAt: string; + updatedAt: string; +} + +interface SharedFileViewerProps { + token: string; +} + +export function SharedFileViewer({ token }: SharedFileViewerProps) { + const [file, setFile] = useState(null); + const [accessLevel, setAccessLevel] = useState(''); + const [passwordRequired, setPasswordRequired] = useState(false); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [showContent, setShowContent] = useState(false); + const [downloading, setDownloading] = useState(false); + + const { toast } = useToast(); + + useEffect(() => { + // 초기 접근 시도 + checkAccess(); + }, [token]); + + const checkAccess = async (pwd?: string) => { + setLoading(true); + setError(null); + + try { + const params = new URLSearchParams(); + if (pwd) params.append('password', pwd); + + const response = await fetch(`/api/shared/${token}?${params}`); + const data = await response.json(); + + if (!response.ok) { + if (data.error?.includes('비밀번호')) { + setPasswordRequired(true); + setError('비밀번호가 필요합니다'); + } else if (data.error?.includes('만료')) { + setError('이 공유 링크는 만료되었습니다'); + } else if (data.error?.includes('최대 다운로드')) { + setError('최대 다운로드 횟수를 초과했습니다'); + } else { + setError(data.error || '파일에 접근할 수 없습니다'); + } + return; + } + + setFile(data.file); + setAccessLevel(data.accessLevel); + setShowContent(true); + setPasswordRequired(false); + } catch (err) { + setError('파일을 불러오는 중 오류가 발생했습니다'); + } finally { + setLoading(false); + } + }; + + const handlePasswordSubmit = (e: React.FormEvent) => { + e.preventDefault(); + checkAccess(password); + }; + + const handleDownload = async () => { + if (!file || accessLevel !== 'view_download') return; + + setDownloading(true); + try { + const response = await fetch(`/api/shared/${token}/download`, { + method: 'POST', + headers: password ? { 'X-Share-Password': password } : {}, + }); + + if (!response.ok) { + throw new Error('다운로드 실패'); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = file.name; + document.body.appendChild(a); + a.click(); + + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + toast({ + title: '다운로드 완료', + description: `${file.name} 파일이 다운로드되었습니다.`, + }); + } catch (error) { + toast({ + title: '다운로드 실패', + description: '파일 다운로드 중 오류가 발생했습니다.', + variant: 'destructive', + }); + } finally { + setDownloading(false); + } + }; + + const getFileIcon = (mimeType?: string, name?: string) => { + if (!mimeType && name) { + const ext = name.split('.').pop()?.toLowerCase(); + if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext || '')) return Image; + if (['mp4', 'avi', 'mov', 'wmv'].includes(ext || '')) return Film; + if (['mp3', 'wav', 'flac'].includes(ext || '')) return Music; + if (['zip', 'rar', '7z', 'tar'].includes(ext || '')) return Archive; + if (['js', 'ts', 'py', 'java', 'cpp'].includes(ext || '')) return Code; + if (['pdf', 'doc', 'docx', 'txt'].includes(ext || '')) return FileText; + } + + if (mimeType?.startsWith('image/')) return Image; + if (mimeType?.startsWith('video/')) return Film; + if (mimeType?.startsWith('audio/')) return Music; + if (mimeType?.includes('zip') || mimeType?.includes('compressed')) return Archive; + if (mimeType?.includes('pdf')) return FileText; + + return File; + }; + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + if (loading) { + return ( +
+
+
+

파일 정보를 불러오는 중...

+
+
+ ); + } + + if (error && !passwordRequired) { + return ( +
+ + +
+ +
+ 접근할 수 없습니다 +
+ + + {error} + + +
+
+ ); + } + + if (passwordRequired && !showContent) { + return ( +
+ + +
+ +
+ 비밀번호 입력 + + 이 파일은 비밀번호로 보호되어 있습니다 + +
+ +
+
+ + setPassword(e.target.value)} + placeholder="비밀번호를 입력하세요" + autoFocus + /> +
+ {error && ( + + {error} + + )} + +
+
+
+
+ ); + } + + if (!file) return null; + + const FileIcon = getFileIcon(file.mimeType, file.name); + + return ( +
+ {/* 헤더 */} +
+
+
+
+ FM +
+
+

공유된 파일

+

File Manager Shared

+
+
+
+
+ + {/* 메인 컨텐츠 */} +
+ + +
+
+
+ +
+
+ {file.name} + + {file.type === 'folder' ? '폴더' : formatFileSize(file.size)} + +
+
+ +
+ {accessLevel === 'view_only' && ( + + + 보기 전용 + + )} + {accessLevel === 'view_download' && ( + + + 다운로드 가능 + + )} +
+
+
+ + +
+ {/* 파일 정보 */} +
+
+
+

파일 유형

+

{file.mimeType || '알 수 없음'}

+
+
+

생성일

+

+ + {new Date(file.createdAt).toLocaleDateString()} +

+
+
+ +
+
+

카테고리

+ {file.category} +
+
+

수정일

+

+ + {new Date(file.updatedAt).toLocaleDateString()} +

+
+
+
+ + + + {/* 미리보기 영역 (이미지인 경우) */} + {file.mimeType?.startsWith('image/') && accessLevel !== 'download_only' && ( +
+

미리보기

+
+ {file.name} +
+
+ )} + + {/* 액션 버튼 */} +
+ {accessLevel === 'view_download' && ( + + )} + + {accessLevel === 'view_only' && ( + + + + 이 파일은 보기 전용입니다. 다운로드할 수 없습니다. + + + )} +
+ + {/* 보안 안내 */} + + + + 이 링크는 보안을 위해 제한된 시간 동안만 유효합니다. + 필요한 경우 파일을 다운로드하여 보관하세요. + + +
+
+
+ + {/* 하단 정보 */} +
+

© 2024 File Manager. All rights reserved.

+

+ 문제가 있으신가요?{' '} + + 고객 지원 + +

+
+
+
+ ); +} \ No newline at end of file -- cgit v1.2.3