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

566 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect } from "react";
import { MenuItem, MenuFormData, menuApi, LangKey } from "@/lib/api/menu";
import { companyAPI } from "@/lib/api/company";
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 { toast } from "sonner";
import { MENU_MANAGEMENT_KEYS } from "@/lib/utils/multilang";
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 [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("");
// 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";
}
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: 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, // 다국어 키 설정
});
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(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
if (!target.closest(".langkey-dropdown")) {
setIsLangKeyDropdownOpen(false);
setLangKeySearchText("");
}
};
if (isLangKeyDropdownOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isLangKeyDropdownOpen]);
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>
<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>
);
};