261 lines
10 KiB
TypeScript
261 lines
10 KiB
TypeScript
"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;
|