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

234 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
import { ComponentDefinition, ComponentCategory } from "@/types/component";
import { Search, Package, Grid, Layers, Palette, Zap, MousePointer } from "lucide-react";
interface ComponentsPanelProps {
className?: string;
}
export function ComponentsPanel({ className }: ComponentsPanelProps) {
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<"all" | "display" | "action" | "layout" | "utility">("all");
// 레지스트리에서 모든 컴포넌트 조회
const allComponents = useMemo(() => {
const components = ComponentRegistry.getAllComponents();
// 수동으로 table-list 컴포넌트 추가 (임시)
const hasTableList = components.some(c => c.id === 'table-list');
if (!hasTableList) {
components.push({
id: 'table-list',
name: '데이터 테이블 v2',
description: '검색, 필터링, 페이징, CRUD 기능이 포함된 완전한 데이터 테이블 컴포넌트',
category: 'display',
tags: ['table', 'data', 'crud'],
defaultSize: { width: 1000, height: 680 },
} as ComponentDefinition);
}
return components;
}, []);
// 카테고리별 컴포넌트 그룹화
const componentsByCategory = useMemo(() => {
return {
all: allComponents,
display: allComponents.filter((c) => c.category === "display"),
action: allComponents.filter((c) => c.category === "action"),
layout: allComponents.filter((c) => c.category === "layout"),
utility: allComponents.filter((c) => c.category === "utility"),
};
}, [allComponents]);
// 검색 및 필터링된 컴포넌트
const filteredComponents = useMemo(() => {
let components = selectedCategory === "all" ? componentsByCategory.all : componentsByCategory[selectedCategory as keyof typeof componentsByCategory];
if (searchQuery) {
const query = searchQuery.toLowerCase();
components = components.filter(
(component: ComponentDefinition) =>
component.name.toLowerCase().includes(query) ||
component.description.toLowerCase().includes(query) ||
component.tags?.some((tag: string) => tag.toLowerCase().includes(query))
);
}
return components;
}, [componentsByCategory, selectedCategory, searchQuery]);
// 카테고리 아이콘 매핑
const getCategoryIcon = (category: ComponentCategory) => {
switch (category) {
case "display":
return <Palette className="h-6 w-6" />;
case "action":
return <Zap className="h-6 w-6" />;
case "layout":
return <Layers className="h-6 w-6" />;
case "utility":
return <Package className="h-6 w-6" />;
default:
return <Grid className="h-6 w-6" />;
}
};
// 드래그 시작 핸들러
const handleDragStart = (e: React.DragEvent<HTMLDivElement>, component: ComponentDefinition) => {
const dragData = {
type: "component",
component: component,
};
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
e.dataTransfer.effectAllowed = "copy";
};
return (
<div className={`flex h-full flex-col bg-slate-50 p-6 border-r border-gray-200/60 shadow-sm ${className}`}>
{/* 헤더 */}
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-1"></h2>
<p className="text-sm text-gray-500">7 </p>
</div>
{/* 검색 */}
<div className="space-y-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={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 border-0 bg-white/80 backdrop-blur-sm shadow-sm focus:bg-white transition-colors"
/>
</div>
{/* 카테고리 필터 */}
<div className="flex flex-wrap gap-2">
<Button
variant={selectedCategory === "all" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("all")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Package className="h-3 w-3" />
<span></span>
</Button>
<Button
variant={selectedCategory === "display" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("display")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Palette className="h-3 w-3" />
<span></span>
</Button>
<Button
variant={selectedCategory === "action" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("action")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Zap className="h-3 w-3" />
<span></span>
</Button>
<Button
variant={selectedCategory === "layout" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("layout")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Layers className="h-3 w-3" />
<span></span>
</Button>
<Button
variant={selectedCategory === "utility" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("utility")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Package className="h-3 w-3" />
<span></span>
</Button>
</div>
</div>
{/* 컴포넌트 목록 */}
<div className="flex-1 space-y-3 overflow-y-auto mt-6">
{filteredComponents.length > 0 ? (
filteredComponents.map((component) => (
<div
key={component.id}
draggable
onDragStart={(e) => {
handleDragStart(e, component);
// 드래그 시작 시 시각적 피드백
e.currentTarget.style.opacity = '0.6';
e.currentTarget.style.transform = 'rotate(2deg) scale(0.98)';
}}
onDragEnd={(e) => {
// 드래그 종료 시 원래 상태로 복원
e.currentTarget.style.opacity = '1';
e.currentTarget.style.transform = 'none';
}}
className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 backdrop-blur-sm p-6 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-purple-500/15 hover:scale-[1.02] hover:border-purple-300/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0"
>
<div className="flex items-start space-x-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-purple-100 text-purple-700 shadow-md group-hover:shadow-lg group-hover:scale-110 transition-all duration-300">
{getCategoryIcon(component.category)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900 text-sm leading-tight">{component.name}</h4>
<Badge variant="secondary" className="text-xs bg-purple-50 text-purple-600 border-0 ml-2 px-2 py-1 rounded-full font-medium">
</Badge>
</div>
<p className="text-xs text-gray-500 line-clamp-2 leading-relaxed mb-3">{component.description}</p>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 text-xs text-gray-400">
<span className="bg-purple-100 px-3 py-1 rounded-full font-medium text-purple-700 shadow-sm">
{component.defaultSize.width}×{component.defaultSize.height}
</span>
</div>
<span className="text-xs font-medium text-primary capitalize bg-gradient-to-r from-purple-50 to-indigo-50 px-3 py-1 rounded-full border border-primary/20/50">
{component.category}
</span>
</div>
</div>
</div>
</div>
))
) : (
<div className="flex h-32 items-center justify-center text-center text-gray-500">
<div className="p-8">
<Package className="mx-auto mb-3 h-12 w-12 text-gray-300" />
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-xs text-gray-400 mt-1"> </p>
</div>
</div>
)}
</div>
{/* 도움말 */}
<div className="rounded-xl bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-100/60 p-4 mt-6">
<div className="flex items-start space-x-3">
<MousePointer className="h-4 w-4 text-purple-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-xs text-gray-700 leading-relaxed">
<span className="font-semibold text-purple-700"></span>
</p>
</div>
</div>
</div>
</div>
);
}