Merge remote-tracking branch 'origin/main' into iipbs/sn/dev

This commit is contained in:
yschoi
2025-06-26 21:46:14 +09:00
319 changed files with 30885 additions and 3402 deletions
+2 -2
View File
@@ -54,9 +54,9 @@ interface PageableList<T> {
const client = getApiClient();
export const getPopupList = async () => {
export const getUserPopupList = async () => {
const response = await client.get<PageableList<PopupInfo>>(
`${ENDPOINT.CMS_SERVICE.POPUPS}`,
`${ENDPOINT.CMS_SERVICE.POPUPS}/user/list`,
);
return response.data;
};
@@ -8,7 +8,7 @@ import { ENDPOINT } from "../../config";
import { setCookie, getCookie } from "../../lib";
import {
getPopupInfo,
getPopupList,
getUserPopupList,
getSiteInfo,
getUserInfo,
type PopupInfo,
@@ -20,6 +20,7 @@ import {
DropdownMenuItem,
} from "../dropdown-menu";
import { Button } from "../button";
import { SidebarTrigger } from "../sidebar";
const { SITE_ID } = getConfig();
@@ -46,9 +47,9 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
enabled: !viewOnly,
});
const { data: popupList } = useQuery({
const { data: userPopupList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS],
queryFn: () => getPopupList(),
queryFn: () => getUserPopupList(),
});
const openPopup = useCallback(async (popup: PopupInfo) => {
@@ -80,20 +81,21 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
}, []);
useEffect(() => {
if (!popupList?.content.length) return;
if (!userPopupList?.content?.length) return;
const timer = setTimeout(() => {
popupList.content.forEach((popup) => {
userPopupList.content.forEach((popup) => {
openPopup(popup);
});
}, 500);
return () => clearTimeout(timer);
}, [popupList, openPopup]);
}, [userPopupList, openPopup]);
return (
<header className="sticky top-0 z-30 box-border flex h-[5rem] w-full items-center justify-between border-b border-solid border-border bg-background">
<div className="flex items-center">
<div className="flex items-center px-6">
<SidebarTrigger className="size-12 !bg-transparent p-2 hover:!bg-accent [&>svg]:!size-full [&>svg]:text-foreground" />
<div className="MobMenuBtn">
<img src={images.menuBtn} />
</div>
+109 -46
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect, useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { ChevronRight } from "lucide-react";
import { getConfig } from "@common/config";
@@ -14,6 +14,9 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from "../sidebar";
import {
Collapsible,
@@ -39,17 +42,58 @@ export function LNB({
enabled: !viewOnly,
});
const [openMenuIds, setOpenMenuIds] = useState<number[]>([]);
const [defaultOpenMenuIds, setDefaultOpenMenuIds] = useState<number[]>([]);
const prevPathRef = useRef(currentPath);
function isPathMatched(path: string) {
if (!path) return false;
return currentPath?.startsWith(path);
}
function MenuItem({ item }: { item: MenuItem }) {
useEffect(() => {
if (!menuList || !currentPath) return;
const matchedMenuIds: number[] = [];
const findMatchedMenus = (items: MenuItem[]) => {
items.forEach((item) => {
if (isPathMatched(item.urlPath)) {
const parentIds = getParentIds(item.menuCd)(menuList);
matchedMenuIds.push(...parentIds, item.menuCd);
}
if (item.children) {
findMatchedMenus(item.children);
}
});
};
findMatchedMenus(menuList);
if (matchedMenuIds.length > 0) {
setDefaultOpenMenuIds((prev) => [
...new Set([...prev, ...matchedMenuIds]),
]);
}
if (prevPathRef.current !== currentPath) {
setDefaultOpenMenuIds(matchedMenuIds);
prevPathRef.current = currentPath;
}
}, [menuList, currentPath]);
function MenuItem({
item,
isSubMenu,
}: {
item: MenuItem;
isSubMenu?: boolean;
}) {
const { open: isSidebarOpen, setOpen } = useSidebar();
const isMatched = isPathMatched(item.urlPath);
const handleClick = () => {
if (!isSidebarOpen) setOpen(true);
setCookie("menu-code-key", item.menuCd.toString());
handleMenuClick?.(item);
};
@@ -58,66 +102,82 @@ export function LNB({
return <MenuGroup item={item} />;
}
const MenuItem = isSubMenu ? SidebarMenuSubItem : SidebarMenuItem;
const MenuButton = isSubMenu ? SidebarMenuSubButton : SidebarMenuButton;
return (
<SidebarMenuItem>
<SidebarMenuButton
<MenuItem>
<MenuButton
className={cn(
"h-auto py-4 text-[1.4rem] text-label transition-colors hover:bg-accent",
"h-auto cursor-pointer py-4 text-[1.4rem] text-label transition-colors hover:bg-accent",
isMatched && "bg-accent !text-primary",
!isSidebarOpen && "justify-center",
)}
data-matched={isMatched}
onClick={handleClick}
tooltip={item.menuKornNm}
>
<i className={cn("diveicon ml-2", `di-${item.iconNm}`)} />
{item.menuKornNm}
</SidebarMenuButton>
</SidebarMenuItem>
<i
className={cn(
"diveicon",
`di-${item.iconNm}`,
isSidebarOpen && "ml-2",
)}
/>
<span className={cn("whitespace-nowrap", !isSidebarOpen && "hidden")}>
{item.menuKornNm}
</span>
</MenuButton>
</MenuItem>
);
}
function MenuGroup({ item }: { item: MenuItem }) {
const hasMatchedChild = item.children?.some((child) =>
isPathMatched(child.urlPath),
);
const isOpen = openMenuIds.includes(item.menuCd) || hasMatchedChild;
const handleOpenChange = (open: boolean) => {
if (open) {
if (!openMenuIds.includes(item.menuCd)) {
const parentIds = getParentIds(item.menuCd)(menuList ?? []);
setOpenMenuIds([...parentIds, item.menuCd]);
}
} else {
setOpenMenuIds((prev) => prev.filter((id) => id !== item.menuCd));
}
};
const { open: isSidebarOpen } = useSidebar();
return (
<Collapsible open={isOpen} onOpenChange={handleOpenChange}>
<Collapsible
defaultOpen={defaultOpenMenuIds.includes(item.menuCd)}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<div className="group">
<SidebarMenuButton className="group/menu-button flex h-auto items-center justify-between py-4 pr-2 text-[1.4rem] group-data-[state=open]:bg-transparent">
<div className="flex items-center gap-2">
<i
className={cn(
"diveicon ml-2 text-label",
`di-${item.iconNm}`,
)}
/>
<span className="text-label group-hover/menu-button:text-foreground">
{item.menuKornNm}
</span>
</div>
<ChevronRight className="!size-6 text-label group-data-[state=open]:rotate-90" />
</SidebarMenuButton>
</div>
<SidebarMenuButton
className={cn(
"flex h-auto items-center py-4 pr-2 text-[1.4rem] group-data-[state=open]/collapsible:bg-transparent",
!isSidebarOpen && "justify-center",
)}
tooltip={item.menuKornNm}
>
<div className="flex items-center gap-2">
<i
className={cn(
"diveicon text-label",
`di-${item.iconNm}`,
isSidebarOpen && "ml-2",
)}
/>
<span
className={cn(
"whitespace-nowrap text-label group-hover/menu-button:text-foreground",
!isSidebarOpen && "hidden",
)}
>
{item.menuKornNm}
</span>
</div>
<ChevronRight
className={cn(
"ml-auto !size-6 text-label group-data-[state=open]/collapsible:rotate-90",
!isSidebarOpen && "hidden",
)}
/>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub className="mr-0">
<SidebarMenuSub>
{item.children?.map((child) => (
<MenuItem key={child.menuCd} item={child} />
<MenuItem key={child.menuCd} item={child} isSubMenu />
))}
</SidebarMenuSub>
</CollapsibleContent>
@@ -127,7 +187,10 @@ export function LNB({
}
return (
<Sidebar className="border-border bg-background px-4 py-[6rem]">
<Sidebar
collapsible="icon"
className="border-border bg-background px-4 py-[6rem]"
>
<SidebarContent className="bg-background">
<SidebarMenu className="gap-0">
{menuList?.map((item) => <MenuItem key={item.menuCd} item={item} />)}
@@ -28,7 +28,7 @@ const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "22rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_WIDTH_ICON = "6rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
@@ -252,7 +252,7 @@ const Sidebar = React.forwardRef<
<div
data-sidebar="sidebar-wrapper"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
"fixed inset-y-0 z-20 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
@@ -530,7 +530,7 @@ const SidebarMenuItem = React.forwardRef<
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-16 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
@@ -602,6 +602,7 @@ const SidebarMenuButton = React.forwardRef<
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
className="text-lg"
{...tooltip}
/>
</Tooltip>
@@ -23,12 +23,19 @@
background: none !important;
color: #fff !important;
}
[data-sidebar="menu-button"]:hover {
[data-sidebar="trigger"]:hover,
[data-sidebar="menu-button"]:hover,
[data-sidebar="menu-sub-button"]:hover {
background: rgba(0, 0, 0, 0.1) !important;
}
[data-sidebar="menu-button"][data-matched="true"] {
[data-sidebar="trigger"][data-matched="true"],
[data-sidebar="menu-button"][data-matched="true"],
[data-sidebar="menu-sub-button"][data-matched="true"] {
background: rgba(0, 0, 0, 0.2) !important;
}
[data-sidebar="trigger"] > svg {
color: #fff !important;
}
}
[data-theme="red"] {
@@ -58,11 +65,18 @@
background: none !important;
color: #fff !important;
}
[data-sidebar="menu-button"]:hover {
[data-sidebar="trigger"]:hover,
[data-sidebar="menu-button"]:hover,
[data-sidebar="menu-sub-button"]:hover {
background: rgba(0, 0, 0, 0.1) !important;
}
[data-sidebar="menu-button"][data-matched="true"] {
[data-sidebar="trigger"][data-matched="true"],
[data-sidebar="menu-button"][data-matched="true"],
[data-sidebar="menu-sub-button"][data-matched="true"] {
background: rgba(0, 0, 0, 0.2) !important;
}
[data-sidebar="trigger"] > svg {
color: #fff !important;
}
}
}