2025-09-24 18:23:57 +09:00
|
|
|
/**
|
|
|
|
|
* 다중 커넥션 관리 API 클라이언트
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { apiClient } from "./client";
|
|
|
|
|
|
2025-09-26 01:28:51 +09:00
|
|
|
/**
|
|
|
|
|
* 데이터 타입을 웹 타입으로 매핑하는 함수
|
|
|
|
|
* @param dataType - 데이터베이스 데이터 타입
|
|
|
|
|
* @returns 웹 타입 문자열
|
|
|
|
|
*/
|
|
|
|
|
const mapDataTypeToWebType = (dataType: string | undefined | null): string => {
|
|
|
|
|
if (!dataType || typeof dataType !== "string") {
|
|
|
|
|
console.warn(`⚠️ 잘못된 데이터 타입: ${dataType}, 기본값 'text' 사용`);
|
|
|
|
|
return "text";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lowerType = dataType.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// 텍스트 타입
|
|
|
|
|
if (lowerType.includes("varchar") || lowerType.includes("char") || lowerType.includes("text")) {
|
|
|
|
|
return "text";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 숫자 타입
|
|
|
|
|
if (lowerType.includes("int") || lowerType.includes("bigint") || lowerType.includes("smallint")) {
|
|
|
|
|
return "number";
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
lowerType.includes("decimal") ||
|
|
|
|
|
lowerType.includes("numeric") ||
|
|
|
|
|
lowerType.includes("float") ||
|
|
|
|
|
lowerType.includes("double")
|
|
|
|
|
) {
|
|
|
|
|
return "decimal";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 날짜/시간 타입
|
|
|
|
|
if (lowerType.includes("timestamp") || lowerType.includes("datetime")) {
|
|
|
|
|
return "datetime";
|
|
|
|
|
}
|
|
|
|
|
if (lowerType.includes("date")) {
|
|
|
|
|
return "date";
|
|
|
|
|
}
|
|
|
|
|
if (lowerType.includes("time")) {
|
|
|
|
|
return "time";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 불린 타입
|
|
|
|
|
if (lowerType.includes("boolean") || lowerType.includes("bit")) {
|
|
|
|
|
return "boolean";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 바이너리/파일 타입
|
|
|
|
|
if (lowerType.includes("bytea") || lowerType.includes("blob") || lowerType.includes("binary")) {
|
|
|
|
|
return "file";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JSON 타입
|
|
|
|
|
if (lowerType.includes("json")) {
|
|
|
|
|
return "text";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기본값
|
|
|
|
|
console.log(`🔍 알 수 없는 데이터 타입: ${dataType} → text로 매핑`);
|
|
|
|
|
return "text";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컬럼명으로부터 코드 카테고리를 추론
|
|
|
|
|
* 실제 존재하는 카테고리만 반환하도록 개선
|
|
|
|
|
*/
|
|
|
|
|
const inferCodeCategory = (columnName: string): string => {
|
|
|
|
|
const lowerName = columnName.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// 실제 데이터베이스에 존재하는 것으로 확인된 카테고리만 반환
|
|
|
|
|
if (lowerName.includes("status")) return "STATUS";
|
|
|
|
|
|
|
|
|
|
// 다른 카테고리들은 실제 존재 여부를 확인한 후 추가
|
|
|
|
|
// if (lowerName.includes("type")) return "TYPE";
|
|
|
|
|
// if (lowerName.includes("grade")) return "GRADE";
|
|
|
|
|
// if (lowerName.includes("level")) return "LEVEL";
|
|
|
|
|
// if (lowerName.includes("priority")) return "PRIORITY";
|
|
|
|
|
// if (lowerName.includes("category")) return "CATEGORY";
|
|
|
|
|
// if (lowerName.includes("role")) return "ROLE";
|
|
|
|
|
|
|
|
|
|
// 확인되지 않은 컬럼은 일단 STATUS로 매핑 (임시)
|
|
|
|
|
return "STATUS";
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-24 18:23:57 +09:00
|
|
|
export interface MultiConnectionTableInfo {
|
|
|
|
|
tableName: string;
|
|
|
|
|
displayName?: string;
|
|
|
|
|
columnCount: number;
|
|
|
|
|
connectionId: number;
|
|
|
|
|
connectionName: string;
|
|
|
|
|
dbType: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ColumnInfo {
|
|
|
|
|
columnName: string;
|
|
|
|
|
displayName: string;
|
|
|
|
|
dataType: string;
|
|
|
|
|
dbType: string;
|
|
|
|
|
webType: string;
|
|
|
|
|
isNullable: boolean;
|
|
|
|
|
isPrimaryKey: boolean;
|
|
|
|
|
defaultValue?: string;
|
|
|
|
|
maxLength?: number;
|
|
|
|
|
description?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ConnectionInfo {
|
|
|
|
|
id: number;
|
|
|
|
|
connection_name: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
db_type: string;
|
|
|
|
|
host: string;
|
|
|
|
|
port: number;
|
|
|
|
|
database_name: string;
|
|
|
|
|
username: string;
|
|
|
|
|
is_active: string;
|
|
|
|
|
company_code: string;
|
|
|
|
|
created_date: Date;
|
|
|
|
|
updated_date: Date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ValidationResult {
|
|
|
|
|
isValid: boolean;
|
|
|
|
|
error?: string;
|
|
|
|
|
warnings?: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 제어관리용 활성 커넥션 목록 조회 (메인 DB 포함)
|
|
|
|
|
*/
|
|
|
|
|
export const getActiveConnections = async (): Promise<ConnectionInfo[]> => {
|
|
|
|
|
const response = await apiClient.get("/external-db-connections/control/active");
|
|
|
|
|
return response.data.data || [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 커넥션의 테이블 목록 조회
|
|
|
|
|
*/
|
|
|
|
|
export const getTablesFromConnection = async (connectionId: number): Promise<MultiConnectionTableInfo[]> => {
|
|
|
|
|
const response = await apiClient.get(`/multi-connection/connections/${connectionId}/tables`);
|
|
|
|
|
return response.data.data || [];
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-26 01:28:51 +09:00
|
|
|
/**
|
|
|
|
|
* 특정 커넥션의 모든 테이블 정보 배치 조회 (컬럼 수 포함)
|
|
|
|
|
*/
|
|
|
|
|
export const getBatchTablesWithColumns = async (
|
|
|
|
|
connectionId: number,
|
|
|
|
|
): Promise<{ tableName: string; displayName?: string; columnCount: number }[]> => {
|
|
|
|
|
console.log(`🚀 getBatchTablesWithColumns 호출: connectionId=${connectionId}`);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await apiClient.get(`/multi-connection/connections/${connectionId}/tables/batch`);
|
|
|
|
|
console.log("✅ 배치 테이블 정보 조회 성공:", response.data);
|
|
|
|
|
|
|
|
|
|
const result = response.data.data || [];
|
|
|
|
|
console.log(`📊 배치 조회 결과: ${result.length}개 테이블`, result);
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ 배치 테이블 정보 조회 실패:", error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-24 18:23:57 +09:00
|
|
|
/**
|
|
|
|
|
* 특정 커넥션의 테이블 컬럼 정보 조회
|
|
|
|
|
*/
|
|
|
|
|
export const getColumnsFromConnection = async (connectionId: number, tableName: string): Promise<ColumnInfo[]> => {
|
2025-09-26 01:28:51 +09:00
|
|
|
console.log(`🔍 getColumnsFromConnection 호출: connectionId=${connectionId}, tableName=${tableName}`);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 메인 데이터베이스(connectionId = 0)인 경우 기존 API 사용
|
|
|
|
|
if (connectionId === 0) {
|
|
|
|
|
console.log("📡 메인 DB API 호출:", `/table-management/tables/${tableName}/columns`);
|
|
|
|
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
|
|
|
|
console.log("✅ 메인 DB 응답:", response.data);
|
|
|
|
|
|
|
|
|
|
const rawResult = response.data.data || [];
|
|
|
|
|
|
|
|
|
|
// 메인 DB는 페이지네이션 구조로 반환됨: {columns: [], total, page, size, totalPages}
|
|
|
|
|
const columns = rawResult.columns || rawResult;
|
|
|
|
|
|
|
|
|
|
// 메인 DB 컬럼에도 코드 타입 감지 로직 적용
|
|
|
|
|
const result = Array.isArray(columns)
|
|
|
|
|
? columns.map((col: any) => {
|
|
|
|
|
const columnName = col.columnName || "";
|
|
|
|
|
|
|
|
|
|
// 컬럼명으로 코드 타입 감지
|
|
|
|
|
const isCodeColumn =
|
|
|
|
|
columnName.toLowerCase().includes("code") ||
|
|
|
|
|
columnName.toLowerCase().includes("status") ||
|
|
|
|
|
columnName.toLowerCase().includes("type") ||
|
|
|
|
|
columnName.toLowerCase().includes("grade") ||
|
|
|
|
|
columnName.toLowerCase().includes("level");
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...col,
|
|
|
|
|
webType: isCodeColumn ? "code" : col.webType || mapDataTypeToWebType(col.dataType),
|
|
|
|
|
codeCategory: isCodeColumn ? inferCodeCategory(columnName) : col.codeCategory,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
: columns;
|
|
|
|
|
|
|
|
|
|
console.log("📊 메인 DB 최종 결과:", {
|
|
|
|
|
rawType: typeof rawResult,
|
|
|
|
|
rawIsArray: Array.isArray(rawResult),
|
|
|
|
|
hasColumns: rawResult && typeof rawResult === "object" && "columns" in rawResult,
|
|
|
|
|
finalType: typeof result,
|
|
|
|
|
finalIsArray: Array.isArray(result),
|
|
|
|
|
length: Array.isArray(result) ? result.length : "N/A",
|
|
|
|
|
sample: Array.isArray(result) ? result.slice(0, 1) : result,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 외부 커넥션인 경우 external-db-connections API 사용
|
|
|
|
|
console.log("📡 외부 DB API 호출:", `/external-db-connections/${connectionId}/tables/${tableName}/columns`);
|
|
|
|
|
const response = await apiClient.get(`/external-db-connections/${connectionId}/tables/${tableName}/columns`);
|
|
|
|
|
console.log("✅ 외부 DB 응답:", response.data);
|
|
|
|
|
|
|
|
|
|
const rawResult = response.data.data || [];
|
|
|
|
|
|
|
|
|
|
// 외부 DB 컬럼 구조를 메인 DB 형식으로 변환
|
|
|
|
|
const result = Array.isArray(rawResult)
|
|
|
|
|
? rawResult.map((col: any) => {
|
|
|
|
|
const columnName = col.column_name || col.columnName || "";
|
|
|
|
|
const dataType = col.data_type || col.dataType || "unknown";
|
|
|
|
|
|
|
|
|
|
// 컬럼명이 '_code'로 끝나거나 'status', 'type' 등의 이름을 가진 경우 코드 타입으로 간주
|
|
|
|
|
const isCodeColumn =
|
|
|
|
|
columnName.toLowerCase().includes("code") ||
|
|
|
|
|
columnName.toLowerCase().includes("status") ||
|
|
|
|
|
columnName.toLowerCase().includes("type") ||
|
|
|
|
|
columnName.toLowerCase().includes("grade") ||
|
|
|
|
|
columnName.toLowerCase().includes("level");
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
columnName: columnName,
|
|
|
|
|
displayName: col.column_comment || col.displayName || columnName,
|
|
|
|
|
dataType: dataType,
|
|
|
|
|
dbType: dataType,
|
|
|
|
|
webType: isCodeColumn ? "code" : mapDataTypeToWebType(dataType),
|
|
|
|
|
isNullable: col.is_nullable === "YES" || col.isNullable === true,
|
|
|
|
|
columnDefault: col.column_default || col.columnDefault,
|
|
|
|
|
description: col.column_comment || col.description,
|
|
|
|
|
// 코드 타입인 경우 카테고리 추론
|
|
|
|
|
codeCategory: isCodeColumn ? inferCodeCategory(columnName) : undefined,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
: rawResult;
|
|
|
|
|
|
|
|
|
|
console.log("📊 외부 DB 최종 결과:", {
|
|
|
|
|
rawType: typeof rawResult,
|
|
|
|
|
rawIsArray: Array.isArray(rawResult),
|
|
|
|
|
finalType: typeof result,
|
|
|
|
|
finalIsArray: Array.isArray(result),
|
|
|
|
|
length: Array.isArray(result) ? result.length : "N/A",
|
|
|
|
|
sample: Array.isArray(result) ? result.slice(0, 1) : result,
|
|
|
|
|
sampleOriginal: Array.isArray(rawResult) ? rawResult.slice(0, 1) : rawResult,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ 컬럼 정보 조회 실패:", error);
|
|
|
|
|
|
|
|
|
|
// 개발 환경에서 Mock 데이터 반환
|
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
|
|
|
console.warn("🔄 개발 환경: Mock 컬럼 데이터 사용");
|
|
|
|
|
const mockResult = getMockColumnsForTable(tableName);
|
|
|
|
|
console.log("📊 Mock 데이터 반환:", {
|
|
|
|
|
type: typeof mockResult,
|
|
|
|
|
isArray: Array.isArray(mockResult),
|
|
|
|
|
length: mockResult.length,
|
|
|
|
|
sample: mockResult.slice(0, 1),
|
|
|
|
|
});
|
|
|
|
|
return mockResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mock 컬럼 데이터 (개발/테스트용)
|
|
|
|
|
*/
|
|
|
|
|
const getMockColumnsForTable = (tableName: string): ColumnInfo[] => {
|
|
|
|
|
const baseColumns: ColumnInfo[] = [
|
|
|
|
|
{
|
|
|
|
|
columnName: "id",
|
|
|
|
|
displayName: "ID",
|
|
|
|
|
dataType: "NUMBER",
|
|
|
|
|
webType: "number",
|
|
|
|
|
isNullable: false,
|
|
|
|
|
isPrimaryKey: true,
|
|
|
|
|
columnComment: "고유 식별자",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
columnName: "name",
|
|
|
|
|
displayName: "이름",
|
|
|
|
|
dataType: "VARCHAR",
|
|
|
|
|
webType: "text",
|
|
|
|
|
isNullable: false,
|
|
|
|
|
isPrimaryKey: false,
|
|
|
|
|
columnComment: "이름",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
columnName: "status",
|
|
|
|
|
displayName: "상태",
|
|
|
|
|
dataType: "VARCHAR",
|
|
|
|
|
webType: "code",
|
|
|
|
|
isNullable: true,
|
|
|
|
|
isPrimaryKey: false,
|
|
|
|
|
columnComment: "상태 코드",
|
|
|
|
|
codeCategory: "STATUS",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
columnName: "created_date",
|
|
|
|
|
displayName: "생성일시",
|
|
|
|
|
dataType: "TIMESTAMP",
|
|
|
|
|
webType: "datetime",
|
|
|
|
|
isNullable: true,
|
|
|
|
|
isPrimaryKey: false,
|
|
|
|
|
columnComment: "생성일시",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
columnName: "updated_date",
|
|
|
|
|
displayName: "수정일시",
|
|
|
|
|
dataType: "TIMESTAMP",
|
|
|
|
|
webType: "datetime",
|
|
|
|
|
isNullable: true,
|
|
|
|
|
isPrimaryKey: false,
|
|
|
|
|
columnComment: "수정일시",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 테이블명에 따라 추가 컬럼 포함
|
|
|
|
|
if (tableName.toLowerCase().includes("user")) {
|
|
|
|
|
baseColumns.push({
|
|
|
|
|
columnName: "email",
|
|
|
|
|
displayName: "이메일",
|
|
|
|
|
dataType: "VARCHAR",
|
|
|
|
|
webType: "email",
|
|
|
|
|
isNullable: true,
|
|
|
|
|
isPrimaryKey: false,
|
|
|
|
|
columnComment: "이메일 주소",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (tableName.toLowerCase().includes("product")) {
|
|
|
|
|
baseColumns.push({
|
|
|
|
|
columnName: "price",
|
|
|
|
|
displayName: "가격",
|
|
|
|
|
dataType: "DECIMAL",
|
|
|
|
|
webType: "decimal",
|
|
|
|
|
isNullable: true,
|
|
|
|
|
isPrimaryKey: false,
|
|
|
|
|
columnComment: "상품 가격",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return baseColumns;
|
2025-09-24 18:23:57 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 커넥션에서 데이터 조회
|
|
|
|
|
*/
|
|
|
|
|
export const queryDataFromConnection = async (
|
|
|
|
|
connectionId: number,
|
|
|
|
|
tableName: string,
|
|
|
|
|
conditions?: Record<string, any>,
|
|
|
|
|
): Promise<Record<string, any>[]> => {
|
|
|
|
|
const response = await apiClient.post(`/multi-connection/connections/${connectionId}/query`, {
|
|
|
|
|
tableName,
|
|
|
|
|
conditions,
|
|
|
|
|
});
|
|
|
|
|
return response.data.data || [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 커넥션에 데이터 삽입
|
|
|
|
|
*/
|
|
|
|
|
export const insertDataToConnection = async (
|
|
|
|
|
connectionId: number,
|
|
|
|
|
tableName: string,
|
|
|
|
|
data: Record<string, any>,
|
|
|
|
|
): Promise<any> => {
|
|
|
|
|
const response = await apiClient.post(`/multi-connection/connections/${connectionId}/insert`, {
|
|
|
|
|
tableName,
|
|
|
|
|
data,
|
|
|
|
|
});
|
|
|
|
|
return response.data.data || {};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 커넥션의 데이터 업데이트
|
|
|
|
|
*/
|
|
|
|
|
export const updateDataInConnection = async (
|
|
|
|
|
connectionId: number,
|
|
|
|
|
tableName: string,
|
|
|
|
|
data: Record<string, any>,
|
|
|
|
|
conditions: Record<string, any>,
|
|
|
|
|
): Promise<any> => {
|
|
|
|
|
const response = await apiClient.put(`/multi-connection/connections/${connectionId}/update`, {
|
|
|
|
|
tableName,
|
|
|
|
|
data,
|
|
|
|
|
conditions,
|
|
|
|
|
});
|
|
|
|
|
return response.data.data || {};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 특정 커넥션에서 데이터 삭제
|
|
|
|
|
*/
|
|
|
|
|
export const deleteDataFromConnection = async (
|
|
|
|
|
connectionId: number,
|
|
|
|
|
tableName: string,
|
|
|
|
|
conditions: Record<string, any>,
|
|
|
|
|
maxDeleteCount?: number,
|
|
|
|
|
): Promise<any> => {
|
|
|
|
|
const response = await apiClient.delete(`/multi-connection/connections/${connectionId}/delete`, {
|
|
|
|
|
data: {
|
|
|
|
|
tableName,
|
|
|
|
|
conditions,
|
|
|
|
|
maxDeleteCount,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return response.data.data || {};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 자기 자신 테이블 작업 검증
|
|
|
|
|
*/
|
|
|
|
|
export const validateSelfTableOperation = async (
|
|
|
|
|
tableName: string,
|
|
|
|
|
operation: "update" | "delete",
|
|
|
|
|
conditions: any[],
|
|
|
|
|
): Promise<ValidationResult> => {
|
|
|
|
|
const response = await apiClient.post("/multi-connection/validate-self-operation", {
|
|
|
|
|
tableName,
|
|
|
|
|
operation,
|
|
|
|
|
conditions,
|
|
|
|
|
});
|
|
|
|
|
return response.data.data || {};
|
|
|
|
|
};
|