6f73631c7c
발주서관리 리스트:
- /purchase/order-wace 임시 라우트 → /purchase/order 통합 (기존 vexplor
변형판 대체). order-wace 폴더 삭제.
- 백엔드 라우트 /order-wace → /order-list, 함수 listPurchaseOrderWace →
listPurchaseOrderList, API 클라이언트 listOrderWace → listOrder.
발주서 폼 (general 양식) GET API:
- services/purchaseOrderFormService.ts 신규 (getPurchaseOrderFormInit,
getPurchaseOrderForm). 품의서 자동채움 = salesMng.getProposalPartList
매퍼 1:1 → 발주 그리드 형식 변환. 발주번호 채번 RPS{YY}-{MMDD}-{NN}.
- 컨트롤러/라우트: GET /api/purchase/order-form/init?proposal_objid=...
+ /api/purchase/order-form/:objid.
- RPS는 OBJID가 varchar라 wace numeric 캐스트 모두 제거.
PageHeader 컨벤션 일괄 변경:
- 자동매칭이 매칭된 menu의 parent_obj_id로 부모를 찾아
"{부모}_{자식}" 형식 표기 (wace 컨벤션). 부모가 루트 그룹이면 자식만.
- description prop과 렌더링 완전 제거 (사용처 없음 확인).
- 모든 메뉴 페이지에 일괄 적용.
DB(별도): menu_info 9857401373575 + rel_menu_auth 3건 제거.
저장/삭제 API + 프론트 다이얼로그는 다음 세션.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
5.6 KiB
TypeScript
151 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* PageHeader — 페이지 상단 "대메뉴_중메뉴" 제목 + 액션/검색 슬롯.
|
|
*
|
|
* 모든 RPS 메뉴 페이지의 상단에 의무 배치.
|
|
*
|
|
* 자동 매칭 (탭 시스템 대응):
|
|
* - RPS 는 탭 기반이라 usePathname() 이 /main 으로 고정됨.
|
|
* - useTabStore 의 활성 탭 adminUrl → /COMPANY_NN prefix 제거 → menu_info.menu_url 매칭.
|
|
* - 매칭된 menu 의 parent_obj_id 로 부모 메뉴를 찾아 "{부모}_{자식}" 으로 표기 (wace 컨벤션).
|
|
* - 루트 그룹(parent_obj_id 가 0뎁스)이면 자식만 단독 표기.
|
|
*
|
|
* 명시 지정:
|
|
* <PageHeader title="M-BOM 관리" actions={...} />
|
|
*
|
|
* 원칙: menu_info 에 등록만 되어 있으면 props 없이도 자동 매칭.
|
|
*/
|
|
|
|
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 { Button } from "@/components/ui/button";
|
|
import { Search, Loader2, RotateCcw } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface PageHeaderProps {
|
|
title?: string;
|
|
/** 업무 액션 슬롯 (등록/삭제/상신 등). 검색·초기화는 onSearch/onReset 로 전달. */
|
|
actions?: React.ReactNode;
|
|
/** 검색 핸들러. 지정 시 우측에 검색 버튼 자동 렌더. */
|
|
onSearch?: () => void;
|
|
/** 초기화 핸들러. 지정 시 우측에 초기화 버튼 자동 렌더. */
|
|
onReset?: () => void;
|
|
/** 검색 중 로딩 표시 */
|
|
loading?: boolean;
|
|
searchLabel?: string;
|
|
resetLabel?: string;
|
|
className?: string;
|
|
}
|
|
|
|
function stripCompanyPrefix(p: string): string {
|
|
return p.replace(/^\/COMPANY_\d+/, "") || "/";
|
|
}
|
|
|
|
function findParentMenu(menus: MenuItem[], menu: MenuItem | null): MenuItem | null {
|
|
if (!menu) return null;
|
|
const pid = menu.parent_obj_id ?? menu.PARENT_OBJ_ID;
|
|
if (!pid) return null;
|
|
for (const m of menus) {
|
|
const oid = m.objid ?? m.OBJID;
|
|
if (oid && String(oid) === String(pid)) return m;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findByUrl(menus: MenuItem[], strippedUrl: string): MenuItem | null {
|
|
// menu_info.menu_url 이 /COMPANY_16/... 으로 저장되어 있으므로 양쪽 비교
|
|
for (const m of menus) {
|
|
if (!m.menu_url) continue;
|
|
if (m.menu_url === strippedUrl) return m;
|
|
if (stripCompanyPrefix(m.menu_url) === strippedUrl) return m;
|
|
}
|
|
let best: MenuItem | null = null;
|
|
let bestLen = 0;
|
|
for (const m of menus) {
|
|
if (!m.menu_url) continue;
|
|
const stripped = stripCompanyPrefix(m.menu_url);
|
|
if (strippedUrl.startsWith(stripped) && stripped.length > bestLen) {
|
|
best = m;
|
|
bestLen = stripped.length;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
export function PageHeader({
|
|
title, actions, onSearch, onReset, loading,
|
|
searchLabel = "검색", resetLabel = "초기화", className,
|
|
}: PageHeaderProps) {
|
|
const pathname = usePathname() ?? "";
|
|
const tabs = useTabStore(selectTabs);
|
|
const activeTabId = useTabStore(selectActiveTabId);
|
|
|
|
let menu: MenuItem | null = null;
|
|
let parentMenu: MenuItem | null = null;
|
|
try {
|
|
const { userMenus, adminMenus } = useMenu();
|
|
// 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);
|
|
}
|
|
}
|
|
const allMenus = [...(userMenus as MenuItem[]), ...(adminMenus as MenuItem[])];
|
|
menu = findByUrl(userMenus as MenuItem[], targetUrl) ?? findByUrl(adminMenus as MenuItem[], targetUrl);
|
|
parentMenu = findParentMenu(allMenus, menu);
|
|
} catch {
|
|
/* Provider 밖 — 자동 매칭 생략 */
|
|
}
|
|
|
|
// wace 컨벤션: "대메뉴_중메뉴" (parent_obj_id 가 루트 그룹이면 단독 표기)
|
|
const parentName = parentMenu?.menu_name_kor ?? parentMenu?.MENU_NAME_KOR ?? "";
|
|
const ownName = menu?.menu_name_kor ?? menu?.MENU_NAME_KOR ?? "";
|
|
const parentParentPid = parentMenu?.parent_obj_id ?? parentMenu?.PARENT_OBJ_ID;
|
|
// 부모의 부모가 있어야 (즉, 부모가 1뎁스 그룹) "부모_자식" 표기. 부모 없거나 부모가 루트이면 자식만.
|
|
const autoTitle = parentName && parentParentPid && ownName
|
|
? `${parentName}_${ownName}`
|
|
: ownName;
|
|
const resolvedTitle = title ?? autoTitle;
|
|
|
|
const hasSearchButtons = !!(onSearch || onReset);
|
|
if (!resolvedTitle && !actions && !hasSearchButtons) return null;
|
|
|
|
return (
|
|
<div className={cn("flex flex-shrink-0 items-center justify-between gap-3 border-b pb-2", className)}>
|
|
<div>
|
|
{resolvedTitle && (
|
|
<h1 className="text-lg font-bold tracking-tight">{resolvedTitle}</h1>
|
|
)}
|
|
</div>
|
|
{(actions || hasSearchButtons) && (
|
|
<div className="flex items-center gap-1.5">
|
|
{actions}
|
|
{hasSearchButtons && (
|
|
<>
|
|
{onReset && (
|
|
<Button size="sm" variant="outline" className="h-8 gap-1 px-2 text-xs" onClick={onReset}>
|
|
<RotateCcw className="h-3.5 w-3.5" />
|
|
{resetLabel}
|
|
</Button>
|
|
)}
|
|
{onSearch && (
|
|
<Button size="sm" className="h-8 gap-1 px-2 text-xs" onClick={onSearch} disabled={loading}>
|
|
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Search className="h-3.5 w-3.5" />}
|
|
{searchLabel}
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|