2025-08-21 09:41:46 +09:00
|
|
|
|
"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 { DataTable } from "@/components/common/DataTable";
|
|
|
|
|
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
|
|
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
|
|
|
|
import LangKeyModal from "./LangKeyModal";
|
|
|
|
|
|
import LanguageModal from "./LanguageModal";
|
2025-08-21 14:47:07 +09:00
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
interface Language {
|
|
|
|
|
|
langCode: string;
|
|
|
|
|
|
langName: string;
|
|
|
|
|
|
langNative: string;
|
|
|
|
|
|
isActive: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface LangKey {
|
|
|
|
|
|
keyId: number;
|
|
|
|
|
|
companyCode: string;
|
|
|
|
|
|
menuName: string;
|
|
|
|
|
|
langKey: string;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
isActive: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface LangText {
|
|
|
|
|
|
textId: number;
|
|
|
|
|
|
keyId: number;
|
|
|
|
|
|
langCode: string;
|
|
|
|
|
|
langText: string;
|
|
|
|
|
|
isActive: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function MultiLangPage() {
|
|
|
|
|
|
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 [companies, setCompanies] = useState<Array<{ code: string; name: string }>>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 회사 목록 조회
|
|
|
|
|
|
const fetchCompanies = async () => {
|
|
|
|
|
|
try {
|
2025-08-21 14:47:07 +09:00
|
|
|
|
console.log("회사 목록 조회 시작");
|
2025-08-25 15:12:31 +09:00
|
|
|
|
const response = await apiClient.get("/admin/companies");
|
2025-08-21 14:47:07 +09:00
|
|
|
|
console.log("회사 목록 응답 데이터:", response.data);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const data = response.data;
|
2025-08-21 09:41:46 +09:00
|
|
|
|
if (data.success) {
|
|
|
|
|
|
const companyList = data.data.map((company: any) => ({
|
2025-08-21 14:47:07 +09:00
|
|
|
|
code: company.company_code,
|
|
|
|
|
|
name: company.company_name,
|
2025-08-21 09:41:46 +09:00
|
|
|
|
}));
|
|
|
|
|
|
console.log("변환된 회사 목록:", companyList);
|
|
|
|
|
|
setCompanies(companyList);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("회사 목록 조회 실패:", data.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("회사 목록 조회 실패:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 언어 목록 조회
|
|
|
|
|
|
const fetchLanguages = async () => {
|
|
|
|
|
|
try {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
const response = await apiClient.get("/multilang/languages");
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const data = response.data;
|
2025-08-21 09:41:46 +09:00
|
|
|
|
if (data.success) {
|
|
|
|
|
|
setLanguages(data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("언어 목록 조회 실패:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 다국어 키 목록 조회
|
|
|
|
|
|
const fetchLangKeys = async () => {
|
|
|
|
|
|
try {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
const response = await apiClient.get("/multilang/keys");
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const data = response.data;
|
2025-08-21 09:41:46 +09:00
|
|
|
|
if (data.success) {
|
|
|
|
|
|
console.log("✅ 전체 키 목록 로드:", data.data.length, "개");
|
|
|
|
|
|
setLangKeys(data.data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("❌ 키 목록 로드 실패:", data.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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 {
|
|
|
|
|
|
console.log("다국어 텍스트 조회 시작: keyId =", keyId);
|
2025-08-25 15:12:31 +09:00
|
|
|
|
const response = await apiClient.get(`/multilang/keys/${keyId}/texts`);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const data = response.data;
|
2025-08-21 09:41:46 +09:00
|
|
|
|
console.log("다국어 텍스트 조회 응답:", data);
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
setLangTexts(data.data);
|
|
|
|
|
|
// 편집용 텍스트 초기화
|
|
|
|
|
|
const editingData = data.data.map((text: LangText) => ({ ...text }));
|
|
|
|
|
|
setEditingTexts(editingData);
|
|
|
|
|
|
console.log("편집용 텍스트 설정:", editingData);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("다국어 텍스트 조회 실패:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 언어 키 선택 처리
|
|
|
|
|
|
const handleKeySelect = (key: LangKey) => {
|
|
|
|
|
|
console.log("언어 키 선택:", key);
|
|
|
|
|
|
setSelectedKey(key);
|
|
|
|
|
|
fetchLangTexts(key.keyId);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 디버깅용 useEffect
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectedKey) {
|
|
|
|
|
|
console.log("선택된 키 변경:", selectedKey);
|
|
|
|
|
|
console.log("언어 목록:", languages);
|
|
|
|
|
|
console.log("편집 텍스트:", editingTexts);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [selectedKey, languages, editingTexts]);
|
|
|
|
|
|
|
|
|
|
|
|
// 텍스트 변경 처리
|
|
|
|
|
|
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 {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
// 백엔드가 기대하는 형식으로 데이터 변환
|
|
|
|
|
|
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);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const data = response.data;
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
alert("저장되었습니다.");
|
|
|
|
|
|
// 저장 후 다시 조회
|
|
|
|
|
|
fetchLangTexts(selectedKey.keyId);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("텍스트 저장 실패:", error);
|
|
|
|
|
|
alert("저장에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 언어 키 추가/수정 모달 열기
|
|
|
|
|
|
const handleAddKey = () => {
|
|
|
|
|
|
setEditingKey(null); // 새 키 추가는 null로 설정
|
|
|
|
|
|
setIsModalOpen(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 언어 추가/수정 모달 열기
|
|
|
|
|
|
const handleAddLanguage = () => {
|
|
|
|
|
|
setEditingLanguage(null);
|
|
|
|
|
|
setIsLanguageModalOpen(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 언어 수정
|
|
|
|
|
|
const handleEditLanguage = (language: Language) => {
|
|
|
|
|
|
setEditingLanguage(language);
|
|
|
|
|
|
setIsLanguageModalOpen(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 언어 저장 (추가/수정)
|
|
|
|
|
|
const handleSaveLanguage = async (languageData: any) => {
|
|
|
|
|
|
try {
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const requestData = {
|
|
|
|
|
|
...languageData,
|
|
|
|
|
|
createdBy: user?.userId || "admin",
|
|
|
|
|
|
updatedBy: user?.userId || "admin",
|
|
|
|
|
|
};
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
2025-08-21 14:47:07 +09:00
|
|
|
|
let response;
|
|
|
|
|
|
if (editingLanguage) {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
response = await apiClient.put(`/multilang/languages/${editingLanguage.langCode}`, requestData);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
} else {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
response = await apiClient.post("/multilang/languages", requestData);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const result = response.data;
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
alert(editingLanguage ? "언어가 수정되었습니다." : "언어가 추가되었습니다.");
|
|
|
|
|
|
setIsLanguageModalOpen(false);
|
|
|
|
|
|
fetchLanguages(); // 언어 목록 새로고침
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert(`오류: ${result.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("언어 저장 중 오류:", 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) =>
|
2025-08-29 10:44:55 +09:00
|
|
|
|
apiClient.delete(`/multilang/languages/${langCode}`),
|
2025-08-21 09:41:46 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const responses = await Promise.all(deletePromises);
|
2025-08-25 15:12:31 +09:00
|
|
|
|
const failedDeletes = responses.filter((response) => !response.data.success);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
if (failedDeletes.length === 0) {
|
|
|
|
|
|
alert("선택된 언어가 삭제되었습니다.");
|
|
|
|
|
|
setSelectedLanguages(new Set());
|
|
|
|
|
|
fetchLanguages(); // 언어 목록 새로고침
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert(`${failedDeletes.length}개의 언어 삭제에 실패했습니다.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("언어 삭제 중 오류:", 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",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-21 14:47:07 +09:00
|
|
|
|
let response;
|
|
|
|
|
|
if (editingKey) {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
response = await apiClient.put(`/multilang/keys/${editingKey.keyId}`, requestData);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
} else {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
response = await apiClient.post("/multilang/keys", requestData);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
}
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const data = response.data;
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
2025-08-21 14:47:07 +09:00
|
|
|
|
if (data.success) {
|
2025-08-21 09:41:46 +09:00
|
|
|
|
alert(editingKey ? "언어 키가 수정되었습니다." : "언어 키가 추가되었습니다.");
|
|
|
|
|
|
fetchLangKeys(); // 목록 새로고침
|
|
|
|
|
|
setIsModalOpen(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 중복 체크 오류 메시지 처리
|
|
|
|
|
|
if (data.message && data.message.includes("이미 존재하는 언어키")) {
|
|
|
|
|
|
alert(data.message);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert(data.message || "언어 키 저장에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("언어 키 저장 실패:", 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 {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
const response = await apiClient.put(`/multilang/keys/${keyId}/toggle`);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const data = response.data;
|
2025-08-21 09:41:46 +09:00
|
|
|
|
if (data.success) {
|
|
|
|
|
|
alert(`키가 ${data.data}되었습니다.`);
|
|
|
|
|
|
fetchLangKeys();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert("상태 변경 중 오류가 발생했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("키 상태 토글 실패:", error);
|
|
|
|
|
|
alert("키 상태 변경 중 오류가 발생했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 언어 상태 토글
|
|
|
|
|
|
const handleToggleLanguageStatus = async (langCode: string) => {
|
|
|
|
|
|
try {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
const response = await apiClient.put(`/multilang/languages/${langCode}/toggle`);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const data = response.data;
|
2025-08-21 09:41:46 +09:00
|
|
|
|
if (data.success) {
|
|
|
|
|
|
alert(`언어가 ${data.data}되었습니다.`);
|
|
|
|
|
|
fetchLanguages();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert("언어 상태 변경 중 오류가 발생했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("언어 상태 토글 실패:", 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 {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
const deletePromises = Array.from(selectedKeys).map((keyId) => apiClient.delete(`/multilang/keys/${keyId}`));
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
const responses = await Promise.all(deletePromises);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const allSuccess = responses.every((response) => response.data.success);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
|
|
if (allSuccess) {
|
|
|
|
|
|
alert(`${selectedKeys.size}개의 언어 키가 영구적으로 삭제되었습니다.`);
|
|
|
|
|
|
setSelectedKeys(new Set());
|
|
|
|
|
|
fetchLangKeys(); // 목록 새로고침
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 키가 삭제된 경우 편집 영역 닫기
|
|
|
|
|
|
if (selectedKey && selectedKeys.has(selectedKey.keyId)) {
|
|
|
|
|
|
handleCancel();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert("일부 키 삭제에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("선택된 키 삭제 실패:", error);
|
|
|
|
|
|
alert("선택된 키 삭제에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 개별 키 삭제 (기존 함수 유지)
|
|
|
|
|
|
const handleDeleteKey = async (keyId: number) => {
|
|
|
|
|
|
if (!confirm("정말로 이 언어 키를 영구적으로 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다.")) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-25 15:12:31 +09:00
|
|
|
|
const response = await apiClient.delete(`/multilang/keys/${keyId}`);
|
2025-08-21 14:47:07 +09:00
|
|
|
|
const data = response.data;
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
alert("언어 키가 영구적으로 삭제되었습니다.");
|
|
|
|
|
|
fetchLangKeys(); // 목록 새로고침
|
|
|
|
|
|
if (selectedKey && selectedKey.keyId === keyId) {
|
|
|
|
|
|
handleCancel(); // 선택된 키가 삭제된 경우 편집 영역 닫기
|
2025-08-21 09:41:46 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("언어 키 삭제 실패:", error);
|
|
|
|
|
|
alert("언어 키 삭제에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 취소 처리
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
setSelectedKey(null);
|
|
|
|
|
|
setLangTexts([]);
|
|
|
|
|
|
setEditingTexts([]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const initializeData = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
await Promise.all([fetchCompanies(), fetchLanguages(), fetchLangKeys()]);
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
initializeData();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 검색 관련 useEffect 제거 - 실시간 필터링만 사용
|
|
|
|
|
|
|
|
|
|
|
|
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="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-blue-500 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-blue-500 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-gray-600">총 {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-10">
|
|
|
|
|
|
{/* 좌측: 언어 키 목록 (7/10) */}
|
|
|
|
|
|
<Card className="lg:col-span-7">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<CardTitle>언어 키 목록</CardTitle>
|
|
|
|
|
|
<div className="flex space-x-2">
|
|
|
|
|
|
<Button variant="destructive" onClick={handleDeleteSelectedKeys} disabled={selectedKeys.size === 0}>
|
|
|
|
|
|
선택 삭제 ({selectedKeys.size})
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button onClick={handleAddKey}>새 키 추가</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
{/* 검색 필터 영역 */}
|
|
|
|
|
|
<div className="mb-2 grid grid-cols-1 gap-2 md:grid-cols-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="company">회사</Label>
|
|
|
|
|
|
<Select value={selectedCompany} onValueChange={setSelectedCompany}>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<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">검색</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="키명, 설명, 메뉴, 회사로 검색..."
|
|
|
|
|
|
value={searchText}
|
|
|
|
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-end">
|
|
|
|
|
|
<div className="text-sm text-gray-600">검색 결과: {getFilteredLangKeys().length}건</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 테이블 영역 */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="mb-2 text-sm text-gray-600">전체: {getFilteredLangKeys().length}건</div>
|
|
|
|
|
|
<DataTable
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
data={getFilteredLangKeys()}
|
|
|
|
|
|
searchable={false}
|
|
|
|
|
|
onRowClick={handleKeySelect}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 우측: 선택된 키의 다국어 관리 (3/10) */}
|
|
|
|
|
|
<Card className="lg:col-span-3">
|
|
|
|
|
|
<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}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|