1760045634
- wace_plm contract_mgmt/contract_item/contract_item_serial/contract_mgmt_option/estimate_template/estimate_template_item/mail_log/sales_registration/shipment_log 9개 테이블 DDL을 vexplor_rps에 적재, 운영 데이터 복사 - 거래처: Wehago/Amaranth ERP api16S11 INBOUND 동기화 결과(customer_code) 기준 LEFT JOIN으로 변경, 25/25 매칭 - 품목: wace part_mng 8,179건을 item_info(varchar id)에 wace objid 그대로 INSERT, contract_item 72/72 매칭 - 공통코드: wace comm_code 847건 복제 + backend SQL에 5종 LEFT JOIN - DataGrid에 formatMoney(천단위콤마+소수점2자리) / formatNumber 자동 우측정렬 분리 - adminService.getUserMenuList company_code 분기 제거(RPS 단독), useMenu.buildMenuTree root 식별 보강 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
175 lines
5.4 KiB
TypeScript
175 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { MenuItem, MenuState } from "@/types/menu";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { AuthLogger } from "@/lib/authLogger";
|
|
|
|
/**
|
|
* 메뉴 관련 비즈니스 로직을 관리하는 커스텀 훅
|
|
* - API 호출은 apiClient를 통해 수행 (토큰 관리/401 처리 자동)
|
|
* - 메뉴 로드 실패 시 토큰 삭제/리다이렉트하지 않음 (client.ts가 처리)
|
|
*/
|
|
export const useMenu = (user: any, authLoading: boolean) => {
|
|
const router = useRouter();
|
|
|
|
const [menuState, setMenuState] = useState<MenuState>({
|
|
menuList: [],
|
|
expandedMenus: new Set(),
|
|
isLoading: true,
|
|
});
|
|
|
|
/**
|
|
* 데이터 키를 대문자로 변환하는 함수
|
|
*/
|
|
const convertToUpperCaseKeys = useCallback((data: Record<string, unknown>[]): MenuItem[] => {
|
|
return data.map((item) => {
|
|
const converted: Record<string, unknown> = {};
|
|
Object.keys(item).forEach((key) => {
|
|
const upperKey = key.toUpperCase();
|
|
converted[upperKey] = item[key];
|
|
});
|
|
return converted as unknown as MenuItem;
|
|
});
|
|
}, []);
|
|
|
|
/**
|
|
* 메뉴 트리 구조 생성
|
|
*/
|
|
const buildMenuTree = useCallback((menuItems: MenuItem[]): MenuItem[] => {
|
|
const menuMap = new Map<string, MenuItem>();
|
|
const rootMenus: MenuItem[] = [];
|
|
|
|
menuItems.forEach((menu) => {
|
|
const objId = String(menu.OBJID);
|
|
const parentId = String(menu.PARENT_OBJ_ID);
|
|
menuMap.set(objId, { ...menu, OBJID: objId, PARENT_OBJ_ID: parentId, children: [] });
|
|
});
|
|
|
|
menuItems.forEach((menu) => {
|
|
const objId = String(menu.OBJID);
|
|
const parentId = String(menu.PARENT_OBJ_ID);
|
|
const menuItem = menuMap.get(objId)!;
|
|
|
|
// root 식별: 매직 넘버("-395553955") · "0" · "" · "null" · 부모를 menuMap에서 못 찾는 orphan
|
|
const isRoot =
|
|
parentId === "-395553955" ||
|
|
parentId === "0" ||
|
|
parentId === "" ||
|
|
parentId === "null" ||
|
|
parentId === "undefined" ||
|
|
!menuMap.has(parentId);
|
|
|
|
if (isRoot) {
|
|
rootMenus.push(menuItem);
|
|
} else {
|
|
const parent = menuMap.get(parentId)!;
|
|
parent.children = parent.children || [];
|
|
parent.children.push(menuItem);
|
|
}
|
|
});
|
|
|
|
return rootMenus.sort((a, b) => (a.SEQ || 0) - (b.SEQ || 0));
|
|
}, []);
|
|
|
|
/**
|
|
* 메뉴 데이터 로드
|
|
* - apiClient 사용으로 토큰/401 자동 처리
|
|
* - 실패 시 빈 메뉴 유지 (로그인 리다이렉트는 client.ts 인터셉터가 담당)
|
|
*/
|
|
const loadMenuData = useCallback(async () => {
|
|
try {
|
|
const response = await apiClient.get("/admin/user-menus");
|
|
|
|
if (response.data?.success && response.data?.data) {
|
|
const convertedMenuData = convertToUpperCaseKeys(response.data.data || []);
|
|
setMenuState((prev: MenuState) => ({
|
|
...prev,
|
|
menuList: buildMenuTree(convertedMenuData),
|
|
isLoading: false,
|
|
}));
|
|
} else {
|
|
setMenuState((prev: MenuState) => ({ ...prev, isLoading: false }));
|
|
}
|
|
} catch (err: any) {
|
|
AuthLogger.log("MENU_LOAD_FAIL", `메뉴 로드 실패: ${err?.response?.status || err?.message || "unknown"}`);
|
|
setMenuState((prev: MenuState) => ({ ...prev, isLoading: false }));
|
|
}
|
|
}, [convertToUpperCaseKeys, buildMenuTree]);
|
|
|
|
/**
|
|
* 메뉴 토글
|
|
*/
|
|
const toggleMenu = useCallback((menuId: string) => {
|
|
setMenuState((prev: MenuState) => {
|
|
const newExpanded = new Set(prev.expandedMenus);
|
|
if (newExpanded.has(menuId)) {
|
|
newExpanded.delete(menuId);
|
|
} else {
|
|
newExpanded.add(menuId);
|
|
}
|
|
return {
|
|
...prev,
|
|
expandedMenus: newExpanded,
|
|
};
|
|
});
|
|
}, []);
|
|
|
|
/**
|
|
* 메뉴 클릭 처리
|
|
*/
|
|
const handleMenuClick = useCallback(
|
|
async (menu: MenuItem) => {
|
|
if (menu.children && menu.children.length > 0) {
|
|
toggleMenu(String(menu.OBJID));
|
|
} else {
|
|
const menuName = menu.MENU_NAME_KOR || menu.menuNameKor || menu.TRANSLATED_NAME || "메뉴";
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem("currentMenuName", menuName);
|
|
}
|
|
|
|
try {
|
|
const menuObjid = menu.OBJID || menu.objid;
|
|
if (menuObjid) {
|
|
const { menuScreenApi } = await import("@/lib/api/screen");
|
|
const assignedScreens = await menuScreenApi.getScreensByMenu(parseInt(menuObjid.toString()));
|
|
|
|
if (assignedScreens.length > 0) {
|
|
const firstScreen = assignedScreens[0];
|
|
router.push(`/screens/${firstScreen.screenId}?menuObjid=${menuObjid}`);
|
|
return;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn("할당된 화면 조회 실패:", error);
|
|
}
|
|
|
|
if (menu.MENU_URL) {
|
|
router.push(menu.MENU_URL);
|
|
} else {
|
|
console.warn("메뉴에 URL이나 할당된 화면이 없습니다:", menu);
|
|
const { toast } = await import("sonner");
|
|
toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다.");
|
|
}
|
|
}
|
|
},
|
|
[toggleMenu, router],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (user && !authLoading) {
|
|
loadMenuData();
|
|
}
|
|
}, [user, authLoading, loadMenuData]);
|
|
|
|
return {
|
|
menuList: menuState.menuList,
|
|
expandedMenus: menuState.expandedMenus,
|
|
isMenuLoading: menuState.isLoading,
|
|
handleMenuClick,
|
|
toggleMenu,
|
|
refreshMenus: loadMenuData,
|
|
};
|
|
};
|