2025-10-01 11:34:17 +09:00
|
|
|
/**
|
|
|
|
|
* 리포트 관리 서비스
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { v4 as uuidv4 } from "uuid";
|
|
|
|
|
import { query, queryOne, transaction } from "../database/db";
|
|
|
|
|
import {
|
|
|
|
|
ReportMaster,
|
|
|
|
|
ReportLayout,
|
2025-10-01 13:53:45 +09:00
|
|
|
ReportQuery,
|
2025-10-01 11:34:17 +09:00
|
|
|
ReportTemplate,
|
|
|
|
|
ReportDetail,
|
|
|
|
|
GetReportsParams,
|
|
|
|
|
GetReportsResponse,
|
|
|
|
|
CreateReportRequest,
|
|
|
|
|
UpdateReportRequest,
|
|
|
|
|
SaveLayoutRequest,
|
|
|
|
|
GetTemplatesResponse,
|
|
|
|
|
CreateTemplateRequest,
|
|
|
|
|
} from "../types/report";
|
2025-10-01 14:36:46 +09:00
|
|
|
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
|
|
|
|
|
import { ExternalDbConnectionService } from "./externalDbConnectionService";
|
2025-10-01 11:34:17 +09:00
|
|
|
|
|
|
|
|
export class ReportService {
|
2025-10-02 09:56:44 +09:00
|
|
|
/**
|
|
|
|
|
* SQL 쿼리 검증 (SELECT만 허용)
|
|
|
|
|
*/
|
|
|
|
|
private validateQuerySafety(sql: string): void {
|
|
|
|
|
// 위험한 SQL 명령어 목록
|
|
|
|
|
const dangerousKeywords = [
|
|
|
|
|
"DELETE",
|
|
|
|
|
"DROP",
|
|
|
|
|
"TRUNCATE",
|
|
|
|
|
"INSERT",
|
|
|
|
|
"UPDATE",
|
|
|
|
|
"ALTER",
|
|
|
|
|
"CREATE",
|
|
|
|
|
"REPLACE",
|
|
|
|
|
"MERGE",
|
|
|
|
|
"GRANT",
|
|
|
|
|
"REVOKE",
|
|
|
|
|
"EXECUTE",
|
|
|
|
|
"EXEC",
|
|
|
|
|
"CALL",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// SQL을 대문자로 변환하여 검사
|
|
|
|
|
const upperSql = sql.toUpperCase().trim();
|
|
|
|
|
|
|
|
|
|
// 위험한 키워드 검사
|
|
|
|
|
for (const keyword of dangerousKeywords) {
|
|
|
|
|
// 단어 경계를 고려하여 검사 (예: DELETE, DELETE FROM 등)
|
|
|
|
|
const regex = new RegExp(`\\b${keyword}\\b`, "i");
|
|
|
|
|
if (regex.test(upperSql)) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`보안상의 이유로 ${keyword} 명령어는 사용할 수 없습니다. SELECT 쿼리만 허용됩니다.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SELECT 쿼리인지 확인
|
|
|
|
|
if (!upperSql.startsWith("SELECT") && !upperSql.startsWith("WITH")) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
"SELECT 쿼리만 허용됩니다. 데이터 조회 용도로만 사용할 수 있습니다."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 세미콜론으로 구분된 여러 쿼리 방지
|
|
|
|
|
const semicolonCount = (sql.match(/;/g) || []).length;
|
|
|
|
|
if (
|
|
|
|
|
semicolonCount > 1 ||
|
|
|
|
|
(semicolonCount === 1 && !sql.trim().endsWith(";"))
|
|
|
|
|
) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
"보안상의 이유로 여러 개의 쿼리를 동시에 실행할 수 없습니다."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 11:34:17 +09:00
|
|
|
/**
|
|
|
|
|
* 리포트 목록 조회
|
|
|
|
|
*/
|
|
|
|
|
async getReports(params: GetReportsParams): Promise<GetReportsResponse> {
|
|
|
|
|
const {
|
|
|
|
|
page = 1,
|
|
|
|
|
limit = 20,
|
|
|
|
|
searchText = "",
|
|
|
|
|
reportType = "",
|
|
|
|
|
useYn = "Y",
|
|
|
|
|
sortBy = "created_at",
|
|
|
|
|
sortOrder = "DESC",
|
|
|
|
|
} = params;
|
|
|
|
|
|
|
|
|
|
const offset = (page - 1) * limit;
|
|
|
|
|
|
|
|
|
|
// WHERE 조건 동적 생성
|
|
|
|
|
const conditions: string[] = [];
|
|
|
|
|
const values: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
if (useYn) {
|
|
|
|
|
conditions.push(`use_yn = $${paramIndex++}`);
|
|
|
|
|
values.push(useYn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (searchText) {
|
|
|
|
|
conditions.push(
|
|
|
|
|
`(report_name_kor LIKE $${paramIndex} OR report_name_eng LIKE $${paramIndex})`
|
|
|
|
|
);
|
|
|
|
|
values.push(`%${searchText}%`);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (reportType) {
|
|
|
|
|
conditions.push(`report_type = $${paramIndex++}`);
|
|
|
|
|
values.push(reportType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const whereClause =
|
|
|
|
|
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
|
|
|
|
|
|
|
|
// 전체 개수 조회
|
|
|
|
|
const countQuery = `
|
|
|
|
|
SELECT COUNT(*) as total
|
|
|
|
|
FROM report_master
|
|
|
|
|
${whereClause}
|
|
|
|
|
`;
|
|
|
|
|
const countResult = await queryOne<{ total: string }>(countQuery, values);
|
|
|
|
|
const total = parseInt(countResult?.total || "0", 10);
|
|
|
|
|
|
|
|
|
|
// 목록 조회
|
|
|
|
|
const listQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
report_id,
|
|
|
|
|
report_name_kor,
|
|
|
|
|
report_name_eng,
|
|
|
|
|
template_id,
|
|
|
|
|
report_type,
|
|
|
|
|
company_code,
|
|
|
|
|
description,
|
|
|
|
|
use_yn,
|
|
|
|
|
created_at,
|
|
|
|
|
created_by,
|
|
|
|
|
updated_at,
|
|
|
|
|
updated_by
|
|
|
|
|
FROM report_master
|
|
|
|
|
${whereClause}
|
|
|
|
|
ORDER BY ${sortBy} ${sortOrder}
|
|
|
|
|
LIMIT $${paramIndex++} OFFSET $${paramIndex}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const items = await query<ReportMaster>(listQuery, [
|
|
|
|
|
...values,
|
|
|
|
|
limit,
|
|
|
|
|
offset,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
items,
|
|
|
|
|
total,
|
|
|
|
|
page,
|
|
|
|
|
limit,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 리포트 상세 조회
|
|
|
|
|
*/
|
|
|
|
|
async getReportById(reportId: string): Promise<ReportDetail | null> {
|
|
|
|
|
// 리포트 마스터 조회
|
|
|
|
|
const reportQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
report_id,
|
|
|
|
|
report_name_kor,
|
|
|
|
|
report_name_eng,
|
|
|
|
|
template_id,
|
|
|
|
|
report_type,
|
|
|
|
|
company_code,
|
|
|
|
|
description,
|
|
|
|
|
use_yn,
|
|
|
|
|
created_at,
|
|
|
|
|
created_by,
|
|
|
|
|
updated_at,
|
|
|
|
|
updated_by
|
|
|
|
|
FROM report_master
|
|
|
|
|
WHERE report_id = $1
|
|
|
|
|
`;
|
|
|
|
|
const report = await queryOne<ReportMaster>(reportQuery, [reportId]);
|
|
|
|
|
|
|
|
|
|
if (!report) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 레이아웃 조회
|
|
|
|
|
const layoutQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
layout_id,
|
|
|
|
|
report_id,
|
|
|
|
|
canvas_width,
|
|
|
|
|
canvas_height,
|
|
|
|
|
page_orientation,
|
|
|
|
|
margin_top,
|
|
|
|
|
margin_bottom,
|
|
|
|
|
margin_left,
|
|
|
|
|
margin_right,
|
|
|
|
|
components,
|
|
|
|
|
created_at,
|
|
|
|
|
created_by,
|
|
|
|
|
updated_at,
|
|
|
|
|
updated_by
|
|
|
|
|
FROM report_layout
|
|
|
|
|
WHERE report_id = $1
|
|
|
|
|
`;
|
|
|
|
|
const layout = await queryOne<ReportLayout>(layoutQuery, [reportId]);
|
|
|
|
|
|
2025-10-01 13:53:45 +09:00
|
|
|
// 쿼리 조회
|
|
|
|
|
const queriesQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
query_id,
|
|
|
|
|
report_id,
|
|
|
|
|
query_name,
|
|
|
|
|
query_type,
|
|
|
|
|
sql_query,
|
|
|
|
|
parameters,
|
2025-10-01 14:36:46 +09:00
|
|
|
external_connection_id,
|
2025-10-01 13:53:45 +09:00
|
|
|
display_order,
|
|
|
|
|
created_at,
|
|
|
|
|
created_by,
|
|
|
|
|
updated_at,
|
|
|
|
|
updated_by
|
|
|
|
|
FROM report_query
|
|
|
|
|
WHERE report_id = $1
|
|
|
|
|
ORDER BY display_order, created_at
|
|
|
|
|
`;
|
|
|
|
|
const queries = await query<ReportQuery>(queriesQuery, [reportId]);
|
|
|
|
|
|
2025-10-01 11:34:17 +09:00
|
|
|
return {
|
|
|
|
|
report,
|
|
|
|
|
layout,
|
2025-10-01 13:53:45 +09:00
|
|
|
queries: queries || [],
|
2025-10-01 11:34:17 +09:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 리포트 생성
|
|
|
|
|
*/
|
|
|
|
|
async createReport(
|
|
|
|
|
data: CreateReportRequest,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const reportId = `RPT_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
|
|
|
|
|
|
|
|
|
|
return transaction(async (client) => {
|
|
|
|
|
// 리포트 마스터 생성
|
|
|
|
|
const insertReportQuery = `
|
|
|
|
|
INSERT INTO report_master (
|
|
|
|
|
report_id,
|
|
|
|
|
report_name_kor,
|
|
|
|
|
report_name_eng,
|
|
|
|
|
template_id,
|
|
|
|
|
report_type,
|
|
|
|
|
company_code,
|
|
|
|
|
description,
|
|
|
|
|
use_yn,
|
|
|
|
|
created_by
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, 'Y', $8)
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await client.query(insertReportQuery, [
|
|
|
|
|
reportId,
|
|
|
|
|
data.reportNameKor,
|
|
|
|
|
data.reportNameEng || null,
|
|
|
|
|
data.templateId || null,
|
|
|
|
|
data.reportType,
|
|
|
|
|
data.companyCode || null,
|
|
|
|
|
data.description || null,
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 템플릿이 있으면 해당 템플릿의 레이아웃 복사
|
|
|
|
|
if (data.templateId) {
|
|
|
|
|
const templateQuery = `
|
|
|
|
|
SELECT layout_config FROM report_template WHERE template_id = $1
|
|
|
|
|
`;
|
|
|
|
|
const template = await client.query(templateQuery, [data.templateId]);
|
|
|
|
|
|
|
|
|
|
if (template.rows.length > 0 && template.rows[0].layout_config) {
|
|
|
|
|
const layoutConfig = JSON.parse(template.rows[0].layout_config);
|
|
|
|
|
const layoutId = `LAY_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
|
|
|
|
|
|
|
|
|
|
const insertLayoutQuery = `
|
|
|
|
|
INSERT INTO report_layout (
|
|
|
|
|
layout_id,
|
|
|
|
|
report_id,
|
|
|
|
|
canvas_width,
|
|
|
|
|
canvas_height,
|
|
|
|
|
page_orientation,
|
|
|
|
|
margin_top,
|
|
|
|
|
margin_bottom,
|
|
|
|
|
margin_left,
|
|
|
|
|
margin_right,
|
|
|
|
|
components,
|
|
|
|
|
created_by
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await client.query(insertLayoutQuery, [
|
|
|
|
|
layoutId,
|
|
|
|
|
reportId,
|
|
|
|
|
layoutConfig.width || 210,
|
|
|
|
|
layoutConfig.height || 297,
|
|
|
|
|
layoutConfig.orientation || "portrait",
|
|
|
|
|
20,
|
|
|
|
|
20,
|
|
|
|
|
20,
|
|
|
|
|
20,
|
|
|
|
|
JSON.stringify([]),
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return reportId;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 리포트 수정
|
|
|
|
|
*/
|
|
|
|
|
async updateReport(
|
|
|
|
|
reportId: string,
|
|
|
|
|
data: UpdateReportRequest,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
const setClauses: string[] = [];
|
|
|
|
|
const values: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
if (data.reportNameKor !== undefined) {
|
|
|
|
|
setClauses.push(`report_name_kor = $${paramIndex++}`);
|
|
|
|
|
values.push(data.reportNameKor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.reportNameEng !== undefined) {
|
|
|
|
|
setClauses.push(`report_name_eng = $${paramIndex++}`);
|
|
|
|
|
values.push(data.reportNameEng);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.reportType !== undefined) {
|
|
|
|
|
setClauses.push(`report_type = $${paramIndex++}`);
|
|
|
|
|
values.push(data.reportType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.description !== undefined) {
|
|
|
|
|
setClauses.push(`description = $${paramIndex++}`);
|
|
|
|
|
values.push(data.description);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.useYn !== undefined) {
|
|
|
|
|
setClauses.push(`use_yn = $${paramIndex++}`);
|
|
|
|
|
values.push(data.useYn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (setClauses.length === 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setClauses.push(`updated_at = CURRENT_TIMESTAMP`);
|
|
|
|
|
setClauses.push(`updated_by = $${paramIndex++}`);
|
|
|
|
|
values.push(userId);
|
|
|
|
|
|
|
|
|
|
values.push(reportId);
|
|
|
|
|
|
|
|
|
|
const updateQuery = `
|
|
|
|
|
UPDATE report_master
|
|
|
|
|
SET ${setClauses.join(", ")}
|
|
|
|
|
WHERE report_id = $${paramIndex}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = await query(updateQuery, values);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 리포트 삭제
|
|
|
|
|
*/
|
|
|
|
|
async deleteReport(reportId: string): Promise<boolean> {
|
|
|
|
|
return transaction(async (client) => {
|
2025-10-01 13:53:45 +09:00
|
|
|
// 쿼리 삭제 (CASCADE로 자동 삭제되지만 명시적으로)
|
|
|
|
|
await client.query(`DELETE FROM report_query WHERE report_id = $1`, [
|
|
|
|
|
reportId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 레이아웃 삭제
|
2025-10-01 11:34:17 +09:00
|
|
|
await client.query(`DELETE FROM report_layout WHERE report_id = $1`, [
|
|
|
|
|
reportId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 리포트 마스터 삭제
|
|
|
|
|
const result = await client.query(
|
|
|
|
|
`DELETE FROM report_master WHERE report_id = $1`,
|
|
|
|
|
[reportId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (result.rowCount ?? 0) > 0;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 리포트 복사
|
|
|
|
|
*/
|
|
|
|
|
async copyReport(reportId: string, userId: string): Promise<string | null> {
|
|
|
|
|
return transaction(async (client) => {
|
|
|
|
|
// 원본 리포트 조회
|
|
|
|
|
const originalQuery = `
|
|
|
|
|
SELECT * FROM report_master WHERE report_id = $1
|
|
|
|
|
`;
|
|
|
|
|
const originalResult = await client.query(originalQuery, [reportId]);
|
|
|
|
|
|
|
|
|
|
if (originalResult.rows.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const original = originalResult.rows[0];
|
|
|
|
|
const newReportId = `RPT_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
|
|
|
|
|
|
|
|
|
|
// 리포트 마스터 복사
|
|
|
|
|
const copyReportQuery = `
|
|
|
|
|
INSERT INTO report_master (
|
|
|
|
|
report_id,
|
|
|
|
|
report_name_kor,
|
|
|
|
|
report_name_eng,
|
|
|
|
|
template_id,
|
|
|
|
|
report_type,
|
|
|
|
|
company_code,
|
|
|
|
|
description,
|
|
|
|
|
use_yn,
|
|
|
|
|
created_by
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await client.query(copyReportQuery, [
|
|
|
|
|
newReportId,
|
|
|
|
|
`${original.report_name_kor} (복사)`,
|
|
|
|
|
original.report_name_eng ? `${original.report_name_eng} (Copy)` : null,
|
|
|
|
|
original.template_id,
|
|
|
|
|
original.report_type,
|
|
|
|
|
original.company_code,
|
|
|
|
|
original.description,
|
|
|
|
|
original.use_yn,
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 레이아웃 복사
|
|
|
|
|
const layoutQuery = `
|
|
|
|
|
SELECT * FROM report_layout WHERE report_id = $1
|
|
|
|
|
`;
|
|
|
|
|
const layoutResult = await client.query(layoutQuery, [reportId]);
|
|
|
|
|
|
|
|
|
|
if (layoutResult.rows.length > 0) {
|
|
|
|
|
const originalLayout = layoutResult.rows[0];
|
|
|
|
|
const newLayoutId = `LAY_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
|
|
|
|
|
|
|
|
|
|
const copyLayoutQuery = `
|
|
|
|
|
INSERT INTO report_layout (
|
|
|
|
|
layout_id,
|
|
|
|
|
report_id,
|
|
|
|
|
canvas_width,
|
|
|
|
|
canvas_height,
|
|
|
|
|
page_orientation,
|
|
|
|
|
margin_top,
|
|
|
|
|
margin_bottom,
|
|
|
|
|
margin_left,
|
|
|
|
|
margin_right,
|
|
|
|
|
components,
|
|
|
|
|
created_by
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await client.query(copyLayoutQuery, [
|
|
|
|
|
newLayoutId,
|
|
|
|
|
newReportId,
|
|
|
|
|
originalLayout.canvas_width,
|
|
|
|
|
originalLayout.canvas_height,
|
|
|
|
|
originalLayout.page_orientation,
|
|
|
|
|
originalLayout.margin_top,
|
|
|
|
|
originalLayout.margin_bottom,
|
|
|
|
|
originalLayout.margin_left,
|
|
|
|
|
originalLayout.margin_right,
|
2025-10-01 14:27:44 +09:00
|
|
|
JSON.stringify(originalLayout.components),
|
2025-10-01 11:34:17 +09:00
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 13:53:45 +09:00
|
|
|
// 쿼리 복사
|
|
|
|
|
const queriesQuery = `
|
|
|
|
|
SELECT * FROM report_query WHERE report_id = $1 ORDER BY display_order
|
|
|
|
|
`;
|
|
|
|
|
const queriesResult = await client.query(queriesQuery, [reportId]);
|
|
|
|
|
|
|
|
|
|
if (queriesResult.rows.length > 0) {
|
|
|
|
|
const copyQuerySql = `
|
|
|
|
|
INSERT INTO report_query (
|
|
|
|
|
query_id,
|
|
|
|
|
report_id,
|
|
|
|
|
query_name,
|
|
|
|
|
query_type,
|
|
|
|
|
sql_query,
|
|
|
|
|
parameters,
|
2025-10-01 14:36:46 +09:00
|
|
|
external_connection_id,
|
2025-10-01 13:53:45 +09:00
|
|
|
display_order,
|
|
|
|
|
created_by
|
2025-10-01 14:36:46 +09:00
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
2025-10-01 13:53:45 +09:00
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
for (const originalQuery of queriesResult.rows) {
|
|
|
|
|
const newQueryId = `QRY_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
|
|
|
|
|
await client.query(copyQuerySql, [
|
|
|
|
|
newQueryId,
|
|
|
|
|
newReportId,
|
|
|
|
|
originalQuery.query_name,
|
|
|
|
|
originalQuery.query_type,
|
|
|
|
|
originalQuery.sql_query,
|
2025-10-01 14:27:44 +09:00
|
|
|
JSON.stringify(originalQuery.parameters),
|
2025-10-01 14:36:46 +09:00
|
|
|
originalQuery.external_connection_id || null,
|
2025-10-01 13:53:45 +09:00
|
|
|
originalQuery.display_order,
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 11:34:17 +09:00
|
|
|
return newReportId;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 레이아웃 조회
|
|
|
|
|
*/
|
|
|
|
|
async getLayout(reportId: string): Promise<ReportLayout | null> {
|
|
|
|
|
const layoutQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
layout_id,
|
|
|
|
|
report_id,
|
|
|
|
|
canvas_width,
|
|
|
|
|
canvas_height,
|
|
|
|
|
page_orientation,
|
|
|
|
|
margin_top,
|
|
|
|
|
margin_bottom,
|
|
|
|
|
margin_left,
|
|
|
|
|
margin_right,
|
|
|
|
|
components,
|
|
|
|
|
created_at,
|
|
|
|
|
created_by,
|
|
|
|
|
updated_at,
|
|
|
|
|
updated_by
|
|
|
|
|
FROM report_layout
|
|
|
|
|
WHERE report_id = $1
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
return queryOne<ReportLayout>(layoutQuery, [reportId]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-01 13:53:45 +09:00
|
|
|
* 레이아웃 저장 (쿼리 포함)
|
2025-10-01 11:34:17 +09:00
|
|
|
*/
|
|
|
|
|
async saveLayout(
|
|
|
|
|
reportId: string,
|
|
|
|
|
data: SaveLayoutRequest,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
return transaction(async (client) => {
|
2025-10-01 13:53:45 +09:00
|
|
|
// 1. 레이아웃 저장
|
2025-10-01 11:34:17 +09:00
|
|
|
const existingQuery = `
|
|
|
|
|
SELECT layout_id FROM report_layout WHERE report_id = $1
|
|
|
|
|
`;
|
|
|
|
|
const existing = await client.query(existingQuery, [reportId]);
|
|
|
|
|
|
|
|
|
|
if (existing.rows.length > 0) {
|
|
|
|
|
// 업데이트
|
|
|
|
|
const updateQuery = `
|
|
|
|
|
UPDATE report_layout
|
|
|
|
|
SET
|
|
|
|
|
canvas_width = $1,
|
|
|
|
|
canvas_height = $2,
|
|
|
|
|
page_orientation = $3,
|
|
|
|
|
margin_top = $4,
|
|
|
|
|
margin_bottom = $5,
|
|
|
|
|
margin_left = $6,
|
|
|
|
|
margin_right = $7,
|
|
|
|
|
components = $8,
|
|
|
|
|
updated_at = CURRENT_TIMESTAMP,
|
|
|
|
|
updated_by = $9
|
|
|
|
|
WHERE report_id = $10
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await client.query(updateQuery, [
|
|
|
|
|
data.canvasWidth,
|
|
|
|
|
data.canvasHeight,
|
|
|
|
|
data.pageOrientation,
|
|
|
|
|
data.marginTop,
|
|
|
|
|
data.marginBottom,
|
|
|
|
|
data.marginLeft,
|
|
|
|
|
data.marginRight,
|
|
|
|
|
JSON.stringify(data.components),
|
|
|
|
|
userId,
|
|
|
|
|
reportId,
|
|
|
|
|
]);
|
|
|
|
|
} else {
|
|
|
|
|
// 생성
|
|
|
|
|
const layoutId = `LAY_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
|
|
|
|
|
const insertQuery = `
|
|
|
|
|
INSERT INTO report_layout (
|
|
|
|
|
layout_id,
|
|
|
|
|
report_id,
|
|
|
|
|
canvas_width,
|
|
|
|
|
canvas_height,
|
|
|
|
|
page_orientation,
|
|
|
|
|
margin_top,
|
|
|
|
|
margin_bottom,
|
|
|
|
|
margin_left,
|
|
|
|
|
margin_right,
|
|
|
|
|
components,
|
|
|
|
|
created_by
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await client.query(insertQuery, [
|
|
|
|
|
layoutId,
|
|
|
|
|
reportId,
|
|
|
|
|
data.canvasWidth,
|
|
|
|
|
data.canvasHeight,
|
|
|
|
|
data.pageOrientation,
|
|
|
|
|
data.marginTop,
|
|
|
|
|
data.marginBottom,
|
|
|
|
|
data.marginLeft,
|
|
|
|
|
data.marginRight,
|
|
|
|
|
JSON.stringify(data.components),
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 13:53:45 +09:00
|
|
|
// 2. 쿼리 저장 (있는 경우)
|
|
|
|
|
if (data.queries && data.queries.length > 0) {
|
|
|
|
|
// 기존 쿼리 모두 삭제
|
|
|
|
|
await client.query(`DELETE FROM report_query WHERE report_id = $1`, [
|
|
|
|
|
reportId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 새 쿼리 삽입
|
|
|
|
|
const insertQuerySql = `
|
|
|
|
|
INSERT INTO report_query (
|
|
|
|
|
query_id,
|
|
|
|
|
report_id,
|
|
|
|
|
query_name,
|
|
|
|
|
query_type,
|
|
|
|
|
sql_query,
|
|
|
|
|
parameters,
|
2025-10-01 14:36:46 +09:00
|
|
|
external_connection_id,
|
2025-10-01 13:53:45 +09:00
|
|
|
display_order,
|
|
|
|
|
created_by
|
2025-10-01 14:36:46 +09:00
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
2025-10-01 13:53:45 +09:00
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < data.queries.length; i++) {
|
|
|
|
|
const q = data.queries[i];
|
|
|
|
|
await client.query(insertQuerySql, [
|
|
|
|
|
q.id,
|
|
|
|
|
reportId,
|
|
|
|
|
q.name,
|
|
|
|
|
q.type,
|
|
|
|
|
q.sqlQuery,
|
|
|
|
|
JSON.stringify(q.parameters),
|
2025-10-01 14:36:46 +09:00
|
|
|
(q as any).externalConnectionId || null, // 외부 DB 연결 ID
|
2025-10-01 13:53:45 +09:00
|
|
|
i,
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 11:34:17 +09:00
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 13:53:45 +09:00
|
|
|
/**
|
2025-10-01 14:36:46 +09:00
|
|
|
* 쿼리 실행 (내부 DB 또는 외부 DB)
|
2025-10-01 13:53:45 +09:00
|
|
|
*/
|
|
|
|
|
async executeQuery(
|
|
|
|
|
reportId: string,
|
|
|
|
|
queryId: string,
|
|
|
|
|
parameters: Record<string, any>,
|
2025-10-01 14:36:46 +09:00
|
|
|
sqlQuery?: string,
|
|
|
|
|
externalConnectionId?: number | null
|
2025-10-01 13:53:45 +09:00
|
|
|
): Promise<{ fields: string[]; rows: any[] }> {
|
|
|
|
|
let sql_query: string;
|
|
|
|
|
let queryParameters: string[] = [];
|
2025-10-01 14:36:46 +09:00
|
|
|
let connectionId: number | null = externalConnectionId ?? null;
|
2025-10-01 13:53:45 +09:00
|
|
|
|
|
|
|
|
// 테스트 모드 (sqlQuery 직접 전달)
|
|
|
|
|
if (sqlQuery) {
|
|
|
|
|
sql_query = sqlQuery;
|
2025-10-01 14:36:46 +09:00
|
|
|
// 파라미터 순서 추출 (등장 순서대로)
|
2025-10-01 13:53:45 +09:00
|
|
|
const matches = sqlQuery.match(/\$\d+/g);
|
|
|
|
|
if (matches) {
|
2025-10-01 14:36:46 +09:00
|
|
|
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;
|
2025-10-01 13:53:45 +09:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// DB에서 쿼리 조회
|
|
|
|
|
const queryResult = await queryOne<ReportQuery>(
|
|
|
|
|
`SELECT * FROM report_query WHERE query_id = $1 AND report_id = $2`,
|
|
|
|
|
[queryId, reportId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!queryResult) {
|
|
|
|
|
throw new Error("쿼리를 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sql_query = queryResult.sql_query;
|
|
|
|
|
queryParameters = Array.isArray(queryResult.parameters)
|
|
|
|
|
? queryResult.parameters
|
|
|
|
|
: [];
|
2025-10-01 14:36:46 +09:00
|
|
|
connectionId = queryResult.external_connection_id;
|
2025-10-01 13:53:45 +09:00
|
|
|
}
|
|
|
|
|
|
2025-10-02 09:56:44 +09:00
|
|
|
// SQL 쿼리 안전성 검증 (SELECT만 허용)
|
|
|
|
|
this.validateQuerySafety(sql_query);
|
|
|
|
|
|
2025-10-01 13:53:45 +09:00
|
|
|
// 파라미터 배열 생성 ($1, $2 순서대로)
|
|
|
|
|
const paramArray: any[] = [];
|
|
|
|
|
for (const param of queryParameters) {
|
|
|
|
|
paramArray.push(parameters[param] || null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-10-01 14:36:46 +09:00
|
|
|
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);
|
|
|
|
|
}
|
2025-10-01 13:53:45 +09:00
|
|
|
|
|
|
|
|
// 필드명 추출
|
|
|
|
|
const fields = result.length > 0 ? Object.keys(result[0]) : [];
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
fields,
|
|
|
|
|
rows: result,
|
|
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
throw new Error(`쿼리 실행 오류: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 11:34:17 +09:00
|
|
|
/**
|
|
|
|
|
* 템플릿 목록 조회
|
|
|
|
|
*/
|
|
|
|
|
async getTemplates(): Promise<GetTemplatesResponse> {
|
|
|
|
|
const templateQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
template_id,
|
|
|
|
|
template_name_kor,
|
|
|
|
|
template_name_eng,
|
|
|
|
|
template_type,
|
|
|
|
|
is_system,
|
|
|
|
|
thumbnail_url,
|
|
|
|
|
description,
|
|
|
|
|
layout_config,
|
|
|
|
|
default_queries,
|
|
|
|
|
use_yn,
|
|
|
|
|
sort_order,
|
|
|
|
|
created_at,
|
|
|
|
|
created_by,
|
|
|
|
|
updated_at,
|
|
|
|
|
updated_by
|
|
|
|
|
FROM report_template
|
|
|
|
|
WHERE use_yn = 'Y'
|
|
|
|
|
ORDER BY is_system DESC, sort_order ASC
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const templates = await query<ReportTemplate>(templateQuery);
|
|
|
|
|
|
|
|
|
|
const system = templates.filter((t) => t.is_system === "Y");
|
|
|
|
|
const custom = templates.filter((t) => t.is_system === "N");
|
|
|
|
|
|
|
|
|
|
return { system, custom };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 템플릿 생성 (사용자 정의)
|
|
|
|
|
*/
|
|
|
|
|
async createTemplate(
|
|
|
|
|
data: CreateTemplateRequest,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const templateId = `TPL_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
|
|
|
|
|
|
|
|
|
|
const insertQuery = `
|
|
|
|
|
INSERT INTO report_template (
|
|
|
|
|
template_id,
|
|
|
|
|
template_name_kor,
|
|
|
|
|
template_name_eng,
|
|
|
|
|
template_type,
|
|
|
|
|
is_system,
|
|
|
|
|
description,
|
|
|
|
|
layout_config,
|
|
|
|
|
default_queries,
|
|
|
|
|
use_yn,
|
|
|
|
|
created_by
|
|
|
|
|
) VALUES ($1, $2, $3, $4, 'N', $5, $6, $7, 'Y', $8)
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await query(insertQuery, [
|
|
|
|
|
templateId,
|
|
|
|
|
data.templateNameKor,
|
|
|
|
|
data.templateNameEng || null,
|
|
|
|
|
data.templateType,
|
|
|
|
|
data.description || null,
|
|
|
|
|
data.layoutConfig ? JSON.stringify(data.layoutConfig) : null,
|
|
|
|
|
data.defaultQueries ? JSON.stringify(data.defaultQueries) : null,
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return templateId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 템플릿 삭제 (사용자 정의만 가능)
|
|
|
|
|
*/
|
|
|
|
|
async deleteTemplate(templateId: string): Promise<boolean> {
|
|
|
|
|
const deleteQuery = `
|
|
|
|
|
DELETE FROM report_template
|
|
|
|
|
WHERE template_id = $1 AND is_system = 'N'
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = await query(deleteQuery, [templateId]);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-10-01 15:03:52 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 현재 리포트를 템플릿으로 저장
|
|
|
|
|
*/
|
|
|
|
|
async saveAsTemplate(
|
|
|
|
|
reportId: string,
|
|
|
|
|
templateNameKor: string,
|
|
|
|
|
templateNameEng: string | null | undefined,
|
|
|
|
|
description: string | null | undefined,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
return transaction(async (client) => {
|
|
|
|
|
// 리포트 정보 조회
|
|
|
|
|
const reportQuery = `
|
|
|
|
|
SELECT report_type FROM report_master WHERE report_id = $1
|
|
|
|
|
`;
|
|
|
|
|
const reportResult = await client.query(reportQuery, [reportId]);
|
|
|
|
|
|
|
|
|
|
if (reportResult.rows.length === 0) {
|
|
|
|
|
throw new Error("리포트를 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reportType = reportResult.rows[0].report_type;
|
|
|
|
|
|
|
|
|
|
// 레이아웃 조회
|
|
|
|
|
const layoutQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
canvas_width,
|
|
|
|
|
canvas_height,
|
|
|
|
|
page_orientation,
|
|
|
|
|
margin_top,
|
|
|
|
|
margin_bottom,
|
|
|
|
|
margin_left,
|
|
|
|
|
margin_right,
|
|
|
|
|
components
|
|
|
|
|
FROM report_layout
|
|
|
|
|
WHERE report_id = $1
|
|
|
|
|
`;
|
|
|
|
|
const layoutResult = await client.query(layoutQuery, [reportId]);
|
|
|
|
|
|
|
|
|
|
if (layoutResult.rows.length === 0) {
|
|
|
|
|
throw new Error("레이아웃을 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const layout = layoutResult.rows[0];
|
|
|
|
|
|
|
|
|
|
// 쿼리 조회
|
|
|
|
|
const queriesQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
query_name,
|
|
|
|
|
query_type,
|
|
|
|
|
sql_query,
|
|
|
|
|
parameters,
|
|
|
|
|
external_connection_id,
|
|
|
|
|
display_order
|
|
|
|
|
FROM report_query
|
|
|
|
|
WHERE report_id = $1
|
|
|
|
|
ORDER BY display_order
|
|
|
|
|
`;
|
|
|
|
|
const queriesResult = await client.query(queriesQuery, [reportId]);
|
|
|
|
|
|
|
|
|
|
// 레이아웃 설정 JSON 생성
|
|
|
|
|
const layoutConfig = {
|
|
|
|
|
width: layout.canvas_width,
|
|
|
|
|
height: layout.canvas_height,
|
|
|
|
|
orientation: layout.page_orientation,
|
|
|
|
|
margins: {
|
|
|
|
|
top: layout.margin_top,
|
|
|
|
|
bottom: layout.margin_bottom,
|
|
|
|
|
left: layout.margin_left,
|
|
|
|
|
right: layout.margin_right,
|
|
|
|
|
},
|
|
|
|
|
components: JSON.parse(layout.components || "[]"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 기본 쿼리 JSON 생성
|
|
|
|
|
const defaultQueries = queriesResult.rows.map((q) => ({
|
|
|
|
|
name: q.query_name,
|
|
|
|
|
type: q.query_type,
|
|
|
|
|
sqlQuery: q.sql_query,
|
|
|
|
|
parameters: Array.isArray(q.parameters) ? q.parameters : [],
|
|
|
|
|
externalConnectionId: q.external_connection_id,
|
|
|
|
|
displayOrder: q.display_order,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 템플릿 생성
|
|
|
|
|
const templateId = `TPL_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
|
|
|
|
|
|
|
|
|
|
const insertQuery = `
|
|
|
|
|
INSERT INTO report_template (
|
|
|
|
|
template_id,
|
|
|
|
|
template_name_kor,
|
|
|
|
|
template_name_eng,
|
|
|
|
|
template_type,
|
|
|
|
|
is_system,
|
|
|
|
|
description,
|
|
|
|
|
layout_config,
|
|
|
|
|
default_queries,
|
|
|
|
|
use_yn,
|
|
|
|
|
sort_order,
|
|
|
|
|
created_by
|
|
|
|
|
) VALUES ($1, $2, $3, $4, 'N', $5, $6, $7, 'Y', 999, $8)
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await client.query(insertQuery, [
|
|
|
|
|
templateId,
|
|
|
|
|
templateNameKor,
|
|
|
|
|
templateNameEng || null,
|
|
|
|
|
reportType,
|
|
|
|
|
description || null,
|
|
|
|
|
JSON.stringify(layoutConfig),
|
|
|
|
|
JSON.stringify(defaultQueries),
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return templateId;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 레이아웃 데이터로 직접 템플릿 생성 (리포트 저장 불필요)
|
|
|
|
|
async createTemplateFromLayout(
|
|
|
|
|
templateNameKor: string,
|
|
|
|
|
templateNameEng: string | null | undefined,
|
|
|
|
|
templateType: string,
|
|
|
|
|
description: string | null | undefined,
|
|
|
|
|
layoutConfig: {
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
orientation: string;
|
|
|
|
|
margins: {
|
|
|
|
|
top: number;
|
|
|
|
|
bottom: number;
|
|
|
|
|
left: number;
|
|
|
|
|
right: number;
|
|
|
|
|
};
|
|
|
|
|
components: any[];
|
|
|
|
|
},
|
|
|
|
|
defaultQueries: Array<{
|
|
|
|
|
name: string;
|
|
|
|
|
type: "MASTER" | "DETAIL";
|
|
|
|
|
sqlQuery: string;
|
|
|
|
|
parameters: string[];
|
|
|
|
|
externalConnectionId?: number | null;
|
|
|
|
|
displayOrder?: number;
|
|
|
|
|
}>,
|
|
|
|
|
userId: string
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const templateId = `TPL_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
|
|
|
|
|
|
|
|
|
|
const insertQuery = `
|
|
|
|
|
INSERT INTO report_template (
|
|
|
|
|
template_id,
|
|
|
|
|
template_name_kor,
|
|
|
|
|
template_name_eng,
|
|
|
|
|
template_type,
|
|
|
|
|
is_system,
|
|
|
|
|
description,
|
|
|
|
|
layout_config,
|
|
|
|
|
default_queries,
|
|
|
|
|
use_yn,
|
|
|
|
|
sort_order,
|
|
|
|
|
created_by
|
|
|
|
|
) VALUES ($1, $2, $3, $4, 'N', $5, $6, $7, 'Y', 999, $8)
|
|
|
|
|
RETURNING template_id
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await query(insertQuery, [
|
|
|
|
|
templateId,
|
|
|
|
|
templateNameKor,
|
|
|
|
|
templateNameEng || null,
|
|
|
|
|
templateType,
|
|
|
|
|
description || null,
|
|
|
|
|
JSON.stringify(layoutConfig),
|
|
|
|
|
JSON.stringify(defaultQueries),
|
|
|
|
|
userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return templateId;
|
|
|
|
|
}
|
2025-10-01 11:34:17 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default new ReportService();
|