diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-25 03:28:27 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-25 03:28:27 +0000 |
| commit | 4c2d4c235bd80368e31cae9c375e9a585f6a6844 (patch) | |
| tree | 7fd1847e1e30ef2052281453bfb7a1c45ac6627a /components/file-manager/SharedFileViewer.tsx | |
| parent | f69e125f1a0b47bbc22e2784208bf829bcdd24f8 (diff) | |
(대표님) archiver 추가, 데이터룸구현
Diffstat (limited to 'components/file-manager/SharedFileViewer.tsx')
| -rw-r--r-- | components/file-manager/SharedFileViewer.tsx | 411 |
1 files changed, 411 insertions, 0 deletions
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<SharedFile | null>(null); + const [accessLevel, setAccessLevel] = useState<string>(''); + const [passwordRequired, setPasswordRequired] = useState(false); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(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 ( + <div className="min-h-screen flex items-center justify-center"> + <div className="text-center"> + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4" /> + <p className="text-muted-foreground">파일 정보를 불러오는 중...</p> + </div> + </div> + ); + } + + if (error && !passwordRequired) { + return ( + <div className="min-h-screen flex items-center justify-center p-4"> + <Card className="max-w-md w-full"> + <CardHeader> + <div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4"> + <AlertCircle className="h-6 w-6 text-red-600" /> + </div> + <CardTitle className="text-center">접근할 수 없습니다</CardTitle> + </CardHeader> + <CardContent> + <Alert variant="destructive"> + <AlertDescription>{error}</AlertDescription> + </Alert> + </CardContent> + </Card> + </div> + ); + } + + if (passwordRequired && !showContent) { + return ( + <div className="min-h-screen flex items-center justify-center p-4"> + <Card className="max-w-md w-full"> + <CardHeader> + <div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4"> + <Lock className="h-6 w-6 text-blue-600" /> + </div> + <CardTitle className="text-center">비밀번호 입력</CardTitle> + <CardDescription className="text-center"> + 이 파일은 비밀번호로 보호되어 있습니다 + </CardDescription> + </CardHeader> + <CardContent> + <form onSubmit={handlePasswordSubmit} className="space-y-4"> + <div> + <Label htmlFor="password">비밀번호</Label> + <Input + id="password" + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="비밀번호를 입력하세요" + autoFocus + /> + </div> + {error && ( + <Alert variant="destructive"> + <AlertDescription>{error}</AlertDescription> + </Alert> + )} + <Button type="submit" className="w-full"> + <Lock className="h-4 w-4 mr-2" /> + 확인 + </Button> + </form> + </CardContent> + </Card> + </div> + ); + } + + if (!file) return null; + + const FileIcon = getFileIcon(file.mimeType, file.name); + + return ( + <div className="min-h-screen bg-gray-50"> + {/* 헤더 */} + <div className="bg-white border-b"> + <div className="container mx-auto px-4 py-4"> + <div className="flex items-center gap-3"> + <div className="h-10 w-10 bg-blue-600 rounded-lg flex items-center justify-center"> + <span className="text-white font-bold">FM</span> + </div> + <div> + <h1 className="text-lg font-semibold">공유된 파일</h1> + <p className="text-sm text-muted-foreground">File Manager Shared</p> + </div> + </div> + </div> + </div> + + {/* 메인 컨텐츠 */} + <div className="container mx-auto px-4 py-8 max-w-4xl"> + <Card> + <CardHeader> + <div className="flex items-start justify-between"> + <div className="flex items-start gap-4"> + <div className={cn( + "h-16 w-16 rounded-lg flex items-center justify-center", + "bg-gradient-to-br from-blue-50 to-blue-100" + )}> + <FileIcon className="h-8 w-8 text-blue-600" /> + </div> + <div> + <CardTitle className="text-2xl">{file.name}</CardTitle> + <CardDescription className="mt-1"> + {file.type === 'folder' ? '폴더' : formatFileSize(file.size)} + </CardDescription> + </div> + </div> + + <div className="flex items-center gap-2"> + {accessLevel === 'view_only' && ( + <Badge variant="secondary"> + <Eye className="h-3 w-3 mr-1" /> + 보기 전용 + </Badge> + )} + {accessLevel === 'view_download' && ( + <Badge variant="default"> + <Download className="h-3 w-3 mr-1" /> + 다운로드 가능 + </Badge> + )} + </div> + </div> + </CardHeader> + + <CardContent> + <div className="space-y-6"> + {/* 파일 정보 */} + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-3"> + <div> + <p className="text-sm text-muted-foreground mb-1">파일 유형</p> + <p className="font-medium">{file.mimeType || '알 수 없음'}</p> + </div> + <div> + <p className="text-sm text-muted-foreground mb-1">생성일</p> + <p className="font-medium flex items-center gap-1"> + <Calendar className="h-4 w-4" /> + {new Date(file.createdAt).toLocaleDateString()} + </p> + </div> + </div> + + <div className="space-y-3"> + <div> + <p className="text-sm text-muted-foreground mb-1">카테고리</p> + <Badge variant="outline">{file.category}</Badge> + </div> + <div> + <p className="text-sm text-muted-foreground mb-1">수정일</p> + <p className="font-medium flex items-center gap-1"> + <Clock className="h-4 w-4" /> + {new Date(file.updatedAt).toLocaleDateString()} + </p> + </div> + </div> + </div> + + <Separator /> + + {/* 미리보기 영역 (이미지인 경우) */} + {file.mimeType?.startsWith('image/') && accessLevel !== 'download_only' && ( + <div className="bg-gray-50 rounded-lg p-4"> + <p className="text-sm text-muted-foreground mb-3">미리보기</p> + <div className="bg-white rounded border p-4"> + <img + src={`/api/shared/${token}/preview`} + alt={file.name} + className="max-w-full h-auto rounded" + /> + </div> + </div> + )} + + {/* 액션 버튼 */} + <div className="flex gap-3"> + {accessLevel === 'view_download' && ( + <Button + onClick={handleDownload} + disabled={downloading} + className="flex-1" + > + {downloading ? ( + <> + <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" /> + 다운로드 중... + </> + ) : ( + <> + <Download className="h-4 w-4 mr-2" /> + 파일 다운로드 + </> + )} + </Button> + )} + + {accessLevel === 'view_only' && ( + <Alert className="flex-1"> + <Eye className="h-4 w-4" /> + <AlertDescription> + 이 파일은 보기 전용입니다. 다운로드할 수 없습니다. + </AlertDescription> + </Alert> + )} + </div> + + {/* 보안 안내 */} + <Alert> + <Lock className="h-4 w-4" /> + <AlertDescription> + 이 링크는 보안을 위해 제한된 시간 동안만 유효합니다. + 필요한 경우 파일을 다운로드하여 보관하세요. + </AlertDescription> + </Alert> + </div> + </CardContent> + </Card> + + {/* 하단 정보 */} + <div className="mt-6 text-center text-sm text-muted-foreground"> + <p>© 2024 File Manager. All rights reserved.</p> + <p className="mt-1"> + 문제가 있으신가요?{' '} + <a href="/support" className="text-primary hover:underline"> + 고객 지원 + </a> + </p> + </div> + </div> + </div> + ); +}
\ No newline at end of file |
