ERP-node/frontend/components/screen/panels/ComponentsPanel.tsx

284 lines
11 KiB
TypeScript

"use client";
import React, { useState, useMemo } from "react";
import { Plus, Layers, Search, Filter } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useComponents } from "@/hooks/admin/useComponents";
interface ComponentsPanelProps {
onDragStart: (e: React.DragEvent, component: ComponentItem) => void;
}
interface ComponentItem {
id: string;
name: string;
description: string;
category: string;
componentType: string;
componentConfig: any;
icon: React.ReactNode;
defaultSize: { width: number; height: number };
}
// 컴포넌트 카테고리 정의 (실제 생성된 컴포넌트에 맞게)
const COMPONENT_CATEGORIES = [
{ id: "action", name: "액션", description: "사용자 동작을 처리하는 컴포넌트" },
{ id: "layout", name: "레이아웃", description: "화면 구조를 제공하는 컴포넌트" },
{ id: "data", name: "데이터", description: "데이터를 표시하는 컴포넌트" },
{ id: "navigation", name: "네비게이션", description: "화면 이동을 도와주는 컴포넌트" },
{ id: "feedback", name: "피드백", description: "사용자 피드백을 제공하는 컴포넌트" },
{ id: "input", name: "입력", description: "사용자 입력을 받는 컴포넌트" },
{ id: "other", name: "기타", description: "기타 컴포넌트" },
];
export const ComponentsPanel: React.FC<ComponentsPanelProps> = ({ onDragStart }) => {
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string>("all");
// 데이터베이스에서 컴포넌트 가져오기
const {
data: componentsData,
isLoading: loading,
error,
} = useComponents({
active: "Y",
});
// 컴포넌트를 ComponentItem으로 변환
const componentItems = useMemo(() => {
if (!componentsData?.components) return [];
return componentsData.components.map((component) => ({
id: component.component_code,
name: component.component_name,
description: component.description || `${component.component_name} 컴포넌트`,
category: component.category || "other",
componentType: component.component_config?.type || component.component_code,
componentConfig: component.component_config,
icon: getComponentIcon(component.icon_name || component.component_config?.type),
defaultSize: component.default_size || getDefaultSize(component.component_config?.type),
}));
}, [componentsData]);
// 필터링된 컴포넌트
const filteredComponents = useMemo(() => {
return componentItems.filter((component) => {
const matchesSearch =
component.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
component.description.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = selectedCategory === "all" || component.category === selectedCategory;
return matchesSearch && matchesCategory;
});
}, [componentItems, searchTerm, selectedCategory]);
// 카테고리별 그룹화
const groupedComponents = useMemo(() => {
const groups: Record<string, ComponentItem[]> = {};
COMPONENT_CATEGORIES.forEach((category) => {
groups[category.id] = filteredComponents.filter((component) => component.category === category.id);
});
return groups;
}, [filteredComponents]);
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<Layers className="mx-auto h-8 w-8 animate-pulse 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">
<Layers className="mx-auto h-8 w-8 text-red-400" />
<p className="mt-2 text-sm text-red-500"> </p>
<p className="text-xs text-gray-500">{error.message}</p>
</div>
</div>
);
}
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="flex items-center space-x-2">
<Layers className="h-4 w-4 text-gray-600" />
<h3 className="font-medium text-gray-900"></h3>
<Badge variant="secondary" className="text-xs">
{filteredComponents.length}
</Badge>
</div>
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
{/* 검색 및 필터 */}
<div className="space-y-3 border-b border-gray-200 p-4">
{/* 검색 */}
<div className="relative">
<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="h-8 pl-9 text-xs"
/>
</div>
{/* 카테고리 필터 */}
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{COMPONENT_CATEGORIES.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 컴포넌트 목록 */}
<div className="flex-1 overflow-y-auto">
{selectedCategory === "all" ? (
// 카테고리별 그룹 표시
<div className="space-y-4 p-4">
{COMPONENT_CATEGORIES.map((category) => {
const categoryComponents = groupedComponents[category.id];
if (categoryComponents.length === 0) return null;
return (
<div key={category.id}>
<div className="mb-2 flex items-center space-x-2">
<h4 className="text-sm font-medium text-gray-700">{category.name}</h4>
<Badge variant="outline" className="text-xs">
{categoryComponents.length}
</Badge>
</div>
<p className="mb-3 text-xs text-gray-500">{category.description}</p>
<div className="grid gap-2">
{categoryComponents.map((component) => (
<ComponentCard key={component.id} component={component} onDragStart={onDragStart} />
))}
</div>
</div>
);
})}
</div>
) : (
// 선택된 카테고리만 표시
<div className="p-4">
<div className="grid gap-2">
{filteredComponents.map((component) => (
<ComponentCard key={component.id} component={component} onDragStart={onDragStart} />
))}
</div>
</div>
)}
{filteredComponents.length === 0 && (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<Layers className="mx-auto h-8 w-8 text-gray-300" />
<p className="mt-2 text-sm text-gray-500"> </p>
<p className="text-xs text-gray-400"> </p>
</div>
</div>
)}
</div>
</div>
);
};
// 컴포넌트 카드 컴포넌트
const ComponentCard: React.FC<{
component: ComponentItem;
onDragStart: (e: React.DragEvent, component: ComponentItem) => void;
}> = ({ component, onDragStart }) => {
return (
<div
draggable
onDragStart={(e) => onDragStart(e, component)}
className="group cursor-move rounded-lg border border-gray-200 bg-white p-3 shadow-sm transition-all hover:border-blue-300 hover:shadow-md"
>
<div className="flex items-start space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 group-hover:bg-blue-100">
{component.icon}
</div>
<div className="min-w-0 flex-1">
<h4 className="truncate text-sm font-medium text-gray-900">{component.name}</h4>
<p className="mt-1 line-clamp-2 text-xs text-gray-500">{component.description}</p>
<div className="mt-2 flex items-center space-x-2">
<Badge variant="outline" className="text-xs">
{component.webType}
</Badge>
</div>
</div>
</div>
</div>
);
};
// 웹타입별 아이콘 매핑
function getComponentIcon(webType: string): React.ReactNode {
const iconMap: Record<string, React.ReactNode> = {
text: <span className="text-xs">Aa</span>,
number: <span className="text-xs">123</span>,
date: <span className="text-xs">📅</span>,
select: <span className="text-xs"></span>,
checkbox: <span className="text-xs"></span>,
radio: <span className="text-xs"></span>,
textarea: <span className="text-xs">📝</span>,
file: <span className="text-xs">📎</span>,
button: <span className="text-xs">🔘</span>,
email: <span className="text-xs">📧</span>,
tel: <span className="text-xs">📞</span>,
password: <span className="text-xs">🔒</span>,
code: <span className="text-xs">&lt;&gt;</span>,
entity: <span className="text-xs">🔗</span>,
};
return iconMap[webType] || <span className="text-xs"></span>;
}
// 웹타입별 기본 크기
function getDefaultSize(webType: string): { width: number; height: number } {
const sizeMap: Record<string, { width: number; height: number }> = {
text: { width: 200, height: 36 },
number: { width: 150, height: 36 },
date: { width: 180, height: 36 },
select: { width: 200, height: 36 },
checkbox: { width: 150, height: 36 },
radio: { width: 200, height: 80 },
textarea: { width: 300, height: 100 },
file: { width: 300, height: 120 },
button: { width: 120, height: 36 },
email: { width: 250, height: 36 },
tel: { width: 180, height: 36 },
password: { width: 200, height: 36 },
code: { width: 200, height: 36 },
entity: { width: 200, height: 36 },
};
return sizeMap[webType] || { width: 200, height: 36 };
}
export default ComponentsPanel;