multilang #2
|
|
@ -763,12 +763,22 @@ export const getBatchTranslations = async (
|
|||
): Promise<void> => {
|
||||
try {
|
||||
const { companyCode, menuCode, userLang } = req.query;
|
||||
const { langKeys } = req.body;
|
||||
const {
|
||||
langKeys,
|
||||
companyCode: bodyCompanyCode,
|
||||
menuCode: bodyMenuCode,
|
||||
userLang: bodyUserLang,
|
||||
} = req.body;
|
||||
|
||||
// query params에서 읽지 못한 경우 body에서 읽기
|
||||
const finalCompanyCode = companyCode || bodyCompanyCode;
|
||||
const finalMenuCode = menuCode || bodyMenuCode;
|
||||
const finalUserLang = userLang || bodyUserLang;
|
||||
|
||||
logger.info("다국어 텍스트 배치 조회 요청", {
|
||||
companyCode,
|
||||
menuCode,
|
||||
userLang,
|
||||
companyCode: finalCompanyCode,
|
||||
menuCode: finalMenuCode,
|
||||
userLang: finalUserLang,
|
||||
keyCount: langKeys?.length || 0,
|
||||
user: req.user,
|
||||
});
|
||||
|
|
@ -785,7 +795,7 @@ export const getBatchTranslations = async (
|
|||
return;
|
||||
}
|
||||
|
||||
if (!companyCode || !userLang) {
|
||||
if (!finalCompanyCode || !finalUserLang) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "companyCode와 userLang은 필수입니다.",
|
||||
|
|
@ -809,9 +819,9 @@ export const getBatchTranslations = async (
|
|||
try {
|
||||
const multiLangService = new MultiLangService(client);
|
||||
const translations = await multiLangService.getBatchTranslations({
|
||||
companyCode: companyCode as string,
|
||||
menuCode: menuCode as string,
|
||||
userLang: userLang as string,
|
||||
companyCode: finalCompanyCode as string,
|
||||
menuCode: finalMenuCode as string,
|
||||
userLang: finalUserLang as string,
|
||||
langKeys,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -313,17 +313,13 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
|||
}
|
||||
}, [userLang]);
|
||||
|
||||
// 컴포넌트 마운트 시 강제로 번역 로드 (userLang이 아직 설정되지 않았을 수 있음)
|
||||
// 컴포넌트 마운트 시 userLang이 설정될 때까지 대기
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (!userLang) {
|
||||
console.log("🔄 Admin Layout 마운트 후 강제 번역 로드 (userLang 없음)");
|
||||
loadTranslations();
|
||||
}
|
||||
}, 100); // 100ms 후 실행
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []); // 컴포넌트 마운트 시 한 번만 실행
|
||||
if (userLang) {
|
||||
console.log("🔄 userLang 설정됨, 번역 로드 시작:", userLang);
|
||||
loadTranslations();
|
||||
}
|
||||
}, [userLang]); // userLang이 설정될 때마다 실행
|
||||
|
||||
// 키보드 단축키로 사이드바 토글
|
||||
useEffect(() => {
|
||||
|
|
@ -359,11 +355,14 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
|||
|
||||
const loadTranslations = async () => {
|
||||
try {
|
||||
// 현재 사용자 언어 사용
|
||||
const currentUserLang = userLang || "en";
|
||||
// userLang이 설정되지 않았으면 번역 로드하지 않음
|
||||
if (!userLang) {
|
||||
console.log("⏳ userLang이 설정되지 않음, 번역 로드 대기");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🌐 Admin Layout 번역 로드 시작", {
|
||||
userLang,
|
||||
currentUserLang,
|
||||
});
|
||||
|
||||
// API 직접 호출로 현재 언어 사용 (배치 조회 방식)
|
||||
|
|
@ -380,7 +379,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
|||
params: {
|
||||
companyCode,
|
||||
menuCode: "MENU_MANAGEMENT",
|
||||
userLang: currentUserLang,
|
||||
userLang: userLang,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -392,24 +391,45 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
|||
translations[MENU_MANAGEMENT_KEYS.DESCRIPTION] || "시스템의 메뉴 구조와 권한을 관리합니다.";
|
||||
|
||||
// 번역 캐시에 저장
|
||||
setTranslationCache(currentUserLang, translations);
|
||||
setTranslationCache(userLang, translations);
|
||||
|
||||
// 상태 업데이트
|
||||
setMenuTranslations({ title, description });
|
||||
|
||||
console.log("🌐 Admin Layout 번역 로드 완료 (배치)", { title, description, userLang: currentUserLang });
|
||||
console.log("🌐 Admin Layout 번역 로드 완료 (배치)", { title, description, userLang });
|
||||
} else {
|
||||
// 기본값 사용
|
||||
const title = "메뉴 관리";
|
||||
const description = "시스템의 메뉴 구조와 권한을 관리합니다.";
|
||||
// 전역 사용자 로케일 확인하여 기본값 설정
|
||||
const globalUserLang = (window as any).__GLOBAL_USER_LANG || localStorage.getItem("userLocale") || "KR";
|
||||
console.log("🌐 전역 사용자 로케일 확인:", globalUserLang);
|
||||
|
||||
// 사용자 로케일에 따른 기본값 설정
|
||||
let title, description;
|
||||
if (globalUserLang === "US") {
|
||||
title = "Menu Management";
|
||||
description = "Manage system menu structure and permissions";
|
||||
} else {
|
||||
title = "메뉴 관리";
|
||||
description = "시스템의 메뉴 구조와 권한을 관리합니다.";
|
||||
}
|
||||
|
||||
setMenuTranslations({ title, description });
|
||||
console.log("🌐 Admin Layout 기본값 사용", { title, description, userLang: currentUserLang });
|
||||
console.log("🌐 Admin Layout 기본값 사용", { title, description, userLang: globalUserLang });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Admin Layout 배치 번역 로드 실패:", error);
|
||||
// 오류 시 기본값 사용
|
||||
const title = "메뉴 관리";
|
||||
const description = "시스템의 메뉴 구조와 권한을 관리합니다.";
|
||||
// 오류 시에도 전역 사용자 로케일 확인하여 기본값 설정
|
||||
const globalUserLang = (window as any).__GLOBAL_USER_LANG || localStorage.getItem("userLocale") || "KR";
|
||||
console.log("🌐 오류 시 전역 사용자 로케일 확인:", globalUserLang);
|
||||
|
||||
let title, description;
|
||||
if (globalUserLang === "US") {
|
||||
title = "Menu Management";
|
||||
description = "Manage system menu structure and permissions";
|
||||
} else {
|
||||
title = "메뉴 관리";
|
||||
description = "시스템의 메뉴 구조와 권한을 관리합니다.";
|
||||
}
|
||||
|
||||
setMenuTranslations({ title, description });
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -510,11 +530,6 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
|||
<div className="text-center">
|
||||
<h1 className="mb-4 text-2xl font-bold text-red-600">인증 실패</h1>
|
||||
<p className="mb-4">토큰이 없습니다. 3초 후 로그인 페이지로 이동합니다.</p>
|
||||
<div className="rounded bg-yellow-100 p-4">
|
||||
<h2 className="mb-2 font-semibold">디버깅 정보</h2>
|
||||
<p>현재 경로: {pathname}</p>
|
||||
<p>토큰: {localStorage.getItem("authToken") ? "존재" : "없음"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -345,58 +345,79 @@ 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)}
|
||||
? getMenuTextSync(MENU_MANAGEMENT_KEYS.MODAL_MENU_MODIFY_TITLE, getCurrentUserLang())
|
||||
: getMenuTextSync(MENU_MANAGEMENT_KEYS.MODAL_MENU_REGISTER_TITLE, getCurrentUserLang())}
|
||||
</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">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE, getCurrentUserLang())}
|
||||
</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">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_ADMIN, getCurrentUserLang())}
|
||||
</SelectItem>
|
||||
<SelectItem value="1">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_TYPE_USER, getCurrentUserLang())}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_STATUS)}</Label>
|
||||
<Label htmlFor="status">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_STATUS, getCurrentUserLang())}</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">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_STATUS_ACTIVE, getCurrentUserLang())}
|
||||
</SelectItem>
|
||||
<SelectItem value="INACTIVE">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_STATUS_INACTIVE, getCurrentUserLang())}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="companyCode">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY)} *</Label>
|
||||
<Label htmlFor="companyCode">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY, getCurrentUserLang())} *
|
||||
</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={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SELECT, getCurrentUserLang())}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY_COMMON)}</SelectItem>
|
||||
<SelectItem value="none">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY_COMMON, getCurrentUserLang())}
|
||||
</SelectItem>
|
||||
{companies.map((company) => (
|
||||
<SelectItem key={company.company_code} value={company.company_code}>
|
||||
{company.company_name}
|
||||
|
|
@ -405,12 +426,14 @@ 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">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SUBMENU_NOTE, getCurrentUserLang())}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="langKey">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY)}</Label>
|
||||
<Label htmlFor="langKey">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY, getCurrentUserLang())}</Label>
|
||||
<div className="langkey-dropdown relative">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -419,7 +442,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 || getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECT, getCurrentUserLang())}
|
||||
</span>
|
||||
<svg
|
||||
className={`h-4 w-4 transition-transform ${isLangKeyDropdownOpen ? "rotate-180" : ""}`}
|
||||
|
|
@ -436,7 +459,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
{/* 검색 입력 */}
|
||||
<div className="border-b p-2">
|
||||
<Input
|
||||
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SEARCH)}
|
||||
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SEARCH, getCurrentUserLang())}
|
||||
value={langKeySearchText}
|
||||
onChange={(e) => setLangKeySearchText(e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
|
|
@ -454,7 +477,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
setLangKeySearchText("");
|
||||
}}
|
||||
>
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_NONE)}
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_NONE, getCurrentUserLang())}
|
||||
</div>
|
||||
|
||||
{langKeys
|
||||
|
|
@ -492,39 +515,48 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="menuNameKor">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME)} *</Label>
|
||||
<Label htmlFor="menuNameKor">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME, getCurrentUserLang())} *
|
||||
</Label>
|
||||
<Input
|
||||
id="menuNameKor"
|
||||
value={formData.menuNameKor}
|
||||
onChange={(e) => handleInputChange("menuNameKor", e.target.value)}
|
||||
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME_PLACEHOLDER)}
|
||||
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_NAME_PLACEHOLDER, getCurrentUserLang())}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="menuUrl">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_URL)}</Label>
|
||||
<Label htmlFor="menuUrl">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_URL, getCurrentUserLang())}</Label>
|
||||
<Input
|
||||
id="menuUrl"
|
||||
value={formData.menuUrl}
|
||||
onChange={(e) => handleInputChange("menuUrl", e.target.value)}
|
||||
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER)}
|
||||
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER, getCurrentUserLang())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="menuDesc">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION)}</Label>
|
||||
<Label htmlFor="menuDesc">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION, getCurrentUserLang())}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="menuDesc"
|
||||
value={formData.menuDesc}
|
||||
onChange={(e) => handleInputChange("menuDesc", e.target.value)}
|
||||
placeholder={getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION_PLACEHOLDER)}
|
||||
placeholder={getMenuTextSync(
|
||||
MENU_MANAGEMENT_KEYS.FORM_MENU_DESCRIPTION_PLACEHOLDER,
|
||||
getCurrentUserLang(),
|
||||
)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="seq">{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_SEQUENCE)}</Label>
|
||||
<Label htmlFor="seq">
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FORM_MENU_SEQUENCE, getCurrentUserLang())}
|
||||
</Label>
|
||||
<Input
|
||||
id="seq"
|
||||
type="number"
|
||||
|
|
@ -536,14 +568,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)}
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_CANCEL, getCurrentUserLang())}
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading
|
||||
? getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_SAVE_PROCESSING)
|
||||
? getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_SAVE_PROCESSING, getCurrentUserLang())
|
||||
: isEdit
|
||||
? getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_MODIFY)
|
||||
: getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_REGISTER)}
|
||||
? getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_MODIFY, getCurrentUserLang())
|
||||
: getMenuTextSync(MENU_MANAGEMENT_KEYS.BUTTON_REGISTER, getCurrentUserLang())}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -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,6 +63,119 @@ 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.status",
|
||||
"form.company",
|
||||
"form.company.select",
|
||||
"form.company.common",
|
||||
"form.company.submenu.note",
|
||||
"form.lang.key",
|
||||
"form.lang.key.select",
|
||||
"form.lang.key.none",
|
||||
"form.lang.key.search",
|
||||
"form.lang.key.selected",
|
||||
"form.menu.name",
|
||||
"form.menu.name.placeholder",
|
||||
"form.menu.url",
|
||||
"form.menu.url.placeholder",
|
||||
"form.menu.description",
|
||||
"form.menu.description.placeholder",
|
||||
"form.menu.sequence",
|
||||
|
||||
// 모달
|
||||
"modal.menu.register.title",
|
||||
"modal.menu.modify.title",
|
||||
"modal.delete.title",
|
||||
"modal.delete.description",
|
||||
"modal.delete.batch.description",
|
||||
|
||||
// 메시지
|
||||
"message.loading",
|
||||
"message.menu.delete.processing",
|
||||
"message.menu.save.success",
|
||||
"message.menu.save.failed",
|
||||
"message.menu.delete.success",
|
||||
"message.menu.delete.failed",
|
||||
"message.menu.delete.batch.success",
|
||||
"message.menu.delete.batch.partial",
|
||||
"message.menu.status.toggle.success",
|
||||
"message.menu.status.toggle.failed",
|
||||
"message.validation.menu.name.required",
|
||||
"message.validation.company.required",
|
||||
"message.validation.select.menu.delete",
|
||||
"message.error.load.menu.list",
|
||||
"message.error.load.menu.info",
|
||||
"message.error.load.company.list",
|
||||
"message.error.load.lang.key.list",
|
||||
|
||||
// 리스트 정보
|
||||
"menu.list.title",
|
||||
"menu.list.total",
|
||||
"menu.list.search.result",
|
||||
|
||||
// UI
|
||||
"ui.expand",
|
||||
"ui.collapse",
|
||||
"ui.menu.collapse",
|
||||
"ui.language",
|
||||
];
|
||||
|
||||
// 초기 로딩
|
||||
useEffect(() => {
|
||||
loadCompanies();
|
||||
|
|
@ -80,6 +188,17 @@ export const MenuManagement: React.FC = () => {
|
|||
}
|
||||
}, [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(() => {
|
||||
|
|
@ -134,10 +253,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 +266,7 @@ export const MenuManagement: React.FC = () => {
|
|||
|
||||
// 회사 목록 조회
|
||||
const loadCompanies = async () => {
|
||||
console.log(`🏢 회사 목록 조회 시작`);
|
||||
console.log("🏢 회사 목록 조회 시작");
|
||||
try {
|
||||
const response = await apiClient.get("/admin/companies");
|
||||
|
||||
|
|
@ -165,230 +284,99 @@ 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] = key; // 키를 기본값으로 사용
|
||||
});
|
||||
setUiTexts(defaultTexts);
|
||||
return;
|
||||
}
|
||||
|
||||
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",
|
||||
];
|
||||
// 번역 결과를 상태에 저장
|
||||
console.log("🔧 setUiTexts 호출 전:", { translationsCount: Object.keys(translations).length });
|
||||
setUiTexts(translations);
|
||||
console.log("🔧 setUiTexts 호출 후 - translations:", translations);
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
texts[key] = results[index];
|
||||
});
|
||||
// 번역 캐시에 저장 (다른 컴포넌트에서도 사용할 수 있도록)
|
||||
setTranslationCache(userLang, translations);
|
||||
} else {
|
||||
console.error("❌ 다국어 텍스트 배치 조회 실패:", response.data.message);
|
||||
|
||||
setUiTexts(texts);
|
||||
|
||||
// 번역 텍스트를 캐시에 저장
|
||||
setTranslationCache(currentUserLang, texts);
|
||||
|
||||
console.log("🌐 UI 다국어 텍스트 로드 완료:", texts);
|
||||
// API 실패 시 기본 텍스트 사용
|
||||
const defaultTexts: Record<string, string> = {};
|
||||
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
|
||||
defaultTexts[key] = key; // 키를 기본값으로 사용
|
||||
});
|
||||
setUiTexts(defaultTexts);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ UI 다국어 텍스트 로드 실패:", error);
|
||||
|
||||
// API 실패 시 기본 텍스트 사용
|
||||
const defaultTexts: Record<string, string> = {};
|
||||
MENU_MANAGEMENT_LANG_KEYS.forEach((key) => {
|
||||
defaultTexts[key] = key; // 키를 기본값으로 사용
|
||||
});
|
||||
setUiTexts(defaultTexts);
|
||||
} 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 상태 확인
|
||||
if (!text) {
|
||||
console.log(`🔍 getUITextSync - 키 "${key}"를 uiTexts에서 찾을 수 없음`);
|
||||
console.log("🔍 uiTexts 상태:", {
|
||||
count: Object.keys(uiTexts).length,
|
||||
sampleKeys: Object.keys(uiTexts).slice(0, 5),
|
||||
});
|
||||
text = fallback || key;
|
||||
} else {
|
||||
console.log(`✅ getUITextSync - 키 "${key}" 번역 텍스트 찾음: "${text}"`);
|
||||
}
|
||||
|
||||
// 파라미터 치환
|
||||
|
|
@ -401,11 +389,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 +501,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 +514,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 +540,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 +555,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 +591,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 +644,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 +676,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 +694,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 +715,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 +733,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 +741,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 +750,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 +771,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 +789,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 +799,7 @@ export const MenuManagement: React.FC = () => {
|
|||
setCompanySearchText("");
|
||||
}}
|
||||
>
|
||||
{getMenuTextSync(MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON)}
|
||||
{getUITextSync("filter.company.common")}
|
||||
</div>
|
||||
|
||||
{companies
|
||||
|
|
@ -819,7 +819,7 @@ export const MenuManagement: React.FC = () => {
|
|||
setCompanySearchText("");
|
||||
}}
|
||||
>
|
||||
{company.code === "*" ? "공통" : company.name}
|
||||
{company.code === "*" ? getUITextSync("filter.company.common") : company.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -829,9 +829,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 +847,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 +862,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 +878,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,
|
||||
})
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,34 @@ export const useAuth = () => {
|
|||
|
||||
if (response.success && response.data) {
|
||||
console.log("사용자 정보 조회 성공:", response.data);
|
||||
|
||||
// 사용자 로케일 정보도 함께 조회하여 전역 저장
|
||||
try {
|
||||
const localeResponse = await apiCall<string>("GET", "/admin/user-locale");
|
||||
if (localeResponse.success && localeResponse.data) {
|
||||
const userLocale = localeResponse.data;
|
||||
console.log("✅ 사용자 로케일 조회 성공:", userLocale);
|
||||
|
||||
// 전역 상태에 저장 (다른 컴포넌트에서 사용)
|
||||
(window as any).__GLOBAL_USER_LANG = userLocale;
|
||||
(window as any).__GLOBAL_USER_LOCALE_LOADED = true;
|
||||
|
||||
// localStorage에도 저장 (새 창에서 공유)
|
||||
localStorage.setItem("userLocale", userLocale);
|
||||
localStorage.setItem("userLocaleLoaded", "true");
|
||||
|
||||
console.log("🌐 전역 사용자 로케일 저장됨:", userLocale);
|
||||
}
|
||||
} catch (localeError) {
|
||||
console.warn("⚠️ 사용자 로케일 조회 실패, 기본값 사용:", localeError);
|
||||
(window as any).__GLOBAL_USER_LANG = "KR";
|
||||
(window as any).__GLOBAL_USER_LOCALE_LOADED = true;
|
||||
|
||||
// localStorage에도 저장
|
||||
localStorage.setItem("userLocale", "KR");
|
||||
localStorage.setItem("userLocaleLoaded", "true");
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
|
@ -348,6 +376,12 @@ export const useAuth = () => {
|
|||
// JWT 토큰 제거
|
||||
TokenManager.removeToken();
|
||||
|
||||
// 로케일 정보도 제거
|
||||
localStorage.removeItem("userLocale");
|
||||
localStorage.removeItem("userLocaleLoaded");
|
||||
(window as any).__GLOBAL_USER_LANG = undefined;
|
||||
(window as any).__GLOBAL_USER_LOCALE_LOADED = undefined;
|
||||
|
||||
// 로그아웃 API 호출 성공 여부와 관계없이 클라이언트 상태 초기화
|
||||
setUser(null);
|
||||
setAuthStatus({
|
||||
|
|
@ -365,6 +399,13 @@ export const useAuth = () => {
|
|||
|
||||
// 오류가 발생해도 JWT 토큰 제거 및 클라이언트 상태 초기화
|
||||
TokenManager.removeToken();
|
||||
|
||||
// 로케일 정보도 제거
|
||||
localStorage.removeItem("userLocale");
|
||||
localStorage.removeItem("userLocaleLoaded");
|
||||
(window as any).__GLOBAL_USER_LANG = undefined;
|
||||
(window as any).__GLOBAL_USER_LOCALE_LOADED = undefined;
|
||||
|
||||
setUser(null);
|
||||
setAuthStatus({
|
||||
isLoggedIn: false,
|
||||
|
|
|
|||
|
|
@ -6,70 +6,82 @@ let globalUserLang = "KR";
|
|||
let globalChangeLangCallback: ((lang: string) => void) | null = null;
|
||||
|
||||
export const useMultiLang = (options: { companyCode?: string } = {}) => {
|
||||
const [userLang, setUserLang] = useState<string>("KR");
|
||||
const [userLang, setUserLang] = useState<string | null>(null); // null로 시작
|
||||
const companyCode = options.companyCode || "*";
|
||||
|
||||
// 전역 언어 상태 동기화
|
||||
// 전역 언어 상태 동기화 (무한 루프 방지)
|
||||
useEffect(() => {
|
||||
if (globalUserLang !== userLang) {
|
||||
// 초기 로딩 시에만 동기화
|
||||
if (globalUserLang && globalUserLang !== userLang) {
|
||||
setUserLang(globalUserLang);
|
||||
}
|
||||
}, [globalUserLang]);
|
||||
}, []); // 의존성 배열을 비워서 한 번만 실행
|
||||
|
||||
// 언어 변경 시 전역 콜백 호출
|
||||
// 언어 변경 시 전역 콜백 호출 (무한 루프 방지)
|
||||
useEffect(() => {
|
||||
if (globalChangeLangCallback) {
|
||||
// 언어가 설정된 경우에만 콜백 호출
|
||||
if (globalChangeLangCallback && userLang) {
|
||||
globalChangeLangCallback(userLang);
|
||||
}
|
||||
}, [userLang]);
|
||||
|
||||
// 사용자 로케일 조회 (한 번만 실행)
|
||||
useEffect(() => {
|
||||
// localStorage에서 로케일 확인 (새 창에서도 공유)
|
||||
const storedLocale = localStorage.getItem("userLocale");
|
||||
const storedLocaleLoaded = localStorage.getItem("userLocaleLoaded");
|
||||
|
||||
if (storedLocaleLoaded === "true" && storedLocale) {
|
||||
console.log("🌐 localStorage에서 사용자 로케일 사용:", storedLocale);
|
||||
setUserLang(storedLocale);
|
||||
globalUserLang = storedLocale;
|
||||
|
||||
// 전역 상태도 동기화
|
||||
(window as any).__GLOBAL_USER_LANG = storedLocale;
|
||||
(window as any).__GLOBAL_USER_LOCALE_LOADED = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 전역에서 이미 로케일이 로드되었는지 확인
|
||||
if ((window as any).__GLOBAL_USER_LOCALE_LOADED) {
|
||||
const globalLocale = (window as any).__GLOBAL_USER_LANG;
|
||||
console.log("🌐 전역에서 사용자 로케일 사용:", globalLocale);
|
||||
setUserLang(globalLocale);
|
||||
globalUserLang = globalLocale;
|
||||
return;
|
||||
}
|
||||
|
||||
// 이미 로케일이 설정되어 있으면 중복 호출 방지
|
||||
if (globalUserLang && globalUserLang !== "KR") {
|
||||
if (globalUserLang) {
|
||||
setUserLang(globalUserLang);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchUserLocale = async () => {
|
||||
try {
|
||||
console.log("🔍 사용자 로케일 조회 시작");
|
||||
const response = await apiClient.get("/admin/user-locale");
|
||||
// 전역 로케일이 아직 로드되지 않았으면 대기
|
||||
console.log("⏳ 전역 로케일 로드 대기 중...");
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const userLocale = response.data.data;
|
||||
console.log("✅ 사용자 로케일 조회 성공:", userLocale);
|
||||
|
||||
// 데이터베이스의 locale 값을 그대로 사용 (매핑 없음)
|
||||
setUserLang(userLocale);
|
||||
globalUserLang = userLocale; // 전역 상태도 업데이트
|
||||
return;
|
||||
}
|
||||
|
||||
// API 호출 실패 시 브라우저 언어 사용
|
||||
console.warn("⚠️ 사용자 로케일 조회 실패, 브라우저 언어 사용");
|
||||
const browserLang = navigator.language.split("-")[0];
|
||||
|
||||
// 브라우저 언어를 그대로 사용 (매핑 없음)
|
||||
if (["ko", "en", "ja", "zh"].includes(browserLang)) {
|
||||
setUserLang(browserLang);
|
||||
globalUserLang = browserLang;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 사용자 로케일 조회 중 오류:", error);
|
||||
|
||||
// 오류 시 브라우저 언어 사용
|
||||
const browserLang = navigator.language.split("-")[0];
|
||||
|
||||
// 브라우저 언어를 그대로 사용 (매핑 없음)
|
||||
if (["ko", "en", "ja", "zh"].includes(browserLang)) {
|
||||
setUserLang(browserLang);
|
||||
globalUserLang = browserLang;
|
||||
}
|
||||
// 주기적으로 전역 로케일 확인
|
||||
const checkInterval = setInterval(() => {
|
||||
if ((window as any).__GLOBAL_USER_LOCALE_LOADED) {
|
||||
const globalLocale = (window as any).__GLOBAL_USER_LANG;
|
||||
console.log("🌐 전역에서 사용자 로케일 확인됨:", globalLocale);
|
||||
setUserLang(globalLocale);
|
||||
globalUserLang = globalLocale;
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
};
|
||||
}, 100);
|
||||
|
||||
fetchUserLocale();
|
||||
// 5초 후 타임아웃
|
||||
setTimeout(() => {
|
||||
clearInterval(checkInterval);
|
||||
if (!userLang) {
|
||||
console.warn("⚠️ 전역 로케일 로드 타임아웃, 기본값 사용");
|
||||
setUserLang("KR");
|
||||
globalUserLang = "KR";
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(checkInterval);
|
||||
}, []);
|
||||
|
||||
// 다국어 텍스트 가져오기 (배치 조회 방식)
|
||||
|
|
@ -114,17 +126,27 @@ export const useMultiLang = (options: { companyCode?: string } = {}) => {
|
|||
}
|
||||
};
|
||||
|
||||
// 언어 변경
|
||||
// 언어 변경 (무한 루프 방지)
|
||||
const changeLang = async (newLang: string) => {
|
||||
// 같은 언어로 변경하려는 경우 무시
|
||||
if (newLang === userLang) {
|
||||
console.log("🔄 같은 언어로 변경 시도 무시:", newLang);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("🔄 언어 변경 시작:", { from: userLang, to: newLang });
|
||||
|
||||
// 백엔드에 사용자 로케일 설정 요청
|
||||
const response = await apiClient.post("/admin/user-locale", {
|
||||
locale: newLang,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
setUserLang(newLang);
|
||||
// 전역 상태 먼저 업데이트
|
||||
globalUserLang = newLang;
|
||||
// 로컬 상태 업데이트
|
||||
setUserLang(newLang);
|
||||
console.log("✅ 사용자 로케일 변경 성공:", newLang);
|
||||
} else {
|
||||
console.error("❌ 사용자 로케일 변경 실패:", response.data.message);
|
||||
|
|
@ -132,8 +154,8 @@ export const useMultiLang = (options: { companyCode?: string } = {}) => {
|
|||
} catch (error) {
|
||||
console.error("❌ 사용자 로케일 변경 중 오류:", error);
|
||||
// 오류 시에도 로컬 상태는 변경
|
||||
setUserLang(newLang);
|
||||
globalUserLang = newLang;
|
||||
setUserLang(newLang);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ apiClient.interceptors.request.use(
|
|||
console.warn("⚠️ 토큰이 없습니다.");
|
||||
}
|
||||
|
||||
// 언어 정보를 쿼리 파라미터에 추가
|
||||
// 언어 정보를 쿼리 파라미터에 추가 (GET 요청 시에만)
|
||||
if (config.method?.toUpperCase() === "GET") {
|
||||
// 전역 언어 상태에서 현재 언어 가져오기
|
||||
const currentLang = typeof window !== "undefined" ? (window as any).__GLOBAL_USER_LANG || "ko" : "ko";
|
||||
// 전역 언어 상태에서 현재 언어 가져오기 (DB 값 그대로 사용)
|
||||
const currentLang = typeof window !== "undefined" ? (window as any).__GLOBAL_USER_LANG || "KR" : "KR";
|
||||
console.log("🌐 API 요청 시 언어 정보:", currentLang);
|
||||
|
||||
if (config.params) {
|
||||
|
|
|
|||
|
|
@ -145,11 +145,11 @@ export const menuApi = {
|
|||
menuCode?: string;
|
||||
keyType?: string;
|
||||
}): Promise<ApiResponse<LangKey[]>> => {
|
||||
console.log("🔍 다국어 키 목록 조회 API 호출:", "/admin/multilang/keys", params);
|
||||
console.log("🔍 다국어 키 목록 조회 API 호출:", "/multilang/keys", params);
|
||||
|
||||
try {
|
||||
// Node.js 백엔드의 실제 라우팅과 일치하도록 수정
|
||||
const response = await apiClient.get("/admin/multilang/keys", { params });
|
||||
const response = await apiClient.get("/multilang/keys", { params });
|
||||
console.log("✅ 다국어 키 목록 조회 성공:", response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { apiClient } from "../api/client";
|
|||
// 메뉴 관리 화면 다국어 키 상수
|
||||
export const MENU_MANAGEMENT_KEYS = {
|
||||
// 기본 정보
|
||||
TITLE: "title",
|
||||
DESCRIPTION: "description",
|
||||
TITLE: "menu.management.title",
|
||||
DESCRIPTION: "menu.management.description",
|
||||
MENU_TYPE_TITLE: "menu.type.title",
|
||||
MENU_TYPE_ADMIN: "menu.type.admin",
|
||||
MENU_TYPE_USER: "menu.type.user",
|
||||
|
|
|
|||
Loading…
Reference in New Issue