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

362 lines
14 KiB
TypeScript

"use client";
import React, { useState, useMemo } from "react";
import { Search, Plus, Edit, Trash2, RefreshCw, Package, Filter } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { AlertModal } from "@/components/common/AlertModal";
import {
useComponents,
useComponentCategories,
useComponentStatistics,
useDeleteComponent,
useCreateComponent,
useUpdateComponent,
} from "@/hooks/admin/useComponents";
import { ComponentFormModal } from "@/components/admin/ComponentFormModal";
// 컴포넌트 카테고리 정의
const COMPONENT_CATEGORIES = [
{ id: "input", name: "입력", color: "blue" },
{ id: "action", name: "액션", color: "green" },
{ id: "display", name: "표시", color: "purple" },
{ id: "layout", name: "레이아웃", color: "orange" },
{ id: "other", name: "기타", color: "gray" },
];
export default function ComponentManagementPage() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [sortBy, setSortBy] = useState<string>("sort_order");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [selectedComponent, setSelectedComponent] = useState<any>(null);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showNewComponentModal, setShowNewComponentModal] = useState(false);
const [showEditComponentModal, setShowEditComponentModal] = useState(false);
// 컴포넌트 데이터 가져오기
const {
data: componentsData,
isLoading: loading,
error,
refetch,
} = useComponents({
category: selectedCategory === "all" ? undefined : selectedCategory,
active: "Y",
search: searchTerm,
sort: sortBy,
order: sortOrder,
});
// 카테고리와 통계 데이터
const { data: categories } = useComponentCategories();
const { data: statistics } = useComponentStatistics();
// 뮤테이션
const deleteComponentMutation = useDeleteComponent();
const createComponentMutation = useCreateComponent();
const updateComponentMutation = useUpdateComponent();
// 컴포넌트 목록 (이미 필터링과 정렬이 적용된 상태)
const components = componentsData?.components || [];
// 카테고리별 통계 (백엔드에서 가져온 데이터 사용)
const categoryStats = useMemo(() => {
if (!statistics?.byCategory) return {};
const stats: Record<string, number> = {};
statistics.byCategory.forEach(({ category, count }) => {
stats[category] = count;
});
return stats;
}, [statistics]);
// 카테고리 이름 및 색상 가져오기
const getCategoryInfo = (categoryId: string) => {
const category = COMPONENT_CATEGORIES.find((cat) => cat.id === categoryId);
return category || { id: "other", name: "기타", color: "gray" };
};
// 삭제 처리
const handleDelete = async () => {
if (!selectedComponent) return;
try {
await deleteComponentMutation.mutateAsync(selectedComponent.component_code);
setShowDeleteModal(false);
setSelectedComponent(null);
} catch (error) {
console.error("컴포넌트 삭제 실패:", error);
}
};
// 컴포넌트 생성 처리
const handleCreate = async (data: any) => {
await createComponentMutation.mutateAsync(data);
setShowNewComponentModal(false);
};
// 컴포넌트 수정 처리
const handleUpdate = async (data: any) => {
if (!selectedComponent) return;
await updateComponentMutation.mutateAsync({
component_code: selectedComponent.component_code,
data,
});
setShowEditComponentModal(false);
setSelectedComponent(null);
};
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<RefreshCw className="mx-auto h-8 w-8 animate-spin text-gray-400" />
<p className="mt-2 text-sm text-gray-500"> ...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<Package className="mx-auto h-8 w-8 text-red-400" />
<p className="mt-2 text-sm text-red-600"> .</p>
<Button variant="outline" size="sm" onClick={() => refetch()} className="mt-4">
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
);
}
return (
<div className="flex h-full flex-col p-6">
{/* 헤더 */}
<div className="mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900"> </h1>
<p className="text-sm text-gray-500"> </p>
</div>
<div className="flex items-center space-x-2">
<Button size="sm" onClick={() => setShowNewComponentModal(true)}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* 카테고리 통계 */}
<div className="mb-6 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">
{COMPONENT_CATEGORIES.map((category) => {
const count = categoryStats[category.id] || 0;
return (
<Card
key={category.id}
className="cursor-pointer hover:shadow-md"
onClick={() => setSelectedCategory(category.id)}
>
<CardContent className="p-4 text-center">
<div className={`mb-2 text-2xl font-bold text-${category.color}-600`}>{count}</div>
<div className="text-sm text-gray-600">{category.name}</div>
</CardContent>
</Card>
);
})}
</div>
{/* 검색 및 필터 */}
<Card className="mb-6">
<CardContent className="p-4">
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:space-y-0 md:space-x-4">
{/* 검색 */}
<div className="relative flex-1">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="컴포넌트 이름, 타입, 설명으로 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9"
/>
</div>
{/* 카테고리 필터 */}
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<SelectTrigger className="w-40">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="카테고리" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{COMPONENT_CATEGORIES.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 정렬 */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-32">
<SelectValue placeholder="정렬" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sort_order"></SelectItem>
<SelectItem value="type_name"></SelectItem>
<SelectItem value="web_type"></SelectItem>
<SelectItem value="category"></SelectItem>
<SelectItem value="updated_date"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm" onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}>
{sortOrder === "asc" ? "↑" : "↓"}
</Button>
<Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* 컴포넌트 목록 테이블 */}
<Card className="flex-1">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span> ({components.length})</span>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead> </TableHead>
<TableHead> </TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-24"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{components.map((component) => {
const categoryInfo = getCategoryInfo(component.category || "other");
return (
<TableRow key={component.component_code}>
<TableCell>
<div>
<div className="font-medium">{component.component_name}</div>
{component.component_name_eng && (
<div className="text-xs text-gray-500">{component.component_name_eng}</div>
)}
</div>
</TableCell>
<TableCell>
<code className="rounded bg-gray-100 px-2 py-1 text-xs">{component.component_code}</code>
</TableCell>
<TableCell>
<Badge
variant="outline"
className={`text-${categoryInfo.color}-600 border-${categoryInfo.color}-200`}
>
{categoryInfo.name}
</Badge>
</TableCell>
<TableCell>
{component.component_config ? (
<code className="text-xs text-blue-600">
{component.component_config.type || component.component_code}
</code>
) : (
<span className="text-xs text-gray-400"></span>
)}
</TableCell>
<TableCell>
<Badge variant={component.is_active === "Y" ? "default" : "secondary"}>
{component.is_active === "Y" ? "활성" : "비활성"}
</Badge>
</TableCell>
<TableCell className="text-xs text-gray-500">
{component.updated_date ? new Date(component.updated_date).toLocaleDateString() : "-"}
</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedComponent(component);
setShowEditComponentModal(true);
}}
>
<Edit className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedComponent(component);
setShowDeleteModal(true);
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* 삭제 확인 모달 */}
<AlertModal
isOpen={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
onConfirm={handleDelete}
type="warning"
title="컴포넌트 삭제"
message={`정말로 "${selectedComponent?.component_name}" 컴포넌트를 삭제하시겠습니까?`}
confirmText="삭제"
/>
{/* 새 컴포넌트 추가 모달 */}
<ComponentFormModal
isOpen={showNewComponentModal}
onClose={() => setShowNewComponentModal(false)}
onSubmit={handleCreate}
mode="create"
/>
{/* 컴포넌트 편집 모달 */}
<ComponentFormModal
isOpen={showEditComponentModal}
onClose={() => {
setShowEditComponentModal(false);
setSelectedComponent(null);
}}
onSubmit={handleUpdate}
initialData={selectedComponent}
mode="edit"
/>
</div>
);
}