summaryrefslogtreecommitdiff
path: root/components/file-manager/SharedFileViewer.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/file-manager/SharedFileViewer.tsx')
-rw-r--r--components/file-manager/SharedFileViewer.tsx411
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