배치관리 중간커밋

This commit is contained in:
2025-09-24 10:46:55 +09:00
parent d9270e6307
commit 4abf5b31c0
12 changed files with 2857 additions and 41 deletions

View File

@@ -31,6 +31,7 @@ import layoutRoutes from "./routes/layoutRoutes";
import dataRoutes from "./routes/dataRoutes";
import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
import batchRoutes from "./routes/batchRoutes";
import ddlRoutes from "./routes/ddlRoutes";
import entityReferenceRoutes from "./routes/entityReferenceRoutes";
// import userRoutes from './routes/userRoutes';
@@ -127,6 +128,7 @@ app.use("/api/screen", screenStandardRoutes);
app.use("/api/data", dataRoutes);
app.use("/api/test-button-dataflow", testButtonDataflowRoutes);
app.use("/api/external-db-connections", externalDbConnectionRoutes);
app.use("/api/batch-configs", batchRoutes);
app.use("/api/ddl", ddlRoutes);
app.use("/api/entity-reference", entityReferenceRoutes);
// app.use('/api/users', userRoutes);

View File

@@ -0,0 +1,312 @@
// 배치관리 컨트롤러
// 작성일: 2024-12-24
import { Request, Response } from "express";
import { BatchService } from "../services/batchService";
import { BatchConfigFilter, BatchMappingRequest } from "../types/batchTypes";
export interface AuthenticatedRequest extends Request {
user?: {
userId: string;
username: string;
companyCode: string;
};
}
export class BatchController {
/**
* 배치 설정 목록 조회
* GET /api/batch-configs
*/
static async getBatchConfigs(req: AuthenticatedRequest, res: Response) {
try {
const filter: BatchConfigFilter = {
is_active: req.query.is_active as string,
company_code: req.query.company_code as string,
search: req.query.search as string,
};
// 빈 값 제거
Object.keys(filter).forEach((key) => {
if (!filter[key as keyof BatchConfigFilter]) {
delete filter[key as keyof BatchConfigFilter];
}
});
const result = await BatchService.getBatchConfigs(filter);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
console.error("배치 설정 목록 조회 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 특정 배치 설정 조회
* GET /api/batch-configs/:id
*/
static async getBatchConfigById(req: AuthenticatedRequest, res: Response) {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({
success: false,
message: "유효하지 않은 배치 설정 ID입니다.",
});
}
const result = await BatchService.getBatchConfigById(id);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(404).json(result);
}
} catch (error) {
console.error("배치 설정 조회 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 배치 설정 생성
* POST /api/batch-configs
*/
static async createBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const data: BatchMappingRequest = req.body;
// 필수 필드 검증
if (!data.batch_name || !data.cron_schedule || !data.mappings) {
return res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (batch_name, cron_schedule, mappings)",
});
}
const result = await BatchService.createBatchConfig(
data,
req.user?.userId
);
if (result.success) {
return res.status(201).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
console.error("배치 설정 생성 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 배치 설정 수정
* PUT /api/batch-configs/:id
*/
static async updateBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const id = parseInt(req.params.id);
const data: Partial<BatchMappingRequest> = req.body;
if (isNaN(id)) {
return res.status(400).json({
success: false,
message: "유효하지 않은 배치 설정 ID입니다.",
});
}
const result = await BatchService.updateBatchConfig(
id,
data,
req.user?.userId
);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
console.error("배치 설정 수정 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 배치 설정 삭제
* DELETE /api/batch-configs/:id
*/
static async deleteBatchConfig(req: AuthenticatedRequest, res: Response) {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({
success: false,
message: "유효하지 않은 배치 설정 ID입니다.",
});
}
const result = await BatchService.deleteBatchConfig(
id,
req.user?.userId
);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(404).json(result);
}
} catch (error) {
console.error("배치 설정 삭제 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 사용 가능한 커넥션 목록 조회
* GET /api/batch-configs/connections
*/
static async getAvailableConnections(req: AuthenticatedRequest, res: Response) {
try {
const result = await BatchService.getAvailableConnections();
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
console.error("커넥션 목록 조회 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 특정 커넥션의 테이블 목록 조회
* GET /api/batch-configs/connections/:type/tables
* GET /api/batch-configs/connections/:type/:id/tables
*/
static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) {
try {
const connectionType = req.params.type as 'internal' | 'external';
const connectionId = req.params.id ? parseInt(req.params.id) : undefined;
if (connectionType !== 'internal' && connectionType !== 'external') {
return res.status(400).json({
success: false,
message: "유효하지 않은 커넥션 타입입니다. (internal 또는 external)",
});
}
if (connectionType === 'external' && (!connectionId || isNaN(connectionId))) {
return res.status(400).json({
success: false,
message: "외부 커넥션의 경우 유효한 커넥션 ID가 필요합니다.",
});
}
const result = await BatchService.getTablesFromConnection(
connectionType,
connectionId
);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
console.error("테이블 목록 조회 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
/**
* 특정 테이블의 컬럼 정보 조회
* GET /api/batch-configs/connections/:type/tables/:tableName/columns
* GET /api/batch-configs/connections/:type/:id/tables/:tableName/columns
*/
static async getTableColumns(req: AuthenticatedRequest, res: Response) {
try {
const connectionType = req.params.type as 'internal' | 'external';
const connectionId = req.params.id ? parseInt(req.params.id) : undefined;
const tableName = req.params.tableName;
if (connectionType !== 'internal' && connectionType !== 'external') {
return res.status(400).json({
success: false,
message: "유효하지 않은 커넥션 타입입니다. (internal 또는 external)",
});
}
if (connectionType === 'external' && (!connectionId || isNaN(connectionId))) {
return res.status(400).json({
success: false,
message: "외부 커넥션의 경우 유효한 커넥션 ID가 필요합니다.",
});
}
if (!tableName) {
return res.status(400).json({
success: false,
message: "테이블명이 필요합니다.",
});
}
const result = await BatchService.getTableColumns(
connectionType,
tableName,
connectionId
);
if (result.success) {
return res.status(200).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
console.error("컬럼 정보 조회 오류:", error);
return res.status(500).json({
success: false,
message: "서버 내부 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
}

View File

@@ -0,0 +1,70 @@
// 배치관리 라우트
// 작성일: 2024-12-24
import { Router } from "express";
import { BatchController } from "../controllers/batchController";
import { authenticateToken } from "../middleware/auth";
const router = Router();
/**
* GET /api/batch-configs
* 배치 설정 목록 조회
*/
router.get("/", authenticateToken, BatchController.getBatchConfigs);
/**
* GET /api/batch-configs/connections
* 사용 가능한 커넥션 목록 조회
*/
router.get("/connections", authenticateToken, BatchController.getAvailableConnections);
/**
* GET /api/batch-configs/connections/:type/tables
* 내부 DB 테이블 목록 조회
*/
router.get("/connections/:type/tables", authenticateToken, BatchController.getTablesFromConnection);
/**
* GET /api/batch-configs/connections/:type/:id/tables
* 외부 DB 테이블 목록 조회
*/
router.get("/connections/:type/:id/tables", authenticateToken, BatchController.getTablesFromConnection);
/**
* GET /api/batch-configs/connections/:type/tables/:tableName/columns
* 내부 DB 테이블 컬럼 정보 조회
*/
router.get("/connections/:type/tables/:tableName/columns", authenticateToken, BatchController.getTableColumns);
/**
* GET /api/batch-configs/connections/:type/:id/tables/:tableName/columns
* 외부 DB 테이블 컬럼 정보 조회
*/
router.get("/connections/:type/:id/tables/:tableName/columns", authenticateToken, BatchController.getTableColumns);
/**
* GET /api/batch-configs/:id
* 특정 배치 설정 조회
*/
router.get("/:id", authenticateToken, BatchController.getBatchConfigById);
/**
* POST /api/batch-configs
* 배치 설정 생성
*/
router.post("/", authenticateToken, BatchController.createBatchConfig);
/**
* PUT /api/batch-configs/:id
* 배치 설정 수정
*/
router.put("/:id", authenticateToken, BatchController.updateBatchConfig);
/**
* DELETE /api/batch-configs/:id
* 배치 설정 삭제 (논리 삭제)
*/
router.delete("/:id", authenticateToken, BatchController.deleteBatchConfig);
export default router;

View File

@@ -0,0 +1,550 @@
// 배치관리 서비스
// 작성일: 2024-12-24
import { PrismaClient } from "@prisma/client";
import {
BatchConfig,
BatchMapping,
BatchConfigFilter,
BatchMappingRequest,
BatchValidationResult,
ApiResponse,
ConnectionInfo,
TableInfo,
ColumnInfo,
} from "../types/batchTypes";
import { ExternalDbConnectionService } from "./externalDbConnectionService";
import { DbConnectionManager } from "./dbConnectionManager";
const prisma = new PrismaClient();
export class BatchService {
/**
* 배치 설정 목록 조회
*/
static async getBatchConfigs(
filter: BatchConfigFilter
): Promise<ApiResponse<BatchConfig[]>> {
try {
const where: any = {};
// 필터 조건 적용
if (filter.is_active) {
where.is_active = filter.is_active;
}
if (filter.company_code) {
where.company_code = filter.company_code;
}
// 검색 조건 적용
if (filter.search && filter.search.trim()) {
where.OR = [
{
batch_name: {
contains: filter.search.trim(),
mode: "insensitive",
},
},
{
description: {
contains: filter.search.trim(),
mode: "insensitive",
},
},
];
}
const batchConfigs = await prisma.batch_configs.findMany({
where,
include: {
batch_mappings: true,
},
orderBy: [{ is_active: "desc" }, { batch_name: "asc" }],
});
return {
success: true,
data: batchConfigs as BatchConfig[],
};
} catch (error) {
console.error("배치 설정 목록 조회 오류:", error);
return {
success: false,
message: "배치 설정 목록 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* 특정 배치 설정 조회
*/
static async getBatchConfigById(
id: number
): Promise<ApiResponse<BatchConfig>> {
try {
const batchConfig = await prisma.batch_configs.findUnique({
where: { id },
include: {
batch_mappings: {
orderBy: [
{ from_table_name: "asc" },
{ from_column_name: "asc" },
{ mapping_order: "asc" },
],
},
},
});
if (!batchConfig) {
return {
success: false,
message: "배치 설정을 찾을 수 없습니다.",
};
}
return {
success: true,
data: batchConfig as BatchConfig,
};
} catch (error) {
console.error("배치 설정 조회 오류:", error);
return {
success: false,
message: "배치 설정 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* 배치 설정 생성
*/
static async createBatchConfig(
data: BatchMappingRequest,
userId?: string
): Promise<ApiResponse<BatchConfig>> {
try {
// 매핑 유효성 검사
const validation = await this.validateBatchMappings(data.mappings);
if (!validation.isValid) {
return {
success: false,
message: "매핑 유효성 검사 실패",
error: validation.errors.join(", "),
};
}
// 트랜잭션으로 배치 설정과 매핑 생성
const result = await prisma.$transaction(async (tx) => {
// 배치 설정 생성
const batchConfig = await tx.batch_configs.create({
data: {
batch_name: data.batch_name,
description: data.description,
cron_schedule: data.cron_schedule,
created_by: userId,
updated_by: userId,
},
});
// 배치 매핑 생성
const mappings = await Promise.all(
data.mappings.map((mapping, index) =>
tx.batch_mappings.create({
data: {
batch_config_id: batchConfig.id,
from_connection_type: mapping.from_connection_type,
from_connection_id: mapping.from_connection_id,
from_table_name: mapping.from_table_name,
from_column_name: mapping.from_column_name,
from_column_type: mapping.from_column_type,
to_connection_type: mapping.to_connection_type,
to_connection_id: mapping.to_connection_id,
to_table_name: mapping.to_table_name,
to_column_name: mapping.to_column_name,
to_column_type: mapping.to_column_type,
mapping_order: mapping.mapping_order || index + 1,
created_by: userId,
},
})
)
);
return {
...batchConfig,
batch_mappings: mappings,
};
});
return {
success: true,
data: result as BatchConfig,
message: "배치 설정이 성공적으로 생성되었습니다.",
};
} catch (error) {
console.error("배치 설정 생성 오류:", error);
return {
success: false,
message: "배치 설정 생성에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* 배치 설정 수정
*/
static async updateBatchConfig(
id: number,
data: Partial<BatchMappingRequest>,
userId?: string
): Promise<ApiResponse<BatchConfig>> {
try {
// 기존 배치 설정 확인
const existingConfig = await prisma.batch_configs.findUnique({
where: { id },
include: { batch_mappings: true },
});
if (!existingConfig) {
return {
success: false,
message: "배치 설정을 찾을 수 없습니다.",
};
}
// 매핑이 제공된 경우 유효성 검사
if (data.mappings) {
const validation = await this.validateBatchMappings(data.mappings);
if (!validation.isValid) {
return {
success: false,
message: "매핑 유효성 검사 실패",
error: validation.errors.join(", "),
};
}
}
// 트랜잭션으로 업데이트
const result = await prisma.$transaction(async (tx) => {
// 배치 설정 업데이트
const updateData: any = {
updated_by: userId,
};
if (data.batch_name) updateData.batch_name = data.batch_name;
if (data.description !== undefined) updateData.description = data.description;
if (data.cron_schedule) updateData.cron_schedule = data.cron_schedule;
const batchConfig = await tx.batch_configs.update({
where: { id },
data: updateData,
});
// 매핑이 제공된 경우 기존 매핑 삭제 후 새로 생성
if (data.mappings) {
await tx.batch_mappings.deleteMany({
where: { batch_config_id: id },
});
const mappings = await Promise.all(
data.mappings.map((mapping, index) =>
tx.batch_mappings.create({
data: {
batch_config_id: id,
from_connection_type: mapping.from_connection_type,
from_connection_id: mapping.from_connection_id,
from_table_name: mapping.from_table_name,
from_column_name: mapping.from_column_name,
from_column_type: mapping.from_column_type,
to_connection_type: mapping.to_connection_type,
to_connection_id: mapping.to_connection_id,
to_table_name: mapping.to_table_name,
to_column_name: mapping.to_column_name,
to_column_type: mapping.to_column_type,
mapping_order: mapping.mapping_order || index + 1,
created_by: userId,
},
})
)
);
return {
...batchConfig,
batch_mappings: mappings,
};
} else {
return {
...batchConfig,
batch_mappings: existingConfig.batch_mappings,
};
}
});
return {
success: true,
data: result as BatchConfig,
message: "배치 설정이 성공적으로 수정되었습니다.",
};
} catch (error) {
console.error("배치 설정 수정 오류:", error);
return {
success: false,
message: "배치 설정 수정에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* 배치 설정 삭제 (논리 삭제)
*/
static async deleteBatchConfig(
id: number,
userId?: string
): Promise<ApiResponse<void>> {
try {
const existingConfig = await prisma.batch_configs.findUnique({
where: { id },
});
if (!existingConfig) {
return {
success: false,
message: "배치 설정을 찾을 수 없습니다.",
};
}
await prisma.batch_configs.update({
where: { id },
data: {
is_active: "N",
updated_by: userId,
},
});
return {
success: true,
message: "배치 설정이 성공적으로 삭제되었습니다.",
};
} catch (error) {
console.error("배치 설정 삭제 오류:", error);
return {
success: false,
message: "배치 설정 삭제에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* 사용 가능한 커넥션 목록 조회
*/
static async getAvailableConnections(): Promise<ApiResponse<ConnectionInfo[]>> {
try {
const connections: ConnectionInfo[] = [];
// 내부 DB 추가
connections.push({
type: 'internal',
name: 'Internal Database',
db_type: 'postgresql',
});
// 외부 DB 연결 조회
const externalConnections = await ExternalDbConnectionService.getConnections({
is_active: 'Y',
});
if (externalConnections.success && externalConnections.data) {
externalConnections.data.forEach((conn) => {
connections.push({
type: 'external',
id: conn.id,
name: conn.connection_name,
db_type: conn.db_type,
});
});
}
return {
success: true,
data: connections,
};
} catch (error) {
console.error("커넥션 목록 조회 오류:", error);
return {
success: false,
message: "커넥션 목록 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* 특정 커넥션의 테이블 목록 조회
*/
static async getTablesFromConnection(
connectionType: 'internal' | 'external',
connectionId?: number
): Promise<ApiResponse<string[]>> {
try {
let tables: string[] = [];
if (connectionType === 'internal') {
// 내부 DB 테이블 조회
const result = await prisma.$queryRaw<Array<{ table_name: string }>>`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name
`;
tables = result.map(row => row.table_name);
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 테이블 조회
const tablesResult = await ExternalDbConnectionService.getTables(connectionId);
if (tablesResult.success && tablesResult.data) {
tables = tablesResult.data;
}
}
return {
success: true,
data: tables,
};
} catch (error) {
console.error("테이블 목록 조회 오류:", error);
return {
success: false,
message: "테이블 목록 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* 특정 테이블의 컬럼 정보 조회
*/
static async getTableColumns(
connectionType: 'internal' | 'external',
tableName: string,
connectionId?: number
): Promise<ApiResponse<ColumnInfo[]>> {
try {
let columns: ColumnInfo[] = [];
if (connectionType === 'internal') {
// 내부 DB 컬럼 조회
const result = await prisma.$queryRaw<Array<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null;
}>>`
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${tableName}
ORDER BY ordinal_position
`;
columns = result.map(row => ({
column_name: row.column_name,
data_type: row.data_type,
is_nullable: row.is_nullable === 'YES',
column_default: row.column_default,
}));
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 컬럼 조회
const columnsResult = await ExternalDbConnectionService.getTableColumns(
connectionId,
tableName
);
if (columnsResult.success && columnsResult.data) {
columns = columnsResult.data.map(col => ({
column_name: col.column_name,
data_type: col.data_type,
is_nullable: col.is_nullable,
column_default: col.column_default,
}));
}
}
return {
success: true,
data: columns,
};
} catch (error) {
console.error("컬럼 정보 조회 오류:", error);
return {
success: false,
message: "컬럼 정보 조회에 실패했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* 배치 매핑 유효성 검사
*/
private static async validateBatchMappings(
mappings: BatchMapping[]
): Promise<BatchValidationResult> {
const errors: string[] = [];
const warnings: string[] = [];
if (!mappings || mappings.length === 0) {
errors.push("최소 하나 이상의 매핑이 필요합니다.");
return { isValid: false, errors, warnings };
}
// n:1 매핑 검사 (여러 FROM이 같은 TO로 매핑되는 것 방지)
const toMappings = new Map<string, number>();
mappings.forEach((mapping, index) => {
const toKey = `${mapping.to_connection_type}:${mapping.to_connection_id || 'internal'}:${mapping.to_table_name}:${mapping.to_column_name}`;
if (toMappings.has(toKey)) {
errors.push(
`매핑 ${index + 1}: TO 컬럼 '${mapping.to_table_name}.${mapping.to_column_name}'에 중복 매핑이 있습니다. n:1 매핑은 허용되지 않습니다.`
);
} else {
toMappings.set(toKey, index);
}
});
// 1:n 매핑 경고 (같은 FROM에서 여러 TO로 매핑)
const fromMappings = new Map<string, number[]>();
mappings.forEach((mapping, index) => {
const fromKey = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}:${mapping.from_column_name}`;
if (!fromMappings.has(fromKey)) {
fromMappings.set(fromKey, []);
}
fromMappings.get(fromKey)!.push(index);
});
fromMappings.forEach((indices, fromKey) => {
if (indices.length > 1) {
const [, , tableName, columnName] = fromKey.split(':');
warnings.push(
`FROM 컬럼 '${tableName}.${columnName}'에서 ${indices.length}개의 TO 컬럼으로 매핑됩니다. (1:n 매핑)`
);
}
});
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
}

View File

@@ -0,0 +1,85 @@
// 배치관리 타입 정의
// 작성일: 2024-12-24
export interface BatchConfig {
id?: number;
batch_name: string;
description?: string;
cron_schedule: string;
is_active?: string;
company_code?: string;
created_date?: Date;
created_by?: string;
updated_date?: Date;
updated_by?: string;
batch_mappings?: BatchMapping[];
}
export interface BatchMapping {
id?: number;
batch_config_id?: number;
// FROM 정보
from_connection_type: 'internal' | 'external';
from_connection_id?: number;
from_table_name: string;
from_column_name: string;
from_column_type?: string;
// TO 정보
to_connection_type: 'internal' | 'external';
to_connection_id?: number;
to_table_name: string;
to_column_name: string;
to_column_type?: string;
mapping_order?: number;
created_date?: Date;
created_by?: string;
}
export interface BatchConfigFilter {
batch_name?: string;
is_active?: string;
company_code?: string;
search?: string;
}
export interface ConnectionInfo {
type: 'internal' | 'external';
id?: number;
name: string;
db_type?: string;
}
export interface TableInfo {
table_name: string;
columns: ColumnInfo[];
}
export interface ColumnInfo {
column_name: string;
data_type: string;
is_nullable?: boolean;
column_default?: string;
}
export interface BatchMappingRequest {
batch_name: string;
description?: string;
cron_schedule: string;
mappings: BatchMapping[];
}
export interface BatchValidationResult {
isValid: boolean;
errors: string[];
warnings?: string[];
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
message?: string;
error?: string;
}