From 04ed774ff60a83c00711d4e8615cb4122954dba5 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 4 Dec 2025 19:46:55 +0900 Subject: (김준회) 메뉴 관리기능 초안 개발 (시딩 필요) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/layout/HeaderV2.tsx | 295 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 components/layout/HeaderV2.tsx (limited to 'components/layout/HeaderV2.tsx') diff --git a/components/layout/HeaderV2.tsx b/components/layout/HeaderV2.tsx new file mode 100644 index 00000000..88d50cc5 --- /dev/null +++ b/components/layout/HeaderV2.tsx @@ -0,0 +1,295 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, +} from "@/components/ui/navigation-menu"; +import { SearchIcon, Loader2 } from "lucide-react"; +import { useParams, usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; +import Image from "next/image"; +import { useSession } from "next-auth/react"; +import { customSignOut } from "@/lib/auth/custom-signout"; +import DynamicMenuRender from "./DynamicMenuRender"; +import { MobileMenuV2 } from "./MobileMenuV2"; +import { CommandMenu } from "./command-menu"; +import { NotificationDropdown } from "./NotificationDropdown"; +import { useVisibleMenuTree } from "@/hooks/use-visible-menu-tree"; +import { useTranslation } from "@/i18n/client"; +import type { MenuDomain, MenuTreeNode } from "@/lib/menu-v2/types"; + +// 도메인별 브랜드명 +const domainBrandingKeys: Record = { + evcp: "branding.evcp_main", + partners: "branding.evcp_partners", +}; + +export function HeaderV2() { + const params = useParams(); + const lng = (params?.lng as string) || "ko"; + const pathname = usePathname(); + const { data: session } = useSession(); + const { t } = useTranslation(lng, "menu"); + + // 현재 도메인 결정 + const domain: MenuDomain = pathname?.includes("/partners") ? "partners" : "evcp"; + + // 메뉴 데이터 로드 (tree에 드롭다운과 단일 링크가 모두 포함됨) + const { tree, isLoading } = useVisibleMenuTree(domain); + + const userName = session?.user?.name || ""; + const initials = userName + .split(" ") + .map((word) => word[0]?.toUpperCase()) + .join(""); + + const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); + const [openMenuKey, setOpenMenuKey] = React.useState(""); + + const toggleMobileMenu = () => { + setIsMobileMenuOpen(!isMobileMenuOpen); + }; + + const toggleMenu = React.useCallback((menuKey: string) => { + setOpenMenuKey((prev) => (prev === menuKey ? "" : menuKey)); + }, []); + + // 페이지 이동 시 메뉴 닫기 + React.useEffect(() => { + setOpenMenuKey(""); + }, [pathname]); + + // 브랜딩 및 경로 설정 + const brandNameKey = domainBrandingKeys[domain]; + const logoHref = `/${lng}/${domain}`; + const basePath = `/${lng}/${domain}`; + + // 다국어 텍스트 선택 + const getTitle = (node: MenuTreeNode) => + lng === "ko" ? node.titleKo : node.titleEn || node.titleKo; + + const getDescription = (node: MenuTreeNode) => + lng === "ko" + ? node.descriptionKo + : node.descriptionEn || node.descriptionKo; + + // 메뉴 노드가 드롭다운(자식 있음)인지 단일 링크인지 판단 + const isDropdownMenu = (node: MenuTreeNode) => + node.nodeType === 'menu_group' && node.children && node.children.length > 0; + + return ( + <> +
+
+
+ {/* 햄버거 메뉴 버튼 (모바일) */} + + + {/* 로고 영역 */} +
+ + EVCP Logo + + {t(brandNameKey)} + + +
+ + {/* 네비게이션 메뉴 */} +
+ {isLoading ? ( +
+ +
+ ) : ( + +
+ + {tree.map((node) => { + // 드롭다운 메뉴 (menu_group with children) + if (isDropdownMenu(node)) { + return ( + + { + e.preventDefault(); + e.stopPropagation(); + toggleMenu(String(node.id)); + }} + onPointerEnter={(e) => e.preventDefault()} + onPointerMove={(e) => e.preventDefault()} + onPointerLeave={(e) => e.preventDefault()} + > + {getTitle(node)} + + + e.preventDefault()} + onPointerLeave={(e) => e.preventDefault()} + forceMount={ + openMenuKey === String(node.id) + ? true + : undefined + } + > + setOpenMenuKey("")} + /> + + + ); + } + + // 단일 링크 메뉴 (최상위 menu) + if (node.nodeType === 'menu' && node.menuPath) { + return ( + + + e.preventDefault()} + onPointerLeave={(e) => e.preventDefault()} + > + {getTitle(node)} + + + + ); + } + + return null; + })} + +
+
+ )} +
+ + {/* 우측 영역 */} +
+ {/* 데스크탑에서는 CommandMenu, 모바일에서는 검색 아이콘만 */} +
+ +
+ + + {/* 알림 버튼 */} + + + {/* 사용자 메뉴 */} + + + + + {initials || "?"} + + + + {t("user.my_account")} + + + {t("user.settings")} + + + + customSignOut({ + callbackUrl: `${window.location.origin}${basePath}`, + }) + } + > + {t("user.logout")} + + + +
+
+
+ + {/* 모바일 메뉴 */} + {isMobileMenuOpen && ( + + )} +
+ + ); +} -- cgit v1.2.3