Files
Groupware/Project/frontend/src/components/layout/Header.tsx

622 lines
22 KiB
TypeScript

import { useState, useRef, useEffect } from 'react';
import { NavLink, useNavigate } from 'react-router-dom';
import {
CheckSquare,
Clock as ClockIcon,
FileText,
FolderKanban,
Code,
Menu,
X,
ChevronDown,
ChevronRight,
Database,
Package,
User,
Users,
CalendarDays,
Mail,
Shield,
List,
AlertTriangle,
Star,
} from 'lucide-react';
import { clsx } from 'clsx';
import { UserInfoDialog } from '@/components/user/UserInfoDialog';
import { UserGroupDialog } from '@/components/user/UserGroupDialog';
import { KuntaeErrorCheckDialog } from '@/components/kuntae/KuntaeErrorCheckDialog';
import { FavoriteDialog } from '@/components/favorite/FavoriteDialog';
import { AmkorLogo } from './AmkorLogo';
interface HeaderProps {
isConnected?: boolean; // deprecated, no longer used
}
interface NavItem {
path?: string;
icon: React.ElementType;
label: string;
action?: string;
}
interface SubMenu {
label: string;
icon: React.ElementType;
items: NavItem[];
}
interface MenuItem {
type: 'link' | 'submenu' | 'action';
path?: string;
icon: React.ElementType;
label: string;
submenu?: SubMenu;
action?: string;
}
interface DropdownMenuConfig {
label: string;
icon: React.ElementType;
items: MenuItem[];
}
// 좌측 메뉴 항목
const leftNavItems: NavItem[] = [
{ path: '/jobreport', icon: FileText, label: '업무일지' },
{ path: '/project', icon: FolderKanban, label: '프로젝트' },
];
// 좌측 드롭다운 메뉴 (근태)
const leftDropdownMenus: DropdownMenuConfig[] = [
{
label: '근태',
icon: ClockIcon,
items: [
{ type: 'link', path: '/kuntae', icon: List, label: '목록' },
{ type: 'action', icon: AlertTriangle, label: '오류검사', action: 'kuntaeErrorCheck' },
],
},
];
// 좌측 단독 액션 버튼 (즐겨찾기)
const leftActionItems: NavItem[] = [
{ icon: Star, label: '즐겨찾기', action: 'favorite' },
];
// 우측 메뉴 항목
const rightNavItems: NavItem[] = [
{ path: '/todo', icon: CheckSquare, label: '할일' },
];
// 드롭다운 메뉴 (2단계 지원)
const dropdownMenus: DropdownMenuConfig[] = [
{
label: '공용정보',
icon: Database,
items: [
{ type: 'link', path: '/common', icon: Code, label: '공용코드' },
{ type: 'link', path: '/items', icon: Package, label: '품목정보' },
{
type: 'submenu',
icon: Users,
label: '사용자',
submenu: {
label: '사용자',
icon: Users,
items: [
{ icon: User, label: '정보', action: 'userInfo' },
{ path: '/user/list', icon: Users, label: '목록' },
{ path: '/user/auth', icon: Shield, label: '권한' },
{ icon: Users, label: '그룹정보', action: 'userGroup' },
],
},
},
{ type: 'link', path: '/monthly-work', icon: CalendarDays, label: '월별근무표' },
{ type: 'link', path: '/mail-form', icon: Mail, label: '메일양식' },
],
},
{
label: '문서',
icon: FileText,
items: [
{ type: 'link', path: '/note', icon: FileText, label: '메모장' },
{ type: 'link', path: '/patch-list', icon: FileText, label: '패치 내역' },
{ type: 'link', path: '/mail-list', icon: Mail, label: '메일 내역' },
],
},
];
function DropdownNavMenu({
menu,
onItemClick,
onAction
}: {
menu: DropdownMenuConfig;
onItemClick?: () => void;
onAction?: (action: string) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
setActiveSubmenu(null);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSubItemClick = (subItem: NavItem) => {
setIsOpen(false);
setActiveSubmenu(null);
if (subItem.action) {
onAction?.(subItem.action);
}
onItemClick?.();
};
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className={clsx(
'flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 text-sm font-medium',
isOpen
? 'bg-white/20 text-white'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)}
>
<menu.icon className="w-4 h-4" />
<span>{menu.label}</span>
<ChevronDown className={clsx('w-3 h-3 transition-transform', isOpen && 'rotate-180')} />
</button>
{isOpen && (
<div className="absolute top-full left-0 mt-1 min-w-[160px] glass-effect-solid rounded-lg py-1 z-[9999]">
{menu.items.map((item) => (
item.type === 'link' ? (
<NavLink
key={item.path}
to={item.path!}
onClick={() => {
setIsOpen(false);
onItemClick?.();
}}
className={({ isActive }) =>
clsx(
'flex items-center space-x-2 px-4 py-2 text-sm transition-colors',
isActive
? 'bg-white/20 text-white'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)
}
>
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</NavLink>
) : item.type === 'action' ? (
<button
key={item.label}
onClick={() => {
setIsOpen(false);
if (item.action) {
onAction?.(item.action);
}
onItemClick?.();
}}
className="flex items-center space-x-2 px-4 py-2 text-sm transition-colors text-white/70 hover:bg-white/10 hover:text-white w-full text-left"
>
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</button>
) : (
<div
key={item.label}
className="relative"
onMouseEnter={() => setActiveSubmenu(item.label)}
onMouseLeave={() => setActiveSubmenu(null)}
>
<div
className={clsx(
'flex items-center justify-between px-4 py-2 text-sm transition-colors cursor-pointer',
activeSubmenu === item.label
? 'bg-white/20 text-white'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)}
>
<div className="flex items-center space-x-2">
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</div>
<ChevronRight className="w-3 h-3" />
</div>
{activeSubmenu === item.label && item.submenu && (
<div className="absolute right-full top-0 mr-1 min-w-[120px] glass-effect-solid rounded-lg py-1 z-[10000]">
{item.submenu.items.map((subItem) => (
subItem.path ? (
<NavLink
key={subItem.path}
to={subItem.path}
onClick={() => handleSubItemClick(subItem)}
className={({ isActive }) =>
clsx(
'flex items-center space-x-2 px-4 py-2 text-sm transition-colors',
isActive
? 'bg-white/20 text-white'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)
}
>
<subItem.icon className="w-4 h-4" />
<span>{subItem.label}</span>
</NavLink>
) : (
<button
key={subItem.label}
onClick={() => handleSubItemClick(subItem)}
className="flex items-center space-x-2 px-4 py-2 text-sm transition-colors text-white/70 hover:bg-white/10 hover:text-white w-full text-left"
>
<subItem.icon className="w-4 h-4" />
<span>{subItem.label}</span>
</button>
)
))}
</div>
)}
</div>
)
))}
</div>
)}
</div>
);
}
// 모바일용 드롭다운 (펼쳐진 형태)
function MobileDropdownMenu({
menu,
onItemClick,
onAction
}: {
menu: DropdownMenuConfig;
onItemClick?: () => void;
onAction?: (action: string) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
const handleSubItemClick = (subItem: NavItem) => {
if (subItem.action) {
onAction?.(subItem.action);
}
onItemClick?.();
};
return (
<div>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between w-full px-4 py-3 rounded-lg text-white/70 hover:bg-white/10 hover:text-white transition-all duration-200"
>
<div className="flex items-center space-x-3">
<menu.icon className="w-5 h-5" />
<span className="font-medium">{menu.label}</span>
</div>
<ChevronDown className={clsx('w-4 h-4 transition-transform', isOpen && 'rotate-180')} />
</button>
{isOpen && (
<div className="ml-6 mt-1 space-y-1">
{menu.items.map((item) => (
item.type === 'link' ? (
<NavLink
key={item.path}
to={item.path!}
onClick={onItemClick}
className={({ isActive }) =>
clsx(
'flex items-center space-x-3 px-4 py-2 rounded-lg transition-all duration-200',
isActive
? 'bg-white/20 text-white'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)
}
>
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</NavLink>
) : item.type === 'action' ? (
<button
key={item.label}
onClick={() => {
if (item.action) {
onAction?.(item.action);
}
onItemClick?.();
}}
className="flex items-center space-x-3 px-4 py-2 rounded-lg transition-all duration-200 text-white/70 hover:bg-white/10 hover:text-white w-full text-left"
>
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</button>
) : (
<div key={item.label}>
<button
onClick={() => setActiveSubmenu(activeSubmenu === item.label ? null : item.label)}
className="flex items-center justify-between w-full px-4 py-2 rounded-lg text-white/70 hover:bg-white/10 hover:text-white transition-all duration-200"
>
<div className="flex items-center space-x-3">
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</div>
<ChevronDown className={clsx('w-3 h-3 transition-transform', activeSubmenu === item.label && 'rotate-180')} />
</button>
{activeSubmenu === item.label && item.submenu && (
<div className="ml-6 mt-1 space-y-1">
{item.submenu.items.map((subItem) => (
subItem.path ? (
<NavLink
key={subItem.path}
to={subItem.path}
onClick={onItemClick}
className={({ isActive }) =>
clsx(
'flex items-center space-x-3 px-4 py-2 rounded-lg transition-all duration-200',
isActive
? 'bg-white/20 text-white'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)
}
>
<subItem.icon className="w-4 h-4" />
<span>{subItem.label}</span>
</NavLink>
) : (
<button
key={subItem.label}
onClick={() => handleSubItemClick(subItem)}
className="flex items-center space-x-3 px-4 py-2 rounded-lg transition-all duration-200 text-white/70 hover:bg-white/10 hover:text-white w-full text-left"
>
<subItem.icon className="w-4 h-4" />
<span>{subItem.label}</span>
</button>
)
))}
</div>
)}
</div>
)
))}
</div>
)}
</div>
);
}
export function Header(_props: HeaderProps) {
const navigate = useNavigate();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [showUserInfoDialog, setShowUserInfoDialog] = useState(false);
const [showUserGroupDialog, setShowUserGroupDialog] = useState(false);
const [showKuntaeErrorCheckDialog, setShowKuntaeErrorCheckDialog] = useState(false);
const [showFavoriteDialog, setShowFavoriteDialog] = useState(false);
const handleAction = (action: string) => {
if (action === 'userInfo') {
setShowUserInfoDialog(true);
} else if (action === 'userGroup') {
setShowUserGroupDialog(true);
} else if (action === 'kuntaeErrorCheck') {
setShowKuntaeErrorCheckDialog(true);
} else if (action === 'favorite') {
setShowFavoriteDialog(true);
}
};
return (
<>
<header className="glass-effect relative z-[9999]">
{/* Main Header Bar */}
<div className="px-4 py-3 flex items-center justify-between">
{/* Logo & Mobile Menu Button */}
<div className="flex items-center space-x-4">
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="lg:hidden text-white/80 hover:text-white transition-colors"
>
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
<div
className="cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => navigate('/')}
>
<AmkorLogo height={36} />
</div>
</div>
{/* Desktop Navigation - Left */}
<nav className="hidden lg:flex items-center space-x-1">
{/* 좌측 일반 메뉴들 */}
{leftNavItems.map((item) => (
<NavLink
key={item.path}
to={item.path!}
className={({ isActive }) =>
clsx(
'flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 text-sm font-medium',
isActive
? 'bg-white/20 text-white shadow-lg'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)
}
>
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</NavLink>
))}
{/* 좌측 드롭다운 메뉴들 (근태) */}
{leftDropdownMenus.map((menu) => (
<DropdownNavMenu key={menu.label} menu={menu} onAction={handleAction} />
))}
{/* 좌측 액션 버튼들 (즐겨찾기) */}
{leftActionItems.map((item) => (
<button
key={item.label}
onClick={() => item.action && handleAction(item.action)}
className="flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 text-sm font-medium text-white/70 hover:bg-white/10 hover:text-white"
>
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</button>
))}
</nav>
{/* Desktop Navigation - Right */}
<nav className="hidden lg:flex items-center space-x-1">
{/* 드롭다운 메뉴들 (공용정보) */}
{dropdownMenus.map((menu) => (
<DropdownNavMenu key={menu.label} menu={menu} onAction={handleAction} />
))}
{/* 우측 메뉴들 (할일) */}
{rightNavItems.map((item) => (
<NavLink
key={item.path}
to={item.path!}
className={({ isActive }) =>
clsx(
'flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 text-sm font-medium',
isActive
? 'bg-white/20 text-white shadow-lg'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)
}
>
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</NavLink>
))}
</nav>
</div>
{/* Mobile Navigation Dropdown */}
{isMobileMenuOpen && (
<div className="lg:hidden border-t border-white/10">
<nav className="px-4 py-2 space-y-1">
{/* 좌측 일반 메뉴들 */}
{leftNavItems.map((item) => (
<NavLink
key={item.path}
to={item.path!}
onClick={() => setIsMobileMenuOpen(false)}
className={({ isActive }) =>
clsx(
'flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-200',
isActive
? 'bg-white/20 text-white'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)
}
>
<item.icon className="w-5 h-5" />
<span className="font-medium">{item.label}</span>
</NavLink>
))}
{/* 좌측 드롭다운 메뉴들 (근태) */}
{leftDropdownMenus.map((menu) => (
<MobileDropdownMenu
key={menu.label}
menu={menu}
onItemClick={() => setIsMobileMenuOpen(false)}
onAction={handleAction}
/>
))}
{/* 좌측 액션 버튼들 (즐겨찾기) */}
{leftActionItems.map((item) => (
<button
key={item.label}
onClick={() => {
if (item.action) handleAction(item.action);
setIsMobileMenuOpen(false);
}}
className="flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-200 text-white/70 hover:bg-white/10 hover:text-white w-full text-left"
>
<item.icon className="w-5 h-5" />
<span className="font-medium">{item.label}</span>
</button>
))}
{/* 구분선 */}
<div className="border-t border-white/10 my-2" />
{/* 우측 드롭다운 메뉴들 (공용정보) */}
{dropdownMenus.map((menu) => (
<MobileDropdownMenu
key={menu.label}
menu={menu}
onItemClick={() => setIsMobileMenuOpen(false)}
onAction={handleAction}
/>
))}
{/* 우측 메뉴들 (할일) */}
{rightNavItems.map((item) => (
<NavLink
key={item.path}
to={item.path!}
onClick={() => setIsMobileMenuOpen(false)}
className={({ isActive }) =>
clsx(
'flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-200',
isActive
? 'bg-white/20 text-white'
: 'text-white/70 hover:bg-white/10 hover:text-white'
)
}
>
<item.icon className="w-5 h-5" />
<span className="font-medium">{item.label}</span>
</NavLink>
))}
</nav>
</div>
)}
</header>
{/* User Info Dialog */}
<UserInfoDialog
isOpen={showUserInfoDialog}
onClose={() => setShowUserInfoDialog(false)}
/>
{/* User Group Dialog */}
<UserGroupDialog
isOpen={showUserGroupDialog}
onClose={() => setShowUserGroupDialog(false)}
/>
{/* Kuntae Error Check Dialog */}
<KuntaeErrorCheckDialog
isOpen={showKuntaeErrorCheckDialog}
onClose={() => setShowKuntaeErrorCheckDialog(false)}
/>
{/* Favorite Dialog */}
<FavoriteDialog
isOpen={showFavoriteDialog}
onClose={() => setShowFavoriteDialog(false)}
/>
</>
);
}