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

917 lines
35 KiB
TypeScript
Raw Normal View History

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";
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 {
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>
);
};