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

938 lines
34 KiB
TypeScript
Raw Normal View History

2025-08-21 09:41:46 +09:00
"use client";
2025-08-25 17:22:20 +09:00
import React, { useState, useEffect, useMemo } from "react";
2025-08-21 09:41:46 +09:00
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";
2025-08-25 17:22:20 +09:00
import { useMenuManagementText, setTranslationCache } from "@/lib/utils/multilang";
2025-08-21 09:41:46 +09:00
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());
// 다국어 텍스트 훅 사용
2025-08-25 17:22:20 +09:00
// getMenuText는 더 이상 사용하지 않음 - getUITextSync만 사용
2025-08-21 09:41:46 +09:00
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: "",
});
2025-08-25 17:22:20 +09:00
// 언어별 텍스트 매핑 테이블 제거 - DB에서 직접 가져옴
// 메뉴관리 페이지에서 사용할 다국어 키들 (실제 DB에 등록된 키들)
const MENU_MANAGEMENT_LANG_KEYS = [
// 페이지 제목 및 설명
"menu.management.title",
"menu.management.description",
"menu.type.title",
"menu.type.admin",
"menu.type.user",
"menu.management.admin",
"menu.management.user",
"menu.management.admin.description",
"menu.management.user.description",
// 버튼
"button.add",
"button.add.top.level",
"button.add.sub",
"button.edit",
"button.delete",
"button.delete.selected",
"button.delete.selected.count",
"button.delete.processing",
"button.cancel",
"button.save",
"button.register",
"button.modify",
// 필터 및 검색
"filter.company",
"filter.company.all",
"filter.company.common",
"filter.company.search",
"filter.search",
"filter.search.placeholder",
"filter.reset",
// 테이블 헤더
"table.header.select",
"table.header.menu.name",
"table.header.menu.url",
"table.header.menu.type",
"table.header.status",
"table.header.company",
"table.header.sequence",
"table.header.actions",
// 상태
"status.active",
"status.inactive",
"status.unspecified",
// 폼
"form.menu.type",
"form.menu.type.admin",
"form.menu.type.user",
"form.status",
"form.company",
"form.company.select",
"form.company.common",
"form.company.submenu.note",
"form.lang.key",
"form.lang.key.select",
"form.lang.key.none",
"form.lang.key.search",
"form.lang.key.selected",
"form.menu.name",
"form.menu.name.placeholder",
"form.menu.url",
"form.menu.url.placeholder",
"form.menu.description",
"form.menu.description.placeholder",
"form.menu.sequence",
// 모달
"modal.menu.register.title",
"modal.menu.modify.title",
"modal.delete.title",
"modal.delete.description",
"modal.delete.batch.description",
// 메시지
"message.loading",
"message.menu.delete.processing",
"message.menu.save.success",
"message.menu.save.failed",
"message.menu.delete.success",
"message.menu.delete.failed",
"message.menu.delete.batch.success",
"message.menu.delete.batch.partial",
"message.menu.status.toggle.success",
"message.menu.status.toggle.failed",
"message.validation.menu.name.required",
"message.validation.company.required",
"message.validation.select.menu.delete",
"message.error.load.menu.list",
"message.error.load.menu.info",
"message.error.load.company.list",
"message.error.load.lang.key.list",
// 리스트 정보
"menu.list.title",
"menu.list.total",
"menu.list.search.result",
// UI
"ui.expand",
"ui.collapse",
"ui.menu.collapse",
"ui.language",
];
2025-08-21 09:41:46 +09:00
// 초기 로딩
useEffect(() => {
loadCompanies();
}, []); // 빈 의존성 배열로 한 번만 실행
// 컴포넌트 마운트 시 및 userLang 변경 시 다국어 텍스트 로드
useEffect(() => {
if (!uiTextsLoading) {
loadUITexts();
}
}, [userLang]); // userLang 변경 시마다 실행
2025-08-25 17:22:20 +09:00
// uiTexts 상태 변경 감지
useEffect(() => {
console.log("🔄 uiTexts 상태 변경됨:", {
count: Object.keys(uiTexts).length,
sampleKeys: Object.keys(uiTexts).slice(0, 5),
sampleValues: Object.entries(uiTexts)
.slice(0, 3)
.map(([k, v]) => `${k}: ${v}`),
});
}, [uiTexts]);
2025-08-21 09:41:46 +09:00
// 컴포넌트 마운트 시 강제로 번역 로드 (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();
2025-08-25 17:22:20 +09:00
console.log("📋 메뉴 목록 조회 성공");
2025-08-21 09:41:46 +09:00
} catch (error) {
console.error("❌ 메뉴 목록 조회 실패:", error);
2025-08-25 17:22:20 +09:00
toast.error(getUITextSync("message.error.load.menu.list"));
2025-08-21 09:41:46 +09:00
} finally {
if (showLoading) {
setLoading(false);
}
}
};
// 회사 목록 조회
const loadCompanies = async () => {
2025-08-25 17:22:20 +09:00
console.log("🏢 회사 목록 조회 시작");
2025-08-21 09:41:46 +09:00
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);
}
};
2025-08-25 17:22:20 +09:00
// 다국어 텍스트 로드 함수 - 배치 API 사용
2025-08-21 09:41:46 +09:00
const loadUITexts = async () => {
if (uiTextsLoading) return; // 이미 로딩 중이면 중단
2025-08-25 17:22:20 +09:00
// userLang이 설정되지 않았으면 기본값 설정
if (!userLang) {
console.log("🌐 사용자 언어가 설정되지 않음, 기본값 설정");
const defaultTexts: Record<string, string> = {};
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
defaultTexts[key] = key; // 키를 기본값으로 사용
});
setUiTexts(defaultTexts);
return;
}
console.log("🌐 UI 다국어 텍스트 로드 시작", {
userLang,
apiParams: {
companyCode: "*",
menuCode: "menu.management",
userLang: userLang,
},
});
2025-08-21 09:41:46 +09:00
setUiTextsLoading(true);
try {
2025-08-25 17:22:20 +09:00
// 배치 API를 사용하여 모든 다국어 키를 한 번에 조회
const response = await apiClient.post(
"/multilang/batch",
{
langKeys: MENU_MANAGEMENT_LANG_KEYS,
companyCode: "*", // 모든 회사
menuCode: "menu.management", // 메뉴관리 메뉴
userLang: userLang, // body에 포함
},
{
params: {}, // query params는 비움
},
);
2025-08-21 09:41:46 +09:00
2025-08-25 17:22:20 +09:00
if (response.data.success) {
const translations = response.data.data;
console.log("🌐 배치 다국어 텍스트 응답:", translations);
2025-08-21 09:41:46 +09:00
2025-08-25 17:22:20 +09:00
// 번역 결과를 상태에 저장
console.log("🔧 setUiTexts 호출 전:", { translationsCount: Object.keys(translations).length });
setUiTexts(translations);
console.log("🔧 setUiTexts 호출 후 - translations:", translations);
2025-08-21 09:41:46 +09:00
2025-08-25 17:22:20 +09:00
// 번역 캐시에 저장 (다른 컴포넌트에서도 사용할 수 있도록)
setTranslationCache(userLang, translations);
} else {
console.error("❌ 다국어 텍스트 배치 조회 실패:", response.data.message);
// API 실패 시 기본 텍스트 사용
const defaultTexts: Record<string, string> = {};
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
defaultTexts[key] = key; // 키를 기본값으로 사용
});
setUiTexts(defaultTexts);
}
2025-08-21 09:41:46 +09:00
} catch (error) {
console.error("❌ UI 다국어 텍스트 로드 실패:", error);
2025-08-25 17:22:20 +09:00
// API 실패 시 기본 텍스트 사용
const defaultTexts: Record<string, string> = {};
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
defaultTexts[key] = key; // 키를 기본값으로 사용
});
setUiTexts(defaultTexts);
2025-08-21 09:41:46 +09:00
} finally {
setUiTextsLoading(false);
}
};
2025-08-25 17:22:20 +09:00
// UI 텍스트 가져오기 함수 (동기 버전만 사용)
// getUIText 함수는 제거 - getUITextSync만 사용
2025-08-21 09:41:46 +09:00
2025-08-25 17:22:20 +09:00
// 동기 버전 (DB에서 가져온 번역 텍스트 사용)
2025-08-21 09:41:46 +09:00
const getUITextSync = (key: string, params?: Record<string, string | number>, fallback?: string): string => {
2025-08-25 17:22:20 +09:00
// uiTexts에서 번역 텍스트 찾기
2025-08-21 09:41:46 +09:00
let text = uiTexts[key];
2025-08-25 17:22:20 +09:00
// 디버깅: uiTexts 상태 확인
2025-08-21 09:41:46 +09:00
if (!text) {
2025-08-25 17:22:20 +09:00
console.log(`🔍 getUITextSync - 키 "${key}"를 uiTexts에서 찾을 수 없음`);
console.log("🔍 uiTexts 상태:", {
count: Object.keys(uiTexts).length,
sampleKeys: Object.keys(uiTexts).slice(0, 5),
});
2025-08-21 09:41:46 +09:00
text = fallback || key;
2025-08-25 17:22:20 +09:00
} else {
console.log(`✅ getUITextSync - 키 "${key}" 번역 텍스트 찾음: "${text}"`);
2025-08-21 09:41:46 +09:00
}
// 파라미터 치환
if (params && text) {
Object.entries(params).forEach(([paramKey, paramValue]) => {
text = text!.replace(`{${paramKey}}`, String(paramValue));
});
}
return text || key;
};
2025-08-25 17:22:20 +09:00
// 다국어 API 테스트 함수 (getUITextSync 사용)
2025-08-21 09:41:46 +09:00
const testMultiLangAPI = async () => {
console.log("🧪 다국어 API 테스트 시작");
try {
2025-08-25 17:22:20 +09:00
const text = getUITextSync("menu.management.admin");
2025-08-21 09:41:46 +09:00
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) => {
console.log("🔧 메뉴 수정 시작 - menuId:", menuId);
// 현재 메뉴 정보 찾기
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId);
if (menuToEdit) {
console.log("수정할 메뉴 정보:", menuToEdit);
setFormData({
menuId: menuId,
parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "",
menuType: selectedMenuType, // 현재 선택된 메뉴 타입
level: 0, // 기본값
parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "",
});
console.log("설정된 formData:", {
menuId: menuId,
parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "",
menuType: selectedMenuType,
level: 0,
parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "",
});
} else {
console.error("수정할 메뉴를 찾을 수 없음:", menuId);
}
2025-08-21 09:41:46 +09:00
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) {
2025-08-25 17:22:20 +09:00
toast.error(getUITextSync("message.validation.select.menu.delete"));
2025-08-21 09:41:46 +09:00
return;
}
2025-08-25 17:22:20 +09:00
if (!confirm(getUITextSync("modal.delete.batch.description", { count: selectedMenus.size }))) {
2025-08-21 09:41:46 +09:00
return;
}
setDeleting(true);
try {
const menuIds = Array.from(selectedMenus);
console.log("삭제할 메뉴 IDs:", menuIds);
2025-08-25 17:22:20 +09:00
toast.info(getUITextSync("message.menu.delete.processing"));
2025-08-21 09:41:46 +09:00
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) {
2025-08-25 17:22:20 +09:00
toast.success(getUITextSync("message.menu.delete.batch.success", { count: deletedCount }));
2025-08-21 09:41:46 +09:00
} else {
toast.success(
2025-08-25 17:22:20 +09:00
getUITextSync("message.menu.delete.batch.partial", {
2025-08-21 09:41:46 +09:00
success: deletedCount,
failed: failedCount,
}),
);
}
} else {
console.error("삭제 실패:", response);
toast.error(response.message || "메뉴 삭제에 실패했습니다.");
}
} catch (error) {
console.error("메뉴 삭제 중 오류:", error);
2025-08-25 17:22:20 +09:00
toast.error(getUITextSync("message.menu.delete.failed"));
2025-08-21 09:41:46 +09:00
} 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);
2025-08-25 17:22:20 +09:00
toast.error(getUITextSync("message.menu.status.toggle.failed"));
2025-08-21 09:41:46 +09:00
}
};
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 = () => {
2025-08-25 17:22:20 +09:00
return selectedMenuType === "admin" ? getUITextSync("menu.type.admin") : getUITextSync("menu.type.user");
2025-08-21 09:41:46 +09:00
};
const getMenuTypeValue = () => {
return selectedMenuType === "admin" ? "0" : "1";
};
2025-08-25 17:22:20 +09:00
// uiTextsCount를 useMemo로 계산하여 상태 변경 시에만 재계산
const uiTextsCount = useMemo(() => Object.keys(uiTexts).length, [uiTexts]);
const adminMenusCount = useMemo(() => adminMenus?.length || 0, [adminMenus]);
const userMenusCount = useMemo(() => userMenus?.length || 0, [userMenus]);
// 디버깅을 위한 간단한 상태 표시
console.log("🔍 MenuManagement 렌더링 상태:", {
loading,
uiTextsLoading,
uiTextsCount,
adminMenusCount,
userMenusCount,
selectedMenuType,
userLang,
});
2025-08-21 09:41:46 +09:00
if (loading) {
return (
<div className="flex h-64 items-center justify-center">
<LoadingSpinner />
</div>
);
}
return (
2025-08-25 17:22:20 +09:00
<LoadingOverlay isLoading={deleting} text={getUITextSync("button.delete.processing")}>
2025-08-21 09:41:46 +09:00
<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">
2025-08-25 17:22:20 +09:00
<h2 className="mb-4 text-lg font-semibold">{getUITextSync("menu.type.title")}</h2>
2025-08-21 09:41:46 +09:00
<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>
2025-08-25 17:22:20 +09:00
<h3 className="font-medium">{getUITextSync("menu.management.admin")}</h3>
2025-08-21 09:41:46 +09:00
<p className="mt-1 text-sm text-gray-600">
2025-08-25 17:22:20 +09:00
{getUITextSync("menu.management.admin.description")}
2025-08-21 09:41:46 +09:00
</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>
2025-08-25 17:22:20 +09:00
<h3 className="font-medium">{getUITextSync("menu.management.user")}</h3>
2025-08-21 09:41:46 +09:00
<p className="mt-1 text-sm text-gray-600">
2025-08-25 17:22:20 +09:00
{getUITextSync("menu.management.user.description")}
2025-08-21 09:41:46 +09:00
</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">
2025-08-25 17:22:20 +09:00
{getMenuTypeString()} {getUITextSync("menu.list.title")}
2025-08-21 09:41:46 +09:00
</h2>
</div>
{/* 검색 및 필터 영역 */}
<div className="mb-4 flex-shrink-0">
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<div>
2025-08-25 17:22:20 +09:00
<Label htmlFor="company">{getUITextSync("filter.company")}</Label>
2025-08-21 09:41:46 +09:00
<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"
2025-08-25 17:22:20 +09:00
? getUITextSync("filter.company.all")
2025-08-21 09:41:46 +09:00
: selectedCompany === "*"
2025-08-25 17:22:20 +09:00
? getUITextSync("filter.company.common")
2025-08-21 09:41:46 +09:00
: companies.find((c) => c.code === selectedCompany)?.name ||
2025-08-25 17:22:20 +09:00
getUITextSync("filter.company.all")}
2025-08-21 09:41:46 +09:00
</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
2025-08-25 17:22:20 +09:00
placeholder={getUITextSync("filter.company.search")}
2025-08-21 09:41:46 +09:00
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("");
}}
>
2025-08-25 17:22:20 +09:00
{getUITextSync("filter.company.all")}
2025-08-21 09:41:46 +09:00
</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("");
}}
>
2025-08-25 17:22:20 +09:00
{getUITextSync("filter.company.common")}
2025-08-21 09:41:46 +09:00
</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("");
}}
>
2025-08-25 17:22:20 +09:00
{company.code === "*" ? getUITextSync("filter.company.common") : company.name}
2025-08-21 09:41:46 +09:00
</div>
))}
</div>
</div>
)}
</div>
</div>
<div>
2025-08-25 17:22:20 +09:00
<Label htmlFor="search">{getUITextSync("filter.search")}</Label>
2025-08-21 09:41:46 +09:00
<Input
2025-08-25 17:22:20 +09:00
placeholder={getUITextSync("filter.search.placeholder")}
2025-08-21 09:41:46 +09:00
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"
>
2025-08-25 17:22:20 +09:00
{getUITextSync("filter.reset")}
2025-08-21 09:41:46 +09:00
</Button>
</div>
<div className="flex items-end">
<div className="text-sm text-gray-600">
2025-08-25 17:22:20 +09:00
{getUITextSync("menu.list.search.result", { count: getCurrentMenus().length })}
2025-08-21 09:41:46 +09:00
</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">
2025-08-25 17:22:20 +09:00
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
2025-08-21 09:41:46 +09:00
</div>
<div className="flex space-x-2">
<Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]">
2025-08-25 17:22:20 +09:00
{getUITextSync("button.add.top.level")}
2025-08-21 09:41:46 +09:00
</Button>
{selectedMenus.size > 0 && (
<Button
variant="destructive"
onClick={handleDeleteSelectedMenus}
disabled={deleting}
className="min-w-[120px]"
>
{deleting ? (
<>
<LoadingSpinner size="sm" className="mr-2" />
2025-08-25 17:22:20 +09:00
{getUITextSync("button.delete.processing")}
2025-08-21 09:41:46 +09:00
</>
) : (
2025-08-25 17:22:20 +09:00
getUITextSync("button.delete.selected.count", {
2025-08-21 09:41:46 +09:00
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>
);
};