diff options
Diffstat (limited to 'components/project/ProjectDashboard.tsx')
| -rw-r--r-- | components/project/ProjectDashboard.tsx | 476 |
1 files changed, 476 insertions, 0 deletions
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<string>('viewer'); + const [stats, setStats] = useState<ProjectStats | null>(null); + const [members, setMembers] = useState<any[]>([]); + 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 ( + <div className="flex items-center justify-center min-h-[400px]"> + <div className="text-center space-y-3"> + <Loader2 className="h-8 w-8 animate-spin text-primary mx-auto" /> + <p className="text-sm text-muted-foreground">프로젝트 정보를 불러오는 중...</p> + </div> + </div> + ); + } + + return ( + <div className="p-6 space-y-6"> + {/* 헤더 */} + <div className="flex items-center justify-between"> + <div className="flex items-center gap-3"> + <h1 className="text-2xl font-bold">프로젝트 대시보드</h1> + <Badge variant="outline" className="flex items-center gap-1"> + {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} + </Badge> + </div> + + {isOwner && ( + <div className="flex gap-2"> + <Button onClick={() => setAddMemberOpen(true)}> + <UserPlus className="h-4 w-4 mr-2" /> + 멤버 추가 + </Button> + <Button variant="outline"> + <Settings className="h-4 w-4 mr-2" /> + 설정 + </Button> + </div> + )} + </div> + + {/* Owner 전용 통계 */} + {isOwner && stats && ( + <div className="grid grid-cols-4 gap-4"> + <Card> + <CardHeader className="pb-2"> + <CardTitle className="text-sm font-medium">총 파일 수</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{stats.storage.fileCount}</div> + <p className="text-xs text-muted-foreground mt-1"> + {formatBytes(stats.storage.used)} + </p> + </CardContent> + </Card> + + <Card> + <CardHeader className="pb-2"> + <CardTitle className="text-sm font-medium">멤버</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{stats.users.total}</div> + <div className="flex gap-2 mt-1"> + <span className="text-xs text-muted-foreground"> + 관리자 {stats.users.byRole.admins} + </span> + <span className="text-xs text-muted-foreground"> + 편집자 {stats.users.byRole.editors} + </span> + </div> + </CardContent> + </Card> + + <Card> + <CardHeader className="pb-2"> + <CardTitle className="text-sm font-medium">조회수 (30일)</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{stats.activity.views}</div> + <p className="text-xs text-muted-foreground mt-1"> + 활성 사용자 {stats.users.active}명 + </p> + </CardContent> + </Card> + + <Card> + <CardHeader className="pb-2"> + <CardTitle className="text-sm font-medium">다운로드 (30일)</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{stats.activity.downloads}</div> + <p className="text-xs text-muted-foreground mt-1"> + 업로드 {stats.activity.uploads}개 + </p> + </CardContent> + </Card> + </div> + )} + + {/* 탭 컨텐츠 */} + <Tabs defaultValue="members"> + <TabsList> + <TabsTrigger value="members">멤버</TabsTrigger> + {isOwner && ( + <> + <TabsTrigger value="permissions">권한 관리</TabsTrigger> + <TabsTrigger value="danger">위험 영역</TabsTrigger> + </> + )} + </TabsList> + + <TabsContent value="members" className="mt-6"> + <Card> + <CardHeader> + <CardTitle>프로젝트 멤버</CardTitle> + <CardDescription> + 이 프로젝트에 접근할 수 있는 사용자 목록 + </CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-3"> + {members.map((member) => ( + <div key={member.id} className="flex items-center justify-between p-3 border rounded-lg"> + <div className="flex items-center gap-3"> + <div className="h-10 w-10 bg-gray-100 rounded-full flex items-center justify-center"> + {member.user.name?.charAt(0).toUpperCase()} + </div> + <div> + <p className="font-medium">{member.user.name}</p> + <p className="text-sm text-muted-foreground">{member.user.email}</p> + </div> + </div> + <Badge variant="secondary"> + {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} + </Badge> + </div> + ))} + </div> + </CardContent> + </Card> + </TabsContent> + + {isOwner && ( + <TabsContent value="danger" className="mt-6"> + <Card className="border-red-200"> + <CardHeader> + <CardTitle className="text-red-600">위험 영역</CardTitle> + <CardDescription> + 이 작업들은 되돌릴 수 없습니다. 신중하게 진행하세요. + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="flex items-center justify-between p-4 border rounded-lg"> + <div> + <h3 className="font-medium">소유권 이전</h3> + <p className="text-sm text-muted-foreground"> + 프로젝트 소유권을 다른 멤버에게 이전합니다 + </p> + </div> + <Button + variant="outline" + onClick={() => setTransferOwnershipOpen(true)} + > + <UserCog className="h-4 w-4 mr-2" /> + 소유권 이전 + </Button> + </div> + + <div className="flex items-center justify-between p-4 border rounded-lg border-red-200"> + <div> + <h3 className="font-medium text-red-600">프로젝트 삭제</h3> + <p className="text-sm text-muted-foreground"> + 프로젝트와 모든 파일을 영구적으로 삭제합니다 + </p> + </div> + <Button variant="destructive"> + <Trash2 className="h-4 w-4 mr-2" /> + 프로젝트 삭제 + </Button> + </div> + </CardContent> + </Card> + </TabsContent> + )} + </Tabs> + + {/* 멤버 추가 다이얼로그 */} + <Dialog open={addMemberOpen} onOpenChange={setAddMemberOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>멤버 추가</DialogTitle> + <DialogDescription> + 프로젝트에 새 멤버를 추가합니다 + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + <div> + <Label htmlFor="email">이메일</Label> + <Input + id="email" + type="email" + value={newMemberEmail} + onChange={(e) => setNewMemberEmail(e.target.value)} + placeholder="user@example.com" + /> + </div> + + <div> + <Label htmlFor="role">역할</Label> + <Select value={newMemberRole} onValueChange={setNewMemberRole}> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="viewer">Viewer - 읽기 전용</SelectItem> + <SelectItem value="editor">Editor - 파일 편집 가능</SelectItem> + <SelectItem value="admin">Admin - 프로젝트 관리</SelectItem> + </SelectContent> + </Select> + </div> + </div> + + <DialogFooter> + <Button variant="outline" onClick={() => setAddMemberOpen(false)}> + 취소 + </Button> + <Button onClick={handleAddMember}>추가</Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* 소유권 이전 다이얼로그 */} + <Dialog open={transferOwnershipOpen} onOpenChange={setTransferOwnershipOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>소유권 이전</DialogTitle> + <DialogDescription className="text-red-600"> + 주의: 이 작업은 되돌릴 수 없습니다. 프로젝트의 모든 권한이 새 소유자에게 이전됩니다. + </DialogDescription> + </DialogHeader> + + <div> + <Label htmlFor="new-owner">새 소유자 선택</Label> + <Select value={newOwnerId} onValueChange={setNewOwnerId}> + <SelectTrigger> + <SelectValue placeholder="멤버 선택" /> + </SelectTrigger> + <SelectContent> + {members + .filter(m => m.role !== 'owner') + .map(member => ( + <SelectItem key={member.userId} value={member.userId.toString()}> + {member.user.name} ({member.user.email}) + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + <DialogFooter> + <Button variant="outline" onClick={() => setTransferOwnershipOpen(false)}> + 취소 + </Button> + <Button variant="destructive" onClick={handleTransferOwnership}> + 소유권 이전 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + </div> + ); +}
\ No newline at end of file |
