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