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/project/ProjectDashboard.tsx | 476 ++++++++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 components/project/ProjectDashboard.tsx (limited to 'components/project/ProjectDashboard.tsx') diff --git a/components/project/ProjectDashboard.tsx b/components/project/ProjectDashboard.tsx new file mode 100644 index 00000000..d9ec2e0c --- /dev/null +++ b/components/project/ProjectDashboard.tsx @@ -0,0 +1,476 @@ +// components/project/ProjectDashboard.tsx +'use client'; + +import React, { useState, useEffect } from 'react'; +import { + Crown, + Users, + Settings, + FolderOpen, + Shield, + UserPlus, + Trash2, + BarChart3, + Eye, + Download, + HardDrive, + UserCog, + Loader2 +} from 'lucide-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useToast } from '@/hooks/use-toast'; +import { useSession } from 'next-auth/react'; + +interface ProjectDashboardProps { + projectId: string; +} + +interface ProjectStats { + files: { + totalFiles: number; + totalSize: number; + publicFiles: number; + restrictedFiles: number; + confidentialFiles: number; + }; + members: { + totalMembers: number; + admins: number; + editors: number; + viewers: number; + }; + activity: { + views: number; + downloads: number; + uploads: number; + uniqueUsers: number; + }; +} + +export function ProjectDashboard({ projectId }: ProjectDashboardProps) { + const { data: session } = useSession(); + const [isOwner, setIsOwner] = useState(false); + const [projectRole, setProjectRole] = useState('viewer'); + const [stats, setStats] = useState(null); + const [members, setMembers] = useState([]); + const [loading, setLoading] = useState(true); + + console.log(stats) + + // 다이얼로그 상태 + const [addMemberOpen, setAddMemberOpen] = useState(false); + const [transferOwnershipOpen, setTransferOwnershipOpen] = useState(false); + const [newMemberEmail, setNewMemberEmail] = useState(''); + const [newMemberRole, setNewMemberRole] = useState('viewer'); + const [newOwnerId, setNewOwnerId] = useState(''); + + const { toast } = useToast(); + + // 프로젝트 정보 및 권한 확인 + useEffect(() => { + const fetchProjectData = async () => { + try { + // 권한 확인 + const accessRes = await fetch(`/api/projects/${projectId}/access`); + const accessData = await accessRes.json(); + setIsOwner(accessData.isOwner); + setProjectRole(accessData.role); + + // Owner인 경우 통계 가져오기 + if (accessData.isOwner) { + const statsRes = await fetch(`/api/projects/${projectId}/stats`); + const statsData = await statsRes.json(); + setStats(statsData); + } + + // 멤버 목록 가져오기 + const membersRes = await fetch(`/api/projects/${projectId}/members`); + const membersData = await membersRes.json(); + setMembers(membersData.member); + + } catch (error) { + console.error('프로젝트 데이터 로드 실패:', error); + } finally { + setLoading(false); + } + }; + + fetchProjectData(); + }, [projectId]); + + // 멤버 추가 + const handleAddMember = async () => { + try { + const response = await fetch(`/api/projects/${projectId}/members`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: newMemberEmail, + role: newMemberRole, + }), + }); + + if (!response.ok) { + throw new Error('멤버 추가 실패'); + } + + toast({ + title: '성공', + description: '새 멤버가 추가되었습니다.', + }); + + setAddMemberOpen(false); + // 멤버 목록 새로고침 + } catch (error) { + toast({ + title: '오류', + description: '멤버 추가에 실패했습니다.', + variant: 'destructive', + }); + } + }; + + // 소유권 이전 + const handleTransferOwnership = async () => { + try { + const response = await fetch(`/api/projects/${projectId}/transfer-ownership`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + newOwnerId: newOwnerId, + }), + }); + + if (!response.ok) { + throw new Error('소유권 이전 실패'); + } + + toast({ + title: '성공', + description: '프로젝트 소유권이 이전되었습니다.', + }); + + setTransferOwnershipOpen(false); + setIsOwner(false); + } catch (error) { + toast({ + title: '오류', + description: '소유권 이전에 실패했습니다.', + variant: 'destructive', + }); + } + }; + + const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const roleConfig = { + owner: { label: 'Owner', icon: Crown, color: 'text-yellow-500' }, + admin: { label: 'Admin', icon: Shield, color: 'text-blue-500' }, + editor: { label: 'Editor', icon: FolderOpen, color: 'text-green-500' }, + viewer: { label: 'Viewer', icon: Eye, color: 'text-gray-500' }, + }; + + if (loading) { + return ( +
+
+ +

프로젝트 정보를 불러오는 중...

+
+
+ ); + } + + return ( +
+ {/* 헤더 */} +
+
+

프로젝트 대시보드

+ + {roleConfig[projectRole as keyof typeof roleConfig].icon && + React.createElement(roleConfig[projectRole as keyof typeof roleConfig].icon, { + className: `h-3 w-3 ${roleConfig[projectRole as keyof typeof roleConfig].color}` + }) + } + {roleConfig[projectRole as keyof typeof roleConfig].label} + +
+ + {isOwner && ( +
+ + +
+ )} +
+ + {/* Owner 전용 통계 */} + {isOwner && stats && ( +
+ + + 총 파일 수 + + +
{stats.storage.fileCount}
+

+ {formatBytes(stats.storage.used)} +

+
+
+ + + + 멤버 + + +
{stats.users.total}
+
+ + 관리자 {stats.users.byRole.admins} + + + 편집자 {stats.users.byRole.editors} + +
+
+
+ + + + 조회수 (30일) + + +
{stats.activity.views}
+

+ 활성 사용자 {stats.users.active}명 +

+
+
+ + + + 다운로드 (30일) + + +
{stats.activity.downloads}
+

+ 업로드 {stats.activity.uploads}개 +

+
+
+
+ )} + + {/* 탭 컨텐츠 */} + + + 멤버 + {isOwner && ( + <> + 권한 관리 + 위험 영역 + + )} + + + + + + 프로젝트 멤버 + + 이 프로젝트에 접근할 수 있는 사용자 목록 + + + +
+ {members.map((member) => ( +
+
+
+ {member.user.name?.charAt(0).toUpperCase()} +
+
+

{member.user.name}

+

{member.user.email}

+
+
+ + {roleConfig[member.role as keyof typeof roleConfig].icon && + React.createElement(roleConfig[member.role as keyof typeof roleConfig].icon, { + className: `h-3 w-3 mr-1 ${roleConfig[member.role as keyof typeof roleConfig].color}` + }) + } + {roleConfig[member.role as keyof typeof roleConfig].label} + +
+ ))} +
+
+
+
+ + {isOwner && ( + + + + 위험 영역 + + 이 작업들은 되돌릴 수 없습니다. 신중하게 진행하세요. + + + +
+
+

소유권 이전

+

+ 프로젝트 소유권을 다른 멤버에게 이전합니다 +

+
+ +
+ +
+
+

프로젝트 삭제

+

+ 프로젝트와 모든 파일을 영구적으로 삭제합니다 +

+
+ +
+
+
+
+ )} +
+ + {/* 멤버 추가 다이얼로그 */} + + + + 멤버 추가 + + 프로젝트에 새 멤버를 추가합니다 + + + +
+
+ + setNewMemberEmail(e.target.value)} + placeholder="user@example.com" + /> +
+ +
+ + +
+
+ + + + + +
+
+ + {/* 소유권 이전 다이얼로그 */} + + + + 소유권 이전 + + 주의: 이 작업은 되돌릴 수 없습니다. 프로젝트의 모든 권한이 새 소유자에게 이전됩니다. + + + +
+ + +
+ + + + + +
+
+
+ ); +} \ No newline at end of file -- cgit v1.2.3