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

320 lines
14 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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
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, RotateCcw } from "lucide-react";
interface ComponentsPanelProps {
className?: string;
}
export function ComponentsPanel({ className }: ComponentsPanelProps) {
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<ComponentCategory | "all">("all");
// 레지스트리에서 모든 컴포넌트 조회
const allComponents = useMemo(() => {
const components = ComponentRegistry.getAllComponents();
// console.log("🔍 ComponentsPanel - 로드된 컴포넌트:", components.map(c => ({ id: c.id, name: c.name, category: c.category })));
// 수동으로 table-list 컴포넌트 추가 (임시)
const hasTableList = components.some(c => c.id === 'table-list');
if (!hasTableList) {
// console.log("⚠️ table-list 컴포넌트가 없어서 수동 추가");
components.push({
id: "table-list",
name: "테이블 리스트",
nameEng: "TableList Component",
description: "데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트",
category: "display",
webType: "text",
defaultConfig: {},
defaultSize: { width: 800, height: 400 },
icon: "Table",
tags: ["테이블", "데이터", "목록", "그리드"],
version: "1.0.0",
author: "개발팀",
});
}
return components;
}, []);
// 카테고리별 분류 (input 카테고리 제외)
const componentsByCategory = useMemo(() => {
// input 카테고리 컴포넌트들을 제외한 컴포넌트만 필터링
const filteredComponents = allComponents.filter((component) => component.category !== "input");
const categories: Record<ComponentCategory | "all", ComponentDefinition[]> = {
all: filteredComponents, // input 카테고리 제외된 컴포넌트들만 포함
input: [], // 빈 배열로 유지 (사용되지 않음)
display: [],
action: [],
layout: [],
utility: [],
};
filteredComponents.forEach((component) => {
if (categories[component.category]) {
categories[component.category].push(component);
}
});
return categories;
}, [allComponents]);
// 검색 및 필터링된 컴포넌트
const filteredComponents = useMemo(() => {
let components = componentsByCategory[selectedCategory] || [];
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
components = components.filter(
(component) =>
component.name.toLowerCase().includes(query) ||
component.description.toLowerCase().includes(query) ||
component.tags?.some((tag) => tag.toLowerCase().includes(query)),
);
}
return components;
}, [componentsByCategory, selectedCategory, searchQuery]);
// 드래그 시작 핸들러
const handleDragStart = (e: React.DragEvent, component: ComponentDefinition) => {
const dragData = {
type: "component",
component: component,
};
// console.log("🚀 컴포넌트 드래그 시작:", component.name, dragData);
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
e.dataTransfer.effectAllowed = "copy";
};
// 카테고리별 아이콘
const getCategoryIcon = (category: ComponentCategory | "all") => {
switch (category) {
case "input":
return <Grid className="h-4 w-4" />;
case "display":
return <Palette className="h-4 w-4" />;
case "action":
return <Zap className="h-4 w-4" />;
case "layout":
return <Layers className="h-4 w-4" />;
case "utility":
return <Package className="h-4 w-4" />;
default:
return <Package className="h-4 w-4" />;
}
};
// 컴포넌트 새로고침
const handleRefresh = () => {
// Hot Reload 트리거 (개발 모드에서만)
if (process.env.NODE_ENV === "development") {
ComponentRegistry.refreshComponents?.();
}
window.location.reload();
};
return (
<div className={`h-full bg-gradient-to-br from-slate-50 to-purple-50/30 border-r border-gray-200/60 shadow-sm ${className}`}>
<div className="p-6">
{/* 헤더 */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-1"></h2>
<p className="text-sm text-gray-500">{componentsByCategory.all.length} </p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
title="컴포넌트 새로고침"
className="bg-white/60 border-gray-200/60 hover:bg-white hover:border-gray-300"
>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
{/* 검색창 */}
<div className="relative mb-6">
<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>
<div className="px-6">
<Tabs
value={selectedCategory}
onValueChange={(value) => setSelectedCategory(value as ComponentCategory | "all")}
>
{/* 카테고리 탭 (input 카테고리 제외) */}
<TabsList className="grid w-full grid-cols-3 lg:grid-cols-5 bg-white/60 backdrop-blur-sm border-0 p-1">
<TabsTrigger value="all" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Package className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="display" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Palette className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="action" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Zap className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="layout" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Layers className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="utility" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Package className="mr-1 h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 주황색 강조 영역 */}
<div className="mt-4 mb-4 p-4 bg-gradient-to-r from-orange-50 to-amber-50 border border-orange-200 rounded-lg">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
<span className="text-sm font-medium text-orange-800"> </span>
</div>
</div>
{/* 컴포넌트 목록 */}
<div className="mt-6">
<TabsContent value={selectedCategory} className="space-y-3">
{filteredComponents.length > 0 ? (
<div className="grid max-h-96 grid-cols-1 gap-3 overflow-y-auto pr-2">
{filteredComponents.map((component) => (
<div
key={component.id}
draggable
onDragStart={(e) => {
handleDragStart(e, component);
// 드래그 시작 시 시각적 피드백
e.currentTarget.style.opacity = '0.5';
e.currentTarget.style.transform = 'rotate(-3deg) scale(0.95)';
}}
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"
title={component.description}
>
<div className="flex items-start space-x-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 text-white 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="default" className="bg-gradient-to-r from-emerald-500 to-emerald-600 text-white text-xs border-0 ml-2 px-2 py-1 rounded-full font-medium shadow-sm">
</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 mb-2">
<div className="flex items-center space-x-2 text-xs text-gray-400">
<span className="bg-gradient-to-r from-gray-100 to-gray-200 px-3 py-1 rounded-full font-medium text-gray-600">
{component.defaultSize.width}×{component.defaultSize.height}
</span>
</div>
<span className="text-xs font-medium text-purple-600 capitalize bg-gradient-to-r from-purple-50 to-indigo-50 px-3 py-1 rounded-full border border-purple-200/50">
{component.category}
</span>
</div>
{/* 태그 */}
{component.tags && component.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{component.tags.slice(0, 2).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs bg-gradient-to-r from-gray-50 to-gray-100 text-gray-600 border-gray-200/50 rounded-full px-2 py-1">
{tag}
</Badge>
))}
{component.tags.length > 2 && (
<Badge variant="outline" className="text-xs bg-gradient-to-r from-gray-50 to-gray-100 text-gray-600 border-gray-200/50 rounded-full px-2 py-1">
+{component.tags.length - 2}
</Badge>
)}
</div>
)}
</div>
</div>
</div>
))}
</div>
) : (
<div className="py-12 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-gray-600">
{searchQuery
? `"${searchQuery}"에 대한 컴포넌트를 찾을 수 없습니다`
: "이 카테고리에 컴포넌트가 없습니다"}
</p>
<p className="text-xs text-gray-400 mt-1"> </p>
</div>
</div>
)}
</TabsContent>
</div>
</Tabs>
{/* 통계 정보 */}
<div className="mt-6 rounded-xl bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-100/60 p-4">
<div className="grid grid-cols-2 gap-4 text-center">
<div>
<div className="text-lg font-bold text-emerald-600">{filteredComponents.length}</div>
<div className="text-xs text-gray-500"></div>
</div>
<div>
<div className="text-lg font-bold text-purple-600">{allComponents.length}</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
</div>
{/* 개발 정보 (개발 모드에서만) */}
{process.env.NODE_ENV === "development" && (
<div className="mt-4 rounded-xl bg-gradient-to-r from-gray-50 to-slate-50 border border-gray-100/60 p-4">
<div className="space-y-1 text-xs text-gray-600">
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
<span> </span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
<span>Hot Reload </span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-purple-500 rounded-full"></span>
<span> </span>
</div>
</div>
</div>
)}
</div>
</div>
);
}
export default ComponentsPanel;