2025-09-09 17:42:23 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useMemo } from "react";
|
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
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";
|
2026-01-16 09:59:16 +09:00
|
|
|
|
import { Search, Package, Grid, Layers, Palette, Zap, MousePointer, Database, GripVertical } 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;
|
2025-10-23 10:07:55 +09:00
|
|
|
|
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합
|
2026-01-15 15:17:52 +09:00
|
|
|
|
// 테이블 선택 관련 props
|
|
|
|
|
|
onTableSelect?: (tableName: string) => void; // 테이블 선택 콜백
|
|
|
|
|
|
showTableSelector?: boolean; // 테이블 선택 UI 표시 여부 (기본: 테이블 없으면 표시)
|
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,
|
2025-10-23 10:07:55 +09:00
|
|
|
|
selectedTableName,
|
2025-10-28 16:16:00 +09:00
|
|
|
|
placedColumns,
|
2026-01-15 15:17:52 +09:00
|
|
|
|
onTableSelect,
|
|
|
|
|
|
showTableSelector = true,
|
2025-10-22 17:19:47 +09:00
|
|
|
|
}: ComponentsPanelProps) {
|
2025-09-11 18:38:28 +09:00
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
// 레지스트리에서 모든 컴포넌트 조회
|
|
|
|
|
|
const allComponents = useMemo(() => {
|
2025-09-25 18:54:25 +09:00
|
|
|
|
const components = ComponentRegistry.getAllComponents();
|
2026-01-19 14:52:11 +09:00
|
|
|
|
// v2-table-list가 자동 등록되므로 수동 추가 불필요
|
2025-09-25 18:54:25 +09:00
|
|
|
|
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)는 테이블 컬럼 드래그 시 자동 생성되므로 숨김
|
2026-01-05 16:14:36 +09:00
|
|
|
|
const unifiedComponents = useMemo(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
[
|
|
|
|
|
|
// unified-input: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
|
|
|
|
|
|
// unified-select: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
|
|
|
|
|
|
// unified-date: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
|
|
|
|
|
|
// unified-layout: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
|
|
|
|
|
|
// unified-group: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
|
2026-01-15 17:00:21 +09:00
|
|
|
|
// unified-list: table-list, card-display로 분리하여 숨김 처리
|
2026-01-05 16:14:36 +09:00
|
|
|
|
// unified-media 제거 - 테이블 컬럼의 image/file 입력 타입으로 사용
|
|
|
|
|
|
// unified-biz 제거 - 개별 컴포넌트(flow-widget, rack-structure, numbering-rule)로 직접 표시
|
|
|
|
|
|
// unified-hierarchy 제거 - 현재 미사용
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "unified-repeater",
|
2026-01-16 09:59:16 +09:00
|
|
|
|
name: "리피터 그리드",
|
|
|
|
|
|
description: "행 단위로 데이터를 추가/수정/삭제",
|
2026-01-05 16:14:36 +09:00
|
|
|
|
category: "data" as ComponentCategory,
|
|
|
|
|
|
tags: ["repeater", "table", "modal", "button", "unified"],
|
|
|
|
|
|
defaultSize: { width: 600, height: 300 },
|
|
|
|
|
|
},
|
|
|
|
|
|
] as ComponentDefinition[],
|
2026-01-05 11:33:57 +09:00
|
|
|
|
[],
|
|
|
|
|
|
);
|
2025-12-19 15:44:38 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
// 카테고리별 컴포넌트 그룹화
|
2025-09-11 18:38:28 +09:00
|
|
|
|
const componentsByCategory = useMemo(() => {
|
2025-12-23 14:20:18 +09:00
|
|
|
|
// 숨길 컴포넌트 ID 목록
|
|
|
|
|
|
const hiddenComponents = [
|
|
|
|
|
|
// 기본 입력 컴포넌트 (테이블 컬럼 드래그 시 자동 생성)
|
2026-01-05 11:33:57 +09:00
|
|
|
|
"text-input",
|
|
|
|
|
|
"number-input",
|
|
|
|
|
|
"date-input",
|
2025-12-23 14:20:18 +09:00
|
|
|
|
"textarea-basic",
|
|
|
|
|
|
// Unified 컴포넌트로 대체됨
|
2026-01-05 11:33:57 +09:00
|
|
|
|
"image-widget", // → UnifiedMedia (image)
|
|
|
|
|
|
"file-upload", // → UnifiedMedia (file)
|
2025-12-23 14:20:18 +09:00
|
|
|
|
"entity-search-input", // → UnifiedSelect (entity 모드)
|
|
|
|
|
|
"autocomplete-search-input", // → UnifiedSelect (autocomplete 모드)
|
|
|
|
|
|
// DataFlow 전용 (일반 화면에서 불필요)
|
|
|
|
|
|
"mail-recipient-selector",
|
|
|
|
|
|
// 현재 사용 안함
|
|
|
|
|
|
"repeater-field-group",
|
2026-01-05 11:33:57 +09:00
|
|
|
|
// unified-repeater로 통합됨
|
|
|
|
|
|
"simple-repeater-table", // → unified-repeater (inline 모드)
|
|
|
|
|
|
"modal-repeater-table", // → unified-repeater (modal 모드)
|
|
|
|
|
|
// 특수 업무용 컴포넌트 (일반 화면에서 불필요)
|
|
|
|
|
|
"tax-invoice-list", // 세금계산서 전용
|
|
|
|
|
|
"customer-item-mapping", // 고객-품목 매핑 전용
|
2026-01-15 17:00:21 +09:00
|
|
|
|
// card-display는 별도 컴포넌트로 유지
|
2026-01-05 11:33:57 +09:00
|
|
|
|
// unified-media로 통합됨
|
|
|
|
|
|
"image-display", // → unified-media (image)
|
|
|
|
|
|
// 공통코드관리로 통합 예정
|
|
|
|
|
|
"category-manager", // → 공통코드관리 기능으로 통합 예정
|
2026-01-05 11:40:24 +09:00
|
|
|
|
// 분할 패널 정리 (split-panel-layout v1 유지)
|
|
|
|
|
|
"split-panel-layout2", // → split-panel-layout로 통합
|
|
|
|
|
|
"screen-split-panel", // 화면 임베딩 방식은 사용하지 않음
|
2026-01-05 15:00:05 +09:00
|
|
|
|
// 미완성/미사용 컴포넌트 (기존 화면 호환성 유지, 새 추가만 막음)
|
|
|
|
|
|
"accordion-basic", // 아코디언 컴포넌트
|
|
|
|
|
|
"conditional-container", // 조건부 컨테이너
|
|
|
|
|
|
"universal-form-modal", // 범용 폼 모달
|
2026-01-05 16:02:33 +09:00
|
|
|
|
// 통합 미디어 (테이블 컬럼 입력 타입으로 사용)
|
|
|
|
|
|
"unified-media", // → 테이블 컬럼의 image/file 입력 타입으로 사용
|
2026-01-15 17:00:21 +09:00
|
|
|
|
// 플로우 위젯 숨김 처리
|
|
|
|
|
|
"flow-widget",
|
|
|
|
|
|
// 선택 항목 상세입력 - 기존 컴포넌트 조합으로 대체 가능
|
|
|
|
|
|
"selected-items-detail-input",
|
|
|
|
|
|
// 연관 데이터 버튼 - unified-repeater로 대체 가능
|
|
|
|
|
|
"related-data-buttons",
|
2025-12-23 14:20:18 +09:00
|
|
|
|
];
|
2025-10-16 15:05:24 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
|
return {
|
2026-01-05 11:33:57 +09:00
|
|
|
|
input: allComponents.filter((c) => c.category === ComponentCategory.INPUT && !hiddenComponents.includes(c.id)),
|
|
|
|
|
|
action: allComponents.filter((c) => c.category === ComponentCategory.ACTION && !hiddenComponents.includes(c.id)),
|
2025-12-23 14:20:18 +09:00
|
|
|
|
display: allComponents.filter(
|
|
|
|
|
|
(c) => c.category === ComponentCategory.DISPLAY && !hiddenComponents.includes(c.id),
|
|
|
|
|
|
),
|
2026-01-05 11:33:57 +09:00
|
|
|
|
data: allComponents.filter((c) => c.category === ComponentCategory.DATA && !hiddenComponents.includes(c.id)),
|
|
|
|
|
|
layout: allComponents.filter((c) => c.category === ComponentCategory.LAYOUT && !hiddenComponents.includes(c.id)),
|
2025-12-23 14:20:18 +09:00
|
|
|
|
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
|
|
|
|
|
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-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
|
|
|
|
|
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-11-14 14:43:53 +09:00
|
|
|
|
case "data":
|
|
|
|
|
|
return <Database 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
|
|
|
|
|
2026-01-16 09:59:16 +09:00
|
|
|
|
// 카테고리별 배경색 매핑
|
|
|
|
|
|
const getCategoryColor = (category: string) => {
|
|
|
|
|
|
switch (category) {
|
|
|
|
|
|
case "data":
|
|
|
|
|
|
return "from-blue-500/10 to-blue-600/10 text-blue-600 group-hover:from-blue-500/20 group-hover:to-blue-600/20";
|
|
|
|
|
|
case "display":
|
|
|
|
|
|
return "from-emerald-500/10 to-emerald-600/10 text-emerald-600 group-hover:from-emerald-500/20 group-hover:to-emerald-600/20";
|
|
|
|
|
|
case "input":
|
|
|
|
|
|
return "from-violet-500/10 to-violet-600/10 text-violet-600 group-hover:from-violet-500/20 group-hover:to-violet-600/20";
|
|
|
|
|
|
case "layout":
|
|
|
|
|
|
return "from-amber-500/10 to-amber-600/10 text-amber-600 group-hover:from-amber-500/20 group-hover:to-amber-600/20";
|
|
|
|
|
|
case "action":
|
|
|
|
|
|
return "from-rose-500/10 to-rose-600/10 text-rose-600 group-hover:from-rose-500/20 group-hover:to-rose-600/20";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "from-slate-500/10 to-slate-600/10 text-slate-600 group-hover:from-slate-500/20 group-hover:to-slate-600/20";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 카드 렌더링 함수 (컴팩트 버전)
|
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";
|
|
|
|
|
|
}}
|
2026-01-16 09:59:16 +09:00
|
|
|
|
className="group bg-card hover:border-primary/40 cursor-grab rounded-lg border px-3 py-2.5 transition-all duration-200 hover:shadow-sm active:scale-[0.98] active:cursor-grabbing"
|
2025-10-15 17:25:38 +09:00
|
|
|
|
>
|
2026-01-16 09:59:16 +09:00
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-gradient-to-br transition-all duration-200 ${getCategoryColor(component.category)}`}
|
|
|
|
|
|
>
|
2025-10-15 17:25:38 +09:00
|
|
|
|
{getCategoryIcon(component.category)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="min-w-0 flex-1">
|
2026-01-16 09:59:16 +09:00
|
|
|
|
<span className="text-foreground block truncate text-xs font-medium">{component.name}</span>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<span className="text-muted-foreground text-[10px] capitalize">{component.category}</span>
|
|
|
|
|
|
<span className="text-muted-foreground/60 text-[10px]">|</span>
|
|
|
|
|
|
<span className="text-muted-foreground text-[10px]">
|
2025-10-15 17:25:38 +09:00
|
|
|
|
{component.defaultSize.width}×{component.defaultSize.height}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-16 09:59:16 +09:00
|
|
|
|
<div className="text-muted-foreground/40 group-hover:text-muted-foreground/60 transition-colors">
|
|
|
|
|
|
<GripVertical className="h-3.5 w-3.5" />
|
|
|
|
|
|
</div>
|
2025-10-15 17:25:38 +09:00
|
|
|
|
</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-02 14:34:15 +09:00
|
|
|
|
{/* 헤더 */}
|
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>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
</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">
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2025-11-25 13:04:58 +09:00
|
|
|
|
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
|
|
|
|
|
2026-01-05 16:14:36 +09:00
|
|
|
|
{/* 테이블 / 컴포넌트 탭 */}
|
|
|
|
|
|
<Tabs defaultValue="tables" className="flex min-h-0 flex-1 flex-col">
|
|
|
|
|
|
<TabsList className="mb-3 grid h-8 w-full shrink-0 grid-cols-2 gap-1 p-1">
|
|
|
|
|
|
<TabsTrigger value="tables" className="flex items-center justify-center gap-1 text-xs">
|
2025-10-22 17:19:47 +09:00
|
|
|
|
<Database className="h-3 w-3" />
|
2026-01-05 16:14:36 +09:00
|
|
|
|
<span>테이블</span>
|
2025-10-15 17:25:38 +09:00
|
|
|
|
</TabsTrigger>
|
2026-01-05 16:14:36 +09:00
|
|
|
|
<TabsTrigger value="components" className="flex items-center justify-center gap-1 text-xs">
|
|
|
|
|
|
<Package className="h-3 w-3" />
|
|
|
|
|
|
<span>컴포넌트</span>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
</TabsTrigger>
|
2025-10-15 17:25:38 +09:00
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
2026-01-05 16:14:36 +09:00
|
|
|
|
{/* 테이블 컬럼 탭 */}
|
2025-10-22 17:19:47 +09:00
|
|
|
|
<TabsContent value="tables" className="mt-0 flex-1 overflow-y-auto">
|
2026-01-16 09:59:16 +09:00
|
|
|
|
<TablesPanel
|
|
|
|
|
|
tables={tables}
|
|
|
|
|
|
searchTerm={searchTerm}
|
|
|
|
|
|
onSearchChange={onSearchChange || (() => {})}
|
2026-01-15 15:17:52 +09:00
|
|
|
|
onDragStart={onTableDragStart || (() => {})}
|
2026-01-16 09:59:16 +09:00
|
|
|
|
selectedTableName={selectedTableName}
|
|
|
|
|
|
placedColumns={placedColumns}
|
2026-01-15 15:17:52 +09:00
|
|
|
|
onTableSelect={onTableSelect}
|
|
|
|
|
|
showTableSelector={showTableSelector}
|
|
|
|
|
|
/>
|
2025-10-22 17:19:47 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
2026-01-05 16:14:36 +09:00
|
|
|
|
{/* 컴포넌트 탭 */}
|
|
|
|
|
|
<TabsContent value="components" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
|
|
|
|
|
{(() => {
|
|
|
|
|
|
const allFilteredComponents = [
|
|
|
|
|
|
...getFilteredComponents("unified"),
|
|
|
|
|
|
...getFilteredComponents("action"),
|
|
|
|
|
|
...getFilteredComponents("display"),
|
|
|
|
|
|
...getFilteredComponents("data"),
|
|
|
|
|
|
...getFilteredComponents("layout"),
|
|
|
|
|
|
...getFilteredComponents("input"),
|
|
|
|
|
|
...getFilteredComponents("utility"),
|
|
|
|
|
|
];
|
2025-11-12 10:48:24 +09:00
|
|
|
|
|
2026-01-05 16:14:36 +09:00
|
|
|
|
return allFilteredComponents.length > 0
|
|
|
|
|
|
? allFilteredComponents.map(renderComponentCard)
|
|
|
|
|
|
: renderEmptyState();
|
|
|
|
|
|
})()}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
</TabsContent>
|
2025-10-15 17:25:38 +09:00
|
|
|
|
</Tabs>
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
2025-10-02 14:34:15 +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>
|
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
|
|
|
|
);
|
|
|
|
|
|
}
|