diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index f43788a..627a196 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,93 +1,95 @@ -import { useEffect, useState } from "react"; -import { NavLink, useLocation, useNavigate } from "react-router-dom"; +// import { NavLink } from "react-router-dom"; +import { twMerge } from "tailwind-merge"; import { footerNav, mainNav } from "@/constants/sidebarNav"; +import { useSidebar } from "@/hooks/sidebar/useSidebar"; + +import { SidebarItem } from "./SidebarItem"; +import { SubMenu } from "./SubMenu"; + import ChevronIcon from "@/assets/icon/common/chevron-up.svg?react"; import CollapseIcon from "@/assets/icon/sidebar/chevron-left.svg?react"; -import Logo from "@/assets/logo/symbol-color.svg?react"; +// import Logo from "@/assets/logo/symbol-color.svg?react"; function getMainItemClass(isActive: boolean, isCollapsed: boolean) { - return [ - "flex items-center rounded-xl px-3 text-sm cursor-pointer transition-colors", - isCollapsed ? "h-13 w-13 mx-auto justify-center" : "h-[55px] gap-4 px-3", + return twMerge( + "flex items-center rounded-xl px-3 text-sm cursor-pointer transition-colors duration-200", + isCollapsed + ? "h-[55px] w-[55px] mx-auto flex justify-center" + : "h-[55px] gap-4 px-3", isActive ? "bg-chart-3 text-white" : "text-text-auth-sub hover:bg-[#F6F6F6]", - ].join(" "); + ); } function getFooterItemClass(isActive: boolean, isCollapsed: boolean) { - return [ - "flex w-full h-[55px] items-center rounded-xl px-3 text-sm cursor-pointer transition-colors", + return twMerge( + "flex w-full h-[55px] items-center rounded-xl px-3 text-sm cursor-pointer transition-all duration-200", isCollapsed ? "justify-center px-0" : "gap-4 px-3", isActive ? "text-chart-3" : "text-text-auth-sub hover:text-chart-3", - ].join(" "); + ); } export default function Sidebar() { - const location = useLocation(); - const navigate = useNavigate(); - - const [openId, setOpenId] = useState(null); - const [isCollapsed, setIsCollapsed] = useState(false); - - useEffect(() => { - const activeParent = mainNav.find((item) => - item.children?.some((c) => c.path === location.pathname), - ); - - if (activeParent) setOpenId(activeParent.id); - }, [location.pathname]); + const { + isCollapsed, + openId, + setOpenId, + toggleSidebar, + handleItemClick, + location, + } = useSidebar(); return (
-
- + {/* Logo */} + {/* - - {!isCollapsed && ( - WhereYouAd )} - + > + + + WhereYouAd + + */} {/* Main */} {/* Footer */} -
+
{footerNav.map((item) => { - const Icon = item.icon; - - if (item.path) { - return ( - setOpenId(null)} - className={({ isActive }) => - getFooterItemClass(isActive, isCollapsed) - } - > - {Icon && ( - - )} - - {item.label} - - - ); - } + const isActive = item.path + ? location.pathname === item.path + : false; return ( - + + handleItemClick(item.id, !!item.children?.length) + } + /> +
); })}
diff --git a/src/components/Sidebar/SidebarItem.tsx b/src/components/Sidebar/SidebarItem.tsx new file mode 100644 index 0000000..342c2c2 --- /dev/null +++ b/src/components/Sidebar/SidebarItem.tsx @@ -0,0 +1,78 @@ +import { NavLink } from "react-router-dom"; +import { twMerge } from "tailwind-merge"; + +import type { INavItem } from "@/types/navigation/navItem"; + +interface ISidebarItemProps { + item: INavItem; + isCollapsed: boolean; + isOpen?: boolean; + className: string; + onClick: (id: string, hasChildren: boolean) => void; +} + +export function SidebarItem({ + item, + isCollapsed, + isOpen, + className, + onClick, +}: ISidebarItemProps) { + const hasChildren = !!item.children?.length; + const Icon = item.icon; + + const content = ( +
+ {Icon && ( + + )} + + {item.label} + +
+ ); + + // path 있는 단일 메뉴 + if (item.path) { + return ( + { + if (e.defaultPrevented) return; + onClick(item.id, hasChildren); + }} + > + {content} + + ); + } + + // path 없는 메뉴 + return ( + + ); +} diff --git a/src/components/Sidebar/SubMenu.tsx b/src/components/Sidebar/SubMenu.tsx new file mode 100644 index 0000000..1af1c17 --- /dev/null +++ b/src/components/Sidebar/SubMenu.tsx @@ -0,0 +1,39 @@ +import { NavLink } from "react-router-dom"; +import { twMerge } from "tailwind-merge"; + +import type { INavItem } from "@/types/navigation/navItem"; + +interface ISubMenuProps { + items: INavItem[]; + isCollapsed: boolean; +} + +export function SubMenu({ items, isCollapsed }: ISubMenuProps) { + const getSubItemClass = (isActive: boolean) => + twMerge( + "flex h-10 items-center rounded-xl px-3 text-sm transition-all duration-200 whitespace-nowrap", + isCollapsed ? "" : "pl-4", + isActive + ? "bg-chart-3 text-white" + : "text-text-auth-sub hover:bg-[#F6F6F6]", + ); + + const menuContainerClass = isCollapsed + ? "absolute left-full top-0 pl-2 w-52 flex flex-col gap-1 rounded-2xl bg-white p-2 shadow-lg z-50 whitespace-nowrap" + : "ml-11 mt-1 flex flex-col gap-1 overflow-hidden transition-all duration-200"; + + return ( +
+ {items.map((child) => ( + getSubItemClass(isActive)} + > + {child.label} + + ))} +
+ ); +} diff --git a/src/hooks/sidebar/useSidebar.ts b/src/hooks/sidebar/useSidebar.ts new file mode 100644 index 0000000..0bf6d43 --- /dev/null +++ b/src/hooks/sidebar/useSidebar.ts @@ -0,0 +1,58 @@ +import { useEffect, useRef, useState } from "react"; +import { useLocation } from "react-router-dom"; + +import { mainNav } from "@/constants/sidebarNav"; + +export const useSidebar = () => { + const location = useLocation(); + const [openId, setOpenId] = useState(null); + const [isCollapsed, setIsCollapsed] = useState(false); + + const lastPathRef = useRef(""); + + useEffect(() => { + if (isCollapsed) return; + const activeParent = mainNav.find((item) => + item.children?.some((c) => c.path === location.pathname), + ); + + if (activeParent && location.pathname !== lastPathRef.current) { + setOpenId(activeParent.id); + lastPathRef.current = location.pathname; + } + }, [location.pathname, isCollapsed]); + + const toggleSidebar = () => { + setIsCollapsed((prev) => { + const next = !prev; + if (next) lastPathRef.current = ""; + return next; + }); + setOpenId(null); + }; + + const handleItemClick = (id: string, hasChildren: boolean) => { + if (hasChildren) { + if (openId !== id) { + setOpenId(id); + } + } else { + const parentOfClicked = mainNav.find((item) => + item.children?.some((child) => child.id === id), + ); + + if (!parentOfClicked || parentOfClicked.id !== openId) { + setOpenId(null); + } + } + }; + + return { + isCollapsed, + openId, + setOpenId, + location, + toggleSidebar, + handleItemClick, + }; +};