ERP-node/frontend/components/admin/MultiLang.tsx

860 lines
28 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 { 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(`/admin/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-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>
);
}