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";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
|
|
|
|
|
|
import { ComponentDefinition, ComponentCategory } from "@/types/component";
|
2025-10-02 14:34:15 +09:00
|
|
|
|
import { Search, Package, Grid, Layers, Palette, Zap, MousePointer } 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("");
|
2025-10-02 14:34:15 +09:00
|
|
|
|
const [selectedCategory, setSelectedCategory] = useState<"all" | "display" | "action" | "layout" | "utility">("all");
|
2025-09-11 18:38:28 +09:00
|
|
|
|
|
|
|
|
|
|
// 레지스트리에서 모든 컴포넌트 조회
|
|
|
|
|
|
const allComponents = useMemo(() => {
|
2025-09-25 18:54:25 +09:00
|
|
|
|
const components = ComponentRegistry.getAllComponents();
|
|
|
|
|
|
|
|
|
|
|
|
// 수동으로 table-list 컴포넌트 추가 (임시)
|
|
|
|
|
|
const hasTableList = components.some(c => c.id === 'table-list');
|
|
|
|
|
|
if (!hasTableList) {
|
|
|
|
|
|
components.push({
|
2025-10-02 14:34:15 +09:00
|
|
|
|
id: 'table-list',
|
|
|
|
|
|
name: '데이터 테이블 v2',
|
|
|
|
|
|
description: '검색, 필터링, 페이징, CRUD 기능이 포함된 완전한 데이터 테이블 컴포넌트',
|
|
|
|
|
|
category: 'display',
|
|
|
|
|
|
tags: ['table', 'data', 'crud'],
|
|
|
|
|
|
defaultSize: { width: 1000, height: 680 },
|
|
|
|
|
|
} as ComponentDefinition);
|
2025-09-25 18:54:25 +09:00
|
|
|
|
}
|
2025-10-02 14:34:15 +09:00
|
|
|
|
|
2025-09-25 18:54:25 +09:00
|
|
|
|
return components;
|
2025-09-11 18:38:28 +09:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
// 카테고리별 컴포넌트 그룹화
|
2025-09-11 18:38:28 +09:00
|
|
|
|
const componentsByCategory = useMemo(() => {
|
2025-10-02 14:34:15 +09:00
|
|
|
|
return {
|
|
|
|
|
|
all: allComponents,
|
|
|
|
|
|
display: allComponents.filter((c) => c.category === "display"),
|
|
|
|
|
|
action: allComponents.filter((c) => c.category === "action"),
|
|
|
|
|
|
layout: allComponents.filter((c) => c.category === "layout"),
|
|
|
|
|
|
utility: allComponents.filter((c) => c.category === "utility"),
|
2025-09-11 18:38:28 +09:00
|
|
|
|
};
|
|
|
|
|
|
}, [allComponents]);
|
2025-09-10 14:09:32 +09:00
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
// 검색 및 필터링된 컴포넌트
|
2025-09-09 17:42:23 +09:00
|
|
|
|
const filteredComponents = useMemo(() => {
|
2025-10-02 14:34:15 +09:00
|
|
|
|
let components = selectedCategory === "all" ? componentsByCategory.all : componentsByCategory[selectedCategory as keyof typeof componentsByCategory];
|
2025-09-11 18:38:28 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
if (searchQuery) {
|
2025-09-11 18:38:28 +09:00
|
|
|
|
const query = searchQuery.toLowerCase();
|
|
|
|
|
|
components = components.filter(
|
2025-10-02 14:34:15 +09:00
|
|
|
|
(component: ComponentDefinition) =>
|
2025-09-11 18:38:28 +09:00
|
|
|
|
component.name.toLowerCase().includes(query) ||
|
|
|
|
|
|
component.description.toLowerCase().includes(query) ||
|
2025-10-02 14:34:15 +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;
|
|
|
|
|
|
}, [componentsByCategory, selectedCategory, searchQuery]);
|
|
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
// 카테고리 아이콘 매핑
|
|
|
|
|
|
const getCategoryIcon = (category: ComponentCategory) => {
|
2025-09-11 18:38:28 +09:00
|
|
|
|
switch (category) {
|
|
|
|
|
|
case "display":
|
2025-10-02 14:34:15 +09:00
|
|
|
|
return <Palette className="h-6 w-6" />;
|
2025-09-11 18:38:28 +09:00
|
|
|
|
case "action":
|
2025-10-02 14:34:15 +09:00
|
|
|
|
return <Zap className="h-6 w-6" />;
|
2025-09-11 18:38:28 +09:00
|
|
|
|
case "layout":
|
2025-10-02 14:34:15 +09:00
|
|
|
|
return <Layers className="h-6 w-6" />;
|
2025-09-11 18:38:28 +09:00
|
|
|
|
case "utility":
|
2025-10-02 14:34:15 +09:00
|
|
|
|
return <Package className="h-6 w-6" />;
|
2025-09-11 18:38:28 +09:00
|
|
|
|
default:
|
2025-10-02 14:34:15 +09:00
|
|
|
|
return <Grid className="h-6 w-6" />;
|
2025-09-11 18:38:28 +09:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +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
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className={`flex h-full flex-col bg-slate-50 p-6 border-r border-gray-200/60 shadow-sm ${className}`}>
|
|
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-1">컴포넌트</h2>
|
|
|
|
|
|
<p className="text-sm text-gray-500">7개의 사용 가능한 컴포넌트</p>
|
|
|
|
|
|
</div>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
{/* 검색 */}
|
|
|
|
|
|
<div className="space-y-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-09-29 17:21:47 +09:00
|
|
|
|
className="pl-10 border-0 bg-white/80 backdrop-blur-sm shadow-sm focus:bg-white transition-colors"
|
2025-09-09 17:42:23 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-11 18:38:28 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
{/* 카테고리 필터 */}
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={selectedCategory === "all" ? "default" : "outline"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => setSelectedCategory("all")}
|
|
|
|
|
|
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Package className="h-3 w-3" />
|
|
|
|
|
|
<span>전체</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={selectedCategory === "display" ? "default" : "outline"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => setSelectedCategory("display")}
|
|
|
|
|
|
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Palette className="h-3 w-3" />
|
|
|
|
|
|
<span>표시</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={selectedCategory === "action" ? "default" : "outline"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => setSelectedCategory("action")}
|
|
|
|
|
|
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Zap className="h-3 w-3" />
|
|
|
|
|
|
<span>액션</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={selectedCategory === "layout" ? "default" : "outline"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => setSelectedCategory("layout")}
|
|
|
|
|
|
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Layers className="h-3 w-3" />
|
|
|
|
|
|
<span>레이아웃</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={selectedCategory === "utility" ? "default" : "outline"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => setSelectedCategory("utility")}
|
|
|
|
|
|
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Package className="h-3 w-3" />
|
|
|
|
|
|
<span>유틸리티</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-29 17:21:47 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
{/* 컴포넌트 목록 */}
|
|
|
|
|
|
<div className="flex-1 space-y-3 overflow-y-auto mt-6">
|
|
|
|
|
|
{filteredComponents.length > 0 ? (
|
|
|
|
|
|
filteredComponents.map((component) => (
|
|
|
|
|
|
<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 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"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-start space-x-4">
|
|
|
|
|
|
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-purple-100 text-purple-700 shadow-md group-hover:shadow-lg group-hover:scale-110 transition-all duration-300">
|
|
|
|
|
|
{getCategoryIcon(component.category)}
|
2025-09-11 18:38:28 +09:00
|
|
|
|
</div>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<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="secondary" className="text-xs bg-purple-50 text-purple-600 border-0 ml-2 px-2 py-1 rounded-full font-medium">
|
|
|
|
|
|
신규
|
|
|
|
|
|
</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">
|
|
|
|
|
|
<div className="flex items-center space-x-2 text-xs text-gray-400">
|
|
|
|
|
|
<span className="bg-purple-100 px-3 py-1 rounded-full font-medium text-purple-700 shadow-sm">
|
|
|
|
|
|
{component.defaultSize.width}×{component.defaultSize.height}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-xs font-medium text-primary capitalize bg-gradient-to-r from-purple-50 to-indigo-50 px-3 py-1 rounded-full border border-primary/20/50">
|
|
|
|
|
|
{component.category}
|
|
|
|
|
|
</span>
|
2025-09-29 17:21:47 +09:00
|
|
|
|
</div>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
</div>
|
2025-09-11 18:38:28 +09:00
|
|
|
|
</div>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
))
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<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-sm font-medium text-muted-foreground">컴포넌트를 찾을 수 없습니다</p>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">검색어나 필터를 조정해보세요</p>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
{/* 도움말 */}
|
|
|
|
|
|
<div className="rounded-xl bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-100/60 p-4 mt-6">
|
|
|
|
|
|
<div className="flex items-start space-x-3">
|
|
|
|
|
|
<MousePointer className="h-4 w-4 text-purple-600 flex-shrink-0 mt-0.5" />
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<p className="text-xs text-gray-700 leading-relaxed">
|
|
|
|
|
|
컴포넌트를 <span className="font-semibold text-purple-700">드래그</span>하여 화면에 추가하세요
|
|
|
|
|
|
</p>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
</div>
|
2025-09-29 17:21:47 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|