888 lines
34 KiB
TypeScript
888 lines
34 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect } from "react";
|
||
import { MenuItem, MenuFormData, menuApi, LangKey } from "@/lib/api/menu";
|
||
import { companyAPI } from "@/lib/api/company";
|
||
import { screenApi } from "@/lib/api/screen";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||
import { toast } from "sonner";
|
||
import { ChevronDown, Search } from "lucide-react";
|
||
import { MENU_MANAGEMENT_KEYS } from "@/lib/utils/multilang";
|
||
import { ScreenDefinition } from "@/types/screen";
|
||
|
||
interface Company {
|
||
company_code: string;
|
||
company_name: string;
|
||
status: string;
|
||
}
|
||
|
||
interface MenuFormModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onSuccess: () => void;
|
||
menuId?: string;
|
||
parentId?: string;
|
||
menuType?: string;
|
||
level?: number;
|
||
parentCompanyCode?: string;
|
||
// 다국어 텍스트 props 추가
|
||
uiTexts: Record<string, string>;
|
||
}
|
||
|
||
export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||
isOpen,
|
||
onClose,
|
||
onSuccess,
|
||
menuId,
|
||
parentId,
|
||
menuType,
|
||
level,
|
||
parentCompanyCode,
|
||
uiTexts,
|
||
}) => {
|
||
// console.log("🎯 MenuFormModal 렌더링 - Props:", {
|
||
// isOpen,
|
||
// menuId,
|
||
// parentId,
|
||
// menuType,
|
||
// level,
|
||
// parentCompanyCode,
|
||
// });
|
||
|
||
// 다국어 텍스트 가져오기 함수
|
||
const getText = (key: string, fallback?: string): string => {
|
||
return uiTexts[key] || fallback || key;
|
||
};
|
||
|
||
// console.log("🔍 MenuFormModal 컴포넌트 마운트됨");
|
||
|
||
const [formData, setFormData] = useState<MenuFormData>({
|
||
parentObjId: parentId || "0",
|
||
menuNameKor: "",
|
||
menuUrl: "",
|
||
menuDesc: "",
|
||
seq: 1,
|
||
menuType: "1",
|
||
status: "ACTIVE",
|
||
companyCode: parentCompanyCode || "none",
|
||
langKey: "",
|
||
});
|
||
|
||
// 화면 할당 관련 상태
|
||
const [urlType, setUrlType] = useState<"direct" | "screen">("screen"); // URL 직접 입력 or 화면 할당 (기본값: 화면 할당)
|
||
const [selectedScreen, setSelectedScreen] = useState<ScreenDefinition | null>(null);
|
||
const [screens, setScreens] = useState<ScreenDefinition[]>([]);
|
||
const [screenSearchText, setScreenSearchText] = useState("");
|
||
const [isScreenDropdownOpen, setIsScreenDropdownOpen] = useState(false);
|
||
|
||
const [loading, setLoading] = useState(false);
|
||
const [isEdit, setIsEdit] = useState(false);
|
||
const [companies, setCompanies] = useState<Company[]>([]);
|
||
const [langKeys, setLangKeys] = useState<LangKey[]>([]);
|
||
const [isLangKeyDropdownOpen, setIsLangKeyDropdownOpen] = useState(false);
|
||
const [langKeySearchText, setLangKeySearchText] = useState("");
|
||
|
||
// 화면 목록 로드
|
||
const loadScreens = async () => {
|
||
try {
|
||
const response = await screenApi.getScreens({ size: 1000 }); // 모든 화면 가져오기
|
||
|
||
// console.log("🔍 화면 목록 로드 디버깅:", {
|
||
// totalScreens: response.data.length,
|
||
// firstScreen: response.data[0],
|
||
// firstScreenFields: response.data[0] ? Object.keys(response.data[0]) : [],
|
||
// firstScreenValues: response.data[0] ? Object.values(response.data[0]) : [],
|
||
// allScreenIds: response.data
|
||
// .map((s) => ({
|
||
// screenId: s.screenId,
|
||
// legacyId: s.id,
|
||
// name: s.screenName,
|
||
// code: s.screenCode,
|
||
// }))
|
||
// .slice(0, 5), // 처음 5개만 출력
|
||
// });
|
||
|
||
setScreens(response.data);
|
||
console.log("✅ 화면 목록 로드 완료:", response.data.length);
|
||
} catch (error) {
|
||
console.error("❌ 화면 목록 로드 실패:", error);
|
||
toast.error("화면 목록을 불러오는데 실패했습니다.");
|
||
}
|
||
};
|
||
|
||
// 화면 선택 시 URL 자동 설정
|
||
const handleScreenSelect = (screen: ScreenDefinition) => {
|
||
// console.log("🖥️ 화면 선택 디버깅:", {
|
||
// screen,
|
||
// screenId: screen.screenId,
|
||
// screenIdType: typeof screen.screenId,
|
||
// legacyId: screen.id,
|
||
// allFields: Object.keys(screen),
|
||
// screenValues: Object.values(screen),
|
||
// });
|
||
|
||
// ScreenDefinition에서는 screenId 필드를 사용
|
||
const actualScreenId = screen.screenId || screen.id;
|
||
|
||
if (!actualScreenId) {
|
||
console.error("❌ 화면 ID를 찾을 수 없습니다:", screen);
|
||
toast.error("화면 ID를 찾을 수 없습니다. 다른 화면을 선택해주세요.");
|
||
return;
|
||
}
|
||
|
||
setSelectedScreen(screen);
|
||
setIsScreenDropdownOpen(false);
|
||
|
||
// 실제 라우팅 패턴에 맞게 URL 생성: /screens/[screenId] (복수형)
|
||
// 관리자 메뉴인 경우 mode=admin 파라미터 추가
|
||
let screenUrl = `/screens/${actualScreenId}`;
|
||
|
||
// 현재 메뉴 타입이 관리자인지 확인 (0 또는 "admin")
|
||
const isAdminMenu = menuType === "0" || menuType === "admin" || formData.menuType === "0";
|
||
if (isAdminMenu) {
|
||
screenUrl += "?mode=admin";
|
||
}
|
||
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
menuUrl: screenUrl,
|
||
}));
|
||
|
||
// console.log("🖥️ 화면 선택 완료:", {
|
||
// screenId: screen.screenId,
|
||
// legacyId: screen.id,
|
||
// actualScreenId,
|
||
// screenName: screen.screenName,
|
||
// menuType: menuType,
|
||
// formDataMenuType: formData.menuType,
|
||
// isAdminMenu,
|
||
// generatedUrl: screenUrl,
|
||
// });
|
||
};
|
||
|
||
// URL 타입 변경 시 처리
|
||
const handleUrlTypeChange = (type: "direct" | "screen") => {
|
||
// console.log("🔄 URL 타입 변경:", {
|
||
// from: urlType,
|
||
// to: type,
|
||
// currentSelectedScreen: selectedScreen?.screenName,
|
||
// currentUrl: formData.menuUrl,
|
||
// });
|
||
|
||
setUrlType(type);
|
||
|
||
if (type === "direct") {
|
||
// 직접 입력 모드로 변경 시 선택된 화면 초기화
|
||
setSelectedScreen(null);
|
||
// URL 필드도 초기화 (사용자가 직접 입력할 수 있도록)
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
menuUrl: "",
|
||
}));
|
||
} else {
|
||
// 화면 할당 모드로 변경 시
|
||
// 기존에 선택된 화면이 있고, 해당 화면의 URL이 있다면 유지
|
||
if (selectedScreen) {
|
||
console.log("📋 기존 선택된 화면 유지:", selectedScreen.screenName);
|
||
// 현재 선택된 화면으로 URL 재생성
|
||
const actualScreenId = selectedScreen.screenId || selectedScreen.id;
|
||
let screenUrl = `/screens/${actualScreenId}`;
|
||
|
||
// 관리자 메뉴인 경우 mode=admin 파라미터 추가
|
||
const isAdminMenu = menuType === "0" || menuType === "admin" || formData.menuType === "0";
|
||
if (isAdminMenu) {
|
||
screenUrl += "?mode=admin";
|
||
}
|
||
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
menuUrl: screenUrl,
|
||
}));
|
||
} else {
|
||
// 선택된 화면이 없으면 URL만 초기화
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
menuUrl: "",
|
||
}));
|
||
}
|
||
}
|
||
};
|
||
|
||
// loadMenuData 함수를 먼저 정의
|
||
const loadMenuData = async () => {
|
||
console.log("loadMenuData 호출됨 - menuId:", menuId);
|
||
if (!menuId) {
|
||
console.log("menuId가 없어서 loadMenuData 종료");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setLoading(true);
|
||
console.log("API 호출 시작 - menuId:", menuId);
|
||
// console.log("API URL:", `/admin/menus/${menuId}`);
|
||
|
||
const response = await menuApi.getMenuInfo(menuId);
|
||
console.log("메뉴 정보 조회 응답:", response);
|
||
console.log("응답 success:", response.success);
|
||
console.log("응답 data:", response.data);
|
||
console.log("응답 message:", response.message);
|
||
console.log("응답 errorCode:", response.errorCode);
|
||
|
||
if (response.success && response.data) {
|
||
const menu = response.data;
|
||
console.log("메뉴 데이터:", menu);
|
||
console.log("메뉴 데이터 키들:", Object.keys(menu));
|
||
|
||
// 대문자 키와 소문자 키 모두 처리
|
||
const menuType = menu.menu_type || menu.MENU_TYPE || "1";
|
||
const status = menu.status || menu.STATUS || "active";
|
||
const companyCode = menu.company_code || menu.COMPANY_CODE || "";
|
||
const langKey = menu.lang_key || menu.LANG_KEY || "";
|
||
|
||
// 메뉴 타입 변환 (admin/user -> 0/1)
|
||
let convertedMenuType = menuType;
|
||
if (menuType === "admin" || menuType === "0") {
|
||
convertedMenuType = "0";
|
||
} else if (menuType === "user" || menuType === "1") {
|
||
convertedMenuType = "1";
|
||
}
|
||
|
||
// 상태 변환 (active/inactive/inActive -> ACTIVE/INACTIVE)
|
||
let convertedStatus = status;
|
||
if (status === "active") {
|
||
convertedStatus = "ACTIVE";
|
||
} else if (status === "inactive" || status === "inActive") {
|
||
convertedStatus = "INACTIVE";
|
||
}
|
||
|
||
const menuUrl = menu.menu_url || menu.MENU_URL || "";
|
||
|
||
// URL이 "/screens/"로 시작하면 화면 할당으로 판단 (실제 라우팅 패턴에 맞게 수정)
|
||
const isScreenUrl = menuUrl.startsWith("/screens/");
|
||
|
||
setFormData({
|
||
objid: menu.objid || menu.OBJID,
|
||
parentObjId: menu.parent_obj_id || menu.PARENT_OBJ_ID || "0",
|
||
menuNameKor: menu.menu_name_kor || menu.MENU_NAME_KOR || "",
|
||
menuUrl: menuUrl,
|
||
menuDesc: menu.menu_desc || menu.MENU_DESC || "",
|
||
seq: menu.seq || menu.SEQ || 1,
|
||
menuType: convertedMenuType,
|
||
status: convertedStatus,
|
||
companyCode: companyCode,
|
||
langKey: langKey, // 다국어 키 설정
|
||
});
|
||
|
||
// URL 타입 설정
|
||
if (isScreenUrl) {
|
||
setUrlType("screen");
|
||
// "/screens/123" 또는 "/screens/123?mode=admin" 형태에서 ID 추출
|
||
const screenId = menuUrl.match(/\/screens\/(\d+)/)?.[1];
|
||
if (screenId) {
|
||
// console.log("🔍 기존 메뉴에서 화면 ID 추출:", {
|
||
// menuUrl,
|
||
// screenId,
|
||
// hasAdminParam: menuUrl.includes("mode=admin"),
|
||
// currentScreensCount: screens.length,
|
||
// });
|
||
|
||
// 화면 설정 함수
|
||
const setScreenFromId = () => {
|
||
const screen = screens.find((s) => s.screenId.toString() === screenId || s.id?.toString() === screenId);
|
||
if (screen) {
|
||
setSelectedScreen(screen);
|
||
// console.log("🖥️ 기존 메뉴의 할당된 화면 설정:", {
|
||
// screen,
|
||
// originalUrl: menuUrl,
|
||
// hasAdminParam: menuUrl.includes("mode=admin"),
|
||
// });
|
||
return true;
|
||
} else {
|
||
// console.warn("⚠️ 해당 ID의 화면을 찾을 수 없음:", {
|
||
// screenId,
|
||
// availableScreens: screens.map((s) => ({ screenId: s.screenId, id: s.id, name: s.screenName })),
|
||
// });
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// 화면 목록이 이미 있으면 즉시 설정, 없으면 로드 완료 대기
|
||
if (screens.length > 0) {
|
||
console.log("📋 화면 목록이 이미 로드됨 - 즉시 설정");
|
||
setScreenFromId();
|
||
} else {
|
||
console.log("⏳ 화면 목록 로드 대기 중...");
|
||
// 화면 ID를 저장해두고, 화면 목록 로드 완료 후 설정
|
||
setTimeout(() => {
|
||
console.log("🔄 재시도: 화면 목록 로드 후 설정");
|
||
setScreenFromId();
|
||
}, 500);
|
||
}
|
||
}
|
||
} else {
|
||
setUrlType("direct");
|
||
setSelectedScreen(null);
|
||
}
|
||
|
||
// console.log("설정된 폼 데이터:", {
|
||
// objid: menu.objid || menu.OBJID,
|
||
// parentObjId: menu.parent_obj_id || menu.PARENT_OBJ_ID || "0",
|
||
// menuNameKor: menu.menu_name_kor || menu.MENU_NAME_KOR || "",
|
||
// menuUrl: menu.menu_url || menu.MENU_URL || "",
|
||
// menuDesc: menu.menu_desc || menu.MENU_DESC || "",
|
||
// seq: menu.seq || menu.SEQ || 1,
|
||
// menuType: convertedMenuType,
|
||
// status: convertedStatus,
|
||
// companyCode: companyCode,
|
||
// langKey: langKey,
|
||
// });
|
||
}
|
||
} catch (error: any) {
|
||
console.error("메뉴 정보 로딩 오류:", error);
|
||
// console.error("오류 상세 정보:", {
|
||
// message: error?.message,
|
||
// stack: error?.stack,
|
||
// response: error?.response,
|
||
// });
|
||
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// useEffect를 loadMenuData 함수 정의 후로 이동
|
||
useEffect(() => {
|
||
console.log("🚀 MenuFormModal useEffect 실행됨!");
|
||
console.log("📋 useEffect 파라미터:", { menuId, parentId, menuType });
|
||
console.log("MenuFormModal useEffect - menuId:", menuId, "parentId:", parentId, "menuType:", menuType);
|
||
|
||
if (menuId) {
|
||
console.log("메뉴 수정 모드 - menuId:", menuId);
|
||
setIsEdit(true);
|
||
loadMenuData();
|
||
} else {
|
||
console.log("메뉴 등록 모드 - parentId:", parentId, "menuType:", menuType);
|
||
setIsEdit(false);
|
||
|
||
// 메뉴 타입 변환 (0 -> 0, 1 -> 1, admin -> 0, user -> 1)
|
||
let defaultMenuType = "1"; // 기본값은 사용자
|
||
if (menuType === "0" || menuType === "admin") {
|
||
defaultMenuType = "0"; // 관리자
|
||
} else if (menuType === "1" || menuType === "user") {
|
||
defaultMenuType = "1"; // 사용자
|
||
}
|
||
|
||
setFormData({
|
||
parentObjId: parentId || "0",
|
||
menuNameKor: "",
|
||
menuUrl: "",
|
||
menuDesc: "",
|
||
seq: 1,
|
||
menuType: defaultMenuType,
|
||
status: "ACTIVE", // 기본값은 활성화
|
||
companyCode: parentCompanyCode || "none", // 상위 메뉴의 회사 코드를 기본값으로 설정
|
||
langKey: "", // 다국어 키 초기화
|
||
});
|
||
|
||
// console.log("메뉴 등록 기본값 설정:", {
|
||
// parentObjId: parentId || "0",
|
||
// menuType: defaultMenuType,
|
||
// status: "ACTIVE",
|
||
// companyCode: "",
|
||
// langKey: "",
|
||
// });
|
||
}
|
||
}, [menuId, parentId, menuType]);
|
||
|
||
// 강제로 useEffect 실행시키기 위한 별도 useEffect
|
||
useEffect(() => {
|
||
console.log("🔧 강제 useEffect 실행 - 컴포넌트 마운트됨");
|
||
console.log("🔧 현재 props:", { isOpen, menuId, parentId, menuType });
|
||
|
||
// isOpen이 true일 때만 실행
|
||
if (isOpen && menuId) {
|
||
console.log("🔧 모달이 열렸고 menuId가 있음 - 강제 실행");
|
||
// 약간의 지연 후 실행
|
||
setTimeout(() => {
|
||
console.log("🔧 setTimeout으로 loadMenuData 실행");
|
||
loadMenuData();
|
||
}, 100);
|
||
}
|
||
}, [isOpen]); // isOpen만 의존성으로 설정
|
||
|
||
// 회사 목록 로드
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
loadCompanies();
|
||
}
|
||
}, [isOpen]);
|
||
|
||
// 다국어 키 목록 로드
|
||
useEffect(() => {
|
||
if (isOpen && formData.companyCode) {
|
||
loadLangKeys();
|
||
}
|
||
}, [isOpen, formData.companyCode]);
|
||
|
||
// 화면 목록 로드
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
loadScreens();
|
||
}
|
||
}, [isOpen]);
|
||
|
||
// 화면 목록 로드 완료 후 기존 메뉴의 할당된 화면 설정
|
||
useEffect(() => {
|
||
if (screens.length > 0 && isEdit && formData.menuUrl && urlType === "screen") {
|
||
const menuUrl = formData.menuUrl;
|
||
if (menuUrl.startsWith("/screens/")) {
|
||
const screenId = menuUrl.match(/\/screens\/(\d+)/)?.[1];
|
||
if (screenId && !selectedScreen) {
|
||
console.log("🔄 화면 목록 로드 완료 - 기존 할당 화면 자동 설정");
|
||
const screen = screens.find((s) => s.screenId.toString() === screenId || s.id?.toString() === screenId);
|
||
if (screen) {
|
||
setSelectedScreen(screen);
|
||
// console.log("✅ 기존 메뉴의 할당된 화면 자동 설정 완료:", {
|
||
// screenId,
|
||
// screenName: screen.screenName,
|
||
// menuUrl,
|
||
// });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}, [screens, isEdit, formData.menuUrl, urlType, selectedScreen]);
|
||
|
||
// 드롭다운 외부 클릭 시 닫기
|
||
useEffect(() => {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
const target = event.target as Element;
|
||
if (!target.closest(".langkey-dropdown")) {
|
||
setIsLangKeyDropdownOpen(false);
|
||
setLangKeySearchText("");
|
||
}
|
||
if (!target.closest(".screen-dropdown")) {
|
||
setIsScreenDropdownOpen(false);
|
||
setScreenSearchText("");
|
||
}
|
||
};
|
||
|
||
if (isLangKeyDropdownOpen || isScreenDropdownOpen) {
|
||
document.addEventListener("mousedown", handleClickOutside);
|
||
}
|
||
|
||
return () => {
|
||
document.removeEventListener("mousedown", handleClickOutside);
|
||
};
|
||
}, [isLangKeyDropdownOpen, isScreenDropdownOpen]);
|
||
|
||
const loadCompanies = async () => {
|
||
try {
|
||
const companyList = await companyAPI.getList({ status: "active" });
|
||
setCompanies(companyList);
|
||
} catch (error) {
|
||
console.error("회사 목록 로딩 오류:", error);
|
||
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST));
|
||
}
|
||
};
|
||
|
||
const loadLangKeys = async () => {
|
||
console.log("🔤 다국어 키 목록 조회 시작 - companyCode:", formData.companyCode);
|
||
try {
|
||
const response = await menuApi.getLangKeys({
|
||
companyCode: formData.companyCode === "none" ? "*" : formData.companyCode,
|
||
});
|
||
|
||
if (response.success && response.data) {
|
||
// 활성화된 다국어 키만 필터링
|
||
const activeKeys = response.data.filter((key) => key.isActive === "Y");
|
||
console.log("🔤 다국어 키 목록 조회 성공:", activeKeys.length, "개 (활성화된 키)");
|
||
setLangKeys(activeKeys);
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ 다국어 키 목록 로딩 오류:", error);
|
||
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST));
|
||
setLangKeys([]);
|
||
}
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
if (!formData.menuNameKor.trim()) {
|
||
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED));
|
||
return;
|
||
}
|
||
|
||
if (!formData.companyCode) {
|
||
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setLoading(true);
|
||
|
||
// 백엔드에 전송할 데이터 변환
|
||
const submitData = {
|
||
...formData,
|
||
// 상태를 소문자로 변환 (백엔드에서 소문자 기대)
|
||
status: formData.status.toLowerCase(),
|
||
};
|
||
|
||
console.log("저장할 데이터:", submitData);
|
||
|
||
let response;
|
||
|
||
if (isEdit && menuId) {
|
||
// 수정 모드: updateMenu API 호출
|
||
console.log("🔧 메뉴 수정 API 호출:", menuId);
|
||
response = await menuApi.updateMenu(menuId, submitData);
|
||
} else {
|
||
// 추가 모드: saveMenu API 호출
|
||
console.log("➕ 메뉴 추가 API 호출");
|
||
response = await menuApi.saveMenu(submitData);
|
||
}
|
||
|
||
if (response.success) {
|
||
toast.success(response.message);
|
||
onSuccess();
|
||
onClose();
|
||
} else {
|
||
toast.error(response.message);
|
||
}
|
||
} catch (error) {
|
||
console.error("메뉴 저장/수정 실패:", error);
|
||
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleInputChange = (field: keyof MenuFormData, value: string | number) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
[field]: value,
|
||
}));
|
||
};
|
||
|
||
// 선택된 다국어 키 정보 가져오기
|
||
const getSelectedLangKeyInfo = () => {
|
||
if (!formData.langKey) return null;
|
||
return langKeys.find((key) => key.langKey === formData.langKey);
|
||
};
|
||
|
||
const selectedLangKeyInfo = getSelectedLangKeyInfo();
|
||
|
||
// 전역 사용자 로케일 가져오기
|
||
const getCurrentUserLang = () => {
|
||
return (window as any).__GLOBAL_USER_LANG || localStorage.getItem("userLocale") || "KR";
|
||
};
|
||
|
||
return (
|
||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||
<DialogContent className="sm:max-w-[600px]">
|
||
<DialogHeader>
|
||
<DialogTitle>
|
||
{isEdit
|
||
? getText(MENU_MANAGEMENT_KEYS.MODAL_MENU_MODIFY_TITLE)
|
||
: getText(MENU_MANAGEMENT_KEYS.MODAL_MENU_REGISTER_TITLE)}
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="menuType">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE)}</Label>
|
||
<Select value={formData.menuType} onValueChange={(value) => handleInputChange("menuType", value)}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="0">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_ADMIN)}</SelectItem>
|
||
<SelectItem value="1">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_USER)}</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="status">{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS)}</Label>
|
||
<Select value={formData.status} onValueChange={(value) => handleInputChange("status", value)}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="ACTIVE">{getText(MENU_MANAGEMENT_KEYS.STATUS_ACTIVE)}</SelectItem>
|
||
<SelectItem value="INACTIVE">{getText(MENU_MANAGEMENT_KEYS.STATUS_INACTIVE)}</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="companyCode">{getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY)} *</Label>
|
||
<Select
|
||
value={formData.companyCode}
|
||
onValueChange={(value) => handleInputChange("companyCode", value)}
|
||
disabled={!isEdit && level !== 1} // 수정 모드가 아니고 최상위 메뉴가 아니면 비활성화
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SELECT)} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">{getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY_COMMON)}</SelectItem>
|
||
{companies.map((company) => (
|
||
<SelectItem key={company.company_code} value={company.company_code}>
|
||
{company.company_name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{!isEdit && level !== 1 && (
|
||
<p className="text-xs text-gray-500">{getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SUBMENU_NOTE)}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="langKey">{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY)}</Label>
|
||
<div className="langkey-dropdown relative">
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsLangKeyDropdownOpen(!isLangKeyDropdownOpen)}
|
||
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"
|
||
disabled={!formData.companyCode}
|
||
>
|
||
<span className={!formData.langKey ? "text-muted-foreground" : ""}>
|
||
{formData.langKey || getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECT)}
|
||
</span>
|
||
<svg
|
||
className={`h-4 w-4 transition-transform ${isLangKeyDropdownOpen ? "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>
|
||
|
||
{isLangKeyDropdownOpen && (
|
||
<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={getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SEARCH)}
|
||
value={langKeySearchText}
|
||
onChange={(e) => setLangKeySearchText(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={() => {
|
||
handleInputChange("langKey", "");
|
||
setIsLangKeyDropdownOpen(false);
|
||
setLangKeySearchText("");
|
||
}}
|
||
>
|
||
{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_NONE)}
|
||
</div>
|
||
|
||
{langKeys
|
||
.filter(
|
||
(key) =>
|
||
key.langKey.toLowerCase().includes(langKeySearchText.toLowerCase()) ||
|
||
key.description.toLowerCase().includes(langKeySearchText.toLowerCase()),
|
||
)
|
||
.map((key) => (
|
||
<div
|
||
key={key.keyId}
|
||
className="hover:bg-accent hover:text-accent-foreground flex cursor-pointer flex-col px-2 py-1.5 text-sm"
|
||
onClick={() => {
|
||
handleInputChange("langKey", key.langKey);
|
||
setIsLangKeyDropdownOpen(false);
|
||
setLangKeySearchText("");
|
||
}}
|
||
>
|
||
<div className="font-medium">{key.langKey}</div>
|
||
{key.description && <div className="text-xs text-gray-500">{key.description}</div>}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{selectedLangKeyInfo && (
|
||
<p className="text-xs text-gray-500">
|
||
{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECTED)
|
||
.replace("{key}", selectedLangKeyInfo.langKey)
|
||
.replace("{description}", selectedLangKeyInfo.description)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="menuNameKor">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME)} *</Label>
|
||
<Input
|
||
id="menuNameKor"
|
||
value={formData.menuNameKor}
|
||
onChange={(e) => handleInputChange("menuNameKor", e.target.value)}
|
||
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME_PLACEHOLDER)}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="menuUrl">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL)}</Label>
|
||
|
||
{/* URL 타입 선택 */}
|
||
<RadioGroup value={urlType} onValueChange={handleUrlTypeChange} className="mb-3 flex space-x-6">
|
||
<div className="flex items-center space-x-2">
|
||
<RadioGroupItem value="screen" id="screen" />
|
||
<Label htmlFor="screen" className="cursor-pointer">
|
||
화면 할당
|
||
</Label>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<RadioGroupItem value="direct" id="direct" />
|
||
<Label htmlFor="direct" className="cursor-pointer">
|
||
URL 직접 입력
|
||
</Label>
|
||
</div>
|
||
</RadioGroup>
|
||
|
||
{/* 화면 할당 */}
|
||
{urlType === "screen" && (
|
||
<div className="space-y-2">
|
||
{/* 화면 선택 드롭다운 */}
|
||
<div className="relative">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => setIsScreenDropdownOpen(!isScreenDropdownOpen)}
|
||
className="w-full justify-between"
|
||
>
|
||
<span className="text-left">
|
||
{selectedScreen ? selectedScreen.screenName : "화면을 선택하세요"}
|
||
</span>
|
||
<ChevronDown className="h-4 w-4" />
|
||
</Button>
|
||
|
||
{isScreenDropdownOpen && (
|
||
<div className="screen-dropdown absolute top-full right-0 left-0 z-50 mt-1 max-h-60 overflow-y-auto rounded-md border bg-white shadow-lg">
|
||
{/* 검색 입력 */}
|
||
<div className="sticky top-0 border-b bg-white p-2">
|
||
<div className="relative">
|
||
<Search className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||
<Input
|
||
placeholder="화면 검색..."
|
||
value={screenSearchText}
|
||
onChange={(e) => setScreenSearchText(e.target.value)}
|
||
className="pl-8"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 화면 목록 */}
|
||
<div className="max-h-48 overflow-y-auto">
|
||
{screens
|
||
.filter(
|
||
(screen) =>
|
||
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
|
||
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
|
||
)
|
||
.map((screen, index) => (
|
||
<div
|
||
key={`screen-${screen.screenId || screen.id || index}-${screen.screenCode || index}`}
|
||
onClick={() => handleScreenSelect(screen)}
|
||
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-gray-100"
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="text-sm font-medium">{screen.screenName}</div>
|
||
<div className="text-xs text-gray-500">{screen.screenCode}</div>
|
||
</div>
|
||
<div className="text-xs text-gray-400">ID: {screen.screenId || screen.id || "N/A"}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
{screens.filter(
|
||
(screen) =>
|
||
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
|
||
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
|
||
).length === 0 && <div className="px-3 py-2 text-sm text-gray-500">검색 결과가 없습니다.</div>}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 선택된 화면 정보 표시 */}
|
||
{selectedScreen && (
|
||
<div className="rounded-md border bg-accent p-3">
|
||
<div className="text-sm font-medium text-blue-900">{selectedScreen.screenName}</div>
|
||
<div className="text-xs text-primary">코드: {selectedScreen.screenCode}</div>
|
||
<div className="text-xs text-primary">생성된 URL: {formData.menuUrl}</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* URL 직접 입력 */}
|
||
{urlType === "direct" && (
|
||
<Input
|
||
id="menuUrl"
|
||
value={formData.menuUrl}
|
||
onChange={(e) => handleInputChange("menuUrl", e.target.value)}
|
||
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER)}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="menuDesc">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION)}</Label>
|
||
<Textarea
|
||
id="menuDesc"
|
||
value={formData.menuDesc}
|
||
onChange={(e) => handleInputChange("menuDesc", e.target.value)}
|
||
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION_PLACEHOLDER)}
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="seq">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_SEQUENCE)}</Label>
|
||
<Input
|
||
id="seq"
|
||
type="number"
|
||
value={formData.seq}
|
||
onChange={(e) => handleInputChange("seq", parseInt(e.target.value) || 1)}
|
||
min="1"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-2 pt-4">
|
||
<Button type="button" variant="outline" onClick={onClose}>
|
||
{getText(MENU_MANAGEMENT_KEYS.BUTTON_CANCEL)}
|
||
</Button>
|
||
<Button type="submit" disabled={loading}>
|
||
{loading
|
||
? getText(MENU_MANAGEMENT_KEYS.BUTTON_SAVE_PROCESSING)
|
||
: isEdit
|
||
? getText(MENU_MANAGEMENT_KEYS.BUTTON_MODIFY)
|
||
: getText(MENU_MANAGEMENT_KEYS.BUTTON_REGISTER)}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|