From 4c2d4c235bd80368e31cae9c375e9a585f6a6844 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<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
-- 
cgit v1.2.3

