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.tsx465
1 files changed, 369 insertions, 96 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>