공용 — PageHeader 탭 시스템 대응 (활성 탭 adminUrl 매칭)
RPS 는 탭 기반 라우터라 usePathname() 이 /main 으로 고정됨. 사용자 보고: M-BOM 페이지에서 PageHeader 가 메뉴명을 못 잡아 빈 상태. 수정: useCurrent2ndLevelMenuObjid 와 동일 패턴 적용 - useTabStore.selectTabs / selectActiveTabId 로 활성 탭 조회 - pathname='/main' 이면 활성 탭의 adminUrl 로 매칭 - stripCompanyPrefix 로 /COMPANY_NN 무시 → menu_info.menu_url 양방향 비교 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,50 +5,52 @@
|
||||
*
|
||||
* customer-cs/cs 페이지 패턴 1:1 추출. 모든 RPS 메뉴 페이지의 상단에 의무 배치.
|
||||
*
|
||||
* 자동 매칭:
|
||||
* - usePathname() 으로 현재 경로를 잡고 MenuContext 의 userMenus/adminMenus 에서
|
||||
* menu_url 이 일치하는 항목을 찾아 menu_name_kor / menu_desc 를 표시.
|
||||
* 자동 매칭 (탭 시스템 대응):
|
||||
* - RPS 는 탭 기반이라 usePathname() 이 /main 으로 고정됨.
|
||||
* - useTabStore 의 활성 탭 adminUrl → /COMPANY_NN prefix 제거 → menu_info.menu_url 매칭.
|
||||
* - useCurrent2ndLevelMenuObjid 와 동일 패턴.
|
||||
*
|
||||
* 명시 지정:
|
||||
* <PageHeader title="M-BOM 관리" description="생산용 BOM 트리 + 본 편집" />
|
||||
*
|
||||
* 액션 슬롯:
|
||||
* <PageHeader actions={<><Button>새로고침</Button><Button>등록</Button></>} />
|
||||
* <PageHeader title="M-BOM 관리" description="생산용 BOM 트리" actions={...} />
|
||||
*
|
||||
* 원칙:
|
||||
* - 모든 page.tsx 의 최상위 자식으로 <PageHeader /> 를 배치한다.
|
||||
* - 메뉴명이 menu_info 에 있다면 props 없이도 자동 매칭되므로 그냥 <PageHeader /> 면 충분.
|
||||
* - 액션 버튼이 있으면 actions 슬롯에만 넣는다 (검색 영역에 두지 말 것).
|
||||
* - menu_info 에 등록만 되어 있으면 props 없이도 자동 매칭.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import React from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMenu } from "@/contexts/MenuContext";
|
||||
import { useTabStore, selectTabs, selectActiveTabId } from "@/stores/tabStore";
|
||||
import type { MenuItem } from "@/lib/api/menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PageHeaderProps {
|
||||
/** 명시 메뉴명. 없으면 usePathname() + MenuContext 자동 매칭. */
|
||||
title?: string;
|
||||
/** 명시 설명. 없으면 자동 매칭 결과의 menu_desc. */
|
||||
description?: string;
|
||||
/** 우측 액션 슬롯 (새로고침/등록/엑셀 다운로드 등) */
|
||||
actions?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function findByUrl(menus: MenuItem[], pathname: string): MenuItem | null {
|
||||
// MenuContext 는 flat 리스트. 정확 매칭 우선, 없으면 prefix(파라미터 라우트 대응).
|
||||
function stripCompanyPrefix(p: string): string {
|
||||
return p.replace(/^\/COMPANY_\d+/, "") || "/";
|
||||
}
|
||||
|
||||
function findByUrl(menus: MenuItem[], strippedUrl: string): MenuItem | null {
|
||||
// menu_info.menu_url 이 /COMPANY_16/... 으로 저장되어 있으므로 양쪽 비교
|
||||
for (const m of menus) {
|
||||
if (m.menu_url && m.menu_url === pathname) return m;
|
||||
if (!m.menu_url) continue;
|
||||
if (m.menu_url === strippedUrl) return m;
|
||||
if (stripCompanyPrefix(m.menu_url) === strippedUrl) return m;
|
||||
}
|
||||
// 동적 라우트 fallback: /COMPANY_16/foo/123 → /COMPANY_16/foo 매칭
|
||||
let best: MenuItem | null = null;
|
||||
let bestLen = 0;
|
||||
for (const m of menus) {
|
||||
if (m.menu_url && pathname.startsWith(m.menu_url) && m.menu_url.length > bestLen) {
|
||||
if (!m.menu_url) continue;
|
||||
const stripped = stripCompanyPrefix(m.menu_url);
|
||||
if (strippedUrl.startsWith(stripped) && stripped.length > bestLen) {
|
||||
best = m;
|
||||
bestLen = m.menu_url.length;
|
||||
bestLen = stripped.length;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
@@ -56,13 +58,24 @@ function findByUrl(menus: MenuItem[], pathname: string): MenuItem | null {
|
||||
|
||||
export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
|
||||
const pathname = usePathname() ?? "";
|
||||
// useMenu() 가 Provider 밖에서 호출되면 throw — 안전하게 try
|
||||
const tabs = useTabStore(selectTabs);
|
||||
const activeTabId = useTabStore(selectActiveTabId);
|
||||
|
||||
let menu: MenuItem | null = null;
|
||||
try {
|
||||
const { userMenus, adminMenus } = useMenu();
|
||||
menu = findByUrl(userMenus, pathname) ?? findByUrl(adminMenus, pathname);
|
||||
// RPS 탭 시스템: pathname=/main 이면 활성 탭의 adminUrl 사용
|
||||
let targetUrl = stripCompanyPrefix(pathname);
|
||||
const isRootLike = pathname === "/main" || pathname === "/" || pathname === "";
|
||||
if (isRootLike) {
|
||||
const activeTab = tabs.find((t: any) => t.id === activeTabId);
|
||||
if (activeTab?.adminUrl) {
|
||||
targetUrl = stripCompanyPrefix(activeTab.adminUrl);
|
||||
}
|
||||
}
|
||||
menu = findByUrl(userMenus as MenuItem[], targetUrl) ?? findByUrl(adminMenus as MenuItem[], targetUrl);
|
||||
} catch {
|
||||
/* MenuProvider 밖 (스토리북/테스트 등) — 자동 매칭 생략 */
|
||||
/* Provider 밖 — 자동 매칭 생략 */
|
||||
}
|
||||
|
||||
const resolvedTitle = title ?? menu?.menu_name_kor ?? "";
|
||||
|
||||
Reference in New Issue
Block a user