db 정보 조회

This commit is contained in:
hyeonsu
2025-09-18 09:32:50 +09:00
parent b1a3ba713a
commit e628c7c4dc
10 changed files with 1971 additions and 59 deletions

View File

@@ -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();

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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}`,
};
}
}
}