feat: Fleet/Collector/엣지 배포 관련 누적 작업 일괄 커밋
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:
chpark
2026-04-23 20:00:06 +09:00
parent 01625d9efd
commit 4c1dc4082e
77 changed files with 14639 additions and 205 deletions
@@ -0,0 +1,162 @@
/**
* 기본 데이터 소스 연결 시드
*
* 부팅 시 IDC 엣지 관련 연결 정보를 external_db_connections 테이블에 등록.
* 이미 같은 이름의 연결이 있으면 스킵.
*
* 등록 대상 (2026-04-21 기준):
* - IDC Central TimescaleDB (edge_telemetry) — 수집 데이터 시계열
* - IDC Digital-Twin PostgreSQL — 메타데이터
* - IDC Fleet PostgreSQL — fleet 관리 메타
* - IDC Vex Space PostgreSQL — Vex Space 전용
*/
import { query, queryOne } from "./db";
import { PasswordEncryption } from "../utils/passwordEncryption";
import logger from "../utils/logger";
interface DefaultDataSource {
connection_name: string;
description: string;
db_type: "postgresql" | "mysql" | "mariadb" | "mssql" | "oracle";
host: string;
port: number;
database_name: string;
username: string;
password: string;
company_code: string;
is_active: "Y" | "N";
connection_options?: Record<string, unknown>;
}
const DEFAULT_SOURCES: DefaultDataSource[] = [
{
connection_name: "IDC_TimescaleDB_edge_telemetry",
description:
"IDC 중앙 TimescaleDB — 엣지 수집 데이터 시계열 (edge_telemetry DB). digital-twin-timescale NodePort :30543",
db_type: "postgresql",
host: "211.115.91.170",
port: 30543,
database_name: "edge_telemetry",
username: "telemetry_user",
password: "qlalfqjsgh11",
company_code: "*",
is_active: "Y",
connection_options: { note: "TimescaleDB extension enabled" },
},
{
connection_name: "IDC_DigitalTwin_Postgres",
description:
"IDC 중앙 Digital-Twin 웹 메타데이터 PostgreSQL (NodePort :30533). digital-twin-web-postgres",
db_type: "postgresql",
host: "211.115.91.170",
port: 30533,
database_name: "digital_twin_web_database",
username: "digital_twin_web_user_dev",
password: "", // 비어 있으면 스킵
company_code: "*",
is_active: "N", // 비밀번호 모르므로 비활성으로 등록
},
{
connection_name: "IDC_VexSpace_Postgres",
description: "IDC VexSpace 전용 PostgreSQL (NodePort :31141). vexspace-postgres",
db_type: "postgresql",
host: "211.115.91.170",
port: 31141,
database_name: "vexspace",
username: "vexspace_user",
password: "", // 비어 있으면 스킵
company_code: "*",
is_active: "N",
},
{
connection_name: "IDC_Fleet_Postgres",
description: "IDC Fleet 관리 PostgreSQL (NodePort :31985). fleet-postgres",
db_type: "postgresql",
host: "211.115.91.170",
port: 31985,
database_name: "fleet",
username: "fleet_user",
password: "", // 비밀번호 모르므로 비활성
company_code: "*",
is_active: "N",
},
];
/**
* 기본 데이터 소스 연결을 시드. 이미 존재하면 스킵.
* 비밀번호가 비어있는 항목도 등록하지만 is_active='N'으로 두어 사용자가 나중에 채울 수 있게.
*/
export async function seedDefaultDataSources(): Promise<void> {
try {
// external_db_connections 테이블이 없으면 스킵
const tableCheck = await queryOne<{ exists: boolean }>(
`SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'external_db_connections'
) AS exists`
);
if (!tableCheck?.exists) {
logger.info("[DataSourceSeed] external_db_connections 없음 — 스킵");
return;
}
let inserted = 0;
let skipped = 0;
for (const src of DEFAULT_SOURCES) {
const existing = await queryOne(
`SELECT id FROM external_db_connections
WHERE connection_name = $1 AND company_code = $2
LIMIT 1`,
[src.connection_name, src.company_code]
);
if (existing) {
skipped++;
continue;
}
const encryptedPassword = src.password
? PasswordEncryption.encrypt(src.password)
: "";
await query(
`INSERT INTO external_db_connections (
connection_name, description, db_type, host, port, database_name,
username, password, connection_timeout, query_timeout, max_connections,
ssl_enabled, connection_options, company_code, is_active,
created_by, updated_by, created_date, updated_date
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW(), NOW())`,
[
src.connection_name,
src.description,
src.db_type,
src.host,
src.port,
src.database_name,
src.username,
encryptedPassword,
30,
60,
10,
"N",
JSON.stringify(src.connection_options || {}),
src.company_code,
src.is_active,
"system",
"system",
]
);
inserted++;
logger.info(
`[DataSourceSeed] 등록: ${src.connection_name} (${src.host}:${src.port}, is_active=${src.is_active})`
);
}
logger.info(
`[DataSourceSeed] 완료: 신규 ${inserted}개, 기존 ${skipped}개 스킵`
);
} catch (err) {
logger.error(`[DataSourceSeed] 실패: ${(err as Error).message}`);
}
}