외부 db연동 구현
This commit is contained in:
parent
12087cbdd7
commit
2ee4dd0b58
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<string, any>,
|
||||
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<string>();
|
||||
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]) : [];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string>>({});
|
||||
const [parameterTypes, setParameterTypes] = useState<Record<string, string>>({});
|
||||
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
||||
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() {
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{/* DB 연결 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2 text-xs">
|
||||
<Link2 className="h-3 w-3" />
|
||||
DB 연결
|
||||
</Label>
|
||||
<Select
|
||||
value={(selectedQuery as any).externalConnectionId?.toString() || "internal"}
|
||||
onValueChange={(value) =>
|
||||
handleUpdateQuery(selectedQuery.id, {
|
||||
externalConnectionId: value === "internal" ? null : parseInt(value),
|
||||
} as any)
|
||||
}
|
||||
disabled={isLoadingConnections}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="internal">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4" />
|
||||
내부 DB (PostgreSQL)
|
||||
</div>
|
||||
</SelectItem>
|
||||
{externalConnections.length > 0 && (
|
||||
<>
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-gray-500">외부 DB</div>
|
||||
{externalConnections.map((conn) => (
|
||||
<SelectItem key={conn.id} value={conn.id.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4" />
|
||||
{conn.connection_name}
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{conn.db_type.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* SQL 쿼리 */}
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface ReportQuery {
|
|||
type: "MASTER" | "DETAIL";
|
||||
sqlQuery: string;
|
||||
parameters: string[];
|
||||
externalConnectionId?: number | null; // 외부 DB 연결 ID
|
||||
}
|
||||
|
||||
// 템플릿 레이아웃 정의
|
||||
|
|
|
|||
|
|
@ -118,14 +118,33 @@ export const reportApi = {
|
|||
},
|
||||
|
||||
// 쿼리 실행
|
||||
executeQuery: async (reportId: string, queryId: string, parameters: Record<string, any>, sqlQuery?: string) => {
|
||||
executeQuery: async (
|
||||
reportId: string,
|
||||
queryId: string,
|
||||
parameters: Record<string, any>,
|
||||
sqlQuery?: string,
|
||||
externalConnectionId?: number | null,
|
||||
) => {
|
||||
const response = await apiClient.post<{
|
||||
success: boolean;
|
||||
data: {
|
||||
fields: string[];
|
||||
rows: any[];
|
||||
};
|
||||
}>(`${BASE_URL}/${reportId}/queries/${queryId}/execute`, { parameters, sqlQuery });
|
||||
}>(`${BASE_URL}/${reportId}/queries/${queryId}/execute`, {
|
||||
parameters,
|
||||
sqlQuery,
|
||||
externalConnectionId,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 외부 DB 연결 목록 조회
|
||||
getExternalConnections: async () => {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: any[];
|
||||
}>(`${BASE_URL}/external-connections`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export interface ReportQuery {
|
|||
query_type: "MASTER" | "DETAIL";
|
||||
sql_query: string;
|
||||
parameters: string[];
|
||||
external_connection_id: number | null; // 외부 DB 연결 ID
|
||||
display_order: number;
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
|
|
@ -70,6 +71,15 @@ export interface ReportQuery {
|
|||
updated_by: string | null;
|
||||
}
|
||||
|
||||
// 외부 DB 연결 (간단한 버전)
|
||||
export interface ExternalConnection {
|
||||
id: number;
|
||||
connection_name: string;
|
||||
db_type: string;
|
||||
description?: string;
|
||||
is_active: string;
|
||||
}
|
||||
|
||||
// 컴포넌트 설정
|
||||
export interface ComponentConfig {
|
||||
id: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue