diff options
Diffstat (limited to 'components/layout')
| -rw-r--r-- | components/layout/GroupedMenuRender.tsx | 80 | ||||
| -rw-r--r-- | components/layout/Header.tsx | 215 | ||||
| -rw-r--r-- | components/layout/MobileMenu.tsx | 8 |
3 files changed, 197 insertions, 106 deletions
diff --git a/components/layout/GroupedMenuRender.tsx b/components/layout/GroupedMenuRender.tsx new file mode 100644 index 00000000..e2a5a225 --- /dev/null +++ b/components/layout/GroupedMenuRender.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import Link from 'next/link'; +import { NavigationMenuLink } from "@/components/ui/navigation-menu"; +import { cn } from "@/lib/utils"; +import * as LucideIcons from "lucide-react"; +import { MenuItem } from '@/config/menuConfig'; + +type GroupedMenuItems = { + [key: string]: MenuItem[]; +}; + +interface GroupedMenuRendererProps { + items: MenuItem[]; + lng: string; +} + +const GroupedMenuRenderer = ({ items, lng }: GroupedMenuRendererProps) => { + // 그룹별로 아이템 분류 + const groupItems = (items: MenuItem[]): GroupedMenuItems => { + return items.reduce((groups, item) => { + const group = item.group || 'default'; + if (!groups[group]) { + groups[group] = []; + } + groups[group].push(item); + return groups; + }, {} as GroupedMenuItems); + }; + + const groupedItems = groupItems(items); + const groups = Object.keys(groupedItems); + + return ( + <div className="p-4 w-[600px]"> + {groups.map((groupName, index) => ( + <div key={groupName} className={cn("mb-4", index < groups.length - 1 && "pb-2 border-b border-border/30")}> + {groupName !== 'default' && ( + <h3 className="text-sm font-semibold mb-2 text-primary">{groupName}</h3> + )} + <div className="grid grid-cols-2 gap-3"> + {groupedItems[groupName].map((item) => ( + <MenuListItem key={item.title} item={item} lng={lng} /> + ))} + </div> + </div> + ))} + </div> + ); +}; + +const MenuListItem = ({ item, lng }: { item: MenuItem; lng: string }) => { + + + + + return ( + <NavigationMenuLink asChild> + <Link + href={`/${lng}${item.href}`} + className={cn( + "flex items-start space-x-2 rounded-md p-3 leading-none no-underline outline-none transition-colors", + "hover:bg-accent hover:text-accent-foreground", + "focus:bg-accent focus:text-accent-foreground", + item.disabled && "pointer-events-none opacity-60" + )} + > + <div className="space-y-1"> + <div className="text-sm font-medium leading-none">{item.title}</div> + {item.description && ( + <p className="line-clamp-2 text-xs leading-snug text-muted-foreground"> + {item.description} + </p> + )} + </div> + </Link> + </NavigationMenuLink> + ); +}; + +export default GroupedMenuRenderer;
\ No newline at end of file diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 1b6c45bb..498668eb 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -26,19 +26,20 @@ import { SearchIcon, BellIcon, Menu } from "lucide-react"; import { useParams, usePathname } from "next/navigation"; import { cn } from "@/lib/utils"; import Image from "next/image"; -import { mainNav, additionalNav, MenuSection, mainNavVendor, additionalNavVendor } from "@/config/menuConfig"; // 메뉴 구성 임포트 +import { mainNav, additionalNav, MenuSection, MenuItem, mainNavVendor, additionalNavVendor } from "@/config/menuConfig"; // 메뉴 구성 임포트 import { MobileMenu } from "./MobileMenu"; import { CommandMenu } from "./command-menu"; import { useSession, signOut } from "next-auth/react"; - +import GroupedMenuRenderer from "./GroupedMenuRender"; export function Header() { const params = useParams(); - const lng = params.lng as string; + const lng = params?.lng as string; const pathname = usePathname(); const { data: session } = useSession(); - const userName = session?.user?.name || ""; // 없을 수도 있으니 안전하게 처리 + const userName = session?.user?.name || ""; + const domain = session?.user?.domain || ""; const initials = userName .split(" ") .map((word) => word[0]?.toUpperCase()) @@ -51,7 +52,7 @@ export function Header() { setIsMobileMenuOpen(!isMobileMenuOpen); }; - const isPartnerRoute = pathname.includes("/partners"); + const isPartnerRoute = pathname?.includes("/partners"); const main = isPartnerRoute ? mainNavVendor : mainNav; const additional = isPartnerRoute ? additionalNavVendor : additionalNav; @@ -60,10 +61,10 @@ export function Header() { return ( <> - <header className="border-grid sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> + <header className="border-grid sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <div className="container-wrapper"> <div className="container flex h-14 items-center"> - + {/* 햄버거 메뉴 버튼 (모바일) */} <Button onClick={toggleMobileMenu} variant="ghost" @@ -86,105 +87,115 @@ export function Header() { <span className="sr-only">Toggle Menu</span> </Button> - - <div className="mr-4 hidden md:flex"> - - {/* 로고 영역 */} - <div className="mr-4 flex items-center gap-2 lg:mr-6"> - <Link href={`/${lng}/evcp`} className="flex items-center gap-2"> - <Image - className="dark:invert" - src="/images/vercel.svg" - alt="EVCP Logo" - width={20} - height={20} - /> - <span className="hidden font-bold lg:inline-block">eVCP</span> - </Link> - </div> - {/* 데스크탑 네비게이션 메뉴 */} - <NavigationMenu className="flex items-center gap-4 text-sm xl:gap-6"> - <NavigationMenuList> - {main.map((section: MenuSection) => ( - <NavigationMenuItem key={section.title}> - <NavigationMenuTrigger>{section.title}</NavigationMenuTrigger> - <NavigationMenuContent> - <ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] "> - {section.items.map((item) => ( - <ListItem - key={item.title} - title={item.title} - href={`/${lng}${item.href}`} - > - {item.description} - </ListItem> - ))} - </ul> - </NavigationMenuContent> - </NavigationMenuItem> - ))} - - - {/* 추가 네비게이션 항목 */} - {additional.map((item) => ( - <NavigationMenuItem key={item.title}> - <Link href={`/${lng}${item.href}`} legacyBehavior passHref> - <NavigationMenuLink className={navigationMenuTriggerStyle()}> - {item.title} - </NavigationMenuLink> - </Link> - </NavigationMenuItem> - ))} - </NavigationMenuList> + {/* 로고 영역 - 항상 표시 */} + <div className="mr-4 flex-shrink-0 flex items-center gap-2 lg:mr-6"> + <Link href={`/${lng}/evcp`} className="flex items-center gap-2"> + <Image + className="dark:invert" + src="/images/vercel.svg" + alt="EVCP Logo" + width={20} + height={20} + /> + <span className="hidden font-bold lg:inline-block">eVCP</span> + </Link> + </div> + + {/* 네비게이션 메뉴 - 내부 스크롤 적용, 드롭다운은 제약 없이 표시 */} + <div className="hidden md:block flex-1 min-w-0"> + {/* NavigationMenu는 z-index를 높게 설정하여 드롭다운이 제대로 표시되도록 함 */} + <NavigationMenu className="relative z-50"> + {/* 스크롤 가능한 메뉴 리스트 컨테이너 */} + <div className="w-full overflow-x-auto pb-1"> + <NavigationMenuList className="flex-nowrap w-max"> + {main.map((section: MenuSection) => ( + <NavigationMenuItem key={section.title}> + <NavigationMenuTrigger className="px-2 xl:px-3 text-sm whitespace-nowrap"> + {section.title} + </NavigationMenuTrigger> + + {/* 그룹핑이 필요한 메뉴의 경우 GroupedMenuRenderer 사용 */} + {section.useGrouping ? ( + <NavigationMenuContent> + <GroupedMenuRenderer items={section.items} lng={lng} /> + </NavigationMenuContent> + ) : ( + <NavigationMenuContent> + <ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] "> + {section.items.map((item) => ( + <ListItem + key={item.title} + title={item.title} + href={`/${lng}${item.href}`} + > + {item.description} + </ListItem> + ))} + </ul> + </NavigationMenuContent> + )} + </NavigationMenuItem> + ))} + + {/* 추가 네비게이션 항목 */} + {additional.map((item) => ( + <NavigationMenuItem key={item.title}> + <Link href={`/${lng}${item.href}`} legacyBehavior passHref> + <NavigationMenuLink + className={cn( + navigationMenuTriggerStyle(), + "px-2 xl:px-3 text-sm whitespace-nowrap" + )} + > + {item.title} + </NavigationMenuLink> + </Link> + </NavigationMenuItem> + ))} + </NavigationMenuList> + </div> </NavigationMenu> - - </div> - - {/* 우측 영역 */} - <div className="flex flex-1 items-center justify-between gap-2 md:justify-end"> - - <CommandMenu /> - - - <div className="flex items-center space-x-4"> - {/* 알림 버튼 */} - <Button variant="ghost" className="relative p-2" aria-label="Notifications"> - <BellIcon className="h-5 w-5" /> - {/* 알림 뱃지 예시 */} - <span className="absolute -top-1 -right-1 inline-flex h-2 w-2 rounded-full bg-red-500"></span> - </Button> - - {/* 사용자 메뉴 (DropdownMenu) */} - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Avatar className="cursor-pointer"> - <AvatarImage src={`/profiles/${session?.user?.image}`||"/user-avatar.jpg"} alt="User Avatar" /> - <AvatarFallback> - {initials || "?"} - </AvatarFallback> - </Avatar> - </DropdownMenuTrigger> - <DropdownMenuContent className="w-48" align="end"> - <DropdownMenuLabel>My Account</DropdownMenuLabel> - <DropdownMenuSeparator /> - {/* <DropdownMenuItem asChild> - <Link href={`${basePath}/profile`}>Profile</Link> - </DropdownMenuItem> */} - <DropdownMenuItem asChild> - <Link href={`${basePath}/settings`}>Settings</Link> - </DropdownMenuItem> - <DropdownMenuSeparator /> - <DropdownMenuItem onSelect={() => signOut({ callbackUrl: `/${lng}/login` })}> - Logout - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - - {/* 모바일 햄버거 메뉴 버튼 */} - + {/* 우측 영역 - 고정 너비와 우선순위로 항상 표시되도록 함 */} + <div className="ml-auto flex flex-shrink-0 items-center space-x-2"> + {/* 데스크탑에서는 CommandMenu, 모바일에서는 검색 아이콘만 */} + <div className="hidden md:block md:w-auto"> + <CommandMenu /> </div> + <Button variant="ghost" size="icon" className="md:hidden" aria-label="Search"> + <SearchIcon className="h-5 w-5" /> + </Button> + + {/* 알림 버튼 */} + <Button variant="ghost" size="icon" className="relative" aria-label="Notifications"> + <BellIcon className="h-5 w-5" /> + {/* 알림 뱃지 예시 */} + <span className="absolute -top-1 -right-1 inline-flex h-2 w-2 rounded-full bg-red-500"></span> + </Button> + + {/* 사용자 메뉴 (DropdownMenu) */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Avatar className="cursor-pointer h-8 w-8"> + <AvatarImage src={`/profiles/${session?.user?.image}`||"/user-avatar.jpg"} alt="User Avatar" /> + <AvatarFallback> + {initials || "?"} + </AvatarFallback> + </Avatar> + </DropdownMenuTrigger> + <DropdownMenuContent className="w-48" align="end"> + <DropdownMenuLabel>My Account</DropdownMenuLabel> + <DropdownMenuSeparator /> + <DropdownMenuItem asChild> + <Link href={`${basePath}/settings`}>Settings</Link> + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem onSelect={() => signOut({ callbackUrl: `/${lng}/${domain}` })}> + Logout + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> </div> </div> </div> diff --git a/components/layout/MobileMenu.tsx b/components/layout/MobileMenu.tsx index d2e6b927..2e70aeba 100644 --- a/components/layout/MobileMenu.tsx +++ b/components/layout/MobileMenu.tsx @@ -4,10 +4,10 @@ import * as React from "react"; import Link from "next/link"; -import { useRouter,usePathname } from "next/navigation"; +import { useRouter, usePathname } from "next/navigation"; import { MenuSection, mainNav, additionalNav, MenuItem, mainNavVendor, additionalNavVendor } from "@/config/menuConfig"; import { cn } from "@/lib/utils"; -import { Drawer, DrawerContent,DrawerTitle,DrawerTrigger } from "@/components/ui/drawer"; +import { Drawer, DrawerContent, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; import { Button } from "@/components/ui/button"; interface MobileMenuProps { @@ -24,14 +24,14 @@ export function MobileMenu({ lng, onClose }: MobileMenuProps) { onClose(); }; const pathname = usePathname(); - const isPartnerRoute = pathname.includes("/partners"); + const isPartnerRoute = pathname?.includes("/partners"); const main = isPartnerRoute ? mainNavVendor : mainNav; const additional = isPartnerRoute ? additionalNavVendor : additionalNav; return ( <Drawer open={true} onOpenChange={onClose}> - <DrawerTrigger asChild> + <DrawerTrigger asChild> </DrawerTrigger> <DrawerTitle /> <DrawerContent className="max-h-[60vh] p-0"> |
