ERP-node/frontend/components/admin/MenuTable.tsx

339 lines
14 KiB
TypeScript

"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 { MENU_MANAGEMENT_KEYS } 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;
// 다국어 텍스트 props 추가
uiTexts: Record<string, string>;
}
export const MenuTable: React.FC<MenuTableProps> = ({
menus,
title,
onAddMenu,
onEditMenu,
onToggleStatus,
selectedMenus,
onMenuSelectionChange,
onSelectAllMenus,
expandedMenus,
onToggleExpand,
uiTexts,
}) => {
// 다국어 텍스트 가져오기 함수
const getText = (key: string, fallback?: string): string => {
return uiTexts[key] || fallback || key;
};
// 다국어 텍스트 표시 함수 (기본값 처리)
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-primary/20 text-primary";
case 1:
return "bg-success/20 text-success";
case 2:
return "bg-warning/20 text-warning";
default:
return "bg-muted/50 text-muted-foreground";
}
};
// 토글 상태에 따라 메뉴를 필터링하는 함수
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-success/20 text-success hover:bg-success/30"
: "bg-muted/50 text-muted-foreground hover:bg-muted"
}`}
>
{status === "active"
? getText(MENU_MANAGEMENT_KEYS.STATUS_ACTIVE)
: getText(MENU_MANAGEMENT_KEYS.STATUS_INACTIVE)}
</button>
);
};
return (
<div className="space-y-4">
{title && <h3 className="text-lg font-semibold">{title}</h3>}
<div className="bg-card shadow-sm">
<div className="max-h-[calc(100vh-350px)] overflow-auto">
<Table noWrapper>
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow>
<TableHead className="h-12 w-12 text-sm font-semibold">
<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="h-12 w-1/3 text-sm font-semibold">
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME)}
</TableHead>
<TableHead className="h-12 w-16 text-sm font-semibold">
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE)}
</TableHead>
<TableHead className="h-12 w-24 text-sm font-semibold">
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY)}
</TableHead>
<TableHead className="h-12 w-48 text-sm font-semibold">
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL)}
</TableHead>
<TableHead className="h-12 w-20 text-sm font-semibold">
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS)}
</TableHead>
<TableHead className="h-12 w-32 text-sm font-semibold">
{getText(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}-${lev}-${parentObjId}`} className="transition-colors hover:bg-muted/50">
<TableCell className="h-16">
<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="h-16 text-left text-sm">
<div className="flex items-center space-x-2">
<span className="font-mono text-sm text-muted-foreground">{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="text-sm font-medium text-foreground">{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-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<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 className="h-16 text-sm">{seq}</TableCell>
<TableCell className="h-16 text-sm text-muted-foreground">
<div className="flex flex-col">
<span
className={`font-medium ${companyName && companyName !== getText(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED) ? "text-success" : "text-muted-foreground"}`}
>
{companyCode === "*"
? getText(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON)
: companyName || getText(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED)}
</span>
{companyCode && companyCode !== "" && (
<span className="font-mono text-xs text-muted-foreground/70">{companyCode}</span>
)}
</div>
</TableCell>
<TableCell className="h-16 text-left text-sm text-muted-foreground">
<div className="max-w-[200px]">
{menuUrl ? (
<div className="group relative">
<div
className={`cursor-pointer transition-colors hover:text-primary ${
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-popover p-3 text-sm text-popover-foreground shadow-lg group-hover:block">
<div className="mb-2 text-xs text-muted-foreground">Full URL</div>
<div className="break-all">{menuUrl}</div>
<div className="mt-2 text-xs text-muted-foreground">Click to copy</div>
</div>
)}
</div>
) : (
<span className="text-muted-foreground/70">-</span>
)}
</div>
</TableCell>
<TableCell className="h-16 text-sm">{getStatusBadge(status, objid)}</TableCell>
<TableCell className="h-16">
<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)}
>
{getText(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)}
>
{getText(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)}
>
{getText(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)}
>
{getText(MENU_MANAGEMENT_KEYS.BUTTON_EDIT)}
</Button>
)}
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</div>
</div>
);
};