2025-08-21 09:41:46 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
import { menuApi } from "@/lib/api/menu";
|
|
|
|
|
import type { MenuItem } from "@/lib/api/menu";
|
|
|
|
|
import { MenuTable } from "./MenuTable";
|
|
|
|
|
import { MenuFormModal } from "./MenuFormModal";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { LoadingSpinner, LoadingOverlay } from "@/components/common/LoadingSpinner";
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
|
import { useMenu } from "@/contexts/MenuContext";
|
|
|
|
|
import {
|
|
|
|
|
getMenuTextSync,
|
|
|
|
|
MENU_MANAGEMENT_KEYS,
|
|
|
|
|
useMenuManagementText,
|
|
|
|
|
setTranslationCache,
|
|
|
|
|
} from "@/lib/utils/multilang";
|
|
|
|
|
import { useMultiLang } from "@/hooks/useMultiLang";
|
2025-08-21 14:47:07 +09:00
|
|
|
import { apiClient } from "@/lib/api/client";
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
type MenuType = "admin" | "user";
|
|
|
|
|
|
|
|
|
|
export const MenuManagement: React.FC = () => {
|
|
|
|
|
const { adminMenus, userMenus, refreshMenus } = useMenu();
|
|
|
|
|
const [selectedMenuType, setSelectedMenuType] = useState<MenuType>("admin");
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [deleting, setDeleting] = useState(false);
|
|
|
|
|
const [formModalOpen, setFormModalOpen] = useState(false);
|
|
|
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
|
|
|
const [selectedMenuId, setSelectedMenuId] = useState<string>("");
|
|
|
|
|
const [selectedMenus, setSelectedMenus] = useState<Set<string>>(new Set());
|
|
|
|
|
|
|
|
|
|
// 다국어 텍스트 훅 사용
|
|
|
|
|
const { getMenuText } = useMenuManagementText();
|
|
|
|
|
const { userLang } = useMultiLang({ companyCode: "*" });
|
|
|
|
|
|
|
|
|
|
// 다국어 텍스트 상태
|
|
|
|
|
const [uiTexts, setUiTexts] = useState<Record<string, string>>({});
|
|
|
|
|
const [uiTextsLoading, setUiTextsLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
// 회사 목록 상태
|
|
|
|
|
const [companies, setCompanies] = useState<Array<{ code: string; name: string }>>([]);
|
|
|
|
|
const [selectedCompany, setSelectedCompany] = useState("all");
|
|
|
|
|
const [searchText, setSearchText] = useState("");
|
|
|
|
|
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set());
|
|
|
|
|
const [companySearchText, setCompanySearchText] = useState("");
|
|
|
|
|
const [isCompanyDropdownOpen, setIsCompanyDropdownOpen] = useState(false);
|
|
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
|
menuId: "",
|
|
|
|
|
parentId: "",
|
|
|
|
|
menuType: "",
|
|
|
|
|
level: 0,
|
|
|
|
|
parentCompanyCode: "",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 초기 로딩
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadCompanies();
|
|
|
|
|
}, []); // 빈 의존성 배열로 한 번만 실행
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 마운트 시 및 userLang 변경 시 다국어 텍스트 로드
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!uiTextsLoading) {
|
|
|
|
|
loadUITexts();
|
|
|
|
|
}
|
|
|
|
|
}, [userLang]); // userLang 변경 시마다 실행
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 마운트 시 강제로 번역 로드 (userLang이 아직 설정되지 않았을 수 있음)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
if (!uiTextsLoading && Object.keys(uiTexts).length === 0) {
|
|
|
|
|
console.log("🔄 컴포넌트 마운트 후 강제 번역 로드");
|
|
|
|
|
loadUITexts();
|
|
|
|
|
}
|
|
|
|
|
}, 100); // 100ms 후 실행
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
}, []); // 컴포넌트 마운트 시 한 번만 실행
|
|
|
|
|
|
|
|
|
|
// 번역 로드 이벤트 감지
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleTranslationLoaded = (event: CustomEvent) => {
|
|
|
|
|
const { key, text, userLang: loadedLang } = event.detail;
|
|
|
|
|
if (loadedLang === userLang) {
|
|
|
|
|
setUiTexts((prev) => ({ ...prev, [key]: text }));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener("translation-loaded", handleTranslationLoaded as EventListener);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener("translation-loaded", handleTranslationLoaded as EventListener);
|
|
|
|
|
};
|
|
|
|
|
}, [userLang]);
|
|
|
|
|
|
|
|
|
|
// 드롭다운 외부 클릭 시 닫기
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
|
|
|
const target = event.target as Element;
|
|
|
|
|
if (!target.closest(".company-dropdown")) {
|
|
|
|
|
setIsCompanyDropdownOpen(false);
|
|
|
|
|
setCompanySearchText("");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isCompanyDropdownOpen) {
|
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
|
|
|
};
|
|
|
|
|
}, [isCompanyDropdownOpen]);
|
|
|
|
|
|
|
|
|
|
const loadMenus = async (showLoading = true) => {
|
|
|
|
|
console.log(`📋 메뉴 목록 조회 시작 (showLoading: ${showLoading})`);
|
|
|
|
|
try {
|
|
|
|
|
if (showLoading) {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
}
|
|
|
|
|
await refreshMenus();
|
|
|
|
|
console.log(`📋 메뉴 목록 조회 성공`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ 메뉴 목록 조회 실패:", error);
|
|
|
|
|
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_LIST));
|
|
|
|
|
} finally {
|
|
|
|
|
if (showLoading) {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 회사 목록 조회
|
|
|
|
|
const loadCompanies = async () => {
|
|
|
|
|
console.log(`🏢 회사 목록 조회 시작`);
|
|
|
|
|
try {
|
2025-08-21 14:47:07 +09:00
|
|
|
const response = await apiClient.get("/admin/companies");
|
|
|
|
|
|
|
|
|
|
if (response.data.success) {
|
|
|
|
|
console.log("🏢 회사 목록 응답:", response.data);
|
|
|
|
|
const companyList = response.data.data.map((company: any) => ({
|
|
|
|
|
code: company.company_code || company.companyCode,
|
|
|
|
|
name: company.company_name || company.companyName,
|
|
|
|
|
}));
|
|
|
|
|
console.log("🏢 변환된 회사 목록:", companyList);
|
|
|
|
|
setCompanies(companyList);
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ 회사 목록 조회 실패:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 다국어 텍스트 로드 함수
|
|
|
|
|
const loadUITexts = async () => {
|
|
|
|
|
if (uiTextsLoading) return; // 이미 로딩 중이면 중단
|
|
|
|
|
|
|
|
|
|
// userLang이 없으면 기본값 사용
|
|
|
|
|
const currentUserLang = userLang || "KR";
|
|
|
|
|
console.log("🌐 UI 다국어 텍스트 로드 시작", { currentUserLang });
|
|
|
|
|
setUiTextsLoading(true);
|
|
|
|
|
|
|
|
|
|
const texts: Record<string, string> = {};
|
|
|
|
|
try {
|
|
|
|
|
const textPromises = [
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.TITLE),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.DESCRIPTION),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.MENU_TYPE_TITLE),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.MENU_TYPE_ADMIN),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.MENU_TYPE_USER),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.ADMIN_MENU),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.USER_MENU),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.USER_DESCRIPTION),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_ADD),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_EDIT),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_DELETE),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED_COUNT),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_PROCESSING),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.FILTER_RESET),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.LIST_TOTAL),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_TYPE),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE),
|
|
|
|
|
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_ACTIONS),
|
|
|
|
|
// 추가 키들 - 실제 UI에서 사용되는 모든 키들
|
|
|
|
|
getMenuText("menu.list.title"),
|
|
|
|
|
getMenuText("filter.company"),
|
|
|
|
|
getMenuText("filter.company.all"),
|
|
|
|
|
getMenuText("filter.search"),
|
|
|
|
|
getMenuText("filter.search.placeholder"),
|
|
|
|
|
getMenuText("status.unspecified"),
|
|
|
|
|
getMenuText("status.active"),
|
|
|
|
|
getMenuText("filter.company.common"),
|
|
|
|
|
getMenuText("modal.menu.register.title"),
|
|
|
|
|
getMenuText("form.menu.type"),
|
|
|
|
|
getMenuText("form.menu.type.admin"),
|
|
|
|
|
getMenuText("form.menu.type.user"),
|
|
|
|
|
getMenuText("form.status"),
|
|
|
|
|
getMenuText("form.status.active"),
|
|
|
|
|
getMenuText("form.status.inactive"),
|
|
|
|
|
getMenuText("form.company"),
|
|
|
|
|
getMenuText("form.company.select"),
|
|
|
|
|
getMenuText("form.company.common"),
|
|
|
|
|
getMenuText("form.company.submenu.note"),
|
|
|
|
|
getMenuText("form.lang.key"),
|
|
|
|
|
getMenuText("form.lang.key.select"),
|
|
|
|
|
getMenuText("form.menu.name"),
|
|
|
|
|
getMenuText("form.menu.name.placeholder"),
|
|
|
|
|
getMenuText("form.menu.url"),
|
|
|
|
|
getMenuText("form.menu.url.placeholder"),
|
|
|
|
|
getMenuText("form.menu.description"),
|
|
|
|
|
getMenuText("form.menu.description.placeholder"),
|
|
|
|
|
getMenuText("form.menu.sequence"),
|
|
|
|
|
getMenuText("button.cancel"),
|
|
|
|
|
getMenuText("button.register"),
|
|
|
|
|
// 테이블 헤더 관련 추가 키들
|
|
|
|
|
getMenuText("table.header.menu.name"),
|
|
|
|
|
getMenuText("table.header.sequence"),
|
|
|
|
|
getMenuText("table.header.company"),
|
|
|
|
|
getMenuText("table.header.menu.url"),
|
|
|
|
|
getMenuText("table.header.status"),
|
|
|
|
|
getMenuText("table.header.actions"),
|
|
|
|
|
// 액션 버튼 관련 추가 키들
|
|
|
|
|
getMenuText("button.add"),
|
|
|
|
|
getMenuText("button.add.sub"),
|
|
|
|
|
getMenuText("button.edit"),
|
|
|
|
|
getMenuText("button.delete"),
|
|
|
|
|
// 페이지 제목 관련
|
|
|
|
|
getMenuText("page.title.menu.management"),
|
|
|
|
|
getMenuText("page.description.menu.management"),
|
|
|
|
|
getMenuText("section.title.menu.type"),
|
|
|
|
|
getMenuText("section.title.admin.menu.list"),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const results = await Promise.all(textPromises);
|
|
|
|
|
|
|
|
|
|
// 결과를 키와 매핑
|
|
|
|
|
const keys = [
|
|
|
|
|
MENU_MANAGEMENT_KEYS.TITLE,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.DESCRIPTION,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.MENU_TYPE_TITLE,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.MENU_TYPE_ADMIN,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.MENU_TYPE_USER,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.ADMIN_MENU,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.USER_MENU,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.USER_DESCRIPTION,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.BUTTON_ADD,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.BUTTON_EDIT,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.BUTTON_DELETE,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED_COUNT,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.BUTTON_DELETE_PROCESSING,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.FILTER_RESET,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.LIST_TOTAL,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_TYPE,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE,
|
|
|
|
|
MENU_MANAGEMENT_KEYS.TABLE_HEADER_ACTIONS,
|
|
|
|
|
// 추가 키들 - 실제 UI에서 사용되는 모든 키들
|
|
|
|
|
"menu.list.title",
|
|
|
|
|
"filter.company",
|
|
|
|
|
"filter.company.all",
|
|
|
|
|
"filter.search",
|
|
|
|
|
"filter.search.placeholder",
|
|
|
|
|
"status.unspecified",
|
|
|
|
|
"status.active",
|
|
|
|
|
"filter.company.common",
|
|
|
|
|
"modal.menu.register.title",
|
|
|
|
|
"form.menu.type",
|
|
|
|
|
"form.menu.type.admin",
|
|
|
|
|
"form.menu.type.user",
|
|
|
|
|
"form.status",
|
|
|
|
|
"form.status.active",
|
|
|
|
|
"form.status.inactive",
|
|
|
|
|
"form.company",
|
|
|
|
|
"form.company.select",
|
|
|
|
|
"form.company.common",
|
|
|
|
|
"form.company.submenu.note",
|
|
|
|
|
"form.lang.key",
|
|
|
|
|
"form.lang.key.select",
|
|
|
|
|
"form.menu.name",
|
|
|
|
|
"form.menu.name.placeholder",
|
|
|
|
|
"form.menu.url",
|
|
|
|
|
"form.menu.url.placeholder",
|
|
|
|
|
"form.menu.description",
|
|
|
|
|
"form.menu.description.placeholder",
|
|
|
|
|
"form.menu.sequence",
|
|
|
|
|
"button.cancel",
|
|
|
|
|
"button.register",
|
|
|
|
|
// 테이블 헤더 관련 추가 키들
|
|
|
|
|
"table.header.menu.name",
|
|
|
|
|
"table.header.sequence",
|
|
|
|
|
"table.header.company",
|
|
|
|
|
"table.header.menu.url",
|
|
|
|
|
"table.header.status",
|
|
|
|
|
"table.header.actions",
|
|
|
|
|
// 액션 버튼 관련 추가 키들
|
|
|
|
|
"button.add",
|
|
|
|
|
"button.add.sub",
|
|
|
|
|
"button.edit",
|
|
|
|
|
"button.delete",
|
|
|
|
|
// 페이지 제목 관련
|
|
|
|
|
"page.title.menu.management",
|
|
|
|
|
"page.description.menu.management",
|
|
|
|
|
"section.title.menu.type",
|
|
|
|
|
"section.title.admin.menu.list",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
keys.forEach((key, index) => {
|
|
|
|
|
texts[key] = results[index];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setUiTexts(texts);
|
|
|
|
|
|
|
|
|
|
// 번역 텍스트를 캐시에 저장
|
|
|
|
|
setTranslationCache(currentUserLang, texts);
|
|
|
|
|
|
|
|
|
|
console.log("🌐 UI 다국어 텍스트 로드 완료:", texts);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ UI 다국어 텍스트 로드 실패:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
setUiTextsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// UI 텍스트 가져오기 함수
|
|
|
|
|
const getUIText = async (
|
|
|
|
|
key: string,
|
|
|
|
|
params?: Record<string, string | number>,
|
|
|
|
|
fallback?: string,
|
|
|
|
|
): Promise<string> => {
|
|
|
|
|
// uiTexts에서 먼저 찾기
|
|
|
|
|
let text = uiTexts[key];
|
|
|
|
|
|
|
|
|
|
// uiTexts에 없으면 비동기적으로 API 호출
|
|
|
|
|
if (!text) {
|
|
|
|
|
try {
|
|
|
|
|
text = await getMenuText(key);
|
|
|
|
|
// 새로운 텍스트를 uiTexts에 추가
|
|
|
|
|
setUiTexts((prev) => ({ ...prev, [key]: text }));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`❌ 키 "${key}" 번역 실패:`, error);
|
|
|
|
|
text = fallback || key;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 파라미터 치환
|
|
|
|
|
if (params && text) {
|
|
|
|
|
Object.entries(params).forEach(([paramKey, paramValue]) => {
|
|
|
|
|
text = text!.replace(`{${paramKey}}`, String(paramValue));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return text || key;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 동기 버전 (기존 호환성을 위해)
|
|
|
|
|
const getUITextSync = (key: string, params?: Record<string, string | number>, fallback?: string): string => {
|
|
|
|
|
let text = uiTexts[key];
|
|
|
|
|
|
|
|
|
|
if (!text) {
|
|
|
|
|
text = fallback || key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 파라미터 치환
|
|
|
|
|
if (params && text) {
|
|
|
|
|
Object.entries(params).forEach(([paramKey, paramValue]) => {
|
|
|
|
|
text = text!.replace(`{${paramKey}}`, String(paramValue));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return text || key;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 다국어 API 테스트 함수
|
|
|
|
|
const testMultiLangAPI = async () => {
|
|
|
|
|
console.log("🧪 다국어 API 테스트 시작");
|
|
|
|
|
try {
|
|
|
|
|
const text = await getMenuText(MENU_MANAGEMENT_KEYS.ADMIN_MENU);
|
|
|
|
|
console.log("🧪 다국어 API 테스트 결과:", text);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ 다국어 API 테스트 실패:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 대문자 키를 소문자 키로 변환하는 함수
|
|
|
|
|
const convertMenuData = (data: any[]): MenuItem[] => {
|
|
|
|
|
return data.map((item) => ({
|
|
|
|
|
objid: item.OBJID || item.objid,
|
|
|
|
|
parent_obj_id: item.PARENT_OBJ_ID || item.parent_obj_id,
|
|
|
|
|
menu_name_kor: item.MENU_NAME_KOR || item.menu_name_kor,
|
|
|
|
|
menu_url: item.MENU_URL || item.menu_url,
|
|
|
|
|
menu_desc: item.MENU_DESC || item.menu_desc,
|
|
|
|
|
seq: item.SEQ || item.seq,
|
|
|
|
|
menu_type: item.MENU_TYPE || item.menu_type,
|
|
|
|
|
status: item.STATUS || item.status,
|
|
|
|
|
lev: item.LEV || item.lev,
|
|
|
|
|
lpad_menu_name_kor: item.LPAD_MENU_NAME_KOR || item.lpad_menu_name_kor,
|
|
|
|
|
status_title: item.STATUS_TITLE || item.status_title,
|
|
|
|
|
writer: item.WRITER || item.writer,
|
|
|
|
|
regdate: item.REGDATE || item.regdate,
|
|
|
|
|
company_code: item.COMPANY_CODE || item.company_code,
|
|
|
|
|
company_name: item.COMPANY_NAME || item.company_name,
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAddTopLevelMenu = () => {
|
|
|
|
|
setFormData({
|
|
|
|
|
menuId: "",
|
|
|
|
|
parentId: "0", // 최상위 메뉴는 parentId가 0
|
|
|
|
|
menuType: getMenuTypeValue(),
|
|
|
|
|
level: 1, // 최상위 메뉴는 level 1
|
|
|
|
|
parentCompanyCode: "", // 최상위 메뉴는 상위 회사 정보 없음
|
|
|
|
|
});
|
|
|
|
|
setFormModalOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAddMenu = (parentId: string, menuType: string, level: number) => {
|
|
|
|
|
// 상위 메뉴의 회사 정보 찾기
|
|
|
|
|
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
|
|
|
|
const parentMenu = currentMenus.find((menu) => menu.objid === parentId);
|
|
|
|
|
|
|
|
|
|
setFormData({
|
|
|
|
|
menuId: "",
|
|
|
|
|
parentId,
|
|
|
|
|
menuType,
|
|
|
|
|
level: level + 1,
|
|
|
|
|
parentCompanyCode: parentMenu?.company_code || "",
|
|
|
|
|
});
|
|
|
|
|
setFormModalOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEditMenu = (menuId: string) => {
|
|
|
|
|
setFormData({
|
|
|
|
|
menuId,
|
|
|
|
|
parentId: "",
|
|
|
|
|
menuType: "",
|
|
|
|
|
level: 0,
|
|
|
|
|
parentCompanyCode: "",
|
|
|
|
|
});
|
|
|
|
|
setFormModalOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMenuSelectionChange = (menuId: string, checked: boolean) => {
|
|
|
|
|
const newSelected = new Set(selectedMenus);
|
|
|
|
|
if (checked) {
|
|
|
|
|
newSelected.add(menuId);
|
|
|
|
|
} else {
|
|
|
|
|
newSelected.delete(menuId);
|
|
|
|
|
}
|
|
|
|
|
setSelectedMenus(newSelected);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSelectAllMenus = (checked: boolean) => {
|
|
|
|
|
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
|
|
|
|
if (checked) {
|
|
|
|
|
// 모든 메뉴 선택 (최상위 메뉴 포함)
|
|
|
|
|
setSelectedMenus(new Set(currentMenus.map((menu) => menu.objid || menu.OBJID || "")));
|
|
|
|
|
} else {
|
|
|
|
|
setSelectedMenus(new Set());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDeleteSelectedMenus = async () => {
|
|
|
|
|
if (selectedMenus.size === 0) {
|
|
|
|
|
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_SELECT_MENU_DELETE));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!confirm(getMenuTextSync(MENU_MANAGEMENT_KEYS.MODAL_DELETE_BATCH_DESCRIPTION, { count: selectedMenus.size }))) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setDeleting(true);
|
|
|
|
|
try {
|
|
|
|
|
const menuIds = Array.from(selectedMenus);
|
|
|
|
|
console.log("삭제할 메뉴 IDs:", menuIds);
|
|
|
|
|
|
|
|
|
|
toast.info(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_PROCESSING));
|
|
|
|
|
|
|
|
|
|
const response = await menuApi.deleteMenusBatch(menuIds);
|
|
|
|
|
console.log("삭제 API 응답:", response);
|
|
|
|
|
console.log("응답 구조:", {
|
|
|
|
|
success: response.success,
|
|
|
|
|
data: response.data,
|
|
|
|
|
message: response.message,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.success && response.data) {
|
|
|
|
|
const { deletedCount, failedCount } = response.data;
|
|
|
|
|
console.log("삭제 결과:", { deletedCount, failedCount });
|
|
|
|
|
|
|
|
|
|
// 선택된 메뉴 초기화
|
|
|
|
|
setSelectedMenus(new Set());
|
|
|
|
|
|
|
|
|
|
// 메뉴 목록 즉시 새로고침 (로딩 상태 없이)
|
|
|
|
|
console.log("메뉴 목록 새로고침 시작");
|
|
|
|
|
await loadMenus(false);
|
|
|
|
|
// 전역 메뉴 상태도 업데이트
|
|
|
|
|
await refreshMenus();
|
|
|
|
|
console.log("메뉴 목록 새로고침 완료");
|
|
|
|
|
|
|
|
|
|
// 삭제 결과 메시지
|
|
|
|
|
if (failedCount === 0) {
|
|
|
|
|
toast.success(
|
|
|
|
|
getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_SUCCESS, { count: deletedCount }),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
toast.success(
|
|
|
|
|
getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_PARTIAL, {
|
|
|
|
|
success: deletedCount,
|
|
|
|
|
failed: failedCount,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.error("삭제 실패:", response);
|
|
|
|
|
toast.error(response.message || "메뉴 삭제에 실패했습니다.");
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("메뉴 삭제 중 오류:", error);
|
|
|
|
|
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_FAILED));
|
|
|
|
|
} finally {
|
|
|
|
|
setDeleting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const confirmDelete = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await menuApi.deleteMenu(selectedMenuId);
|
|
|
|
|
if (response.success) {
|
|
|
|
|
toast.success(response.message);
|
|
|
|
|
await loadMenus(false);
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(response.message);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error("메뉴 삭제에 실패했습니다.");
|
|
|
|
|
} finally {
|
|
|
|
|
setDeleteDialogOpen(false);
|
|
|
|
|
setSelectedMenuId("");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleToggleStatus = async (menuId: string) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await menuApi.toggleMenuStatus(menuId);
|
|
|
|
|
if (response.success) {
|
|
|
|
|
toast.success(response.message);
|
|
|
|
|
await loadMenus(false); // 메뉴 목록 새로고침
|
|
|
|
|
// 전역 메뉴 상태도 업데이트
|
|
|
|
|
await refreshMenus();
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(response.message);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("메뉴 상태 토글 오류:", error);
|
|
|
|
|
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_FAILED));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFormSuccess = () => {
|
|
|
|
|
loadMenus(false);
|
|
|
|
|
// 전역 메뉴 상태도 업데이트
|
|
|
|
|
refreshMenus();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getCurrentMenus = () => {
|
|
|
|
|
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
|
|
|
|
|
|
|
|
|
// 검색어 필터링
|
|
|
|
|
let filteredMenus = currentMenus;
|
|
|
|
|
if (searchText.trim()) {
|
|
|
|
|
const searchLower = searchText.toLowerCase();
|
|
|
|
|
filteredMenus = currentMenus.filter((menu) => {
|
|
|
|
|
const menuName = (menu.menu_name_kor || menu.MENU_NAME_KOR || "").toLowerCase();
|
|
|
|
|
const menuUrl = (menu.menu_url || menu.MENU_URL || "").toLowerCase();
|
|
|
|
|
return menuName.includes(searchLower) || menuUrl.includes(searchLower);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 회사 필터링
|
|
|
|
|
if (selectedCompany !== "all") {
|
|
|
|
|
filteredMenus = filteredMenus.filter((menu) => {
|
|
|
|
|
const menuCompanyCode = menu.company_code || menu.COMPANY_CODE || "";
|
|
|
|
|
return menuCompanyCode === selectedCompany;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filteredMenus;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 메뉴 타입 변경 시 선택된 메뉴 초기화
|
|
|
|
|
const handleMenuTypeChange = (type: MenuType) => {
|
|
|
|
|
setSelectedMenuType(type);
|
|
|
|
|
setSelectedMenus(new Set()); // 선택된 메뉴 초기화
|
|
|
|
|
setExpandedMenus(new Set()); // 메뉴 타입 변경 시 확장 상태 초기화
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleToggleExpand = (menuId: string) => {
|
|
|
|
|
const newExpandedMenus = new Set(expandedMenus);
|
|
|
|
|
if (newExpandedMenus.has(menuId)) {
|
|
|
|
|
newExpandedMenus.delete(menuId);
|
|
|
|
|
} else {
|
|
|
|
|
newExpandedMenus.add(menuId);
|
|
|
|
|
}
|
|
|
|
|
setExpandedMenus(newExpandedMenus);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getMenuTypeString = () => {
|
|
|
|
|
return selectedMenuType === "admin"
|
|
|
|
|
? getMenuTextSync(MENU_MANAGEMENT_KEYS.MENU_TYPE_ADMIN)
|
|
|
|
|
: getMenuTextSync(MENU_MANAGEMENT_KEYS.MENU_TYPE_USER);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getMenuTypeValue = () => {
|
|
|
|
|
return selectedMenuType === "admin" ? "0" : "1";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-64 items-center justify-center">
|
|
|
|
|
<LoadingSpinner />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<LoadingOverlay isLoading={deleting} text="메뉴 삭제 중...">
|
|
|
|
|
<div className="flex h-full flex-col">
|
|
|
|
|
{/* 메인 컨텐츠 - 2:8 비율 */}
|
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
|
|
|
{/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */}
|
|
|
|
|
<div className="w-[20%] border-r bg-gray-50">
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
<h2 className="mb-4 text-lg font-semibold">{getMenuTextSync(MENU_MANAGEMENT_KEYS.MENU_TYPE_TITLE)}</h2>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<Card
|
|
|
|
|
className={`cursor-pointer transition-all ${
|
|
|
|
|
selectedMenuType === "admin" ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
|
|
|
|
|
}`}
|
|
|
|
|
onClick={() => handleMenuTypeChange("admin")}
|
|
|
|
|
>
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="font-medium">{getUITextSync(MENU_MANAGEMENT_KEYS.ADMIN_MENU)}</h3>
|
|
|
|
|
<p className="mt-1 text-sm text-gray-600">
|
|
|
|
|
{getUITextSync(MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge variant={selectedMenuType === "admin" ? "default" : "secondary"}>
|
|
|
|
|
{adminMenus.length}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card
|
|
|
|
|
className={`cursor-pointer transition-all ${
|
|
|
|
|
selectedMenuType === "user" ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
|
|
|
|
|
}`}
|
|
|
|
|
onClick={() => handleMenuTypeChange("user")}
|
|
|
|
|
>
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="font-medium">{getUITextSync(MENU_MANAGEMENT_KEYS.USER_MENU)}</h3>
|
|
|
|
|
<p className="mt-1 text-sm text-gray-600">
|
|
|
|
|
{getUITextSync(MENU_MANAGEMENT_KEYS.USER_DESCRIPTION)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge variant={selectedMenuType === "user" ? "default" : "secondary"}>{userMenus.length}</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 우측 메인 영역 - 메뉴 목록 (80%) */}
|
|
|
|
|
<div className="w-[80%] overflow-hidden">
|
|
|
|
|
<div className="flex h-full flex-col p-6">
|
|
|
|
|
<div className="mb-6 flex-shrink-0">
|
|
|
|
|
<h2 className="mb-2 text-xl font-semibold">
|
|
|
|
|
{getMenuTypeString()} {getMenuTextSync(MENU_MANAGEMENT_KEYS.LIST_TITLE)}
|
|
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 검색 및 필터 영역 */}
|
|
|
|
|
<div className="mb-4 flex-shrink-0">
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="company">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY)}</Label>
|
|
|
|
|
<div className="company-dropdown relative">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setIsCompanyDropdownOpen(!isCompanyDropdownOpen)}
|
|
|
|
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
<span className={selectedCompany === "all" ? "text-muted-foreground" : ""}>
|
|
|
|
|
{selectedCompany === "all"
|
|
|
|
|
? getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL)
|
|
|
|
|
: selectedCompany === "*"
|
|
|
|
|
? getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON)
|
|
|
|
|
: companies.find((c) => c.code === selectedCompany)?.name ||
|
|
|
|
|
getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL)}
|
|
|
|
|
</span>
|
|
|
|
|
<svg
|
|
|
|
|
className={`h-4 w-4 transition-transform ${isCompanyDropdownOpen ? "rotate-180" : ""}`}
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
>
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{isCompanyDropdownOpen && (
|
|
|
|
|
<div className="bg-popover text-popover-foreground absolute top-full left-0 z-50 mt-1 w-full rounded-md border shadow-md">
|
|
|
|
|
{/* 검색 입력 */}
|
|
|
|
|
<div className="border-b p-2">
|
|
|
|
|
<Input
|
|
|
|
|
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_SEARCH)}
|
|
|
|
|
value={companySearchText}
|
|
|
|
|
onChange={(e) => setCompanySearchText(e.target.value)}
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 회사 목록 */}
|
|
|
|
|
<div className="max-h-48 overflow-y-auto">
|
|
|
|
|
<div
|
|
|
|
|
className="hover:bg-accent hover:text-accent-foreground flex cursor-pointer items-center px-2 py-1.5 text-sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSelectedCompany("all");
|
|
|
|
|
setIsCompanyDropdownOpen(false);
|
|
|
|
|
setCompanySearchText("");
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL)}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className="hover:bg-accent hover:text-accent-foreground flex cursor-pointer items-center px-2 py-1.5 text-sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSelectedCompany("*");
|
|
|
|
|
setIsCompanyDropdownOpen(false);
|
|
|
|
|
setCompanySearchText("");
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{companies
|
|
|
|
|
.filter((company) => company.code && company.code.trim() !== "")
|
|
|
|
|
.filter(
|
|
|
|
|
(company) =>
|
|
|
|
|
company.name.toLowerCase().includes(companySearchText.toLowerCase()) ||
|
|
|
|
|
company.code.toLowerCase().includes(companySearchText.toLowerCase()),
|
|
|
|
|
)
|
|
|
|
|
.map((company, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={company.code || `company-${index}`}
|
|
|
|
|
className="hover:bg-accent hover:text-accent-foreground flex cursor-pointer items-center px-2 py-1.5 text-sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSelectedCompany(company.code);
|
|
|
|
|
setIsCompanyDropdownOpen(false);
|
|
|
|
|
setCompanySearchText("");
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{company.code === "*" ? "공통" : company.name}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="search">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_SEARCH)}</Label>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_SEARCH_PLACEHOLDER)}
|
|
|
|
|
value={searchText}
|
|
|
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-end">
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSearchText("");
|
|
|
|
|
setSelectedCompany("all");
|
|
|
|
|
setCompanySearchText("");
|
|
|
|
|
}}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
|
|
|
|
{getUITextSync(MENU_MANAGEMENT_KEYS.FILTER_RESET)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-end">
|
|
|
|
|
<div className="text-sm text-gray-600">
|
|
|
|
|
{getUITextSync(MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT, { count: getCurrentMenus().length })}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 overflow-hidden">
|
|
|
|
|
<div className="mb-4 flex items-center justify-between">
|
|
|
|
|
<div className="text-sm text-gray-600">
|
|
|
|
|
{getUITextSync(MENU_MANAGEMENT_KEYS.LIST_TOTAL, { count: getCurrentMenus().length })}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex space-x-2">
|
|
|
|
|
<Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]">
|
|
|
|
|
{getUITextSync(MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL)}
|
|
|
|
|
</Button>
|
|
|
|
|
{selectedMenus.size > 0 && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={handleDeleteSelectedMenus}
|
|
|
|
|
disabled={deleting}
|
|
|
|
|
className="min-w-[120px]"
|
|
|
|
|
>
|
|
|
|
|
{deleting ? (
|
|
|
|
|
<>
|
|
|
|
|
<LoadingSpinner size="sm" className="mr-2" />
|
|
|
|
|
{getUITextSync(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_PROCESSING)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
getUITextSync(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED_COUNT, {
|
|
|
|
|
count: selectedMenus.size,
|
|
|
|
|
})
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<MenuTable
|
|
|
|
|
menus={getCurrentMenus()}
|
|
|
|
|
title=""
|
|
|
|
|
onAddMenu={handleAddMenu}
|
|
|
|
|
onEditMenu={handleEditMenu}
|
|
|
|
|
onToggleStatus={handleToggleStatus}
|
|
|
|
|
selectedMenus={selectedMenus}
|
|
|
|
|
onMenuSelectionChange={handleMenuSelectionChange}
|
|
|
|
|
onSelectAllMenus={handleSelectAllMenus}
|
|
|
|
|
expandedMenus={expandedMenus}
|
|
|
|
|
onToggleExpand={handleToggleExpand}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<MenuFormModal
|
|
|
|
|
isOpen={formModalOpen}
|
|
|
|
|
onClose={() => setFormModalOpen(false)}
|
|
|
|
|
onSuccess={handleFormSuccess}
|
|
|
|
|
menuId={formData.menuId}
|
|
|
|
|
parentId={formData.parentId}
|
|
|
|
|
menuType={formData.menuType}
|
|
|
|
|
level={formData.level}
|
|
|
|
|
parentCompanyCode={formData.parentCompanyCode}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>메뉴 삭제</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
해당 메뉴를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction onClick={confirmDelete}>삭제</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
</div>
|
|
|
|
|
</LoadingOverlay>
|
|
|
|
|
);
|
|
|
|
|
};
|