2025-09-09 17:42:23 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useMemo } from "react";
|
2025-09-11 18:38:28 +09:00
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
2025-09-09 17:42:23 +09:00
|
|
|
|
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 { 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";
|
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 [selectedCategory, setSelectedCategory] = useState<ComponentCategory | "all">("all");
|
|
|
|
|
|
|
|
|
|
|
|
// 레지스트리에서 모든 컴포넌트 조회
|
|
|
|
|
|
const allComponents = useMemo(() => {
|
2025-09-25 18:54:25 +09:00
|
|
|
|
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;
|
2025-09-11 18:38:28 +09:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
|
// 카테고리별 분류 (input 카테고리 제외)
|
2025-09-11 18:38:28 +09:00
|
|
|
|
const componentsByCategory = useMemo(() => {
|
2025-09-12 16:47:02 +09:00
|
|
|
|
// input 카테고리 컴포넌트들을 제외한 컴포넌트만 필터링
|
|
|
|
|
|
const filteredComponents = allComponents.filter((component) => component.category !== "input");
|
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
const categories: Record<ComponentCategory | "all", ComponentDefinition[]> = {
|
2025-09-12 16:47:02 +09:00
|
|
|
|
all: filteredComponents, // input 카테고리 제외된 컴포넌트들만 포함
|
|
|
|
|
|
input: [], // 빈 배열로 유지 (사용되지 않음)
|
2025-09-11 18:38:28 +09:00
|
|
|
|
display: [],
|
|
|
|
|
|
action: [],
|
|
|
|
|
|
layout: [],
|
|
|
|
|
|
utility: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
|
filteredComponents.forEach((component) => {
|
2025-09-11 18:38:28 +09:00
|
|
|
|
if (categories[component.category]) {
|
|
|
|
|
|
categories[component.category].push(component);
|
|
|
|
|
|
}
|
2025-09-10 15:49:32 +09:00
|
|
|
|
});
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
return categories;
|
|
|
|
|
|
}, [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-09-11 18:38:28 +09:00
|
|
|
|
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)),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
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";
|
|
|
|
|
|
};
|
2025-09-10 15:49:32 +09:00
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
// 카테고리별 아이콘
|
|
|
|
|
|
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" />;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
// 컴포넌트 새로고침
|
|
|
|
|
|
const handleRefresh = () => {
|
|
|
|
|
|
// Hot Reload 트리거 (개발 모드에서만)
|
|
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
|
|
|
|
ComponentRegistry.refreshComponents?.();
|
|
|
|
|
|
}
|
|
|
|
|
|
window.location.reload();
|
|
|
|
|
|
};
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
|
|
|
|
|
return (
|
2025-09-11 18:38:28 +09:00
|
|
|
|
<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" />
|
2025-09-12 16:47:02 +09:00
|
|
|
|
컴포넌트 ({componentsByCategory.all.length})
|
2025-09-11 18:38:28 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
<Button variant="outline" size="sm" onClick={handleRefresh} title="컴포넌트 새로고침">
|
|
|
|
|
|
<RotateCcw className="h-4 w-4" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</CardTitle>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
{/* 검색창 */}
|
2025-09-09 17:42:23 +09:00
|
|
|
|
<div className="relative">
|
2025-09-11 18:38:28 +09:00
|
|
|
|
<Search className="text-muted-foreground absolute top-2.5 left-2 h-4 w-4" />
|
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)}
|
|
|
|
|
|
className="pl-8"
|
2025-09-09 17:42:23 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-11 18:38:28 +09:00
|
|
|
|
</CardHeader>
|
|
|
|
|
|
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Tabs
|
|
|
|
|
|
value={selectedCategory}
|
|
|
|
|
|
onValueChange={(value) => setSelectedCategory(value as ComponentCategory | "all")}
|
|
|
|
|
|
>
|
2025-09-12 16:47:02 +09:00
|
|
|
|
{/* 카테고리 탭 (input 카테고리 제외) */}
|
|
|
|
|
|
<TabsList className="grid w-full grid-cols-3 lg:grid-cols-5">
|
2025-09-11 18:38:28 +09:00
|
|
|
|
<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>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
2025-09-11 18:38:28 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</TabsContent>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
2025-09-11 18:38:28 +09:00
|
|
|
|
</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>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-11 18:38:28 +09:00
|
|
|
|
</div>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
{/* 개발 정보 (개발 모드에서만) */}
|
|
|
|
|
|
{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>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-09-11 18:38:28 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default ComponentsPanel;
|