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 +++++++++++++++++++++++++------- components/project/ProjectList.tsx | 221 ++++++++++----- components/project/ProjectNav.tsx | 30 ++- 3 files changed, 533 insertions(+), 183 deletions(-) (limited to 'components/project') 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.
- + setSearchQuery(e.target.value)} @@ -251,22 +261,85 @@ export function ProjectList() {
- {/* 프로젝트 목록 */} + {/* Project List */} + {internal && + + My Projects ({filteredProjects.owned?.length}) + + } + - 참여 프로젝트 ({filteredProjects.member?.length}) + Joined Projects ({filteredProjects.member?.length}) - 공개 프로젝트 ({filteredProjects.public?.length}) + Public Projects ({filteredProjects.public?.length}) + {/* My Projects Tab */} + {internal && + + {filteredProjects.owned?.length === 0 ? ( +
+ +

You don't own any projects

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

{project.code} {project.name}

+

+ {project.description || 'No description'} +

+
+
+
+ Owner + {project.isPublic ? ( + + ) : ( + + )} +
+
+
+ ))} +
+ )} +
+ + + } + {filteredProjects.member?.length === 0 ? (
-

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

+

You are not a member of any projects

) : viewMode === 'grid' ? (
@@ -277,7 +350,7 @@ export function ProjectList() { ) : (
{filteredProjects.member?.map(project => ( - router.push(`/evcp/data-room/${project.id}/files`)} @@ -286,9 +359,9 @@ export function ProjectList() {
-

{project.name}

+

{project.code} {project.name}

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

@@ -311,7 +384,7 @@ export function ProjectList() { {filteredProjects.public?.length === 0 ? (
-

공개 프로젝트가 없습니다

+

No public projects

) : viewMode === 'grid' ? (
@@ -322,7 +395,7 @@ export function ProjectList() { ) : (
{filteredProjects.public?.map(project => ( - router.push(`/evcp/data-room/${project.id}/files`)} @@ -331,13 +404,13 @@ export function ProjectList() {
-

{project.name}

+

{project.code} {project.name}

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

- 공개 + Public
))} @@ -346,32 +419,32 @@ export function ProjectList() { - {/* 프로젝트 생성 다이얼로그 */} + {/* Create Project Dialog */} - 새 프로젝트 만들기 + Create New Project - 팀과 파일을 공유할 새 프로젝트를 생성합니다 + Create a new project to share files with your team - +
{errors.name && (

{errors.name.message}

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

{errors.description.message}

)}
- +
- +

- 모든 사용자가 이 프로젝트를 볼 수 있습니다 + All users can view this project

setValue('isPublic', checked)} />
- + - -
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) { - 프로젝트 + Project @@ -119,7 +123,7 @@ export function ProjectNav({ projectId }: ProjectNavProps) {
-- cgit v1.2.3