233 lines
8.6 KiB
TypeScript
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>
|
|
);
|
|
}
|