2025-09-02 16:18:38 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2026-01-15 15:17:52 +09:00
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
2025-09-02 16:18:38 +09:00
|
|
|
import { Badge } from "@/components/ui/badge";
|
2026-01-15 15:17:52 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
2026-01-15 10:39:23 +09:00
|
|
|
import {
|
|
|
|
|
Database,
|
|
|
|
|
Type,
|
|
|
|
|
Hash,
|
|
|
|
|
Calendar,
|
|
|
|
|
CheckSquare,
|
|
|
|
|
List,
|
|
|
|
|
AlignLeft,
|
|
|
|
|
Code,
|
|
|
|
|
Building,
|
|
|
|
|
File,
|
|
|
|
|
Link2,
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ChevronRight,
|
2026-01-15 15:17:52 +09:00
|
|
|
Plus,
|
|
|
|
|
Search,
|
|
|
|
|
X,
|
2026-01-15 10:39:23 +09:00
|
|
|
} from "lucide-react";
|
2025-09-02 16:18:38 +09:00
|
|
|
import { TableInfo, WebType } from "@/types/screen";
|
2026-01-15 10:39:23 +09:00
|
|
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
2026-01-15 15:17:52 +09:00
|
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
2026-01-15 10:39:23 +09:00
|
|
|
|
|
|
|
|
interface EntityJoinColumn {
|
|
|
|
|
columnName: string;
|
|
|
|
|
columnLabel: string;
|
|
|
|
|
dataType: string;
|
|
|
|
|
inputType?: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface EntityJoinTable {
|
|
|
|
|
tableName: string;
|
|
|
|
|
currentDisplayColumn: string;
|
|
|
|
|
availableColumns: EntityJoinColumn[];
|
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
|
|
|
interface TablesPanelProps {
|
|
|
|
|
tables: TableInfo[];
|
|
|
|
|
searchTerm: string;
|
|
|
|
|
onSearchChange: (term: string) => void;
|
|
|
|
|
onDragStart: (e: React.DragEvent, table: TableInfo, column?: any) => void;
|
|
|
|
|
selectedTableName?: string;
|
2025-10-23 10:07:55 +09:00
|
|
|
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합 (tableName.columnName 형식)
|
2026-01-15 15:17:52 +09:00
|
|
|
// 테이블 선택 관련 props
|
|
|
|
|
onTableSelect?: (tableName: string) => void; // 테이블 선택 콜백
|
|
|
|
|
showTableSelector?: boolean; // 테이블 선택 UI 표시 여부
|
2025-09-02 16:18:38 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 위젯 타입별 아이콘
|
|
|
|
|
const getWidgetIcon = (widgetType: WebType) => {
|
|
|
|
|
switch (widgetType) {
|
|
|
|
|
case "text":
|
|
|
|
|
case "email":
|
|
|
|
|
case "tel":
|
2025-10-17 16:21:08 +09:00
|
|
|
return <Type className="text-primary h-3 w-3" />;
|
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":
|
2025-10-17 16:21:08 +09:00
|
|
|
return <CheckSquare className="text-primary h-3 w-3" />;
|
2025-09-02 16:18:38 +09:00
|
|
|
case "code":
|
2025-10-17 16:21:08 +09:00
|
|
|
return <Code className="text-muted-foreground h-3 w-3" />;
|
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,
|
|
|
|
|
onDragStart,
|
2025-10-23 10:07:55 +09:00
|
|
|
placedColumns = new Set(),
|
2026-01-15 15:17:52 +09:00
|
|
|
onTableSelect,
|
|
|
|
|
showTableSelector = false,
|
2025-09-02 16:18:38 +09:00
|
|
|
}) => {
|
2026-01-15 10:39:23 +09:00
|
|
|
// 엔티티 조인 컬럼 상태
|
|
|
|
|
const [entityJoinTables, setEntityJoinTables] = useState<EntityJoinTable[]>([]);
|
|
|
|
|
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
|
|
|
|
const [expandedJoinTables, setExpandedJoinTables] = useState<Set<string>>(new Set());
|
|
|
|
|
|
2026-01-15 15:17:52 +09:00
|
|
|
// 전체 테이블 목록 (테이블 선택용)
|
|
|
|
|
const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
|
|
|
|
const [loadingAllTables, setLoadingAllTables] = useState(false);
|
|
|
|
|
const [tableSearchTerm, setTableSearchTerm] = useState("");
|
|
|
|
|
const [showTableSelectDropdown, setShowTableSelectDropdown] = useState(false);
|
|
|
|
|
|
2025-11-10 14:24:16 +09:00
|
|
|
// 시스템 컬럼 목록 (숨김 처리)
|
|
|
|
|
const systemColumns = new Set([
|
2026-01-15 10:39:23 +09:00
|
|
|
"id",
|
|
|
|
|
"created_date",
|
|
|
|
|
"updated_date",
|
|
|
|
|
"writer",
|
|
|
|
|
"company_code",
|
2025-11-10 14:24:16 +09:00
|
|
|
]);
|
|
|
|
|
|
2026-01-15 15:17:52 +09:00
|
|
|
// 전체 테이블 목록 로드
|
|
|
|
|
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;
|
|
|
|
|
|
2026-01-15 10:39:23 +09:00
|
|
|
// 메인 테이블명 추출
|
|
|
|
|
const mainTableName = tables[0]?.tableName;
|
|
|
|
|
|
|
|
|
|
// 엔티티 조인 컬럼 로드
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchEntityJoinColumns = async () => {
|
|
|
|
|
if (!mainTableName) {
|
|
|
|
|
setEntityJoinTables([]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setLoadingEntityJoins(true);
|
|
|
|
|
try {
|
|
|
|
|
const result = await entityJoinApi.getEntityJoinColumns(mainTableName);
|
|
|
|
|
setEntityJoinTables(result.joinTables || []);
|
|
|
|
|
// 기본적으로 모든 조인 테이블 펼치기
|
|
|
|
|
setExpandedJoinTables(new Set(result.joinTables?.map((t) => t.tableName) || []));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("엔티티 조인 컬럼 조회 오류:", error);
|
|
|
|
|
setEntityJoinTables([]);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoadingEntityJoins(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fetchEntityJoinColumns();
|
|
|
|
|
}, [mainTableName]);
|
|
|
|
|
|
|
|
|
|
// 조인 테이블 펼치기/접기 토글
|
|
|
|
|
const toggleJoinTable = (tableName: string) => {
|
|
|
|
|
setExpandedJoinTables((prev) => {
|
|
|
|
|
const newSet = new Set(prev);
|
|
|
|
|
if (newSet.has(tableName)) {
|
|
|
|
|
newSet.delete(tableName);
|
|
|
|
|
} else {
|
|
|
|
|
newSet.add(tableName);
|
|
|
|
|
}
|
|
|
|
|
return newSet;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 엔티티 조인 컬럼 드래그 핸들러
|
|
|
|
|
const handleEntityJoinDragStart = (
|
|
|
|
|
e: React.DragEvent,
|
|
|
|
|
joinTable: EntityJoinTable,
|
|
|
|
|
column: EntityJoinColumn,
|
|
|
|
|
) => {
|
|
|
|
|
// "테이블명.컬럼명" 형식으로 컬럼 정보 생성
|
|
|
|
|
const fullColumnName = `${joinTable.tableName}.${column.columnName}`;
|
|
|
|
|
|
|
|
|
|
const columnData = {
|
|
|
|
|
columnName: fullColumnName,
|
|
|
|
|
columnLabel: column.columnLabel || column.columnName,
|
|
|
|
|
dataType: column.dataType,
|
|
|
|
|
widgetType: "text" as WebType,
|
|
|
|
|
isEntityJoin: true,
|
|
|
|
|
entityJoinTable: joinTable.tableName,
|
|
|
|
|
entityJoinColumn: column.columnName,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 기존 테이블 정보를 기반으로 가상의 테이블 정보 생성
|
|
|
|
|
const virtualTable: TableInfo = {
|
|
|
|
|
tableName: mainTableName || "",
|
|
|
|
|
tableLabel: tables[0]?.tableLabel || mainTableName || "",
|
|
|
|
|
columns: [columnData],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onDragStart(e, virtualTable, columnData);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-10 14:24:16 +09:00
|
|
|
// 이미 배치된 컬럼과 시스템 컬럼을 제외한 테이블 정보 생성
|
2025-10-23 10:07:55 +09:00
|
|
|
const tablesWithAvailableColumns = tables.map((table) => ({
|
|
|
|
|
...table,
|
|
|
|
|
columns: table.columns.filter((col) => {
|
|
|
|
|
const columnKey = `${table.tableName}.${col.columnName}`;
|
2025-11-10 14:24:16 +09:00
|
|
|
// 시스템 컬럼이거나 이미 배치된 컬럼은 제외
|
|
|
|
|
return !systemColumns.has(col.columnName.toLowerCase()) && !placedColumns.has(columnKey);
|
2025-10-23 10:07:55 +09:00
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
2025-10-28 16:26:55 +09:00
|
|
|
// 검색어가 있으면 컬럼 필터링
|
2025-10-23 10:07:55 +09:00
|
|
|
const filteredTables = tablesWithAvailableColumns
|
2025-10-28 16:26:55 +09:00
|
|
|
.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); // 컬럼이 있는 테이블만 표시
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full flex-col">
|
2026-01-15 15:17:52 +09:00
|
|
|
{/* 테이블 선택 버튼 (메인 테이블이 없을 때 또는 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>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-10-28 16:26:55 +09:00
|
|
|
{/* 테이블과 컬럼 평면 목록 */}
|
|
|
|
|
<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>
|
2025-09-02 16:18:38 +09:00
|
|
|
|
2025-10-28 16:26:55 +09:00
|
|
|
{/* 컬럼 목록 (항상 표시) */}
|
|
|
|
|
<div className="space-y-1 pl-2">
|
|
|
|
|
{table.columns.map((column) => (
|
|
|
|
|
<div
|
|
|
|
|
key={column.columnName}
|
2026-01-15 10:39:23 +09:00
|
|
|
className="hover:bg-accent/50 flex cursor-grab items-center gap-2 rounded-md p-2 transition-colors"
|
2025-09-02 16:18:38 +09:00
|
|
|
draggable
|
2025-10-28 16:26:55 +09:00
|
|
|
onDragStart={(e) => onDragStart(e, table, column)}
|
2025-09-02 16:18:38 +09:00
|
|
|
>
|
2026-01-15 12:22:45 +09:00
|
|
|
{getWidgetIcon(column.widgetType)}
|
|
|
|
|
<div className="min-w-0 flex-1">
|
2026-01-15 10:39:23 +09:00
|
|
|
<div
|
|
|
|
|
className="text-xs font-medium"
|
|
|
|
|
title={column.columnLabel || column.columnName}
|
|
|
|
|
>
|
|
|
|
|
{column.columnLabel || column.columnName}
|
2025-10-28 16:26:55 +09:00
|
|
|
</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>
|
2025-09-02 16:18:38 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-28 16:26:55 +09:00
|
|
|
))}
|
2025-09-02 16:18:38 +09:00
|
|
|
</div>
|
2025-10-28 16:26:55 +09:00
|
|
|
</div>
|
|
|
|
|
))}
|
2026-01-15 10:39:23 +09:00
|
|
|
|
|
|
|
|
{/* 엔티티 조인 컬럼 섹션 */}
|
|
|
|
|
{entityJoinTables.length > 0 && (
|
|
|
|
|
<div className="mt-4 space-y-2">
|
|
|
|
|
<div className="flex items-center gap-2 px-2 py-1">
|
|
|
|
|
<Link2 className="h-3.5 w-3.5 text-cyan-600" />
|
|
|
|
|
<span className="text-muted-foreground text-xs font-medium">엔티티 조인 컬럼</span>
|
|
|
|
|
<Badge variant="outline" className="h-4 px-1.5 text-[10px]">
|
|
|
|
|
{entityJoinTables.length}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{entityJoinTables.map((joinTable) => {
|
|
|
|
|
const isExpanded = expandedJoinTables.has(joinTable.tableName);
|
|
|
|
|
// 검색어로 필터링
|
|
|
|
|
const filteredColumns = searchTerm
|
|
|
|
|
? joinTable.availableColumns.filter(
|
|
|
|
|
(col) =>
|
|
|
|
|
col.columnName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
col.columnLabel.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
|
|
|
)
|
|
|
|
|
: joinTable.availableColumns;
|
|
|
|
|
|
|
|
|
|
// 검색 결과가 없으면 표시하지 않음
|
|
|
|
|
if (searchTerm && filteredColumns.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={joinTable.tableName} className="space-y-1">
|
|
|
|
|
{/* 조인 테이블 헤더 */}
|
|
|
|
|
<div
|
|
|
|
|
className="flex cursor-pointer items-center justify-between rounded-md bg-cyan-50 p-2 hover:bg-cyan-100"
|
|
|
|
|
onClick={() => toggleJoinTable(joinTable.tableName)}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{isExpanded ? (
|
|
|
|
|
<ChevronDown className="h-3 w-3 text-cyan-600" />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight className="h-3 w-3 text-cyan-600" />
|
|
|
|
|
)}
|
|
|
|
|
<Building className="h-3.5 w-3.5 text-cyan-600" />
|
|
|
|
|
<span className="text-xs font-semibold text-cyan-800">{joinTable.tableName}</span>
|
|
|
|
|
<Badge variant="secondary" className="h-4 px-1.5 text-[10px]">
|
|
|
|
|
{filteredColumns.length}개
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 조인 컬럼 목록 */}
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div className="space-y-1 pl-4">
|
|
|
|
|
{filteredColumns.map((column) => {
|
|
|
|
|
const fullColumnName = `${joinTable.tableName}.${column.columnName}`;
|
|
|
|
|
const isPlaced = placedColumns.has(fullColumnName);
|
|
|
|
|
|
|
|
|
|
if (isPlaced) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={column.columnName}
|
|
|
|
|
className="flex cursor-grab items-center gap-2 rounded-md border border-cyan-200 bg-cyan-50/50 p-2 transition-colors hover:bg-cyan-100"
|
|
|
|
|
draggable
|
|
|
|
|
onDragStart={(e) => handleEntityJoinDragStart(e, joinTable, column)}
|
|
|
|
|
title="읽기 전용 - 조인된 테이블에서 참조"
|
|
|
|
|
>
|
|
|
|
|
<Link2 className="h-3 w-3 flex-shrink-0 text-cyan-500" />
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<div className="text-xs font-medium" title={column.columnLabel || column.columnName}>
|
|
|
|
|
{column.columnLabel || column.columnName}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-shrink-0 items-center gap-1">
|
|
|
|
|
<Badge variant="secondary" className="h-4 border-gray-300 bg-gray-100 px-1 text-[9px] text-gray-600">
|
|
|
|
|
읽기
|
|
|
|
|
</Badge>
|
|
|
|
|
<Badge variant="outline" className="h-4 border-cyan-300 px-1.5 text-[10px]">
|
|
|
|
|
{column.inputType || "text"}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 로딩 표시 */}
|
|
|
|
|
{loadingEntityJoins && (
|
|
|
|
|
<div className="text-muted-foreground flex items-center justify-center py-4 text-xs">
|
|
|
|
|
엔티티 조인 컬럼 로드 중...
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-02 16:18:38 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default TablesPanel;
|