304 lines
12 KiB
TypeScript
304 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useMemo } from "react";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Badge } from "@/components/ui/badge";
|
||
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, MousePointer, Edit3, BarChart3, Database, Wrench } from "lucide-react";
|
||
import { TableInfo, ColumnInfo } from "@/types/screen";
|
||
import TablesPanel from "./TablesPanel";
|
||
|
||
interface ComponentsPanelProps {
|
||
className?: string;
|
||
// 테이블 관련 props
|
||
tables?: TableInfo[];
|
||
searchTerm?: string;
|
||
onSearchChange?: (value: string) => void;
|
||
onTableDragStart?: (e: React.DragEvent, table: TableInfo, column?: ColumnInfo) => void;
|
||
selectedTableName?: string;
|
||
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합
|
||
}
|
||
|
||
export function ComponentsPanel({
|
||
className,
|
||
tables = [],
|
||
searchTerm = "",
|
||
onSearchChange,
|
||
onTableDragStart,
|
||
selectedTableName,
|
||
placedColumns,
|
||
}: ComponentsPanelProps) {
|
||
const [searchQuery, setSearchQuery] = useState("");
|
||
|
||
// 레지스트리에서 모든 컴포넌트 조회
|
||
const allComponents = useMemo(() => {
|
||
const components = ComponentRegistry.getAllComponents();
|
||
|
||
// 수동으로 table-list 컴포넌트 추가 (임시)
|
||
const hasTableList = components.some((c) => c.id === "table-list");
|
||
if (!hasTableList) {
|
||
components.push({
|
||
id: "table-list",
|
||
name: "데이터 테이블 v2",
|
||
description: "검색, 필터링, 페이징, CRUD 기능이 포함된 완전한 데이터 테이블 컴포넌트",
|
||
category: "display",
|
||
tags: ["table", "data", "crud"],
|
||
defaultSize: { width: 1000, height: 680 },
|
||
} as ComponentDefinition);
|
||
}
|
||
|
||
return components;
|
||
}, []);
|
||
|
||
// 카테고리별 컴포넌트 그룹화
|
||
const componentsByCategory = useMemo(() => {
|
||
// 숨길 컴포넌트 ID 목록 (기본 입력 컴포넌트들)
|
||
const hiddenInputComponents = ["text-input", "number-input", "date-input", "textarea-basic"];
|
||
|
||
return {
|
||
input: allComponents.filter(
|
||
(c) => c.category === ComponentCategory.INPUT && !hiddenInputComponents.includes(c.id),
|
||
),
|
||
action: allComponents.filter((c) => c.category === ComponentCategory.ACTION),
|
||
display: allComponents.filter((c) => c.category === ComponentCategory.DISPLAY),
|
||
layout: allComponents.filter((c) => c.category === ComponentCategory.LAYOUT),
|
||
utility: allComponents.filter((c) => c.category === ComponentCategory.UTILITY), // 🆕 유틸리티 카테고리 추가
|
||
};
|
||
}, [allComponents]);
|
||
|
||
// 카테고리별 검색 필터링
|
||
const getFilteredComponents = (category: keyof typeof componentsByCategory) => {
|
||
let components = componentsByCategory[category];
|
||
|
||
if (searchQuery) {
|
||
const query = searchQuery.toLowerCase();
|
||
components = components.filter(
|
||
(component: ComponentDefinition) =>
|
||
component.name.toLowerCase().includes(query) ||
|
||
component.description.toLowerCase().includes(query) ||
|
||
component.tags?.some((tag: string) => tag.toLowerCase().includes(query)),
|
||
);
|
||
}
|
||
|
||
return components;
|
||
};
|
||
|
||
// 카테고리 아이콘 매핑
|
||
const getCategoryIcon = (category: ComponentCategory) => {
|
||
switch (category) {
|
||
case "display":
|
||
return <Palette className="h-6 w-6" />;
|
||
case "action":
|
||
return <Zap className="h-6 w-6" />;
|
||
case "layout":
|
||
return <Layers className="h-6 w-6" />;
|
||
case "utility":
|
||
return <Package className="h-6 w-6" />;
|
||
default:
|
||
return <Grid className="h-6 w-6" />;
|
||
}
|
||
};
|
||
|
||
// 드래그 시작 핸들러
|
||
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";
|
||
};
|
||
|
||
// 컴포넌트 카드 렌더링 함수
|
||
const renderComponentCard = (component: ComponentDefinition) => (
|
||
<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 bg-card hover:border-primary/50 cursor-grab rounded-lg border p-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:scale-[0.98] active:cursor-grabbing"
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<div className="bg-primary/10 text-primary group-hover:bg-primary/20 flex h-10 w-10 items-center justify-center rounded-md transition-all duration-200">
|
||
{getCategoryIcon(component.category)}
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<h4 className="mb-1 text-xs leading-tight font-semibold">{component.name}</h4>
|
||
<p className="text-muted-foreground mb-1.5 line-clamp-2 text-xs leading-relaxed">{component.description}</p>
|
||
<div className="flex items-center">
|
||
<span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-medium">
|
||
{component.defaultSize.width}×{component.defaultSize.height}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// 빈 상태 렌더링
|
||
const renderEmptyState = () => (
|
||
<div className="flex h-32 items-center justify-center text-center">
|
||
<div className="p-6">
|
||
<Package className="text-muted-foreground/40 mx-auto mb-2 h-10 w-10" />
|
||
<p className="text-muted-foreground text-xs font-medium">컴포넌트를 찾을 수 없습니다</p>
|
||
<p className="text-muted-foreground/60 mt-1 text-xs">검색어를 조정해보세요</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<div className={`bg-background flex h-full flex-col p-4 ${className}`}>
|
||
{/* 헤더 */}
|
||
<div className="mb-3">
|
||
<h2 className="mb-0.5 text-sm font-semibold">컴포넌트</h2>
|
||
<p className="text-muted-foreground text-xs">{allComponents.length}개 사용 가능</p>
|
||
</div>
|
||
|
||
{/* 통합 검색 */}
|
||
<div className="mb-3">
|
||
<div className="relative">
|
||
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
|
||
<Input
|
||
placeholder="컴포넌트, 테이블, 컬럼 검색..."
|
||
value={searchQuery}
|
||
onChange={(e) => {
|
||
const value = e.target.value;
|
||
setSearchQuery(value);
|
||
// 테이블 검색도 함께 업데이트
|
||
if (onSearchChange) {
|
||
onSearchChange(value);
|
||
}
|
||
}}
|
||
className="h-8 pl-8 text-xs" style={{ fontSize: "12px" }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 카테고리 탭 */}
|
||
<Tabs defaultValue="input" className="flex min-h-0 flex-1 flex-col">
|
||
<TabsList className="mb-3 grid h-8 w-full flex-shrink-0 grid-cols-6 gap-1 p-1">
|
||
<TabsTrigger
|
||
value="tables"
|
||
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
|
||
title="테이블"
|
||
>
|
||
<Database className="h-3 w-3" />
|
||
<span className="hidden">테이블</span>
|
||
</TabsTrigger>
|
||
<TabsTrigger value="input" className="flex items-center justify-center gap-0.5 px-0 text-[10px]" title="입력">
|
||
<Edit3 className="h-3 w-3" />
|
||
<span className="hidden">입력</span>
|
||
</TabsTrigger>
|
||
<TabsTrigger
|
||
value="action"
|
||
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
|
||
title="액션"
|
||
>
|
||
<Zap className="h-3 w-3" />
|
||
<span className="hidden">액션</span>
|
||
</TabsTrigger>
|
||
<TabsTrigger
|
||
value="display"
|
||
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
|
||
title="표시"
|
||
>
|
||
<BarChart3 className="h-3 w-3" />
|
||
<span className="hidden">표시</span>
|
||
</TabsTrigger>
|
||
<TabsTrigger
|
||
value="layout"
|
||
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
|
||
title="레이아웃"
|
||
>
|
||
<Layers className="h-3 w-3" />
|
||
<span className="hidden">레이아웃</span>
|
||
</TabsTrigger>
|
||
<TabsTrigger
|
||
value="utility"
|
||
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
|
||
title="유틸리티"
|
||
>
|
||
<Wrench className="h-3 w-3" />
|
||
<span className="hidden">유틸리티</span>
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* 테이블 탭 */}
|
||
<TabsContent value="tables" className="mt-0 flex-1 overflow-y-auto">
|
||
{tables.length > 0 && onTableDragStart ? (
|
||
<TablesPanel
|
||
tables={tables}
|
||
searchTerm={searchTerm}
|
||
onSearchChange={onSearchChange || (() => {})}
|
||
onDragStart={onTableDragStart}
|
||
selectedTableName={selectedTableName}
|
||
placedColumns={placedColumns}
|
||
/>
|
||
) : (
|
||
<div className="flex h-32 items-center justify-center text-center">
|
||
<div className="p-6">
|
||
<Database className="text-muted-foreground/40 mx-auto mb-2 h-10 w-10" />
|
||
<p className="text-muted-foreground text-xs font-medium">테이블이 없습니다</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</TabsContent>
|
||
|
||
{/* 입력 컴포넌트 */}
|
||
<TabsContent value="input" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||
{getFilteredComponents("input").length > 0
|
||
? getFilteredComponents("input").map(renderComponentCard)
|
||
: renderEmptyState()}
|
||
</TabsContent>
|
||
|
||
{/* 액션 컴포넌트 */}
|
||
<TabsContent value="action" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||
{getFilteredComponents("action").length > 0
|
||
? getFilteredComponents("action").map(renderComponentCard)
|
||
: renderEmptyState()}
|
||
</TabsContent>
|
||
|
||
{/* 표시 컴포넌트 */}
|
||
<TabsContent value="display" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||
{getFilteredComponents("display").length > 0
|
||
? getFilteredComponents("display").map(renderComponentCard)
|
||
: renderEmptyState()}
|
||
</TabsContent>
|
||
|
||
{/* 레이아웃 컴포넌트 */}
|
||
<TabsContent value="layout" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||
{getFilteredComponents("layout").length > 0
|
||
? getFilteredComponents("layout").map(renderComponentCard)
|
||
: renderEmptyState()}
|
||
</TabsContent>
|
||
|
||
{/* 유틸리티 컴포넌트 */}
|
||
<TabsContent value="utility" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||
{getFilteredComponents("utility").length > 0
|
||
? getFilteredComponents("utility").map(renderComponentCard)
|
||
: renderEmptyState()}
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
{/* 도움말 */}
|
||
<div className="border-primary/20 bg-primary/5 mt-3 rounded-lg border p-3">
|
||
<div className="flex items-start gap-2">
|
||
<MousePointer className="text-primary mt-0.5 h-3.5 w-3.5 flex-shrink-0" />
|
||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||
컴포넌트를 <span className="text-foreground font-semibold">드래그</span>하여 화면에 추가하세요
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|