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

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;