ERP-node/frontend/app/(main)/multilang/page.tsx

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>
);
}