3d에서 테이블 가져올 때 테이블, 컬럼 코멘트 같이 가져오기
This commit is contained in:
parent
6ccaa85413
commit
dd913d3ecf
|
|
@ -1,7 +1,11 @@
|
||||||
import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector';
|
import {
|
||||||
import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes';
|
DatabaseConnector,
|
||||||
|
ConnectionConfig,
|
||||||
|
QueryResult,
|
||||||
|
} from "../interfaces/DatabaseConnector";
|
||||||
|
import { ConnectionTestResult, TableInfo } from "../types/externalDbTypes";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as mysql from 'mysql2/promise';
|
import * as mysql from "mysql2/promise";
|
||||||
|
|
||||||
export class MariaDBConnector implements DatabaseConnector {
|
export class MariaDBConnector implements DatabaseConnector {
|
||||||
private connection: mysql.Connection | null = null;
|
private connection: mysql.Connection | null = null;
|
||||||
|
|
@ -20,7 +24,7 @@ export class MariaDBConnector implements DatabaseConnector {
|
||||||
password: this.config.password,
|
password: this.config.password,
|
||||||
database: this.config.database,
|
database: this.config.database,
|
||||||
connectTimeout: this.config.connectionTimeoutMillis,
|
connectTimeout: this.config.connectionTimeoutMillis,
|
||||||
ssl: typeof this.config.ssl === 'boolean' ? undefined : this.config.ssl,
|
ssl: typeof this.config.ssl === "boolean" ? undefined : this.config.ssl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +40,9 @@ export class MariaDBConnector implements DatabaseConnector {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
try {
|
try {
|
||||||
await this.connect();
|
await this.connect();
|
||||||
const [rows] = await this.connection!.query("SELECT VERSION() as version");
|
const [rows] = await this.connection!.query(
|
||||||
|
"SELECT VERSION() as version"
|
||||||
|
);
|
||||||
const version = (rows as any[])[0]?.version || "Unknown";
|
const version = (rows as any[])[0]?.version || "Unknown";
|
||||||
const responseTime = Date.now() - startTime;
|
const responseTime = Date.now() - startTime;
|
||||||
await this.disconnect();
|
await this.disconnect();
|
||||||
|
|
@ -111,21 +117,28 @@ export class MariaDBConnector implements DatabaseConnector {
|
||||||
console.log(`[MariaDBConnector] getColumns 호출: tableName=${tableName}`);
|
console.log(`[MariaDBConnector] getColumns 호출: tableName=${tableName}`);
|
||||||
await this.connect();
|
await this.connect();
|
||||||
console.log(`[MariaDBConnector] 연결 완료, 쿼리 실행 시작`);
|
console.log(`[MariaDBConnector] 연결 완료, 쿼리 실행 시작`);
|
||||||
|
|
||||||
const [rows] = await this.connection!.query(`
|
const [rows] = await this.connection!.query(
|
||||||
|
`
|
||||||
SELECT
|
SELECT
|
||||||
COLUMN_NAME as column_name,
|
COLUMN_NAME as column_name,
|
||||||
DATA_TYPE as data_type,
|
DATA_TYPE as data_type,
|
||||||
IS_NULLABLE as is_nullable,
|
IS_NULLABLE as is_nullable,
|
||||||
COLUMN_DEFAULT as column_default
|
COLUMN_DEFAULT as column_default,
|
||||||
|
COLUMN_COMMENT as description
|
||||||
FROM information_schema.COLUMNS
|
FROM information_schema.COLUMNS
|
||||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
||||||
ORDER BY ORDINAL_POSITION;
|
ORDER BY ORDINAL_POSITION;
|
||||||
`, [tableName]);
|
`,
|
||||||
|
[tableName]
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`[MariaDBConnector] 쿼리 결과:`, rows);
|
console.log(`[MariaDBConnector] 쿼리 결과:`, rows);
|
||||||
console.log(`[MariaDBConnector] 결과 개수:`, Array.isArray(rows) ? rows.length : 'not array');
|
console.log(
|
||||||
|
`[MariaDBConnector] 결과 개수:`,
|
||||||
|
Array.isArray(rows) ? rows.length : "not array"
|
||||||
|
);
|
||||||
|
|
||||||
await this.disconnect();
|
await this.disconnect();
|
||||||
return rows as any[];
|
return rows as any[];
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
|
|
||||||
// 동적 계층 구조 설정
|
// 동적 계층 구조 설정
|
||||||
const [hierarchyConfig, setHierarchyConfig] = useState<HierarchyConfig | null>(null);
|
const [hierarchyConfig, setHierarchyConfig] = useState<HierarchyConfig | null>(null);
|
||||||
const [availableTables, setAvailableTables] = useState<string[]>([]);
|
const [availableTables, setAvailableTables] = useState<Array<{ table_name: string; description?: string }>>([]);
|
||||||
const [loadingTables, setLoadingTables] = useState(false);
|
const [loadingTables, setLoadingTables] = useState(false);
|
||||||
|
|
||||||
// 레거시: 테이블 매핑 (구 Area/Location 방식 호환용)
|
// 레거시: 테이블 매핑 (구 Area/Location 방식 호환용)
|
||||||
|
|
@ -210,9 +210,9 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
const { getTables } = await import("@/lib/api/digitalTwin");
|
const { getTables } = await import("@/lib/api/digitalTwin");
|
||||||
const response = await getTables(selectedDbConnection);
|
const response = await getTables(selectedDbConnection);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
const tableNames = response.data.map((t) => t.table_name);
|
// 테이블 정보 전체 저장 (이름 + 설명)
|
||||||
setAvailableTables(tableNames);
|
setAvailableTables(response.data);
|
||||||
console.log("📋 테이블 목록:", tableNames);
|
console.log("📋 테이블 목록:", response.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("테이블 목록 조회 실패:", error);
|
console.error("테이블 목록 조회 실패:", error);
|
||||||
|
|
@ -1266,10 +1266,12 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
try {
|
try {
|
||||||
const response = await ExternalDbConnectionAPI.getTableColumns(selectedDbConnection, tableName);
|
const response = await ExternalDbConnectionAPI.getTableColumns(selectedDbConnection, tableName);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
// 객체 배열을 문자열 배열로 변환
|
// 컬럼 정보 객체 배열로 반환 (이름 + 설명)
|
||||||
return response.data.map((col: any) =>
|
return response.data.map((col: any) => ({
|
||||||
typeof col === "string" ? col : col.column_name || col.COLUMN_NAME || String(col),
|
column_name: typeof col === "string" ? col : col.column_name || col.COLUMN_NAME || String(col),
|
||||||
);
|
data_type: col.data_type || col.DATA_TYPE,
|
||||||
|
description: col.description || col.COLUMN_COMMENT || undefined,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,24 @@ export interface HierarchyConfig {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TableInfo {
|
||||||
|
table_name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColumnInfo {
|
||||||
|
column_name: string;
|
||||||
|
data_type?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface HierarchyConfigPanelProps {
|
interface HierarchyConfigPanelProps {
|
||||||
externalDbConnectionId: number | null;
|
externalDbConnectionId: number | null;
|
||||||
hierarchyConfig: HierarchyConfig | null;
|
hierarchyConfig: HierarchyConfig | null;
|
||||||
onHierarchyConfigChange: (config: HierarchyConfig) => void;
|
onHierarchyConfigChange: (config: HierarchyConfig) => void;
|
||||||
availableTables: string[];
|
availableTables: TableInfo[];
|
||||||
onLoadTables: () => Promise<void>;
|
onLoadTables: () => Promise<void>;
|
||||||
onLoadColumns: (tableName: string) => Promise<string[]>;
|
onLoadColumns: (tableName: string) => Promise<ColumnInfo[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HierarchyConfigPanel({
|
export default function HierarchyConfigPanel({
|
||||||
|
|
@ -65,7 +76,7 @@ export default function HierarchyConfigPanel({
|
||||||
);
|
);
|
||||||
|
|
||||||
const [loadingColumns, setLoadingColumns] = useState(false);
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
||||||
const [columnsCache, setColumnsCache] = useState<{ [tableName: string]: string[] }>({});
|
const [columnsCache, setColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({});
|
||||||
|
|
||||||
// 외부에서 변경된 경우 동기화
|
// 외부에서 변경된 경우 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -187,8 +198,13 @@ export default function HierarchyConfigPanel({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{availableTables.map((table) => (
|
{availableTables.map((table) => (
|
||||||
<SelectItem key={table} value={table} className="text-[10px]">
|
<SelectItem key={table.table_name} value={table.table_name} className="text-[10px]">
|
||||||
{table}
|
<div className="flex flex-col">
|
||||||
|
<span>{table.table_name}</span>
|
||||||
|
{table.description && (
|
||||||
|
<span className="text-[9px] text-muted-foreground">{table.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -209,8 +225,13 @@ export default function HierarchyConfigPanel({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{columnsCache[localConfig.warehouse.tableName].map((col) => (
|
{columnsCache[localConfig.warehouse.tableName].map((col) => (
|
||||||
<SelectItem key={col} value={col} className="text-[10px]">
|
<SelectItem key={col.column_name} value={col.column_name} className="text-[10px]">
|
||||||
{col}
|
<div className="flex flex-col">
|
||||||
|
<span>{col.column_name}</span>
|
||||||
|
{col.description && (
|
||||||
|
<span className="text-[8px] text-muted-foreground">{col.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -228,8 +249,13 @@ export default function HierarchyConfigPanel({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{columnsCache[localConfig.warehouse.tableName].map((col) => (
|
{columnsCache[localConfig.warehouse.tableName].map((col) => (
|
||||||
<SelectItem key={col} value={col} className="text-[10px]">
|
<SelectItem key={col.column_name} value={col.column_name} className="text-[10px]">
|
||||||
{col}
|
<div className="flex flex-col">
|
||||||
|
<span>{col.column_name}</span>
|
||||||
|
{col.description && (
|
||||||
|
<span className="text-[8px] text-muted-foreground">{col.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -287,8 +313,13 @@ export default function HierarchyConfigPanel({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{availableTables.map((table) => (
|
{availableTables.map((table) => (
|
||||||
<SelectItem key={table} value={table} className="text-xs">
|
<SelectItem key={table.table_name} value={table.table_name} className="text-xs">
|
||||||
{table}
|
<div className="flex flex-col">
|
||||||
|
<span>{table.table_name}</span>
|
||||||
|
{table.description && (
|
||||||
|
<span className="text-[10px] text-muted-foreground">{table.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -308,8 +339,13 @@ export default function HierarchyConfigPanel({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{columnsCache[level.tableName].map((col) => (
|
{columnsCache[level.tableName].map((col) => (
|
||||||
<SelectItem key={col} value={col} className="text-xs">
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||||||
{col}
|
<div className="flex flex-col">
|
||||||
|
<span>{col.column_name}</span>
|
||||||
|
{col.description && (
|
||||||
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -327,8 +363,13 @@ export default function HierarchyConfigPanel({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{columnsCache[level.tableName].map((col) => (
|
{columnsCache[level.tableName].map((col) => (
|
||||||
<SelectItem key={col} value={col} className="text-xs">
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||||||
{col}
|
<div className="flex flex-col">
|
||||||
|
<span>{col.column_name}</span>
|
||||||
|
{col.description && (
|
||||||
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -409,8 +450,13 @@ export default function HierarchyConfigPanel({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{availableTables.map((table) => (
|
{availableTables.map((table) => (
|
||||||
<SelectItem key={table} value={table} className="text-xs">
|
<SelectItem key={table.table_name} value={table.table_name} className="text-xs">
|
||||||
{table}
|
<div className="flex flex-col">
|
||||||
|
<span>{table.table_name}</span>
|
||||||
|
{table.description && (
|
||||||
|
<span className="text-[10px] text-muted-foreground">{table.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -430,8 +476,13 @@ export default function HierarchyConfigPanel({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{columnsCache[localConfig.material.tableName].map((col) => (
|
{columnsCache[localConfig.material.tableName].map((col) => (
|
||||||
<SelectItem key={col} value={col} className="text-xs">
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||||||
{col}
|
<div className="flex flex-col">
|
||||||
|
<span>{col.column_name}</span>
|
||||||
|
{col.description && (
|
||||||
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -449,8 +500,13 @@ export default function HierarchyConfigPanel({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{columnsCache[localConfig.material.tableName].map((col) => (
|
{columnsCache[localConfig.material.tableName].map((col) => (
|
||||||
<SelectItem key={col} value={col} className="text-xs">
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||||||
{col}
|
<div className="flex flex-col">
|
||||||
|
<span>{col.column_name}</span>
|
||||||
|
{col.description && (
|
||||||
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -469,8 +525,13 @@ export default function HierarchyConfigPanel({
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__none__">없음</SelectItem>
|
<SelectItem value="__none__">없음</SelectItem>
|
||||||
{columnsCache[localConfig.material.tableName].map((col) => (
|
{columnsCache[localConfig.material.tableName].map((col) => (
|
||||||
<SelectItem key={col} value={col} className="text-xs">
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||||||
{col}
|
<div className="flex flex-col">
|
||||||
|
<span>{col.column_name}</span>
|
||||||
|
{col.description && (
|
||||||
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -489,8 +550,13 @@ export default function HierarchyConfigPanel({
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__none__">없음</SelectItem>
|
<SelectItem value="__none__">없음</SelectItem>
|
||||||
{columnsCache[localConfig.material.tableName].map((col) => (
|
{columnsCache[localConfig.material.tableName].map((col) => (
|
||||||
<SelectItem key={col} value={col} className="text-xs">
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||||||
{col}
|
<div className="flex flex-col">
|
||||||
|
<span>{col.column_name}</span>
|
||||||
|
{col.description && (
|
||||||
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -507,30 +573,35 @@ export default function HierarchyConfigPanel({
|
||||||
</p>
|
</p>
|
||||||
<div className="max-h-60 space-y-2 overflow-y-auto rounded border p-2">
|
<div className="max-h-60 space-y-2 overflow-y-auto rounded border p-2">
|
||||||
{columnsCache[localConfig.material.tableName].map((col) => {
|
{columnsCache[localConfig.material.tableName].map((col) => {
|
||||||
const displayItem = localConfig.material?.displayColumns?.find((d) => d.column === col);
|
const displayItem = localConfig.material?.displayColumns?.find((d) => d.column === col.column_name);
|
||||||
const isSelected = !!displayItem;
|
const isSelected = !!displayItem;
|
||||||
return (
|
return (
|
||||||
<div key={col} className="flex items-center gap-2">
|
<div key={col.column_name} className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const currentDisplay = localConfig.material?.displayColumns || [];
|
const currentDisplay = localConfig.material?.displayColumns || [];
|
||||||
const newDisplay = e.target.checked
|
const newDisplay = e.target.checked
|
||||||
? [...currentDisplay, { column: col, label: col }]
|
? [...currentDisplay, { column: col.column_name, label: col.column_name }]
|
||||||
: currentDisplay.filter((d) => d.column !== col);
|
: currentDisplay.filter((d) => d.column !== col.column_name);
|
||||||
handleMaterialChange("displayColumns", newDisplay);
|
handleMaterialChange("displayColumns", newDisplay);
|
||||||
}}
|
}}
|
||||||
className="h-3 w-3 shrink-0"
|
className="h-3 w-3 shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="w-20 shrink-0 text-[10px]">{col}</span>
|
<div className="flex w-24 shrink-0 flex-col">
|
||||||
|
<span className="text-[10px]">{col.column_name}</span>
|
||||||
|
{col.description && (
|
||||||
|
<span className="text-[8px] text-muted-foreground">{col.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<Input
|
<Input
|
||||||
value={displayItem?.label ?? ""}
|
value={displayItem?.label ?? ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const currentDisplay = localConfig.material?.displayColumns || [];
|
const currentDisplay = localConfig.material?.displayColumns || [];
|
||||||
const newDisplay = currentDisplay.map((d) =>
|
const newDisplay = currentDisplay.map((d) =>
|
||||||
d.column === col ? { ...d, label: e.target.value } : d,
|
d.column === col.column_name ? { ...d, label: e.target.value } : d,
|
||||||
);
|
);
|
||||||
handleMaterialChange("displayColumns", newDisplay);
|
handleMaterialChange("displayColumns", newDisplay);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue