diff --git a/docs/migration/production/data-sync/05_mbom_menu_desc.sql b/docs/migration/production/data-sync/05_mbom_menu_desc.sql
new file mode 100644
index 00000000..5a4201fb
--- /dev/null
+++ b/docs/migration/production/data-sync/05_mbom_menu_desc.sql
@@ -0,0 +1,8 @@
+-- ============================================================
+-- M-BOM 관리 메뉴 menu_desc 보강 (PageHeader 자동 매칭용)
+-- 2026-05-13
+-- ============================================================
+
+UPDATE menu_info
+ SET menu_desc = '생산용 BOM 트리 + read-only 조회 (운영판 mBomMgmtList 1:1)'
+ WHERE objid IN (100016, 100032);
diff --git a/frontend/app/(main)/COMPANY_16/production/mbom/page.tsx b/frontend/app/(main)/COMPANY_16/production/mbom/page.tsx
index 42bdd7c0..775b5a4f 100644
--- a/frontend/app/(main)/COMPANY_16/production/mbom/page.tsx
+++ b/frontend/app/(main)/COMPANY_16/production/mbom/page.tsx
@@ -15,6 +15,7 @@ import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
import { CustomerSelect } from "@/components/common/CustomerSelect";
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
+import { PageHeader } from "@/components/common/PageHeader";
import { apiClient } from "@/lib/api/client";
import { mbomApi, MbomListFilter, MbomRow } from "@/lib/api/mbom";
import { MbomDetailDialog } from "@/components/production/MbomDetailDialog";
@@ -140,6 +141,7 @@ export default function MbomMgmtPage() {
return (
+
+ *
+ * 액션 슬롯:
+ *
>} />
+ *
+ * 원칙:
+ * - 모든 page.tsx 의 최상위 자식으로 를 배치한다.
+ * - 메뉴명이 menu_info 에 있다면 props 없이도 자동 매칭되므로 그냥 면 충분.
+ * - 액션 버튼이 있으면 actions 슬롯에만 넣는다 (검색 영역에 두지 말 것).
+ */
+
+import React, { useMemo } from "react";
+import { usePathname } from "next/navigation";
+import { useMenu } from "@/contexts/MenuContext";
+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(파라미터 라우트 대응).
+ for (const m of menus) {
+ if (m.menu_url && m.menu_url === pathname) 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) {
+ best = m;
+ bestLen = m.menu_url.length;
+ }
+ }
+ return best;
+}
+
+export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
+ const pathname = usePathname() ?? "";
+ // useMenu() 가 Provider 밖에서 호출되면 throw — 안전하게 try
+ let menu: MenuItem | null = null;
+ try {
+ const { userMenus, adminMenus } = useMenu();
+ menu = findByUrl(userMenus, pathname) ?? findByUrl(adminMenus, pathname);
+ } catch {
+ /* MenuProvider 밖 (스토리북/테스트 등) — 자동 매칭 생략 */
+ }
+
+ const resolvedTitle = title ?? menu?.menu_name_kor ?? "";
+ const resolvedDesc = description ?? menu?.menu_desc ?? "";
+
+ if (!resolvedTitle && !resolvedDesc && !actions) return null;
+
+ return (
+
+
+ {resolvedTitle && (
+
{resolvedTitle}
+ )}
+ {resolvedDesc && (
+
{resolvedDesc}
+ )}
+
+ {actions &&
{actions}
}
+
+ );
+}