185 lines
5.6 KiB
TypeScript
185 lines
5.6 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState, useEffect, useCallback } from "react";
|
||
|
|
import { useRouter } from "next/navigation";
|
||
|
|
import { MenuItem, MenuState } from "@/types/menu";
|
||
|
|
import { LAYOUT_CONFIG } from "@/constants/layout";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 메뉴 관련 비즈니스 로직을 관리하는 커스텀 훅
|
||
|
|
*/
|
||
|
|
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[] => {
|
||
|
|
console.log("빌드 메뉴 트리 - 원본 메뉴 아이템들:", menuItems);
|
||
|
|
|
||
|
|
const menuMap = new Map<string, MenuItem>();
|
||
|
|
const rootMenus: MenuItem[] = [];
|
||
|
|
|
||
|
|
// 모든 메뉴를 맵에 저장 (ID를 문자열로 변환)
|
||
|
|
menuItems.forEach((menu) => {
|
||
|
|
const objId = String(menu.OBJID);
|
||
|
|
const parentId = String(menu.PARENT_OBJ_ID);
|
||
|
|
console.log(`메뉴 처리: ${menu.MENU_NAME_KOR}, OBJID: ${objId}, PARENT_OBJ_ID: ${parentId}`);
|
||
|
|
menuMap.set(objId, { ...menu, OBJID: objId, PARENT_OBJ_ID: parentId, children: [] });
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log("메뉴 맵 생성 완료, 총 메뉴 수:", menuMap.size);
|
||
|
|
|
||
|
|
// 부모-자식 관계 설정
|
||
|
|
menuItems.forEach((menu) => {
|
||
|
|
const objId = String(menu.OBJID);
|
||
|
|
const parentId = String(menu.PARENT_OBJ_ID);
|
||
|
|
const menuItem = menuMap.get(objId)!;
|
||
|
|
|
||
|
|
// PARENT_OBJ_ID가 특정 값이 아닌 경우 (루트가 아닌 경우)
|
||
|
|
if (parentId !== "-395553955") {
|
||
|
|
const parent = menuMap.get(parentId);
|
||
|
|
if (parent) {
|
||
|
|
parent.children = parent.children || [];
|
||
|
|
parent.children.push(menuItem);
|
||
|
|
console.log(`자식 메뉴 추가: ${menu.MENU_NAME_KOR} -> ${parent.MENU_NAME_KOR}`);
|
||
|
|
} else {
|
||
|
|
console.log(`부모 메뉴를 찾을 수 없음: ${menu.MENU_NAME_KOR}, 부모 ID: ${parentId}`);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
rootMenus.push(menuItem);
|
||
|
|
console.log(`루트 메뉴 추가: ${menu.MENU_NAME_KOR}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log("루트 메뉴 개수:", rootMenus.length);
|
||
|
|
console.log(
|
||
|
|
"최종 루트 메뉴들:",
|
||
|
|
rootMenus.map((m) => m.MENU_NAME_KOR),
|
||
|
|
);
|
||
|
|
|
||
|
|
return rootMenus.sort((a, b) => (a.SEQ || 0) - (b.SEQ || 0));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 메뉴 데이터 로드
|
||
|
|
*/
|
||
|
|
const loadMenuData = useCallback(async () => {
|
||
|
|
try {
|
||
|
|
// JWT 토큰 가져오기
|
||
|
|
const token = localStorage.getItem("authToken");
|
||
|
|
|
||
|
|
if (!token) {
|
||
|
|
console.error("JWT 토큰이 없습니다.");
|
||
|
|
router.push("/login");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 메뉴 목록 조회
|
||
|
|
const menuResponse = await fetch(`${LAYOUT_CONFIG.API_BASE_URL}${LAYOUT_CONFIG.ENDPOINTS.USER_MENUS}`, {
|
||
|
|
method: "GET",
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${token}`,
|
||
|
|
Accept: "application/json",
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
if (menuResponse.ok) {
|
||
|
|
const menuResult = await menuResponse.json();
|
||
|
|
console.log("메뉴 응답 데이터:", menuResult);
|
||
|
|
|
||
|
|
if (menuResult.success && menuResult.data) {
|
||
|
|
console.log("메뉴 데이터 배열:", menuResult.data);
|
||
|
|
const convertedMenuData = convertToUpperCaseKeys(menuResult.data || []);
|
||
|
|
console.log("변환된 메뉴 데이터:", convertedMenuData);
|
||
|
|
|
||
|
|
setMenuState((prev: MenuState) => ({
|
||
|
|
...prev,
|
||
|
|
menuList: buildMenuTree(convertedMenuData),
|
||
|
|
isLoading: false,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
} else if (menuResponse.status === 401) {
|
||
|
|
// 인증 실패 시 토큰 제거 및 로그인 페이지로 리다이렉트
|
||
|
|
localStorage.removeItem("authToken");
|
||
|
|
router.push("/login");
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("메뉴 데이터 로드 실패:", error);
|
||
|
|
localStorage.removeItem("authToken");
|
||
|
|
router.push("/login");
|
||
|
|
setMenuState((prev: MenuState) => ({ ...prev, isLoading: false }));
|
||
|
|
}
|
||
|
|
}, [router, 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(
|
||
|
|
(menu: MenuItem) => {
|
||
|
|
if (menu.children && menu.children.length > 0) {
|
||
|
|
toggleMenu(String(menu.OBJID));
|
||
|
|
} else if (menu.MENU_URL) {
|
||
|
|
// URL이 있는 경우 해당 페이지로 이동
|
||
|
|
router.push(menu.MENU_URL);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[toggleMenu, router],
|
||
|
|
);
|
||
|
|
|
||
|
|
// 사용자 정보가 있고 로딩이 완료되면 메뉴 데이터 로드
|
||
|
|
useEffect(() => {
|
||
|
|
if (user && !authLoading) {
|
||
|
|
loadMenuData();
|
||
|
|
}
|
||
|
|
}, [user, authLoading, loadMenuData]);
|
||
|
|
|
||
|
|
return {
|
||
|
|
menuList: menuState.menuList,
|
||
|
|
expandedMenus: menuState.expandedMenus,
|
||
|
|
isMenuLoading: menuState.isLoading,
|
||
|
|
handleMenuClick,
|
||
|
|
toggleMenu,
|
||
|
|
};
|
||
|
|
};
|