ERP-node/frontend/app/(main)/admin/systemMng/i18nList/page.tsx

904 lines
31 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Plus } from "lucide-react";
import { DataTable } from "@/components/common/DataTable";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { useAuth } from "@/hooks/useAuth";
import LangKeyModal from "@/components/admin/LangKeyModal";
import LanguageModal from "@/components/admin/LanguageModal";
import { CategoryTree } from "@/components/admin/multilang/CategoryTree";
import { KeyGenerateModal } from "@/components/admin/multilang/KeyGenerateModal";
import { apiClient } from "@/lib/api/client";
import { LangCategory } from "@/lib/api/multilang";
interface Language {
langCode: string;
langName: string;
langNative: string;
isActive: string;
}
interface LangKey {
keyId: number;
companyCode: string;
menuName: string;
langKey: string;
description: string;
isActive: string;
categoryId?: number;
}
interface LangText {
textId: number;
keyId: number;
langCode: string;
langText: string;
isActive: string;
}
export default function I18nPage() {
const { user } = useAuth();
const [loading, setLoading] = useState(true);
const [languages, setLanguages] = useState<Language[]>([]);
const [langKeys, setLangKeys] = useState<LangKey[]>([]);
const [selectedKey, setSelectedKey] = useState<LangKey | null>(null);
const [langTexts, setLangTexts] = useState<LangText[]>([]);
const [editingTexts, setEditingTexts] = useState<LangText[]>([]);
const [selectedCompany, setSelectedCompany] = useState("all");
const [searchText, setSearchText] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingKey, setEditingKey] = useState<LangKey | null>(null);
const [selectedKeys, setSelectedKeys] = useState<Set<number>>(new Set());
// 언어 관리 관련 상태
const [isLanguageModalOpen, setIsLanguageModalOpen] = useState(false);
const [editingLanguage, setEditingLanguage] = useState<Language | null>(null);
const [selectedLanguages, setSelectedLanguages] = useState<Set<string>>(new Set());
const [activeTab, setActiveTab] = useState<"keys" | "languages">("keys");
// 카테고리 관련 상태
const [selectedCategory, setSelectedCategory] = useState<LangCategory | null>(null);
const [isGenerateModalOpen, setIsGenerateModalOpen] = useState(false);
const [companies, setCompanies] = useState<Array<{ code: string; name: string }>>([]);
// 회사 목록 조회
const fetchCompanies = async () => {
try {
const response = await apiClient.get("/admin/companies");
const data = response.data;
if (data.success) {
const companyList = data.data.map((company: any) => ({
code: company.company_code,
name: company.company_name,
}));
setCompanies(companyList);
}
} catch (error) {
// console.error("회사 목록 조회 실패:", error);
}
};
// 언어 목록 조회
const fetchLanguages = async () => {
try {
const response = await apiClient.get("/multilang/languages");
const data = response.data;
if (data.success) {
setLanguages(data.data);
}
} catch (error) {
// console.error("언어 목록 조회 실패:", error);
}
};
// 다국어 키 목록 조회
const fetchLangKeys = async (categoryId?: number | null) => {
try {
const params = new URLSearchParams();
if (categoryId) {
params.append("categoryId", categoryId.toString());
}
const url = `/multilang/keys${params.toString() ? `?${params.toString()}` : ""}`;
const response = await apiClient.get(url);
const data = response.data;
if (data.success) {
setLangKeys(data.data);
}
} catch (error) {
// console.error("다국어 키 목록 조회 실패:", error);
}
};
// 필터링된 데이터 계산
const getFilteredLangKeys = () => {
let filteredKeys = langKeys;
// 회사 필터링
if (selectedCompany && selectedCompany !== "all") {
filteredKeys = filteredKeys.filter((key) => key.companyCode === selectedCompany);
}
// 텍스트 검색 필터링
if (searchText.trim()) {
const searchLower = searchText.toLowerCase();
filteredKeys = filteredKeys.filter((key) => {
const langKey = (key.langKey || "").toLowerCase();
const description = (key.description || "").toLowerCase();
const menuName = (key.menuName || "").toLowerCase();
const companyName = companies.find((c) => c.code === key.companyCode)?.name?.toLowerCase() || "";
return (
langKey.includes(searchLower) ||
description.includes(searchLower) ||
menuName.includes(searchLower) ||
companyName.includes(searchLower)
);
});
}
return filteredKeys;
};
// 선택된 키의 다국어 텍스트 조회
const fetchLangTexts = async (keyId: number) => {
try {
const response = await apiClient.get(`/multilang/keys/${keyId}/texts`);
const data = response.data;
if (data.success) {
setLangTexts(data.data);
const editingData = data.data.map((text: LangText) => ({ ...text }));
setEditingTexts(editingData);
}
} catch (error) {
// console.error("다국어 텍스트 조회 실패:", error);
}
};
// 언어 키 선택 처리
const handleKeySelect = (key: LangKey) => {
setSelectedKey(key);
fetchLangTexts(key.keyId);
};
// 텍스트 변경 처리
const handleTextChange = (langCode: string, value: string) => {
const newEditingTexts = [...editingTexts];
const existingIndex = newEditingTexts.findIndex((t) => t.langCode === langCode);
if (existingIndex >= 0) {
newEditingTexts[existingIndex].langText = value;
} else {
newEditingTexts.push({
textId: 0,
keyId: selectedKey!.keyId,
langCode: langCode,
langText: value,
isActive: "Y",
});
}
setEditingTexts(newEditingTexts);
};
// 텍스트 저장
const handleSave = async () => {
if (!selectedKey) return;
try {
const requestData = {
texts: editingTexts.map((text) => ({
langCode: text.langCode,
langText: text.langText,
isActive: text.isActive || "Y",
createdBy: user?.userId || "system",
updatedBy: user?.userId || "system",
})),
};
const response = await apiClient.post(`/multilang/keys/${selectedKey.keyId}/texts`, requestData);
const data = response.data;
if (data.success) {
alert("저장되었습니다.");
fetchLangTexts(selectedKey.keyId);
}
} catch (error) {
alert("저장에 실패했습니다.");
}
};
// 언어 키 추가/수정 모달 열기
const handleAddKey = () => {
setEditingKey(null);
setIsModalOpen(true);
};
// 언어 추가/수정 모달 열기
const handleAddLanguage = () => {
setEditingLanguage(null);
setIsLanguageModalOpen(true);
};
// 언어 수정
const handleEditLanguage = (language: Language) => {
setEditingLanguage(language);
setIsLanguageModalOpen(true);
};
// 언어 저장 (추가/수정)
const handleSaveLanguage = async (languageData: any) => {
try {
const requestData = {
...languageData,
createdBy: user?.userId || "admin",
updatedBy: user?.userId || "admin",
};
let response;
if (editingLanguage) {
response = await apiClient.put(`/multilang/languages/${editingLanguage.langCode}`, requestData);
} else {
response = await apiClient.post("/multilang/languages", requestData);
}
const result = response.data;
if (result.success) {
alert(editingLanguage ? "언어가 수정되었습니다." : "언어가 추가되었습니다.");
setIsLanguageModalOpen(false);
fetchLanguages();
} else {
alert(`오류: ${result.message}`);
}
} catch (error) {
alert("언어 저장 중 오류가 발생했습니다.");
}
};
// 언어 삭제
const handleDeleteLanguages = async () => {
if (selectedLanguages.size === 0) {
alert("삭제할 언어를 선택해주세요.");
return;
}
if (
!confirm(
`선택된 ${selectedLanguages.size}개의 언어를 영구적으로 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`,
)
) {
return;
}
try {
const deletePromises = Array.from(selectedLanguages).map((langCode) =>
apiClient.delete(`/multilang/languages/${langCode}`),
);
const responses = await Promise.all(deletePromises);
const failedDeletes = responses.filter((response) => !response.data.success);
if (failedDeletes.length === 0) {
alert("선택된 언어가 삭제되었습니다.");
setSelectedLanguages(new Set());
fetchLanguages();
} else {
alert(`${failedDeletes.length}개의 언어 삭제에 실패했습니다.`);
}
} catch (error) {
alert("언어 삭제 중 오류가 발생했습니다.");
}
};
// 언어 선택 체크박스 처리
const handleLanguageCheckboxChange = (langCode: string, checked: boolean) => {
const newSelected = new Set(selectedLanguages);
if (checked) {
newSelected.add(langCode);
} else {
newSelected.delete(langCode);
}
setSelectedLanguages(newSelected);
};
// 언어 전체 선택/해제
const handleSelectAllLanguages = (checked: boolean) => {
if (checked) {
setSelectedLanguages(new Set(languages.map((lang) => lang.langCode)));
} else {
setSelectedLanguages(new Set());
}
};
// 언어 키 수정 모달 열기
const handleEditKey = (key: LangKey) => {
setEditingKey(key);
setIsModalOpen(true);
};
// 언어 키 저장 (추가/수정)
const handleSaveKey = async (keyData: any) => {
try {
const requestData = {
...keyData,
createdBy: user?.userId || "admin",
updatedBy: user?.userId || "admin",
};
let response;
if (editingKey) {
response = await apiClient.put(`/multilang/keys/${editingKey.keyId}`, requestData);
} else {
response = await apiClient.post("/multilang/keys", requestData);
}
const data = response.data;
if (data.success) {
alert(editingKey ? "언어 키가 수정되었습니다." : "언어 키가 추가되었습니다.");
fetchLangKeys();
setIsModalOpen(false);
} else {
if (data.message && data.message.includes("이미 존재하는 언어키")) {
alert(data.message);
} else {
alert(data.message || "언어 키 저장에 실패했습니다.");
}
}
} catch (error) {
alert("언어 키 저장에 실패했습니다.");
}
};
// 체크박스 선택/해제
const handleCheckboxChange = (keyId: number, checked: boolean) => {
const newSelectedKeys = new Set(selectedKeys);
if (checked) {
newSelectedKeys.add(keyId);
} else {
newSelectedKeys.delete(keyId);
}
setSelectedKeys(newSelectedKeys);
};
// 키 상태 토글
const handleToggleStatus = async (keyId: number) => {
try {
const response = await apiClient.put(`/multilang/keys/${keyId}/toggle`);
const data = response.data;
if (data.success) {
alert(`키가 ${data.data}되었습니다.`);
fetchLangKeys();
} else {
alert("상태 변경 중 오류가 발생했습니다.");
}
} catch (error) {
alert("키 상태 변경 중 오류가 발생했습니다.");
}
};
// 언어 상태 토글
const handleToggleLanguageStatus = async (langCode: string) => {
try {
const response = await apiClient.put(`/multilang/languages/${langCode}/toggle`);
const data = response.data;
if (data.success) {
alert(`언어가 ${data.data}되었습니다.`);
fetchLanguages();
} else {
alert("언어 상태 변경 중 오류가 발생했습니다.");
}
} catch (error) {
alert("언어 상태 변경 중 오류가 발생했습니다.");
}
};
// 전체 선택/해제
const handleSelectAll = (checked: boolean) => {
if (checked) {
const allKeyIds = getFilteredLangKeys().map((key) => key.keyId);
setSelectedKeys(new Set(allKeyIds));
} else {
setSelectedKeys(new Set());
}
};
// 선택된 키들 일괄 삭제
const handleDeleteSelectedKeys = async () => {
if (selectedKeys.size === 0) {
alert("삭제할 키를 선택해주세요.");
return;
}
if (
!confirm(
`선택된 ${selectedKeys.size}개의 언어 키를 영구적으로 삭제하시겠습니까?\n\n⚠ 이 작업은 되돌릴 수 없습니다.`,
)
) {
return;
}
try {
const deletePromises = Array.from(selectedKeys).map((keyId) => apiClient.delete(`/multilang/keys/${keyId}`));
const responses = await Promise.all(deletePromises);
const allSuccess = responses.every((response) => response.data.success);
if (allSuccess) {
alert(`${selectedKeys.size}개의 언어 키가 영구적으로 삭제되었습니다.`);
setSelectedKeys(new Set());
fetchLangKeys();
if (selectedKey && selectedKeys.has(selectedKey.keyId)) {
handleCancel();
}
} else {
alert("일부 키 삭제에 실패했습니다.");
}
} catch (error) {
alert("선택된 키 삭제에 실패했습니다.");
}
};
// 개별 키 삭제
const handleDeleteKey = async (keyId: number) => {
if (!confirm("정말로 이 언어 키를 영구적으로 삭제하시겠습니까?\n\n⚠ 이 작업은 되돌릴 수 없습니다.")) {
return;
}
try {
const response = await apiClient.delete(`/multilang/keys/${keyId}`);
const data = response.data;
if (data.success) {
alert("언어 키가 영구적으로 삭제되었습니다.");
fetchLangKeys();
if (selectedKey && selectedKey.keyId === keyId) {
handleCancel();
}
}
} catch (error) {
alert("언어 키 삭제에 실패했습니다.");
}
};
// 취소 처리
const handleCancel = () => {
setSelectedKey(null);
setLangTexts([]);
setEditingTexts([]);
};
useEffect(() => {
const initializeData = async () => {
setLoading(true);
await Promise.all([fetchCompanies(), fetchLanguages(), fetchLangKeys()]);
setLoading(false);
};
initializeData();
}, []);
// 카테고리 변경 시 키 목록 다시 조회
useEffect(() => {
if (!loading) {
fetchLangKeys(selectedCategory?.categoryId);
}
}, [selectedCategory?.categoryId]);
const columns = [
{
id: "select",
header: () => {
const filteredKeys = getFilteredLangKeys();
return (
<input
type="checkbox"
checked={selectedKeys.size === filteredKeys.length && filteredKeys.length > 0}
onChange={(e) => handleSelectAll(e.target.checked)}
className="h-4 w-4"
/>
);
},
cell: ({ row }: any) => (
<input
type="checkbox"
checked={selectedKeys.has(row.original.keyId)}
onChange={(e) => handleCheckboxChange(row.original.keyId, e.target.checked)}
onClick={(e) => e.stopPropagation()}
className="h-4 w-4"
disabled={row.original.isActive === "N"}
/>
),
},
{
accessorKey: "companyCode",
header: "회사",
cell: ({ row }: any) => {
const companyName =
row.original.companyCode === "*"
? "공통"
: companies.find((c) => c.code === row.original.companyCode)?.name || row.original.companyCode;
return <span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{companyName}</span>;
},
},
{
accessorKey: "menuName",
header: "메뉴명",
cell: ({ row }: any) => (
<span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{row.original.menuName}</span>
),
},
{
accessorKey: "langKey",
header: "언어 키",
cell: ({ row }: any) => (
<div
className={`cursor-pointer rounded p-1 hover:bg-gray-100 ${
row.original.isActive === "N" ? "text-gray-400" : ""
}`}
onDoubleClick={() => handleEditKey(row.original)}
>
{row.original.langKey}
</div>
),
},
{
accessorKey: "description",
header: "설명",
cell: ({ row }: any) => (
<span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{row.original.description}</span>
),
},
{
accessorKey: "isActive",
header: "상태",
cell: ({ row }: any) => (
<button
onClick={() => handleToggleStatus(row.original.keyId)}
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
row.original.isActive === "Y"
? "bg-green-100 text-green-800 hover:bg-green-200"
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
}`}
>
{row.original.isActive === "Y" ? "활성" : "비활성"}
</button>
),
},
];
// 언어 테이블 컬럼 정의
const languageColumns = [
{
id: "select",
header: () => (
<input
type="checkbox"
checked={selectedLanguages.size === languages.length && languages.length > 0}
onChange={(e) => handleSelectAllLanguages(e.target.checked)}
className="h-4 w-4"
/>
),
cell: ({ row }: any) => (
<input
type="checkbox"
checked={selectedLanguages.has(row.original.langCode)}
onChange={(e) => handleLanguageCheckboxChange(row.original.langCode, e.target.checked)}
onClick={(e) => e.stopPropagation()}
className="h-4 w-4"
disabled={row.original.isActive === "N"}
/>
),
},
{
accessorKey: "langCode",
header: "언어 코드",
cell: ({ row }: any) => (
<div
className={`cursor-pointer rounded p-1 hover:bg-gray-100 ${
row.original.isActive === "N" ? "text-gray-400" : ""
}`}
onDoubleClick={() => handleEditLanguage(row.original)}
>
{row.original.langCode}
</div>
),
},
{
accessorKey: "langName",
header: "언어명 (영문)",
cell: ({ row }: any) => (
<span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{row.original.langName}</span>
),
},
{
accessorKey: "langNative",
header: "언어명 (원어)",
cell: ({ row }: any) => (
<span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{row.original.langNative}</span>
),
},
{
accessorKey: "isActive",
header: "상태",
cell: ({ row }: any) => (
<button
onClick={() => handleToggleLanguageStatus(row.original.langCode)}
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
row.original.isActive === "Y"
? "bg-green-100 text-green-800 hover:bg-green-200"
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
}`}
>
{row.original.isActive === "Y" ? "활성" : "비활성"}
</button>
),
},
];
if (loading) {
return <LoadingSpinner />;
}
return (
<div className="min-h-screen bg-gray-50">
<div className="w-full max-w-none px-4 py-8">
<div className="container mx-auto p-2">
{/* 탭 네비게이션 */}
<div className="flex space-x-1 border-b">
<button
onClick={() => setActiveTab("keys")}
className={`rounded-t-lg px-3 py-1.5 text-sm font-medium transition-colors ${
activeTab === "keys" ? "bg-accent0 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
</button>
<button
onClick={() => setActiveTab("languages")}
className={`rounded-t-lg px-3 py-1.5 text-sm font-medium transition-colors ${
activeTab === "languages" ? "bg-accent0 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
</button>
</div>
{/* 메인 콘텐츠 영역 */}
<div className="mt-2">
{/* 언어 관리 탭 */}
{activeTab === "languages" && (
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="mb-4 flex items-center justify-between">
<div className="text-sm text-muted-foreground"> {languages.length} .</div>
<div className="flex space-x-2">
{selectedLanguages.size > 0 && (
<Button variant="destructive" onClick={handleDeleteLanguages}>
({selectedLanguages.size})
</Button>
)}
<Button onClick={handleAddLanguage}> </Button>
</div>
</div>
<DataTable data={languages} columns={languageColumns} searchable />
</CardContent>
</Card>
)}
{/* 다국어 키 관리 탭 */}
{activeTab === "keys" && (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-12">
{/* 좌측: 카테고리 트리 (2/12) */}
<Card className="lg:col-span-2">
<CardHeader className="py-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm"></CardTitle>
</div>
</CardHeader>
<CardContent className="p-2">
<ScrollArea className="h-[500px]">
<CategoryTree
selectedCategoryId={selectedCategory?.categoryId || null}
onSelectCategory={(cat) => setSelectedCategory(cat)}
onDoubleClickCategory={(cat) => {
setSelectedCategory(cat);
setIsGenerateModalOpen(true);
}}
/>
</ScrollArea>
</CardContent>
</Card>
{/* 중앙: 언어 키 목록 (6/12) */}
<Card className="lg:col-span-6">
<CardHeader className="py-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm">
{selectedCategory && (
<Badge variant="secondary" className="ml-2">
{selectedCategory.categoryName}
</Badge>
)}
</CardTitle>
<div className="flex space-x-2">
<Button
size="sm"
variant="destructive"
onClick={handleDeleteSelectedKeys}
disabled={selectedKeys.size === 0}
>
({selectedKeys.size})
</Button>
<Button size="sm" variant="outline" onClick={handleAddKey}>
</Button>
<Button
size="sm"
onClick={() => setIsGenerateModalOpen(true)}
disabled={!selectedCategory}
>
<Plus className="mr-1 h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
{/* 검색 필터 영역 */}
<div className="mb-2 grid grid-cols-1 gap-2 md:grid-cols-3">
<div>
<Label htmlFor="company" className="text-xs"></Label>
<Select value={selectedCompany} onValueChange={setSelectedCompany}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="전체 회사" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{companies.map((company) => (
<SelectItem key={company.code} value={company.code}>
{company.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="search" className="text-xs"></Label>
<Input
placeholder="키명, 설명으로 검색..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="h-8 text-xs"
/>
</div>
<div className="flex items-end">
<div className="text-xs text-muted-foreground">: {getFilteredLangKeys().length}</div>
</div>
</div>
{/* 테이블 영역 */}
<div>
<DataTable
columns={columns}
data={getFilteredLangKeys()}
searchable={false}
onRowClick={handleKeySelect}
/>
</div>
</CardContent>
</Card>
{/* 우측: 선택된 키의 다국어 관리 (4/12) */}
<Card className="lg:col-span-4">
<CardHeader>
<CardTitle>
{selectedKey ? (
<>
:{" "}
<Badge variant="secondary" className="ml-2">
{selectedKey.companyCode}.{selectedKey.menuName}.{selectedKey.langKey}
</Badge>
</>
) : (
"다국어 편집"
)}
</CardTitle>
</CardHeader>
<CardContent>
{selectedKey ? (
<div>
{/* 스크롤 가능한 텍스트 영역 */}
<div className="max-h-80 space-y-4 overflow-y-auto pr-2">
{languages
.filter((lang) => lang.isActive === "Y")
.map((lang) => {
const text = editingTexts.find((t) => t.langCode === lang.langCode);
return (
<div key={lang.langCode} className="flex items-center space-x-4">
<Badge variant="outline" className="w-20 flex-shrink-0 text-center">
{lang.langName}
</Badge>
<Input
placeholder={`${lang.langName} 텍스트 입력`}
value={text?.langText || ""}
onChange={(e) => handleTextChange(lang.langCode, e.target.value)}
className="flex-1"
/>
</div>
);
})}
</div>
{/* 저장 버튼 - 고정 위치 */}
<div className="mt-4 flex space-x-2 border-t pt-4">
<Button onClick={handleSave}></Button>
<Button variant="outline" onClick={handleCancel}>
</Button>
</div>
</div>
) : (
<div className="flex h-64 items-center justify-center text-gray-500">
<div className="text-center">
<div className="mb-2 text-lg font-medium"> </div>
<div className="text-sm"> </div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
)}
</div>
{/* 언어 키 추가/수정 모달 */}
<LangKeyModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSave={handleSaveKey}
keyData={editingKey}
companies={companies}
/>
{/* 언어 추가/수정 모달 */}
<LanguageModal
isOpen={isLanguageModalOpen}
onClose={() => setIsLanguageModalOpen(false)}
onSave={handleSaveLanguage}
languageData={editingLanguage}
/>
{/* 키 자동 생성 모달 */}
<KeyGenerateModal
isOpen={isGenerateModalOpen}
onClose={() => setIsGenerateModalOpen(false)}
selectedCategory={selectedCategory}
companyCode={user?.companyCode || ""}
isSuperAdmin={user?.companyCode === "*"}
onSuccess={() => {
fetchLangKeys(selectedCategory?.categoryId);
}}
/>
</div>
</div>
</div>
);
}