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";
|
2025-09-01 18:42:59 +09:00
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
2025-08-21 09:41:46 +09:00
|
|
|
|
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-09-01 18:42:59 +09:00
|
|
|
|
import { ScreenAssignmentTab } from "./ScreenAssignmentTab";
|
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.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();
|
2025-08-25 17:56:33 +09:00
|
|
|
|
// 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정
|
|
|
|
|
|
if (!userLang) {
|
|
|
|
|
|
initializeDefaultTexts();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [userLang]); // userLang 변경 시마다 실행
|
|
|
|
|
|
|
|
|
|
|
|
// 초기 기본 텍스트 설정 함수
|
|
|
|
|
|
const initializeDefaultTexts = () => {
|
|
|
|
|
|
const defaultTexts: Record<string, string> = {};
|
|
|
|
|
|
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
|
|
|
|
|
|
// 기본 한국어 텍스트 제공
|
|
|
|
|
|
const defaultText = getDefaultText(key);
|
|
|
|
|
|
defaultTexts[key] = defaultText;
|
|
|
|
|
|
});
|
|
|
|
|
|
setUiTexts(defaultTexts);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🌐 초기 기본 텍스트 설정 완료:", Object.keys(defaultTexts).length);
|
2025-08-25 17:56:33 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 기본 텍스트 반환 함수
|
|
|
|
|
|
const getDefaultText = (key: string): string => {
|
|
|
|
|
|
const defaultTexts: Record<string, string> = {
|
|
|
|
|
|
"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": "선택 삭제 ({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": "메뉴명 또는 URL로 검색...",
|
|
|
|
|
|
"filter.reset": "초기화",
|
|
|
|
|
|
"table.header.select": "선택",
|
|
|
|
|
|
"table.header.menu.name": "메뉴명",
|
|
|
|
|
|
"table.header.menu.url": "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.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": "선택된 키: {key} - {description}",
|
|
|
|
|
|
"form.menu.name": "메뉴명",
|
|
|
|
|
|
"form.menu.name.placeholder": "메뉴명을 입력하세요",
|
|
|
|
|
|
"form.menu.url": "URL",
|
|
|
|
|
|
"form.menu.url.placeholder": "메뉴 URL을 입력하세요",
|
|
|
|
|
|
"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":
|
|
|
|
|
|
"선택된 {count}개의 메뉴를 영구적으로 삭제하시겠습니까?\n\n⚠️ 주의: 상위 메뉴를 삭제하면 하위 메뉴들도 함께 삭제됩니다.\n이 작업은 되돌릴 수 없습니다.",
|
|
|
|
|
|
"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": "총 {count}개",
|
|
|
|
|
|
"menu.list.search.result": "검색 결과: {count}개",
|
|
|
|
|
|
"ui.expand": "펼치기",
|
|
|
|
|
|
"ui.collapse": "접기",
|
|
|
|
|
|
"ui.menu.collapse": "메뉴 접기",
|
|
|
|
|
|
"ui.language": "언어",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return defaultTexts[key] || key;
|
|
|
|
|
|
};
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 마운트 시 및 userLang 변경 시 다국어 텍스트 로드
|
|
|
|
|
|
useEffect(() => {
|
2025-08-25 17:56:33 +09:00
|
|
|
|
if (userLang && !uiTextsLoading) {
|
2025-08-21 09:41:46 +09:00
|
|
|
|
loadUITexts();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [userLang]); // userLang 변경 시마다 실행
|
|
|
|
|
|
|
2025-08-25 17:22:20 +09:00
|
|
|
|
// uiTexts 상태 변경 감지
|
|
|
|
|
|
useEffect(() => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// 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}`),
|
|
|
|
|
|
// });
|
2025-08-25 17:22:20 +09:00
|
|
|
|
}, [uiTexts]);
|
|
|
|
|
|
|
2025-08-25 17:56:33 +09:00
|
|
|
|
// 컴포넌트 마운트 후 다국어 텍스트 강제 로드 (userLang이 아직 설정되지 않았을 수 있음)
|
2025-08-21 09:41:46 +09:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const timer = setTimeout(() => {
|
2025-08-25 17:56:33 +09:00
|
|
|
|
if (userLang && !uiTextsLoading) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컴포넌트 마운트 후 다국어 텍스트 강제 로드");
|
2025-08-21 09:41:46 +09:00
|
|
|
|
loadUITexts();
|
|
|
|
|
|
}
|
2025-08-25 17:56:33 +09:00
|
|
|
|
}, 300); // 300ms 후 실행
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timer);
|
2025-08-25 17:56:33 +09:00
|
|
|
|
}, [userLang]); // userLang이 설정된 후 실행
|
|
|
|
|
|
|
|
|
|
|
|
// 추가 안전장치: 컴포넌트 마운트 후 일정 시간이 지나면 강제로 다국어 텍스트 로드
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fallbackTimer = setTimeout(() => {
|
|
|
|
|
|
if (!uiTextsLoading && Object.keys(uiTexts).length === 0) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 안전장치: 컴포넌트 마운트 후 강제 다국어 텍스트 로드");
|
2025-08-25 17:56:33 +09:00
|
|
|
|
// 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정
|
|
|
|
|
|
if (!userLang) {
|
|
|
|
|
|
initializeDefaultTexts();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 사용자 언어가 설정된 경우 다국어 텍스트 로드
|
|
|
|
|
|
loadUITexts();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000); // 1초 후 실행
|
|
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(fallbackTimer);
|
|
|
|
|
|
}, [userLang]); // userLang 변경 시마다 실행
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
// 번역 로드 이벤트 감지
|
|
|
|
|
|
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) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`📋 메뉴 목록 조회 시작 (showLoading: ${showLoading})`);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
try {
|
|
|
|
|
|
if (showLoading) {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
await refreshMenus();
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("📋 메뉴 목록 조회 성공");
|
2025-08-21 09:41:46 +09:00
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// 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-10-01 18:17:30 +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) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🏢 회사 목록 응답:", response.data);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const companyList = response.data.data.map((company: any) => ({
|
|
|
|
|
|
code: company.company_code || company.companyCode,
|
|
|
|
|
|
name: company.company_name || company.companyName,
|
|
|
|
|
|
}));
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🏢 변환된 회사 목록:", companyList);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
setCompanies(companyList);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("❌ 회사 목록 조회 실패:", error);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🌐 사용자 언어가 설정되지 않음, 기본값 설정");
|
2025-08-25 17:22:20 +09:00
|
|
|
|
const defaultTexts: Record<string, string> = {};
|
|
|
|
|
|
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
|
2025-08-25 17:56:33 +09:00
|
|
|
|
defaultTexts[key] = getDefaultText(key); // 기본 한국어 텍스트 사용
|
2025-08-25 17:22:20 +09:00
|
|
|
|
});
|
|
|
|
|
|
setUiTexts(defaultTexts);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-25 17:56:33 +09:00
|
|
|
|
// 사용자 언어가 설정된 경우, 기존 uiTexts가 비어있으면 기본 텍스트로 초기화
|
|
|
|
|
|
if (Object.keys(uiTexts).length === 0) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🌐 기존 uiTexts가 비어있음, 기본 텍스트로 초기화");
|
2025-08-25 17:56:33 +09:00
|
|
|
|
const defaultTexts: Record<string, string> = {};
|
|
|
|
|
|
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
|
|
|
|
|
|
defaultTexts[key] = getDefaultText(key);
|
|
|
|
|
|
});
|
|
|
|
|
|
setUiTexts(defaultTexts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// 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;
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🌐 배치 다국어 텍스트 응답:", translations);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
2025-08-25 17:56:33 +09:00
|
|
|
|
// 번역 결과를 상태에 저장 (기존 uiTexts와 병합)
|
|
|
|
|
|
const mergedTranslations = { ...uiTexts, ...translations };
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔧 setUiTexts 호출 전:", {
|
|
|
|
|
|
// translationsCount: Object.keys(translations).length,
|
|
|
|
|
|
// mergedCount: Object.keys(mergedTranslations).length,
|
|
|
|
|
|
// });
|
2025-08-25 17:56:33 +09:00
|
|
|
|
setUiTexts(mergedTranslations);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔧 setUiTexts 호출 후 - mergedTranslations:", mergedTranslations);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
2025-08-25 17:22:20 +09:00
|
|
|
|
// 번역 캐시에 저장 (다른 컴포넌트에서도 사용할 수 있도록)
|
2025-08-25 17:56:33 +09:00
|
|
|
|
setTranslationCache(userLang, mergedTranslations);
|
2025-08-25 17:22:20 +09:00
|
|
|
|
} else {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("❌ 다국어 텍스트 배치 조회 실패:", response.data.message);
|
2025-08-25 17:56:33 +09:00
|
|
|
|
// API 실패 시에도 기존 uiTexts는 유지
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 API 실패로 인해 기존 uiTexts 유지");
|
2025-08-25 17:22:20 +09:00
|
|
|
|
}
|
2025-08-21 09:41:46 +09:00
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("❌ UI 다국어 텍스트 로드 실패:", error);
|
2025-08-25 17:56:33 +09:00
|
|
|
|
// API 실패 시에도 기존 uiTexts는 유지
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 API 실패로 인해 기존 uiTexts 유지");
|
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:56:33 +09:00
|
|
|
|
// uiTexts에 없으면 fallback 또는 키 사용
|
2025-08-21 09:41:46 +09:00
|
|
|
|
if (!text) {
|
|
|
|
|
|
text = fallback || key;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 파라미터 치환
|
|
|
|
|
|
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 () => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🧪 다국어 API 테스트 시작");
|
2025-08-21 09:41:46 +09:00
|
|
|
|
try {
|
2025-08-25 17:22:20 +09:00
|
|
|
|
const text = getUITextSync("menu.management.admin");
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🧪 다국어 API 테스트 결과:", text);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("❌ 다국어 API 테스트 실패:", error);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 대문자 키를 소문자 키로 변환하는 함수
|
|
|
|
|
|
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-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔧 메뉴 수정 시작 - menuId:", menuId);
|
2025-08-25 11:07:39 +09:00
|
|
|
|
|
|
|
|
|
|
// 현재 메뉴 정보 찾기
|
|
|
|
|
|
const currentMenus = selectedMenuType === "admin" ? adminMenus : userMenus;
|
|
|
|
|
|
const menuToEdit = currentMenus.find((menu) => (menu.objid || menu.OBJID) === menuId);
|
|
|
|
|
|
|
|
|
|
|
|
if (menuToEdit) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("수정할 메뉴 정보:", menuToEdit);
|
2025-08-25 11:07:39 +09:00
|
|
|
|
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
menuId: menuId,
|
|
|
|
|
|
parentId: menuToEdit.parent_obj_id || menuToEdit.PARENT_OBJ_ID || "",
|
|
|
|
|
|
menuType: selectedMenuType, // 현재 선택된 메뉴 타입
|
|
|
|
|
|
level: 0, // 기본값
|
|
|
|
|
|
parentCompanyCode: menuToEdit.company_code || menuToEdit.COMPANY_CODE || "",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// 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 || "",
|
|
|
|
|
|
// });
|
2025-08-25 11:07:39 +09:00
|
|
|
|
} else {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("수정할 메뉴를 찾을 수 없음:", menuId);
|
2025-08-25 11:07:39 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
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);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("삭제할 메뉴 IDs:", menuIds);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
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);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("삭제 API 응답:", response);
|
|
|
|
|
|
// console.log("응답 구조:", {
|
|
|
|
|
|
// success: response.success,
|
|
|
|
|
|
// data: response.data,
|
|
|
|
|
|
// message: response.message,
|
|
|
|
|
|
// });
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
if (response.success && response.data) {
|
|
|
|
|
|
const { deletedCount, failedCount } = response.data;
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("삭제 결과:", { deletedCount, failedCount });
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
// 선택된 메뉴 초기화
|
|
|
|
|
|
setSelectedMenus(new Set());
|
|
|
|
|
|
|
|
|
|
|
|
// 메뉴 목록 즉시 새로고침 (로딩 상태 없이)
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("메뉴 목록 새로고침 시작");
|
2025-08-21 09:41:46 +09:00
|
|
|
|
await loadMenus(false);
|
|
|
|
|
|
// 전역 메뉴 상태도 업데이트
|
|
|
|
|
|
await refreshMenus();
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("메뉴 목록 새로고침 완료");
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
// 삭제 결과 메시지
|
|
|
|
|
|
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 {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("삭제 실패:", response);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
toast.error(response.message || "메뉴 삭제에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// 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) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// 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]);
|
|
|
|
|
|
|
|
|
|
|
|
// 디버깅을 위한 간단한 상태 표시
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔍 MenuManagement 렌더링 상태:", {
|
|
|
|
|
|
// loading,
|
|
|
|
|
|
// uiTextsLoading,
|
|
|
|
|
|
// uiTextsCount,
|
|
|
|
|
|
// adminMenusCount,
|
|
|
|
|
|
// userMenusCount,
|
|
|
|
|
|
// selectedMenuType,
|
|
|
|
|
|
// userLang,
|
|
|
|
|
|
// });
|
2025-08-25 17:22:20 +09:00
|
|
|
|
|
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">
|
2025-09-01 18:42:59 +09:00
|
|
|
|
{/* 탭 컨테이너 */}
|
|
|
|
|
|
<Tabs defaultValue="menus" className="flex flex-1 flex-col">
|
|
|
|
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
|
|
|
|
<TabsTrigger value="menus">메뉴 관리</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="screen-assignment">화면 할당</TabsTrigger>
|
|
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 메뉴 관리 탭 */}
|
|
|
|
|
|
<TabsContent value="menus" className="flex-1 overflow-hidden">
|
|
|
|
|
|
<div className="flex h-full">
|
|
|
|
|
|
{/* 메인 컨텐츠 - 2:8 비율 */}
|
|
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
|
|
|
|
{/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */}
|
|
|
|
|
|
<div className="w-[20%] border-r bg-gray-50">
|
|
|
|
|
|
<div className="p-6">
|
2025-09-25 09:29:56 +09:00
|
|
|
|
<Card className="shadow-sm">
|
|
|
|
|
|
<CardHeader className="bg-gray-50/50 pb-3">
|
|
|
|
|
|
<CardTitle className="text-lg">{getUITextSync("menu.type.title")}</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-3 pt-4">
|
2025-09-01 18:42:59 +09:00
|
|
|
|
<Card
|
|
|
|
|
|
className={`cursor-pointer transition-all ${
|
2025-10-02 14:34:15 +09:00
|
|
|
|
selectedMenuType === "admin" ? "border-primary bg-accent" : "hover:border-gray-300"
|
2025-09-01 18:42:59 +09:00
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => handleMenuTypeChange("admin")}
|
2025-08-21 09:41:46 +09:00
|
|
|
|
>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 className="font-medium">{getUITextSync("menu.management.admin")}</h3>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
2025-09-01 18:42:59 +09:00
|
|
|
|
{getUITextSync("menu.management.admin.description")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Badge variant={selectedMenuType === "admin" ? "default" : "secondary"}>
|
|
|
|
|
|
{adminMenus.length}
|
|
|
|
|
|
</Badge>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
</div>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card
|
|
|
|
|
|
className={`cursor-pointer transition-all ${
|
2025-10-02 14:34:15 +09:00
|
|
|
|
selectedMenuType === "user" ? "border-primary bg-accent" : "hover:border-gray-300"
|
2025-09-01 18:42:59 +09:00
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => handleMenuTypeChange("user")}
|
|
|
|
|
|
>
|
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 className="font-medium">{getUITextSync("menu.management.user")}</h3>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
2025-09-01 18:42:59 +09:00
|
|
|
|
{getUITextSync("menu.management.user.description")}
|
|
|
|
|
|
</p>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
</div>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
<Badge variant={selectedMenuType === "user" ? "default" : "secondary"}>
|
|
|
|
|
|
{userMenus.length}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-25 09:29:56 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 우측 메인 영역 - 메뉴 목록 (80%) */}
|
|
|
|
|
|
<div className="w-[80%] overflow-hidden">
|
|
|
|
|
|
<div className="flex h-full flex-col p-6">
|
2025-09-25 09:29:56 +09:00
|
|
|
|
<Card className="flex-1 shadow-sm">
|
|
|
|
|
|
<CardHeader className="bg-gray-50/50">
|
|
|
|
|
|
<CardTitle className="text-xl">
|
|
|
|
|
|
{getMenuTypeString()} {getUITextSync("menu.list.title")}
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="flex-1 overflow-hidden">
|
|
|
|
|
|
{/* 검색 및 필터 영역 */}
|
|
|
|
|
|
<div className="mb-4 flex-shrink-0">
|
2025-09-01 18:42:59 +09:00
|
|
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="company">{getUITextSync("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"
|
2025-08-21 09:41:46 +09:00
|
|
|
|
>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
<span className={selectedCompany === "all" ? "text-muted-foreground" : ""}>
|
|
|
|
|
|
{selectedCompany === "all"
|
|
|
|
|
|
? getUITextSync("filter.company.all")
|
|
|
|
|
|
: selectedCompany === "*"
|
|
|
|
|
|
? getUITextSync("filter.company.common")
|
|
|
|
|
|
: companies.find((c) => c.code === selectedCompany)?.name ||
|
|
|
|
|
|
getUITextSync("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={getUITextSync("filter.company.search")}
|
|
|
|
|
|
value={companySearchText}
|
|
|
|
|
|
onChange={(e) => setCompanySearchText(e.target.value)}
|
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
2025-09-01 18:42:59 +09:00
|
|
|
|
{/* 회사 목록 */}
|
|
|
|
|
|
<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("");
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{getUITextSync("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("");
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{getUITextSync("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 === "*" ? getUITextSync("filter.company.common") : company.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
2025-08-21 09:41:46 +09:00
|
|
|
|
</div>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-08-21 09:41:46 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-01 18:42:59 +09:00
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="search">{getUITextSync("filter.search")}</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder={getUITextSync("filter.search.placeholder")}
|
|
|
|
|
|
value={searchText}
|
|
|
|
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
2025-09-01 18:42:59 +09:00
|
|
|
|
<div className="flex items-end">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setSearchText("");
|
|
|
|
|
|
setSelectedCompany("all");
|
|
|
|
|
|
setCompanySearchText("");
|
|
|
|
|
|
}}
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
className="w-full"
|
|
|
|
|
|
>
|
|
|
|
|
|
{getUITextSync("filter.reset")}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
2025-09-01 18:42:59 +09:00
|
|
|
|
<div className="flex items-end">
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="text-sm text-muted-foreground">
|
2025-09-01 18:42:59 +09:00
|
|
|
|
{getUITextSync("menu.list.search.result", { count: getCurrentMenus().length })}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-25 09:29:56 +09:00
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 overflow-hidden">
|
|
|
|
|
|
<div className="mb-4 flex items-center justify-between">
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="text-sm text-muted-foreground">
|
2025-09-25 09:29:56 +09:00
|
|
|
|
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex space-x-2">
|
|
|
|
|
|
<Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]">
|
|
|
|
|
|
{getUITextSync("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("button.delete.processing")}
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
getUITextSync("button.delete.selected.count", {
|
|
|
|
|
|
count: selectedMenus.size,
|
|
|
|
|
|
})
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Button>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
)}
|
2025-09-25 09:29:56 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<MenuTable
|
|
|
|
|
|
menus={getCurrentMenus()}
|
|
|
|
|
|
title=""
|
|
|
|
|
|
onAddMenu={handleAddMenu}
|
|
|
|
|
|
onEditMenu={handleEditMenu}
|
|
|
|
|
|
onToggleStatus={handleToggleStatus}
|
|
|
|
|
|
selectedMenus={selectedMenus}
|
|
|
|
|
|
onMenuSelectionChange={handleMenuSelectionChange}
|
|
|
|
|
|
onSelectAllMenus={handleSelectAllMenus}
|
|
|
|
|
|
expandedMenus={expandedMenus}
|
|
|
|
|
|
onToggleExpand={handleToggleExpand}
|
|
|
|
|
|
uiTexts={uiTexts}
|
|
|
|
|
|
/>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
</div>
|
2025-09-25 09:29:56 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 화면 할당 탭 */}
|
2025-09-25 09:29:56 +09:00
|
|
|
|
<TabsContent value="screen-assignment" className="flex-1 overflow-hidden p-6">
|
|
|
|
|
|
<Card className="h-full shadow-sm">
|
|
|
|
|
|
<CardHeader className="bg-gray-50/50">
|
|
|
|
|
|
<CardTitle>화면 할당</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="h-full overflow-hidden">
|
|
|
|
|
|
<ScreenAssignmentTab menus={[...adminMenus, ...userMenus]} />
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
</Tabs>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
<MenuFormModal
|
|
|
|
|
|
isOpen={formModalOpen}
|
|
|
|
|
|
onClose={() => setFormModalOpen(false)}
|
|
|
|
|
|
onSuccess={handleFormSuccess}
|
|
|
|
|
|
menuId={formData.menuId}
|
|
|
|
|
|
parentId={formData.parentId}
|
|
|
|
|
|
menuType={formData.menuType}
|
|
|
|
|
|
level={formData.level}
|
|
|
|
|
|
parentCompanyCode={formData.parentCompanyCode}
|
2025-08-25 17:56:33 +09:00
|
|
|
|
uiTexts={uiTexts}
|
2025-08-21 09:41:46 +09:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogTitle>메뉴 삭제</AlertDialogTitle>
|
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
|
해당 메뉴를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
|
|
|
|
<AlertDialogAction onClick={confirmDelete}>삭제</AlertDialogAction>
|
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</LoadingOverlay>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|