ERP-node/frontend/components/dataflow/node-editor/panels/properties/TableSourceProperties.tsx

339 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
/**
* 테이블 소스 노드 속성 편집
*/
import { useEffect, useState } from "react";
import { Check, ChevronsUpDown, Table, FileText } from "lucide-react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
import { tableTypeApi } from "@/lib/api/screen";
import type { TableSourceNodeData } from "@/types/node-editor";
interface TableSourcePropertiesProps {
nodeId: string;
data: TableSourceNodeData;
}
interface TableOption {
tableName: string;
displayName: string;
description: string;
label: string; // 표시용 (라벨 또는 테이블명)
}
export function TableSourceProperties({ nodeId, data }: TableSourcePropertiesProps) {
const { updateNode } = useFlowEditorStore();
const [displayName, setDisplayName] = useState(data.displayName || data.tableName);
const [tableName, setTableName] = useState(data.tableName);
// 🆕 데이터 소스 타입 (기본값: context-data)
const [dataSourceType, setDataSourceType] = useState<"context-data" | "table-all">(
(data as any).dataSourceType || "context-data"
);
// 테이블 선택 관련 상태
const [tables, setTables] = useState<TableOption[]>([]);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
// 데이터 변경 시 로컬 상태 업데이트
useEffect(() => {
setDisplayName(data.displayName || data.tableName);
setTableName(data.tableName);
setDataSourceType((data as any).dataSourceType || "context-data");
}, [data.displayName, data.tableName, (data as any).dataSourceType]);
// 테이블 목록 로딩
useEffect(() => {
loadTables();
}, []);
/**
* 테이블 목록 로드
*/
const loadTables = async () => {
try {
setLoading(true);
console.log("🔍 테이블 목록 로딩 중...");
const tableList = await tableTypeApi.getTables();
// 테이블 목록 변환 (라벨 또는 displayName 우선 표시)
const options: TableOption[] = tableList.map((table) => {
// tableLabel이 있으면 우선 사용, 없으면 displayName, 그것도 없으면 tableName
const label = (table as any).tableLabel || table.displayName || table.tableName || "알 수 없는 테이블";
return {
tableName: table.tableName,
displayName: table.displayName || table.tableName,
description: table.description || "",
label,
};
});
setTables(options);
console.log(`✅ 테이블 ${options.length}개 로딩 완료`);
} catch (error) {
console.error("❌ 테이블 목록 로딩 실패:", error);
setTables([]);
} finally {
setLoading(false);
}
};
/**
* 테이블 선택 핸들러 (즉시 노드 업데이트 + 컬럼 로드)
*/
const handleTableSelect = async (selectedTableName: string) => {
const selectedTable = tables.find((t) => t.tableName === selectedTableName);
if (selectedTable) {
const newTableName = selectedTable.tableName;
const newDisplayName = selectedTable.label;
setTableName(newTableName);
setDisplayName(newDisplayName);
setOpen(false);
// 컬럼 정보 로드
console.log(`🔍 테이블 "${newTableName}" 컬럼 로드 중...`);
try {
const columns = await tableTypeApi.getColumns(newTableName);
console.log("🔍 API에서 받은 컬럼 데이터:", columns);
const fields = columns.map((col: any) => ({
name: col.column_name || col.columnName,
type: col.data_type || col.dataType || "unknown",
nullable: col.is_nullable === "YES" || col.isNullable === true,
// displayName이 라벨입니다!
label: col.displayName || col.label_ko || col.columnLabel || col.column_label,
}));
console.log(`${fields.length}개 컬럼 로드 완료:`, fields);
// 필드 정보와 함께 노드 업데이트
updateNode(nodeId, {
displayName: newDisplayName,
tableName: newTableName,
fields,
});
} catch (error) {
console.error("❌ 컬럼 로드 실패:", error);
// 실패해도 테이블 정보는 업데이트
updateNode(nodeId, {
displayName: newDisplayName,
tableName: newTableName,
fields: [],
});
}
console.log(`✅ 테이블 선택: ${newTableName} (${newDisplayName})`);
}
};
/**
* 표시 이름 변경 핸들러
*/
const handleDisplayNameChange = (newDisplayName: string) => {
setDisplayName(newDisplayName);
updateNode(nodeId, {
displayName: newDisplayName,
tableName,
});
};
/**
* 🆕 데이터 소스 타입 변경 핸들러
*/
const handleDataSourceTypeChange = (newType: "context-data" | "table-all") => {
setDataSourceType(newType);
updateNode(nodeId, {
dataSourceType: newType,
});
console.log(`✅ 데이터 소스 타입 변경: ${newType}`);
};
// 현재 선택된 테이블의 라벨 찾기
const selectedTableLabel = tables.find((t) => t.tableName === tableName)?.label || tableName;
return (
<div className="space-y-4 p-4 pb-8">
{/* 기본 정보 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
<div className="space-y-3">
<div>
<Label htmlFor="displayName" className="text-xs">
</Label>
<Input
id="displayName"
value={displayName}
onChange={(e) => handleDisplayNameChange(e.target.value)}
className="mt-1"
placeholder="노드 표시 이름"
/>
</div>
{/* 테이블 선택 Combobox */}
<div>
<Label className="text-xs"> </Label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="mt-1 w-full justify-between"
disabled={loading}
>
{loading ? (
<span className="text-muted-foreground"> ...</span>
) : tableName ? (
<span className="truncate">{selectedTableLabel}</span>
) : (
<span className="text-muted-foreground"> </span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[320px] p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="h-9" />
<CommandList>
<CommandEmpty> .</CommandEmpty>
<CommandGroup>
<ScrollArea className="h-[300px]">
{tables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.label} ${table.tableName} ${table.description}`}
onSelect={() => handleTableSelect(table.tableName)}
className="cursor-pointer"
>
<Check
className={cn(
"mr-2 h-4 w-4",
tableName === table.tableName ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{table.label}</span>
{table.label !== table.tableName && (
<span className="text-muted-foreground text-xs">{table.tableName}</span>
)}
{table.description && (
<span className="text-muted-foreground text-xs">{table.description}</span>
)}
</div>
</CommandItem>
))}
</ScrollArea>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{tableName && selectedTableLabel !== tableName && (
<p className="text-muted-foreground mt-1 text-xs">
: <code className="rounded bg-gray-100 px-1 py-0.5">{tableName}</code>
</p>
)}
</div>
</div>
</div>
{/* 🆕 데이터 소스 설정 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
<div className="space-y-3">
<div>
<Label className="text-xs"> </Label>
<Select value={dataSourceType} onValueChange={handleDataSourceTypeChange}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="데이터 소스 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="context-data">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4" />
<div className="flex flex-col">
<span className="font-medium"> </span>
<span className="text-muted-foreground text-xs">
(, )
</span>
</div>
</div>
</SelectItem>
<SelectItem value="table-all">
<div className="flex items-center gap-2">
<Table className="h-4 w-4" />
<div className="flex flex-col">
<span className="font-medium"> </span>
<span className="text-muted-foreground text-xs">
( )
</span>
</div>
</div>
</SelectItem>
</SelectContent>
</Select>
{/* 설명 텍스트 */}
<div className="mt-2 rounded bg-blue-50 p-3 text-xs text-blue-700">
{dataSourceType === "context-data" ? (
<>
<p className="font-medium mb-1">💡 </p>
<p> ( , ) .</p>
<p className="mt-1 text-blue-600"> 데이터: 1개 </p>
<p className="text-blue-600"> 선택: N개 </p>
</>
) : (
<>
<p className="font-medium mb-1">📊 </p>
<p> ** ** .</p>
<p className="mt-1 text-orange-600 font-medium"> </p>
</>
)}
</div>
</div>
</div>
</div>
{/* 필드 정보 */}
<div>
<h3 className="mb-3 text-sm font-semibold">
{data.fields && data.fields.length > 0 && `(${data.fields.length}개)`}
</h3>
{data.fields && data.fields.length > 0 ? (
<div className="max-h-[300px] space-y-1 overflow-y-auto rounded border bg-gray-50 p-2">
{data.fields.map((field) => (
<div key={field.name} className="flex items-center justify-between rounded bg-white px-2 py-1.5 text-xs">
<span className="truncate font-mono text-gray-700" title={field.name}>
{field.name}
</span>
<span className="ml-2 shrink-0 text-gray-400">{field.type}</span>
</div>
))}
</div>
) : (
<div className="rounded border p-4 text-center text-xs text-gray-400"> </div>
)}
</div>
</div>
);
}