dev #46
|
|
@ -1,59 +0,0 @@
|
|||
const { PrismaClient } = require("@prisma/client");
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function checkDatabase() {
|
||||
try {
|
||||
console.log("=== 데이터베이스 연결 확인 ===");
|
||||
const userCount = await prisma.user_info.count();
|
||||
console.log(`총 사용자 수: ${userCount}`);
|
||||
|
||||
if (userCount > 0) {
|
||||
const users = await prisma.user_info.findMany({
|
||||
take: 10,
|
||||
select: {
|
||||
user_id: true,
|
||||
user_name: true,
|
||||
dept_name: true,
|
||||
company_code: true,
|
||||
},
|
||||
});
|
||||
console.log("\n=== 사용자 목록 (대소문자 확인) ===");
|
||||
users.forEach((user, index) => {
|
||||
console.log(
|
||||
`${index + 1}. "${user.user_id}" - ${user.user_name || "이름 없음"} (${user.dept_name || "부서 없음"})`
|
||||
);
|
||||
});
|
||||
|
||||
console.log("\n=== 특정 사용자 검색 테스트 ===");
|
||||
const userLower = await prisma.user_info.findUnique({
|
||||
where: { user_id: "arvin" },
|
||||
});
|
||||
console.log('소문자 "arvin" 검색 결과:', userLower ? "찾음" : "없음");
|
||||
const userUpper = await prisma.user_info.findUnique({
|
||||
where: { user_id: "ARVIN" },
|
||||
});
|
||||
console.log('대문자 "ARVIN" 검색 결과:', userUpper ? "찾음" : "없음");
|
||||
|
||||
const rawUsers = await prisma.$queryRaw`
|
||||
SELECT user_id, user_name, dept_name
|
||||
FROM user_info
|
||||
WHERE user_id IN ('arvin', 'ARVIN', 'Arvin')
|
||||
LIMIT 5
|
||||
`;
|
||||
console.log("\n=== 원본 데이터 확인 ===");
|
||||
rawUsers.forEach((user) => {
|
||||
console.log(`"${user.user_id}" - ${user.user_name || "이름 없음"}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 로그인 로그 확인
|
||||
const logCount = await prisma.login_access_log.count();
|
||||
console.log(`\n총 로그인 로그 수: ${logCount}`);
|
||||
} catch (error) {
|
||||
console.error("오류 발생:", error);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
checkDatabase();
|
||||
|
|
@ -39,6 +39,35 @@ model external_call_configs {
|
|||
@@index([is_active])
|
||||
}
|
||||
|
||||
model external_db_connections {
|
||||
id Int @id @default(autoincrement())
|
||||
connection_name String @db.VarChar(100)
|
||||
description String? @db.Text
|
||||
db_type String @db.VarChar(20)
|
||||
host String @db.VarChar(255)
|
||||
port Int
|
||||
database_name String @db.VarChar(100)
|
||||
username String @db.VarChar(100)
|
||||
password String @db.Text
|
||||
connection_timeout Int? @default(30)
|
||||
query_timeout Int? @default(60)
|
||||
max_connections Int? @default(10)
|
||||
ssl_enabled String @default("N") @db.Char(1)
|
||||
ssl_cert_path String? @db.VarChar(500)
|
||||
connection_options Json?
|
||||
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([company_code])
|
||||
@@index([is_active])
|
||||
@@index([db_type])
|
||||
@@index([connection_name])
|
||||
}
|
||||
|
||||
model admin_supply_mng {
|
||||
objid Decimal @id @default(0) @db.Decimal
|
||||
supply_code String? @default("NULL::character varying") @db.VarChar(100)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import layoutRoutes from "./routes/layoutRoutes";
|
|||
import dataRoutes from "./routes/dataRoutes";
|
||||
import externalCallRoutes from "./routes/externalCallRoutes";
|
||||
import externalCallConfigRoutes from "./routes/externalCallConfigRoutes";
|
||||
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
// import menuRoutes from './routes/menuRoutes';
|
||||
|
||||
|
|
@ -123,6 +124,7 @@ app.use("/api/screen", screenStandardRoutes);
|
|||
app.use("/api/data", dataRoutes);
|
||||
app.use("/api/external-calls", externalCallRoutes);
|
||||
app.use("/api/external-call-configs", externalCallConfigRoutes);
|
||||
app.use("/api/external-db-connections", externalDbConnectionRoutes);
|
||||
// app.use('/api/users', userRoutes);
|
||||
// app.use('/api/menus', menuRoutes);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,242 @@
|
|||
// 외부 DB 연결 API 라우트
|
||||
// 작성일: 2024-12-17
|
||||
|
||||
import { Router, Response } from "express";
|
||||
import { ExternalDbConnectionService } from "../services/externalDbConnectionService";
|
||||
import {
|
||||
ExternalDbConnection,
|
||||
ExternalDbConnectionFilter,
|
||||
} from "../types/externalDbTypes";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/external-db-connections
|
||||
* 외부 DB 연결 목록 조회
|
||||
*/
|
||||
router.get(
|
||||
"/",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const filter: ExternalDbConnectionFilter = {
|
||||
db_type: req.query.db_type as string,
|
||||
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 ExternalDbConnectionFilter]) {
|
||||
delete filter[key as keyof ExternalDbConnectionFilter];
|
||||
}
|
||||
});
|
||||
|
||||
const result = await ExternalDbConnectionService.getConnections(filter);
|
||||
|
||||
if (result.success) {
|
||||
return res.status(200).json(result);
|
||||
} else {
|
||||
return res.status(400).json(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 목록 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/external-db-connections/:id
|
||||
* 특정 외부 DB 연결 조회
|
||||
*/
|
||||
router.get(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
async (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 ExternalDbConnectionService.getConnectionById(id);
|
||||
|
||||
if (result.success) {
|
||||
return res.status(200).json(result);
|
||||
} else {
|
||||
return res.status(404).json(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/external-db-connections
|
||||
* 새 외부 DB 연결 생성
|
||||
*/
|
||||
router.post(
|
||||
"/",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const connectionData: ExternalDbConnection = req.body;
|
||||
|
||||
// 사용자 정보 추가
|
||||
if (req.user) {
|
||||
connectionData.created_by = req.user.userId;
|
||||
connectionData.updated_by = req.user.userId;
|
||||
}
|
||||
|
||||
const result =
|
||||
await ExternalDbConnectionService.createConnection(connectionData);
|
||||
|
||||
if (result.success) {
|
||||
return res.status(201).json(result);
|
||||
} else {
|
||||
return res.status(400).json(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 생성 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* PUT /api/external-db-connections/:id
|
||||
* 외부 DB 연결 수정
|
||||
*/
|
||||
router.put(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const updateData: Partial<ExternalDbConnection> = req.body;
|
||||
|
||||
// 사용자 정보 추가
|
||||
if (req.user) {
|
||||
updateData.updated_by = req.user.userId;
|
||||
}
|
||||
|
||||
const result = await ExternalDbConnectionService.updateConnection(
|
||||
id,
|
||||
updateData
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
return res.status(200).json(result);
|
||||
} else {
|
||||
return res.status(400).json(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 수정 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/external-db-connections/:id
|
||||
* 외부 DB 연결 삭제 (논리 삭제)
|
||||
*/
|
||||
router.delete(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
async (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 ExternalDbConnectionService.deleteConnection(id);
|
||||
|
||||
if (result.success) {
|
||||
return res.status(200).json(result);
|
||||
} else {
|
||||
return res.status(404).json(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 삭제 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/external-db-connections/types/supported
|
||||
* 지원하는 DB 타입 목록 조회
|
||||
*/
|
||||
router.get(
|
||||
"/types/supported",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { DB_TYPE_OPTIONS, DB_TYPE_DEFAULTS } = await import(
|
||||
"../types/externalDbTypes"
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
types: DB_TYPE_OPTIONS,
|
||||
defaults: DB_TYPE_DEFAULTS,
|
||||
},
|
||||
message: "지원하는 DB 타입 목록을 조회했습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("DB 타입 목록 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
// 외부 DB 연결 서비스
|
||||
// 작성일: 2024-12-17
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import {
|
||||
ExternalDbConnection,
|
||||
ExternalDbConnectionFilter,
|
||||
ApiResponse,
|
||||
} from "../types/externalDbTypes";
|
||||
import { PasswordEncryption } from "../utils/passwordEncryption";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class ExternalDbConnectionService {
|
||||
/**
|
||||
* 외부 DB 연결 목록 조회
|
||||
*/
|
||||
static async getConnections(
|
||||
filter: ExternalDbConnectionFilter
|
||||
): Promise<ApiResponse<ExternalDbConnection[]>> {
|
||||
try {
|
||||
const where: any = {};
|
||||
|
||||
// 필터 조건 적용
|
||||
if (filter.db_type) {
|
||||
where.db_type = filter.db_type;
|
||||
}
|
||||
|
||||
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 = [
|
||||
{
|
||||
connection_name: {
|
||||
contains: filter.search.trim(),
|
||||
mode: "insensitive",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: {
|
||||
contains: filter.search.trim(),
|
||||
mode: "insensitive",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const connections = await prisma.external_db_connections.findMany({
|
||||
where,
|
||||
orderBy: [{ is_active: "desc" }, { connection_name: "asc" }],
|
||||
});
|
||||
|
||||
// 비밀번호는 반환하지 않음 (보안)
|
||||
const safeConnections = connections.map((conn) => ({
|
||||
...conn,
|
||||
password: "***ENCRYPTED***", // 실제 비밀번호 대신 마스킹
|
||||
description: conn.description || undefined,
|
||||
})) as ExternalDbConnection[];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: safeConnections,
|
||||
message: `${connections.length}개의 연결 설정을 조회했습니다.`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 목록 조회 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 외부 DB 연결 조회
|
||||
*/
|
||||
static async getConnectionById(
|
||||
id: number
|
||||
): Promise<ApiResponse<ExternalDbConnection>> {
|
||||
try {
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
message: "해당 연결 설정을 찾을 수 없습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 비밀번호는 반환하지 않음 (보안)
|
||||
const safeConnection = {
|
||||
...connection,
|
||||
password: "***ENCRYPTED***",
|
||||
description: connection.description || undefined,
|
||||
} as ExternalDbConnection;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: safeConnection,
|
||||
message: "연결 설정을 조회했습니다.",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 조회 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 외부 DB 연결 생성
|
||||
*/
|
||||
static async createConnection(
|
||||
data: ExternalDbConnection
|
||||
): Promise<ApiResponse<ExternalDbConnection>> {
|
||||
try {
|
||||
// 데이터 검증
|
||||
this.validateConnectionData(data);
|
||||
|
||||
// 연결명 중복 확인
|
||||
const existingConnection = await prisma.external_db_connections.findFirst(
|
||||
{
|
||||
where: {
|
||||
connection_name: data.connection_name,
|
||||
company_code: data.company_code,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (existingConnection) {
|
||||
return {
|
||||
success: false,
|
||||
message: "이미 존재하는 연결명입니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 비밀번호 암호화
|
||||
const encryptedPassword = PasswordEncryption.encrypt(data.password);
|
||||
|
||||
const newConnection = await prisma.external_db_connections.create({
|
||||
data: {
|
||||
connection_name: data.connection_name,
|
||||
description: data.description,
|
||||
db_type: data.db_type,
|
||||
host: data.host,
|
||||
port: data.port,
|
||||
database_name: data.database_name,
|
||||
username: data.username,
|
||||
password: encryptedPassword,
|
||||
connection_timeout: data.connection_timeout,
|
||||
query_timeout: data.query_timeout,
|
||||
max_connections: data.max_connections,
|
||||
ssl_enabled: data.ssl_enabled,
|
||||
ssl_cert_path: data.ssl_cert_path,
|
||||
connection_options: data.connection_options as any,
|
||||
company_code: data.company_code,
|
||||
is_active: data.is_active,
|
||||
created_by: data.created_by,
|
||||
updated_by: data.updated_by,
|
||||
created_date: new Date(),
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// 비밀번호는 반환하지 않음
|
||||
const safeConnection = {
|
||||
...newConnection,
|
||||
password: "***ENCRYPTED***",
|
||||
description: newConnection.description || undefined,
|
||||
} as ExternalDbConnection;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: safeConnection,
|
||||
message: "연결 설정이 생성되었습니다.",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 생성 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 생성 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 수정
|
||||
*/
|
||||
static async updateConnection(
|
||||
id: number,
|
||||
data: Partial<ExternalDbConnection>
|
||||
): Promise<ApiResponse<ExternalDbConnection>> {
|
||||
try {
|
||||
// 기존 연결 확인
|
||||
const existingConnection =
|
||||
await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!existingConnection) {
|
||||
return {
|
||||
success: false,
|
||||
message: "해당 연결 설정을 찾을 수 없습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 연결명 중복 확인 (자신 제외)
|
||||
if (data.connection_name) {
|
||||
const duplicateConnection =
|
||||
await prisma.external_db_connections.findFirst({
|
||||
where: {
|
||||
connection_name: data.connection_name,
|
||||
company_code:
|
||||
data.company_code || existingConnection.company_code,
|
||||
id: { not: id },
|
||||
},
|
||||
});
|
||||
|
||||
if (duplicateConnection) {
|
||||
return {
|
||||
success: false,
|
||||
message: "이미 존재하는 연결명입니다.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 업데이트 데이터 준비
|
||||
const updateData: any = {
|
||||
...data,
|
||||
updated_date: new Date(),
|
||||
};
|
||||
|
||||
// 비밀번호가 변경된 경우 암호화
|
||||
if (data.password && data.password !== "***ENCRYPTED***") {
|
||||
updateData.password = PasswordEncryption.encrypt(data.password);
|
||||
} else {
|
||||
// 비밀번호 필드 제거 (변경하지 않음)
|
||||
delete updateData.password;
|
||||
}
|
||||
|
||||
const updatedConnection = await prisma.external_db_connections.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
// 비밀번호는 반환하지 않음
|
||||
const safeConnection = {
|
||||
...updatedConnection,
|
||||
password: "***ENCRYPTED***",
|
||||
description: updatedConnection.description || undefined,
|
||||
} as ExternalDbConnection;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: safeConnection,
|
||||
message: "연결 설정이 수정되었습니다.",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 수정 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 수정 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 삭제 (논리 삭제)
|
||||
*/
|
||||
static async deleteConnection(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const existingConnection =
|
||||
await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!existingConnection) {
|
||||
return {
|
||||
success: false,
|
||||
message: "해당 연결 설정을 찾을 수 없습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 논리 삭제 (is_active를 'N'으로 변경)
|
||||
await prisma.external_db_connections.update({
|
||||
where: { id },
|
||||
data: {
|
||||
is_active: "N",
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "연결 설정이 삭제되었습니다.",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 삭제 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 삭제 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 연결 데이터 검증
|
||||
*/
|
||||
private static validateConnectionData(data: ExternalDbConnection): void {
|
||||
const requiredFields = [
|
||||
"connection_name",
|
||||
"db_type",
|
||||
"host",
|
||||
"port",
|
||||
"database_name",
|
||||
"username",
|
||||
"password",
|
||||
"company_code",
|
||||
];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!data[field as keyof ExternalDbConnection]) {
|
||||
throw new Error(`필수 필드가 누락되었습니다: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 포트 번호 유효성 검사
|
||||
if (data.port < 1 || data.port > 65535) {
|
||||
throw new Error("유효하지 않은 포트 번호입니다. (1-65535)");
|
||||
}
|
||||
|
||||
// DB 타입 유효성 검사
|
||||
const validDbTypes = ["mysql", "postgresql", "oracle", "mssql", "sqlite"];
|
||||
if (!validDbTypes.includes(data.db_type)) {
|
||||
throw new Error("지원하지 않는 DB 타입입니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장된 연결의 실제 비밀번호 조회 (내부용)
|
||||
*/
|
||||
static async getDecryptedPassword(id: number): Promise<string | null> {
|
||||
try {
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
select: { password: true },
|
||||
});
|
||||
|
||||
if (!connection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PasswordEncryption.decrypt(connection.password);
|
||||
} catch (error) {
|
||||
console.error("비밀번호 복호화 실패:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
// 외부 DB 연결 관련 타입 정의
|
||||
// 작성일: 2024-12-17
|
||||
|
||||
export interface ExternalDbConnection {
|
||||
id?: number;
|
||||
connection_name: string;
|
||||
description?: string;
|
||||
db_type: "mysql" | "postgresql" | "oracle" | "mssql" | "sqlite";
|
||||
host: string;
|
||||
port: number;
|
||||
database_name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
connection_timeout?: number;
|
||||
query_timeout?: number;
|
||||
max_connections?: number;
|
||||
ssl_enabled?: string;
|
||||
ssl_cert_path?: string;
|
||||
connection_options?: Record<string, unknown>;
|
||||
company_code: string;
|
||||
is_active: string;
|
||||
created_date?: Date;
|
||||
created_by?: string;
|
||||
updated_date?: Date;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface ExternalDbConnectionFilter {
|
||||
db_type?: string;
|
||||
is_active?: string;
|
||||
company_code?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// DB 타입 옵션
|
||||
export const DB_TYPE_OPTIONS = [
|
||||
{ value: "mysql", label: "MySQL" },
|
||||
{ value: "postgresql", label: "PostgreSQL" },
|
||||
{ value: "oracle", label: "Oracle" },
|
||||
{ value: "mssql", label: "SQL Server" },
|
||||
{ value: "sqlite", label: "SQLite" },
|
||||
];
|
||||
|
||||
// DB 타입별 기본 설정
|
||||
export const DB_TYPE_DEFAULTS = {
|
||||
mysql: { port: 3306, driver: "mysql2" },
|
||||
postgresql: { port: 5432, driver: "pg" },
|
||||
oracle: { port: 1521, driver: "oracledb" },
|
||||
mssql: { port: 1433, driver: "mssql" },
|
||||
sqlite: { port: 0, driver: "sqlite3" },
|
||||
};
|
||||
|
||||
// 활성 상태 옵션
|
||||
export const ACTIVE_STATUS_OPTIONS = [
|
||||
{ value: "Y", label: "활성" },
|
||||
{ value: "N", label: "비활성" },
|
||||
{ value: "", label: "전체" },
|
||||
];
|
||||
|
||||
// 연결 옵션 스키마 (각 DB 타입별 추가 옵션)
|
||||
export interface MySQLConnectionOptions {
|
||||
charset?: string;
|
||||
timezone?: string;
|
||||
connectTimeout?: number;
|
||||
acquireTimeout?: number;
|
||||
multipleStatements?: boolean;
|
||||
}
|
||||
|
||||
export interface PostgreSQLConnectionOptions {
|
||||
schema?: string;
|
||||
ssl?: boolean | object;
|
||||
application_name?: string;
|
||||
statement_timeout?: number;
|
||||
}
|
||||
|
||||
export interface OracleConnectionOptions {
|
||||
serviceName?: string;
|
||||
sid?: string;
|
||||
connectString?: string;
|
||||
poolMin?: number;
|
||||
poolMax?: number;
|
||||
}
|
||||
|
||||
export interface SQLServerConnectionOptions {
|
||||
encrypt?: boolean;
|
||||
trustServerCertificate?: boolean;
|
||||
requestTimeout?: number;
|
||||
connectionTimeout?: number;
|
||||
}
|
||||
|
||||
export interface SQLiteConnectionOptions {
|
||||
mode?: string;
|
||||
cache?: string;
|
||||
foreign_keys?: boolean;
|
||||
}
|
||||
|
||||
export type SupportedConnectionOptions =
|
||||
| MySQLConnectionOptions
|
||||
| PostgreSQLConnectionOptions
|
||||
| OracleConnectionOptions
|
||||
| SQLServerConnectionOptions
|
||||
| SQLiteConnectionOptions;
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
// 비밀번호 암호화/복호화 유틸리티
|
||||
// 작성일: 2024-12-17
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
export class PasswordEncryption {
|
||||
private static readonly ALGORITHM = "aes-256-cbc";
|
||||
private static readonly SECRET_KEY =
|
||||
process.env.DB_PASSWORD_SECRET ||
|
||||
"default-fallback-key-change-in-production";
|
||||
private static readonly IV_LENGTH = 16; // AES-CBC의 경우 16바이트
|
||||
|
||||
/**
|
||||
* 비밀번호를 암호화합니다.
|
||||
* @param password 암호화할 평문 비밀번호
|
||||
* @returns 암호화된 비밀번호 (base64 인코딩)
|
||||
*/
|
||||
static encrypt(password: string): string {
|
||||
try {
|
||||
// 랜덤 IV 생성
|
||||
const iv = crypto.randomBytes(this.IV_LENGTH);
|
||||
|
||||
// 암호화 키 생성 (SECRET_KEY를 해시하여 32바이트 키 생성)
|
||||
const key = crypto.scryptSync(this.SECRET_KEY, "salt", 32);
|
||||
|
||||
// 암호화 객체 생성
|
||||
const cipher = crypto.createCipher("aes-256-cbc", key);
|
||||
|
||||
// 암호화 실행
|
||||
let encrypted = cipher.update(password, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
// IV와 암호화된 데이터를 결합하여 반환
|
||||
return `${iv.toString("hex")}:${encrypted}`;
|
||||
} catch (error) {
|
||||
console.error("Password encryption failed:", error);
|
||||
throw new Error("비밀번호 암호화에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 암호화된 비밀번호를 복호화합니다.
|
||||
* @param encryptedPassword 암호화된 비밀번호
|
||||
* @returns 복호화된 평문 비밀번호
|
||||
*/
|
||||
static decrypt(encryptedPassword: string): string {
|
||||
try {
|
||||
// IV와 암호화된 데이터 분리
|
||||
const parts = encryptedPassword.split(":");
|
||||
if (parts.length !== 2) {
|
||||
throw new Error("잘못된 암호화된 비밀번호 형식입니다.");
|
||||
}
|
||||
|
||||
const iv = Buffer.from(parts[0], "hex");
|
||||
const encrypted = parts[1];
|
||||
|
||||
// 암호화 키 생성 (암호화 시와 동일)
|
||||
const key = crypto.scryptSync(this.SECRET_KEY, "salt", 32);
|
||||
|
||||
// 복호화 객체 생성
|
||||
const decipher = crypto.createDecipher("aes-256-cbc", key);
|
||||
|
||||
// 복호화 실행
|
||||
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return decrypted;
|
||||
} catch (error) {
|
||||
console.error("Password decryption failed:", error);
|
||||
throw new Error("비밀번호 복호화에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 암호화 키가 설정되어 있는지 확인합니다.
|
||||
* @returns 키 설정 여부
|
||||
*/
|
||||
static isKeyConfigured(): boolean {
|
||||
return (
|
||||
process.env.DB_PASSWORD_SECRET !== undefined &&
|
||||
process.env.DB_PASSWORD_SECRET !== ""
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 암호화/복호화 기능을 테스트합니다.
|
||||
* @returns 테스트 결과
|
||||
*/
|
||||
static testEncryption(): { success: boolean; message: string } {
|
||||
try {
|
||||
const testPassword = "test123!@#";
|
||||
const encrypted = this.encrypt(testPassword);
|
||||
const decrypted = this.decrypt(encrypted);
|
||||
|
||||
if (testPassword === decrypted) {
|
||||
return {
|
||||
success: true,
|
||||
message: "암호화/복호화 테스트가 성공했습니다.",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: "암호화/복호화 결과가 일치하지 않습니다.",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `암호화/복호화 테스트 실패: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,411 @@
|
|||
# 외부 커넥션 관리 시스템 구현 계획서
|
||||
|
||||
## 📋 프로젝트 개요
|
||||
|
||||
### 목적
|
||||
|
||||
- 제어관리 시스템에서 외부 데이터베이스에 접근할 수 있도록 DB 접속 정보를 중앙 관리
|
||||
- 관리자가 외부 DB 연결 설정을 쉽게 등록, 수정, 삭제, 테스트할 수 있는 시스템 구축
|
||||
|
||||
### 주요 기능
|
||||
|
||||
- 외부 DB 접속 정보 CRUD 관리
|
||||
- 다양한 DB 타입 지원 (MySQL, PostgreSQL, Oracle, SQL Server, SQLite)
|
||||
- 연결 테스트 기능
|
||||
- 비밀번호 암호화 저장
|
||||
- 회사별 접속 정보 관리
|
||||
|
||||
## 🗄️ 데이터베이스 설계
|
||||
|
||||
### 테이블: `external_db_connections`
|
||||
|
||||
```sql
|
||||
CREATE TABLE external_db_connections (
|
||||
id SERIAL PRIMARY KEY,
|
||||
connection_name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- DB 연결 정보
|
||||
db_type VARCHAR(20) NOT NULL, -- mysql, postgresql, oracle, mssql, sqlite
|
||||
host VARCHAR(255) NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
database_name VARCHAR(100) NOT NULL,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
password TEXT NOT NULL, -- 암호화된 비밀번호
|
||||
|
||||
-- 고급 설정
|
||||
connection_timeout INTEGER DEFAULT 30,
|
||||
query_timeout INTEGER DEFAULT 60,
|
||||
max_connections INTEGER DEFAULT 10,
|
||||
ssl_enabled CHAR(1) DEFAULT 'N',
|
||||
ssl_cert_path VARCHAR(500),
|
||||
connection_options JSONB, -- 추가 연결 옵션
|
||||
|
||||
-- 관리 정보
|
||||
company_code VARCHAR(20) DEFAULT '*',
|
||||
is_active CHAR(1) DEFAULT 'Y',
|
||||
created_date TIMESTAMP DEFAULT NOW(),
|
||||
created_by VARCHAR(50),
|
||||
updated_date TIMESTAMP DEFAULT NOW(),
|
||||
updated_by VARCHAR(50)
|
||||
);
|
||||
|
||||
-- 인덱스
|
||||
CREATE INDEX idx_external_db_connections_company ON external_db_connections(company_code);
|
||||
CREATE INDEX idx_external_db_connections_active ON external_db_connections(is_active);
|
||||
CREATE INDEX idx_external_db_connections_type ON external_db_connections(db_type);
|
||||
```
|
||||
|
||||
### 샘플 데이터
|
||||
|
||||
```sql
|
||||
INSERT INTO external_db_connections (
|
||||
connection_name, description, db_type, host, port,
|
||||
database_name, username, password, company_code
|
||||
) VALUES
|
||||
(
|
||||
'영업팀 MySQL',
|
||||
'영업팀에서 사용하는 고객 데이터베이스',
|
||||
'mysql',
|
||||
'sales-db.company.com',
|
||||
3306,
|
||||
'sales_db',
|
||||
'sales_user',
|
||||
'encrypted_password_here',
|
||||
'COMP001'
|
||||
),
|
||||
(
|
||||
'재무팀 PostgreSQL',
|
||||
'재무 데이터 및 회계 정보',
|
||||
'postgresql',
|
||||
'finance-db.company.com',
|
||||
5432,
|
||||
'finance_db',
|
||||
'finance_user',
|
||||
'encrypted_password_here',
|
||||
'COMP001'
|
||||
);
|
||||
```
|
||||
|
||||
## 🔧 백엔드 구현
|
||||
|
||||
### 1. Prisma 모델 정의
|
||||
|
||||
```typescript
|
||||
// prisma/schema.prisma
|
||||
model external_db_connections {
|
||||
id Int @id @default(autoincrement())
|
||||
connection_name String @db.VarChar(100)
|
||||
description String? @db.Text
|
||||
db_type String @db.VarChar(20)
|
||||
host String @db.VarChar(255)
|
||||
port Int
|
||||
database_name String @db.VarChar(100)
|
||||
username String @db.VarChar(100)
|
||||
password String @db.Text
|
||||
connection_timeout Int? @default(30)
|
||||
query_timeout Int? @default(60)
|
||||
max_connections Int? @default(10)
|
||||
ssl_enabled String @default("N") @db.Char(1)
|
||||
ssl_cert_path String? @db.VarChar(500)
|
||||
connection_options Json?
|
||||
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([company_code])
|
||||
@@index([is_active])
|
||||
@@index([db_type])
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 타입 정의
|
||||
|
||||
```typescript
|
||||
// backend-node/src/types/externalDbTypes.ts
|
||||
export interface ExternalDbConnection {
|
||||
id?: number;
|
||||
connection_name: string;
|
||||
description?: string;
|
||||
db_type: "mysql" | "postgresql" | "oracle" | "mssql" | "sqlite";
|
||||
host: string;
|
||||
port: number;
|
||||
database_name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
connection_timeout?: number;
|
||||
query_timeout?: number;
|
||||
max_connections?: number;
|
||||
ssl_enabled?: string;
|
||||
ssl_cert_path?: string;
|
||||
connection_options?: Record<string, unknown>;
|
||||
company_code: string;
|
||||
is_active: string;
|
||||
created_date?: Date;
|
||||
created_by?: string;
|
||||
updated_date?: Date;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface ExternalDbConnectionFilter {
|
||||
db_type?: string;
|
||||
is_active?: string;
|
||||
company_code?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface ConnectionTestRequest {
|
||||
id?: number;
|
||||
connection_name?: string;
|
||||
db_type: string;
|
||||
host: string;
|
||||
port: number;
|
||||
database_name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
connection_timeout?: number;
|
||||
ssl_enabled?: string;
|
||||
ssl_cert_path?: string;
|
||||
}
|
||||
|
||||
export interface ConnectionTestResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
connection_time?: number;
|
||||
server_version?: string;
|
||||
error_details?: string;
|
||||
}
|
||||
|
||||
export const DB_TYPE_OPTIONS = [
|
||||
{ value: "mysql", label: "MySQL" },
|
||||
{ value: "postgresql", label: "PostgreSQL" },
|
||||
{ value: "oracle", label: "Oracle" },
|
||||
{ value: "mssql", label: "SQL Server" },
|
||||
{ value: "sqlite", label: "SQLite" },
|
||||
];
|
||||
|
||||
export const DB_TYPE_DEFAULTS = {
|
||||
mysql: { port: 3306, driver: "mysql2" },
|
||||
postgresql: { port: 5432, driver: "pg" },
|
||||
oracle: { port: 1521, driver: "oracledb" },
|
||||
mssql: { port: 1433, driver: "mssql" },
|
||||
sqlite: { port: 0, driver: "sqlite3" },
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 서비스 계층
|
||||
|
||||
```typescript
|
||||
// backend-node/src/services/externalDbConnectionService.ts
|
||||
export class ExternalDbConnectionService {
|
||||
// CRUD 메서드들
|
||||
static async getConnections(filter: ExternalDbConnectionFilter);
|
||||
static async getConnectionById(id: number);
|
||||
static async createConnection(data: ExternalDbConnection);
|
||||
static async updateConnection(
|
||||
id: number,
|
||||
data: Partial<ExternalDbConnection>
|
||||
);
|
||||
static async deleteConnection(id: number); // 논리 삭제
|
||||
static async testConnection(connectionData: ConnectionTestRequest);
|
||||
|
||||
// 유틸리티 메서드들
|
||||
private static encryptPassword(password: string): string;
|
||||
private static decryptPassword(encryptedPassword: string): string;
|
||||
private static validateConnectionData(data: ExternalDbConnection): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. API 라우트
|
||||
|
||||
```typescript
|
||||
// backend-node/src/routes/externalDbConnectionRoutes.ts
|
||||
// GET /api/external-db-connections - 목록 조회
|
||||
// GET /api/external-db-connections/:id - 상세 조회
|
||||
// POST /api/external-db-connections - 새 연결 생성
|
||||
// PUT /api/external-db-connections/:id - 연결 수정
|
||||
// DELETE /api/external-db-connections/:id - 연결 삭제 (논리삭제)
|
||||
// POST /api/external-db-connections/test - 연결 테스트
|
||||
```
|
||||
|
||||
## 🎨 프론트엔드 구현
|
||||
|
||||
### 1. API 클라이언트
|
||||
|
||||
```typescript
|
||||
// frontend/lib/api/externalDbConnection.ts
|
||||
export class ExternalDbConnectionAPI {
|
||||
static async getConnections(filter?: ExternalDbConnectionFilter);
|
||||
static async getConnectionById(id: number);
|
||||
static async createConnection(data: ExternalDbConnection);
|
||||
static async updateConnection(
|
||||
id: number,
|
||||
data: Partial<ExternalDbConnection>
|
||||
);
|
||||
static async deleteConnection(id: number);
|
||||
static async testConnection(connectionData: ConnectionTestRequest);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 메인 페이지
|
||||
|
||||
```typescript
|
||||
// frontend/app/(main)/admin/external-connections/page.tsx
|
||||
- 연결 목록 테이블 (리스트형)
|
||||
- 검색 및 필터링 (DB 타입, 상태, 회사)
|
||||
- 새 연결 추가 버튼
|
||||
- 각 행별 편집/삭제/테스트 버튼
|
||||
```
|
||||
|
||||
### 3. 연결 설정 모달
|
||||
|
||||
```typescript
|
||||
// frontend/components/admin/ExternalDbConnectionModal.tsx
|
||||
- 기본 정보 입력 (연결명, 설명)
|
||||
- DB 연결 정보 (타입, 호스트, 포트, DB명, 계정)
|
||||
- 고급 설정 (타임아웃, SSL 등) - 접기/펼치기
|
||||
- 연결 테스트 버튼
|
||||
- 저장/취소 버튼
|
||||
```
|
||||
|
||||
### 4. 연결 테스트 다이얼로그
|
||||
|
||||
```typescript
|
||||
// frontend/components/admin/ConnectionTestDialog.tsx
|
||||
- 테스트 진행 상태 표시
|
||||
- 연결 결과 (성공/실패, 응답시간, 서버 버전)
|
||||
- 오류 상세 정보 표시
|
||||
```
|
||||
|
||||
## 🔒 보안 구현
|
||||
|
||||
### 1. 비밀번호 암호화
|
||||
|
||||
```typescript
|
||||
// backend-node/src/utils/passwordEncryption.ts
|
||||
export class PasswordEncryption {
|
||||
private static readonly ALGORITHM = "aes-256-gcm";
|
||||
private static readonly SECRET_KEY = process.env.DB_PASSWORD_SECRET;
|
||||
|
||||
static encrypt(password: string): string;
|
||||
static decrypt(encryptedPassword: string): string;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 환경 변수 설정
|
||||
|
||||
```env
|
||||
# .env
|
||||
DB_PASSWORD_SECRET=your-super-secret-encryption-key-here
|
||||
```
|
||||
|
||||
### 3. 접근 권한 제어
|
||||
|
||||
- 관리자 권한만 접근 가능
|
||||
- 회사별 데이터 분리
|
||||
- API 호출 시 인증 토큰 검증
|
||||
|
||||
## 📅 구현 일정
|
||||
|
||||
### Phase 1: 데이터베이스 및 백엔드 (2-3일)
|
||||
|
||||
- [ ] 데이터베이스 테이블 생성
|
||||
- [ ] Prisma 모델 정의
|
||||
- [ ] 타입 정의 작성
|
||||
- [ ] 서비스 계층 구현
|
||||
- [ ] API 라우트 구현
|
||||
- [ ] 비밀번호 암호화 구현
|
||||
|
||||
### Phase 2: 프론트엔드 기본 구현 (2-3일)
|
||||
|
||||
- [ ] API 클라이언트 작성
|
||||
- [ ] 메인 페이지 구현 (리스트)
|
||||
- [ ] 연결 설정 모달 구현
|
||||
- [ ] 기본 CRUD 기능 구현
|
||||
|
||||
### Phase 3: 고급 기능 및 테스트 (1-2일)
|
||||
|
||||
- [ ] 연결 테스트 기능 구현
|
||||
- [ ] 고급 설정 옵션 구현
|
||||
- [ ] 에러 처리 및 검증 강화
|
||||
- [ ] UI/UX 개선
|
||||
|
||||
### Phase 4: 통합 및 배포 (1일)
|
||||
|
||||
- [ ] 메뉴 등록
|
||||
- [ ] 권한 설정
|
||||
- [ ] 전체 테스트
|
||||
- [ ] 문서화 완료
|
||||
|
||||
## 🧪 테스트 계획
|
||||
|
||||
### 1. 단위 테스트
|
||||
|
||||
- 비밀번호 암호화/복호화
|
||||
- 연결 데이터 검증
|
||||
- API 엔드포인트 테스트
|
||||
|
||||
### 2. 통합 테스트
|
||||
|
||||
- 실제 DB 연결 테스트 (다양한 DB 타입)
|
||||
- 프론트엔드-백엔드 연동 테스트
|
||||
- 권한 및 보안 테스트
|
||||
|
||||
### 3. 사용자 테스트
|
||||
|
||||
- 관리자 시나리오 테스트
|
||||
- UI/UX 사용성 테스트
|
||||
- 오류 상황 처리 테스트
|
||||
|
||||
## 🚀 배포 및 운영
|
||||
|
||||
### 1. 환경 설정
|
||||
|
||||
- 프로덕션 환경 암호화 키 설정
|
||||
- DB 접속 권한 최소화
|
||||
- 로그 모니터링 설정
|
||||
|
||||
### 2. 모니터링
|
||||
|
||||
- 외부 DB 연결 상태 모니터링
|
||||
- 연결 풀 사용률 모니터링
|
||||
- 쿼리 성능 모니터링
|
||||
|
||||
### 3. 백업 및 복구
|
||||
|
||||
- 연결 설정 정보 백업
|
||||
- 암호화 키 관리
|
||||
- 장애 복구 절차
|
||||
|
||||
## 📚 참고사항
|
||||
|
||||
### 1. 지원 DB 드라이버
|
||||
|
||||
- MySQL: `mysql2`
|
||||
- PostgreSQL: `pg`
|
||||
- Oracle: `oracledb`
|
||||
- SQL Server: `mssql`
|
||||
- SQLite: `sqlite3`
|
||||
|
||||
### 2. 연결 풀 관리
|
||||
|
||||
- 각 DB별 연결 풀 생성
|
||||
- 최대 연결 수 제한
|
||||
- 유휴 연결 정리
|
||||
|
||||
### 3. 확장 가능성
|
||||
|
||||
- NoSQL DB 지원 (MongoDB, Redis)
|
||||
- API 연결 지원
|
||||
- 파일 시스템 연결 지원
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2024년 12월 17일
|
||||
**작성자**: AI Assistant
|
||||
**버전**: 1.0
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,432 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Database, ChevronDown, ChevronRight, Eye, EyeOff } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
ExternalDbConnectionAPI,
|
||||
ExternalDbConnection,
|
||||
DB_TYPE_OPTIONS,
|
||||
DB_TYPE_DEFAULTS,
|
||||
} from "@/lib/api/externalDbConnection";
|
||||
|
||||
interface ExternalDbConnectionModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
editingConnection?: ExternalDbConnection | null;
|
||||
}
|
||||
|
||||
export function ExternalDbConnectionModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
editingConnection,
|
||||
}: ExternalDbConnectionModalProps) {
|
||||
const [formData, setFormData] = useState<Partial<ExternalDbConnection>>({
|
||||
connection_name: "",
|
||||
description: "",
|
||||
db_type: "mysql",
|
||||
host: "",
|
||||
port: 3306,
|
||||
database_name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
connection_timeout: 30,
|
||||
query_timeout: 60,
|
||||
max_connections: 10,
|
||||
ssl_enabled: "N",
|
||||
ssl_cert_path: "",
|
||||
company_code: "*",
|
||||
is_active: "Y",
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
// 편집 모드일 때 기존 데이터 로드
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (editingConnection) {
|
||||
setFormData({
|
||||
...editingConnection,
|
||||
password: "", // 보안상 비밀번호는 빈 값으로 시작
|
||||
});
|
||||
setShowAdvancedSettings(true); // 편집 시 고급 설정 펼치기
|
||||
} else {
|
||||
// 새 연결 생성 시 기본값 설정
|
||||
setFormData({
|
||||
connection_name: "",
|
||||
description: "",
|
||||
db_type: "mysql",
|
||||
host: "",
|
||||
port: 3306,
|
||||
database_name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
connection_timeout: 30,
|
||||
query_timeout: 60,
|
||||
max_connections: 10,
|
||||
ssl_enabled: "N",
|
||||
ssl_cert_path: "",
|
||||
company_code: "*",
|
||||
is_active: "Y",
|
||||
});
|
||||
setShowAdvancedSettings(false);
|
||||
}
|
||||
setShowPassword(false);
|
||||
}
|
||||
}, [isOpen, editingConnection]);
|
||||
|
||||
// DB 타입 변경 시 기본 포트 설정
|
||||
const handleDbTypeChange = (dbType: string) => {
|
||||
const defaultPort = DB_TYPE_DEFAULTS[dbType as keyof typeof DB_TYPE_DEFAULTS]?.port || 3306;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
db_type: dbType as ExternalDbConnection["db_type"],
|
||||
port: defaultPort,
|
||||
}));
|
||||
};
|
||||
|
||||
// 폼 데이터 변경 핸들러
|
||||
const handleInputChange = (field: keyof ExternalDbConnection, value: string | number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
// 필수 필드 검증
|
||||
if (
|
||||
!formData.connection_name ||
|
||||
!formData.db_type ||
|
||||
!formData.host ||
|
||||
!formData.port ||
|
||||
!formData.database_name ||
|
||||
!formData.username
|
||||
) {
|
||||
toast.error("필수 필드를 모두 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 편집 모드에서 비밀번호가 비어있으면 기존 비밀번호 유지
|
||||
if (editingConnection && !formData.password) {
|
||||
formData.password = "***ENCRYPTED***"; // 서버에서 기존 비밀번호 유지하도록 표시
|
||||
} else if (!editingConnection && !formData.password) {
|
||||
toast.error("새 연결 생성 시 비밀번호는 필수입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const connectionData = {
|
||||
...formData,
|
||||
port: Number(formData.port),
|
||||
connection_timeout: Number(formData.connection_timeout),
|
||||
query_timeout: Number(formData.query_timeout),
|
||||
max_connections: Number(formData.max_connections),
|
||||
} as ExternalDbConnection;
|
||||
|
||||
let response;
|
||||
if (editingConnection?.id) {
|
||||
response = await ExternalDbConnectionAPI.updateConnection(editingConnection.id, connectionData);
|
||||
} else {
|
||||
response = await ExternalDbConnectionAPI.createConnection(connectionData);
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
toast.success(editingConnection ? "연결이 수정되었습니다." : "연결이 생성되었습니다.");
|
||||
onSave();
|
||||
} else {
|
||||
toast.error(response.message || "저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("저장 오류:", error);
|
||||
toast.error("저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 취소
|
||||
const handleCancel = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 저장 버튼 비활성화 조건
|
||||
const isSaveDisabled = () => {
|
||||
return (
|
||||
loading ||
|
||||
!formData.connection_name ||
|
||||
!formData.host ||
|
||||
!formData.port ||
|
||||
!formData.database_name ||
|
||||
!formData.username ||
|
||||
(!editingConnection && !formData.password)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleCancel}>
|
||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
{editingConnection ? "외부 DB 연결 수정" : "새 외부 DB 연결 추가"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">기본 정보</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="connection_name">연결명 *</Label>
|
||||
<Input
|
||||
id="connection_name"
|
||||
value={formData.connection_name || ""}
|
||||
onChange={(e) => handleInputChange("connection_name", e.target.value)}
|
||||
placeholder="예: 영업팀 MySQL"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="db_type">DB 타입 *</Label>
|
||||
<Select value={formData.db_type || "mysql"} onValueChange={handleDbTypeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DB_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">설명</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description || ""}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
placeholder="연결에 대한 설명을 입력하세요"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 연결 정보 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">연결 정보</h3>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="host">호스트 주소 *</Label>
|
||||
<Input
|
||||
id="host"
|
||||
value={formData.host || ""}
|
||||
onChange={(e) => handleInputChange("host", e.target.value)}
|
||||
placeholder="예: localhost, db.company.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="port">포트 *</Label>
|
||||
<Input
|
||||
id="port"
|
||||
type="number"
|
||||
value={formData.port || ""}
|
||||
onChange={(e) => handleInputChange("port", parseInt(e.target.value) || 0)}
|
||||
min={1}
|
||||
max={65535}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="database_name">데이터베이스명 *</Label>
|
||||
<Input
|
||||
id="database_name"
|
||||
value={formData.database_name || ""}
|
||||
onChange={(e) => handleInputChange("database_name", e.target.value)}
|
||||
placeholder="예: sales_db, production"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="username">사용자명 *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username || ""}
|
||||
onChange={(e) => handleInputChange("username", e.target.value)}
|
||||
placeholder="DB 사용자명"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="password">비밀번호 {!editingConnection && "*"}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={formData.password || ""}
|
||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
||||
placeholder={editingConnection ? "변경하려면 입력하세요" : "DB 비밀번호"}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-1/2 right-2 h-8 w-8 -translate-y-1/2 p-0"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 고급 설정 (접기/펼치기) */}
|
||||
<Collapsible open={showAdvancedSettings} onOpenChange={setShowAdvancedSettings}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" className="flex w-full justify-between p-0">
|
||||
<h3 className="text-lg font-medium">고급 설정</h3>
|
||||
{showAdvancedSettings ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-4 pt-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="connection_timeout">연결 타임아웃 (초)</Label>
|
||||
<Input
|
||||
id="connection_timeout"
|
||||
type="number"
|
||||
value={formData.connection_timeout || 30}
|
||||
onChange={(e) => handleInputChange("connection_timeout", parseInt(e.target.value) || 30)}
|
||||
min={1}
|
||||
max={300}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="query_timeout">쿼리 타임아웃 (초)</Label>
|
||||
<Input
|
||||
id="query_timeout"
|
||||
type="number"
|
||||
value={formData.query_timeout || 60}
|
||||
onChange={(e) => handleInputChange("query_timeout", parseInt(e.target.value) || 60)}
|
||||
min={1}
|
||||
max={3600}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="max_connections">최대 연결 수</Label>
|
||||
<Input
|
||||
id="max_connections"
|
||||
type="number"
|
||||
value={formData.max_connections || 10}
|
||||
onChange={(e) => handleInputChange("max_connections", parseInt(e.target.value) || 10)}
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="ssl_enabled"
|
||||
checked={formData.ssl_enabled === "Y"}
|
||||
onCheckedChange={(checked) => handleInputChange("ssl_enabled", checked ? "Y" : "N")}
|
||||
/>
|
||||
<Label htmlFor="ssl_enabled">SSL 사용</Label>
|
||||
</div>
|
||||
|
||||
{formData.ssl_enabled === "Y" && (
|
||||
<div>
|
||||
<Label htmlFor="ssl_cert_path">SSL 인증서 경로</Label>
|
||||
<Input
|
||||
id="ssl_cert_path"
|
||||
value={formData.ssl_cert_path || ""}
|
||||
onChange={(e) => handleInputChange("ssl_cert_path", e.target.value)}
|
||||
placeholder="/path/to/certificate.pem"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="company_code">회사 코드</Label>
|
||||
<Input
|
||||
id="company_code"
|
||||
value={formData.company_code || "*"}
|
||||
onChange={(e) => handleInputChange("company_code", e.target.value)}
|
||||
placeholder="회사 코드 (기본: *)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="is_active">상태</Label>
|
||||
<Select
|
||||
value={formData.is_active || "Y"}
|
||||
onValueChange={(value) => handleInputChange("is_active", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Y">활성</SelectItem>
|
||||
<SelectItem value="N">비활성</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* 편집 모드일 때 정보 표시 */}
|
||||
{editingConnection && (
|
||||
<div className="bg-muted rounded-lg p-4 text-sm">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Badge variant="secondary">편집 모드</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
생성일:{" "}
|
||||
{editingConnection.created_date ? new Date(editingConnection.created_date).toLocaleString() : "-"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
비밀번호를 변경하지 않으려면 비워두세요. 기존 비밀번호가 유지됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleCancel} disabled={loading}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaveDisabled()}>
|
||||
{loading ? "저장 중..." : editingConnection ? "수정" : "생성"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
// 외부 DB 연결 API 클라이언트
|
||||
// 작성일: 2024-12-17
|
||||
|
||||
// API 기본 설정
|
||||
const getApiBaseUrl = () => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return "http://localhost:8080/api";
|
||||
}
|
||||
return "/api";
|
||||
};
|
||||
|
||||
// 타입 정의
|
||||
export interface ExternalDbConnection {
|
||||
id?: number;
|
||||
connection_name: string;
|
||||
description?: string;
|
||||
db_type: "mysql" | "postgresql" | "oracle" | "mssql" | "sqlite";
|
||||
host: string;
|
||||
port: number;
|
||||
database_name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
connection_timeout?: number;
|
||||
query_timeout?: number;
|
||||
max_connections?: number;
|
||||
ssl_enabled?: string;
|
||||
ssl_cert_path?: string;
|
||||
connection_options?: Record<string, unknown>;
|
||||
company_code: string;
|
||||
is_active: string;
|
||||
created_date?: Date;
|
||||
created_by?: string;
|
||||
updated_date?: Date;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface ExternalDbConnectionFilter {
|
||||
db_type?: string;
|
||||
is_active?: string;
|
||||
company_code?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// DB 타입 옵션
|
||||
export const DB_TYPE_OPTIONS = [
|
||||
{ value: "mysql", label: "MySQL" },
|
||||
{ value: "postgresql", label: "PostgreSQL" },
|
||||
{ value: "oracle", label: "Oracle" },
|
||||
{ value: "mssql", label: "SQL Server" },
|
||||
{ value: "sqlite", label: "SQLite" },
|
||||
];
|
||||
|
||||
// DB 타입별 기본 설정
|
||||
export const DB_TYPE_DEFAULTS = {
|
||||
mysql: { port: 3306, driver: "mysql2" },
|
||||
postgresql: { port: 5432, driver: "pg" },
|
||||
oracle: { port: 1521, driver: "oracledb" },
|
||||
mssql: { port: 1433, driver: "mssql" },
|
||||
sqlite: { port: 0, driver: "sqlite3" },
|
||||
};
|
||||
|
||||
// 활성 상태 옵션
|
||||
export const ACTIVE_STATUS_OPTIONS = [
|
||||
{ value: "Y", label: "활성" },
|
||||
{ value: "N", label: "비활성" },
|
||||
{ value: "ALL", label: "전체" },
|
||||
];
|
||||
|
||||
// API 클라이언트 클래스
|
||||
export class ExternalDbConnectionAPI {
|
||||
private static getAuthHeaders() {
|
||||
const token = localStorage.getItem("authToken");
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
};
|
||||
}
|
||||
|
||||
private static async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
// 응답이 JSON인지 확인
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
throw new Error(`서버에서 JSON이 아닌 응답을 받았습니다: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: data.message || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: data.error,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("API 응답 처리 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 목록 조회
|
||||
*/
|
||||
static async getConnections(filter?: ExternalDbConnectionFilter): Promise<ApiResponse<ExternalDbConnection[]>> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filter) {
|
||||
Object.entries(filter).forEach(([key, value]) => {
|
||||
if (value && value.trim()) {
|
||||
params.append(key, value.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${getApiBaseUrl()}/external-db-connections${params.toString() ? `?${params.toString()}` : ""}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
return this.handleResponse<ExternalDbConnection[]>(response);
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 목록 조회 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "네트워크 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Network error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 외부 DB 연결 조회
|
||||
*/
|
||||
static async getConnectionById(id: number): Promise<ApiResponse<ExternalDbConnection>> {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/external-db-connections/${id}`, {
|
||||
method: "GET",
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
return this.handleResponse<ExternalDbConnection>(response);
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 조회 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "네트워크 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Network error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 외부 DB 연결 생성
|
||||
*/
|
||||
static async createConnection(data: ExternalDbConnection): Promise<ApiResponse<ExternalDbConnection>> {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/external-db-connections`, {
|
||||
method: "POST",
|
||||
headers: this.getAuthHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return this.handleResponse<ExternalDbConnection>(response);
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 생성 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "네트워크 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Network error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 수정
|
||||
*/
|
||||
static async updateConnection(
|
||||
id: number,
|
||||
data: Partial<ExternalDbConnection>,
|
||||
): Promise<ApiResponse<ExternalDbConnection>> {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/external-db-connections/${id}`, {
|
||||
method: "PUT",
|
||||
headers: this.getAuthHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return this.handleResponse<ExternalDbConnection>(response);
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 수정 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "네트워크 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Network error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 삭제
|
||||
*/
|
||||
static async deleteConnection(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/external-db-connections/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
return this.handleResponse<void>(response);
|
||||
} catch (error) {
|
||||
console.error("외부 DB 연결 삭제 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "네트워크 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Network error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 지원하는 DB 타입 목록 조회
|
||||
*/
|
||||
static async getSupportedTypes(): Promise<
|
||||
ApiResponse<{ types: typeof DB_TYPE_OPTIONS; defaults: typeof DB_TYPE_DEFAULTS }>
|
||||
> {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/external-db-connections/types/supported`, {
|
||||
method: "GET",
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
return this.handleResponse<{ types: typeof DB_TYPE_OPTIONS; defaults: typeof DB_TYPE_DEFAULTS }>(response);
|
||||
} catch (error) {
|
||||
console.error("지원 DB 타입 조회 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "네트워크 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Network error",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue