Files
distribution_erp/src/components/layout/sidebar.tsx
T

233 lines
8.6 KiB
TypeScript

"use client";
import { useEffect, useState, useRef } from "react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { useMenuStore } from "@/store/menu-store";
import { MENU_ICON_MAP } from "@/lib/constants";
import { mapMenuUrl } from "@/lib/menu-url-map";
import {
LayoutDashboard, TrendingUp, FolderKanban, Package, ListChecks,
ShoppingCart, FileText, Warehouse, Boxes, Factory, Wrench,
Headset, Clock, Calculator, Coins, Truck, Settings,
ClipboardCheck, Compass, GitBranch, Puzzle, Stamp, Folder,
ChevronDown, Menu as MenuIcon,
} from "lucide-react";
const ICON_COMPONENTS: Record<string, React.ElementType> = {
LayoutDashboard, TrendingUp, FolderKanban, Package, ListChecks,
ShoppingCart, FileText, Warehouse, Boxes, Factory, Wrench,
Headset, UserClock: Clock, Calculator, Coins, Truck, Settings,
ClipboardCheck, Compass, GitBranch, Puzzle, Stamp,
};
function getMenuIcon(menuName: string): React.ElementType {
for (const [key, iconName] of Object.entries(MENU_ICON_MAP)) {
if (menuName?.toUpperCase().includes(key.toUpperCase())) {
return ICON_COMPONENTS[iconName] || Folder;
}
}
return Folder;
}
export function Sidebar() {
const router = useRouter();
const {
sideMenus, activeSubMenu, isCollapsed,
setActiveSubMenu, toggleCollapsed,
} = useMenuStore();
const [openCategories, setOpenCategories] = useState<Set<string>>(new Set());
// 축소 모드 호버 팝업
const [hoveredCategory, setHoveredCategory] = useState<string | null>(null);
const [popupPos, setPopupPos] = useState<{ top: number }>({ top: 0 });
const hoverTimeout = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (sideMenus.length > 0 && openCategories.size === 0) {
setOpenCategories(new Set([sideMenus[0].objid]));
}
}, [sideMenus, openCategories.size]);
const toggleCategory = (objid: string) => {
if (isCollapsed) return; // 축소모드에서는 클릭 무시 (호버로 동작)
setOpenCategories((prev) => {
const next = new Set(prev);
if (next.has(objid)) next.delete(objid);
else next.add(objid);
return next;
});
};
const handleSubMenuClick = (menuObjId: string, menuUrl: string) => {
setActiveSubMenu(menuObjId);
setHoveredCategory(null); // 팝업 닫기
const path = mapMenuUrl(menuUrl);
if (path) router.push(path);
};
// 축소 모드: 카테고리 호버 시 팝업
const handleCategoryHover = (objid: string, e: React.MouseEvent) => {
if (!isCollapsed) return;
if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
setPopupPos({ top: rect.top });
setHoveredCategory(objid);
};
const handleCategoryLeave = () => {
if (!isCollapsed) return;
hoverTimeout.current = setTimeout(() => setHoveredCategory(null), 200);
};
const handlePopupEnter = () => {
if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
};
const handlePopupLeave = () => {
setHoveredCategory(null);
};
// 호버된 카테고리의 서브메뉴
const hoveredMenu = sideMenus.find((m) => m.objid === hoveredCategory);
return (
<aside
className={cn(
"h-screen flex flex-col bg-[#1e2432] text-[#c8cdd5] transition-all duration-200 relative",
isCollapsed ? "w-[52px]" : "w-[240px]"
)}
>
{/* 헤더 — MOMO 브랜드 */}
<div className="flex items-center justify-between px-3 py-3 border-b border-white/[0.06] shrink-0">
{!isCollapsed ? (
<div className="flex items-center gap-2 min-w-0">
<img
src="/momo-icon.svg"
alt="MOMO"
width={28}
height={28}
className="shrink-0"
/>
<div className="flex flex-col leading-tight min-w-0">
<span className="text-white font-extrabold text-[13px] tracking-wide truncate">
</span>
<span className="text-emerald-300/80 text-[10px] tracking-wider truncate">
ERP
</span>
</div>
</div>
) : (
<img src="/momo-icon.svg" alt="MOMO" width={24} height={24} className="mx-auto" />
)}
<button
onClick={toggleCollapsed}
className="text-[#8890a0] hover:text-white hover:bg-white/[0.08] rounded p-1 transition-colors shrink-0"
>
<MenuIcon size={16} />
</button>
</div>
{/* 메뉴 영역 */}
<nav className="flex-1 overflow-y-auto overflow-x-hidden py-2 scrollbar-thin">
{sideMenus.map((menu) => {
const IconComponent = getMenuIcon(menu.menuNameKor);
const isOpen = openCategories.has(menu.objid);
const isHovered = hoveredCategory === menu.objid;
return (
<div
key={menu.objid}
onMouseEnter={(e) => handleCategoryHover(menu.objid, e)}
onMouseLeave={handleCategoryLeave}
>
{/* 카테고리 헤더 */}
<button
onClick={() => toggleCategory(menu.objid)}
className={cn(
"w-full flex items-center px-3.5 py-2.5 text-[13px] font-semibold transition-colors",
"hover:bg-white/[0.05] hover:text-white",
(isOpen || isHovered) && "text-white",
isCollapsed && "justify-center px-0 py-3"
)}
>
<span className={cn(
"w-[22px] h-[22px] flex items-center justify-center shrink-0",
!isCollapsed && "mr-2.5",
(isOpen || isHovered) ? "text-[#4da3ff]" : "text-[#8890a0]"
)}>
<IconComponent size={14} />
</span>
{!isCollapsed && (
<>
<span className="flex-1 text-left truncate">{menu.menuNameKor}</span>
<ChevronDown
size={10}
className={cn(
"text-[#555e6e] transition-transform ml-1 shrink-0",
isOpen && "rotate-180"
)}
/>
</>
)}
</button>
{/* 서브메뉴 - 확장 모드 */}
{!isCollapsed && isOpen && menu.children && (
<div className="py-0.5">
{menu.children.map((sub) => (
<button
key={sub.objid}
onClick={() => handleSubMenuClick(sub.objid, sub.menuUrl)}
className={cn(
"w-full flex items-center pl-[46px] pr-3.5 py-[7px] text-[12px] text-[#8890a0] relative",
"hover:text-white hover:bg-white/[0.04] transition-colors",
activeSubMenu === sub.objid && "text-white bg-[#1C90FB] mr-2 rounded-r"
)}
>
<span
className={cn(
"absolute left-8 top-1/2 -translate-y-1/2 w-1 h-1 rounded-full",
activeSubMenu === sub.objid ? "bg-white" : "bg-[#555e6e]"
)}
/>
<span className="truncate">{sub.menuNameKor}</span>
</button>
))}
</div>
)}
</div>
);
})}
</nav>
{/* 축소 모드 호버 팝업 (menu.jsp 축소모드 대응) */}
{isCollapsed && hoveredMenu && hoveredMenu.children && hoveredMenu.children.length > 0 && (
<div
className="fixed z-50 bg-[#252d3d] border border-white/10 rounded-r-lg shadow-xl py-2 min-w-[180px]"
style={{ left: 52, top: popupPos.top }}
onMouseEnter={handlePopupEnter}
onMouseLeave={handlePopupLeave}
>
{/* 카테고리 타이틀 */}
<div className="px-3 py-1.5 text-[11px] font-bold text-[#4da3ff] border-b border-white/[0.06] mb-1">
{hoveredMenu.menuNameKor}
</div>
{hoveredMenu.children.map((sub) => (
<button
key={sub.objid}
onClick={() => handleSubMenuClick(sub.objid, sub.menuUrl)}
className={cn(
"w-full text-left px-3 py-1.5 text-[12px] text-[#8890a0] hover:text-white hover:bg-white/[0.06] transition-colors",
activeSubMenu === sub.objid && "text-white bg-[#1C90FB]"
)}
>
{sub.menuNameKor}
</button>
))}
</div>
)}
</aside>
);
}