리피터 케이블 설정 구현

This commit is contained in:
kjs
2026-01-15 15:17:52 +09:00
parent bed7f5f5c4
commit e168753d87
8 changed files with 893 additions and 116 deletions

View File

@@ -18,6 +18,9 @@ interface ComponentsPanelProps {
onTableDragStart?: (e: React.DragEvent, table: TableInfo, column?: ColumnInfo) => void;
selectedTableName?: string;
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합
// 테이블 선택 관련 props
onTableSelect?: (tableName: string) => void; // 테이블 선택 콜백
showTableSelector?: boolean; // 테이블 선택 UI 표시 여부 (기본: 테이블 없으면 표시)
}
export function ComponentsPanel({
@@ -28,6 +31,8 @@ export function ComponentsPanel({
onTableDragStart,
selectedTableName,
placedColumns,
onTableSelect,
showTableSelector = true,
}: ComponentsPanelProps) {
const [searchQuery, setSearchQuery] = useState("");
@@ -272,24 +277,16 @@ export function ComponentsPanel({
{/* 테이블 컬럼 탭 */}
<TabsContent value="tables" className="mt-0 flex-1 overflow-y-auto">
{tables.length > 0 && onTableDragStart ? (
<TablesPanel
tables={tables}
searchTerm={searchTerm}
onSearchChange={onSearchChange || (() => {})}
onDragStart={onTableDragStart}
selectedTableName={selectedTableName}
placedColumns={placedColumns}
/>
) : (
<div className="flex h-32 items-center justify-center text-center">
<div className="p-6">
<Database 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>
</div>
</div>
)}
<TablesPanel
tables={tables}
searchTerm={searchTerm}
onSearchChange={onSearchChange || (() => {})}
onDragStart={onTableDragStart || (() => {})}
selectedTableName={selectedTableName}
placedColumns={placedColumns}
onTableSelect={onTableSelect}
showTableSelector={showTableSelector}
/>
</TabsContent>
{/* 컴포넌트 탭 */}

View File

@@ -1,7 +1,15 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Database,
Type,
@@ -16,9 +24,13 @@ import {
Link2,
ChevronDown,
ChevronRight,
Plus,
Search,
X,
} from "lucide-react";
import { TableInfo, WebType } from "@/types/screen";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { tableManagementApi } from "@/lib/api/tableManagement";
interface EntityJoinColumn {
columnName: string;
@@ -41,6 +53,9 @@ interface TablesPanelProps {
onDragStart: (e: React.DragEvent, table: TableInfo, column?: any) => void;
selectedTableName?: string;
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합 (tableName.columnName 형식)
// 테이블 선택 관련 props
onTableSelect?: (tableName: string) => void; // 테이블 선택 콜백
showTableSelector?: boolean; // 테이블 선택 UI 표시 여부
}
// 위젯 타입별 아이콘
@@ -81,12 +96,20 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
searchTerm,
onDragStart,
placedColumns = new Set(),
onTableSelect,
showTableSelector = false,
}) => {
// 엔티티 조인 컬럼 상태
const [entityJoinTables, setEntityJoinTables] = useState<EntityJoinTable[]>([]);
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
const [expandedJoinTables, setExpandedJoinTables] = useState<Set<string>>(new Set());
// 전체 테이블 목록 (테이블 선택용)
const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
const [loadingAllTables, setLoadingAllTables] = useState(false);
const [tableSearchTerm, setTableSearchTerm] = useState("");
const [showTableSelectDropdown, setShowTableSelectDropdown] = useState(false);
// 시스템 컬럼 목록 (숨김 처리)
const systemColumns = new Set([
"id",
@@ -96,6 +119,42 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
"company_code",
]);
// 전체 테이블 목록 로드
const loadAllTables = useCallback(async () => {
if (allTables.length > 0) return; // 이미 로드됨
setLoadingAllTables(true);
try {
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
setAllTables(response.data.map((t: any) => ({
tableName: t.tableName || t.table_name,
displayName: t.displayName || t.table_label || t.tableName || t.table_name,
})));
}
} catch (error) {
console.error("테이블 목록 조회 오류:", error);
} finally {
setLoadingAllTables(false);
}
}, [allTables.length]);
// 테이블 선택 시 호출
const handleTableSelect = (tableName: string) => {
setShowTableSelectDropdown(false);
setTableSearchTerm("");
onTableSelect?.(tableName);
};
// 필터링된 테이블 목록
const filteredAllTables = tableSearchTerm
? allTables.filter(
(t) =>
t.tableName.toLowerCase().includes(tableSearchTerm.toLowerCase()) ||
t.displayName.toLowerCase().includes(tableSearchTerm.toLowerCase())
)
: allTables;
// 메인 테이블명 추출
const mainTableName = tables[0]?.tableName;
@@ -209,6 +268,91 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
return (
<div className="flex h-full flex-col">
{/* 테이블 선택 버튼 (메인 테이블이 없을 때 또는 showTableSelector가 true일 때) */}
{(showTableSelector || tables.length === 0) && (
<div className="border-b p-3">
<div className="relative">
<Button
variant="outline"
size="sm"
className="w-full justify-between"
onClick={() => {
setShowTableSelectDropdown(!showTableSelectDropdown);
if (!showTableSelectDropdown) {
loadAllTables();
}
}}
>
<span className="flex items-center gap-2">
<Plus className="h-3.5 w-3.5" />
{tables.length > 0 ? "테이블 추가/변경" : "테이블 선택"}
</span>
<ChevronDown className="h-4 w-4" />
</Button>
{/* 드롭다운 */}
{showTableSelectDropdown && (
<div className="absolute top-full left-0 z-50 mt-1 w-full rounded-md border bg-white shadow-lg">
{/* 검색 */}
<div className="border-b p-2">
<div className="relative">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="테이블 검색..."
value={tableSearchTerm}
onChange={(e) => setTableSearchTerm(e.target.value)}
autoFocus
className="w-full rounded-md border px-8 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{tableSearchTerm && (
<button
onClick={() => setTableSearchTerm("")}
className="absolute right-2 top-1/2 -translate-y-1/2"
>
<X className="h-3.5 w-3.5 text-gray-400" />
</button>
)}
</div>
</div>
{/* 테이블 목록 */}
<div className="max-h-60 overflow-y-auto">
{loadingAllTables ? (
<div className="p-4 text-center text-sm text-gray-500"> ...</div>
) : filteredAllTables.length === 0 ? (
<div className="p-4 text-center text-sm text-gray-500">
{tableSearchTerm ? "검색 결과 없음" : "테이블 없음"}
</div>
) : (
filteredAllTables.map((t) => (
<button
key={t.tableName}
onClick={() => handleTableSelect(t.tableName)}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-100"
>
<Database className="h-3.5 w-3.5 text-blue-600" />
<div className="min-w-0 flex-1">
<div className="truncate font-medium">{t.displayName}</div>
<div className="truncate text-xs text-gray-500">{t.tableName}</div>
</div>
</button>
))
)}
</div>
</div>
)}
</div>
{/* 현재 테이블 정보 */}
{tables.length > 0 && (
<div className="mt-2 text-xs text-muted-foreground">
: {tables[0]?.tableLabel || tables[0]?.tableName}
</div>
)}
</div>
)}
{/* 테이블과 컬럼 평면 목록 */}
<div className="flex-1 overflow-y-auto p-3">
<div className="space-y-2">