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

387 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { 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, Sparkles } 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;
}, []);
// Unified 컴포넌트 정의 (새로운 통합 컴포넌트 시스템)
// 입력 컴포넌트(unified-input, unified-select, unified-date)는 테이블 컬럼 드래그 시 자동 생성되므로 숨김
const unifiedComponents: ComponentDefinition[] = useMemo(() => [
// unified-input: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
// unified-select: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
// unified-date: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
// unified-layout: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
// unified-group: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
{
id: "unified-list",
name: "통합 목록",
description: "테이블, 카드, 칸반, 리스트 등 다양한 데이터 표시 방식 지원",
category: "display" as ComponentCategory,
tags: ["table", "list", "card", "kanban", "unified"],
defaultSize: { width: 600, height: 400 },
},
{
id: "unified-media",
name: "통합 미디어",
description: "이미지, 비디오, 오디오, 파일 업로드 등 미디어 컴포넌트",
category: "display" as ComponentCategory,
tags: ["image", "video", "audio", "file", "unified"],
defaultSize: { width: 300, height: 200 },
},
{
id: "unified-biz",
name: "통합 비즈니스",
description: "플로우 다이어그램, 랙 구조, 채번규칙 등 비즈니스 컴포넌트",
category: "utility" as ComponentCategory,
tags: ["flow", "rack", "numbering", "unified"],
defaultSize: { width: 600, height: 400 },
},
{
id: "unified-hierarchy",
name: "통합 계층",
description: "트리, 조직도, BOM, 연쇄 선택박스 등 계층 구조 컴포넌트",
category: "data" as ComponentCategory,
tags: ["tree", "org", "bom", "cascading", "unified"],
defaultSize: { width: 400, height: 300 },
},
], []);
// 카테고리별 컴포넌트 그룹화
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),
data: allComponents.filter((c) => c.category === ComponentCategory.DATA), // 🆕 데이터 카테고리 추가
layout: allComponents.filter((c) => c.category === ComponentCategory.LAYOUT),
utility: allComponents.filter((c) => c.category === ComponentCategory.UTILITY),
unified: unifiedComponents, // 🆕 Unified 컴포넌트 카테고리 추가
};
}, [allComponents, unifiedComponents]);
// 카테고리별 검색 필터링
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 "data":
return <Database 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"
/>
</div>
</div>
{/* 카테고리 탭 */}
<Tabs defaultValue="unified" className="flex min-h-0 flex-1 flex-col">
<TabsList className="mb-3 grid h-16 w-full flex-shrink-0 grid-cols-4 grid-rows-2 gap-1 p-1">
{/* 1행: Unified, 테이블, 입력, 데이터 */}
<TabsTrigger
value="unified"
className="flex items-center justify-center gap-0.5 px-0 text-[10px] bg-primary/10 data-[state=active]:bg-primary data-[state=active]:text-white"
title="Unified 컴포넌트"
>
<Sparkles className="h-3 w-3" />
<span className="text-[9px]">Unified</span>
</TabsTrigger>
<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="text-[9px]"></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="text-[9px]"></span>
</TabsTrigger>
<TabsTrigger
value="data"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="데이터"
>
<Grid className="h-3 w-3" />
<span className="text-[9px]"></span>
</TabsTrigger>
{/* 2행: 액션, 표시, 레이아웃, 유틸리티 */}
<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="text-[9px]"></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="text-[9px]"></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="text-[9px]"></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="text-[9px]"></span>
</TabsTrigger>
</TabsList>
{/* Unified 컴포넌트 탭 */}
<TabsContent value="unified" className="mt-0 flex-1 space-y-2 overflow-y-auto">
<div className="mb-2 rounded-md bg-primary/5 border border-primary/20 p-2">
<p className="text-[10px] text-muted-foreground leading-relaxed">
<span className="font-semibold text-primary">Unified </span> .
</p>
</div>
{getFilteredComponents("unified").length > 0
? getFilteredComponents("unified").map(renderComponentCard)
: renderEmptyState()}
</TabsContent>
{/* 테이블 탭 */}
<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="data" className="mt-0 flex-1 space-y-2 overflow-y-auto">
{getFilteredComponents("data").length > 0
? getFilteredComponents("data").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>
);
}