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

422 lines
17 KiB
TypeScript
Raw Normal View History

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";
2025-10-15 17:25:38 +09:00
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
2025-09-11 18:38:28 +09:00
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
import { ComponentDefinition, ComponentCategory } from "@/types/component";
2025-12-19 15:44:38 +09:00
import { Search, Package, Grid, Layers, Palette, Zap, MousePointer, Edit3, BarChart3, Database, Wrench, Sparkles } from "lucide-react";
2025-10-22 17:19:47 +09:00
import { TableInfo, ColumnInfo } from "@/types/screen";
import TablesPanel from "./TablesPanel";
2025-09-09 17:42:23 +09:00
interface ComponentsPanelProps {
2025-09-11 18:38:28 +09:00
className?: string;
2025-10-22 17:19:47 +09:00
// 테이블 관련 props
tables?: TableInfo[];
searchTerm?: string;
onSearchChange?: (value: string) => void;
onTableDragStart?: (e: React.DragEvent, table: TableInfo, column?: ColumnInfo) => void;
selectedTableName?: string;
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합
2025-09-09 17:42:23 +09:00
}
2025-10-28 16:16:00 +09:00
export function ComponentsPanel({
className,
tables = [],
searchTerm = "",
onSearchChange,
2025-10-22 17:19:47 +09:00
onTableDragStart,
selectedTableName,
2025-10-28 16:16:00 +09:00
placedColumns,
2025-10-22 17:19:47 +09:00
}: ComponentsPanelProps) {
2025-09-11 18:38:28 +09:00
const [searchQuery, setSearchQuery] = useState("");
// 레지스트리에서 모든 컴포넌트 조회
const allComponents = useMemo(() => {
const components = ComponentRegistry.getAllComponents();
2025-10-15 17:25:38 +09:00
// 수동으로 table-list 컴포넌트 추가 (임시)
2025-10-15 17:25:38 +09:00
const hasTableList = components.some((c) => c.id === "table-list");
if (!hasTableList) {
components.push({
2025-10-15 17:25:38 +09:00
id: "table-list",
name: "데이터 테이블 v2",
description: "검색, 필터링, 페이징, CRUD 기능이 포함된 완전한 데이터 테이블 컴포넌트",
category: "display",
tags: ["table", "data", "crud"],
defaultSize: { width: 1000, height: 680 },
} as ComponentDefinition);
}
return components;
2025-09-11 18:38:28 +09:00
}, []);
2025-12-19 15:44:38 +09:00
// Unified 컴포넌트 정의 (새로운 통합 컴포넌트 시스템)
2025-12-23 10:49:28 +09:00
// 입력 컴포넌트(unified-input, unified-select, unified-date)는 테이블 컬럼 드래그 시 자동 생성되므로 숨김
2025-12-19 15:44:38 +09:00
const unifiedComponents: ComponentDefinition[] = useMemo(() => [
2025-12-23 10:49:28 +09:00
// unified-input: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
// unified-select: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
// unified-date: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
// unified-layout: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
// unified-group: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
2025-12-19 15:44:38 +09:00
{
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 },
},
{
id: "unified-repeater",
name: "통합 반복 데이터",
description: "반복 데이터 관리 (인라인/모달/버튼 모드)",
category: "data" as ComponentCategory,
tags: ["repeater", "table", "modal", "button", "unified"],
defaultSize: { width: 600, height: 300 },
},
2025-12-19 15:44:38 +09:00
], []);
// 카테고리별 컴포넌트 그룹화
2025-09-11 18:38:28 +09:00
const componentsByCategory = useMemo(() => {
2025-12-23 14:20:18 +09:00
// 숨길 컴포넌트 ID 목록
const hiddenComponents = [
// 기본 입력 컴포넌트 (테이블 컬럼 드래그 시 자동 생성)
"text-input",
"number-input",
"date-input",
"textarea-basic",
// Unified 컴포넌트로 대체됨
"image-widget", // → UnifiedMedia (image)
"file-upload", // → UnifiedMedia (file)
"entity-search-input", // → UnifiedSelect (entity 모드)
"autocomplete-search-input", // → UnifiedSelect (autocomplete 모드)
// UnifiedBiz로 통합 예정
"rack-structure", // → UnifiedBiz (rack)
// DataFlow 전용 (일반 화면에서 불필요)
"mail-recipient-selector",
// 현재 사용 안함
"repeater-field-group",
];
return {
input: allComponents.filter(
2025-12-23 14:20:18 +09:00
(c) => c.category === ComponentCategory.INPUT && !hiddenComponents.includes(c.id),
),
2025-12-23 14:20:18 +09:00
action: allComponents.filter(
(c) => c.category === ComponentCategory.ACTION && !hiddenComponents.includes(c.id),
),
display: allComponents.filter(
(c) => c.category === ComponentCategory.DISPLAY && !hiddenComponents.includes(c.id),
),
data: allComponents.filter(
(c) => c.category === ComponentCategory.DATA && !hiddenComponents.includes(c.id),
),
layout: allComponents.filter(
(c) => c.category === ComponentCategory.LAYOUT && !hiddenComponents.includes(c.id),
),
utility: allComponents.filter(
(c) => c.category === ComponentCategory.UTILITY && !hiddenComponents.includes(c.id),
),
unified: unifiedComponents,
2025-09-11 18:38:28 +09:00
};
2025-12-19 15:44:38 +09:00
}, [allComponents, unifiedComponents]);
2025-09-10 14:09:32 +09:00
2025-10-15 17:25:38 +09:00
// 카테고리별 검색 필터링
const getFilteredComponents = (category: keyof typeof componentsByCategory) => {
let components = componentsByCategory[category];
2025-09-11 18:38:28 +09:00
if (searchQuery) {
2025-09-11 18:38:28 +09:00
const query = searchQuery.toLowerCase();
components = components.filter(
(component: ComponentDefinition) =>
2025-09-11 18:38:28 +09:00
component.name.toLowerCase().includes(query) ||
component.description.toLowerCase().includes(query) ||
2025-10-15 17:25:38 +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;
2025-10-15 17:25:38 +09:00
};
2025-09-11 18:38:28 +09:00
// 카테고리 아이콘 매핑
const getCategoryIcon = (category: ComponentCategory) => {
2025-09-11 18:38:28 +09:00
switch (category) {
case "display":
return <Palette className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
case "action":
return <Zap className="h-6 w-6" />;
case "data":
return <Database className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
case "layout":
return <Layers className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
case "utility":
return <Package className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
default:
return <Grid className="h-6 w-6" />;
2025-09-11 18:38:28 +09:00
}
};
2025-09-09 17:42:23 +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
2025-10-15 17:25:38 +09:00
// 컴포넌트 카드 렌더링 함수
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";
}}
2025-10-17 16:21:08 +09:00
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"
2025-10-15 17:25:38 +09:00
>
2025-10-17 16:21:08 +09:00
<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">
2025-10-15 17:25:38 +09:00
{getCategoryIcon(component.category)}
</div>
<div className="min-w-0 flex-1">
2025-10-17 16:21:08 +09:00
<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">
2025-10-15 17:25:38 +09:00
{component.defaultSize.width}×{component.defaultSize.height}
</span>
</div>
</div>
</div>
</div>
);
// 빈 상태 렌더링
const renderEmptyState = () => (
2025-10-17 16:21:08 +09:00
<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>
2025-10-15 17:25:38 +09:00
</div>
</div>
);
2025-09-09 17:42:23 +09:00
return (
2025-10-17 16:21:08 +09:00
<div className={`bg-background flex h-full flex-col p-4 ${className}`}>
{/* 헤더 */}
2025-10-17 16:21:08 +09:00
<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>
2025-09-09 17:42:23 +09:00
2025-10-28 16:26:55 +09:00
{/* 통합 검색 */}
2025-10-17 16:21:08 +09:00
<div className="mb-3">
<div className="relative">
2025-10-17 16:21:08 +09:00
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
2025-09-09 17:42:23 +09:00
<Input
2025-10-28 16:26:55 +09:00
placeholder="컴포넌트, 테이블, 컬럼 검색..."
2025-09-11 18:38:28 +09:00
value={searchQuery}
2025-10-28 16:26:55 +09:00
onChange={(e) => {
const value = e.target.value;
setSearchQuery(value);
// 테이블 검색도 함께 업데이트
if (onSearchChange) {
onSearchChange(value);
}
}}
className="h-8 pl-8 text-xs"
2025-09-09 17:42:23 +09:00
/>
</div>
2025-10-15 17:25:38 +09:00
</div>
2025-09-11 18:38:28 +09:00
2025-10-15 17:25:38 +09:00
{/* 카테고리 탭 */}
2025-12-19 15:44:38 +09:00
<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>
2025-10-28 16:26:55 +09:00
<TabsTrigger
value="tables"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="테이블"
>
2025-10-22 17:19:47 +09:00
<Database className="h-3 w-3" />
2025-12-19 15:44:38 +09:00
<span className="text-[9px]"></span>
2025-10-22 17:19:47 +09:00
</TabsTrigger>
2025-10-28 16:26:55 +09:00
<TabsTrigger value="input" className="flex items-center justify-center gap-0.5 px-0 text-[10px]" title="입력">
2025-10-15 17:25:38 +09:00
<Edit3 className="h-3 w-3" />
2025-12-19 15:44:38 +09:00
<span className="text-[9px]"></span>
2025-10-15 17:25:38 +09:00
</TabsTrigger>
<TabsTrigger
value="data"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="데이터"
>
<Grid className="h-3 w-3" />
2025-12-19 15:44:38 +09:00
<span className="text-[9px]"></span>
</TabsTrigger>
2025-12-19 15:44:38 +09:00
{/* 2행: 액션, 표시, 레이아웃, 유틸리티 */}
2025-10-28 16:26:55 +09:00
<TabsTrigger
value="action"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="액션"
>
<Zap className="h-3 w-3" />
2025-12-19 15:44:38 +09:00
<span className="text-[9px]"></span>
2025-10-15 17:25:38 +09:00
</TabsTrigger>
2025-10-28 16:26:55 +09:00
<TabsTrigger
value="display"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="표시"
>
2025-10-15 17:25:38 +09:00
<BarChart3 className="h-3 w-3" />
2025-12-19 15:44:38 +09:00
<span className="text-[9px]"></span>
2025-10-15 17:25:38 +09:00
</TabsTrigger>
2025-10-28 16:26:55 +09:00
<TabsTrigger
value="layout"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="레이아웃"
>
<Layers className="h-3 w-3" />
2025-12-19 15:44:38 +09:00
<span className="text-[9px]"></span>
2025-10-15 17:25:38 +09:00
</TabsTrigger>
<TabsTrigger
value="utility"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="유틸리티"
>
<Wrench className="h-3 w-3" />
2025-12-19 15:44:38 +09:00
<span className="text-[9px]"></span>
</TabsTrigger>
2025-10-15 17:25:38 +09:00
</TabsList>
2025-12-19 15:44:38 +09:00
{/* 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>
2025-10-22 17:19:47 +09:00
{/* 테이블 탭 */}
<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}
2025-10-22 17:19:47 +09:00
/>
) : (
<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>
2025-10-15 17:25:38 +09:00
{/* 입력 컴포넌트 */}
2025-10-17 16:21:08 +09:00
<TabsContent value="input" className="mt-0 flex-1 space-y-2 overflow-y-auto">
2025-10-15 17:25:38 +09:00
{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>
2025-10-15 17:25:38 +09:00
{/* 액션 컴포넌트 */}
2025-10-17 16:21:08 +09:00
<TabsContent value="action" className="mt-0 flex-1 space-y-2 overflow-y-auto">
2025-10-15 17:25:38 +09:00
{getFilteredComponents("action").length > 0
? getFilteredComponents("action").map(renderComponentCard)
: renderEmptyState()}
</TabsContent>
{/* 표시 컴포넌트 */}
2025-10-17 16:21:08 +09:00
<TabsContent value="display" className="mt-0 flex-1 space-y-2 overflow-y-auto">
2025-10-15 17:25:38 +09:00
{getFilteredComponents("display").length > 0
? getFilteredComponents("display").map(renderComponentCard)
: renderEmptyState()}
</TabsContent>
{/* 레이아웃 컴포넌트 */}
2025-10-17 16:21:08 +09:00
<TabsContent value="layout" className="mt-0 flex-1 space-y-2 overflow-y-auto">
2025-10-15 17:25:38 +09:00
{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>
2025-10-15 17:25:38 +09:00
</Tabs>
2025-09-09 17:42:23 +09:00
{/* 도움말 */}
2025-10-17 16:21:08 +09:00
<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>
2025-09-29 17:21:47 +09:00
</div>
</div>
2025-09-09 17:42:23 +09:00
);
}