외부커넥션관리
This commit is contained in:
769
backend-node/package-lock.json
generated
769
backend-node/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.7.1",
|
||||
"@types/mssql": "^9.1.8",
|
||||
"axios": "^1.11.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
@@ -38,9 +39,12 @@
|
||||
"helmet": "^7.1.0",
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mssql": "^11.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.15.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^6.9.7",
|
||||
"oracledb": "^6.9.0",
|
||||
"pg": "^8.16.3",
|
||||
"prisma": "^5.7.1",
|
||||
"redis": "^4.6.10",
|
||||
@@ -59,6 +63,7 @@
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/oracledb": "^6.9.1",
|
||||
"@types/pg": "^8.15.5",
|
||||
"@types/sanitize-html": "^2.9.5",
|
||||
"@types/supertest": "^6.0.2",
|
||||
|
||||
@@ -27,16 +27,12 @@ model external_call_configs {
|
||||
api_type String? @db.VarChar(20)
|
||||
config_data Json
|
||||
description String?
|
||||
company_code String @default("*") @db.VarChar(20)
|
||||
is_active String? @default("Y") @db.Char(1)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
created_by String? @db.VarChar(50)
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(50)
|
||||
|
||||
@@index([is_active], map: "idx_external_call_configs_active")
|
||||
@@index([company_code], map: "idx_external_call_configs_company")
|
||||
@@index([call_type, api_type], map: "idx_external_call_configs_type")
|
||||
company_code String @default("*") @db.VarChar(20)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
}
|
||||
|
||||
model external_db_connections {
|
||||
@@ -62,6 +58,9 @@ model external_db_connections {
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(50)
|
||||
|
||||
// 관계
|
||||
collection_configs data_collection_configs[]
|
||||
|
||||
@@index([connection_name], map: "idx_external_db_connections_name")
|
||||
}
|
||||
|
||||
@@ -3968,9 +3967,6 @@ model table_relationships {
|
||||
updated_date DateTime? @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(50)
|
||||
diagram_id Int?
|
||||
|
||||
// 역방향 관계
|
||||
bridges data_relationship_bridge[]
|
||||
}
|
||||
|
||||
model data_relationship_bridge {
|
||||
@@ -3993,9 +3989,6 @@ model data_relationship_bridge {
|
||||
to_key_value String? @db.VarChar(500)
|
||||
to_record_id String? @db.VarChar(100)
|
||||
|
||||
// 관계 정의
|
||||
relationship table_relationships? @relation(fields: [relationship_id], references: [relationship_id])
|
||||
|
||||
@@index([connection_type], map: "idx_data_bridge_connection_type")
|
||||
@@index([company_code, is_active], map: "idx_data_bridge_company_active")
|
||||
}
|
||||
@@ -4088,55 +4081,434 @@ model table_relationships_backup {
|
||||
}
|
||||
|
||||
model test_sales_info {
|
||||
sales_no String @id @db.VarChar(20)
|
||||
contract_type String? @db.VarChar(50)
|
||||
order_seq Int?
|
||||
domestic_foreign String? @db.VarChar(20)
|
||||
customer_name String? @db.VarChar(200)
|
||||
product_type String? @db.VarChar(100)
|
||||
machine_type String? @db.VarChar(100)
|
||||
customer_project_name String? @db.VarChar(200)
|
||||
expected_delivery_date DateTime? @db.Date
|
||||
receiving_location String? @db.VarChar(200)
|
||||
setup_location String? @db.VarChar(200)
|
||||
equipment_direction String? @db.VarChar(100)
|
||||
equipment_count Int? @default(0)
|
||||
equipment_type String? @db.VarChar(100)
|
||||
equipment_length Decimal? @db.Decimal(10,2)
|
||||
manager_name String? @db.VarChar(100)
|
||||
reg_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
status String? @default("진행중") @db.VarChar(50)
|
||||
|
||||
// 관계 정의: 영업 정보에서 프로젝트로
|
||||
projects test_project_info[]
|
||||
sales_no String @id(map: "pk_test_sales_info") @db.VarChar(200)
|
||||
contract_type String? @db.VarChar(50)
|
||||
order_seq Int?
|
||||
domestic_foreign String? @db.VarChar(20)
|
||||
customer_name String? @db.VarChar(200)
|
||||
product_type String? @db.VarChar(100)
|
||||
machine_type String? @db.VarChar(100)
|
||||
customer_project_name String? @db.VarChar(200)
|
||||
expected_delivery_date DateTime? @db.Date
|
||||
receiving_location String? @db.VarChar(200)
|
||||
setup_location String? @db.VarChar(200)
|
||||
equipment_direction String? @db.VarChar(100)
|
||||
equipment_count Int? @default(0)
|
||||
equipment_type String? @db.VarChar(100)
|
||||
equipment_length Decimal? @db.Decimal(10, 2)
|
||||
manager_name String? @db.VarChar(100)
|
||||
reg_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
status String? @default("진행중") @db.VarChar(50)
|
||||
}
|
||||
|
||||
model test_project_info {
|
||||
project_no String @id @db.VarChar(200)
|
||||
sales_no String? @db.VarChar(20)
|
||||
contract_type String? @db.VarChar(50)
|
||||
order_seq Int?
|
||||
domestic_foreign String? @db.VarChar(20)
|
||||
customer_name String? @db.VarChar(200)
|
||||
|
||||
// 프로젝트 전용 컬럼들
|
||||
project_status String? @default("PLANNING") @db.VarChar(50)
|
||||
project_start_date DateTime? @db.Date
|
||||
project_end_date DateTime? @db.Date
|
||||
project_manager String? @db.VarChar(100)
|
||||
project_description String? @db.Text
|
||||
|
||||
// 시스템 관리 컬럼들
|
||||
created_by String? @db.VarChar(100)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(100)
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
|
||||
// 관계 정의: 영업 정보 참조
|
||||
sales test_sales_info? @relation(fields: [sales_no], references: [sales_no])
|
||||
|
||||
project_no String @id @db.VarChar(200)
|
||||
sales_no String? @db.VarChar(20)
|
||||
contract_type String? @db.VarChar(50)
|
||||
order_seq Int?
|
||||
domestic_foreign String? @db.VarChar(20)
|
||||
customer_name String? @db.VarChar(200)
|
||||
project_status String? @default("PLANNING") @db.VarChar(50)
|
||||
project_start_date DateTime? @db.Date
|
||||
project_end_date DateTime? @db.Date
|
||||
project_manager String? @db.VarChar(100)
|
||||
project_description String?
|
||||
created_by String? @db.VarChar(100)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(100)
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
|
||||
@@index([sales_no], map: "idx_project_sales_no")
|
||||
@@index([project_status], map: "idx_project_status")
|
||||
@@index([customer_name], map: "idx_project_customer")
|
||||
@@index([project_manager], map: "idx_project_manager")
|
||||
}
|
||||
|
||||
model batch_jobs {
|
||||
id Int @id @default(autoincrement())
|
||||
job_name String @db.VarChar(100)
|
||||
job_type String @db.VarChar(20)
|
||||
description String?
|
||||
created_by String? @db.VarChar(50)
|
||||
updated_by String? @db.VarChar(50)
|
||||
company_code String @default("*") @db.VarChar(20)
|
||||
config_json Json?
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
execution_count Int @default(0)
|
||||
failure_count Int @default(0)
|
||||
last_executed_at DateTime? @db.Timestamp(6)
|
||||
next_execution_at DateTime? @db.Timestamp(6)
|
||||
schedule_cron String? @db.VarChar(100)
|
||||
success_count Int @default(0)
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
is_active String @default("Y") @db.Char(1)
|
||||
|
||||
@@index([job_type], map: "idx_batch_jobs_type")
|
||||
@@index([company_code], map: "idx_batch_jobs_company_code")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model batch_job_executions {
|
||||
id Int @id @default(autoincrement())
|
||||
job_id Int
|
||||
execution_id String @unique @db.VarChar(100)
|
||||
start_time DateTime @db.Timestamp(6)
|
||||
end_time DateTime? @db.Timestamp(6)
|
||||
status String @default("STARTED") @db.VarChar(20)
|
||||
exit_code Int?
|
||||
exit_message String?
|
||||
parameters Json?
|
||||
logs String?
|
||||
created_at DateTime? @default(now()) @db.Timestamp(6)
|
||||
|
||||
@@index([execution_id], map: "idx_batch_executions_execution_id")
|
||||
@@index([job_id], map: "idx_batch_executions_job_id")
|
||||
@@index([start_time], map: "idx_batch_executions_start_time")
|
||||
@@index([status], map: "idx_batch_executions_status")
|
||||
}
|
||||
|
||||
model batch_job_parameters {
|
||||
id Int @id @default(autoincrement())
|
||||
job_id Int
|
||||
parameter_name String @db.VarChar(100)
|
||||
parameter_value String?
|
||||
parameter_type String? @default("STRING") @db.VarChar(50)
|
||||
is_required Boolean? @default(false)
|
||||
description String?
|
||||
created_at DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_at DateTime? @db.Timestamp(6)
|
||||
|
||||
@@unique([job_id, parameter_name])
|
||||
@@index([job_id], map: "idx_batch_parameters_job_id")
|
||||
}
|
||||
|
||||
model batch_schedules {
|
||||
id Int @id @default(autoincrement())
|
||||
job_id Int
|
||||
schedule_name String @db.VarChar(255)
|
||||
cron_expression String @db.VarChar(100)
|
||||
timezone String? @default("Asia/Seoul") @db.VarChar(50)
|
||||
is_active Boolean? @default(true)
|
||||
start_date DateTime? @db.Date
|
||||
end_date DateTime? @db.Date
|
||||
created_by String @db.VarChar(100)
|
||||
created_at DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(100)
|
||||
updated_at DateTime? @db.Timestamp(6)
|
||||
|
||||
@@index([is_active], map: "idx_batch_schedules_active")
|
||||
@@index([job_id], map: "idx_batch_schedules_job_id")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model dataflow_external_calls {
|
||||
id Int @id @default(autoincrement())
|
||||
diagram_id Int
|
||||
source_table String @db.VarChar(100)
|
||||
trigger_condition Json
|
||||
external_call_config_id Int
|
||||
message_template String?
|
||||
is_active String? @default("Y") @db.Char(1)
|
||||
created_by Int?
|
||||
updated_by Int?
|
||||
created_at DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamp(6)
|
||||
}
|
||||
|
||||
model ddl_execution_log {
|
||||
id Int @id @default(autoincrement())
|
||||
user_id String @db.VarChar(100)
|
||||
company_code String @db.VarChar(50)
|
||||
ddl_type String @db.VarChar(50)
|
||||
table_name String @db.VarChar(100)
|
||||
ddl_query String
|
||||
success Boolean
|
||||
error_message String?
|
||||
executed_at DateTime? @default(now()) @db.Timestamp(6)
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model external_call_logs {
|
||||
id Int @id @default(autoincrement())
|
||||
dataflow_external_call_id Int?
|
||||
external_call_config_id Int
|
||||
trigger_data Json?
|
||||
request_data Json?
|
||||
response_data Json?
|
||||
status String @db.VarChar(20)
|
||||
error_message String?
|
||||
execution_time Int?
|
||||
executed_at DateTime? @default(now()) @db.Timestamp(6)
|
||||
|
||||
@@index([executed_at], map: "idx_external_call_logs_executed")
|
||||
}
|
||||
|
||||
model my_custom_table {
|
||||
id Int @id @default(autoincrement())
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
company_code String? @default("*") @db.VarChar(50)
|
||||
customer_name String? @db.VarChar
|
||||
email_address String? @db.VarChar(255)
|
||||
}
|
||||
|
||||
model table_type_columns {
|
||||
id Int @id @default(autoincrement())
|
||||
table_name String @db.VarChar(255)
|
||||
column_name String @db.VarChar(255)
|
||||
input_type String @default("text") @db.VarChar(50)
|
||||
detail_settings String? @default("{}")
|
||||
is_nullable String? @default("Y") @db.VarChar(10)
|
||||
display_order Int? @default(0)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
|
||||
@@unique([table_name, column_name])
|
||||
@@index([input_type], map: "idx_table_type_columns_input_type")
|
||||
@@index([table_name], map: "idx_table_type_columns_table_name")
|
||||
}
|
||||
|
||||
model test_api_integration_1758589777139 {
|
||||
id String @id @default(dbgenerated("(gen_random_uuid())::text")) @db.VarChar(500)
|
||||
created_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
updated_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
writer String? @db.VarChar(500)
|
||||
company_code String? @default("*") @db.VarChar(500)
|
||||
product_name String? @db.VarChar(500)
|
||||
price String? @db.VarChar(500)
|
||||
category String? @db.VarChar(500)
|
||||
}
|
||||
|
||||
model test_new_table {
|
||||
id Int @id @default(autoincrement())
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
company_code String? @default("*") @db.VarChar(50)
|
||||
name String? @db.VarChar
|
||||
email String? @db.VarChar(255)
|
||||
user_test_column String? @db.VarChar
|
||||
dsfsdf123215 String? @db.VarChar
|
||||
aaaassda String? @db.VarChar
|
||||
}
|
||||
|
||||
model test_new_table33333 {
|
||||
id Int @id @default(autoincrement())
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
writer String? @db.VarChar(100)
|
||||
company_code String? @default("*") @db.VarChar(50)
|
||||
eeeeeeee String? @db.VarChar(500)
|
||||
wwww String? @db.VarChar(500)
|
||||
sssss String? @db.VarChar(500)
|
||||
}
|
||||
|
||||
model test_new_table44444 {
|
||||
id String @id @default(dbgenerated("(gen_random_uuid())::text")) @db.VarChar(500)
|
||||
created_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
updated_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
writer String? @db.VarChar(500)
|
||||
company_code String? @db.VarChar(500)
|
||||
ttttttt String? @db.VarChar(500)
|
||||
yyyyyyy String? @db.VarChar(500)
|
||||
uuuuuuu String? @db.VarChar(500)
|
||||
iiiiiii String? @db.VarChar(500)
|
||||
}
|
||||
|
||||
model test_new_table555555 {
|
||||
id String @id @default(dbgenerated("(gen_random_uuid())::text")) @db.VarChar(500)
|
||||
created_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
updated_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
writer String? @db.VarChar(500)
|
||||
company_code String? @db.VarChar(500)
|
||||
rtrtrtrtr String? @db.VarChar(500)
|
||||
ererwewewe String? @db.VarChar(500)
|
||||
wetyeryrtyut String? @db.VarChar(500)
|
||||
werwqq String? @db.VarChar(500)
|
||||
saved_file_name String? @db.VarChar(500)
|
||||
}
|
||||
|
||||
model test_table_info {
|
||||
id Int @id @default(autoincrement())
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
company_code String? @default("*") @db.VarChar(50)
|
||||
objid Int
|
||||
test_name String? @db.VarChar(250)
|
||||
ggggggggggg String? @db.VarChar
|
||||
test_column_1 String? @db.VarChar
|
||||
test_column_2 String? @db.VarChar
|
||||
test_column_3 String? @db.VarChar
|
||||
final_test_column String? @db.VarChar
|
||||
zzzzzzz String? @db.VarChar
|
||||
bbbbbbb String? @db.VarChar
|
||||
realtime_test String? @db.VarChar
|
||||
table_update_test String? @db.VarChar
|
||||
}
|
||||
|
||||
model test_table_info2222 {
|
||||
id Int @id @default(autoincrement())
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
company_code String? @default("*") @db.VarChar(50)
|
||||
clll_cc String? @db.VarChar
|
||||
eeee_eee String? @db.VarChar
|
||||
saved_file_name String? @db.VarChar
|
||||
debug_test_column String? @db.VarChar
|
||||
field_1 String? @db.VarChar
|
||||
rrrrrrrrrr String? @db.VarChar
|
||||
tttttttt String? @db.VarChar
|
||||
}
|
||||
|
||||
model test_varchar_unified {
|
||||
id String @id @default(dbgenerated("(gen_random_uuid())::text")) @db.VarChar(500)
|
||||
created_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
updated_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
writer String? @db.VarChar(500)
|
||||
company_code String? @default("*") @db.VarChar(500)
|
||||
product_name String? @db.VarChar(500)
|
||||
price String? @db.VarChar(500)
|
||||
launch_date String? @db.VarChar(500)
|
||||
is_active String? @db.VarChar(500)
|
||||
}
|
||||
|
||||
model test_varchar_unified_1758588878993 {
|
||||
id String @id @default(dbgenerated("(gen_random_uuid())::text")) @db.VarChar(500)
|
||||
created_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
updated_date String? @default(dbgenerated("(now())::text")) @db.VarChar(500)
|
||||
writer String? @db.VarChar(500)
|
||||
company_code String? @default("*") @db.VarChar(500)
|
||||
product_name String? @db.VarChar(500)
|
||||
price String? @db.VarChar(500)
|
||||
launch_date String? @db.VarChar(500)
|
||||
is_active String? @db.VarChar(500)
|
||||
}
|
||||
|
||||
model writer_test_table {
|
||||
id Int @id @default(autoincrement())
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
writer String? @db.VarChar(100)
|
||||
company_code String? @default("*") @db.VarChar(50)
|
||||
test_field String? @db.VarChar
|
||||
field_1 String? @db.VarChar
|
||||
}
|
||||
|
||||
// 데이터 수집 설정 테이블
|
||||
model data_collection_configs {
|
||||
id Int @id @default(autoincrement())
|
||||
config_name String @db.VarChar(100)
|
||||
description String?
|
||||
source_connection_id Int
|
||||
source_table String @db.VarChar(100)
|
||||
target_table String? @db.VarChar(100)
|
||||
collection_type String @db.VarChar(20) // full, incremental, delta
|
||||
schedule_cron String? @db.VarChar(100)
|
||||
is_active String @default("Y") @db.Char(1)
|
||||
last_collected_at DateTime? @db.Timestamp(6)
|
||||
collection_options Json?
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
created_by String? @db.VarChar(50)
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(50)
|
||||
company_code String @default("*") @db.VarChar(20)
|
||||
|
||||
// 관계
|
||||
collection_jobs data_collection_jobs[]
|
||||
collection_history data_collection_history[]
|
||||
external_connection external_db_connections @relation(fields: [source_connection_id], references: [id])
|
||||
|
||||
@@index([source_connection_id], map: "idx_data_collection_configs_connection")
|
||||
@@index([is_active], map: "idx_data_collection_configs_active")
|
||||
@@index([company_code], map: "idx_data_collection_configs_company")
|
||||
}
|
||||
|
||||
// 데이터 수집 작업 테이블
|
||||
model data_collection_jobs {
|
||||
id Int @id @default(autoincrement())
|
||||
config_id Int
|
||||
job_status String @db.VarChar(20) // pending, running, completed, failed
|
||||
started_at DateTime? @db.Timestamp(6)
|
||||
completed_at DateTime? @db.Timestamp(6)
|
||||
records_processed Int? @default(0)
|
||||
error_message String?
|
||||
job_details Json?
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
|
||||
// 관계
|
||||
config data_collection_configs @relation(fields: [config_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([config_id], map: "idx_data_collection_jobs_config")
|
||||
@@index([job_status], map: "idx_data_collection_jobs_status")
|
||||
@@index([created_date], map: "idx_data_collection_jobs_created")
|
||||
}
|
||||
|
||||
// 데이터 수집 이력 테이블
|
||||
model data_collection_history {
|
||||
id Int @id @default(autoincrement())
|
||||
config_id Int
|
||||
collection_date DateTime @db.Timestamp(6)
|
||||
records_collected Int @default(0)
|
||||
execution_time_ms Int @default(0)
|
||||
status String @db.VarChar(20) // success, partial, failed
|
||||
error_details String?
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
|
||||
// 관계
|
||||
config data_collection_configs @relation(fields: [config_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([config_id], map: "idx_data_collection_history_config")
|
||||
@@index([collection_date], map: "idx_data_collection_history_date")
|
||||
@@index([status], map: "idx_data_collection_history_status")
|
||||
}
|
||||
|
||||
// 데이터 수집 배치 관리 테이블 (기존 batch_jobs와 구분)
|
||||
model collection_batch_management {
|
||||
id Int @id @default(autoincrement())
|
||||
batch_name String @db.VarChar(100)
|
||||
description String?
|
||||
batch_type String @db.VarChar(20) // collection, sync, cleanup, custom
|
||||
schedule_cron String? @db.VarChar(100)
|
||||
is_active String @default("Y") @db.Char(1)
|
||||
config_json Json?
|
||||
last_executed_at DateTime? @db.Timestamp(6)
|
||||
next_execution_at DateTime? @db.Timestamp(6)
|
||||
execution_count Int @default(0)
|
||||
success_count Int @default(0)
|
||||
failure_count Int @default(0)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
created_by String? @db.VarChar(50)
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(50)
|
||||
company_code String @default("*") @db.VarChar(20)
|
||||
|
||||
// 관계
|
||||
batch_executions collection_batch_executions[]
|
||||
|
||||
@@index([batch_type], map: "idx_collection_batch_mgmt_type")
|
||||
@@index([is_active], map: "idx_collection_batch_mgmt_active")
|
||||
@@index([company_code], map: "idx_collection_batch_mgmt_company")
|
||||
@@index([next_execution_at], map: "idx_collection_batch_mgmt_next_execution")
|
||||
}
|
||||
|
||||
// 데이터 수집 배치 실행 테이블
|
||||
model collection_batch_executions {
|
||||
id Int @id @default(autoincrement())
|
||||
batch_id Int
|
||||
execution_status String @db.VarChar(20) // pending, running, completed, failed, cancelled
|
||||
started_at DateTime? @db.Timestamp(6)
|
||||
completed_at DateTime? @db.Timestamp(6)
|
||||
execution_time_ms Int?
|
||||
result_data Json?
|
||||
error_message String?
|
||||
log_details String?
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
|
||||
// 관계
|
||||
batch collection_batch_management @relation(fields: [batch_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([batch_id], map: "idx_collection_batch_executions_batch")
|
||||
@@index([execution_status], map: "idx_collection_batch_executions_status")
|
||||
@@index([created_date], map: "idx_collection_batch_executions_created")
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
|
||||
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
|
||||
import ddlRoutes from "./routes/ddlRoutes";
|
||||
import entityReferenceRoutes from "./routes/entityReferenceRoutes";
|
||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
// import menuRoutes from './routes/menuRoutes';
|
||||
|
||||
@@ -129,6 +131,8 @@ app.use("/api/test-button-dataflow", testButtonDataflowRoutes);
|
||||
app.use("/api/external-db-connections", externalDbConnectionRoutes);
|
||||
app.use("/api/ddl", ddlRoutes);
|
||||
app.use("/api/entity-reference", entityReferenceRoutes);
|
||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||
// app.use("/api/batch", batchRoutes); // 임시 주석
|
||||
// app.use('/api/users', userRoutes);
|
||||
// app.use('/api/menus', menuRoutes);
|
||||
|
||||
|
||||
294
backend-node/src/controllers/batchController.ts
Normal file
294
backend-node/src/controllers/batchController.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
// 배치 관리 컨트롤러
|
||||
// 작성일: 2024-12-23
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import { BatchService } from '../services/batchService';
|
||||
import { BatchJob, BatchJobFilter } from '../types/batchManagement';
|
||||
import { AuthenticatedRequest } from '../middleware/authMiddleware';
|
||||
|
||||
export class BatchController {
|
||||
/**
|
||||
* 배치 작업 목록 조회
|
||||
*/
|
||||
static async getBatchJobs(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const filter: BatchJobFilter = {
|
||||
job_name: req.query.job_name as string,
|
||||
job_type: req.query.job_type as string,
|
||||
is_active: req.query.is_active as string,
|
||||
company_code: req.user?.companyCode || '*',
|
||||
search: req.query.search as string,
|
||||
};
|
||||
|
||||
const jobs = await BatchService.getBatchJobs(filter);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: jobs,
|
||||
message: '배치 작업 목록을 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('배치 작업 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '배치 작업 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 상세 조회
|
||||
*/
|
||||
static async getBatchJobById(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 ID입니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const job = await BatchService.getBatchJobById(id);
|
||||
if (!job) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '배치 작업을 찾을 수 없습니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: job,
|
||||
message: '배치 작업을 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('배치 작업 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '배치 작업 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 생성
|
||||
*/
|
||||
static async createBatchJob(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const data: BatchJob = {
|
||||
...req.body,
|
||||
company_code: req.user?.companyCode || '*',
|
||||
created_by: req.user?.userId,
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!data.job_name || !data.job_type) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const job = await BatchService.createBatchJob(data);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: job,
|
||||
message: '배치 작업을 생성했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('배치 작업 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '배치 작업 생성에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 수정
|
||||
*/
|
||||
static async updateBatchJob(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 ID입니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data: Partial<BatchJob> = {
|
||||
...req.body,
|
||||
updated_by: req.user?.userId,
|
||||
};
|
||||
|
||||
const job = await BatchService.updateBatchJob(id, data);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: job,
|
||||
message: '배치 작업을 수정했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('배치 작업 수정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '배치 작업 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 삭제
|
||||
*/
|
||||
static async deleteBatchJob(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 ID입니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await BatchService.deleteBatchJob(id);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '배치 작업을 삭제했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('배치 작업 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '배치 작업 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 작업 수동 실행
|
||||
*/
|
||||
static async executeBatchJob(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 ID입니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const execution = await BatchService.executeBatchJob(id);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: execution,
|
||||
message: '배치 작업을 실행했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('배치 작업 실행 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '배치 작업 실행에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 실행 목록 조회
|
||||
*/
|
||||
static async getBatchExecutions(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const jobId = req.query.job_id ? parseInt(req.query.job_id as string) : undefined;
|
||||
const executions = await BatchService.getBatchExecutions(jobId);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: executions,
|
||||
message: '배치 실행 목록을 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('배치 실행 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '배치 실행 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 모니터링 정보 조회
|
||||
*/
|
||||
static async getBatchMonitoring(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const monitoring = await BatchService.getBatchMonitoring();
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: monitoring,
|
||||
message: '배치 모니터링 정보를 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('배치 모니터링 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '배치 모니터링 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 지원되는 작업 타입 조회
|
||||
*/
|
||||
static async getSupportedJobTypes(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { BATCH_JOB_TYPE_OPTIONS } = await import('../types/batchManagement');
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
types: BATCH_JOB_TYPE_OPTIONS,
|
||||
},
|
||||
message: '지원하는 작업 타입 목록을 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('작업 타입 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '작업 타입 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄 프리셋 조회
|
||||
*/
|
||||
static async getSchedulePresets(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { SCHEDULE_PRESETS } = await import('../types/batchManagement');
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
presets: SCHEDULE_PRESETS,
|
||||
},
|
||||
message: '스케줄 프리셋 목록을 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('스케줄 프리셋 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '스케줄 프리셋 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
258
backend-node/src/controllers/collectionController.ts
Normal file
258
backend-node/src/controllers/collectionController.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
// 수집 관리 컨트롤러
|
||||
// 작성일: 2024-12-23
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import { CollectionService } from '../services/collectionService';
|
||||
import { DataCollectionConfig, CollectionFilter } from '../types/collectionManagement';
|
||||
import { AuthenticatedRequest } from '../middleware/authMiddleware';
|
||||
|
||||
export class CollectionController {
|
||||
/**
|
||||
* 수집 설정 목록 조회
|
||||
*/
|
||||
static async getCollectionConfigs(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const filter: CollectionFilter = {
|
||||
config_name: req.query.config_name as string,
|
||||
source_connection_id: req.query.source_connection_id ? parseInt(req.query.source_connection_id as string) : undefined,
|
||||
collection_type: req.query.collection_type as string,
|
||||
is_active: req.query.is_active as string,
|
||||
company_code: req.user?.companyCode || '*',
|
||||
search: req.query.search as string,
|
||||
};
|
||||
|
||||
const configs = await CollectionService.getCollectionConfigs(filter);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: configs,
|
||||
message: '수집 설정 목록을 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수집 설정 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '수집 설정 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수집 설정 상세 조회
|
||||
*/
|
||||
static async getCollectionConfigById(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 ID입니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await CollectionService.getCollectionConfigById(id);
|
||||
if (!config) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '수집 설정을 찾을 수 없습니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: config,
|
||||
message: '수집 설정을 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수집 설정 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '수집 설정 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수집 설정 생성
|
||||
*/
|
||||
static async createCollectionConfig(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const data: DataCollectionConfig = {
|
||||
...req.body,
|
||||
company_code: req.user?.companyCode || '*',
|
||||
created_by: req.user?.userId,
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!data.config_name || !data.source_connection_id || !data.source_table || !data.collection_type) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await CollectionService.createCollectionConfig(data);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: config,
|
||||
message: '수집 설정을 생성했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수집 설정 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '수집 설정 생성에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수집 설정 수정
|
||||
*/
|
||||
static async updateCollectionConfig(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 ID입니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data: Partial<DataCollectionConfig> = {
|
||||
...req.body,
|
||||
updated_by: req.user?.userId,
|
||||
};
|
||||
|
||||
const config = await CollectionService.updateCollectionConfig(id, data);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: config,
|
||||
message: '수집 설정을 수정했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수집 설정 수정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '수집 설정 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수집 설정 삭제
|
||||
*/
|
||||
static async deleteCollectionConfig(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 ID입니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await CollectionService.deleteCollectionConfig(id);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '수집 설정을 삭제했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수집 설정 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '수집 설정 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수집 작업 실행
|
||||
*/
|
||||
static async executeCollection(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 ID입니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const job = await CollectionService.executeCollection(id);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: job,
|
||||
message: '수집 작업을 시작했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수집 작업 실행 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '수집 작업 실행에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수집 작업 목록 조회
|
||||
*/
|
||||
static async getCollectionJobs(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const configId = req.query.config_id ? parseInt(req.query.config_id as string) : undefined;
|
||||
const jobs = await CollectionService.getCollectionJobs(configId);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: jobs,
|
||||
message: '수집 작업 목록을 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수집 작업 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '수집 작업 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수집 이력 조회
|
||||
*/
|
||||
static async getCollectionHistory(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const configId = parseInt(req.params.configId);
|
||||
if (isNaN(configId)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 설정 ID입니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const history = await CollectionService.getCollectionHistory(configId);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: history,
|
||||
message: '수집 이력을 조회했습니다.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수집 이력 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '수집 이력 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { DatabaseConnector, ConnectionConfig } from '../interfaces/DatabaseConnector';
|
||||
import { PostgreSQLConnector } from './PostgreSQLConnector';
|
||||
import { MariaDBConnector } from './MariaDBConnector';
|
||||
import { MSSQLConnector } from './MSSQLConnector';
|
||||
import { OracleConnector } from './OracleConnector';
|
||||
|
||||
export class DatabaseConnectorFactory {
|
||||
private static connectors = new Map<string, DatabaseConnector>();
|
||||
@@ -20,6 +23,16 @@ export class DatabaseConnectorFactory {
|
||||
case 'postgresql':
|
||||
connector = new PostgreSQLConnector(config);
|
||||
break;
|
||||
case 'mariadb':
|
||||
case 'mysql': // mysql 타입도 MariaDB 커넥터 사용
|
||||
connector = new MariaDBConnector(config);
|
||||
break;
|
||||
case 'mssql':
|
||||
connector = new MSSQLConnector(config);
|
||||
break;
|
||||
case 'oracle':
|
||||
connector = new OracleConnector(config);
|
||||
break;
|
||||
// Add other database types here
|
||||
default:
|
||||
throw new Error(`지원하지 않는 데이터베이스 타입: ${type}`);
|
||||
|
||||
182
backend-node/src/database/MSSQLConnector.ts
Normal file
182
backend-node/src/database/MSSQLConnector.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector';
|
||||
import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes';
|
||||
import * as mssql from 'mssql';
|
||||
|
||||
export class MSSQLConnector implements DatabaseConnector {
|
||||
private pool: mssql.ConnectionPool | null = null;
|
||||
private config: ConnectionConfig;
|
||||
|
||||
constructor(config: ConnectionConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (!this.pool) {
|
||||
const config: mssql.config = {
|
||||
server: this.config.host,
|
||||
port: this.config.port,
|
||||
user: this.config.user,
|
||||
password: this.config.password,
|
||||
database: this.config.database,
|
||||
options: {
|
||||
encrypt: this.config.ssl === true,
|
||||
trustServerCertificate: true
|
||||
},
|
||||
connectionTimeout: this.config.connectionTimeoutMillis || 15000,
|
||||
requestTimeout: this.config.queryTimeoutMillis || 15000
|
||||
};
|
||||
this.pool = await new mssql.ConnectionPool(config).connect();
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.pool) {
|
||||
await this.pool.close();
|
||||
this.pool = null;
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection(): Promise<ConnectionTestResult> {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
await this.connect();
|
||||
|
||||
// 버전 정보 조회
|
||||
const versionResult = await this.pool!.request().query('SELECT @@VERSION as version');
|
||||
|
||||
// 데이터베이스 크기 조회
|
||||
const sizeResult = await this.pool!.request()
|
||||
.input('dbName', mssql.VarChar, this.config.database)
|
||||
.query(`
|
||||
SELECT SUM(size * 8 * 1024) as size
|
||||
FROM sys.master_files
|
||||
WHERE database_id = DB_ID(@dbName)
|
||||
`);
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "MSSQL 연결이 성공했습니다.",
|
||||
details: {
|
||||
response_time: responseTime,
|
||||
server_version: versionResult.recordset[0]?.version || "알 수 없음",
|
||||
database_size: this.formatBytes(parseInt(sizeResult.recordset[0]?.size || "0")),
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: "MSSQL 연결에 실패했습니다.",
|
||||
error: {
|
||||
code: "CONNECTION_FAILED",
|
||||
details: error.message || "알 수 없는 오류",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async executeQuery(query: string): Promise<QueryResult> {
|
||||
try {
|
||||
await this.connect();
|
||||
const result = await this.pool!.request().query(query);
|
||||
return {
|
||||
rows: result.recordset,
|
||||
rowCount: result.rowsAffected[0],
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new Error(`쿼리 실행 오류: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getTables(): Promise<TableInfo[]> {
|
||||
try {
|
||||
await this.connect();
|
||||
const result = await this.pool!.request().query(`
|
||||
SELECT
|
||||
t.TABLE_NAME as table_name,
|
||||
c.COLUMN_NAME as column_name,
|
||||
c.DATA_TYPE as data_type,
|
||||
c.IS_NULLABLE as is_nullable,
|
||||
c.COLUMN_DEFAULT as column_default,
|
||||
CAST(p.value AS NVARCHAR(MAX)) as description
|
||||
FROM INFORMATION_SCHEMA.TABLES t
|
||||
LEFT JOIN INFORMATION_SCHEMA.COLUMNS c
|
||||
ON c.TABLE_NAME = t.TABLE_NAME
|
||||
LEFT JOIN sys.extended_properties p
|
||||
ON p.major_id = OBJECT_ID(t.TABLE_NAME)
|
||||
AND p.minor_id = 0
|
||||
AND p.name = 'MS_Description'
|
||||
WHERE t.TABLE_TYPE = 'BASE TABLE'
|
||||
ORDER BY t.TABLE_NAME, c.ORDINAL_POSITION
|
||||
`);
|
||||
|
||||
// 결과를 TableInfo[] 형식으로 변환
|
||||
const tables = new Map<string, TableInfo>();
|
||||
|
||||
result.recordset.forEach((row: any) => {
|
||||
if (!tables.has(row.table_name)) {
|
||||
tables.set(row.table_name, {
|
||||
table_name: row.table_name,
|
||||
columns: [],
|
||||
description: row.description || null
|
||||
});
|
||||
}
|
||||
|
||||
if (row.column_name) {
|
||||
tables.get(row.table_name)!.columns.push({
|
||||
column_name: row.column_name,
|
||||
data_type: row.data_type,
|
||||
is_nullable: row.is_nullable === 'YES' ? 'Y' : 'N',
|
||||
column_default: row.column_default
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(tables.values());
|
||||
} catch (error: any) {
|
||||
throw new Error(`테이블 목록 조회 오류: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getColumns(tableName: string): Promise<any[]> {
|
||||
try {
|
||||
await this.connect();
|
||||
const result = await this.pool!.request()
|
||||
.input('tableName', mssql.VarChar, tableName)
|
||||
.query(`
|
||||
SELECT
|
||||
c.COLUMN_NAME as name,
|
||||
c.DATA_TYPE as type,
|
||||
c.IS_NULLABLE as nullable,
|
||||
c.COLUMN_DEFAULT as default_value,
|
||||
c.CHARACTER_MAXIMUM_LENGTH as max_length,
|
||||
c.NUMERIC_PRECISION as precision,
|
||||
c.NUMERIC_SCALE as scale,
|
||||
CAST(p.value AS NVARCHAR(MAX)) as description
|
||||
FROM INFORMATION_SCHEMA.COLUMNS c
|
||||
LEFT JOIN sys.columns sc
|
||||
ON sc.object_id = OBJECT_ID(@tableName)
|
||||
AND sc.name = c.COLUMN_NAME
|
||||
LEFT JOIN sys.extended_properties p
|
||||
ON p.major_id = sc.object_id
|
||||
AND p.minor_id = sc.column_id
|
||||
AND p.name = 'MS_Description'
|
||||
WHERE c.TABLE_NAME = @tableName
|
||||
ORDER BY c.ORDINAL_POSITION
|
||||
`);
|
||||
|
||||
return result.recordset;
|
||||
} catch (error: any) {
|
||||
throw new Error(`컬럼 정보 조회 오류: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
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];
|
||||
}
|
||||
}
|
||||
127
backend-node/src/database/MariaDBConnector.ts
Normal file
127
backend-node/src/database/MariaDBConnector.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector';
|
||||
import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes';
|
||||
import * as mysql from 'mysql2/promise';
|
||||
|
||||
export class MariaDBConnector implements DatabaseConnector {
|
||||
private connection: mysql.Connection | null = null;
|
||||
private config: ConnectionConfig;
|
||||
|
||||
constructor(config: ConnectionConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (!this.connection) {
|
||||
this.connection = await mysql.createConnection({
|
||||
host: this.config.host,
|
||||
port: this.config.port,
|
||||
user: this.config.user,
|
||||
password: this.config.password,
|
||||
database: this.config.database,
|
||||
connectTimeout: this.config.connectionTimeoutMillis,
|
||||
ssl: typeof this.config.ssl === 'boolean' ? undefined : this.config.ssl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.connection) {
|
||||
await this.connection.end();
|
||||
this.connection = null;
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection(): Promise<ConnectionTestResult> {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
await this.connect();
|
||||
const [rows] = await this.connection!.query("SELECT VERSION() as version");
|
||||
const version = (rows as any[])[0]?.version || "Unknown";
|
||||
const responseTime = Date.now() - startTime;
|
||||
await this.disconnect();
|
||||
return {
|
||||
success: true,
|
||||
message: "MariaDB/MySQL 연결이 성공했습니다.",
|
||||
details: {
|
||||
response_time: responseTime,
|
||||
server_version: version,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
await this.disconnect();
|
||||
return {
|
||||
success: false,
|
||||
message: "MariaDB/MySQL 연결에 실패했습니다.",
|
||||
error: {
|
||||
code: "CONNECTION_FAILED",
|
||||
details: error.message || "알 수 없는 오류",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async executeQuery(query: string): Promise<QueryResult> {
|
||||
try {
|
||||
await this.connect();
|
||||
const [rows, fields] = await this.connection!.query(query);
|
||||
await this.disconnect();
|
||||
return {
|
||||
rows: rows as any[],
|
||||
fields: fields as any[],
|
||||
};
|
||||
} catch (error: any) {
|
||||
await this.disconnect();
|
||||
throw new Error(`쿼리 실행 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getTables(): Promise<TableInfo[]> {
|
||||
try {
|
||||
await this.connect();
|
||||
const [rows] = await this.connection!.query(`
|
||||
SELECT
|
||||
TABLE_NAME as table_name,
|
||||
TABLE_COMMENT as description
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
ORDER BY TABLE_NAME;
|
||||
`);
|
||||
|
||||
const tables: TableInfo[] = [];
|
||||
for (const row of rows as any[]) {
|
||||
const columns = await this.getColumns(row.table_name);
|
||||
tables.push({
|
||||
table_name: row.table_name,
|
||||
description: row.description || null,
|
||||
columns: columns,
|
||||
});
|
||||
}
|
||||
await this.disconnect();
|
||||
return tables;
|
||||
} catch (error: any) {
|
||||
await this.disconnect();
|
||||
throw new Error(`테이블 목록 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getColumns(tableName: string): Promise<any[]> {
|
||||
try {
|
||||
await this.connect();
|
||||
const [rows] = await this.connection!.query(`
|
||||
SELECT
|
||||
COLUMN_NAME as column_name,
|
||||
DATA_TYPE as data_type,
|
||||
IS_NULLABLE as is_nullable,
|
||||
COLUMN_DEFAULT as column_default
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
`, [tableName]);
|
||||
await this.disconnect();
|
||||
return rows as any[];
|
||||
} catch (error: any) {
|
||||
await this.disconnect();
|
||||
throw new Error(`컬럼 정보 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
225
backend-node/src/database/OracleConnector.ts
Normal file
225
backend-node/src/database/OracleConnector.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import * as oracledb from 'oracledb';
|
||||
import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector';
|
||||
import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes';
|
||||
|
||||
export class OracleConnector implements DatabaseConnector {
|
||||
private connection: oracledb.Connection | null = null;
|
||||
private config: ConnectionConfig;
|
||||
|
||||
constructor(config: ConnectionConfig) {
|
||||
this.config = config;
|
||||
|
||||
// Oracle XE 21c 특화 설정
|
||||
// oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
|
||||
// oracledb.autoCommit = true;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
try {
|
||||
// Oracle XE 21c 연결 문자열 구성
|
||||
const connectionString = this.buildConnectionString();
|
||||
|
||||
const connectionConfig: any = {
|
||||
user: this.config.user,
|
||||
password: this.config.password,
|
||||
connectString: connectionString
|
||||
};
|
||||
|
||||
this.connection = await oracledb.getConnection(connectionConfig);
|
||||
console.log('Oracle XE 21c 연결 성공');
|
||||
} catch (error: any) {
|
||||
console.error('Oracle XE 21c 연결 실패:', error);
|
||||
throw new Error(`Oracle 연결 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private buildConnectionString(): string {
|
||||
const { host, port, database } = this.config;
|
||||
|
||||
// Oracle XE 21c는 기본적으로 XE 서비스명을 사용
|
||||
// 다양한 연결 문자열 형식 지원
|
||||
if (database.includes('/') || database.includes(':')) {
|
||||
// 이미 완전한 연결 문자열인 경우
|
||||
return database;
|
||||
}
|
||||
|
||||
// Oracle XE 21c 표준 형식
|
||||
return `${host}:${port}/${database}`;
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.connection) {
|
||||
try {
|
||||
await this.connection.close();
|
||||
this.connection = null;
|
||||
console.log('Oracle 연결 해제됨');
|
||||
} catch (error: any) {
|
||||
console.error('Oracle 연결 해제 실패:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection(): Promise<ConnectionTestResult> {
|
||||
try {
|
||||
if (!this.connection) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
// Oracle XE 21c 버전 확인 쿼리
|
||||
const result = await this.connection!.execute(
|
||||
'SELECT BANNER FROM V$VERSION WHERE BANNER LIKE \'Oracle%\''
|
||||
);
|
||||
|
||||
console.log('Oracle 버전:', result.rows);
|
||||
return {
|
||||
success: true,
|
||||
message: '연결 성공',
|
||||
details: {
|
||||
server_version: (result.rows as any)?.[0]?.BANNER || 'Unknown'
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('Oracle 연결 테스트 실패:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: '연결 실패',
|
||||
details: {
|
||||
server_version: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async executeQuery(query: string, params: any[] = []): Promise<QueryResult> {
|
||||
if (!this.connection) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Oracle XE 21c 쿼리 실행 옵션
|
||||
const options: any = {
|
||||
outFormat: oracledb.OUT_FORMAT_OBJECT, // OBJECT format
|
||||
maxRows: 10000, // XE 제한 고려
|
||||
fetchArraySize: 100
|
||||
};
|
||||
|
||||
const result = await this.connection!.execute(query, params, options);
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
console.log('Oracle 쿼리 실행 결과:', {
|
||||
query,
|
||||
rowCount: result.rows?.length || 0,
|
||||
metaData: result.metaData?.length || 0,
|
||||
executionTime: `${executionTime}ms`,
|
||||
actualRows: result.rows,
|
||||
metaDataInfo: result.metaData
|
||||
});
|
||||
|
||||
return {
|
||||
rows: result.rows || [],
|
||||
rowCount: result.rowsAffected || (result.rows?.length || 0),
|
||||
fields: this.extractFieldInfo(result.metaData || [])
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('Oracle 쿼리 실행 실패:', error);
|
||||
throw new Error(`쿼리 실행 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private extractFieldInfo(metaData: any[]): any[] {
|
||||
return metaData.map(field => ({
|
||||
name: field.name,
|
||||
type: this.mapOracleType(field.dbType),
|
||||
length: field.precision || field.byteSize,
|
||||
nullable: field.nullable
|
||||
}));
|
||||
}
|
||||
|
||||
private mapOracleType(oracleType: any): string {
|
||||
// Oracle XE 21c 타입 매핑 (간단한 방식)
|
||||
if (typeof oracleType === 'string') {
|
||||
return oracleType;
|
||||
}
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
async getTables(): Promise<TableInfo[]> {
|
||||
try {
|
||||
// 현재 사용자 스키마의 테이블들만 조회
|
||||
const query = `
|
||||
SELECT table_name, USER as owner
|
||||
FROM user_tables
|
||||
ORDER BY table_name
|
||||
`;
|
||||
|
||||
console.log('Oracle 테이블 조회 시작 - 사용자:', this.config.user);
|
||||
|
||||
const result = await this.executeQuery(query);
|
||||
console.log('사용자 스키마 테이블 조회 결과:', result.rows);
|
||||
|
||||
const tables = result.rows.map((row: any) => ({
|
||||
table_name: row.TABLE_NAME,
|
||||
columns: [],
|
||||
description: null
|
||||
}));
|
||||
|
||||
console.log(`총 ${tables.length}개의 사용자 테이블을 찾았습니다.`);
|
||||
return tables;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Oracle 테이블 목록 조회 실패:', error);
|
||||
throw new Error(`테이블 목록 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getColumns(tableName: string): Promise<any[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
data_length,
|
||||
data_precision,
|
||||
data_scale,
|
||||
nullable,
|
||||
data_default
|
||||
FROM user_tab_columns
|
||||
WHERE table_name = UPPER(:tableName)
|
||||
ORDER BY column_id
|
||||
`;
|
||||
|
||||
const result = await this.executeQuery(query, [tableName]);
|
||||
|
||||
return result.rows.map((row: any) => ({
|
||||
column_name: row.COLUMN_NAME,
|
||||
data_type: this.formatOracleDataType(row),
|
||||
is_nullable: row.NULLABLE === 'Y' ? 'YES' : 'NO',
|
||||
column_default: row.DATA_DEFAULT
|
||||
}));
|
||||
} catch (error: any) {
|
||||
console.error('Oracle 테이블 컬럼 조회 실패:', error);
|
||||
throw new Error(`테이블 컬럼 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private formatOracleDataType(row: any): string {
|
||||
const { DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE } = row;
|
||||
|
||||
switch (DATA_TYPE) {
|
||||
case 'NUMBER':
|
||||
if (DATA_PRECISION && DATA_SCALE !== null) {
|
||||
return `NUMBER(${DATA_PRECISION},${DATA_SCALE})`;
|
||||
} else if (DATA_PRECISION) {
|
||||
return `NUMBER(${DATA_PRECISION})`;
|
||||
}
|
||||
return 'NUMBER';
|
||||
case 'VARCHAR2':
|
||||
case 'CHAR':
|
||||
return `${DATA_TYPE}(${DATA_LENGTH})`;
|
||||
default:
|
||||
return DATA_TYPE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import { JwtUtils } from "../utils/jwtUtils";
|
||||
import { AuthenticatedRequest, PersonBean } from "../types/auth";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// AuthenticatedRequest 타입을 다른 모듈에서 사용할 수 있도록 re-export
|
||||
export { AuthenticatedRequest } from "../types/auth";
|
||||
|
||||
// Express Request 타입 확장
|
||||
declare global {
|
||||
namespace Express {
|
||||
|
||||
73
backend-node/src/routes/batchRoutes.ts
Normal file
73
backend-node/src/routes/batchRoutes.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// 배치 관리 라우트
|
||||
// 작성일: 2024-12-23
|
||||
|
||||
import { Router } from 'express';
|
||||
import { BatchController } from '../controllers/batchController';
|
||||
import { authenticateToken } from '../middleware/authMiddleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(authenticateToken);
|
||||
|
||||
/**
|
||||
* GET /api/batch
|
||||
* 배치 작업 목록 조회
|
||||
*/
|
||||
router.get('/', BatchController.getBatchJobs);
|
||||
|
||||
/**
|
||||
* GET /api/batch/:id
|
||||
* 배치 작업 상세 조회
|
||||
*/
|
||||
router.get('/:id', BatchController.getBatchJobById);
|
||||
|
||||
/**
|
||||
* POST /api/batch
|
||||
* 배치 작업 생성
|
||||
*/
|
||||
router.post('/', BatchController.createBatchJob);
|
||||
|
||||
/**
|
||||
* PUT /api/batch/:id
|
||||
* 배치 작업 수정
|
||||
*/
|
||||
router.put('/:id', BatchController.updateBatchJob);
|
||||
|
||||
/**
|
||||
* DELETE /api/batch/:id
|
||||
* 배치 작업 삭제
|
||||
*/
|
||||
router.delete('/:id', BatchController.deleteBatchJob);
|
||||
|
||||
/**
|
||||
* POST /api/batch/:id/execute
|
||||
* 배치 작업 수동 실행
|
||||
*/
|
||||
router.post('/:id/execute', BatchController.executeBatchJob);
|
||||
|
||||
/**
|
||||
* GET /api/batch/executions
|
||||
* 배치 실행 목록 조회
|
||||
*/
|
||||
router.get('/executions/list', BatchController.getBatchExecutions);
|
||||
|
||||
/**
|
||||
* GET /api/batch/monitoring
|
||||
* 배치 모니터링 정보 조회
|
||||
*/
|
||||
router.get('/monitoring/status', BatchController.getBatchMonitoring);
|
||||
|
||||
/**
|
||||
* GET /api/batch/types/supported
|
||||
* 지원되는 작업 타입 조회
|
||||
*/
|
||||
router.get('/types/supported', BatchController.getSupportedJobTypes);
|
||||
|
||||
/**
|
||||
* GET /api/batch/schedules/presets
|
||||
* 스케줄 프리셋 조회
|
||||
*/
|
||||
router.get('/schedules/presets', BatchController.getSchedulePresets);
|
||||
|
||||
export default router;
|
||||
61
backend-node/src/routes/collectionRoutes.ts
Normal file
61
backend-node/src/routes/collectionRoutes.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// 수집 관리 라우트
|
||||
// 작성일: 2024-12-23
|
||||
|
||||
import { Router } from 'express';
|
||||
import { CollectionController } from '../controllers/collectionController';
|
||||
import { authenticateToken } from '../middleware/authMiddleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(authenticateToken);
|
||||
|
||||
/**
|
||||
* GET /api/collections
|
||||
* 수집 설정 목록 조회
|
||||
*/
|
||||
router.get('/', CollectionController.getCollectionConfigs);
|
||||
|
||||
/**
|
||||
* GET /api/collections/:id
|
||||
* 수집 설정 상세 조회
|
||||
*/
|
||||
router.get('/:id', CollectionController.getCollectionConfigById);
|
||||
|
||||
/**
|
||||
* POST /api/collections
|
||||
* 수집 설정 생성
|
||||
*/
|
||||
router.post('/', CollectionController.createCollectionConfig);
|
||||
|
||||
/**
|
||||
* PUT /api/collections/:id
|
||||
* 수집 설정 수정
|
||||
*/
|
||||
router.put('/:id', CollectionController.updateCollectionConfig);
|
||||
|
||||
/**
|
||||
* DELETE /api/collections/:id
|
||||
* 수집 설정 삭제
|
||||
*/
|
||||
router.delete('/:id', CollectionController.deleteCollectionConfig);
|
||||
|
||||
/**
|
||||
* POST /api/collections/:id/execute
|
||||
* 수집 작업 실행
|
||||
*/
|
||||
router.post('/:id/execute', CollectionController.executeCollection);
|
||||
|
||||
/**
|
||||
* GET /api/collections/jobs
|
||||
* 수집 작업 목록 조회
|
||||
*/
|
||||
router.get('/jobs/list', CollectionController.getCollectionJobs);
|
||||
|
||||
/**
|
||||
* GET /api/collections/:configId/history
|
||||
* 수집 이력 조회
|
||||
*/
|
||||
router.get('/:configId/history', CollectionController.getCollectionHistory);
|
||||
|
||||
export default router;
|
||||
@@ -340,5 +340,37 @@ router.get(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/external-db-connections/:id/tables/:tableName/columns
|
||||
* 특정 테이블의 컬럼 정보 조회
|
||||
*/
|
||||
router.get(
|
||||
"/:id/tables/:tableName/columns",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const tableName = req.params.tableName;
|
||||
|
||||
if (!tableName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "테이블명이 입력되지 않았습니다."
|
||||
});
|
||||
}
|
||||
|
||||
const result = await ExternalDbConnectionService.getTableColumns(id, tableName);
|
||||
return res.json(result);
|
||||
} catch (error) {
|
||||
console.error("테이블 컬럼 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "테이블 컬럼 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TableInfo,
|
||||
} from "../types/externalDbTypes";
|
||||
import { PasswordEncryption } from "../utils/passwordEncryption";
|
||||
import { DbConnectionManager } from "./dbConnectionManager";
|
||||
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -254,11 +254,8 @@ export class ExternalDbConnectionService {
|
||||
};
|
||||
|
||||
// 연결 테스트 수행
|
||||
const testResult = await DbConnectionManager.testConnection(
|
||||
id,
|
||||
existingConnection.db_type,
|
||||
testConfig
|
||||
);
|
||||
const connector = await DatabaseConnectorFactory.createConnector(existingConnection.db_type, testConfig, id);
|
||||
const testResult = await connector.testConnection();
|
||||
|
||||
if (!testResult.success) {
|
||||
return {
|
||||
@@ -401,8 +398,15 @@ export class ExternalDbConnectionService {
|
||||
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
|
||||
};
|
||||
|
||||
// DbConnectionManager를 통한 연결 테스트
|
||||
return await DbConnectionManager.testConnection(id, connection.db_type, config);
|
||||
// DatabaseConnectorFactory를 통한 연결 테스트
|
||||
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, id);
|
||||
const testResult = await connector.testConnection();
|
||||
|
||||
return {
|
||||
success: testResult.success,
|
||||
message: testResult.message,
|
||||
details: testResult.details
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -453,7 +457,7 @@ export class ExternalDbConnectionService {
|
||||
}
|
||||
|
||||
// DB 타입 유효성 검사
|
||||
const validDbTypes = ["mysql", "postgresql", "oracle", "mssql", "sqlite"];
|
||||
const validDbTypes = ["mysql", "postgresql", "oracle", "mssql", "sqlite", "mariadb"];
|
||||
if (!validDbTypes.includes(data.db_type)) {
|
||||
throw new Error("지원하지 않는 DB 타입입니다.");
|
||||
}
|
||||
@@ -524,8 +528,9 @@ export class ExternalDbConnectionService {
|
||||
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
|
||||
};
|
||||
|
||||
// DbConnectionManager를 통한 쿼리 실행
|
||||
const result = await DbConnectionManager.executeQuery(id, connection.db_type, config, query);
|
||||
// DatabaseConnectorFactory를 통한 쿼리 실행
|
||||
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, id);
|
||||
const result = await connector.executeQuery(query);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -632,8 +637,9 @@ export class ExternalDbConnectionService {
|
||||
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
|
||||
};
|
||||
|
||||
// DbConnectionManager를 통한 테이블 목록 조회
|
||||
const tables = await DbConnectionManager.getTables(id, connection.db_type, config);
|
||||
// DatabaseConnectorFactory를 통한 테이블 목록 조회
|
||||
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, id);
|
||||
const tables = await connector.getTables();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -713,4 +719,57 @@ export class ExternalDbConnectionService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 테이블의 컬럼 정보 조회
|
||||
*/
|
||||
static async getTableColumns(connectionId: number, tableName: string): Promise<ApiResponse<any[]>> {
|
||||
let client: any = null;
|
||||
|
||||
try {
|
||||
const connection = await this.getConnectionById(connectionId);
|
||||
if (!connection.success || !connection.data) {
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 정보를 찾을 수 없습니다."
|
||||
};
|
||||
}
|
||||
|
||||
const connectionData = connection.data;
|
||||
|
||||
// 비밀번호 복호화
|
||||
const decryptedPassword = PasswordEncryption.decrypt(connectionData.password);
|
||||
|
||||
// 연결 설정 준비
|
||||
const config = {
|
||||
host: connectionData.host,
|
||||
port: connectionData.port,
|
||||
database: connectionData.database_name,
|
||||
user: connectionData.username,
|
||||
password: decryptedPassword,
|
||||
connectionTimeoutMillis: connectionData.connection_timeout != null ? connectionData.connection_timeout * 1000 : undefined,
|
||||
queryTimeoutMillis: connectionData.query_timeout != null ? connectionData.query_timeout * 1000 : undefined,
|
||||
ssl: connectionData.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
|
||||
};
|
||||
|
||||
// 데이터베이스 타입에 따른 커넥터 생성
|
||||
const connector = await DatabaseConnectorFactory.createConnector(connectionData.db_type, config, connectionId);
|
||||
|
||||
// 컬럼 정보 조회
|
||||
const columns = await connector.getColumns(tableName);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: columns,
|
||||
message: "컬럼 정보를 조회했습니다."
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("컬럼 정보 조회 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
98
backend-node/src/types/batchManagement.ts
Normal file
98
backend-node/src/types/batchManagement.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
// 배치 관리 관련 타입 정의
|
||||
// 작성일: 2024-12-23
|
||||
|
||||
export interface BatchJob {
|
||||
id?: number;
|
||||
job_name: string;
|
||||
description?: string | null;
|
||||
job_type: string;
|
||||
schedule_cron?: string | null;
|
||||
is_active: string; // 'Y' | 'N'
|
||||
config_json?: Record<string, any> | null;
|
||||
last_executed_at?: Date | null;
|
||||
next_execution_at?: Date | null;
|
||||
execution_count: number;
|
||||
success_count: number;
|
||||
failure_count: number;
|
||||
created_date?: Date | null;
|
||||
created_by?: string | null;
|
||||
updated_date?: Date | null;
|
||||
updated_by?: string | null;
|
||||
company_code: string;
|
||||
}
|
||||
|
||||
export interface BatchJobFilter {
|
||||
job_name?: string;
|
||||
job_type?: string;
|
||||
is_active?: string;
|
||||
company_code?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface BatchExecution {
|
||||
id?: number;
|
||||
job_id: number;
|
||||
execution_status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
started_at?: Date;
|
||||
completed_at?: Date;
|
||||
execution_time_ms?: number;
|
||||
result_data?: Record<string, any>;
|
||||
error_message?: string;
|
||||
log_details?: string;
|
||||
created_date?: Date;
|
||||
}
|
||||
|
||||
export interface BatchSchedule {
|
||||
id?: number;
|
||||
job_id: number;
|
||||
schedule_name: string;
|
||||
cron_expression: string;
|
||||
timezone?: string;
|
||||
is_active: string;
|
||||
last_triggered_at?: Date;
|
||||
next_trigger_at?: Date;
|
||||
created_date?: Date;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
export interface BatchMonitoring {
|
||||
total_jobs: number;
|
||||
active_jobs: number;
|
||||
running_jobs: number;
|
||||
failed_jobs_today: number;
|
||||
successful_jobs_today: number;
|
||||
recent_executions: BatchExecution[];
|
||||
}
|
||||
|
||||
// 배치 작업 타입 옵션
|
||||
export const BATCH_JOB_TYPE_OPTIONS = [
|
||||
{ value: 'collection', label: '데이터 수집' },
|
||||
{ value: 'sync', label: '데이터 동기화' },
|
||||
{ value: 'cleanup', label: '데이터 정리' },
|
||||
{ value: 'custom', label: '사용자 정의' },
|
||||
];
|
||||
|
||||
// 실행 상태 옵션
|
||||
export const EXECUTION_STATUS_OPTIONS = [
|
||||
{ value: 'pending', label: '대기 중' },
|
||||
{ value: 'running', label: '실행 중' },
|
||||
{ value: 'completed', label: '완료' },
|
||||
{ value: 'failed', label: '실패' },
|
||||
{ value: 'cancelled', label: '취소됨' },
|
||||
];
|
||||
|
||||
// 스케줄 프리셋
|
||||
export const SCHEDULE_PRESETS = [
|
||||
{ value: '0 */1 * * *', label: '매시간' },
|
||||
{ value: '0 0 */6 * *', label: '6시간마다' },
|
||||
{ value: '0 0 * * *', label: '매일 자정' },
|
||||
{ value: '0 0 * * 0', label: '매주 일요일' },
|
||||
{ value: '0 0 1 * *', label: '매월 1일' },
|
||||
];
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
75
backend-node/src/types/collectionManagement.ts
Normal file
75
backend-node/src/types/collectionManagement.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// 수집 관리 관련 타입 정의
|
||||
// 작성일: 2024-12-23
|
||||
|
||||
export interface DataCollectionConfig {
|
||||
id?: number;
|
||||
config_name: string;
|
||||
description?: string | null;
|
||||
source_connection_id: number;
|
||||
source_table: string;
|
||||
target_table?: string | null;
|
||||
collection_type: string;
|
||||
schedule_cron?: string | null;
|
||||
is_active: string; // 'Y' | 'N'
|
||||
last_collected_at?: Date | null;
|
||||
collection_options?: Record<string, any> | null;
|
||||
created_date?: Date | null;
|
||||
created_by?: string | null;
|
||||
updated_date?: Date | null;
|
||||
updated_by?: string | null;
|
||||
company_code: string;
|
||||
}
|
||||
|
||||
export interface CollectionFilter {
|
||||
config_name?: string;
|
||||
source_connection_id?: number;
|
||||
collection_type?: string;
|
||||
is_active?: string;
|
||||
company_code?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface CollectionJob {
|
||||
id?: number;
|
||||
config_id: number;
|
||||
job_status: string;
|
||||
started_at?: Date | null;
|
||||
completed_at?: Date | null;
|
||||
records_processed?: number | null;
|
||||
error_message?: string | null;
|
||||
job_details?: Record<string, any> | null;
|
||||
created_date?: Date | null;
|
||||
}
|
||||
|
||||
export interface CollectionHistory {
|
||||
id?: number;
|
||||
config_id: number;
|
||||
collection_date: Date;
|
||||
records_collected: number;
|
||||
execution_time_ms: number;
|
||||
status: string;
|
||||
error_details?: string | null;
|
||||
created_date?: Date | null;
|
||||
}
|
||||
|
||||
// 수집 타입 옵션
|
||||
export const COLLECTION_TYPE_OPTIONS = [
|
||||
{ value: 'full', label: '전체 수집' },
|
||||
{ value: 'incremental', label: '증분 수집' },
|
||||
{ value: 'delta', label: '변경분 수집' },
|
||||
];
|
||||
|
||||
// 작업 상태 옵션
|
||||
export const JOB_STATUS_OPTIONS = [
|
||||
{ value: 'pending', label: '대기 중' },
|
||||
{ value: 'running', label: '실행 중' },
|
||||
{ value: 'completed', label: '완료' },
|
||||
{ value: 'failed', label: '실패' },
|
||||
];
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export interface ExternalDbConnection {
|
||||
id?: number;
|
||||
connection_name: string;
|
||||
description?: string | null;
|
||||
db_type: "mysql" | "postgresql" | "oracle" | "mssql" | "sqlite";
|
||||
db_type: "mysql" | "postgresql" | "oracle" | "mssql" | "sqlite" | "mariadb";
|
||||
host: string;
|
||||
port: number;
|
||||
database_name: string;
|
||||
@@ -58,6 +58,7 @@ export const DB_TYPE_OPTIONS = [
|
||||
{ value: "postgresql", label: "PostgreSQL" },
|
||||
{ value: "oracle", label: "Oracle" },
|
||||
{ value: "mssql", label: "SQL Server" },
|
||||
{ value: "mariadb", label: "MariaDB" },
|
||||
{ value: "sqlite", label: "SQLite" },
|
||||
];
|
||||
|
||||
@@ -67,6 +68,7 @@ export const DB_TYPE_DEFAULTS = {
|
||||
postgresql: { port: 5432, driver: "pg" },
|
||||
oracle: { port: 1521, driver: "oracledb" },
|
||||
mssql: { port: 1433, driver: "mssql" },
|
||||
mariadb: { port: 3306, driver: "mysql2" },
|
||||
sqlite: { port: 0, driver: "sqlite3" },
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user