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

225 lines
9.0 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
import { ComponentDefinition, ComponentCategory } from "@/types/component";
import { Search, Package, Grid, Layers, Palette, Zap, MousePointer, Edit3, BarChart3 } from "lucide-react";
interface ComponentsPanelProps {
className?: string;
}
export function ComponentsPanel({ className }: ComponentsPanelProps) {
const [searchQuery, setSearchQuery] = useState("");
// 레지스트리에서 모든 컴포넌트 조회
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(() => {
// 숨길 컴포넌트 ID 목록 (기본 입력 컴포넌트들)
const hiddenInputComponents = ["text-input", "number-input", "date-input", "textarea-basic"];
return {
input: allComponents.filter(
(c) => c.category === ComponentCategory.INPUT && !hiddenInputComponents.includes(c.id),
),
action: allComponents.filter((c) => c.category === ComponentCategory.ACTION),
display: allComponents.filter((c) => c.category === ComponentCategory.DISPLAY),
layout: allComponents.filter((c) => c.category === ComponentCategory.LAYOUT),
};
}, [allComponents]);
// 카테고리별 검색 필터링
const getFilteredComponents = (category: keyof typeof componentsByCategory) => {
let components = componentsByCategory[category];
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;
};
// 카테고리 아이콘 매핑
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";
};
// 컴포넌트 카드 렌더링 함수
const renderComponentCard = (component: ComponentDefinition) => (
<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 p-4 shadow-sm backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:scale-[1.02] hover:border-purple-300/60 hover:bg-white hover:shadow-lg hover:shadow-purple-500/15 active:translate-y-0 active:scale-[0.98] active:cursor-grabbing"
>
<div className="flex items-start space-x-3">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-100 text-purple-700 shadow-md transition-all duration-300 group-hover:scale-110 group-hover:shadow-lg">
{getCategoryIcon(component.category)}
</div>
<div className="min-w-0 flex-1">
<h4 className="mb-1 text-sm leading-tight font-semibold text-gray-900">{component.name}</h4>
<p className="mb-2 line-clamp-2 text-xs leading-relaxed text-gray-500">{component.description}</p>
<div className="flex items-center space-x-2 text-xs text-gray-400">
<span className="rounded-full bg-purple-100 px-2 py-0.5 font-medium text-purple-700">
{component.defaultSize.width}×{component.defaultSize.height}
</span>
</div>
</div>
</div>
</div>
);
// 빈 상태 렌더링
const renderEmptyState = () => (
<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-muted-foreground text-sm font-medium"> </p>
<p className="mt-1 text-xs text-gray-400"> </p>
</div>
</div>
);
return (
<div className={`flex h-full flex-col border-r border-gray-200/60 bg-slate-50 p-6 shadow-sm ${className}`}>
{/* 헤더 */}
<div className="mb-4">
<h2 className="mb-1 text-lg font-semibold text-gray-900"></h2>
<p className="text-sm text-gray-500">{allComponents.length} </p>
</div>
{/* 검색 */}
<div className="mb-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="border-0 bg-white/80 pl-10 shadow-sm backdrop-blur-sm transition-colors focus:bg-white"
/>
</div>
</div>
{/* 카테고리 탭 */}
<Tabs defaultValue="input" className="flex flex-1 flex-col">
<TabsList className="mb-4 grid w-full grid-cols-4 bg-white/80 p-1">
<TabsTrigger value="input" className="flex items-center gap-1 text-xs">
<Edit3 className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="action" className="flex items-center gap-1 text-xs">
<Zap className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="display" className="flex items-center gap-1 text-xs">
<BarChart3 className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="layout" className="flex items-center gap-1 text-xs">
<Layers className="h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 입력 컴포넌트 */}
<TabsContent value="input" className="mt-0 flex-1 space-y-3 overflow-y-auto">
{getFilteredComponents("input").length > 0
? getFilteredComponents("input").map(renderComponentCard)
: renderEmptyState()}
</TabsContent>
{/* 액션 컴포넌트 */}
<TabsContent value="action" className="mt-0 flex-1 space-y-3 overflow-y-auto">
{getFilteredComponents("action").length > 0
? getFilteredComponents("action").map(renderComponentCard)
: renderEmptyState()}
</TabsContent>
{/* 표시 컴포넌트 */}
<TabsContent value="display" className="mt-0 flex-1 space-y-3 overflow-y-auto">
{getFilteredComponents("display").length > 0
? getFilteredComponents("display").map(renderComponentCard)
: renderEmptyState()}
</TabsContent>
{/* 레이아웃 컴포넌트 */}
<TabsContent value="layout" className="mt-0 flex-1 space-y-3 overflow-y-auto">
{getFilteredComponents("layout").length > 0
? getFilteredComponents("layout").map(renderComponentCard)
: renderEmptyState()}
</TabsContent>
</Tabs>
{/* 도움말 */}
<div className="mt-4 rounded-xl border border-purple-100/60 bg-gradient-to-r from-purple-50 to-pink-50 p-4">
<div className="flex items-start space-x-3">
<MousePointer className="mt-0.5 h-4 w-4 flex-shrink-0 text-purple-600" />
<div className="flex-1">
<p className="text-xs leading-relaxed text-gray-700">
<span className="font-semibold text-purple-700"></span>
</p>
</div>
</div>
</div>
</div>
);
}