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";
|
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());
|
|
|
|
|
|
|
|
|
|
// 다국어 텍스트 훅 사용
|
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 {
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
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) => {
|
2025-08-25 11:07:39 +09:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
};
|