Merge branch 'ycshin-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
@@ -30,6 +30,9 @@ import { toast } from "sonner";
|
||||
import { ProfileModal } from "./ProfileModal";
|
||||
import { Logo } from "./Logo";
|
||||
import { SideMenu } from "./SideMenu";
|
||||
import { TabBar } from "./TabBar";
|
||||
import { TabContent } from "./TabContent";
|
||||
import { useTabStore } from "@/stores/tabStore";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -97,7 +100,8 @@ const getMenuIcon = (menuName: string, dbIconName?: string | null) => {
|
||||
};
|
||||
|
||||
// 메뉴 데이터를 UI용으로 변환하는 함수 (최상위 "사용자", "관리자" 제외)
|
||||
const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, parentId: string = "0"): any[] => {
|
||||
// parentPath: 탭 제목에 "기준정보 - 회사관리" 형태로 상위 카테고리를 포함하기 위한 경로
|
||||
const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, parentId: string = "0", parentPath: string = ""): any[] => {
|
||||
const filteredMenus = menus
|
||||
.filter((menu) => (menu.parent_obj_id || menu.PARENT_OBJ_ID) === parentId)
|
||||
.filter((menu) => (menu.status || menu.STATUS) === "active")
|
||||
@@ -110,40 +114,34 @@ const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, p
|
||||
for (const menu of filteredMenus) {
|
||||
const menuName = (menu.menu_name_kor || menu.MENU_NAME_KOR || "").toLowerCase();
|
||||
|
||||
// "사용자" 또는 "관리자" 카테고리면 하위 메뉴들을 직접 추가
|
||||
if (menuName.includes("사용자") || menuName.includes("관리자")) {
|
||||
const childMenus = convertMenuToUI(menus, userInfo, menu.objid || menu.OBJID);
|
||||
const childMenus = convertMenuToUI(menus, userInfo, menu.objid || menu.OBJID, "");
|
||||
allMenus.push(...childMenus);
|
||||
} else {
|
||||
// 일반 메뉴는 그대로 추가
|
||||
allMenus.push(convertSingleMenu(menu, menus, userInfo));
|
||||
allMenus.push(convertSingleMenu(menu, menus, userInfo, ""));
|
||||
}
|
||||
}
|
||||
|
||||
return allMenus;
|
||||
}
|
||||
|
||||
return filteredMenus.map((menu) => convertSingleMenu(menu, menus, userInfo));
|
||||
return filteredMenus.map((menu) => convertSingleMenu(menu, menus, userInfo, parentPath));
|
||||
};
|
||||
|
||||
// 단일 메뉴 변환 함수
|
||||
const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: ExtendedUserInfo | null): any => {
|
||||
const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: ExtendedUserInfo | null, parentPath: string = ""): any => {
|
||||
const menuId = menu.objid || menu.OBJID;
|
||||
|
||||
// 사용자 locale 기준으로 번역 처리
|
||||
const getDisplayText = (menu: MenuItem) => {
|
||||
// 다국어 텍스트가 있으면 사용, 없으면 기본 텍스트 사용
|
||||
if (menu.translated_name || menu.TRANSLATED_NAME) {
|
||||
return menu.translated_name || menu.TRANSLATED_NAME;
|
||||
const getDisplayText = (m: MenuItem) => {
|
||||
if (m.translated_name || m.TRANSLATED_NAME) {
|
||||
return m.translated_name || m.TRANSLATED_NAME;
|
||||
}
|
||||
|
||||
const baseName = menu.menu_name_kor || menu.MENU_NAME_KOR || "메뉴명 없음";
|
||||
|
||||
// 사용자 정보에서 locale 가져오기
|
||||
const baseName = m.menu_name_kor || m.MENU_NAME_KOR || "메뉴명 없음";
|
||||
const userLocale = userInfo?.locale || "ko";
|
||||
|
||||
if (userLocale === "EN") {
|
||||
// 영어 번역
|
||||
const translations: { [key: string]: string } = {
|
||||
관리자: "Administrator",
|
||||
사용자: "User Management",
|
||||
@@ -163,7 +161,6 @@ const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: Exten
|
||||
}
|
||||
}
|
||||
} else if (userLocale === "JA") {
|
||||
// 일본어 번역
|
||||
const translations: { [key: string]: string } = {
|
||||
관리자: "管理者",
|
||||
사용자: "ユーザー管理",
|
||||
@@ -183,7 +180,6 @@ const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: Exten
|
||||
}
|
||||
}
|
||||
} else if (userLocale === "ZH") {
|
||||
// 중국어 번역
|
||||
const translations: { [key: string]: string } = {
|
||||
관리자: "管理员",
|
||||
사용자: "用户管理",
|
||||
@@ -207,11 +203,15 @@ const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: Exten
|
||||
return baseName;
|
||||
};
|
||||
|
||||
const children = convertMenuToUI(allMenus, userInfo, menuId);
|
||||
const displayName = getDisplayText(menu);
|
||||
const tabTitle = parentPath ? `${parentPath} - ${displayName}` : displayName;
|
||||
|
||||
const children = convertMenuToUI(allMenus, userInfo, menuId, tabTitle);
|
||||
|
||||
return {
|
||||
id: menuId,
|
||||
name: getDisplayText(menu),
|
||||
name: displayName,
|
||||
tabTitle,
|
||||
icon: getMenuIcon(menu.menu_name_kor || menu.MENU_NAME_KOR || "", menu.menu_icon || menu.MENU_ICON),
|
||||
url: menu.menu_url || menu.MENU_URL || "#",
|
||||
children: children.length > 0 ? children : undefined,
|
||||
@@ -231,6 +231,28 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
const [showCompanySwitcher, setShowCompanySwitcher] = useState(false);
|
||||
const [currentCompanyName, setCurrentCompanyName] = useState<string>("");
|
||||
|
||||
// URL 직접 접근 시 탭 자동 열기 (북마크/공유 링크 대응)
|
||||
useEffect(() => {
|
||||
const store = useTabStore.getState();
|
||||
const currentModeTabs = store[store.mode].tabs;
|
||||
if (currentModeTabs.length > 0) return;
|
||||
|
||||
// /screens/[screenId] 패턴 감지
|
||||
const screenMatch = pathname.match(/^\/screens\/(\d+)/);
|
||||
if (screenMatch) {
|
||||
const screenId = parseInt(screenMatch[1]);
|
||||
const menuObjid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined;
|
||||
store.openTab({ type: "screen", title: `화면 ${screenId}`, screenId, menuObjid });
|
||||
return;
|
||||
}
|
||||
|
||||
// /admin/* 패턴 감지 -> admin 모드로 전환 후 탭 열기
|
||||
if (pathname.startsWith("/admin") && pathname !== "/admin") {
|
||||
store.setMode("admin");
|
||||
store.openTab({ type: "admin", title: pathname.split("/").pop() || "관리자", adminUrl: pathname });
|
||||
}
|
||||
}, []); // 마운트 시 1회만 실행
|
||||
|
||||
// 현재 회사명 조회 (SUPER_ADMIN 전용)
|
||||
useEffect(() => {
|
||||
const fetchCurrentCompanyName = async () => {
|
||||
@@ -306,8 +328,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
handleRegisterVehicle,
|
||||
} = useProfile(user, refreshUserData, refreshMenus);
|
||||
|
||||
// 현재 경로에 따라 어드민 모드인지 판단 (쿼리 파라미터도 고려)
|
||||
const isAdminMode = pathname.startsWith("/admin") || searchParams.get("mode") === "admin";
|
||||
// 탭 스토어에서 현재 모드 가져오기
|
||||
const tabMode = useTabStore((s) => s.mode);
|
||||
const setTabMode = useTabStore((s) => s.setMode);
|
||||
const isAdminMode = tabMode === "admin";
|
||||
|
||||
// 프리뷰 모드: 사이드바/헤더 없이 화면만 표시 (iframe 임베드용)
|
||||
const isPreviewMode = searchParams.get("preview") === "true";
|
||||
@@ -327,67 +351,55 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
setExpandedMenus(newExpanded);
|
||||
};
|
||||
|
||||
// 메뉴 클릭 핸들러
|
||||
const { openTab } = useTabStore();
|
||||
|
||||
// 메뉴 클릭 핸들러 (탭으로 열기)
|
||||
const handleMenuClick = async (menu: any) => {
|
||||
if (menu.hasChildren) {
|
||||
toggleMenu(menu.id);
|
||||
} else {
|
||||
// 메뉴 이름 저장 (엑셀 다운로드 파일명에 사용)
|
||||
const menuName = menu.label || menu.name || "메뉴";
|
||||
// tabTitle: "기준정보 - 회사관리" 형태의 상위 포함 이름
|
||||
const menuName = menu.tabTitle || menu.label || menu.name || "메뉴";
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("currentMenuName", menuName);
|
||||
}
|
||||
|
||||
// 먼저 할당된 화면이 있는지 확인 (URL 유무와 관계없이)
|
||||
try {
|
||||
const menuObjid = menu.objid || menu.id;
|
||||
const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid);
|
||||
|
||||
if (assignedScreens.length > 0) {
|
||||
// 할당된 화면이 있으면 첫 번째 화면으로 이동
|
||||
const firstScreen = assignedScreens[0];
|
||||
|
||||
// 관리자 모드 상태와 menuObjid를 쿼리 파라미터로 전달
|
||||
const params = new URLSearchParams();
|
||||
if (isAdminMode) {
|
||||
params.set("mode", "admin");
|
||||
}
|
||||
params.set("menuObjid", menuObjid.toString());
|
||||
|
||||
const screenPath = `/screens/${firstScreen.screenId}?${params.toString()}`;
|
||||
|
||||
router.push(screenPath);
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
openTab({
|
||||
type: "screen",
|
||||
title: menuName,
|
||||
screenId: firstScreen.screenId,
|
||||
menuObjid: parseInt(menuObjid),
|
||||
});
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("할당된 화면 조회 실패:", error);
|
||||
}
|
||||
|
||||
// 할당된 화면이 없고 URL이 있으면 기존 URL로 이동
|
||||
if (menu.url && menu.url !== "#") {
|
||||
router.push(menu.url);
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
openTab({
|
||||
type: "admin",
|
||||
title: menuName,
|
||||
adminUrl: menu.url,
|
||||
});
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
} else {
|
||||
// URL도 없고 할당된 화면도 없으면 경고 메시지
|
||||
toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 모드 전환 핸들러
|
||||
// 모드 전환: 탭 스토어의 모드만 변경 (각 모드 탭은 독립 보존)
|
||||
const handleModeSwitch = () => {
|
||||
if (isAdminMode) {
|
||||
// 관리자 → 사용자 모드: 선택한 회사 유지
|
||||
router.push("/main");
|
||||
} else {
|
||||
// 사용자 → 관리자 모드: 선택한 회사 유지 (회사 전환 없음)
|
||||
router.push("/admin");
|
||||
}
|
||||
setTabMode(isAdminMode ? "user" : "admin");
|
||||
};
|
||||
|
||||
// 로그아웃 핸들러
|
||||
@@ -400,13 +412,57 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// 메뉴 트리 렌더링 (기존 MainLayout 스타일 적용)
|
||||
// 사이드바 메뉴 -> 탭 바 드래그용 데이터 생성
|
||||
const buildMenuDragData = async (menu: any): Promise<string | null> => {
|
||||
const menuName = menu.label || menu.name || "메뉴";
|
||||
const menuObjid = menu.objid || menu.id;
|
||||
|
||||
try {
|
||||
const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid);
|
||||
if (assignedScreens.length > 0) {
|
||||
return JSON.stringify({
|
||||
type: "screen" as const,
|
||||
title: menuName,
|
||||
screenId: assignedScreens[0].screenId,
|
||||
menuObjid: parseInt(menuObjid),
|
||||
});
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
if (menu.url && menu.url !== "#") {
|
||||
return JSON.stringify({
|
||||
type: "admin" as const,
|
||||
title: menuName,
|
||||
adminUrl: menu.url,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleMenuDragStart = (e: React.DragEvent, menu: any) => {
|
||||
if (menu.hasChildren) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
const menuName = menu.tabTitle || menu.label || menu.name || "메뉴";
|
||||
const menuObjid = menu.objid || menu.id;
|
||||
const dragPayload = JSON.stringify({ menuName, menuObjid, url: menu.url });
|
||||
e.dataTransfer.setData("application/tab-menu-pending", dragPayload);
|
||||
e.dataTransfer.setData("text/plain", menuName);
|
||||
};
|
||||
|
||||
// 메뉴 트리 렌더링 (드래그 가능)
|
||||
const renderMenu = (menu: any, level: number = 0) => {
|
||||
const isExpanded = expandedMenus.has(menu.id);
|
||||
const isLeaf = !menu.hasChildren;
|
||||
|
||||
return (
|
||||
<div key={menu.id}>
|
||||
<div
|
||||
draggable={isLeaf}
|
||||
onDragStart={(e) => handleMenuDragStart(e, menu)}
|
||||
className={`group flex h-10 cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out ${
|
||||
pathname === menu.url
|
||||
? "border-primary border-l-4 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
@@ -435,6 +491,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
{menu.children?.map((child: any) => (
|
||||
<div
|
||||
key={child.id}
|
||||
draggable={!child.hasChildren}
|
||||
onDragStart={(e) => handleMenuDragStart(e, child)}
|
||||
className={`flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm transition-colors hover:cursor-pointer ${
|
||||
pathname === child.url
|
||||
? "border-primary border-l-4 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
@@ -712,9 +770,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
|
||||
<main className={`min-w-0 flex-1 overflow-auto bg-white ${isMobile ? "h-[calc(100vh-56px)]" : "h-screen"}`}>
|
||||
{children}
|
||||
{/* 가운데 컨텐츠 영역 - 탭 시스템 */}
|
||||
<main className={`flex min-w-0 flex-1 flex-col overflow-hidden bg-white ${isMobile ? "h-[calc(100vh-56px)]" : "h-screen"}`}>
|
||||
<TabBar />
|
||||
<TabContent />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user