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/ProjectList.tsx | 463 +++++++++++++++++++++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 components/project/ProjectList.tsx (limited to 'components/project/ProjectList.tsx') diff --git a/components/project/ProjectList.tsx b/components/project/ProjectList.tsx new file mode 100644 index 00000000..4a4f7962 --- /dev/null +++ b/components/project/ProjectList.tsx @@ -0,0 +1,463 @@ +// components/project/ProjectList.tsx +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { + Plus, + Folder, + Users, + Globe, + Lock, + Crown, + Calendar, + Search, + Filter, + Grid3x3, + List +} 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 { Input } from '@/components/ui/input'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { useToast } from '@/hooks/use-toast'; +import { cn } from '@/lib/utils'; + +interface Project { + id: string; + code: string; + name: string; + description?: string; + isPublic: boolean; + createdAt: string; + updatedAt: string; + role?: string; + memberCount?: number; + fileCount?: number; +} + +interface ProjectFormData { + code: string; + name: string; + description?: string; + isPublic: boolean; +} + +export function ProjectList() { + const [projects, setProjects] = useState<{ + owned: Project[]; + member: Project[]; + public: Project[]; + }>({ owned: [], member: [], public: [] }); + const [searchQuery, setSearchQuery] = useState(''); + const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); + const [createDialogOpen, setCreateDialogOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const router = useRouter(); + const { toast } = useToast(); + + // React Hook Form 설정 + const { + register, + handleSubmit, + reset, + formState: { errors, isValid }, + watch, + setValue, + } = useForm({ + mode: 'onChange', + defaultValues: { + code: '', + name: '', + description: '', + isPublic: false, + }, + }); + + const watchIsPublic = watch('isPublic'); + + useEffect(() => { + fetchProjects(); + }, []); + + const fetchProjects = async () => { + try { + const response = await fetch('/api/projects'); + const data = await response.json(); + setProjects(data); + } catch (error) { + toast({ + title: '오류', + description: '프로젝트 목록을 불러올 수 없습니다.', + variant: 'destructive', + }); + } + }; + + const onSubmit = async (data: ProjectFormData) => { + setIsSubmitting(true); + try { + const response = await fetch('/api/projects', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + if (!response.ok) throw new Error('프로젝트 생성 실패'); + + const project = await response.json(); + + toast({ + title: '성공', + description: '프로젝트가 생성되었습니다.', + }); + + setCreateDialogOpen(false); + reset(); + fetchProjects(); + + // 생성된 프로젝트로 이동 + router.push(`/evcp/data-room/${project.id}`); + } catch (error) { + toast({ + title: '오류', + description: '프로젝트 생성에 실패했습니다.', + variant: 'destructive', + }); + } finally { + setIsSubmitting(false); + } + }; + + const handleDialogClose = (open: boolean) => { + setCreateDialogOpen(open); + if (!open) { + reset(); + } + }; + + const filteredProjects = { + owned: projects.owned?.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) + ), + member: projects.member?.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) + ), + public: projects.public?.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) + ), + }; + + const ProjectCard = ({ project, role }: { project: Project; role?: string }) => ( + router.push(`/evcp/data-room/${project.id}/files`)} + > + +
+
+ + {project.code} {project.name} +
+ {role === 'owner' && ( + + )} + {project.isPublic ? ( + + ) : ( + + )} +
+ + {project.description || '설명이 없습니다'} + +
+ +
+
+ {project.memberCount && ( + + + {project.memberCount} + + )} + {project.fileCount !== undefined && ( + + + {project.fileCount} + + )} +
+ + + {new Date(project.updatedAt).toLocaleDateString()} + +
+ {role && ( + + {role} + + )} +
+
+ ); + + return ( + <> + {/* 헤더 */} +
+
+

프로젝트

+

+ 파일을 관리하고 팀과 협업하세요 +

+
+ {/* */} +
+ + {/* 검색 및 필터 */} +
+
+ + setSearchQuery(e.target.value)} + /> +
+ +
+ + {/* 프로젝트 목록 */} + + + + 참여 프로젝트 ({filteredProjects.member?.length}) + + + 공개 프로젝트 ({filteredProjects.public?.length}) + + + + + {filteredProjects.member?.length === 0 ? ( +
+ +

참여 중인 프로젝트가 없습니다

+
+ ) : viewMode === 'grid' ? ( +
+ {filteredProjects.member?.map(project => ( + + ))} +
+ ) : ( +
+ {filteredProjects.member?.map(project => ( + router.push(`/evcp/data-room/${project.id}/files`)} + > + +
+ +
+

{project.name}

+

+ {project.description || '설명이 없습니다'} +

+
+
+
+ {project.role} + {project.isPublic ? ( + + ) : ( + + )} +
+
+
+ ))} +
+ )} +
+ + + {filteredProjects.public?.length === 0 ? ( +
+ +

공개 프로젝트가 없습니다

+
+ ) : viewMode === 'grid' ? ( +
+ {filteredProjects.public?.map(project => ( + + ))} +
+ ) : ( +
+ {filteredProjects.public?.map(project => ( + router.push(`/evcp/data-room/${project.id}/files`)} + > + +
+ +
+

{project.name}

+

+ {project.description || '설명이 없습니다'} +

+
+
+ 공개 +
+
+ ))} +
+ )} +
+
+ + {/* 프로젝트 생성 다이얼로그 */} + + + + 새 프로젝트 만들기 + + 팀과 파일을 공유할 새 프로젝트를 생성합니다 + + + +
+
+ + + {errors.code && ( +

{errors.code.message}

+ )} +
+ +
+ + + {errors.name && ( +

{errors.name.message}

+ )} +
+ +
+ + + {errors.description && ( +

{errors.description.message}

+ )} +
+ +
+
+ +

+ 모든 사용자가 이 프로젝트를 볼 수 있습니다 +

+
+ setValue('isPublic', checked)} + /> +
+ + + + + +
+
+
+ + ); +} \ No newline at end of file -- cgit v1.2.3