리포트 템플릿 저장 구현

This commit is contained in:
dohyeons
2025-10-01 15:03:52 +09:00
parent 2ee4dd0b58
commit 62d36abb65
9 changed files with 852 additions and 104 deletions

View File

@@ -302,6 +302,101 @@ export class ReportController {
}
}
/**
* 현재 리포트를 템플릿으로 저장
* POST /api/admin/reports/:reportId/save-as-template
*/
async saveAsTemplate(req: Request, res: Response, next: NextFunction) {
try {
const { reportId } = req.params;
const { templateNameKor, templateNameEng, description } = req.body;
const userId = (req as any).user?.userId || "SYSTEM";
// 필수 필드 검증
if (!templateNameKor) {
return res.status(400).json({
success: false,
message: "템플릿명은 필수입니다.",
});
}
const templateId = await reportService.saveAsTemplate(
reportId,
templateNameKor,
templateNameEng,
description,
userId
);
return res.status(201).json({
success: true,
data: {
templateId,
},
message: "템플릿이 저장되었습니다.",
});
} catch (error) {
return next(error);
}
}
/**
* 레이아웃 데이터로 직접 템플릿 생성 (리포트 저장 불필요)
* POST /api/admin/reports/templates/create-from-layout
*/
async createTemplateFromLayout(
req: Request,
res: Response,
next: NextFunction
) {
try {
const {
templateNameKor,
templateNameEng,
templateType,
description,
layoutConfig,
defaultQueries = [],
} = req.body;
const userId = (req as any).user?.userId || "SYSTEM";
// 필수 필드 검증
if (!templateNameKor) {
return res.status(400).json({
success: false,
message: "템플릿명은 필수입니다.",
});
}
if (!layoutConfig) {
return res.status(400).json({
success: false,
message: "레이아웃 설정은 필수입니다.",
});
}
const templateId = await reportService.createTemplateFromLayout(
templateNameKor,
templateNameEng,
templateType || "GENERAL",
description,
layoutConfig,
defaultQueries,
userId
);
return res.status(201).json({
success: true,
data: {
templateId,
},
message: "템플릿이 생성되었습니다.",
});
} catch (error) {
return next(error);
}
}
/**
* 템플릿 삭제
* DELETE /api/admin/reports/templates/:templateId

View File

@@ -19,6 +19,10 @@ router.get("/templates", (req, res, next) =>
router.post("/templates", (req, res, next) =>
reportController.createTemplate(req, res, next)
);
// 레이아웃 데이터로 직접 템플릿 생성 (리포트 저장 불필요)
router.post("/templates/create-from-layout", (req, res, next) =>
reportController.createTemplateFromLayout(req, res, next)
);
router.delete("/templates/:templateId", (req, res, next) =>
reportController.deleteTemplate(req, res, next)
);
@@ -38,6 +42,11 @@ router.post("/:reportId/copy", (req, res, next) =>
reportController.copyReport(req, res, next)
);
// 템플릿으로 저장
router.post("/:reportId/save-as-template", (req, res, next) =>
reportController.saveAsTemplate(req, res, next)
);
// 레이아웃 관련 라우트
router.get("/:reportId/layout", (req, res, next) =>
reportController.getLayout(req, res, next)

View File

@@ -821,6 +821,185 @@ export class ReportService {
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();