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

225 lines
9.0 KiB
TypeScript
Raw Normal View History

2025-09-09 17:42:23 +09:00
"use client";
import React, { useState, useMemo } from "react";
import { Input } from "@/components/ui/input";
2025-09-11 18:38:28 +09:00
import { Badge } from "@/components/ui/badge";
2025-10-15 17:25:38 +09:00
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
2025-09-11 18:38:28 +09:00
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
import { ComponentDefinition, ComponentCategory } from "@/types/component";
2025-10-15 17:25:38 +09:00
import { Search, Package, Grid, Layers, Palette, Zap, MousePointer, Edit3, BarChart3 } from "lucide-react";
2025-09-09 17:42:23 +09:00
interface ComponentsPanelProps {
2025-09-11 18:38:28 +09:00
className?: string;
2025-09-09 17:42:23 +09:00
}
2025-09-11 18:38:28 +09:00
export function ComponentsPanel({ className }: ComponentsPanelProps) {
const [searchQuery, setSearchQuery] = useState("");
// 레지스트리에서 모든 컴포넌트 조회
const allComponents = useMemo(() => {
const components = ComponentRegistry.getAllComponents();
2025-10-15 17:25:38 +09:00
// 수동으로 table-list 컴포넌트 추가 (임시)
2025-10-15 17:25:38 +09:00
const hasTableList = components.some((c) => c.id === "table-list");
if (!hasTableList) {
components.push({
2025-10-15 17:25:38 +09:00
id: "table-list",
name: "데이터 테이블 v2",
description: "검색, 필터링, 페이징, CRUD 기능이 포함된 완전한 데이터 테이블 컴포넌트",
category: "display",
tags: ["table", "data", "crud"],
defaultSize: { width: 1000, height: 680 },
} as ComponentDefinition);
}
return components;
2025-09-11 18:38:28 +09:00
}, []);
// 카테고리별 컴포넌트 그룹화
2025-09-11 18:38:28 +09:00
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),
),
2025-10-15 17:25:38 +09:00
action: allComponents.filter((c) => c.category === ComponentCategory.ACTION),
display: allComponents.filter((c) => c.category === ComponentCategory.DISPLAY),
layout: allComponents.filter((c) => c.category === ComponentCategory.LAYOUT),
2025-09-11 18:38:28 +09:00
};
}, [allComponents]);
2025-09-10 14:09:32 +09:00
2025-10-15 17:25:38 +09:00
// 카테고리별 검색 필터링
const getFilteredComponents = (category: keyof typeof componentsByCategory) => {
let components = componentsByCategory[category];
2025-09-11 18:38:28 +09:00
if (searchQuery) {
2025-09-11 18:38:28 +09:00
const query = searchQuery.toLowerCase();
components = components.filter(
(component: ComponentDefinition) =>
2025-09-11 18:38:28 +09:00
component.name.toLowerCase().includes(query) ||
component.description.toLowerCase().includes(query) ||
2025-10-15 17:25:38 +09:00
component.tags?.some((tag: string) => tag.toLowerCase().includes(query)),
2025-09-11 18:38:28 +09:00
);
}
2025-09-09 17:42:23 +09:00
2025-09-11 18:38:28 +09:00
return components;
2025-10-15 17:25:38 +09:00
};
2025-09-11 18:38:28 +09:00
// 카테고리 아이콘 매핑
const getCategoryIcon = (category: ComponentCategory) => {
2025-09-11 18:38:28 +09:00
switch (category) {
case "display":
return <Palette className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
case "action":
return <Zap className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
case "layout":
return <Layers className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
case "utility":
return <Package className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
default:
return <Grid className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
}
};
2025-09-09 17:42:23 +09:00
// 드래그 시작 핸들러
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";
2025-09-11 18:38:28 +09:00
};
2025-09-09 17:42:23 +09:00
2025-10-15 17:25:38 +09:00
// 컴포넌트 카드 렌더링 함수
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>
);
2025-09-09 17:42:23 +09:00
return (
2025-10-15 17:25:38 +09:00
<div className={`flex h-full flex-col border-r border-gray-200/60 bg-slate-50 p-6 shadow-sm ${className}`}>
{/* 헤더 */}
2025-10-15 17:25:38 +09:00
<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>
2025-09-09 17:42:23 +09:00
{/* 검색 */}
2025-10-15 17:25:38 +09:00
<div className="mb-4">
<div className="relative">
2025-09-29 17:21:47 +09:00
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
2025-09-09 17:42:23 +09:00
<Input
placeholder="컴포넌트 검색..."
2025-09-11 18:38:28 +09:00
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
2025-10-15 17:25:38 +09:00
className="border-0 bg-white/80 pl-10 shadow-sm backdrop-blur-sm transition-colors focus:bg-white"
2025-09-09 17:42:23 +09:00
/>
</div>
2025-10-15 17:25:38 +09:00
</div>
2025-09-11 18:38:28 +09:00
2025-10-15 17:25:38 +09:00
{/* 카테고리 탭 */}
<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" />
2025-10-15 17:25:38 +09:00
</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" />
2025-10-15 17:25:38 +09:00
</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>
2025-09-09 17:42:23 +09:00
{/* 도움말 */}
2025-10-15 17:25:38 +09:00
<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">
2025-10-15 17:25:38 +09:00
<MousePointer className="mt-0.5 h-4 w-4 flex-shrink-0 text-purple-600" />
<div className="flex-1">
2025-10-15 17:25:38 +09:00
<p className="text-xs leading-relaxed text-gray-700">
<span className="font-semibold text-purple-700"></span>
</p>
2025-09-09 17:42:23 +09:00
</div>
</div>
2025-09-29 17:21:47 +09:00
</div>
</div>
2025-09-09 17:42:23 +09:00
);
}