860 lines
29 KiB
TypeScript
860 lines
29 KiB
TypeScript
"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";
|
||
import { apiClient } from "@/lib/api/client";
|
||
|
||
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 {
|
||
// console.log("회사 목록 조회 시작");
|
||
const response = await apiClient.get("/admin/companies");
|
||
// console.log("회사 목록 응답 데이터:", response.data);
|
||
|
||
const data = response.data;
|
||
if (data.success) {
|
||
const companyList = data.data.map((company: any) => ({
|
||
code: company.company_code,
|
||
name: company.company_name,
|
||
}));
|
||
// console.log("변환된 회사 목록:", companyList);
|
||
setCompanies(companyList);
|
||
} else {
|
||
// console.error("회사 목록 조회 실패:", data.message);
|
||
}
|
||
} 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 () => {
|
||
try {
|
||
const response = await apiClient.get("/multilang/keys");
|
||
const data = response.data;
|
||
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);
|
||
const response = await apiClient.get(`/multilang/keys/${keyId}/texts`);
|
||
const data = response.data;
|
||
// 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 {
|
||
// 백엔드가 기대하는 형식으로 데이터 변환
|
||
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) {
|
||
// 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 {
|
||
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) {
|
||
// 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) =>
|
||
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) {
|
||
// 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",
|
||
};
|
||
|
||
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) {
|
||
// 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 {
|
||
const response = await apiClient.put(`/multilang/keys/${keyId}/toggle`);
|
||
const data = response.data;
|
||
if (data.success) {
|
||
alert(`키가 ${data.data}되었습니다.`);
|
||
fetchLangKeys();
|
||
} else {
|
||
alert("상태 변경 중 오류가 발생했습니다.");
|
||
}
|
||
} catch (error) {
|
||
// console.error("키 상태 토글 실패:", 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) {
|
||
// 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 {
|
||
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) {
|
||
// console.error("선택된 키 삭제 실패:", 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) {
|
||
// 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-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-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-muted-foreground">검색 결과: {getFilteredLangKeys().length}건</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 테이블 영역 */}
|
||
<div>
|
||
<div className="mb-2 text-sm text-muted-foreground">전체: {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>
|
||
);
|
||
}
|