From 2ee4dd0b5812666d11b854485374af48fc0de0e0 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 1 Oct 2025 14:36:46 +0900 Subject: [PATCH] =?UTF-8?q?=EC=99=B8=EB=B6=80=20db=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/reportController.ts | 30 ++++++- backend-node/src/routes/reportRoutes.ts | 7 +- backend-node/src/services/reportService.ts | 77 +++++++++++++++--- backend-node/src/types/report.ts | 1 + .../report/designer/QueryManager.tsx | 81 ++++++++++++++++++- frontend/contexts/ReportDesignerContext.tsx | 1 + frontend/lib/api/reportApi.ts | 23 +++++- frontend/types/report.ts | 10 +++ 8 files changed, 212 insertions(+), 18 deletions(-) diff --git a/backend-node/src/controllers/reportController.ts b/backend-node/src/controllers/reportController.ts index d188758b..256aba68 100644 --- a/backend-node/src/controllers/reportController.ts +++ b/backend-node/src/controllers/reportController.ts @@ -335,13 +335,14 @@ export class ReportController { async executeQuery(req: Request, res: Response, next: NextFunction) { try { const { reportId, queryId } = req.params; - const { parameters = {}, sqlQuery } = req.body; + const { parameters = {}, sqlQuery, externalConnectionId } = req.body; const result = await reportService.executeQuery( reportId, queryId, parameters, - sqlQuery + sqlQuery, + externalConnectionId ); return res.json({ @@ -355,6 +356,31 @@ export class ReportController { }); } } + + /** + * 외부 DB 연결 목록 조회 (활성화된 것만) + * GET /api/admin/reports/external-connections + */ + async getExternalConnections( + req: Request, + res: Response, + next: NextFunction + ) { + try { + const { ExternalDbConnectionService } = await import( + "../services/externalDbConnectionService" + ); + + const result = await ExternalDbConnectionService.getConnections({ + is_active: "Y", + company_code: req.body.companyCode || "", + }); + + return res.json(result); + } catch (error) { + return next(error); + } + } } export default new ReportController(); diff --git a/backend-node/src/routes/reportRoutes.ts b/backend-node/src/routes/reportRoutes.ts index 44b20993..b511e28b 100644 --- a/backend-node/src/routes/reportRoutes.ts +++ b/backend-node/src/routes/reportRoutes.ts @@ -7,7 +7,12 @@ const router = Router(); // 모든 리포트 API는 인증이 필요 router.use(authenticateToken); -// 템플릿 관련 라우트 (구체적인 경로를 먼저 배치) +// 외부 DB 연결 목록 (구체적인 경로를 먼저 배치) +router.get("/external-connections", (req, res, next) => + reportController.getExternalConnections(req, res, next) +); + +// 템플릿 관련 라우트 router.get("/templates", (req, res, next) => reportController.getTemplates(req, res, next) ); diff --git a/backend-node/src/services/reportService.ts b/backend-node/src/services/reportService.ts index fb582db9..ecf49cb2 100644 --- a/backend-node/src/services/reportService.ts +++ b/backend-node/src/services/reportService.ts @@ -18,6 +18,8 @@ import { GetTemplatesResponse, CreateTemplateRequest, } from "../types/report"; +import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; +import { ExternalDbConnectionService } from "./externalDbConnectionService"; export class ReportService { /** @@ -165,6 +167,7 @@ export class ReportService { query_type, sql_query, parameters, + external_connection_id, display_order, created_at, created_by, @@ -449,9 +452,10 @@ export class ReportService { query_type, sql_query, parameters, + external_connection_id, display_order, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) `; for (const originalQuery of queriesResult.rows) { @@ -463,6 +467,7 @@ export class ReportService { originalQuery.query_type, originalQuery.sql_query, JSON.stringify(originalQuery.parameters), + originalQuery.external_connection_id || null, originalQuery.display_order, userId, ]); @@ -595,9 +600,10 @@ export class ReportService { query_type, sql_query, parameters, + external_connection_id, display_order, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) `; for (let i = 0; i < data.queries.length; i++) { @@ -609,6 +615,7 @@ export class ReportService { q.type, q.sqlQuery, JSON.stringify(q.parameters), + (q as any).externalConnectionId || null, // 외부 DB 연결 ID i, userId, ]); @@ -620,26 +627,34 @@ export class ReportService { } /** - * 쿼리 실행 + * 쿼리 실행 (내부 DB 또는 외부 DB) */ async executeQuery( reportId: string, queryId: string, parameters: Record, - sqlQuery?: string + sqlQuery?: string, + externalConnectionId?: number | null ): Promise<{ fields: string[]; rows: any[] }> { let sql_query: string; let queryParameters: string[] = []; + let connectionId: number | null = externalConnectionId ?? null; // 테스트 모드 (sqlQuery 직접 전달) if (sqlQuery) { sql_query = sqlQuery; - // 파라미터 순서 추출 + // 파라미터 순서 추출 (등장 순서대로) const matches = sqlQuery.match(/\$\d+/g); if (matches) { - queryParameters = Array.from(new Set(matches)).sort((a, b) => { - return parseInt(a.substring(1)) - parseInt(b.substring(1)); - }); + const seen = new Set(); + const result: string[] = []; + for (const match of matches) { + if (!seen.has(match)) { + seen.add(match); + result.push(match); + } + } + queryParameters = result; } } else { // DB에서 쿼리 조회 @@ -656,6 +671,7 @@ export class ReportService { queryParameters = Array.isArray(queryResult.parameters) ? queryResult.parameters : []; + connectionId = queryResult.external_connection_id; } // 파라미터 배열 생성 ($1, $2 순서대로) @@ -665,8 +681,49 @@ export class ReportService { } try { - // 쿼리 실행 - const result = await query(sql_query, paramArray); + let result: any[]; + + // 외부 DB 연결이 있으면 외부 DB에서 실행 + if (connectionId) { + // 외부 DB 연결 정보 조회 + const connectionResult = + await ExternalDbConnectionService.getConnectionById(connectionId); + + if (!connectionResult.success || !connectionResult.data) { + throw new Error("외부 DB 연결 정보를 찾을 수 없습니다."); + } + + const connection = connectionResult.data; + + // DatabaseConnectorFactory를 사용하여 외부 DB 쿼리 실행 + const config = { + host: connection.host, + port: connection.port, + database: connection.database_name, + user: connection.username, + password: connection.password, + connectionTimeout: connection.connection_timeout || 30000, + queryTimeout: connection.query_timeout || 30000, + }; + + const connector = await DatabaseConnectorFactory.createConnector( + connection.db_type, + config, + connectionId + ); + + await connector.connect(); + + try { + const queryResult = await connector.executeQuery(sql_query); + result = queryResult.rows || []; + } finally { + await connector.disconnect(); + } + } else { + // 내부 DB에서 실행 + result = await query(sql_query, paramArray); + } // 필드명 추출 const fields = result.length > 0 ? Object.keys(result[0]) : []; diff --git a/backend-node/src/types/report.ts b/backend-node/src/types/report.ts index 0b492734..77cc35d7 100644 --- a/backend-node/src/types/report.ts +++ b/backend-node/src/types/report.ts @@ -63,6 +63,7 @@ export interface ReportQuery { query_type: "MASTER" | "DETAIL"; sql_query: string; parameters: string[] | null; + external_connection_id: number | null; // 외부 DB 연결 ID (NULL이면 내부 DB) display_order: number; created_at: Date; created_by: string | null; diff --git a/frontend/components/report/designer/QueryManager.tsx b/frontend/components/report/designer/QueryManager.tsx index ced3f9ff..1e07dfed 100644 --- a/frontend/components/report/designer/QueryManager.tsx +++ b/frontend/components/report/designer/QueryManager.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -8,12 +8,13 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Plus, Trash2, Play, AlertCircle, Database } from "lucide-react"; +import { Plus, Trash2, Play, AlertCircle, Database, Link2 } from "lucide-react"; import { useReportDesigner, ReportQuery } from "@/contexts/ReportDesignerContext"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { reportApi } from "@/lib/api/reportApi"; import { useToast } from "@/hooks/use-toast"; +import type { ExternalConnection } from "@/types/report"; export function QueryManager() { const { queries, setQueries, reportId, setQueryResult, getQueryResult } = useReportDesigner(); @@ -21,11 +22,32 @@ export function QueryManager() { const [isTestRunning, setIsTestRunning] = useState(false); const [parameterValues, setParameterValues] = useState>({}); const [parameterTypes, setParameterTypes] = useState>({}); + const [externalConnections, setExternalConnections] = useState([]); + const [isLoadingConnections, setIsLoadingConnections] = useState(false); const { toast } = useToast(); const selectedQuery = queries.find((q) => q.id === selectedQueryId); const testResult = selectedQuery ? getQueryResult(selectedQuery.id) : null; + // 외부 DB 연결 목록 조회 + useEffect(() => { + const fetchConnections = async () => { + setIsLoadingConnections(true); + try { + const response = await reportApi.getExternalConnections(); + if (response.success && response.data) { + setExternalConnections(response.data); + } + } catch (error) { + console.error("외부 DB 연결 목록 조회 실패:", error); + } finally { + setIsLoadingConnections(false); + } + }; + + fetchConnections(); + }, []); + // 파라미터 감지 ($1, $2 등, 단 작은따옴표 안은 제외) const detectParameters = (sql: string): string[] => { // 작은따옴표 안의 내용을 제거 @@ -55,6 +77,7 @@ export function QueryManager() { type: "MASTER", sqlQuery: "", parameters: [], + externalConnectionId: null, // 기본값: 내부 DB }; setQueries([...queries, newQuery]); setSelectedQueryId(newQuery.id); @@ -121,9 +144,16 @@ export function QueryManager() { // new 리포트는 임시 ID 사용하고 SQL 쿼리 직접 전달 const testReportId = reportId === "new" ? "TEMP_TEST" : reportId; const sqlQuery = reportId === "new" ? selectedQuery.sqlQuery : undefined; + const externalConnectionId = (selectedQuery as any).externalConnectionId || null; // 실제 API 호출 - const response = await reportApi.executeQuery(testReportId, selectedQuery.id, parameterValues, sqlQuery); + const response = await reportApi.executeQuery( + testReportId, + selectedQuery.id, + parameterValues, + sqlQuery, + externalConnectionId, + ); if (response.success && response.data) { // Context에 실행 결과 저장 @@ -246,6 +276,51 @@ export function QueryManager() { + {/* DB 연결 선택 */} +
+ + +
+ {/* SQL 쿼리 */}