feat: Fleet/Collector/엣지 배포 관련 누적 작업 일괄 커밋
Build and Push Images / build-and-push (push) Has been cancelled
Build and Push Images / build-and-push (push) Has been cancelled
이전 세션들에서 작업된 아래 범위를 모두 포함: Fleet 서브시스템 (src/fleet/) - fleetDeviceService / fleetCommandService / fleetDeploymentService / fleetReleaseService - fleetMetricsService, fleetScriptService, fleetEdgeConfigService - Edge 디바이스 관리, 커맨드 발행, 배포/릴리스, 스크립트 동기화 Collector 확장 - centralMqttForwarder / centralForwarderConfigService - equipmentStateService, pythonHookRunner, scriptCache - Modbus/OPC-UA/S7/XGT 프로토콜 클라이언트 - targetDbIntrospection (저장 DB 조회) Routes / API - automationDashboardRoutes, centralForwarderRoutes, equipmentStateRoutes DB - importEdgeConfig (Python cached config → Pipeline DB) - seedDataSources (external_db_connections 초기 시드) 엣지 배포 리소스 - docker/edge/Dockerfile.backend.prod, Dockerfile.frontend.prod - docker/edge/docker-compose.edge.yml 프론트엔드 - admin/automaticMng (centralForwarder, dashboard, equipmentState) - admin/fleet (commands, devices, deployments, releases, scripts, alerts) - admin/pipeline-device 개선 (저장 DB 드롭다운, 태그 매핑 등) - ExternalDbConnectionModal, ScriptsManagerDialog 등 신규 컴포넌트 - lib/api: automationDashboard, centralForwarder, equipmentState, fleet docs/ - EDGE_SERVER_STRUCTURE, FLEET_COMPLETE, FLEET_EDGE_INTEGRATION, FLEET_HOOK_INTEGRATION Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// 자동화 통합 대시보드 API (조회 전용)
|
||||
|
||||
import { apiClient } from "./client";
|
||||
|
||||
export interface AutomationStats {
|
||||
batches_total: number;
|
||||
batches_active: number;
|
||||
pollings_total: number;
|
||||
pollings_active: number;
|
||||
pollings_connected: number;
|
||||
total_tags: number;
|
||||
forwarders_total: number;
|
||||
forwarders_enabled: number;
|
||||
messages_forwarded_total: number;
|
||||
}
|
||||
|
||||
export interface BatchRow {
|
||||
id: number;
|
||||
batch_name: string;
|
||||
cron_schedule: string;
|
||||
is_active: string | boolean;
|
||||
company_code?: string;
|
||||
last_run_date?: string;
|
||||
last_run_result?: string;
|
||||
next_run_date?: string;
|
||||
}
|
||||
|
||||
export interface PollingRow {
|
||||
id: number;
|
||||
connection_name: string;
|
||||
protocol: string;
|
||||
host: string;
|
||||
port: number;
|
||||
polling_interval_ms: number;
|
||||
is_active: string;
|
||||
status: string;
|
||||
last_test_result?: string;
|
||||
last_test_date?: string;
|
||||
target_db_connection_id?: number | null;
|
||||
target_table_name?: string | null;
|
||||
tag_count: number;
|
||||
last_collected_at?: string | null;
|
||||
}
|
||||
|
||||
export interface ForwarderRow {
|
||||
id: number;
|
||||
config_name: string;
|
||||
company_code: string;
|
||||
company_id: string;
|
||||
edge_id: string;
|
||||
broker_host: string;
|
||||
broker_port: number;
|
||||
topic_pattern: string;
|
||||
batch_size: number;
|
||||
batch_timeout_ms: number;
|
||||
is_enabled: string;
|
||||
messages_forwarded: number | null;
|
||||
messages_failed: number | null;
|
||||
messages_dropped: number | null;
|
||||
batches_sent: number | null;
|
||||
last_published_at?: string | null;
|
||||
last_error?: string | null;
|
||||
is_connected: string | null;
|
||||
reconnect_attempts: number | null;
|
||||
}
|
||||
|
||||
export interface DashboardOverview {
|
||||
stats: AutomationStats;
|
||||
batches: BatchRow[];
|
||||
pollings: PollingRow[];
|
||||
forwarders: ForwarderRow[];
|
||||
}
|
||||
|
||||
export const AutomationDashboardAPI = {
|
||||
async overview(): Promise<DashboardOverview> {
|
||||
const r = await apiClient.get<{ success: boolean; data: DashboardOverview }>(
|
||||
"/api/automation-dashboard/overview"
|
||||
);
|
||||
return r.data.data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
// Central MQTT Forwarder 관리 API 클라이언트
|
||||
|
||||
import { apiClient } from "./client";
|
||||
|
||||
export interface CentralForwarderConfig {
|
||||
id?: number;
|
||||
config_name: string;
|
||||
company_code?: string;
|
||||
company_id: string;
|
||||
edge_id: string;
|
||||
broker_host: string;
|
||||
broker_port: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
use_tls?: string;
|
||||
client_id_prefix?: string;
|
||||
topic_pattern?: string;
|
||||
status_topic_pattern?: string;
|
||||
batch_size?: number;
|
||||
batch_timeout_ms?: number;
|
||||
heartbeat_interval_sec?: number;
|
||||
qos?: number;
|
||||
is_enabled?: string;
|
||||
description?: string;
|
||||
created_date?: string;
|
||||
updated_date?: string;
|
||||
}
|
||||
|
||||
export interface ForwarderRuntimeStatus {
|
||||
config_id: number;
|
||||
config_name: string;
|
||||
company_code: string;
|
||||
edge_id: string;
|
||||
broker: string;
|
||||
connected: boolean;
|
||||
buffered: number;
|
||||
messagesForwarded: number;
|
||||
messagesFailed: number;
|
||||
messagesDropped: number;
|
||||
batchesSent: number;
|
||||
lastPublishedAt: string | null;
|
||||
startedAt: string;
|
||||
isConnected: boolean;
|
||||
reconnectAttempts: number;
|
||||
lastError: string | null;
|
||||
lastErrorAt: string | null;
|
||||
}
|
||||
|
||||
const BASE = "/api/central-forwarder";
|
||||
|
||||
export const CentralForwarderAPI = {
|
||||
async list(companyCode?: string): Promise<CentralForwarderConfig[]> {
|
||||
const url = companyCode ? `${BASE}?company_code=${companyCode}` : BASE;
|
||||
const r = await apiClient.get<{ success: boolean; data: CentralForwarderConfig[] }>(url);
|
||||
return r.data.data || [];
|
||||
},
|
||||
|
||||
async get(id: number): Promise<CentralForwarderConfig> {
|
||||
const r = await apiClient.get<{ success: boolean; data: CentralForwarderConfig }>(
|
||||
`${BASE}/${id}`
|
||||
);
|
||||
return r.data.data;
|
||||
},
|
||||
|
||||
async create(input: CentralForwarderConfig): Promise<{ id: number }> {
|
||||
const r = await apiClient.post<{ success: boolean; data: { id: number } }>(BASE, input);
|
||||
return r.data.data;
|
||||
},
|
||||
|
||||
async update(id: number, input: Partial<CentralForwarderConfig>): Promise<void> {
|
||||
await apiClient.put(`${BASE}/${id}`, input);
|
||||
},
|
||||
|
||||
async delete(id: number): Promise<void> {
|
||||
await apiClient.delete(`${BASE}/${id}`);
|
||||
},
|
||||
|
||||
async toggle(id: number, enabled: boolean): Promise<void> {
|
||||
await apiClient.post(`${BASE}/${id}/toggle`, { enabled });
|
||||
},
|
||||
|
||||
async runtimeStatus(): Promise<ForwarderRuntimeStatus[]> {
|
||||
const r = await apiClient.get<{ success: boolean; data: ForwarderRuntimeStatus[] }>(
|
||||
`${BASE}/runtime/status`
|
||||
);
|
||||
return r.data.data || [];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
// Equipment Current State API 클라이언트
|
||||
|
||||
import { apiClient } from "./client";
|
||||
|
||||
export interface EquipmentTagState {
|
||||
id: number;
|
||||
connection_id: number;
|
||||
company_code: string;
|
||||
tag_name: string;
|
||||
tag_display_name: string | null;
|
||||
tag_unit: string | null;
|
||||
value_numeric: number | null;
|
||||
value_text: string | null;
|
||||
value_boolean: boolean | null;
|
||||
quality: string;
|
||||
last_collected_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ConnectionStatusSummary {
|
||||
connection_id: number;
|
||||
connection_name: string;
|
||||
protocol: string;
|
||||
host: string;
|
||||
port: number;
|
||||
connection_status: string;
|
||||
last_test_result: string | null;
|
||||
last_test_message: string | null;
|
||||
last_test_date: string | null;
|
||||
company_code: string;
|
||||
tag_count: number;
|
||||
last_collected_at: string | null;
|
||||
good_tag_count: number;
|
||||
}
|
||||
|
||||
const BASE = "/api/equipment-state";
|
||||
|
||||
export const EquipmentStateAPI = {
|
||||
async summary(companyCode?: string): Promise<ConnectionStatusSummary[]> {
|
||||
const url = companyCode ? `${BASE}/summary?company_code=${companyCode}` : `${BASE}/summary`;
|
||||
const r = await apiClient.get<{ success: boolean; data: ConnectionStatusSummary[] }>(url);
|
||||
return r.data.data || [];
|
||||
},
|
||||
|
||||
async tagsByConnection(connectionId: number): Promise<EquipmentTagState[]> {
|
||||
const r = await apiClient.get<{ success: boolean; data: EquipmentTagState[] }>(
|
||||
`${BASE}/${connectionId}`
|
||||
);
|
||||
return r.data.data || [];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,213 @@
|
||||
import { apiClient } from "./client";
|
||||
|
||||
const BASE = "/fleet";
|
||||
|
||||
export interface FleetDevice {
|
||||
id?: number;
|
||||
device_id: string;
|
||||
company_code?: string;
|
||||
device_name?: string;
|
||||
device_type?: string;
|
||||
ip_address?: string;
|
||||
mac_address?: string;
|
||||
hardware_fingerprint?: string;
|
||||
last_seen_at?: string;
|
||||
is_online?: boolean;
|
||||
equipment_id?: number | null;
|
||||
equipment_name?: string;
|
||||
equipment_code?: string;
|
||||
agent_version?: string;
|
||||
os_info?: Record<string, any>;
|
||||
hardware_info?: Record<string, any>;
|
||||
device_group?: string;
|
||||
tags?: any[];
|
||||
}
|
||||
|
||||
export interface FleetCommand {
|
||||
id?: number;
|
||||
device_id: string;
|
||||
command_type: string;
|
||||
payload?: Record<string, any>;
|
||||
status?: string;
|
||||
result?: Record<string, any>;
|
||||
error_message?: string;
|
||||
issued_by?: string;
|
||||
issued_at?: string;
|
||||
sent_at?: string;
|
||||
responded_at?: string;
|
||||
}
|
||||
|
||||
export interface FleetAlert {
|
||||
id: number;
|
||||
rule_id: number;
|
||||
rule_name?: string;
|
||||
device_id: string;
|
||||
severity: string;
|
||||
title: string;
|
||||
message: string;
|
||||
metric: string;
|
||||
value: number;
|
||||
threshold: number;
|
||||
status: "open" | "acknowledged" | "resolved";
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const fleetApi = {
|
||||
// 디바이스
|
||||
getDevices: (filter?: { is_online?: boolean; search?: string }) =>
|
||||
apiClient.get(`${BASE}/devices`, { params: filter }).then((r) => r.data),
|
||||
|
||||
getDevice: (deviceId: string) =>
|
||||
apiClient.get(`${BASE}/devices/${deviceId}`).then((r) => r.data),
|
||||
|
||||
registerDevice: (data: Partial<FleetDevice>) =>
|
||||
apiClient.post(`${BASE}/devices/register`, data).then((r) => r.data),
|
||||
|
||||
updateDevice: (deviceId: string, data: Partial<FleetDevice>) =>
|
||||
apiClient.patch(`${BASE}/devices/${deviceId}`, data).then((r) => r.data),
|
||||
|
||||
deleteDevice: (deviceId: string) =>
|
||||
apiClient.delete(`${BASE}/devices/${deviceId}`).then((r) => r.data),
|
||||
|
||||
getMetrics: (deviceId: string, limit = 100) =>
|
||||
apiClient.get(`${BASE}/devices/${deviceId}/metrics`, { params: { limit } }).then((r) => r.data),
|
||||
|
||||
// 커맨드
|
||||
getCommands: (filter?: { device_id?: string; status?: string; limit?: number }) =>
|
||||
apiClient.get(`${BASE}/commands`, { params: filter }).then((r) => r.data),
|
||||
|
||||
getCommandTypes: () =>
|
||||
apiClient.get(`${BASE}/commands/types`).then((r) => r.data),
|
||||
|
||||
issueCommand: (data: { device_id: string; command_type: string; payload?: any; timeout_sec?: number }) =>
|
||||
apiClient.post(`${BASE}/commands`, data).then((r) => r.data),
|
||||
|
||||
// 알림
|
||||
getAlerts: (status: string = "open") =>
|
||||
apiClient.get(`${BASE}/alerts`, { params: { status } }).then((r) => r.data),
|
||||
|
||||
ackAlert: (id: number) =>
|
||||
apiClient.post(`${BASE}/alerts/${id}/ack`).then((r) => r.data),
|
||||
|
||||
resolveAlert: (id: number) =>
|
||||
apiClient.post(`${BASE}/alerts/${id}/resolve`).then((r) => r.data),
|
||||
|
||||
getAlertRules: () =>
|
||||
apiClient.get(`${BASE}/alert-rules`).then((r) => r.data),
|
||||
|
||||
// 배포
|
||||
getDeployments: () =>
|
||||
apiClient.get(`${BASE}/deployments`).then((r) => r.data),
|
||||
|
||||
getReleases: () =>
|
||||
apiClient.get(`${BASE}/releases`).then((r) => r.data),
|
||||
|
||||
// 통계
|
||||
getStats: () =>
|
||||
apiClient.get(`${BASE}/stats`).then((r) => r.data),
|
||||
|
||||
// 실시간 데이터
|
||||
getLatestValues: (deviceId: string) =>
|
||||
apiClient.get(`${BASE}/devices/${deviceId}/latest-values`).then((r) => r.data),
|
||||
|
||||
getLatestValuesByEquipment: (equipmentId: number) =>
|
||||
apiClient.get(`${BASE}/equipment/${equipmentId}/latest-values`).then((r) => r.data),
|
||||
|
||||
getTagTimeseries: (deviceId: string, tagName: string, limit = 500) =>
|
||||
apiClient
|
||||
.get(`${BASE}/devices/${deviceId}/tags/${encodeURIComponent(tagName)}/timeseries`, {
|
||||
params: { limit },
|
||||
})
|
||||
.then((r) => r.data),
|
||||
|
||||
getDataStats: (deviceId?: string) =>
|
||||
apiClient.get(`${BASE}/data/stats`, { params: { device_id: deviceId } }).then((r) => r.data),
|
||||
|
||||
// ===== Python Hook 스크립트 =====
|
||||
getHookTypes: () =>
|
||||
apiClient.get(`${BASE}/scripts/hook-types`).then((r) => r.data),
|
||||
|
||||
listScripts: (filter?: any) =>
|
||||
apiClient.get(`${BASE}/scripts`, { params: filter }).then((r) => r.data),
|
||||
|
||||
getScript: (id: number) =>
|
||||
apiClient.get(`${BASE}/scripts/${id}`).then((r) => r.data),
|
||||
|
||||
createScript: (data: any) =>
|
||||
apiClient.post(`${BASE}/scripts`, data).then((r) => r.data),
|
||||
|
||||
updateScript: (id: number, data: any) =>
|
||||
apiClient.put(`${BASE}/scripts/${id}`, data).then((r) => r.data),
|
||||
|
||||
deleteScript: (id: number) =>
|
||||
apiClient.delete(`${BASE}/scripts/${id}`).then((r) => r.data),
|
||||
|
||||
dryRunScript: (code: string, hook_type: string, test_input: any, timeout_ms?: number) =>
|
||||
apiClient.post(`${BASE}/scripts/dry-run`, { code, hook_type, test_input, timeout_ms }).then((r) => r.data),
|
||||
|
||||
getScriptVersions: (id: number) =>
|
||||
apiClient.get(`${BASE}/scripts/${id}/versions`).then((r) => r.data),
|
||||
|
||||
getScriptVersion: (id: number, version: number) =>
|
||||
apiClient.get(`${BASE}/scripts/${id}/versions/${version}`).then((r) => r.data),
|
||||
|
||||
rollbackScript: (id: number, version: number) =>
|
||||
apiClient.post(`${BASE}/scripts/${id}/rollback/${version}`).then((r) => r.data),
|
||||
|
||||
// ===== 릴리즈 =====
|
||||
getReleases: (filter?: any) => apiClient.get(`${BASE}/releases`, { params: filter }).then(r => r.data),
|
||||
getRelease: (id: number) => apiClient.get(`${BASE}/releases/${id}`).then(r => r.data),
|
||||
createRelease: (data: any) => apiClient.post(`${BASE}/releases`, data).then(r => r.data),
|
||||
updateRelease: (id: number, data: any) => apiClient.put(`${BASE}/releases/${id}`, data).then(r => r.data),
|
||||
deleteRelease: (id: number) => apiClient.delete(`${BASE}/releases/${id}`).then(r => r.data),
|
||||
transitionRelease: (id: number, status: string) =>
|
||||
apiClient.post(`${BASE}/releases/${id}/transition`, { status }).then(r => r.data),
|
||||
|
||||
// ===== 배포 =====
|
||||
createDeployment: (data: any) => apiClient.post(`${BASE}/deployments`, data).then(r => r.data),
|
||||
getDeploymentDetail: (id: number) => apiClient.get(`${BASE}/deployments/${id}`).then(r => r.data),
|
||||
getDeploymentStatus: (id: number) => apiClient.get(`${BASE}/deployments/${id}/status`).then(r => r.data),
|
||||
startDeployment: (id: number) => apiClient.post(`${BASE}/deployments/${id}/start`).then(r => r.data),
|
||||
cancelDeployment: (id: number) => apiClient.post(`${BASE}/deployments/${id}/cancel`).then(r => r.data),
|
||||
rollbackDeployment: (id: number) => apiClient.post(`${BASE}/deployments/${id}/rollback`).then(r => r.data),
|
||||
|
||||
// ===== Harbor =====
|
||||
getHarborProjects: () => apiClient.get(`${BASE}/harbor/projects`).then(r => r.data),
|
||||
getHarborRepos: (project: string) => apiClient.get(`${BASE}/harbor/projects/${project}/repos`).then(r => r.data),
|
||||
getHarborTags: (project: string, repo: string) =>
|
||||
apiClient.get(`${BASE}/harbor/projects/${project}/repos/${repo}/tags`).then(r => r.data),
|
||||
pingHarbor: () => apiClient.get(`${BASE}/harbor/ping`).then(r => r.data),
|
||||
|
||||
// ===== 태그 템플릿 =====
|
||||
getTagTemplates: (filter?: any) => apiClient.get(`${BASE}/tag-templates`, { params: filter }).then(r => r.data),
|
||||
getTagTemplate: (id: number) => apiClient.get(`${BASE}/tag-templates/${id}`).then(r => r.data),
|
||||
createTagTemplate: (data: any) => apiClient.post(`${BASE}/tag-templates`, data).then(r => r.data),
|
||||
updateTagTemplate: (id: number, data: any) => apiClient.put(`${BASE}/tag-templates/${id}`, data).then(r => r.data),
|
||||
deleteTagTemplate: (id: number) => apiClient.delete(`${BASE}/tag-templates/${id}`).then(r => r.data),
|
||||
applyTagTemplate: (templateId: number, connectionId: number, overwrite = false) =>
|
||||
apiClient.post(`${BASE}/tag-templates/${templateId}/apply/${connectionId}`, { overwrite }).then(r => r.data),
|
||||
|
||||
// ===== 알림 규칙 =====
|
||||
createAlertRule: (data: any) => apiClient.post(`${BASE}/alert-rules`, data).then(r => r.data),
|
||||
updateAlertRule: (id: number, data: any) => apiClient.put(`${BASE}/alert-rules/${id}`, data).then(r => r.data),
|
||||
deleteAlertRule: (id: number) => apiClient.delete(`${BASE}/alert-rules/${id}`).then(r => r.data),
|
||||
toggleAlertRule: (id: number) => apiClient.post(`${BASE}/alert-rules/${id}/toggle`).then(r => r.data),
|
||||
|
||||
// ===== V1 매핑 =====
|
||||
getV1Mappings: (filter?: any) => apiClient.get(`${BASE}/v1-mappings`, { params: filter }).then(r => r.data),
|
||||
createV1Mapping: (data: any) => apiClient.post(`${BASE}/v1-mappings`, data).then(r => r.data),
|
||||
updateV1Mapping: (id: number, data: any) => apiClient.put(`${BASE}/v1-mappings/${id}`, data).then(r => r.data),
|
||||
deleteV1Mapping: (id: number) => apiClient.delete(`${BASE}/v1-mappings/${id}`).then(r => r.data),
|
||||
|
||||
// ===== PLC 상태 =====
|
||||
getPlcStatus: (filter?: any) => apiClient.get(`${BASE}/plc-status`, { params: filter }).then(r => r.data),
|
||||
getPlcSummary: () => apiClient.get(`${BASE}/plc-status/summary`).then(r => r.data),
|
||||
|
||||
// ===== Audit =====
|
||||
getAuditLogs: (filter?: any) => apiClient.get(`${BASE}/audit-logs`, { params: filter }).then(r => r.data),
|
||||
getAuditStats: () => apiClient.get(`${BASE}/audit-logs/stats`).then(r => r.data),
|
||||
|
||||
// ===== Provisioning =====
|
||||
getPreRegistered: () => apiClient.get(`${BASE}/provision/pre-registered`).then(r => r.data),
|
||||
preRegister: (data: any) => apiClient.post(`${BASE}/provision/pre-register`, data).then(r => r.data),
|
||||
};
|
||||
@@ -7,6 +7,10 @@ export const pipelineDeviceApi = {
|
||||
getProtocols: () =>
|
||||
apiClient.get(`${BASE}/protocols`).then((r) => r.data),
|
||||
|
||||
// 장비 목록 (pipeline_equipment)
|
||||
getEquipmentList: (search?: string) =>
|
||||
apiClient.get(`${BASE}/equipment-list`, { params: search ? { search } : {} }).then((r) => r.data),
|
||||
|
||||
// 연결 CRUD
|
||||
getConnections: (params?: { protocol?: string; is_active?: string; search?: string; status?: string }) =>
|
||||
apiClient.get(BASE, { params }).then((r) => r.data),
|
||||
@@ -21,6 +25,36 @@ export const pipelineDeviceApi = {
|
||||
testConnection: (id: number) =>
|
||||
apiClient.post(`${BASE}/${id}/test`).then((r) => r.data),
|
||||
|
||||
// 훅 체인 테스트 (원본값 → 체인 실행 → 결과 + DB 저장 옵션)
|
||||
testChain: (
|
||||
id: number,
|
||||
payload: { tag_name: string; raw_value: unknown; save_to_db?: boolean }
|
||||
) =>
|
||||
apiClient.post(`${BASE}/${id}/test-chain`, payload).then((r) => r.data),
|
||||
|
||||
// 수동 1회 수집 (실제 PLC에서 읽기 + 훅 적용 + DB 저장)
|
||||
collectOnce: (id: number) =>
|
||||
apiClient.post(`${BASE}/${id}/collect-once`).then((r) => r.data),
|
||||
|
||||
// Target DB introspection
|
||||
listTargetDatabases: () =>
|
||||
apiClient.get(`${BASE}/target-databases`).then((r) => r.data),
|
||||
listTargetTables: (dbId: number) =>
|
||||
apiClient.get(`${BASE}/target-databases/${dbId}/tables`).then((r) => r.data),
|
||||
listTargetColumns: (dbId: number, tableName: string) =>
|
||||
apiClient
|
||||
.get(`${BASE}/target-databases/${dbId}/tables/${tableName}/columns`)
|
||||
.then((r) => r.data),
|
||||
|
||||
// 태그 컬럼 매핑 일괄 업데이트
|
||||
updateTagColumnMapping: (
|
||||
connectionId: number,
|
||||
mapping: Array<{ tag_id: number; target_column_name: string | null }>
|
||||
) =>
|
||||
apiClient
|
||||
.put(`${BASE}/${connectionId}/tag-column-mapping`, { mapping })
|
||||
.then((r) => r.data),
|
||||
|
||||
// 태그 매핑
|
||||
getTagMappings: (connectionId: number) =>
|
||||
apiClient.get(`${BASE}/${connectionId}/tags`).then((r) => r.data),
|
||||
|
||||
Reference in New Issue
Block a user