diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
| commit | 4614210aa9878922cfa1e424ce677ef893a1b6b2 (patch) | |
| tree | 5e7edcce05fbee207230af0a43ed08cd351d7c4f /components/project | |
| parent | e41e3af4e72870d44a94b03e0f3246d6ccaaca48 (diff) | |
(대표님) 구매 권한설정, data room 등
Diffstat (limited to 'components/project')
| -rw-r--r-- | components/project/ProjectDashboard.tsx | 465 | ||||
| -rw-r--r-- | components/project/ProjectList.tsx | 221 | ||||
| -rw-r--r-- | components/project/ProjectNav.tsx | 30 |
3 files changed, 533 insertions, 183 deletions
diff --git a/components/project/ProjectDashboard.tsx b/components/project/ProjectDashboard.tsx index d9ec2e0c..5f8afb75 100644 --- a/components/project/ProjectDashboard.tsx +++ b/components/project/ProjectDashboard.tsx @@ -15,7 +15,10 @@ import { Download, HardDrive, UserCog, - Loader2 + Loader2, + Edit2, + Check, + ChevronsUpDown } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -36,10 +39,25 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useToast } from '@/hooks/use-toast'; import { useSession } from 'next-auth/react'; +import { getUsersForFilter } from '@/lib/gtc-contract/service'; +import { cn } from '@/lib/utils'; interface ProjectDashboardProps { projectId: string; @@ -67,6 +85,13 @@ interface ProjectStats { }; } +interface User { + id: number; + name: string; + email: string; + domain?: string; // 'partners' | 'internal' etc +} + export function ProjectDashboard({ projectId }: ProjectDashboardProps) { const { data: session } = useSession(); const [isOwner, setIsOwner] = useState(false); @@ -75,41 +100,46 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { const [members, setMembers] = useState<any[]>([]); const [loading, setLoading] = useState(true); - console.log(stats) - - // 다이얼로그 상태 + // Dialog states const [addMemberOpen, setAddMemberOpen] = useState(false); const [transferOwnershipOpen, setTransferOwnershipOpen] = useState(false); - const [newMemberEmail, setNewMemberEmail] = useState(''); - const [newMemberRole, setNewMemberRole] = useState('viewer'); const [newOwnerId, setNewOwnerId] = useState(''); + // User selection related states + const [availableUsers, setAvailableUsers] = useState<User[]>([]); + const [selectedUser, setSelectedUser] = useState<User | null>(null); + const [userSearchTerm, setUserSearchTerm] = useState(''); + const [userPopoverOpen, setUserPopoverOpen] = useState(false); + const [loadingUsers, setLoadingUsers] = useState(false); + const [isExternalUser, setIsExternalUser] = useState(false); + const [newMemberRole, setNewMemberRole] = useState<string>('viewer'); + const { toast } = useToast(); - // 프로젝트 정보 및 권한 확인 + // Fetch project info and permissions useEffect(() => { const fetchProjectData = async () => { try { - // 권한 확인 + // Check permissions const accessRes = await fetch(`/api/projects/${projectId}/access`); const accessData = await accessRes.json(); setIsOwner(accessData.isOwner); setProjectRole(accessData.role); - // Owner인 경우 통계 가져오기 + // Get stats if owner if (accessData.isOwner) { const statsRes = await fetch(`/api/projects/${projectId}/stats`); const statsData = await statsRes.json(); setStats(statsData); } - // 멤버 목록 가져오기 + // Get member list const membersRes = await fetch(`/api/projects/${projectId}/members`); const membersData = await membersRes.json(); setMembers(membersData.member); } catch (error) { - console.error('프로젝트 데이터 로드 실패:', error); + console.error('Failed to load project data:', error); } finally { setLoading(false); } @@ -118,39 +148,84 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { fetchProjectData(); }, [projectId]); - // 멤버 추가 + // Fetch user list when dialog opens + useEffect(() => { + if (addMemberOpen) { + fetchAvailableUsers(); + } else { + // Reset when dialog closes + setSelectedUser(null); + setUserSearchTerm(''); + setNewMemberRole('viewer'); + setIsExternalUser(false); + } + }, [addMemberOpen]); + + const fetchAvailableUsers = async () => { + try { + setLoadingUsers(true); + const users = await getUsersForFilter(); + // Exclude members already in project + const memberUserIds = members.map(m => m.userId); + const filteredUsers = users.filter(u => !memberUserIds.includes(u.id)); + setAvailableUsers(filteredUsers); + } catch (error) { + console.error('Failed to load user list:', error); + toast({ + title: 'Error', + description: 'Unable to load user list.', + variant: 'destructive', + }); + } finally { + setLoadingUsers(false); + } + }; + + // Add member const handleAddMember = async () => { + if (!selectedUser) { + toast({ + title: 'Error', + description: 'Please select a user.', + variant: 'destructive', + }); + return; + } + try { const response = await fetch(`/api/projects/${projectId}/members`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - email: newMemberEmail, + userId: selectedUser.id, role: newMemberRole, }), }); if (!response.ok) { - throw new Error('멤버 추가 실패'); + throw new Error('Failed to add member'); } toast({ - title: '성공', - description: '새 멤버가 추가되었습니다.', + title: 'Success', + description: 'New member has been added.', }); setAddMemberOpen(false); - // 멤버 목록 새로고침 + // Refresh member list + const membersRes = await fetch(`/api/projects/${projectId}/members`); + const membersData = await membersRes.json(); + setMembers(membersData.member); } catch (error) { toast({ - title: '오류', - description: '멤버 추가에 실패했습니다.', + title: 'Error', + description: 'Failed to add member.', variant: 'destructive', }); } }; - // 소유권 이전 + // Transfer ownership const handleTransferOwnership = async () => { try { const response = await fetch(`/api/projects/${projectId}/transfer-ownership`, { @@ -162,20 +237,20 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { }); if (!response.ok) { - throw new Error('소유권 이전 실패'); + throw new Error('Failed to transfer ownership'); } toast({ - title: '성공', - description: '프로젝트 소유권이 이전되었습니다.', + title: 'Success', + description: 'Project ownership has been transferred.', }); setTransferOwnershipOpen(false); setIsOwner(false); } catch (error) { toast({ - title: '오류', - description: '소유권 이전에 실패했습니다.', + title: 'Error', + description: 'Failed to transfer ownership.', variant: 'destructive', }); } @@ -192,16 +267,22 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { 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' }, + editor: { label: 'Editor', icon: Edit2, color: 'text-green-500' }, viewer: { label: 'Viewer', icon: Eye, color: 'text-gray-500' }, }; + // User search filtering + const filteredUsers = availableUsers.filter(user => + user.name.toLowerCase().includes(userSearchTerm.toLowerCase()) || + user.email.toLowerCase().includes(userSearchTerm.toLowerCase()) + ); + 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> + <p className="text-sm text-muted-foreground">Loading project information...</p> </div> </div> ); @@ -209,10 +290,10 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { return ( <div className="p-6 space-y-6"> - {/* 헤더 */} + {/* Header */} <div className="flex items-center justify-between"> <div className="flex items-center gap-3"> - <h1 className="text-2xl font-bold">프로젝트 대시보드</h1> + <h1 className="text-2xl font-bold">Project Dashboard</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, { @@ -227,22 +308,22 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { <div className="flex gap-2"> <Button onClick={() => setAddMemberOpen(true)}> <UserPlus className="h-4 w-4 mr-2" /> - 멤버 추가 + Add Member </Button> <Button variant="outline"> <Settings className="h-4 w-4 mr-2" /> - 설정 + Settings </Button> </div> )} </div> - {/* Owner 전용 통계 */} + {/* Owner-only statistics */} {isOwner && stats && ( <div className="grid grid-cols-4 gap-4"> <Card> <CardHeader className="pb-2"> - <CardTitle className="text-sm font-medium">총 파일 수</CardTitle> + <CardTitle className="text-sm font-medium">Total Files</CardTitle> </CardHeader> <CardContent> <div className="text-2xl font-bold">{stats.storage.fileCount}</div> @@ -254,16 +335,16 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { <Card> <CardHeader className="pb-2"> - <CardTitle className="text-sm font-medium">멤버</CardTitle> + <CardTitle className="text-sm font-medium">Members</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} + Admins {stats.users.byRole.admins} </span> <span className="text-xs text-muted-foreground"> - 편집자 {stats.users.byRole.editors} + Editors {stats.users.byRole.editors} </span> </div> </CardContent> @@ -271,38 +352,38 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { <Card> <CardHeader className="pb-2"> - <CardTitle className="text-sm font-medium">조회수 (30일)</CardTitle> + <CardTitle className="text-sm font-medium">Views (30 days)</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}명 + {stats.users.active} active users </p> </CardContent> </Card> <Card> <CardHeader className="pb-2"> - <CardTitle className="text-sm font-medium">다운로드 (30일)</CardTitle> + <CardTitle className="text-sm font-medium">Downloads (30 days)</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}개 + {stats.activity.uploads} uploads </p> </CardContent> </Card> </div> )} - {/* 탭 컨텐츠 */} + {/* Tab content */} <Tabs defaultValue="members"> <TabsList> - <TabsTrigger value="members">멤버</TabsTrigger> + <TabsTrigger value="members">Members</TabsTrigger> {isOwner && ( <> - <TabsTrigger value="permissions">권한 관리</TabsTrigger> - <TabsTrigger value="danger">위험 영역</TabsTrigger> + <TabsTrigger value="permissions">Permission Management</TabsTrigger> + <TabsTrigger value="danger">Danger Zone</TabsTrigger> </> )} </TabsList> @@ -310,9 +391,9 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { <TabsContent value="members" className="mt-6"> <Card> <CardHeader> - <CardTitle>프로젝트 멤버</CardTitle> + <CardTitle>Project Members</CardTitle> <CardDescription> - 이 프로젝트에 접근할 수 있는 사용자 목록 + List of users who can access this project </CardDescription> </CardHeader> <CardContent> @@ -347,17 +428,17 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { <TabsContent value="danger" className="mt-6"> <Card className="border-red-200"> <CardHeader> - <CardTitle className="text-red-600">위험 영역</CardTitle> + <CardTitle className="text-red-600">Danger Zone</CardTitle> <CardDescription> - 이 작업들은 되돌릴 수 없습니다. 신중하게 진행하세요. + These actions cannot be undone. Please proceed with caution. </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> + <h3 className="font-medium">Transfer Ownership</h3> <p className="text-sm text-muted-foreground"> - 프로젝트 소유권을 다른 멤버에게 이전합니다 + Transfer project ownership to another member </p> </div> <Button @@ -365,20 +446,20 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { onClick={() => setTransferOwnershipOpen(true)} > <UserCog className="h-4 w-4 mr-2" /> - 소유권 이전 + Transfer Ownership </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> + <h3 className="font-medium text-red-600">Delete Project</h3> <p className="text-sm text-muted-foreground"> - 프로젝트와 모든 파일을 영구적으로 삭제합니다 + Permanently delete project and all files </p> </div> <Button variant="destructive"> <Trash2 className="h-4 w-4 mr-2" /> - 프로젝트 삭제 + Delete Project </Button> </div> </CardContent> @@ -387,67 +468,259 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { )} </Tabs> - {/* 멤버 추가 다이얼로그 */} + {/* Add Member Dialog */} <Dialog open={addMemberOpen} onOpenChange={setAddMemberOpen}> - <DialogContent> + <DialogContent className="max-w-lg"> <DialogHeader> - <DialogTitle>멤버 추가</DialogTitle> + <DialogTitle>Add Member</DialogTitle> <DialogDescription> - 프로젝트에 새 멤버를 추가합니다 + Add a member to the project </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> - + + <Tabs defaultValue="internal" className="w-full"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="internal">Internal Users</TabsTrigger> + <TabsTrigger value="external" className="flex items-center gap-2"> + External Users + <Badge variant="outline" className="ml-1 text-xs">Viewer Only</Badge> + </TabsTrigger> + </TabsList> + + <TabsContent value="internal" className="space-y-4 mt-4"> + <div className="space-y-2"> + <Label htmlFor="internal-user">Select User</Label> + + {loadingUsers ? ( + <div className="flex items-center justify-center py-4"> + <Loader2 className="h-4 w-4 animate-spin" /> + <span className="ml-2 text-sm text-muted-foreground">Loading user list...</span> + </div> + ) : ( + <> + <Popover open={userPopoverOpen} onOpenChange={setUserPopoverOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={userPopoverOpen} + className="w-full justify-between" + > + <span className="truncate"> + {selectedUser && selectedUser.domain !== 'partners' ? ( + <div className="text-left"> + <div className="font-medium">{selectedUser.name}</div> + <div className="text-xs text-muted-foreground">{selectedUser.email}</div> + </div> + ) : ( + "Select internal user..." + )} + </span> + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[460px] p-0"> + <Command> + <CommandInput + placeholder="Search by name or email..." + value={userSearchTerm} + onValueChange={setUserSearchTerm} + /> + <CommandList className="max-h-[300px]"> + <CommandEmpty>No user found.</CommandEmpty> + <CommandGroup heading="Internal User List"> + {filteredUsers + .filter(u => u.domain !== 'partners') + .map((user) => ( + <CommandItem + key={user.id} + onSelect={() => { + setSelectedUser(user); + setUserPopoverOpen(false); + setIsExternalUser(false); + setNewMemberRole('viewer'); + }} + value={`${user.name} ${user.email}`} + className="truncate" + > + <Users className="mr-2 h-4 w-4 text-blue-500 flex-shrink-0" /> + <div className="flex-1 truncate"> + <div className="font-medium truncate">{user.name}</div> + <div className="text-xs text-muted-foreground truncate">{user.email}</div> + </div> + <Check + className={cn( + "ml-2 h-4 w-4 flex-shrink-0", + selectedUser?.id === user.id && !isExternalUser ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + + <p className="text-xs text-muted-foreground"> + Internal users can be assigned any role. + </p> + </> + )} + </div> + + <div className="space-y-2"> + <Label htmlFor="internal-role">Role</Label> + <Select + value={newMemberRole} + onValueChange={setNewMemberRole} + disabled={!selectedUser || isExternalUser} + > + <SelectTrigger id="internal-role"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="viewer">Viewer - Read only</SelectItem> + <SelectItem value="editor">Editor - Can edit files</SelectItem> + <SelectItem value="admin">Admin - Project management</SelectItem> + </SelectContent> + </Select> + </div> + </TabsContent> + + <TabsContent value="external" className="space-y-4 mt-4"> + <div className="rounded-lg bg-amber-50 border border-amber-200 p-3 mb-4"> + <p className="text-sm text-amber-800"> + <strong>Security Policy Notice</strong><br/> + External users (partners) can only be granted Viewer permissions due to security policy. + </p> + </div> + + <div className="space-y-2"> + <Label htmlFor="external-user">Select Partner</Label> + + {loadingUsers ? ( + <div className="flex items-center justify-center py-4"> + <Loader2 className="h-4 w-4 animate-spin" /> + <span className="ml-2 text-sm text-muted-foreground">Loading user list...</span> + </div> + ) : ( + <Popover open={userPopoverOpen} onOpenChange={setUserPopoverOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={userPopoverOpen} + className="w-full justify-between" + > + <span className="truncate"> + {selectedUser && selectedUser.domain === 'partners' ? ( + <span className="flex items-center gap-2"> + {selectedUser.name} + <Badge variant="outline" className="ml-1 text-xs">External</Badge> + </span> + ) : ( + "Select external user..." + )} + </span> + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[460px] p-0"> + <Command> + <CommandInput + placeholder="Search by name..." + value={userSearchTerm} + onValueChange={setUserSearchTerm} + /> + <CommandList className="max-h-[300px]"> + <CommandEmpty>No external users found.</CommandEmpty> + <CommandGroup heading="Partner List"> + {filteredUsers + .filter(u => u.domain === 'partners') + .map((user) => ( + <CommandItem + key={user.id} + onSelect={() => { + setSelectedUser(user); + setUserPopoverOpen(false); + setIsExternalUser(true); + setNewMemberRole('viewer'); + }} + value={user.name} + className="truncate" + > + <Users className="mr-2 h-4 w-4 text-amber-600" /> + <span className="truncate flex-1">{user.name}</span> + <Badge variant="outline" className="text-xs mx-2">External</Badge> + <Check + className={cn( + "ml-auto h-4 w-4 flex-shrink-0", + selectedUser?.id === user.id && isExternalUser ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + )} + </div> + + <div className="space-y-2"> + <Label htmlFor="external-role">Role</Label> + <Select value="viewer" disabled> + <SelectTrigger id="external-role" className="opacity-60"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="viewer">Viewer - Read Only (Fixed)</SelectItem> + </SelectContent> + </Select> + </div> + </TabsContent> + </Tabs> + <DialogFooter> - <Button variant="outline" onClick={() => setAddMemberOpen(false)}> - 취소 + <Button + variant="outline" + onClick={() => { + setAddMemberOpen(false); + setSelectedUser(null); + setUserSearchTerm(''); + setNewMemberRole('viewer'); + setIsExternalUser(false); + }} + > + Cancel + </Button> + <Button + onClick={handleAddMember} + disabled={!selectedUser} + > + Add </Button> - <Button onClick={handleAddMember}>추가</Button> </DialogFooter> </DialogContent> </Dialog> - {/* 소유권 이전 다이얼로그 */} + {/* Transfer Ownership Dialog */} <Dialog open={transferOwnershipOpen} onOpenChange={setTransferOwnershipOpen}> <DialogContent> <DialogHeader> - <DialogTitle>소유권 이전</DialogTitle> + <DialogTitle>Transfer Ownership</DialogTitle> <DialogDescription className="text-red-600"> - 주의: 이 작업은 되돌릴 수 없습니다. 프로젝트의 모든 권한이 새 소유자에게 이전됩니다. + Warning: This action is irreversible. All permissions will be transferred to the new owner. </DialogDescription> </DialogHeader> <div> - <Label htmlFor="new-owner">새 소유자 선택</Label> + <Label htmlFor="new-owner">Select New Owner</Label> <Select value={newOwnerId} onValueChange={setNewOwnerId}> <SelectTrigger> - <SelectValue placeholder="멤버 선택" /> + <SelectValue placeholder="Choose member" /> </SelectTrigger> <SelectContent> {members @@ -463,10 +736,10 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { <DialogFooter> <Button variant="outline" onClick={() => setTransferOwnershipOpen(false)}> - 취소 + Cancel </Button> <Button variant="destructive" onClick={handleTransferOwnership}> - 소유권 이전 + Transfer </Button> </DialogFooter> </DialogContent> diff --git a/components/project/ProjectList.tsx b/components/project/ProjectList.tsx index 4a4f7962..9dec7e77 100644 --- a/components/project/ProjectList.tsx +++ b/components/project/ProjectList.tsx @@ -2,13 +2,12 @@ 'use client'; import { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { useForm } from 'react-hook-form'; -import { - Plus, - Folder, - Users, - Globe, +import { + Plus, + Folder, + Users, + Globe, Lock, Crown, Calendar, @@ -34,6 +33,7 @@ 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'; +import { useRouter, usePathname } from "next/navigation" interface Project { id: string; @@ -65,11 +65,16 @@ export function ProjectList() { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - + const pathname = usePathname() + + const internal = pathname?.includes('evcp') + + console.log(projects) + const router = useRouter(); const { toast } = useToast(); - // React Hook Form 설정 + // React Hook Form setup const { register, handleSubmit, @@ -100,8 +105,8 @@ export function ProjectList() { setProjects(data); } catch (error) { toast({ - title: '오류', - description: '프로젝트 목록을 불러올 수 없습니다.', + title: 'Error', + description: 'Unable to load project list.', variant: 'destructive', }); } @@ -116,25 +121,25 @@ export function ProjectList() { body: JSON.stringify(data), }); - if (!response.ok) throw new Error('프로젝트 생성 실패'); + if (!response.ok) throw new Error('Failed to create project'); const project = await response.json(); - + toast({ - title: '성공', - description: '프로젝트가 생성되었습니다.', + title: 'Success', + description: 'Project has been created.', }); setCreateDialogOpen(false); reset(); fetchProjects(); - - // 생성된 프로젝트로 이동 + + // Navigate to created project router.push(`/evcp/data-room/${project.id}`); } catch (error) { toast({ - title: '오류', - description: '프로젝트 생성에 실패했습니다.', + title: 'Error', + description: 'Failed to create project.', variant: 'destructive', }); } finally { @@ -150,19 +155,20 @@ export function ProjectList() { }; const filteredProjects = { - owned: projects.owned?.filter(p => + owned: projects.owned?.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()) - ), - member: projects.member?.filter(p => + ) || [], // Return empty array instead of undefined + member: projects.member?.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()) - ), - public: projects.public?.filter(p => + ) || [], + public: projects.public?.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()) - ), + ) || [], }; + const ProjectCard = ({ project, role }: { project: Project; role?: string }) => ( - <Card + <Card className="cursor-pointer hover:shadow-lg transition-shadow" onClick={() => router.push(`/evcp/data-room/${project.id}/files`)} > @@ -182,7 +188,7 @@ export function ProjectList() { )} </div> <CardDescription className="line-clamp-2"> - {project.description || '설명이 없습니다'} + {project.description || 'No description'} </CardDescription> </CardHeader> <CardContent> @@ -217,26 +223,30 @@ export function ProjectList() { return ( <> - {/* 헤더 */} + {/* Header */} <div className="flex items-center justify-between mb-6"> <div> - <h1 className="text-3xl font-bold">프로젝트</h1> + <h1 className="text-3xl font-bold">Projects</h1> <p className="text-muted-foreground mt-1"> - 파일을 관리하고 팀과 협업하세요 + Manage files and collaborate with your team </p> </div> - {/* <Button onClick={() => setCreateDialogOpen(true)}> - <Plus className="h-4 w-4 mr-2" /> - 새 프로젝트 - </Button> */} + {internal && + <Button onClick={() => setCreateDialogOpen(true)}> + <Plus className="h-4 w-4 mr-2" /> + New Project + </Button> + + } + </div> - {/* 검색 및 필터 */} + {/* Search and Filter */} <div className="flex items-center gap-3 mb-6"> <div className="relative flex-1 max-w-md"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Input - placeholder="프로젝트 검색..." + placeholder="Search projects..." className="pl-9" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} @@ -251,22 +261,85 @@ export function ProjectList() { </Button> </div> - {/* 프로젝트 목록 */} + {/* Project List */} <Tabs defaultValue="owned" className="space-y-6"> <TabsList> + {internal && + <TabsTrigger value="owned"> + My Projects ({filteredProjects.owned?.length}) + </TabsTrigger> + } + <TabsTrigger value="member"> - 참여 프로젝트 ({filteredProjects.member?.length}) + Joined Projects ({filteredProjects.member?.length}) </TabsTrigger> <TabsTrigger value="public"> - 공개 프로젝트 ({filteredProjects.public?.length}) + Public Projects ({filteredProjects.public?.length}) </TabsTrigger> </TabsList> + {/* My Projects Tab */} + {internal && + <TabsContent value="owned"> + {filteredProjects.owned?.length === 0 ? ( + <div className="text-center py-12"> + <Crown className="h-12 w-12 text-muted-foreground mx-auto mb-3" /> + <p className="text-muted-foreground">You don't own any projects</p> + <Button + className="mt-4" + onClick={() => setCreateDialogOpen(true)} + > + <Plus className="h-4 w-4 mr-2" /> + Create your first project + </Button> + </div> + ) : viewMode === 'grid' ? ( + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> + {filteredProjects.owned?.map(project => ( + <ProjectCard key={project.id} project={project} role="owner" /> + ))} + </div> + ) : ( + <div className="space-y-2"> + {filteredProjects.owned?.map(project => ( + <Card + key={project.id} + className="cursor-pointer hover:shadow transition-shadow" + onClick={() => router.push(`/evcp/data-room/${project.id}/files`)} + > + <CardContent className="flex items-center justify-between p-4"> + <div className="flex items-center gap-3"> + <Folder className="h-5 w-5 text-blue-500" /> + <div> + <p className="font-medium">{project.code} {project.name}</p> + <p className="text-sm text-muted-foreground"> + {project.description || 'No description'} + </p> + </div> + </div> + <div className="flex items-center gap-2"> + <Badge variant="secondary">Owner</Badge> + {project.isPublic ? ( + <Globe className="h-4 w-4 text-green-500" /> + ) : ( + <Lock className="h-4 w-4 text-gray-500" /> + )} + </div> + </CardContent> + </Card> + ))} + </div> + )} + </TabsContent> + + + } + <TabsContent value="member"> {filteredProjects.member?.length === 0 ? ( <div className="text-center py-12"> <Users className="h-12 w-12 text-muted-foreground mx-auto mb-3" /> - <p className="text-muted-foreground">참여 중인 프로젝트가 없습니다</p> + <p className="text-muted-foreground">You are not a member of any projects</p> </div> ) : viewMode === 'grid' ? ( <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> @@ -277,7 +350,7 @@ export function ProjectList() { ) : ( <div className="space-y-2"> {filteredProjects.member?.map(project => ( - <Card + <Card key={project.id} className="cursor-pointer hover:shadow transition-shadow" onClick={() => router.push(`/evcp/data-room/${project.id}/files`)} @@ -286,9 +359,9 @@ export function ProjectList() { <div className="flex items-center gap-3"> <Folder className="h-5 w-5 text-blue-500" /> <div> - <p className="font-medium">{project.name}</p> + <p className="font-medium">{project.code} {project.name}</p> <p className="text-sm text-muted-foreground"> - {project.description || '설명이 없습니다'} + {project.description || 'No description'} </p> </div> </div> @@ -311,7 +384,7 @@ export function ProjectList() { {filteredProjects.public?.length === 0 ? ( <div className="text-center py-12"> <Globe className="h-12 w-12 text-muted-foreground mx-auto mb-3" /> - <p className="text-muted-foreground">공개 프로젝트가 없습니다</p> + <p className="text-muted-foreground">No public projects</p> </div> ) : viewMode === 'grid' ? ( <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> @@ -322,7 +395,7 @@ export function ProjectList() { ) : ( <div className="space-y-2"> {filteredProjects.public?.map(project => ( - <Card + <Card key={project.id} className="cursor-pointer hover:shadow transition-shadow" onClick={() => router.push(`/evcp/data-room/${project.id}/files`)} @@ -331,13 +404,13 @@ export function ProjectList() { <div className="flex items-center gap-3"> <Globe className="h-5 w-5 text-green-500" /> <div> - <p className="font-medium">{project.name}</p> + <p className="font-medium">{project.code} {project.name}</p> <p className="text-sm text-muted-foreground"> - {project.description || '설명이 없습니다'} + {project.description || 'No description'} </p> </div> </div> - <Badge variant="outline">공개</Badge> + <Badge variant="outline">Public</Badge> </CardContent> </Card> ))} @@ -346,32 +419,32 @@ export function ProjectList() { </TabsContent> </Tabs> - {/* 프로젝트 생성 다이얼로그 */} + {/* Create Project Dialog */} <Dialog open={createDialogOpen} onOpenChange={handleDialogClose}> <DialogContent> <DialogHeader> - <DialogTitle>새 프로젝트 만들기</DialogTitle> + <DialogTitle>Create New Project</DialogTitle> <DialogDescription> - 팀과 파일을 공유할 새 프로젝트를 생성합니다 + Create a new project to share files with your team </DialogDescription> </DialogHeader> - + <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <div> <Label htmlFor="code"> - 프로젝트 코드 <span className="text-red-500">*</span> + Project Code <span className="text-red-500">*</span> </Label> <Input id="code" {...register('code', { - required: '프로젝트 코드는 필수입니다', + required: 'Project code is required', minLength: { value: 2, - message: '프로젝트 코드는 최소 2자 이상이어야 합니다', + message: 'Project code must be at least 2 characters', }, pattern: { value: /^[A-Z0-9]+$/, - message: '프로젝트 코드는 대문자와 숫자만 사용 가능합니다', + message: 'Project code can only contain uppercase letters and numbers', }, })} placeholder="SN1001" @@ -384,52 +457,52 @@ export function ProjectList() { <div> <Label htmlFor="name"> - 프로젝트 이름 <span className="text-red-500">*</span> + Project Name <span className="text-red-500">*</span> </Label> <Input id="name" {...register('name', { - required: '프로젝트 이름은 필수입니다', + required: 'Project name is required', minLength: { value: 2, - message: '프로젝트 이름은 최소 2자 이상이어야 합니다', + message: 'Project name must be at least 2 characters', }, maxLength: { value: 50, - message: '프로젝트 이름은 50자를 초과할 수 없습니다', + message: 'Project name cannot exceed 50 characters', }, })} - placeholder="예: FNLG" + placeholder="e.g. FNLG" className={errors.name ? 'border-red-500' : ''} /> {errors.name && ( <p className="text-sm text-red-500 mt-1">{errors.name.message}</p> )} </div> - + <div> - <Label htmlFor="description">설명 (선택)</Label> + <Label htmlFor="description">Description (Optional)</Label> <Input id="description" {...register('description', { maxLength: { value: 200, - message: '설명은 200자를 초과할 수 없습니다', + message: 'Description cannot exceed 200 characters', }, })} - placeholder="프로젝트에 대한 간단한 설명" + placeholder="Brief description of the project" className={errors.description ? 'border-red-500' : ''} /> {errors.description && ( <p className="text-sm text-red-500 mt-1">{errors.description.message}</p> )} </div> - + <div className="flex items-center justify-between"> <div> - <Label htmlFor="public">공개 프로젝트</Label> + <Label htmlFor="public">Public Project</Label> <p className="text-sm text-muted-foreground"> - 모든 사용자가 이 프로젝트를 볼 수 있습니다 + All users can view this project </p> </div> <Switch @@ -438,21 +511,21 @@ export function ProjectList() { onCheckedChange={(checked) => setValue('isPublic', checked)} /> </div> - + <DialogFooter> - <Button - type="button" - variant="outline" + <Button + type="button" + variant="outline" onClick={() => handleDialogClose(false)} disabled={isSubmitting} > - 취소 + Cancel </Button> - <Button + <Button type="submit" disabled={!isValid || isSubmitting} > - {isSubmitting ? '생성 중...' : '프로젝트 생성'} + {isSubmitting ? 'Creating...' : 'Create Project'} </Button> </DialogFooter> </form> diff --git a/components/project/ProjectNav.tsx b/components/project/ProjectNav.tsx index acf9bfd8..aac934ad 100644 --- a/components/project/ProjectNav.tsx +++ b/components/project/ProjectNav.tsx @@ -2,7 +2,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { useRouter, usePathname } from 'next/navigation'; +import { useRouter, usePathname, useParams } from 'next/navigation'; import { Home, FolderOpen, @@ -39,6 +39,8 @@ export function ProjectNav({ projectId }: ProjectNavProps) { const [projectRole, setProjectRole] = useState(''); const router = useRouter(); const pathname = usePathname(); + const params = useParams() || {} + const lng = params.lng ? String(params.lng) : "ko" useEffect(() => { // 프로젝트 정보 가져오기 @@ -56,38 +58,40 @@ export function ProjectNav({ projectId }: ProjectNavProps) { } }; + console.log(pathname, projectId) + const navItems = [ { - label: '대시보드', + label: 'Dashboard', icon: Home, href: `/evcp/data-room/${projectId}`, - active: pathname === `/evcp/data-room/${projectId}`, + active: pathname === `/${lng}/evcp/data-room/${projectId}`, }, { - label: '파일', + label: 'Files', icon: FolderOpen, href: `/evcp/data-room/${projectId}/files`, - active: pathname === `/evcp/data-room/${projectId}/files`, + active: pathname?.includes('files') , }, { - label: '멤버', + label: 'Members', icon: Users, href: `/evcp/data-room/${projectId}/members`, - active: pathname === `/evcp/data-room/${projectId}/members`, + active: pathname?.includes('members'), requireRole: ['owner', 'admin'], }, { - label: '통계', + label: 'Stats', icon: BarChart3, href: `/evcp/data-room/${projectId}/stats`, - active: pathname === `/evcp/data-room/${projectId}/stats`, + active: pathname?.includes('stats') , requireRole: ['owner'], }, { - label: '설정', + label: 'Settings', icon: Settings, href: `/evcp/data-room/${projectId}/settings`, - active: pathname === `/evcp/data-room/${projectId}/settings`, + active: pathname?.includes('settiings') , requireRole: ['owner', 'admin'], }, ]; @@ -104,7 +108,7 @@ export function ProjectNav({ projectId }: ProjectNavProps) { <Breadcrumb> <BreadcrumbList> <BreadcrumbItem> - <BreadcrumbLink href="/evcp/data-room">프로젝트</BreadcrumbLink> + <BreadcrumbLink href="/evcp/data-room">Project</BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> <BreadcrumbItem> @@ -119,7 +123,7 @@ export function ProjectNav({ projectId }: ProjectNavProps) { </Badge> <Button variant="outline" size="sm"> <Share2 className="h-4 w-4 mr-1" /> - 공유 + Share </Button> </div> </div> |
