From 5b6313f16f508882a0ea67716b7dbaa1c6967f04 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 30 Jun 2025 08:28:13 +0000 Subject: (대표님) 20250630 16시 - 유저 도메인별 라우터 분리와 보안성검토 대응 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/layout/GroupedMenuRender.tsx | 53 +++++++++----- components/layout/Header.tsx | 117 ++++++++++++++++++++++++------- components/layout/MobileMenu.tsx | 87 +++++++++++++---------- components/layout/user-profile-badge.tsx | 62 ++++++++++++++++ 4 files changed, 241 insertions(+), 78 deletions(-) create mode 100644 components/layout/user-profile-badge.tsx (limited to 'components') diff --git a/components/layout/GroupedMenuRender.tsx b/components/layout/GroupedMenuRender.tsx index e2a5a225..9006c85d 100644 --- a/components/layout/GroupedMenuRender.tsx +++ b/components/layout/GroupedMenuRender.tsx @@ -4,6 +4,7 @@ import { NavigationMenuLink } from "@/components/ui/navigation-menu"; import { cn } from "@/lib/utils"; import * as LucideIcons from "lucide-react"; import { MenuItem } from '@/config/menuConfig'; +import { filterActiveAdditionalMenus } from "@/hooks/use-active-menus"; type GroupedMenuItems = { [key: string]: MenuItem[]; @@ -12,9 +13,15 @@ type GroupedMenuItems = { interface GroupedMenuRendererProps { items: MenuItem[]; lng: string; + activeMenus?: Record; // 활성 메뉴 상태 추가 } -const GroupedMenuRenderer = ({ items, lng }: GroupedMenuRendererProps) => { +const GroupedMenuRenderer = ({ items, lng, activeMenus = {} }: GroupedMenuRendererProps) => { + // 활성 메뉴만 필터링 (activeMenus가 빈 객체면 모든 메뉴 표시) + const filteredItems = Object.keys(activeMenus).length > 0 + ? filterActiveAdditionalMenus(items, activeMenus) + : items; + // 그룹별로 아이템 분류 const groupItems = (items: MenuItem[]): GroupedMenuItems => { return items.reduce((groups, item) => { @@ -27,32 +34,44 @@ const GroupedMenuRenderer = ({ items, lng }: GroupedMenuRendererProps) => { }, {} as GroupedMenuItems); }; - const groupedItems = groupItems(items); + const groupedItems = groupItems(filteredItems); const groups = Object.keys(groupedItems); + // 활성 메뉴가 없으면 아무것도 렌더링하지 않음 + if (filteredItems.length === 0) { + return ( +
+

+ 사용 가능한 메뉴가 없습니다. +

+
+ ); + } + return (
- {groups.map((groupName, index) => ( -
- {groupName !== 'default' && ( -

{groupName}

- )} -
- {groupedItems[groupName].map((item) => ( - - ))} + {groups.map((groupName, index) => { + // 빈 그룹은 건너뛰기 + if (groupedItems[groupName].length === 0) return null; + + return ( +
+ {groupName !== 'default' && ( +

{groupName}

+ )} +
+ {groupedItems[groupName].map((item) => ( + + ))} +
-
- ))} + ); + })}
); }; const MenuListItem = ({ item, lng }: { item: MenuItem; lng: string }) => { - - - - return ( word[0]?.toUpperCase()) .join(""); - - const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); // 모바일 메뉴 상태 + const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); const toggleMobileMenu = () => { setIsMobileMenuOpen(!isMobileMenuOpen); }; - const isPartnerRoute = pathname?.includes("/partners"); + // 도메인별 메뉴 및 브랜딩 정보 가져오기 + const getDomainConfig = (pathname: string) => { + if (pathname?.includes("/partners")) { + return { + main: mainNavVendor, + additional: additionalNavVendor, + logoHref: `/${lng}/partners`, + brandName: "eVCP Partners", + basePath: `/${lng}/partners` + }; + } + + if (pathname?.includes("/procurement")) { + return { + main: procurementNav, + additional: additional2Nav, + logoHref: `/${lng}/procurement`, + brandName: "eVCP 구매관리", + basePath: `/${lng}/procurement` + }; + } + + if (pathname?.includes("/sales")) { + return { + main: salesNav, + additional: additional2Nav, + logoHref: `/${lng}/sales`, + brandName: "eVCP 기술영업", + basePath: `/${lng}/sales` + }; + } + + if (pathname?.includes("/engineering")) { + return { + main: engineeringNav, + additional: additional2Nav, + logoHref: `/${lng}/engineering`, + brandName: "eVCP 설계관리", + basePath: `/${lng}/engineering` + }; + } + + // 기본값: /evcp (전체 메뉴) + return { + main: mainNav, + additional: additionalNav, + logoHref: `/${lng}/evcp`, + brandName: "eVCP 삼성중공업", + basePath: `/${lng}/evcp` + }; + }; - const main = isPartnerRoute ? mainNavVendor : mainNav; - const additional = isPartnerRoute ? additionalNavVendor : additionalNav; + const { main: originalMain, additional: originalAdditional, logoHref, brandName, basePath } = getDomainConfig(pathname); - const basePath = `/${lng}${isPartnerRoute ? "/partners" : "/evcp"}`; + // 활성 메뉴만 필터링 (로딩 중이거나 에러 시에는 모든 메뉴 표시) + const main = isLoading ? originalMain : filterActiveMenus(originalMain, activeMenus); + const additional = isLoading ? originalAdditional : filterActiveAdditionalMenus(originalAdditional, activeMenus); return ( <> - {/*
*/}
@@ -88,9 +150,9 @@ export function Header() { Toggle Menu - {/* 로고 영역 - 항상 표시 */} + {/* 로고 영역 - 도메인별 브랜딩 */}
- + - {isPartnerRoute - ? "eVCP Partners" - : pathname?.includes("/evcp") - ? "eVCP 삼성중공업" - : "eVCP"} + {brandName}
- {/* 네비게이션 메뉴 - 내부 스크롤 적용, 드롭다운은 제약 없이 표시 */} + {/* 네비게이션 메뉴 - 도메인별 활성화된 메뉴만 표시 */}
- {/* NavigationMenu는 z-index를 높게 설정하여 드롭다운이 제대로 표시되도록 함 */} - {/* 스크롤 가능한 메뉴 리스트 컨테이너 */}
{main.map((section: MenuSection) => ( @@ -124,7 +180,11 @@ export function Header() { {/* 그룹핑이 필요한 메뉴의 경우 GroupedMenuRenderer 사용 */} {section.useGrouping ? ( - + ) : ( @@ -144,7 +204,7 @@ export function Header() { ))} - {/* 추가 네비게이션 항목 */} + {/* 추가 네비게이션 항목 - 도메인별 활성화된 것만 */} {additional.map((item) => ( @@ -164,7 +224,7 @@ export function Header() {
- {/* 우측 영역 - 고정 너비와 우선순위로 항상 표시되도록 함 */} + {/* 우측 영역 */}
{/* 데스크탑에서는 CommandMenu, 모바일에서는 검색 아이콘만 */}
@@ -177,11 +237,10 @@ export function Header() { {/* 알림 버튼 */} - {/* 사용자 메뉴 (DropdownMenu) */} + {/* 사용자 메뉴 */} @@ -207,8 +266,16 @@ export function Header() {
- {/* 모바일 메뉴 */} - {isMobileMenuOpen && } + {/* 모바일 메뉴 - 도메인별 활성화된 메뉴만 전달 */} + {isMobileMenuOpen && ( + + )}
); diff --git a/components/layout/MobileMenu.tsx b/components/layout/MobileMenu.tsx index 2e70aeba..dc02d2e3 100644 --- a/components/layout/MobileMenu.tsx +++ b/components/layout/MobileMenu.tsx @@ -5,29 +5,42 @@ import * as React from "react"; import Link from "next/link"; import { useRouter, usePathname } from "next/navigation"; -import { MenuSection, mainNav, additionalNav, MenuItem, mainNavVendor, additionalNavVendor } from "@/config/menuConfig"; +import { MenuSection, MenuItem } from "@/config/menuConfig"; import { cn } from "@/lib/utils"; import { Drawer, DrawerContent, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; import { Button } from "@/components/ui/button"; +import { filterActiveMenus, filterActiveAdditionalMenus } from "@/hooks/use-active-menus"; interface MobileMenuProps { lng: string; onClose: () => void; + activeMenus?: Record; + domainMain?: MenuSection[]; // 헤더에서 계산된 도메인별 메인 메뉴 + domainAdditional?: MenuItem[]; // 헤더에서 계산된 도메인별 추가 메뉴 } -export function MobileMenu({ lng, onClose }: MobileMenuProps) { +export function MobileMenu({ + lng, + onClose, + activeMenus = {}, + domainMain = [], + domainAdditional = [] +}: MobileMenuProps) { const router = useRouter(); - - + const handleLinkClick = (href: string) => { router.push(href); onClose(); }; - const pathname = usePathname(); - const isPartnerRoute = pathname?.includes("/partners"); - const main = isPartnerRoute ? mainNavVendor : mainNav; - const additional = isPartnerRoute ? additionalNavVendor : additionalNav; + // 활성 메뉴만 필터링 (activeMenus가 빈 객체면 모든 메뉴 표시) + const main = Object.keys(activeMenus).length > 0 + ? filterActiveMenus(domainMain, activeMenus) + : domainMain; + + const additional = Object.keys(activeMenus).length > 0 + ? filterActiveAdditionalMenus(domainAdditional, activeMenus) + : domainAdditional; return ( @@ -36,42 +49,44 @@ export function MobileMenu({ lng, onClose }: MobileMenuProps) {
-