summaryrefslogtreecommitdiff
path: root/components/layout
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-04-08 03:08:19 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-04-08 03:08:19 +0000
commit9ceed79cf32c896f8a998399bf1b296506b2cd4a (patch)
treef84750fa6cac954d5e31221fc47a54c655fc06a9 /components/layout
parent230ce796836c25df26c130dbcd616ef97d12b2ec (diff)
로그인 및 미들웨어 처리. 구조 변경
Diffstat (limited to 'components/layout')
-rw-r--r--components/layout/GroupedMenuRender.tsx80
-rw-r--r--components/layout/Header.tsx215
-rw-r--r--components/layout/MobileMenu.tsx8
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">