테이블 컬럼 검색 기능

This commit is contained in:
kjs 2025-10-28 16:26:55 +09:00
parent 711e051b1c
commit b5605d93da
2 changed files with 109 additions and 163 deletions

View File

@ -162,14 +162,21 @@ export function ComponentsPanel({
<p className="text-muted-foreground text-xs">{allComponents.length} </p>
</div>
{/* 검색 */}
{/* 통합 검색 */}
<div className="mb-3">
<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
placeholder="컴포넌트 검색..."
placeholder="컴포넌트, 테이블, 컬럼 검색..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onChange={(e) => {
const value = e.target.value;
setSearchQuery(value);
// 테이블 검색도 함께 업데이트
if (onSearchChange) {
onSearchChange(value);
}
}}
className="h-8 pl-8 text-xs"
/>
</div>
@ -177,26 +184,42 @@ export function ComponentsPanel({
{/* 카테고리 탭 */}
<Tabs defaultValue="input" className="flex min-h-0 flex-1 flex-col">
<TabsList className="mb-3 grid h-8 w-full flex-shrink-0 grid-cols-5">
<TabsTrigger value="tables" className="flex items-center gap-1 px-1 text-xs">
<TabsList className="mb-3 grid h-8 w-full flex-shrink-0 grid-cols-5 gap-1 p-1">
<TabsTrigger
value="tables"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="테이블"
>
<Database className="h-3 w-3" />
<span className="hidden sm:inline"></span>
<span className="hidden"></span>
</TabsTrigger>
<TabsTrigger value="input" className="flex items-center gap-1 px-1 text-xs">
<TabsTrigger value="input" className="flex items-center justify-center gap-0.5 px-0 text-[10px]" title="입력">
<Edit3 className="h-3 w-3" />
<span className="hidden sm:inline"></span>
<span className="hidden"></span>
</TabsTrigger>
<TabsTrigger value="action" className="flex items-center gap-1 px-1 text-xs">
<TabsTrigger
value="action"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="액션"
>
<Zap className="h-3 w-3" />
<span className="hidden sm:inline"></span>
<span className="hidden"></span>
</TabsTrigger>
<TabsTrigger value="display" className="flex items-center gap-1 px-1 text-xs">
<TabsTrigger
value="display"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="표시"
>
<BarChart3 className="h-3 w-3" />
<span className="hidden sm:inline"></span>
<span className="hidden"></span>
</TabsTrigger>
<TabsTrigger value="layout" className="flex items-center gap-1 px-1 text-xs">
<TabsTrigger
value="layout"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="레이아웃"
>
<Layers className="h-3 w-3" />
<span className="hidden sm:inline"></span>
<span className="hidden"></span>
</TabsTrigger>
</TabsList>

View File

@ -1,23 +1,8 @@
"use client";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import React from "react";
import { Badge } from "@/components/ui/badge";
import {
Database,
ChevronDown,
ChevronRight,
Type,
Hash,
Calendar,
CheckSquare,
List,
AlignLeft,
Code,
Building,
File,
Search,
} from "lucide-react";
import { Database, Type, Hash, Calendar, CheckSquare, List, AlignLeft, Code, Building, File } from "lucide-react";
import { TableInfo, WebType } from "@/types/screen";
interface TablesPanelProps {
@ -65,23 +50,9 @@ const getWidgetIcon = (widgetType: WebType) => {
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,
@ -91,137 +62,89 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
}),
}));
// 검색어가 있으면 컬럼 필터링
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())),
);
.map((table) => {
if (!searchTerm) {
return table;
}
const searchLower = searchTerm.toLowerCase();
// 테이블명이 검색어와 일치하면 모든 컬럼 표시
if (
table.tableName.toLowerCase().includes(searchLower) ||
(table.tableLabel && table.tableLabel.toLowerCase().includes(searchLower))
) {
return table;
}
// 그렇지 않으면 컬럼명/라벨이 검색어와 일치하는 컬럼만 필터링
const filteredColumns = table.columns.filter(
(col) =>
col.columnName.toLowerCase().includes(searchLower) ||
(col.columnLabel && col.columnLabel.toLowerCase().includes(searchLower)),
);
return {
...table,
columns: filteredColumns,
};
})
.filter((table) => table.columns.length > 0); // 컬럼이 있는 테이블만 표시
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 className="flex-1 overflow-y-auto p-3">
<div className="space-y-2">
{filteredTables.map((table) => (
<div key={table.tableName} className="space-y-1">
{/* 테이블 헤더 */}
<div className="bg-muted/50 flex items-center justify-between rounded-md p-2">
<div className="flex items-center gap-2">
<Database className="text-primary h-3.5 w-3.5" />
<span className="text-xs font-semibold">{table.tableLabel || table.tableName}</span>
<Badge variant="secondary" className="h-4 px-1.5 text-[10px]">
{table.columns.length}
</Badge>
</div>
</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="space-y-1 pl-2">
{table.columns.map((column) => (
<div
key={column.columnName}
className="hover:bg-accent/50 flex cursor-grab items-center justify-between rounded-md p-2 transition-colors"
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-medium">{column.columnLabel || column.columnName}</div>
<div className="text-muted-foreground truncate text-[10px]">{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 className="flex flex-shrink-0 items-center gap-1">
<Badge variant="secondary" className="h-4 px-1.5 text-[10px]">
{column.widgetType}
</Badge>
{column.required && (
<Badge variant="destructive" className="h-4 px-1 text-[10px]">
</Badge>
)}
</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>
);
};