최초커밋
This commit is contained in:
332
frontend/components/admin/MenuTable.tsx
Normal file
332
frontend/components/admin/MenuTable.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { MenuItem } from "@/lib/api/menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { toast } from "sonner";
|
||||
import { useMultiLang } from "@/hooks/useMultiLang";
|
||||
import { getMenuTextSync, MENU_MANAGEMENT_KEYS, setTranslationCache } from "@/lib/utils/multilang";
|
||||
|
||||
interface MenuTableProps {
|
||||
menus: MenuItem[];
|
||||
title: string;
|
||||
onAddMenu: (parentId: string, menuType: string, level: number) => void;
|
||||
onEditMenu: (menuId: string) => void;
|
||||
onToggleStatus: (menuId: string) => void;
|
||||
selectedMenus: Set<string>;
|
||||
onMenuSelectionChange: (menuId: string, checked: boolean) => void;
|
||||
onSelectAllMenus: (checked: boolean) => void;
|
||||
expandedMenus: Set<string>;
|
||||
onToggleExpand: (menuId: string) => void;
|
||||
}
|
||||
|
||||
export const MenuTable: React.FC<MenuTableProps> = ({
|
||||
menus,
|
||||
title,
|
||||
onAddMenu,
|
||||
onEditMenu,
|
||||
onToggleStatus,
|
||||
selectedMenus,
|
||||
onMenuSelectionChange,
|
||||
onSelectAllMenus,
|
||||
expandedMenus,
|
||||
onToggleExpand,
|
||||
}) => {
|
||||
const { userLang } = useMultiLang();
|
||||
|
||||
// 다국어 텍스트 표시 함수 (기본값 처리)
|
||||
const getDisplayText = (menu: MenuItem) => {
|
||||
// 다국어 텍스트가 있으면 사용, 없으면 기본 텍스트 사용
|
||||
if (menu.translated_name || menu.TRANSLATED_NAME) {
|
||||
return menu.translated_name || menu.TRANSLATED_NAME;
|
||||
}
|
||||
return menu.menu_name_kor || menu.MENU_NAME_KOR || "No menu name";
|
||||
};
|
||||
|
||||
const getDisplayDesc = (menu: MenuItem) => {
|
||||
if (menu.translated_desc || menu.TRANSLATED_DESC) {
|
||||
return menu.translated_desc || menu.TRANSLATED_DESC;
|
||||
}
|
||||
return menu.menu_desc || menu.MENU_DESC || "";
|
||||
};
|
||||
|
||||
// 계층 표시 함수
|
||||
const getTreeIndentation = (level: number) => {
|
||||
return " ".repeat(level);
|
||||
};
|
||||
|
||||
// 레벨 배지 함수
|
||||
const getLevelBadge = (level: number) => {
|
||||
switch (level) {
|
||||
case 0:
|
||||
return "bg-blue-100 text-blue-800";
|
||||
case 1:
|
||||
return "bg-green-100 text-green-800";
|
||||
case 2:
|
||||
return "bg-yellow-100 text-yellow-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
// 토글 상태에 따라 메뉴를 필터링하는 함수
|
||||
const getFilteredMenus = () => {
|
||||
const filtered: MenuItem[] = [];
|
||||
|
||||
for (let i = 0; i < menus.length; i++) {
|
||||
const menu = menus[i];
|
||||
const menuId = menu.objid || menu.OBJID;
|
||||
const level = menu.lev || menu.LEV || 1;
|
||||
|
||||
// 최상위 메뉴는 항상 표시
|
||||
if (level === 1) {
|
||||
filtered.push(menu);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 하위 메뉴는 상위 메뉴가 확장되어 있을 때만 표시
|
||||
let shouldShow = true;
|
||||
let currentLevel: number = level;
|
||||
let currentMenu = menu;
|
||||
|
||||
// 상위 메뉴들을 확인하여 모두 확장되어 있는지 체크
|
||||
while (currentLevel > 1) {
|
||||
// 현재 메뉴의 상위 메뉴 찾기
|
||||
const parentMenu = menus.find(
|
||||
(m) => (m.objid || m.OBJID) === (currentMenu.parent_obj_id || currentMenu.PARENT_OBJ_ID),
|
||||
);
|
||||
if (!parentMenu) break;
|
||||
|
||||
const parentId = parentMenu.objid || parentMenu.OBJID;
|
||||
if (!parentId || !expandedMenus.has(parentId)) {
|
||||
shouldShow = false;
|
||||
break;
|
||||
}
|
||||
|
||||
currentMenu = parentMenu;
|
||||
const nextLevel = currentMenu.lev || currentMenu.LEV;
|
||||
if (nextLevel === undefined) break;
|
||||
currentLevel = nextLevel;
|
||||
}
|
||||
|
||||
if (shouldShow) {
|
||||
filtered.push(menu);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string, menuId: string) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onToggleStatus(menuId)}
|
||||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
status === "active"
|
||||
? "bg-green-100 text-green-800 hover:bg-green-200"
|
||||
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
{status === "active"
|
||||
? getMenuTextSync(MENU_MANAGEMENT_KEYS.STATUS_ACTIVE)
|
||||
: getMenuTextSync(MENU_MANAGEMENT_KEYS.STATUS_INACTIVE)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{title && <h3 className="text-lg font-semibold">{title}</h3>}
|
||||
<div className="rounded-lg border">
|
||||
<div className="max-h-[calc(100vh-350px)] overflow-auto">
|
||||
<Table noWrapper>
|
||||
<TableHeader className="sticky top-0 z-20 bg-gray-50 shadow-sm">
|
||||
<TableRow>
|
||||
<TableHead className="w-12 bg-gray-50 font-semibold text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
selectedMenus.size === menus.filter((menu) => (menu.lev || menu.LEV || 0) > 1).length &&
|
||||
menus.filter((menu) => (menu.lev || menu.LEV || 0) > 1).length > 0
|
||||
}
|
||||
onChange={(e) => onSelectAllMenus(e.target.checked)}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="w-1/3 bg-gray-50 font-semibold text-gray-700">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME)}
|
||||
</TableHead>
|
||||
<TableHead className="w-16 bg-gray-50 font-semibold text-gray-700">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE)}
|
||||
</TableHead>
|
||||
<TableHead className="w-24 bg-gray-50 font-semibold text-gray-700">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY)}
|
||||
</TableHead>
|
||||
<TableHead className="w-48 bg-gray-50 font-semibold text-gray-700">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL)}
|
||||
</TableHead>
|
||||
<TableHead className="w-20 bg-gray-50 font-semibold text-gray-700">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS)}
|
||||
</TableHead>
|
||||
<TableHead className="w-32 bg-gray-50 font-semibold text-gray-700">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_ACTIONS)}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{getFilteredMenus().map((menu) => {
|
||||
const objid = menu.objid || menu.OBJID || "";
|
||||
const lev = menu.lev || menu.LEV || 0;
|
||||
const menuNameKor = menu.menu_name_kor || menu.MENU_NAME_KOR || "No menu name";
|
||||
const seq = menu.seq || menu.SEQ || 0;
|
||||
const companyCode = menu.company_code || menu.COMPANY_CODE || "";
|
||||
const companyName =
|
||||
menu.company_name ||
|
||||
menu.COMPANY_NAME ||
|
||||
companyCode ||
|
||||
getMenuTextSync(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED);
|
||||
const menuUrl = menu.menu_url || menu.MENU_URL || "";
|
||||
const status = menu.status || menu.STATUS || "";
|
||||
const menuType = menu.menu_type || menu.MENU_TYPE || "";
|
||||
const parentObjId = menu.parent_obj_id || menu.PARENT_OBJ_ID || "";
|
||||
|
||||
return (
|
||||
<TableRow key={objid} className="hover:bg-gray-50">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMenus.has(objid)}
|
||||
onChange={(e) => onMenuSelectionChange(objid, e.target.checked)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-left">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-mono text-sm text-gray-400">{getTreeIndentation(lev)}</span>
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${getLevelBadge(lev)}`}
|
||||
>
|
||||
L{lev}
|
||||
</span>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="font-medium text-gray-900">{getDisplayText(menu)}</span>
|
||||
{/* 하위 메뉴가 있는 경우에만 토글 버튼 표시 */}
|
||||
{menus.some((m) => (m.parent_obj_id || m.PARENT_OBJ_ID) === objid) && (
|
||||
<button
|
||||
onClick={() => onToggleExpand(objid)}
|
||||
className="ml-2 rounded p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-700"
|
||||
>
|
||||
<svg
|
||||
className={`h-4 w-4 transition-transform ${expandedMenus.has(objid) ? "rotate-90" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{seq}</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={`font-medium ${companyName && companyName !== getMenuTextSync(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED) ? "text-green-600" : "text-gray-500"}`}
|
||||
>
|
||||
{companyCode === "*"
|
||||
? getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON)
|
||||
: companyName || getMenuTextSync(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED)}
|
||||
</span>
|
||||
{companyCode && companyCode !== "" && (
|
||||
<span className="font-mono text-xs text-gray-400">{companyCode}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-left text-sm text-gray-600">
|
||||
<div className="max-w-[200px]">
|
||||
{menuUrl ? (
|
||||
<div className="group relative">
|
||||
<div
|
||||
className={`cursor-pointer transition-colors hover:text-blue-600 ${
|
||||
menuUrl.length > 30 ? "truncate" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(menuUrl);
|
||||
toast.success("URL copied to clipboard!");
|
||||
}}
|
||||
>
|
||||
{menuUrl}
|
||||
</div>
|
||||
{menuUrl.length > 30 && (
|
||||
<div className="absolute top-full left-0 z-20 mt-1 hidden max-w-xs rounded-lg bg-gray-900 p-3 text-sm text-white shadow-lg group-hover:block">
|
||||
<div className="mb-2 text-xs text-gray-300">Full URL</div>
|
||||
<div className="break-all text-white">{menuUrl}</div>
|
||||
<div className="mt-2 text-xs text-gray-400">Click to copy</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400">-</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(status, objid)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-nowrap gap-1">
|
||||
{lev === 1 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-w-[40px] px-1 py-1 text-xs"
|
||||
onClick={() => onAddMenu(objid, menuType, lev)}
|
||||
>
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_ADD)}
|
||||
</Button>
|
||||
)}
|
||||
{lev === 2 && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-w-[40px] px-1 py-1 text-xs"
|
||||
onClick={() => onAddMenu(objid, menuType, lev)}
|
||||
>
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-w-[40px] px-1 py-1 text-xs"
|
||||
onClick={() => onEditMenu(objid)}
|
||||
>
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_EDIT)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{lev > 2 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-w-[40px] px-1 py-1 text-xs"
|
||||
onClick={() => onEditMenu(objid)}
|
||||
>
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_EDIT)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user