summaryrefslogtreecommitdiff
path: root/components/project
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-29 13:31:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-29 13:31:40 +0000
commit4614210aa9878922cfa1e424ce677ef893a1b6b2 (patch)
tree5e7edcce05fbee207230af0a43ed08cd351d7c4f /components/project
parente41e3af4e72870d44a94b03e0f3246d6ccaaca48 (diff)
(대표님) 구매 권한설정, data room 등
Diffstat (limited to 'components/project')
-rw-r--r--components/project/ProjectDashboard.tsx465
-rw-r--r--components/project/ProjectList.tsx221
-rw-r--r--components/project/ProjectNav.tsx30
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>