339 lines
14 KiB
TypeScript
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">
|
|
<div className="max-h-[calc(100vh-350px)] overflow-auto">
|
|
<Table noWrapper>
|
|
<TableHeader className="sticky top-0 z-20 bg-background">
|
|
<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>
|
|
);
|
|
};
|