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

227 lines
8.2 KiB
TypeScript
Raw Normal View History

2025-09-02 16:18:38 +09:00
"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;
}
// 위젯 타입별 아이콘
const getWidgetIcon = (widgetType: WebType) => {
switch (widgetType) {
case "text":
case "email":
case "tel":
return <Type className="h-3 w-3 text-primary" />;
2025-09-02 16:18:38 +09:00
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="h-3 w-3 text-primary" />;
2025-09-02 16:18:38 +09:00
case "code":
return <Code className="h-3 w-3 text-muted-foreground" />;
2025-09-02 16:18:38 +09:00
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,
}) => {
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 filteredTables = tables.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 border-gray-200 p-4">
{selectedTableName && (
<div className="mb-3 rounded-md bg-accent p-3">
2025-09-02 16:18:38 +09:00
<div className="text-sm font-medium text-blue-900"> </div>
<div className="mt-1 flex items-center space-x-2">
<Database className="h-3 w-3 text-primary" />
2025-09-02 16:18:38 +09:00
<span className="font-mono text-xs text-blue-800">{selectedTableName}</span>
</div>
</div>
)}
{/* 검색 */}
<div className="relative">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
<input
type="text"
placeholder="테이블명, 컬럼명으로 검색..."
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
className="w-full rounded-md border border-gray-300 py-2 pr-3 pl-10 focus:border-transparent focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="mt-2 text-xs text-muted-foreground"> {filteredTables.length} </div>
2025-09-02 16:18:38 +09:00
</div>
{/* 테이블 목록 */}
<div className="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 flex-1 overflow-y-auto">
<div className="space-y-1 p-2">
{filteredTables.map((table) => {
const isExpanded = expandedTables.has(table.tableName);
return (
<div key={table.tableName} className="rounded-md border border-gray-200">
{/* 테이블 헤더 */}
<div
className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50"
onClick={() => toggleTable(table.tableName)}
>
<div className="flex flex-1 items-center space-x-2">
{isExpanded ? (
<ChevronDown className="h-4 w-4 text-gray-500" />
) : (
<ChevronRight className="h-4 w-4 text-gray-500" />
)}
<Database className="h-4 w-4 text-primary" />
2025-09-02 16:18:38 +09:00
<div className="flex-1">
2025-09-08 14:20:01 +09:00
<div className="text-sm font-medium">{table.tableLabel || table.tableName}</div>
2025-09-02 16:18:38 +09:00
<div className="text-xs text-gray-500">{table.columns.length} </div>
</div>
</div>
<Button
size="sm"
variant="ghost"
draggable
onDragStart={(e) => onDragStart(e, table)}
className="ml-2 text-xs"
>
</Button>
</div>
{/* 컬럼 목록 */}
{isExpanded && (
<div className="border-t border-gray-200 bg-gray-50">
<div
className={`${
table.columns.length > 8
? "scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 max-h-64 overflow-y-auto"
: ""
}`}
style={{
scrollbarWidth: "thin",
scrollbarColor: "#cbd5e1 #f1f5f9",
}}
>
{table.columns.map((column, index) => (
<div
key={column.columnName}
className={`flex cursor-pointer items-center justify-between p-2 hover:bg-white ${
index < table.columns.length - 1 ? "border-b border-gray-100" : ""
}`}
draggable
onDragStart={(e) => onDragStart(e, table, column)}
>
<div className="flex flex-1 items-center space-x-2">
{getWidgetIcon(column.widgetType)}
<div className="min-w-0 flex-1">
2025-09-08 14:20:01 +09:00
<div className="truncate text-sm font-medium">
{column.columnLabel || column.columnName}
</div>
2025-09-02 16:18:38 +09:00
<div className="truncate text-xs text-gray-500">{column.dataType}</div>
</div>
</div>
<div className="flex flex-shrink-0 items-center space-x-1">
<Badge variant="secondary" className="text-xs">
{column.widgetType}
</Badge>
{column.required && (
<Badge variant="destructive" className="text-xs">
</Badge>
)}
</div>
</div>
))}
{/* 컬럼 수가 많을 때 안내 메시지 */}
{table.columns.length > 8 && (
<div className="sticky bottom-0 bg-gray-100 p-2 text-center">
<div className="text-xs text-muted-foreground">
2025-09-02 16:18:38 +09:00
📜 {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-xs text-muted-foreground">💡 </div>
2025-09-02 16:18:38 +09:00
</div>
</div>
);
};
export default TablesPanel;