Merge branch 'dev' of http://39.117.244.52:3000/kjs/ERP-node into userMng

This commit is contained in:
dohyeons
2025-08-26 09:58:13 +09:00
11 changed files with 1610 additions and 406 deletions

View File

@@ -10,7 +10,7 @@ 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 { getMenuTextSync, MENU_MANAGEMENT_KEYS, setTranslationCache } from "@/lib/utils/multilang";
import { MENU_MANAGEMENT_KEYS } from "@/lib/utils/multilang";
interface Company {
company_code: string;
@@ -27,6 +27,8 @@ interface MenuFormModalProps {
menuType?: string;
level?: number;
parentCompanyCode?: string;
// 다국어 텍스트 props 추가
uiTexts: Record<string, string>;
}
export const MenuFormModal: React.FC<MenuFormModalProps> = ({
@@ -38,6 +40,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
menuType,
level,
parentCompanyCode,
uiTexts,
}) => {
console.log("🎯 MenuFormModal 렌더링 - Props:", {
isOpen,
@@ -48,6 +51,11 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
parentCompanyCode,
});
// 다국어 텍스트 가져오기 함수
const getText = (key: string, fallback?: string): string => {
return uiTexts[key] || fallback || key;
};
console.log("🔍 MenuFormModal 컴포넌트 마운트됨");
const [formData, setFormData] = useState<MenuFormData>({
@@ -149,7 +157,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
stack: error?.stack,
response: error?.response,
});
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO));
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO));
} finally {
setLoading(false);
}
@@ -254,7 +262,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
setCompanies(companyList);
} catch (error) {
console.error("회사 목록 로딩 오류:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST));
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST));
}
};
@@ -273,7 +281,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
}
} catch (error) {
console.error("❌ 다국어 키 목록 로딩 오류:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST));
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST));
setLangKeys([]);
}
};
@@ -282,12 +290,12 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
e.preventDefault();
if (!formData.menuNameKor.trim()) {
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED));
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED));
return;
}
if (!formData.companyCode) {
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED));
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED));
return;
}
@@ -324,7 +332,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
}
} catch (error) {
console.error("메뉴 저장/수정 실패:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED));
toast.error(getText(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED));
} finally {
setLoading(false);
}
@@ -345,58 +353,63 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
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
? getMenuTextSync(MENU_MANAGEMENT_KEYS.MODAL_MENU_MODIFY_TITLE)
: getMenuTextSync(MENU_MANAGEMENT_KEYS.MODAL_MENU_REGISTER_TITLE)}
? 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">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE)}</Label>
<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">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_ADMIN)}</SelectItem>
<SelectItem value="1">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_USER)}</SelectItem>
<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">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_STATUS)}</Label>
<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">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_STATUS_ACTIVE)}</SelectItem>
<SelectItem value="INACTIVE">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_STATUS_INACTIVE)}</SelectItem>
<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">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY)} *</Label>
<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={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SELECT)} />
<SelectValue placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SELECT)} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY_COMMON)}</SelectItem>
<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}
@@ -405,12 +418,12 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
</SelectContent>
</Select>
{!isEdit && level !== 1 && (
<p className="text-xs text-gray-500">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SUBMENU_NOTE)}</p>
<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">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY)}</Label>
<Label htmlFor="langKey">{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY)}</Label>
<div className="langkey-dropdown relative">
<button
type="button"
@@ -419,7 +432,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
disabled={!formData.companyCode}
>
<span className={!formData.langKey ? "text-muted-foreground" : ""}>
{formData.langKey || getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECT)}
{formData.langKey || getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECT)}
</span>
<svg
className={`h-4 w-4 transition-transform ${isLangKeyDropdownOpen ? "rotate-180" : ""}`}
@@ -436,7 +449,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
{/* 검색 입력 */}
<div className="border-b p-2">
<Input
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SEARCH)}
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SEARCH)}
value={langKeySearchText}
onChange={(e) => setLangKeySearchText(e.target.value)}
className="h-8 text-sm"
@@ -454,7 +467,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
setLangKeySearchText("");
}}
>
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_NONE)}
{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_NONE)}
</div>
{langKeys
@@ -483,48 +496,47 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
</div>
{selectedLangKeyInfo && (
<p className="text-xs text-gray-500">
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECTED, {
key: selectedLangKeyInfo.langKey,
description: selectedLangKeyInfo.description,
})}
{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">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME)} *</Label>
<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={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME_PLACEHOLDER)}
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME_PLACEHOLDER)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="menuUrl">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_URL)}</Label>
<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={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER)}
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="menuDesc">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION)}</Label>
<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={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION_PLACEHOLDER)}
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION_PLACEHOLDER)}
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="seq">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_SEQUENCE)}</Label>
<Label htmlFor="seq">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_SEQUENCE)}</Label>
<Input
id="seq"
type="number"
@@ -536,14 +548,14 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={onClose}>
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_CANCEL)}
{getText(MENU_MANAGEMENT_KEYS.BUTTON_CANCEL)}
</Button>
<Button type="submit" disabled={loading}>
{loading
? getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_SAVE_PROCESSING)
? getText(MENU_MANAGEMENT_KEYS.BUTTON_SAVE_PROCESSING)
: isEdit
? getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_MODIFY)
: getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_REGISTER)}
? getText(MENU_MANAGEMENT_KEYS.BUTTON_MODIFY)
: getText(MENU_MANAGEMENT_KEYS.BUTTON_REGISTER)}
</Button>
</div>
</form>

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useMemo } from "react";
import { menuApi } from "@/lib/api/menu";
import type { MenuItem } from "@/lib/api/menu";
import { MenuTable } from "./MenuTable";
@@ -24,12 +24,7 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useMenu } from "@/contexts/MenuContext";
import {
getMenuTextSync,
MENU_MANAGEMENT_KEYS,
useMenuManagementText,
setTranslationCache,
} from "@/lib/utils/multilang";
import { useMenuManagementText, setTranslationCache } from "@/lib/utils/multilang";
import { useMultiLang } from "@/hooks/useMultiLang";
import { apiClient } from "@/lib/api/client";
@@ -46,7 +41,7 @@ export const MenuManagement: React.FC = () => {
const [selectedMenus, setSelectedMenus] = useState<Set<string>>(new Set());
// 다국어 텍스트 훅 사용
const { getMenuText } = useMenuManagementText();
// getMenuText는 더 이상 사용하지 않음 - getUITextSync만 사용
const { userLang } = useMultiLang({ companyCode: "*" });
// 다국어 텍스트 상태
@@ -68,29 +63,282 @@ export const MenuManagement: React.FC = () => {
parentCompanyCode: "",
});
// 언어별 텍스트 매핑 테이블 제거 - 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",
];
// 초기 로딩
useEffect(() => {
loadCompanies();
}, []); // 빈 의존성 배열로 한 번만 실행
// 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정
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);
console.log("🌐 초기 기본 텍스트 설정 완료:", Object.keys(defaultTexts).length);
};
// 기본 텍스트 반환 함수
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;
};
// 컴포넌트 마운트 시 및 userLang 변경 시 다국어 텍스트 로드
useEffect(() => {
if (!uiTextsLoading) {
if (userLang && !uiTextsLoading) {
loadUITexts();
}
}, [userLang]); // userLang 변경 시마다 실행
// 컴포넌트 마운트 시 강제로 번역 로드 (userLang이 아직 설정되지 않았을 수 있음)
// uiTexts 상태 변경 감지
useEffect(() => {
console.log("🔄 uiTexts 상태 변경됨:", {
count: Object.keys(uiTexts).length,
sampleKeys: Object.keys(uiTexts).slice(0, 5),
sampleValues: Object.entries(uiTexts)
.slice(0, 3)
.map(([k, v]) => `${k}: ${v}`),
});
}, [uiTexts]);
// 컴포넌트 마운트 후 다국어 텍스트 강제 로드 (userLang이 아직 설정되지 않았을 수 있음)
useEffect(() => {
const timer = setTimeout(() => {
if (!uiTextsLoading && Object.keys(uiTexts).length === 0) {
console.log("🔄 컴포넌트 마운트 후 강제 번역 로드");
if (userLang && !uiTextsLoading) {
console.log("🔄 컴포넌트 마운트 후 다국어 텍스트 강제 로드");
loadUITexts();
}
}, 100); // 100ms 후 실행
}, 300); // 300ms 후 실행
return () => clearTimeout(timer);
}, []); // 컴포넌트 마운트 시 한 번만 실행
}, [userLang]); // userLang이 설정된 후 실행
// 추가 안전장치: 컴포넌트 마운트 후 일정 시간이 지나면 강제로 다국어 텍스트 로드
useEffect(() => {
const fallbackTimer = setTimeout(() => {
if (!uiTextsLoading && Object.keys(uiTexts).length === 0) {
console.log("🔄 안전장치: 컴포넌트 마운트 후 강제 다국어 텍스트 로드");
// 사용자 언어가 설정되지 않았을 때만 기본 텍스트 설정
if (!userLang) {
initializeDefaultTexts();
} else {
// 사용자 언어가 설정된 경우 다국어 텍스트 로드
loadUITexts();
}
}
}, 1000); // 1초 후 실행
return () => clearTimeout(fallbackTimer);
}, [userLang]); // userLang 변경 시마다 실행
// 번역 로드 이벤트 감지
useEffect(() => {
@@ -134,10 +382,10 @@ export const MenuManagement: React.FC = () => {
setLoading(true);
}
await refreshMenus();
console.log(`📋 메뉴 목록 조회 성공`);
console.log("📋 메뉴 목록 조회 성공");
} catch (error) {
console.error("❌ 메뉴 목록 조회 실패:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_LIST));
toast.error(getUITextSync("message.error.load.menu.list"));
} finally {
if (showLoading) {
setLoading(false);
@@ -147,7 +395,7 @@ export const MenuManagement: React.FC = () => {
// 회사 목록 조회
const loadCompanies = async () => {
console.log(`🏢 회사 목록 조회 시작`);
console.log("🏢 회사 목록 조회 시작");
try {
const response = await apiClient.get("/admin/companies");
@@ -165,228 +413,94 @@ export const MenuManagement: React.FC = () => {
}
};
// 다국어 텍스트 로드 함수
// 다국어 텍스트 로드 함수 - 배치 API 사용
const loadUITexts = async () => {
if (uiTextsLoading) return; // 이미 로딩 중이면 중단
// userLang이 으면 기본값 사용
const currentUserLang = userLang || "KR";
console.log("🌐 UI 다국어 텍스트 로드 시작", { currentUserLang });
// userLang이 설정되지 않았으면 기본값 설정
if (!userLang) {
console.log("🌐 사용자 언어가 설정되지 않음, 기본값 설정");
const defaultTexts: Record<string, string> = {};
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
defaultTexts[key] = getDefaultText(key); // 기본 한국어 텍스트 사용
});
setUiTexts(defaultTexts);
return;
}
// 사용자 언어가 설정된 경우, 기존 uiTexts가 비어있으면 기본 텍스트로 초기화
if (Object.keys(uiTexts).length === 0) {
console.log("🌐 기존 uiTexts가 비어있음, 기본 텍스트로 초기화");
const defaultTexts: Record<string, string> = {};
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
defaultTexts[key] = getDefaultText(key);
});
setUiTexts(defaultTexts);
}
console.log("🌐 UI 다국어 텍스트 로드 시작", {
userLang,
apiParams: {
companyCode: "*",
menuCode: "menu.management",
userLang: userLang,
},
});
setUiTextsLoading(true);
const texts: Record<string, string> = {};
try {
const textPromises = [
getMenuText(MENU_MANAGEMENT_KEYS.TITLE),
getMenuText(MENU_MANAGEMENT_KEYS.DESCRIPTION),
getMenuText(MENU_MANAGEMENT_KEYS.MENU_TYPE_TITLE),
getMenuText(MENU_MANAGEMENT_KEYS.MENU_TYPE_ADMIN),
getMenuText(MENU_MANAGEMENT_KEYS.MENU_TYPE_USER),
getMenuText(MENU_MANAGEMENT_KEYS.ADMIN_MENU),
getMenuText(MENU_MANAGEMENT_KEYS.USER_MENU),
getMenuText(MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION),
getMenuText(MENU_MANAGEMENT_KEYS.USER_DESCRIPTION),
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_ADD),
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL),
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB),
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_EDIT),
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_DELETE),
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED),
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED_COUNT),
getMenuText(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_PROCESSING),
getMenuText(MENU_MANAGEMENT_KEYS.FILTER_RESET),
getMenuText(MENU_MANAGEMENT_KEYS.LIST_TOTAL),
getMenuText(MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT),
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME),
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL),
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_TYPE),
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS),
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY),
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE),
getMenuText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_ACTIONS),
// 추가 키들 - 실제 UI에서 사용되는 모든 키들
getMenuText("menu.list.title"),
getMenuText("filter.company"),
getMenuText("filter.company.all"),
getMenuText("filter.search"),
getMenuText("filter.search.placeholder"),
getMenuText("status.unspecified"),
getMenuText("status.active"),
getMenuText("filter.company.common"),
getMenuText("modal.menu.register.title"),
getMenuText("form.menu.type"),
getMenuText("form.menu.type.admin"),
getMenuText("form.menu.type.user"),
getMenuText("form.status"),
getMenuText("form.status.active"),
getMenuText("form.status.inactive"),
getMenuText("form.company"),
getMenuText("form.company.select"),
getMenuText("form.company.common"),
getMenuText("form.company.submenu.note"),
getMenuText("form.lang.key"),
getMenuText("form.lang.key.select"),
getMenuText("form.menu.name"),
getMenuText("form.menu.name.placeholder"),
getMenuText("form.menu.url"),
getMenuText("form.menu.url.placeholder"),
getMenuText("form.menu.description"),
getMenuText("form.menu.description.placeholder"),
getMenuText("form.menu.sequence"),
getMenuText("button.cancel"),
getMenuText("button.register"),
// 테이블 헤더 관련 추가 키들
getMenuText("table.header.menu.name"),
getMenuText("table.header.sequence"),
getMenuText("table.header.company"),
getMenuText("table.header.menu.url"),
getMenuText("table.header.status"),
getMenuText("table.header.actions"),
// 액션 버튼 관련 추가 키들
getMenuText("button.add"),
getMenuText("button.add.sub"),
getMenuText("button.edit"),
getMenuText("button.delete"),
// 페이지 제목 관련
getMenuText("page.title.menu.management"),
getMenuText("page.description.menu.management"),
getMenuText("section.title.menu.type"),
getMenuText("section.title.admin.menu.list"),
];
// 배치 API를 사용하여 모든 다국어 키를 한 번에 조회
const response = await apiClient.post(
"/multilang/batch",
{
langKeys: MENU_MANAGEMENT_LANG_KEYS,
companyCode: "*", // 모든 회사
menuCode: "menu.management", // 메뉴관리 메뉴
userLang: userLang, // body에 포함
},
{
params: {}, // query params는 비움
},
);
const results = await Promise.all(textPromises);
if (response.data.success) {
const translations = response.data.data;
console.log("🌐 배치 다국어 텍스트 응답:", translations);
// 결과를 키와 매핑
const keys = [
MENU_MANAGEMENT_KEYS.TITLE,
MENU_MANAGEMENT_KEYS.DESCRIPTION,
MENU_MANAGEMENT_KEYS.MENU_TYPE_TITLE,
MENU_MANAGEMENT_KEYS.MENU_TYPE_ADMIN,
MENU_MANAGEMENT_KEYS.MENU_TYPE_USER,
MENU_MANAGEMENT_KEYS.ADMIN_MENU,
MENU_MANAGEMENT_KEYS.USER_MENU,
MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION,
MENU_MANAGEMENT_KEYS.USER_DESCRIPTION,
MENU_MANAGEMENT_KEYS.BUTTON_ADD,
MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL,
MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB,
MENU_MANAGEMENT_KEYS.BUTTON_EDIT,
MENU_MANAGEMENT_KEYS.BUTTON_DELETE,
MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED,
MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED_COUNT,
MENU_MANAGEMENT_KEYS.BUTTON_DELETE_PROCESSING,
MENU_MANAGEMENT_KEYS.FILTER_RESET,
MENU_MANAGEMENT_KEYS.LIST_TOTAL,
MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT,
MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME,
MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL,
MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_TYPE,
MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS,
MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY,
MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE,
MENU_MANAGEMENT_KEYS.TABLE_HEADER_ACTIONS,
// 추가 키들 - 실제 UI에서 사용되는 모든 키들
"menu.list.title",
"filter.company",
"filter.company.all",
"filter.search",
"filter.search.placeholder",
"status.unspecified",
"status.active",
"filter.company.common",
"modal.menu.register.title",
"form.menu.type",
"form.menu.type.admin",
"form.menu.type.user",
"form.status",
"form.status.active",
"form.status.inactive",
"form.company",
"form.company.select",
"form.company.common",
"form.company.submenu.note",
"form.lang.key",
"form.lang.key.select",
"form.menu.name",
"form.menu.name.placeholder",
"form.menu.url",
"form.menu.url.placeholder",
"form.menu.description",
"form.menu.description.placeholder",
"form.menu.sequence",
"button.cancel",
"button.register",
// 테이블 헤더 관련 추가 키들
"table.header.menu.name",
"table.header.sequence",
"table.header.company",
"table.header.menu.url",
"table.header.status",
"table.header.actions",
// 액션 버튼 관련 추가 키들
"button.add",
"button.add.sub",
"button.edit",
"button.delete",
// 페이지 제목 관련
"page.title.menu.management",
"page.description.menu.management",
"section.title.menu.type",
"section.title.admin.menu.list",
];
// 번역 결과를 상태에 저장 (기존 uiTexts와 병합)
const mergedTranslations = { ...uiTexts, ...translations };
console.log("🔧 setUiTexts 호출 전:", {
translationsCount: Object.keys(translations).length,
mergedCount: Object.keys(mergedTranslations).length,
});
setUiTexts(mergedTranslations);
console.log("🔧 setUiTexts 호출 후 - mergedTranslations:", mergedTranslations);
keys.forEach((key, index) => {
texts[key] = results[index];
});
setUiTexts(texts);
// 번역 텍스트를 캐시에 저장
setTranslationCache(currentUserLang, texts);
console.log("🌐 UI 다국어 텍스트 로드 완료:", texts);
// 번역 캐시에 저장 (다른 컴포넌트에서도 사용할 수 있도록)
setTranslationCache(userLang, mergedTranslations);
} else {
console.error("❌ 다국어 텍스트 배치 조회 실패:", response.data.message);
// API 실패 시에도 기존 uiTexts는 유지
console.log("🔄 API 실패로 인해 기존 uiTexts 유지");
}
} catch (error) {
console.error("❌ UI 다국어 텍스트 로드 실패:", error);
// API 실패 시에도 기존 uiTexts는 유지
console.log("🔄 API 실패로 인해 기존 uiTexts 유지");
} finally {
setUiTextsLoading(false);
}
};
// UI 텍스트 가져오기 함수
const getUIText = async (
key: string,
params?: Record<string, string | number>,
fallback?: string,
): Promise<string> => {
// uiTexts에서 먼저 찾기
let text = uiTexts[key];
// UI 텍스트 가져오기 함수 (동기 버전만 사용)
// getUIText 함수는 제거 - getUITextSync만 사용
// uiTexts에 없으면 비동기적으로 API 호출
if (!text) {
try {
text = await getMenuText(key);
// 새로운 텍스트를 uiTexts에 추가
setUiTexts((prev) => ({ ...prev, [key]: text }));
} catch (error) {
console.error(`❌ 키 "${key}" 번역 실패:`, error);
text = fallback || key;
}
}
// 파라미터 치환
if (params && text) {
Object.entries(params).forEach(([paramKey, paramValue]) => {
text = text!.replace(`{${paramKey}}`, String(paramValue));
});
}
return text || key;
};
// 동기 버전 (기존 호환성을 위해)
// 동기 버전 (DB에서 가져온 번역 텍스트 사용)
const getUITextSync = (key: string, params?: Record<string, string | number>, fallback?: string): string => {
// uiTexts에서 번역 텍스트 찾기
let text = uiTexts[key];
// uiTexts에 없으면 fallback 또는 키 사용
if (!text) {
text = fallback || key;
}
@@ -401,11 +515,11 @@ export const MenuManagement: React.FC = () => {
return text || key;
};
// 다국어 API 테스트 함수
// 다국어 API 테스트 함수 (getUITextSync 사용)
const testMultiLangAPI = async () => {
console.log("🧪 다국어 API 테스트 시작");
try {
const text = await getMenuText(MENU_MANAGEMENT_KEYS.ADMIN_MENU);
const text = getUITextSync("menu.management.admin");
console.log("🧪 다국어 API 테스트 결과:", text);
} catch (error) {
console.error("❌ 다국어 API 테스트 실패:", error);
@@ -513,11 +627,11 @@ export const MenuManagement: React.FC = () => {
const handleDeleteSelectedMenus = async () => {
if (selectedMenus.size === 0) {
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_SELECT_MENU_DELETE));
toast.error(getUITextSync("message.validation.select.menu.delete"));
return;
}
if (!confirm(getMenuTextSync(MENU_MANAGEMENT_KEYS.MODAL_DELETE_BATCH_DESCRIPTION, { count: selectedMenus.size }))) {
if (!confirm(getUITextSync("modal.delete.batch.description", { count: selectedMenus.size }))) {
return;
}
@@ -526,7 +640,7 @@ export const MenuManagement: React.FC = () => {
const menuIds = Array.from(selectedMenus);
console.log("삭제할 메뉴 IDs:", menuIds);
toast.info(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_PROCESSING));
toast.info(getUITextSync("message.menu.delete.processing"));
const response = await menuApi.deleteMenusBatch(menuIds);
console.log("삭제 API 응답:", response);
@@ -552,12 +666,10 @@ export const MenuManagement: React.FC = () => {
// 삭제 결과 메시지
if (failedCount === 0) {
toast.success(
getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_SUCCESS, { count: deletedCount }),
);
toast.success(getUITextSync("message.menu.delete.batch.success", { count: deletedCount }));
} else {
toast.success(
getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_PARTIAL, {
getUITextSync("message.menu.delete.batch.partial", {
success: deletedCount,
failed: failedCount,
}),
@@ -569,7 +681,7 @@ export const MenuManagement: React.FC = () => {
}
} catch (error) {
console.error("메뉴 삭제 중 오류:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_FAILED));
toast.error(getUITextSync("message.menu.delete.failed"));
} finally {
setDeleting(false);
}
@@ -605,7 +717,7 @@ export const MenuManagement: React.FC = () => {
}
} catch (error) {
console.error("메뉴 상태 토글 오류:", error);
toast.error(getMenuTextSync(MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_FAILED));
toast.error(getUITextSync("message.menu.status.toggle.failed"));
}
};
@@ -658,15 +770,29 @@ export const MenuManagement: React.FC = () => {
};
const getMenuTypeString = () => {
return selectedMenuType === "admin"
? getMenuTextSync(MENU_MANAGEMENT_KEYS.MENU_TYPE_ADMIN)
: getMenuTextSync(MENU_MANAGEMENT_KEYS.MENU_TYPE_USER);
return selectedMenuType === "admin" ? getUITextSync("menu.type.admin") : getUITextSync("menu.type.user");
};
const getMenuTypeValue = () => {
return selectedMenuType === "admin" ? "0" : "1";
};
// uiTextsCount를 useMemo로 계산하여 상태 변경 시에만 재계산
const uiTextsCount = useMemo(() => Object.keys(uiTexts).length, [uiTexts]);
const adminMenusCount = useMemo(() => adminMenus?.length || 0, [adminMenus]);
const userMenusCount = useMemo(() => userMenus?.length || 0, [userMenus]);
// 디버깅을 위한 간단한 상태 표시
console.log("🔍 MenuManagement 렌더링 상태:", {
loading,
uiTextsLoading,
uiTextsCount,
adminMenusCount,
userMenusCount,
selectedMenuType,
userLang,
});
if (loading) {
return (
<div className="flex h-64 items-center justify-center">
@@ -676,14 +802,14 @@ export const MenuManagement: React.FC = () => {
}
return (
<LoadingOverlay isLoading={deleting} text="메뉴 삭제 중...">
<LoadingOverlay isLoading={deleting} text={getUITextSync("button.delete.processing")}>
<div className="flex h-full flex-col">
{/* 메인 컨텐츠 - 2:8 비율 */}
<div className="flex flex-1 overflow-hidden">
{/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */}
<div className="w-[20%] border-r bg-gray-50">
<div className="p-6">
<h2 className="mb-4 text-lg font-semibold">{getMenuTextSync(MENU_MANAGEMENT_KEYS.MENU_TYPE_TITLE)}</h2>
<h2 className="mb-4 text-lg font-semibold">{getUITextSync("menu.type.title")}</h2>
<div className="space-y-3">
<Card
className={`cursor-pointer transition-all ${
@@ -694,9 +820,9 @@ export const MenuManagement: React.FC = () => {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium">{getUITextSync(MENU_MANAGEMENT_KEYS.ADMIN_MENU)}</h3>
<h3 className="font-medium">{getUITextSync("menu.management.admin")}</h3>
<p className="mt-1 text-sm text-gray-600">
{getUITextSync(MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION)}
{getUITextSync("menu.management.admin.description")}
</p>
</div>
<Badge variant={selectedMenuType === "admin" ? "default" : "secondary"}>
@@ -715,9 +841,9 @@ export const MenuManagement: React.FC = () => {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium">{getUITextSync(MENU_MANAGEMENT_KEYS.USER_MENU)}</h3>
<h3 className="font-medium">{getUITextSync("menu.management.user")}</h3>
<p className="mt-1 text-sm text-gray-600">
{getUITextSync(MENU_MANAGEMENT_KEYS.USER_DESCRIPTION)}
{getUITextSync("menu.management.user.description")}
</p>
</div>
<Badge variant={selectedMenuType === "user" ? "default" : "secondary"}>{userMenus.length}</Badge>
@@ -733,7 +859,7 @@ export const MenuManagement: React.FC = () => {
<div className="flex h-full flex-col p-6">
<div className="mb-6 flex-shrink-0">
<h2 className="mb-2 text-xl font-semibold">
{getMenuTypeString()} {getMenuTextSync(MENU_MANAGEMENT_KEYS.LIST_TITLE)}
{getMenuTypeString()} {getUITextSync("menu.list.title")}
</h2>
</div>
@@ -741,7 +867,7 @@ export const MenuManagement: React.FC = () => {
<div className="mb-4 flex-shrink-0">
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<div>
<Label htmlFor="company">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY)}</Label>
<Label htmlFor="company">{getUITextSync("filter.company")}</Label>
<div className="company-dropdown relative">
<button
type="button"
@@ -750,11 +876,11 @@ export const MenuManagement: React.FC = () => {
>
<span className={selectedCompany === "all" ? "text-muted-foreground" : ""}>
{selectedCompany === "all"
? getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL)
? getUITextSync("filter.company.all")
: selectedCompany === "*"
? getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON)
? getUITextSync("filter.company.common")
: companies.find((c) => c.code === selectedCompany)?.name ||
getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL)}
getUITextSync("filter.company.all")}
</span>
<svg
className={`h-4 w-4 transition-transform ${isCompanyDropdownOpen ? "rotate-180" : ""}`}
@@ -771,7 +897,7 @@ export const MenuManagement: React.FC = () => {
{/* 검색 입력 */}
<div className="border-b p-2">
<Input
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_SEARCH)}
placeholder={getUITextSync("filter.company.search")}
value={companySearchText}
onChange={(e) => setCompanySearchText(e.target.value)}
className="h-8 text-sm"
@@ -789,7 +915,7 @@ export const MenuManagement: React.FC = () => {
setCompanySearchText("");
}}
>
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL)}
{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"
@@ -799,7 +925,7 @@ export const MenuManagement: React.FC = () => {
setCompanySearchText("");
}}
>
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON)}
{getUITextSync("filter.company.common")}
</div>
{companies
@@ -819,7 +945,7 @@ export const MenuManagement: React.FC = () => {
setCompanySearchText("");
}}
>
{company.code === "*" ? "공통" : company.name}
{company.code === "*" ? getUITextSync("filter.company.common") : company.name}
</div>
))}
</div>
@@ -829,9 +955,9 @@ export const MenuManagement: React.FC = () => {
</div>
<div>
<Label htmlFor="search">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_SEARCH)}</Label>
<Label htmlFor="search">{getUITextSync("filter.search")}</Label>
<Input
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_SEARCH_PLACEHOLDER)}
placeholder={getUITextSync("filter.search.placeholder")}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
@@ -847,13 +973,13 @@ export const MenuManagement: React.FC = () => {
variant="outline"
className="w-full"
>
{getUITextSync(MENU_MANAGEMENT_KEYS.FILTER_RESET)}
{getUITextSync("filter.reset")}
</Button>
</div>
<div className="flex items-end">
<div className="text-sm text-gray-600">
{getUITextSync(MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT, { count: getCurrentMenus().length })}
{getUITextSync("menu.list.search.result", { count: getCurrentMenus().length })}
</div>
</div>
</div>
@@ -862,11 +988,11 @@ export const MenuManagement: React.FC = () => {
<div className="flex-1 overflow-hidden">
<div className="mb-4 flex items-center justify-between">
<div className="text-sm text-gray-600">
{getUITextSync(MENU_MANAGEMENT_KEYS.LIST_TOTAL, { count: getCurrentMenus().length })}
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
</div>
<div className="flex space-x-2">
<Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]">
{getUITextSync(MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL)}
{getUITextSync("button.add.top.level")}
</Button>
{selectedMenus.size > 0 && (
<Button
@@ -878,10 +1004,10 @@ export const MenuManagement: React.FC = () => {
{deleting ? (
<>
<LoadingSpinner size="sm" className="mr-2" />
{getUITextSync(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_PROCESSING)}
{getUITextSync("button.delete.processing")}
</>
) : (
getUITextSync(MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED_COUNT, {
getUITextSync("button.delete.selected.count", {
count: selectedMenus.size,
})
)}
@@ -900,6 +1026,7 @@ export const MenuManagement: React.FC = () => {
onSelectAllMenus={handleSelectAllMenus}
expandedMenus={expandedMenus}
onToggleExpand={handleToggleExpand}
uiTexts={uiTexts}
/>
</div>
</div>
@@ -915,6 +1042,7 @@ export const MenuManagement: React.FC = () => {
menuType={formData.menuType}
level={formData.level}
parentCompanyCode={formData.parentCompanyCode}
uiTexts={uiTexts}
/>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>

View File

@@ -6,8 +6,8 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { toast } from "sonner";
import { useMultiLang } from "@/hooks/useMultiLang";
import { getMenuTextSync, MENU_MANAGEMENT_KEYS, setTranslationCache } from "@/lib/utils/multilang";
import { MENU_MANAGEMENT_KEYS } from "@/lib/utils/multilang";
interface MenuTableProps {
menus: MenuItem[];
@@ -20,6 +20,8 @@ interface MenuTableProps {
onSelectAllMenus: (checked: boolean) => void;
expandedMenus: Set<string>;
onToggleExpand: (menuId: string) => void;
// 다국어 텍스트 props 추가
uiTexts: Record<string, string>;
}
export const MenuTable: React.FC<MenuTableProps> = ({
@@ -33,8 +35,12 @@ export const MenuTable: React.FC<MenuTableProps> = ({
onSelectAllMenus,
expandedMenus,
onToggleExpand,
uiTexts,
}) => {
const { userLang } = useMultiLang();
// 다국어 텍스트 가져오기 함수
const getText = (key: string, fallback?: string): string => {
return uiTexts[key] || fallback || key;
};
// 다국어 텍스트 표시 함수 (기본값 처리)
const getDisplayText = (menu: MenuItem) => {
@@ -130,8 +136,8 @@ export const MenuTable: React.FC<MenuTableProps> = ({
}`}
>
{status === "active"
? getMenuTextSync(MENU_MANAGEMENT_KEYS.STATUS_ACTIVE)
: getMenuTextSync(MENU_MANAGEMENT_KEYS.STATUS_INACTIVE)}
? getText(MENU_MANAGEMENT_KEYS.STATUS_ACTIVE)
: getText(MENU_MANAGEMENT_KEYS.STATUS_INACTIVE)}
</button>
);
};
@@ -156,22 +162,22 @@ export const MenuTable: React.FC<MenuTableProps> = ({
/>
</TableHead>
<TableHead className="w-1/3 bg-gray-50 font-semibold text-gray-700">
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME)}
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_NAME)}
</TableHead>
<TableHead className="w-16 bg-gray-50 font-semibold text-gray-700">
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE)}
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_SEQUENCE)}
</TableHead>
<TableHead className="w-24 bg-gray-50 font-semibold text-gray-700">
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY)}
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_COMPANY)}
</TableHead>
<TableHead className="w-48 bg-gray-50 font-semibold text-gray-700">
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL)}
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_MENU_URL)}
</TableHead>
<TableHead className="w-20 bg-gray-50 font-semibold text-gray-700">
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS)}
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_STATUS)}
</TableHead>
<TableHead className="w-32 bg-gray-50 font-semibold text-gray-700">
{getMenuTextSync(MENU_MANAGEMENT_KEYS.TABLE_HEADER_ACTIONS)}
{getText(MENU_MANAGEMENT_KEYS.TABLE_HEADER_ACTIONS)}
</TableHead>
</TableRow>
</TableHeader>
@@ -236,11 +242,11 @@ export const MenuTable: React.FC<MenuTableProps> = ({
<TableCell className="text-sm text-gray-600">
<div className="flex flex-col">
<span
className={`font-medium ${companyName && companyName !== getMenuTextSync(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED) ? "text-green-600" : "text-gray-500"}`}
className={`font-medium ${companyName && companyName !== getText(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED) ? "text-green-600" : "text-gray-500"}`}
>
{companyCode === "*"
? getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON)
: companyName || getMenuTextSync(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED)}
? getText(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON)
: companyName || getText(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED)}
</span>
{companyCode && companyCode !== "" && (
<span className="font-mono text-xs text-gray-400">{companyCode}</span>
@@ -285,7 +291,7 @@ export const MenuTable: React.FC<MenuTableProps> = ({
className="min-w-[40px] px-1 py-1 text-xs"
onClick={() => onAddMenu(objid, menuType, lev)}
>
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_ADD)}
{getText(MENU_MANAGEMENT_KEYS.BUTTON_ADD)}
</Button>
)}
{lev === 2 && (
@@ -296,7 +302,7 @@ export const MenuTable: React.FC<MenuTableProps> = ({
className="min-w-[40px] px-1 py-1 text-xs"
onClick={() => onAddMenu(objid, menuType, lev)}
>
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB)}
{getText(MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB)}
</Button>
<Button
size="sm"
@@ -304,7 +310,7 @@ export const MenuTable: React.FC<MenuTableProps> = ({
className="min-w-[40px] px-1 py-1 text-xs"
onClick={() => onEditMenu(objid)}
>
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_EDIT)}
{getText(MENU_MANAGEMENT_KEYS.BUTTON_EDIT)}
</Button>
</>
)}
@@ -315,7 +321,7 @@ export const MenuTable: React.FC<MenuTableProps> = ({
className="min-w-[40px] px-1 py-1 text-xs"
onClick={() => onEditMenu(objid)}
>
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_EDIT)}
{getText(MENU_MANAGEMENT_KEYS.BUTTON_EDIT)}
</Button>
)}
</div>