622 lines
22 KiB
TypeScript
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)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|