458 lines
17 KiB
TypeScript
458 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useMemo } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { DataTable } from "@/components/common/DataTable";
|
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
interface Language {
|
|
langCode: string;
|
|
langName: string;
|
|
langNative: string;
|
|
isActive: string;
|
|
}
|
|
|
|
interface LangKey {
|
|
keyId: number;
|
|
companyCode: string;
|
|
menuCode: string;
|
|
langKey: string;
|
|
keyType: 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 [selectedCompany, setSelectedCompany] = useState("");
|
|
const [selectedMenu, setSelectedMenu] = useState("");
|
|
const [selectedKeyType, setSelectedKeyType] = useState("");
|
|
|
|
// 검색 관련 상태 추가
|
|
const [searchText, setSearchText] = useState("");
|
|
const [companySearchText, setCompanySearchText] = useState("");
|
|
const [isCompanyDropdownOpen, setIsCompanyDropdownOpen] = useState(false);
|
|
|
|
const [companies] = useState([
|
|
{ code: "ILSHIN", name: "일신공업" },
|
|
{ code: "HUTECH", name: "후테크" },
|
|
{ code: "DAIN", name: "다인" },
|
|
]);
|
|
const [menus] = useState([
|
|
{ code: "DASHBOARD", name: "대시보드" },
|
|
{ code: "USER_MANAGEMENT", name: "사용자 관리" },
|
|
{ code: "MENU_MANAGEMENT", name: "메뉴 관리" },
|
|
{ code: "MULTI_LANG", name: "다국어 관리" },
|
|
]);
|
|
const [keyTypes] = useState([
|
|
{ code: "TEXT", name: "텍스트" },
|
|
{ code: "TITLE", name: "제목" },
|
|
{ code: "LABEL", name: "라벨" },
|
|
{ code: "BUTTON", name: "버튼" },
|
|
{ code: "MESSAGE", name: "메시지" },
|
|
{ code: "ERROR", name: "오류" },
|
|
]);
|
|
|
|
// 드롭다운 외부 클릭 시 닫기
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
const target = event.target as Element;
|
|
if (!target.closest(".company-dropdown")) {
|
|
setIsCompanyDropdownOpen(false);
|
|
setCompanySearchText("");
|
|
}
|
|
};
|
|
|
|
if (isCompanyDropdownOpen) {
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
};
|
|
}, [isCompanyDropdownOpen]);
|
|
|
|
// 언어 목록 조회
|
|
const fetchLanguages = async () => {
|
|
try {
|
|
const response = await apiClient.get("/api/admin/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("/api/admin/multilang/keys");
|
|
const data = response.data;
|
|
if (data.success) {
|
|
setLangKeys(data.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("다국어 키 목록 조회 실패:", error);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const initializeData = async () => {
|
|
setLoading(true);
|
|
await Promise.all([fetchLanguages(), fetchLangKeys()]);
|
|
setLoading(false);
|
|
};
|
|
initializeData();
|
|
}, []);
|
|
|
|
// 필터링된 데이터 계산 - 메뉴관리와 동일한 방식
|
|
const getFilteredLangKeys = () => {
|
|
let filteredKeys = langKeys;
|
|
|
|
// 회사 필터링
|
|
if (selectedCompany) {
|
|
filteredKeys = filteredKeys.filter((key) => key.companyCode === selectedCompany);
|
|
}
|
|
|
|
// 메뉴 필터링
|
|
if (selectedMenu) {
|
|
filteredKeys = filteredKeys.filter((key) => key.menuCode === selectedMenu);
|
|
}
|
|
|
|
// 키 타입 필터링
|
|
if (selectedKeyType) {
|
|
filteredKeys = filteredKeys.filter((key) => key.keyType === selectedKeyType);
|
|
}
|
|
|
|
// 텍스트 검색 필터링
|
|
if (searchText.trim()) {
|
|
const searchLower = searchText.toLowerCase();
|
|
filteredKeys = filteredKeys.filter((key) => {
|
|
const langKey = (key.langKey || "").toLowerCase();
|
|
const description = (key.description || "").toLowerCase();
|
|
const companyName = companies.find((c) => c.code === key.companyCode)?.name?.toLowerCase() || "";
|
|
const menuName = menus.find((m) => m.code === key.menuCode)?.name?.toLowerCase() || "";
|
|
const keyTypeName = keyTypes.find((t) => t.code === key.keyType)?.name?.toLowerCase() || "";
|
|
|
|
return (
|
|
langKey.includes(searchLower) ||
|
|
description.includes(searchLower) ||
|
|
companyName.includes(searchLower) ||
|
|
menuName.includes(searchLower) ||
|
|
keyTypeName.includes(searchLower)
|
|
);
|
|
});
|
|
}
|
|
|
|
return filteredKeys;
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
accessorKey: "companyCode",
|
|
header: "회사",
|
|
cell: ({ row }: any) => {
|
|
const company = companies.find((c) => c.code === row.original.companyCode);
|
|
return company ? company.name : row.original.companyCode;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "menuCode",
|
|
header: "메뉴",
|
|
cell: ({ row }: any) => {
|
|
const menu = menus.find((m) => m.code === row.original.menuCode);
|
|
return menu ? menu.name : row.original.menuCode;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "langKey",
|
|
header: "언어 키",
|
|
},
|
|
{
|
|
accessorKey: "keyType",
|
|
header: "타입",
|
|
cell: ({ row }: any) => {
|
|
const type = keyTypes.find((t) => t.code === row.original.keyType);
|
|
return type ? type.name : row.original.keyType;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "description",
|
|
header: "설명",
|
|
},
|
|
{
|
|
accessorKey: "isActive",
|
|
header: "상태",
|
|
cell: ({ row }: any) => (
|
|
<Badge variant={row.original.isActive === "Y" ? "default" : "secondary"}>
|
|
{row.original.isActive === "Y" ? "활성" : "비활성"}
|
|
</Badge>
|
|
),
|
|
},
|
|
];
|
|
|
|
if (loading) {
|
|
return <LoadingSpinner />;
|
|
}
|
|
|
|
const filteredLangKeys = getFilteredLangKeys();
|
|
|
|
return (
|
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-3xl font-bold">다국어 관리</h1>
|
|
<Button>새 키 추가</Button>
|
|
</div>
|
|
|
|
<Tabs defaultValue="keys" className="space-y-4">
|
|
<TabsList>
|
|
<TabsTrigger value="keys">다국어 키 관리</TabsTrigger>
|
|
<TabsTrigger value="languages">언어 관리</TabsTrigger>
|
|
<TabsTrigger value="menus">메뉴 관리</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="keys" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>다국어 키 목록</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* 검색 및 필터 영역 */}
|
|
<div className="mb-4">
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
<div>
|
|
<Label htmlFor="company">회사</Label>
|
|
<div className="company-dropdown relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsCompanyDropdownOpen(!isCompanyDropdownOpen)}
|
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<span className={selectedCompany === "" ? "text-muted-foreground" : ""}>
|
|
{selectedCompany === ""
|
|
? "전체 회사"
|
|
: companies.find((c) => c.code === selectedCompany)?.name || "전체 회사"}
|
|
</span>
|
|
<svg
|
|
className={`h-4 w-4 transition-transform ${isCompanyDropdownOpen ? "rotate-180" : ""}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{isCompanyDropdownOpen && (
|
|
<div className="bg-popover text-popover-foreground absolute top-full left-0 z-50 mt-1 w-full rounded-md border shadow-md">
|
|
{/* 검색 입력 */}
|
|
<div className="border-b p-2">
|
|
<Input
|
|
placeholder="회사 검색..."
|
|
value={companySearchText}
|
|
onChange={(e) => setCompanySearchText(e.target.value)}
|
|
className="h-8 text-sm"
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
</div>
|
|
|
|
{/* 회사 목록 */}
|
|
<div className="max-h-48 overflow-y-auto">
|
|
<div
|
|
className="hover:bg-accent hover:text-accent-foreground flex cursor-pointer items-center px-2 py-1.5 text-sm"
|
|
onClick={() => {
|
|
setSelectedCompany("");
|
|
setIsCompanyDropdownOpen(false);
|
|
setCompanySearchText("");
|
|
}}
|
|
>
|
|
전체 회사
|
|
</div>
|
|
|
|
{companies
|
|
.filter(
|
|
(company) =>
|
|
company.name.toLowerCase().includes(companySearchText.toLowerCase()) ||
|
|
company.code.toLowerCase().includes(companySearchText.toLowerCase()),
|
|
)
|
|
.map((company) => (
|
|
<div
|
|
key={company.code}
|
|
className="hover:bg-accent hover:text-accent-foreground flex cursor-pointer items-center px-2 py-1.5 text-sm"
|
|
onClick={() => {
|
|
setSelectedCompany(company.code);
|
|
setIsCompanyDropdownOpen(false);
|
|
setCompanySearchText("");
|
|
}}
|
|
>
|
|
{company.name}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="menu">메뉴</Label>
|
|
{/* Radix UI Select v2.x: 빈 문자열 value="" 금지 → "__all__" 사용 */}
|
|
<Select
|
|
value={selectedMenu || "__all__"}
|
|
onValueChange={(value) => setSelectedMenu(value === "__all__" ? "" : value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="전체 메뉴" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__all__">전체 메뉴</SelectItem>
|
|
{menus.map((menu) => (
|
|
<SelectItem key={menu.code} value={menu.code}>
|
|
{menu.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="keyType">키 타입</Label>
|
|
{/* Radix UI Select v2.x: 빈 문자열 value="" 금지 → "__all__" 사용 */}
|
|
<Select
|
|
value={selectedKeyType || "__all__"}
|
|
onValueChange={(value) => setSelectedKeyType(value === "__all__" ? "" : value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="전체 타입" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__all__">전체 타입</SelectItem>
|
|
{keyTypes.map((type) => (
|
|
<SelectItem key={type.code} value={type.code}>
|
|
{type.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="search">검색</Label>
|
|
<Input
|
|
placeholder="언어 키, 설명, 회사, 메뉴, 타입으로 검색..."
|
|
value={searchText}
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
<div className="flex items-end">
|
|
<Button
|
|
onClick={() => {
|
|
setSearchText("");
|
|
setSelectedCompany("");
|
|
setSelectedMenu("");
|
|
setSelectedKeyType("");
|
|
setCompanySearchText("");
|
|
}}
|
|
variant="outline"
|
|
className="w-full"
|
|
>
|
|
필터 초기화
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex items-end">
|
|
<div className="text-sm text-gray-600">검색 결과: {filteredLangKeys.length}건</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div className="text-sm text-gray-600">전체: {filteredLangKeys.length}건</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
columns={columns}
|
|
data={filteredLangKeys}
|
|
searchable={false} // 커스텀 검색을 사용하므로 false
|
|
onRowClick={(row) => {
|
|
// 키 상세 정보 모달 열기
|
|
console.log("선택된 키:", row);
|
|
}}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="languages" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>언어 관리</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
{languages.map((lang) => (
|
|
<div key={lang.langCode} className="rounded-lg border p-4">
|
|
<div className="font-semibold">{lang.langName}</div>
|
|
<div className="text-sm text-gray-600">{lang.langNative}</div>
|
|
<Badge variant={lang.isActive === "Y" ? "default" : "secondary"} className="mt-2">
|
|
{lang.isActive === "Y" ? "활성" : "비활성"}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="menus" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>메뉴 관리</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
{companies.map((company) => (
|
|
<div key={company.code} className="rounded-lg border p-4">
|
|
<h3 className="mb-2 font-semibold">{company.name}</h3>
|
|
<div className="space-y-1">
|
|
{menus.map((menu) => (
|
|
<div key={menu.code} className="text-sm text-gray-600">
|
|
{menu.name}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|