ERP-node/backend-node/src/services/reportService.ts

1133 lines
30 KiB
TypeScript

/**
* 리포트 관리 서비스
*/
import { v4 as uuidv4 } from "uuid";
import { query, queryOne, transaction } from "../database/db";
import {
ReportMaster,
ReportLayout,
ReportQuery,
ReportTemplate,
ReportDetail,
GetReportsParams,
GetReportsResponse,
CreateReportRequest,
UpdateReportRequest,
SaveLayoutRequest,
GetTemplatesResponse,
CreateTemplateRequest,
} from "../types/report";
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
import { ExternalDbConnectionService } from "./externalDbConnectionService";
export class ReportService {
/**
* 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(
"보안상의 이유로 여러 개의 쿼리를 동시에 실행할 수 없습니다."
);
}
}
/**
* 리포트 목록 조회
*/
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]);
// 쿼리 조회
const queriesQuery = `
SELECT
query_id,
report_id,
query_name,
query_type,
sql_query,
parameters,
external_connection_id,
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]);
// 메뉴 매핑 조회
const menuMappingQuery = `
SELECT menu_objid
FROM report_menu_mapping
WHERE report_id = $1
ORDER BY created_at
`;
const menuMappings = await query<{ menu_objid: number }>(menuMappingQuery, [
reportId,
]);
const menuObjids = menuMappings?.map((m) => Number(m.menu_objid)) || [];
return {
report,
layout,
queries: queries || [],
menuObjids,
};
}
/**
* 리포트 생성
*/
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) => {
// 쿼리 삭제 (CASCADE로 자동 삭제되지만 명시적으로)
await client.query(`DELETE FROM report_query WHERE report_id = $1`, [
reportId,
]);
// 레이아웃 삭제
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)
`;
// components가 이미 문자열이면 그대로, 객체면 JSON.stringify
const componentsData =
typeof originalLayout.components === "string"
? originalLayout.components
: JSON.stringify(originalLayout.components);
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,
componentsData,
userId,
]);
}
// 쿼리 복사
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,
external_connection_id,
display_order,
created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`;
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,
JSON.stringify(originalQuery.parameters),
originalQuery.external_connection_id || null,
originalQuery.display_order,
userId,
]);
}
}
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]);
}
/**
* 레이아웃 저장 (쿼리 포함) - 페이지 기반 구조
*/
async saveLayout(
reportId: string,
data: SaveLayoutRequest,
userId: string
): Promise<boolean> {
return transaction(async (client) => {
// 첫 번째 페이지 정보를 기본 레이아웃으로 사용
const firstPage = data.layoutConfig.pages[0];
const canvasWidth = firstPage?.width || 210;
const canvasHeight = firstPage?.height || 297;
const pageOrientation =
canvasWidth > canvasHeight ? "landscape" : "portrait";
const margins = firstPage?.margins || {
top: 20,
bottom: 20,
left: 20,
right: 20,
};
// 1. 레이아웃 저장
const existingQuery = `
SELECT layout_id FROM report_layout WHERE report_id = $1
`;
const existing = await client.query(existingQuery, [reportId]);
if (existing.rows.length > 0) {
// 업데이트 - components 컬럼에 전체 layoutConfig 저장
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, [
canvasWidth,
canvasHeight,
pageOrientation,
margins.top,
margins.bottom,
margins.left,
margins.right,
JSON.stringify(data.layoutConfig), // 전체 layoutConfig 저장
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,
canvasWidth,
canvasHeight,
pageOrientation,
margins.top,
margins.bottom,
margins.left,
margins.right,
JSON.stringify(data.layoutConfig), // 전체 layoutConfig 저장
userId,
]);
}
// 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,
external_connection_id,
display_order,
created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`;
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),
(q as any).externalConnectionId || null, // 외부 DB 연결 ID
i,
userId,
]);
}
}
// 3. 메뉴 매핑 저장 (있는 경우)
if (data.menuObjids !== undefined) {
// 기존 메뉴 매핑 모두 삭제
await client.query(
`DELETE FROM report_menu_mapping WHERE report_id = $1`,
[reportId]
);
// 새 메뉴 매핑 삽입
if (data.menuObjids.length > 0) {
// 리포트의 company_code 조회
const reportResult = await client.query(
`SELECT company_code FROM report_master WHERE report_id = $1`,
[reportId]
);
const companyCode = reportResult.rows[0]?.company_code || "*";
const insertMappingSql = `
INSERT INTO report_menu_mapping (
report_id,
menu_objid,
company_code,
created_by
) VALUES ($1, $2, $3, $4)
`;
for (const menuObjid of data.menuObjids) {
await client.query(insertMappingSql, [
reportId,
menuObjid,
companyCode,
userId,
]);
}
}
}
return true;
});
}
/**
* 쿼리 실행 (내부 DB 또는 외부 DB)
*/
async executeQuery(
reportId: string,
queryId: string,
parameters: Record<string, any>,
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) {
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에서 쿼리 조회
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
: [];
connectionId = queryResult.external_connection_id;
}
// SQL 쿼리 안전성 검증 (SELECT만 허용)
this.validateQuerySafety(sql_query);
// 파라미터 배열 생성 ($1, $2 순서대로)
const paramArray: any[] = [];
for (const param of queryParameters) {
paramArray.push(parameters[param] || null);
}
try {
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]) : [];
return {
fields,
rows: result,
};
} catch (error: any) {
throw new Error(`쿼리 실행 오류: ${error.message}`);
}
}
/**
* 템플릿 목록 조회
*/
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;
}
/**
* 현재 리포트를 템플릿으로 저장
*/
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;
}
}
export default new ReportService();