362 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|