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

261 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(() => {
return ComponentRegistry.getAllComponents();
}, []);
// 카테고리별 분류 (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 (
<Card className={className}>
<CardHeader className="pb-3">
<CardTitle className="flex items-center justify-between">
<div className="flex items-center">
<Package className="mr-2 h-5 w-5" />
({componentsByCategory.all.length})
</div>
<Button variant="outline" size="sm" onClick={handleRefresh} title="컴포넌트 새로고침">
<RotateCcw className="h-4 w-4" />
</Button>
</CardTitle>
{/* 검색창 */}
<div className="relative">
<Search className="text-muted-foreground absolute top-2.5 left-2 h-4 w-4" />
<Input
placeholder="컴포넌트 검색..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8"
/>
</div>
</CardHeader>
<CardContent>
<Tabs
value={selectedCategory}
onValueChange={(value) => setSelectedCategory(value as ComponentCategory | "all")}
>
{/* 카테고리 탭 (input 카테고리 제외) */}
<TabsList className="grid w-full grid-cols-3 lg:grid-cols-5">
<TabsTrigger value="all" className="flex items-center">
<Package className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="display" className="flex items-center">
<Palette className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="action" className="flex items-center">
<Zap className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="layout" className="flex items-center">
<Layers className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="utility" className="flex items-center">
<Package className="mr-1 h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 컴포넌트 목록 */}
<div className="mt-4">
<TabsContent value={selectedCategory} className="space-y-2">
{filteredComponents.length > 0 ? (
<div className="grid max-h-96 grid-cols-1 gap-2 overflow-y-auto">
{filteredComponents.map((component) => (
<div
key={component.id}
draggable
onDragStart={(e) => handleDragStart(e, component)}
className="hover:bg-accent flex cursor-grab items-center rounded-lg border p-3 transition-colors active:cursor-grabbing"
title={component.description}
>
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center justify-between">
<h4 className="truncate text-sm font-medium">{component.name}</h4>
<div className="flex items-center space-x-1">
{/* 카테고리 뱃지 */}
<Badge variant="secondary" className="text-xs">
{getCategoryIcon(component.category)}
<span className="ml-1">{component.category}</span>
</Badge>
{/* 새 컴포넌트 뱃지 */}
<Badge variant="default" className="bg-green-500 text-xs">
</Badge>
</div>
</div>
<p className="text-muted-foreground truncate text-xs">{component.description}</p>
{/* 웹타입 및 크기 정보 */}
<div className="text-muted-foreground mt-2 flex items-center justify-between text-xs">
<span>: {component.webType}</span>
<span>
{component.defaultSize.width}×{component.defaultSize.height}
</span>
</div>
{/* 태그 */}
{component.tags && component.tags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{component.tags.slice(0, 3).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{component.tags.length > 3 && (
<Badge variant="outline" className="text-xs">
+{component.tags.length - 3}
</Badge>
)}
</div>
)}
</div>
</div>
))}
</div>
) : (
<div className="text-muted-foreground py-8 text-center">
<Package className="mx-auto mb-3 h-12 w-12 opacity-50" />
<p className="text-sm">
{searchQuery
? `"${searchQuery}"에 대한 검색 결과가 없습니다.`
: "이 카테고리에 컴포넌트가 없습니다."}
</p>
</div>
)}
</TabsContent>
</div>
</Tabs>
{/* 통계 정보 */}
<div className="mt-4 border-t pt-3">
<div className="grid grid-cols-2 gap-4 text-center">
<div>
<div className="text-lg font-bold text-green-600">{filteredComponents.length}</div>
<div className="text-muted-foreground text-xs"> </div>
</div>
<div>
<div className="text-lg font-bold text-blue-600">{allComponents.length}</div>
<div className="text-muted-foreground text-xs"> </div>
</div>
</div>
</div>
{/* 개발 정보 (개발 모드에서만) */}
{process.env.NODE_ENV === "development" && (
<div className="mt-4 border-t pt-3">
<div className="text-muted-foreground space-y-1 text-xs">
<div>🔧 </div>
<div> Hot Reload </div>
<div>🛡 </div>
</div>
</div>
)}
</CardContent>
</Card>
);
}
export default ComponentsPanel;