From 4c2d4c235bd80368e31cae9c375e9a585f6a6844 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 25 Sep 2025 03:28:27 +0000 Subject: (대표님) archiver 추가, 데이터룸구현 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/project/ProjectDashboard.tsx | 476 ++++++++++++++++++++++++++++++++ components/project/ProjectHeader.tsx | 84 ++++++ components/project/ProjectList.tsx | 463 +++++++++++++++++++++++++++++++ components/project/ProjectNav.tsx | 149 ++++++++++ components/project/ProjectSidebar.tsx | 318 +++++++++++++++++++++ 5 files changed, 1490 insertions(+) create mode 100644 components/project/ProjectDashboard.tsx create mode 100644 components/project/ProjectHeader.tsx create mode 100644 components/project/ProjectList.tsx create mode 100644 components/project/ProjectNav.tsx create mode 100644 components/project/ProjectSidebar.tsx (limited to 'components/project') diff --git a/components/project/ProjectDashboard.tsx b/components/project/ProjectDashboard.tsx new file mode 100644 index 00000000..d9ec2e0c --- /dev/null +++ b/components/project/ProjectDashboard.tsx @@ -0,0 +1,476 @@ +// components/project/ProjectDashboard.tsx +'use client'; + +import React, { useState, useEffect } from 'react'; +import { + Crown, + Users, + Settings, + FolderOpen, + Shield, + UserPlus, + Trash2, + BarChart3, + Eye, + Download, + HardDrive, + UserCog, + Loader2 +} from 'lucide-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useToast } from '@/hooks/use-toast'; +import { useSession } from 'next-auth/react'; + +interface ProjectDashboardProps { + projectId: string; +} + +interface ProjectStats { + files: { + totalFiles: number; + totalSize: number; + publicFiles: number; + restrictedFiles: number; + confidentialFiles: number; + }; + members: { + totalMembers: number; + admins: number; + editors: number; + viewers: number; + }; + activity: { + views: number; + downloads: number; + uploads: number; + uniqueUsers: number; + }; +} + +export function ProjectDashboard({ projectId }: ProjectDashboardProps) { + const { data: session } = useSession(); + const [isOwner, setIsOwner] = useState(false); + const [projectRole, setProjectRole] = useState('viewer'); + const [stats, setStats] = useState(null); + const [members, setMembers] = useState([]); + const [loading, setLoading] = useState(true); + + console.log(stats) + + // 다이얼로그 상태 + const [addMemberOpen, setAddMemberOpen] = useState(false); + const [transferOwnershipOpen, setTransferOwnershipOpen] = useState(false); + const [newMemberEmail, setNewMemberEmail] = useState(''); + const [newMemberRole, setNewMemberRole] = useState('viewer'); + const [newOwnerId, setNewOwnerId] = useState(''); + + const { toast } = useToast(); + + // 프로젝트 정보 및 권한 확인 + useEffect(() => { + const fetchProjectData = async () => { + try { + // 권한 확인 + const accessRes = await fetch(`/api/projects/${projectId}/access`); + const accessData = await accessRes.json(); + setIsOwner(accessData.isOwner); + setProjectRole(accessData.role); + + // Owner인 경우 통계 가져오기 + if (accessData.isOwner) { + const statsRes = await fetch(`/api/projects/${projectId}/stats`); + const statsData = await statsRes.json(); + setStats(statsData); + } + + // 멤버 목록 가져오기 + const membersRes = await fetch(`/api/projects/${projectId}/members`); + const membersData = await membersRes.json(); + setMembers(membersData.member); + + } catch (error) { + console.error('프로젝트 데이터 로드 실패:', error); + } finally { + setLoading(false); + } + }; + + fetchProjectData(); + }, [projectId]); + + // 멤버 추가 + const handleAddMember = async () => { + try { + const response = await fetch(`/api/projects/${projectId}/members`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: newMemberEmail, + role: newMemberRole, + }), + }); + + if (!response.ok) { + throw new Error('멤버 추가 실패'); + } + + toast({ + title: '성공', + description: '새 멤버가 추가되었습니다.', + }); + + setAddMemberOpen(false); + // 멤버 목록 새로고침 + } catch (error) { + toast({ + title: '오류', + description: '멤버 추가에 실패했습니다.', + variant: 'destructive', + }); + } + }; + + // 소유권 이전 + const handleTransferOwnership = async () => { + try { + const response = await fetch(`/api/projects/${projectId}/transfer-ownership`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + newOwnerId: newOwnerId, + }), + }); + + if (!response.ok) { + throw new Error('소유권 이전 실패'); + } + + toast({ + title: '성공', + description: '프로젝트 소유권이 이전되었습니다.', + }); + + setTransferOwnershipOpen(false); + setIsOwner(false); + } catch (error) { + toast({ + title: '오류', + description: '소유권 이전에 실패했습니다.', + variant: 'destructive', + }); + } + }; + + const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + 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' }, + viewer: { label: 'Viewer', icon: Eye, color: 'text-gray-500' }, + }; + + if (loading) { + return ( +
+
+ +

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

+
+
+ ); + } + + return ( +
+ {/* 헤더 */} +
+
+

프로젝트 대시보드

+ + {roleConfig[projectRole as keyof typeof roleConfig].icon && + React.createElement(roleConfig[projectRole as keyof typeof roleConfig].icon, { + className: `h-3 w-3 ${roleConfig[projectRole as keyof typeof roleConfig].color}` + }) + } + {roleConfig[projectRole as keyof typeof roleConfig].label} + +
+ + {isOwner && ( +
+ + +
+ )} +
+ + {/* Owner 전용 통계 */} + {isOwner && stats && ( +
+ + + 총 파일 수 + + +
{stats.storage.fileCount}
+

+ {formatBytes(stats.storage.used)} +

+
+
+ + + + 멤버 + + +
{stats.users.total}
+
+ + 관리자 {stats.users.byRole.admins} + + + 편집자 {stats.users.byRole.editors} + +
+
+
+ + + + 조회수 (30일) + + +
{stats.activity.views}
+

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

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

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

+
+
+
+ )} + + {/* 탭 컨텐츠 */} + + + 멤버 + {isOwner && ( + <> + 권한 관리 + 위험 영역 + + )} + + + + + + 프로젝트 멤버 + + 이 프로젝트에 접근할 수 있는 사용자 목록 + + + +
+ {members.map((member) => ( +
+
+
+ {member.user.name?.charAt(0).toUpperCase()} +
+
+

{member.user.name}

+

{member.user.email}

+
+
+ + {roleConfig[member.role as keyof typeof roleConfig].icon && + React.createElement(roleConfig[member.role as keyof typeof roleConfig].icon, { + className: `h-3 w-3 mr-1 ${roleConfig[member.role as keyof typeof roleConfig].color}` + }) + } + {roleConfig[member.role as keyof typeof roleConfig].label} + +
+ ))} +
+
+
+
+ + {isOwner && ( + + + + 위험 영역 + + 이 작업들은 되돌릴 수 없습니다. 신중하게 진행하세요. + + + +
+
+

소유권 이전

+

+ 프로젝트 소유권을 다른 멤버에게 이전합니다 +

+
+ +
+ +
+
+

프로젝트 삭제

+

+ 프로젝트와 모든 파일을 영구적으로 삭제합니다 +

+
+ +
+
+
+
+ )} +
+ + {/* 멤버 추가 다이얼로그 */} + + + + 멤버 추가 + + 프로젝트에 새 멤버를 추가합니다 + + + +
+
+ + setNewMemberEmail(e.target.value)} + placeholder="user@example.com" + /> +
+ +
+ + +
+
+ + + + + +
+
+ + {/* 소유권 이전 다이얼로그 */} + + + + 소유권 이전 + + 주의: 이 작업은 되돌릴 수 없습니다. 프로젝트의 모든 권한이 새 소유자에게 이전됩니다. + + + +
+ + +
+ + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/components/project/ProjectHeader.tsx b/components/project/ProjectHeader.tsx new file mode 100644 index 00000000..34a3f43e --- /dev/null +++ b/components/project/ProjectHeader.tsx @@ -0,0 +1,84 @@ +// components/project/ProjectHeader.tsx +'use client'; + +import { useSession } from 'next-auth/react'; +import { Bell, Search, HelpCircle, User } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Badge } from '@/components/ui/badge'; + +export function ProjectHeader() { + const { data: session } = useSession(); + + return ( +
+
+
+ {/* 로고 */} +
+
+
+ FM +
+ File Manager +
+
+ + {/* 우측 메뉴 */} +
+ {/* 검색 */} + + + {/* 알림 */} + + + {/* 도움말 */} + + + {/* 사용자 메뉴 */} + + + + + + +
+

{session?.user?.name}

+

{session?.user?.email}

+
+
+ + 프로필 + 설정 + 팀 관리 + + + 로그아웃 + +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/project/ProjectList.tsx b/components/project/ProjectList.tsx new file mode 100644 index 00000000..4a4f7962 --- /dev/null +++ b/components/project/ProjectList.tsx @@ -0,0 +1,463 @@ +// components/project/ProjectList.tsx +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { + Plus, + Folder, + Users, + Globe, + Lock, + Crown, + Calendar, + Search, + Filter, + Grid3x3, + List +} from 'lucide-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +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'; + +interface Project { + id: string; + code: string; + name: string; + description?: string; + isPublic: boolean; + createdAt: string; + updatedAt: string; + role?: string; + memberCount?: number; + fileCount?: number; +} + +interface ProjectFormData { + code: string; + name: string; + description?: string; + isPublic: boolean; +} + +export function ProjectList() { + const [projects, setProjects] = useState<{ + owned: Project[]; + member: Project[]; + public: Project[]; + }>({ owned: [], member: [], public: [] }); + const [searchQuery, setSearchQuery] = useState(''); + const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); + const [createDialogOpen, setCreateDialogOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const router = useRouter(); + const { toast } = useToast(); + + // React Hook Form 설정 + const { + register, + handleSubmit, + reset, + formState: { errors, isValid }, + watch, + setValue, + } = useForm({ + mode: 'onChange', + defaultValues: { + code: '', + name: '', + description: '', + isPublic: false, + }, + }); + + const watchIsPublic = watch('isPublic'); + + useEffect(() => { + fetchProjects(); + }, []); + + const fetchProjects = async () => { + try { + const response = await fetch('/api/projects'); + const data = await response.json(); + setProjects(data); + } catch (error) { + toast({ + title: '오류', + description: '프로젝트 목록을 불러올 수 없습니다.', + variant: 'destructive', + }); + } + }; + + const onSubmit = async (data: ProjectFormData) => { + setIsSubmitting(true); + try { + const response = await fetch('/api/projects', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + if (!response.ok) throw new Error('프로젝트 생성 실패'); + + const project = await response.json(); + + toast({ + title: '성공', + description: '프로젝트가 생성되었습니다.', + }); + + setCreateDialogOpen(false); + reset(); + fetchProjects(); + + // 생성된 프로젝트로 이동 + router.push(`/evcp/data-room/${project.id}`); + } catch (error) { + toast({ + title: '오류', + description: '프로젝트 생성에 실패했습니다.', + variant: 'destructive', + }); + } finally { + setIsSubmitting(false); + } + }; + + const handleDialogClose = (open: boolean) => { + setCreateDialogOpen(open); + if (!open) { + reset(); + } + }; + + const filteredProjects = { + owned: projects.owned?.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) + ), + member: projects.member?.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) + ), + public: projects.public?.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) + ), + }; + + const ProjectCard = ({ project, role }: { project: Project; role?: string }) => ( + router.push(`/evcp/data-room/${project.id}/files`)} + > + +
+
+ + {project.code} {project.name} +
+ {role === 'owner' && ( + + )} + {project.isPublic ? ( + + ) : ( + + )} +
+ + {project.description || '설명이 없습니다'} + +
+ +
+
+ {project.memberCount && ( + + + {project.memberCount} + + )} + {project.fileCount !== undefined && ( + + + {project.fileCount} + + )} +
+ + + {new Date(project.updatedAt).toLocaleDateString()} + +
+ {role && ( + + {role} + + )} +
+
+ ); + + return ( + <> + {/* 헤더 */} +
+
+

프로젝트

+

+ 파일을 관리하고 팀과 협업하세요 +

+
+ {/* */} +
+ + {/* 검색 및 필터 */} +
+
+ + setSearchQuery(e.target.value)} + /> +
+ +
+ + {/* 프로젝트 목록 */} + + + + 참여 프로젝트 ({filteredProjects.member?.length}) + + + 공개 프로젝트 ({filteredProjects.public?.length}) + + + + + {filteredProjects.member?.length === 0 ? ( +
+ +

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

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

{project.name}

+

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

+
+
+
+ {project.role} + {project.isPublic ? ( + + ) : ( + + )} +
+
+
+ ))} +
+ )} +
+ + + {filteredProjects.public?.length === 0 ? ( +
+ +

공개 프로젝트가 없습니다

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

{project.name}

+

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

+
+
+ 공개 +
+
+ ))} +
+ )} +
+
+ + {/* 프로젝트 생성 다이얼로그 */} + + + + 새 프로젝트 만들기 + + 팀과 파일을 공유할 새 프로젝트를 생성합니다 + + + +
+
+ + + {errors.code && ( +

{errors.code.message}

+ )} +
+ +
+ + + {errors.name && ( +

{errors.name.message}

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

{errors.description.message}

+ )} +
+ +
+
+ +

+ 모든 사용자가 이 프로젝트를 볼 수 있습니다 +

+
+ setValue('isPublic', checked)} + /> +
+ + + + + +
+
+
+ + ); +} \ No newline at end of file diff --git a/components/project/ProjectNav.tsx b/components/project/ProjectNav.tsx new file mode 100644 index 00000000..acf9bfd8 --- /dev/null +++ b/components/project/ProjectNav.tsx @@ -0,0 +1,149 @@ +// components/project/ProjectNav.tsx +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter, usePathname } from 'next/navigation'; +import { + Home, + FolderOpen, + Users, + Settings, + BarChart3, + Share2, + ChevronDown, + ExternalLink +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbSeparator,BreadcrumbList +} from '@/components/ui/breadcrumb'; +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; + +interface ProjectNavProps { + projectId: string; +} + +export function ProjectNav({ projectId }: ProjectNavProps) { + const [projectName, setProjectName] = useState(''); + const [projectRole, setProjectRole] = useState(''); + const router = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + // 프로젝트 정보 가져오기 + fetchProjectInfo(); + }, [projectId]); + + const fetchProjectInfo = async () => { + try { + const response = await fetch(`/api/projects/${projectId}`); + const data = await response.json(); + setProjectName(data.name); + setProjectRole(data.role); + } catch (error) { + console.error('프로젝트 정보 로드 실패:', error); + } + }; + + const navItems = [ + { + label: '대시보드', + icon: Home, + href: `/evcp/data-room/${projectId}`, + active: pathname === `/evcp/data-room/${projectId}`, + }, + { + label: '파일', + icon: FolderOpen, + href: `/evcp/data-room/${projectId}/files`, + active: pathname === `/evcp/data-room/${projectId}/files`, + }, + { + label: '멤버', + icon: Users, + href: `/evcp/data-room/${projectId}/members`, + active: pathname === `/evcp/data-room/${projectId}/members`, + requireRole: ['owner', 'admin'], + }, + { + label: '통계', + icon: BarChart3, + href: `/evcp/data-room/${projectId}/stats`, + active: pathname === `/evcp/data-room/${projectId}/stats`, + requireRole: ['owner'], + }, + { + label: '설정', + icon: Settings, + href: `/evcp/data-room/${projectId}/settings`, + active: pathname === `/evcp/data-room/${projectId}/settings`, + requireRole: ['owner', 'admin'], + }, + ]; + + const visibleNavItems = navItems.filter(item => + !item.requireRole || item.requireRole.includes(projectRole) + ); + + return ( +
+
+ {/* Breadcrumb */} +
+ + + + 프로젝트 + + + + {projectName || '로딩...'} + + + + +
+ + {projectRole || 'viewer'} + + +
+
+ + {/* 네비게이션 탭 */} +
+ {visibleNavItems.map(item => ( + + ))} +
+
+
+ ); +} + diff --git a/components/project/ProjectSidebar.tsx b/components/project/ProjectSidebar.tsx new file mode 100644 index 00000000..ce2007b1 --- /dev/null +++ b/components/project/ProjectSidebar.tsx @@ -0,0 +1,318 @@ +// components/project/ProjectSidebar.tsx +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter, usePathname } from 'next/navigation'; +import { + Home, + FolderOpen, + Users, + Settings, + Plus, + ChevronLeft, + ChevronRight, + Search, + Crown, + Shield, + Eye, + Clock, + Star, + LogOut +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import { Badge } from '@/components/ui/badge'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { useSession, signOut } from 'next-auth/react'; + +interface RecentProject { + id: string; + name: string; + role: string; + lastAccessed: string; +} + +export function ProjectSidebar() { + const [collapsed, setCollapsed] = useState(false); + const [recentProjects, setRecentProjects] = useState([]); + const [favoriteProjects, setFavoriteProjects] = useState([]); + + const router = useRouter(); + const pathname = usePathname(); + const { data: session } = useSession(); + + const isInternalUser = session?.user?.domain !== 'partners'; + + useEffect(() => { + // 최근 프로젝트 로드 + const stored = localStorage.getItem('recentProjects'); + if (stored) { + setRecentProjects(JSON.parse(stored)); + } + + // 즐겨찾기 프로젝트 로드 + const favorites = localStorage.getItem('favoriteProjects'); + if (favorites) { + setFavoriteProjects(JSON.parse(favorites)); + } + }, [pathname]); + + const menuItems = [ + { + label: '홈', + icon: Home, + href: '/projects', + active: pathname === '/projects', + }, + { + label: '모든 프로젝트', + icon: FolderOpen, + href: '/projects', + active: pathname === '/projects', + }, + ...(isInternalUser ? [{ + label: '팀 관리', + icon: Users, + href: '/projects/team', + active: pathname === '/projects/team', + }] : []), + { + label: '설정', + icon: Settings, + href: '/projects/settings', + active: pathname === '/projects/settings', + }, + ]; + + const roleIcons = { + owner: { icon: Crown, color: 'text-yellow-500' }, + admin: { icon: Shield, color: 'text-blue-500' }, + viewer: { icon: Eye, color: 'text-gray-500' }, + }; + + return ( + +
+ {/* 헤더 */} +
+ {!collapsed && ( +
+

파일 매니저

+

+ {session?.user?.name} +

+
+ )} + +
+ + {/* 검색 */} + {!collapsed && ( +
+
+ + +
+
+ )} + + {/* 메인 메뉴 */} + +
+
+ {!collapsed && ( +

메뉴

+ )} + {menuItems.map((item) => ( + + + + + {collapsed && ( + + {item.label} + + )} + + ))} +
+ + + + {/* 빠른 액세스 */} + {!collapsed && ( +
+
+

빠른 액세스

+ +
+ + {/* 즐겨찾기 프로젝트 */} + {favoriteProjects.length > 0 && ( +
+ {favoriteProjects.slice(0, 3).map((projectId) => ( + + ))} +
+ )} + + {/* 최근 프로젝트 */} +
+

최근 프로젝트

+ {recentProjects.slice(0, 5).map((project) => { + const RoleIcon = roleIcons[project.role as keyof typeof roleIcons]; + return ( + + ); + })} +
+
+ )} + + {collapsed && ( +
+ + + + + + 새 프로젝트 + + + + {recentProjects.slice(0, 3).map((project) => { + const RoleIcon = roleIcons[project.role as keyof typeof roleIcons]; + return ( + + + + + + {project.name} + + + ); + })} +
+ )} +
+
+ + {/* 하단 사용자 정보 */} +
+ {!collapsed ? ( +
+
+ + {session?.user?.name?.charAt(0).toUpperCase()} + +
+
+

{session?.user?.name}

+ + {isInternalUser ? '내부' : '외부'} + +
+ +
+ ) : ( + + + + + + 로그아웃 + + + )} +
+
+
+ ); +} + -- cgit v1.2.3