diff --git a/backend-node/src/database/MySQLConnector.ts b/backend-node/src/database/MySQLConnector.ts index 6d27bad6..450e5472 100644 --- a/backend-node/src/database/MySQLConnector.ts +++ b/backend-node/src/database/MySQLConnector.ts @@ -1,6 +1,10 @@ -import * as mysql from 'mysql2/promise'; -import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector'; -import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes'; +import * as mysql from "mysql2/promise"; +import { + DatabaseConnector, + ConnectionConfig, + QueryResult, +} from "../interfaces/DatabaseConnector"; +import { ConnectionTestResult, TableInfo } from "../types/externalDbTypes"; export class MySQLConnector implements DatabaseConnector { private connection: mysql.Connection | null = null; @@ -12,17 +16,17 @@ export class MySQLConnector implements DatabaseConnector { async connect(): Promise { if (this.connection) { - throw new Error('이미 연결되어 있습니다.'); + throw new Error("이미 연결되어 있습니다."); } this.connection = await mysql.createConnection({ host: this.config.host, port: this.config.port, database: this.config.database, - user: this.config.username, + user: this.config.user, password: this.config.password, - connectTimeout: this.config.connectionTimeout || 30000, - ssl: this.config.sslEnabled ? { rejectUnauthorized: false } : undefined, + connectTimeout: this.config.connectionTimeoutMillis || 30000, + ssl: this.config.ssl ? { rejectUnauthorized: false } : undefined, }); } @@ -37,30 +41,34 @@ export class MySQLConnector implements DatabaseConnector { const startTime = Date.now(); try { await this.connect(); - - const [versionResult] = await this.connection!.query('SELECT VERSION() as version'); - const [sizeResult] = await this.connection!.query( - 'SELECT SUM(data_length + index_length) as size FROM information_schema.tables WHERE table_schema = DATABASE()' + + const [versionResult] = await this.connection!.query( + "SELECT VERSION() as version" ); - + const [sizeResult] = await this.connection!.query( + "SELECT SUM(data_length + index_length) as size FROM information_schema.tables WHERE table_schema = DATABASE()" + ); + const responseTime = Date.now() - startTime; return { success: true, - message: 'MySQL 연결이 성공했습니다.', + message: "MySQL 연결이 성공했습니다.", details: { response_time: responseTime, - server_version: (versionResult as any)[0]?.version || '알 수 없음', - database_size: this.formatBytes(parseInt((sizeResult as any)[0]?.size || '0')), + server_version: (versionResult as any)[0]?.version || "알 수 없음", + database_size: this.formatBytes( + parseInt((sizeResult as any)[0]?.size || "0") + ), }, }; } catch (error) { return { success: false, - message: 'MySQL 연결에 실패했습니다.', + message: "MySQL 연결에 실패했습니다.", error: { - code: 'CONNECTION_FAILED', - details: error instanceof Error ? error.message : '알 수 없는 오류', + code: "CONNECTION_FAILED", + details: error instanceof Error ? error.message : "알 수 없는 오류", }, }; } finally { @@ -78,9 +86,9 @@ export class MySQLConnector implements DatabaseConnector { return { rows: rows as any[], rowCount: Array.isArray(rows) ? rows.length : 0, - fields: (fields as mysql.FieldPacket[]).map(field => ({ + fields: (fields as mysql.FieldPacket[]).map((field) => ({ name: field.name, - dataType: field.type.toString(), + dataType: field.type?.toString() || "unknown", })), }; } finally { @@ -106,7 +114,8 @@ export class MySQLConnector implements DatabaseConnector { const result: TableInfo[] = []; for (const table of tables as any[]) { - const [columns] = await this.connection!.query(` + const [columns] = await this.connection!.query( + ` SELECT COLUMN_NAME as column_name, DATA_TYPE as data_type, @@ -116,7 +125,9 @@ export class MySQLConnector implements DatabaseConnector { WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION - `, [table.table_name]); + `, + [table.table_name] + ); result.push({ table_name: table.table_name, @@ -131,18 +142,21 @@ export class MySQLConnector implements DatabaseConnector { } } - async getColumns(tableName: string): Promise> { + async getColumns(tableName: string): Promise< + Array<{ + name: string; + dataType: string; + isNullable: boolean; + defaultValue?: string; + }> + > { if (!this.connection) { await this.connect(); } try { - const [columns] = await this.connection!.query(` + const [columns] = await this.connection!.query( + ` SELECT COLUMN_NAME as name, DATA_TYPE as dataType, @@ -152,9 +166,11 @@ export class MySQLConnector implements DatabaseConnector { WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION - `, [tableName]); + `, + [tableName] + ); - return (columns as any[]).map(col => ({ + return (columns as any[]).map((col) => ({ name: col.name, dataType: col.dataType, isNullable: col.isNullable, @@ -166,10 +182,10 @@ export class MySQLConnector implements DatabaseConnector { } private formatBytes(bytes: number): string { - if (bytes === 0) return '0 Bytes'; + if (bytes === 0) return "0 Bytes"; const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } } diff --git a/backend-node/src/services/batchService.ts b/backend-node/src/services/batchService.ts new file mode 100644 index 00000000..f20945d8 --- /dev/null +++ b/backend-node/src/services/batchService.ts @@ -0,0 +1,275 @@ +// 배치 관리 서비스 +// 작성일: 2024-12-23 + +import { PrismaClient } from "@prisma/client"; +import { + BatchJob, + BatchJobFilter, + BatchExecution, + BatchMonitoring, +} from "../types/batchManagement"; + +const prisma = new PrismaClient(); + +export class BatchService { + /** + * 배치 작업 목록 조회 + */ + static async getBatchJobs(filter: BatchJobFilter): Promise { + const whereCondition: any = { + company_code: filter.company_code || "*", + }; + + if (filter.job_name) { + whereCondition.job_name = { + contains: filter.job_name, + mode: "insensitive", + }; + } + + if (filter.job_type) { + whereCondition.job_type = filter.job_type; + } + + if (filter.is_active) { + whereCondition.is_active = filter.is_active === "Y"; + } + + if (filter.search) { + whereCondition.OR = [ + { job_name: { contains: filter.search, mode: "insensitive" } }, + { description: { contains: filter.search, mode: "insensitive" } }, + ]; + } + + const jobs = await prisma.batch_jobs.findMany({ + where: whereCondition, + orderBy: { created_date: "desc" }, + }); + + return jobs.map((job: any) => ({ + ...job, + is_active: job.is_active ? "Y" : "N", + })) as BatchJob[]; + } + + /** + * 배치 작업 상세 조회 + */ + static async getBatchJobById(id: number): Promise { + const job = await prisma.batch_jobs.findUnique({ + where: { id }, + }); + + if (!job) return null; + + return { + ...job, + is_active: job.is_active ? "Y" : "N", + } as BatchJob; + } + + /** + * 배치 작업 생성 + */ + static async createBatchJob(data: BatchJob): Promise { + const { id, config_json, ...createData } = data; + const job = await prisma.batch_jobs.create({ + data: { + ...createData, + is_active: data.is_active, + config_json: config_json || undefined, + created_date: new Date(), + updated_date: new Date(), + }, + }); + + return { + ...job, + is_active: job.is_active ? "Y" : "N", + } as BatchJob; + } + + /** + * 배치 작업 수정 + */ + static async updateBatchJob( + id: number, + data: Partial + ): Promise { + const updateData: any = { + ...data, + updated_date: new Date(), + }; + + if (data.is_active !== undefined) { + updateData.is_active = data.is_active; + } + + const job = await prisma.batch_jobs.update({ + where: { id }, + data: updateData, + }); + + return { + ...job, + is_active: job.is_active ? "Y" : "N", + } as BatchJob; + } + + /** + * 배치 작업 삭제 + */ + static async deleteBatchJob(id: number): Promise { + await prisma.batch_jobs.delete({ + where: { id }, + }); + } + + /** + * 배치 작업 수동 실행 + */ + static async executeBatchJob(id: number): Promise { + const job = await prisma.batch_jobs.findUnique({ + where: { id }, + }); + + if (!job) { + throw new Error("배치 작업을 찾을 수 없습니다."); + } + + if (!job.is_active) { + throw new Error("비활성화된 배치 작업입니다."); + } + + // 배치 실행 기록 생성 + const execution = await prisma.batch_job_executions.create({ + data: { + job_id: id, + execution_id: `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + status: "RUNNING", + start_time: new Date(), + created_at: new Date(), + }, + }); + + // 실제 배치 작업 실행 로직은 여기에 구현 + // 현재는 시뮬레이션으로 처리 + setTimeout(async () => { + try { + // 배치 작업 시뮬레이션 + await new Promise((resolve) => setTimeout(resolve, 5000)); + + await prisma.batch_job_executions.update({ + where: { id: execution.id }, + data: { + status: "SUCCESS", + end_time: new Date(), + exit_message: "배치 작업이 성공적으로 완료되었습니다.", + }, + }); + } catch (error) { + await prisma.batch_job_executions.update({ + where: { id: execution.id }, + data: { + status: "FAILED", + end_time: new Date(), + exit_message: + error instanceof Error ? error.message : "알 수 없는 오류", + }, + }); + } + }, 0); + + return { + ...execution, + execution_status: execution.status as any, + started_at: execution.start_time, + completed_at: execution.end_time, + error_message: execution.exit_message, + } as BatchExecution; + } + + /** + * 배치 실행 목록 조회 + */ + static async getBatchExecutions(jobId?: number): Promise { + const whereCondition: any = {}; + + if (jobId) { + whereCondition.job_id = jobId; + } + + const executions = await prisma.batch_job_executions.findMany({ + where: whereCondition, + orderBy: { start_time: "desc" }, + // include 제거 - 관계가 정의되지 않음 + }); + + return executions.map((exec: any) => ({ + ...exec, + execution_status: exec.status, + started_at: exec.start_time, + completed_at: exec.end_time, + error_message: exec.exit_message, + })) as BatchExecution[]; + } + + /** + * 배치 모니터링 정보 조회 + */ + static async getBatchMonitoring(): Promise { + const totalJobs = await prisma.batch_jobs.count(); + const activeJobs = await prisma.batch_jobs.count({ + where: { is_active: "Y" }, + }); + + const runningExecutions = await prisma.batch_job_executions.count({ + where: { status: "RUNNING" }, + }); + + const recentExecutions = await prisma.batch_job_executions.findMany({ + where: { + created_at: { + gte: new Date(Date.now() - 24 * 60 * 60 * 1000), // 최근 24시간 + }, + }, + orderBy: { start_time: "desc" }, + take: 10, + // include 제거 - 관계가 정의되지 않음 + }); + + const successCount = await prisma.batch_job_executions.count({ + where: { + status: "SUCCESS", + start_time: { + gte: new Date(Date.now() - 24 * 60 * 60 * 1000), + }, + }, + }); + + const failedCount = await prisma.batch_job_executions.count({ + where: { + status: "FAILED", + start_time: { + gte: new Date(Date.now() - 24 * 60 * 60 * 1000), + }, + }, + }); + + return { + total_jobs: totalJobs, + active_jobs: activeJobs, + running_jobs: runningExecutions, + failed_jobs_today: failedCount, + successful_jobs_today: successCount, + recent_executions: recentExecutions.map((exec: any) => ({ + ...exec, + execution_status: exec.status, + started_at: exec.start_time, + completed_at: exec.end_time, + error_message: exec.exit_message, + })) as BatchExecution[], + }; + } +} diff --git a/backend-node/src/services/collectionService.ts b/backend-node/src/services/collectionService.ts new file mode 100644 index 00000000..020e96f8 --- /dev/null +++ b/backend-node/src/services/collectionService.ts @@ -0,0 +1,253 @@ +// 수집 관리 서비스 +// 작성일: 2024-12-23 + +import { PrismaClient } from "@prisma/client"; +import { + DataCollectionConfig, + CollectionFilter, + CollectionJob, + CollectionHistory, +} from "../types/collectionManagement"; + +const prisma = new PrismaClient(); + +export class CollectionService { + /** + * 수집 설정 목록 조회 + */ + static async getCollectionConfigs( + filter: CollectionFilter + ): Promise { + const whereCondition: any = { + company_code: filter.company_code || "*", + }; + + if (filter.config_name) { + whereCondition.config_name = { + contains: filter.config_name, + mode: "insensitive", + }; + } + + if (filter.source_connection_id) { + whereCondition.source_connection_id = filter.source_connection_id; + } + + if (filter.collection_type) { + whereCondition.collection_type = filter.collection_type; + } + + if (filter.is_active) { + whereCondition.is_active = filter.is_active === "Y"; + } + + if (filter.search) { + whereCondition.OR = [ + { config_name: { contains: filter.search, mode: "insensitive" } }, + { description: { contains: filter.search, mode: "insensitive" } }, + ]; + } + + const configs = await prisma.data_collection_configs.findMany({ + where: whereCondition, + orderBy: { created_date: "desc" }, + }); + + return configs.map((config: any) => ({ + ...config, + is_active: config.is_active ? "Y" : "N", + })) as DataCollectionConfig[]; + } + + /** + * 수집 설정 상세 조회 + */ + static async getCollectionConfigById( + id: number + ): Promise { + const config = await prisma.data_collection_configs.findUnique({ + where: { id }, + }); + + if (!config) return null; + + return { + ...config, + is_active: config.is_active ? "Y" : "N", + } as DataCollectionConfig; + } + + /** + * 수집 설정 생성 + */ + static async createCollectionConfig( + data: DataCollectionConfig + ): Promise { + const { id, collection_options, ...createData } = data; + const config = await prisma.data_collection_configs.create({ + data: { + ...createData, + is_active: data.is_active, + collection_options: collection_options || undefined, + created_date: new Date(), + updated_date: new Date(), + }, + }); + + return { + ...config, + is_active: config.is_active ? "Y" : "N", + } as DataCollectionConfig; + } + + /** + * 수집 설정 수정 + */ + static async updateCollectionConfig( + id: number, + data: Partial + ): Promise { + const updateData: any = { + ...data, + updated_date: new Date(), + }; + + if (data.is_active !== undefined) { + updateData.is_active = data.is_active; + } + + const config = await prisma.data_collection_configs.update({ + where: { id }, + data: updateData, + }); + + return { + ...config, + is_active: config.is_active ? "Y" : "N", + } as DataCollectionConfig; + } + + /** + * 수집 설정 삭제 + */ + static async deleteCollectionConfig(id: number): Promise { + await prisma.data_collection_configs.delete({ + where: { id }, + }); + } + + /** + * 수집 작업 실행 + */ + static async executeCollection(configId: number): Promise { + const config = await prisma.data_collection_configs.findUnique({ + where: { id: configId }, + }); + + if (!config) { + throw new Error("수집 설정을 찾을 수 없습니다."); + } + + if (!config.is_active) { + throw new Error("비활성화된 수집 설정입니다."); + } + + // 수집 작업 기록 생성 + const job = await prisma.data_collection_jobs.create({ + data: { + config_id: configId, + job_status: "running", + started_at: new Date(), + created_date: new Date(), + }, + }); + + // 실제 수집 작업 실행 로직은 여기에 구현 + // 현재는 시뮬레이션으로 처리 + setTimeout(async () => { + try { + // 수집 작업 시뮬레이션 + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const recordsCollected = Math.floor(Math.random() * 1000) + 100; + + await prisma.data_collection_jobs.update({ + where: { id: job.id }, + data: { + job_status: "completed", + completed_at: new Date(), + records_processed: recordsCollected, + }, + }); + } catch (error) { + await prisma.data_collection_jobs.update({ + where: { id: job.id }, + data: { + job_status: "failed", + completed_at: new Date(), + error_message: + error instanceof Error ? error.message : "알 수 없는 오류", + }, + }); + } + }, 0); + + return job as CollectionJob; + } + + /** + * 수집 작업 목록 조회 + */ + static async getCollectionJobs(configId?: number): Promise { + const whereCondition: any = {}; + + if (configId) { + whereCondition.config_id = configId; + } + + const jobs = await prisma.data_collection_jobs.findMany({ + where: whereCondition, + orderBy: { started_at: "desc" }, + include: { + config: { + select: { + config_name: true, + collection_type: true, + }, + }, + }, + }); + + return jobs as CollectionJob[]; + } + + /** + * 수집 이력 조회 + */ + static async getCollectionHistory( + configId: number + ): Promise { + const history = await prisma.data_collection_jobs.findMany({ + where: { config_id: configId }, + orderBy: { started_at: "desc" }, + take: 50, // 최근 50개 이력 + }); + + return history.map((item: any) => ({ + id: item.id, + config_id: item.config_id, + status: item.job_status, + collection_date: item.started_at, + started_at: item.started_at, + completed_at: item.completed_at, + execution_time_ms: + item.completed_at && item.started_at + ? new Date(item.completed_at).getTime() - + new Date(item.started_at).getTime() + : null, + records_collected: item.records_processed || 0, + result_message: `${item.records_processed || 0}개의 레코드가 처리되었습니다.`, + error_message: item.error_message, + })) as CollectionHistory[]; + } +} diff --git a/backend-node/src/services/dataflowService.ts b/backend-node/src/services/dataflowService.ts index 5f41f18d..df18398f 100644 --- a/backend-node/src/services/dataflowService.ts +++ b/backend-node/src/services/dataflowService.ts @@ -471,15 +471,7 @@ export class DataflowService { const linkedData = await prisma.data_relationship_bridge.findMany({ where: whereCondition, orderBy: { created_at: "desc" }, - include: { - relationship: { - select: { - relationship_name: true, - relationship_type: true, - connection_type: true, - }, - }, - }, + // include 제거 - relationship 관계가 스키마에 정의되지 않음 }); logger.info( @@ -520,15 +512,7 @@ export class DataflowService { const linkedData = await prisma.data_relationship_bridge.findMany({ where: whereCondition, orderBy: { created_at: "desc" }, - include: { - relationship: { - select: { - relationship_name: true, - relationship_type: true, - connection_type: true, - }, - }, - }, + // include 제거 - relationship 관계가 스키마에 정의되지 않음 }); logger.info( diff --git a/backend-node/src/types/screen.ts b/backend-node/src/types/screen.ts index d774cee4..8075c78c 100644 --- a/backend-node/src/types/screen.ts +++ b/backend-node/src/types/screen.ts @@ -5,7 +5,8 @@ export type ComponentType = "container" | "row" | "column" | "widget" | "group"; // 웹 타입 정의 // WebType은 통합 타입에서 import (중복 정의 제거) -export { WebType } from "./unified-web-types"; +import { WebType } from "./unified-web-types"; +export { WebType }; // 위치 정보 export interface Position { diff --git a/docker/dev/backend.Dockerfile b/docker/dev/backend.Dockerfile index a916accd..9099b18e 100644 --- a/docker/dev/backend.Dockerfile +++ b/docker/dev/backend.Dockerfile @@ -1,5 +1,5 @@ # 개발용 백엔드 Dockerfile -FROM node:20-alpine +FROM node:20-bookworm-slim WORKDIR /app