230 lines
8.5 KiB
TypeScript
230 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Database,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Type,
|
|
Hash,
|
|
Calendar,
|
|
CheckSquare,
|
|
List,
|
|
AlignLeft,
|
|
Code,
|
|
Building,
|
|
File,
|
|
Search,
|
|
} from "lucide-react";
|
|
import { TableInfo, WebType } from "@/types/screen";
|
|
|
|
interface TablesPanelProps {
|
|
tables: TableInfo[];
|
|
searchTerm: string;
|
|
onSearchChange: (term: string) => void;
|
|
onDragStart: (e: React.DragEvent, table: TableInfo, column?: any) => void;
|
|
selectedTableName?: string;
|
|
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합 (tableName.columnName 형식)
|
|
}
|
|
|
|
// 위젯 타입별 아이콘
|
|
const getWidgetIcon = (widgetType: WebType) => {
|
|
switch (widgetType) {
|
|
case "text":
|
|
case "email":
|
|
case "tel":
|
|
return <Type className="text-primary h-3 w-3" />;
|
|
case "number":
|
|
case "decimal":
|
|
return <Hash className="h-3 w-3 text-green-600" />;
|
|
case "date":
|
|
case "datetime":
|
|
return <Calendar className="h-3 w-3 text-purple-600" />;
|
|
case "select":
|
|
case "dropdown":
|
|
return <List className="h-3 w-3 text-orange-600" />;
|
|
case "textarea":
|
|
case "text_area":
|
|
return <AlignLeft className="h-3 w-3 text-indigo-600" />;
|
|
case "boolean":
|
|
case "checkbox":
|
|
return <CheckSquare className="text-primary h-3 w-3" />;
|
|
case "code":
|
|
return <Code className="text-muted-foreground h-3 w-3" />;
|
|
case "entity":
|
|
return <Building className="h-3 w-3 text-cyan-600" />;
|
|
case "file":
|
|
return <File className="h-3 w-3 text-yellow-600" />;
|
|
default:
|
|
return <Type className="h-3 w-3 text-gray-500" />;
|
|
}
|
|
};
|
|
|
|
export const TablesPanel: React.FC<TablesPanelProps> = ({
|
|
tables,
|
|
searchTerm,
|
|
onSearchChange,
|
|
onDragStart,
|
|
selectedTableName,
|
|
placedColumns = new Set(),
|
|
}) => {
|
|
const [expandedTables, setExpandedTables] = useState<Set<string>>(new Set());
|
|
|
|
const toggleTable = (tableName: string) => {
|
|
const newExpanded = new Set(expandedTables);
|
|
if (newExpanded.has(tableName)) {
|
|
newExpanded.delete(tableName);
|
|
} else {
|
|
newExpanded.add(tableName);
|
|
}
|
|
setExpandedTables(newExpanded);
|
|
};
|
|
|
|
// 이미 배치된 컬럼을 제외한 테이블 정보 생성
|
|
const tablesWithAvailableColumns = tables.map((table) => ({
|
|
...table,
|
|
columns: table.columns.filter((col) => {
|
|
const columnKey = `${table.tableName}.${col.columnName}`;
|
|
return !placedColumns.has(columnKey);
|
|
}),
|
|
}));
|
|
|
|
const filteredTables = tablesWithAvailableColumns
|
|
.filter((table) => table.columns.length > 0) // 사용 가능한 컬럼이 있는 테이블만 표시
|
|
.filter(
|
|
(table) =>
|
|
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
table.columns.some((col) => col.columnName.toLowerCase().includes(searchTerm.toLowerCase())),
|
|
);
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
{/* 헤더 */}
|
|
<div className="border-b p-4">
|
|
{selectedTableName && (
|
|
<div className="border-primary/20 bg-primary/5 mb-3 rounded-lg border p-3">
|
|
<div className="text-xs font-semibold">선택된 테이블</div>
|
|
<div className="mt-1.5 flex items-center gap-2">
|
|
<Database className="text-primary h-3 w-3" />
|
|
<span className="font-mono text-xs font-medium">{selectedTableName}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 검색 */}
|
|
<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
|
|
type="text"
|
|
placeholder="테이블명, 컬럼명 검색..."
|
|
value={searchTerm}
|
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 pl-8 text-xs focus-visible:ring-1 focus-visible:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="text-muted-foreground mt-2 text-xs">총 {filteredTables.length}개</div>
|
|
</div>
|
|
|
|
{/* 테이블 목록 */}
|
|
<div className="flex-1 overflow-y-auto">
|
|
<div className="space-y-1.5 p-3">
|
|
{filteredTables.map((table) => {
|
|
const isExpanded = expandedTables.has(table.tableName);
|
|
|
|
return (
|
|
<div key={table.tableName} className="bg-card rounded-lg border">
|
|
{/* 테이블 헤더 */}
|
|
<div
|
|
className="hover:bg-accent/50 flex cursor-pointer items-center justify-between p-2.5 transition-colors"
|
|
onClick={() => toggleTable(table.tableName)}
|
|
>
|
|
<div className="flex flex-1 items-center gap-2">
|
|
{isExpanded ? (
|
|
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
|
|
) : (
|
|
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
|
|
)}
|
|
<Database className="text-primary h-3.5 w-3.5" />
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-xs font-semibold">{table.tableLabel || table.tableName}</div>
|
|
<div className="text-muted-foreground text-xs">{table.columns.length}개</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
draggable
|
|
onDragStart={(e) => onDragStart(e, table)}
|
|
className="h-6 px-2 text-xs"
|
|
>
|
|
드래그
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 컬럼 목록 */}
|
|
{isExpanded && (
|
|
<div className="bg-muted/30 border-t">
|
|
<div className={`${table.columns.length > 8 ? "max-h-64 overflow-y-auto" : ""}`}>
|
|
{table.columns.map((column, index) => (
|
|
<div
|
|
key={column.columnName}
|
|
className={`hover:bg-accent/50 flex cursor-grab items-center justify-between p-2 transition-colors ${
|
|
index < table.columns.length - 1 ? "border-border/50 border-b" : ""
|
|
}`}
|
|
draggable
|
|
onDragStart={(e) => onDragStart(e, table, column)}
|
|
>
|
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
{getWidgetIcon(column.widgetType)}
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-xs font-semibold">
|
|
{column.columnLabel || column.columnName}
|
|
</div>
|
|
<div className="text-muted-foreground truncate text-xs">{column.dataType}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-shrink-0 items-center gap-1">
|
|
<Badge variant="secondary" className="h-4 px-1.5 text-xs">
|
|
{column.widgetType}
|
|
</Badge>
|
|
{column.required && (
|
|
<Badge variant="destructive" className="h-4 px-1.5 text-xs">
|
|
필수
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* 컬럼 수가 많을 때 안내 메시지 */}
|
|
{table.columns.length > 8 && (
|
|
<div className="bg-muted sticky bottom-0 p-2 text-center">
|
|
<div className="text-muted-foreground text-xs">
|
|
📜 총 {table.columns.length}개 컬럼 (스크롤하여 더 보기)
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 푸터 */}
|
|
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
|
<div className="text-muted-foreground text-xs">💡 테이블이나 컬럼을 캔버스로 드래그하세요</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TablesPanel;
|