From 4614210aa9878922cfa1e424ce677ef893a1b6b2 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 29 Sep 2025 13:31:40 +0000 Subject: (대표님) 구매 권한설정, data room 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/project/ProjectDashboard.tsx | 465 +++++++++++++++++++++++++------- 1 file changed, 369 insertions(+), 96 deletions(-) (limited to 'components/project/ProjectDashboard.tsx') 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([]); 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([]); + const [selectedUser, setSelectedUser] = useState(null); + const [userSearchTerm, setUserSearchTerm] = useState(''); + const [userPopoverOpen, setUserPopoverOpen] = useState(false); + const [loadingUsers, setLoadingUsers] = useState(false); + const [isExternalUser, setIsExternalUser] = useState(false); + const [newMemberRole, setNewMemberRole] = useState('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 (
-

프로젝트 정보를 불러오는 중...

+

Loading project information...

); @@ -209,10 +290,10 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { return (
- {/* 헤더 */} + {/* Header */}
-

프로젝트 대시보드

+

Project Dashboard

{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) {
)}
- {/* Owner 전용 통계 */} + {/* Owner-only statistics */} {isOwner && stats && (
- 총 파일 수 + Total Files
{stats.storage.fileCount}
@@ -254,16 +335,16 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { - 멤버 + Members
{stats.users.total}
- 관리자 {stats.users.byRole.admins} + Admins {stats.users.byRole.admins} - 편집자 {stats.users.byRole.editors} + Editors {stats.users.byRole.editors}
@@ -271,38 +352,38 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { - 조회수 (30일) + Views (30 days)
{stats.activity.views}

- 활성 사용자 {stats.users.active}명 + {stats.users.active} active users

- 다운로드 (30일) + Downloads (30 days)
{stats.activity.downloads}

- 업로드 {stats.activity.uploads}개 + {stats.activity.uploads} uploads

)} - {/* 탭 컨텐츠 */} + {/* Tab content */} - 멤버 + Members {isOwner && ( <> - 권한 관리 - 위험 영역 + Permission Management + Danger Zone )} @@ -310,9 +391,9 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { - 프로젝트 멤버 + Project Members - 이 프로젝트에 접근할 수 있는 사용자 목록 + List of users who can access this project @@ -347,17 +428,17 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { - 위험 영역 + Danger Zone - 이 작업들은 되돌릴 수 없습니다. 신중하게 진행하세요. + These actions cannot be undone. Please proceed with caution.
-

소유권 이전

+

Transfer Ownership

- 프로젝트 소유권을 다른 멤버에게 이전합니다 + Transfer project ownership to another member

-

프로젝트 삭제

+

Delete Project

- 프로젝트와 모든 파일을 영구적으로 삭제합니다 + Permanently delete project and all files

@@ -387,67 +468,259 @@ export function ProjectDashboard({ projectId }: ProjectDashboardProps) { )}
- {/* 멤버 추가 다이얼로그 */} + {/* Add Member Dialog */} - + - 멤버 추가 + Add Member - 프로젝트에 새 멤버를 추가합니다 + Add a member to the project - -
-
- - setNewMemberEmail(e.target.value)} - placeholder="user@example.com" - /> -
- -
- - -
-
- + + + + Internal Users + + External Users + Viewer Only + + + + +
+ + + {loadingUsers ? ( +
+ + Loading user list... +
+ ) : ( + <> + + + + + + + + + No user found. + + {filteredUsers + .filter(u => u.domain !== 'partners') + .map((user) => ( + { + setSelectedUser(user); + setUserPopoverOpen(false); + setIsExternalUser(false); + setNewMemberRole('viewer'); + }} + value={`${user.name} ${user.email}`} + className="truncate" + > + +
+
{user.name}
+
{user.email}
+
+ +
+ ))} +
+
+
+
+
+ +

+ Internal users can be assigned any role. +

+ + )} +
+ +
+ + +
+
+ + +
+

+ Security Policy Notice
+ External users (partners) can only be granted Viewer permissions due to security policy. +

+
+ +
+ + + {loadingUsers ? ( +
+ + Loading user list... +
+ ) : ( + + + + + + + + + No external users found. + + {filteredUsers + .filter(u => u.domain === 'partners') + .map((user) => ( + { + setSelectedUser(user); + setUserPopoverOpen(false); + setIsExternalUser(true); + setNewMemberRole('viewer'); + }} + value={user.name} + className="truncate" + > + + {user.name} + External + + + ))} + + + + + + )} +
+ +
+ + +
+
+
+ - + -
- {/* 소유권 이전 다이얼로그 */} + {/* Transfer Ownership Dialog */} - 소유권 이전 + Transfer Ownership - 주의: 이 작업은 되돌릴 수 없습니다. 프로젝트의 모든 권한이 새 소유자에게 이전됩니다. + Warning: This action is irreversible. All permissions will be transferred to the new owner.
- +