백엔드 api 구현
This commit is contained in:
@@ -43,6 +43,7 @@ import entityReferenceRoutes from "./routes/entityReferenceRoutes";
|
||||
import externalCallRoutes from "./routes/externalCallRoutes";
|
||||
import externalCallConfigRoutes from "./routes/externalCallConfigRoutes";
|
||||
import dataflowExecutionRoutes from "./routes/dataflowExecutionRoutes";
|
||||
import reportRoutes from "./routes/reportRoutes";
|
||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
||||
@@ -171,6 +172,7 @@ app.use("/api/entity-reference", entityReferenceRoutes);
|
||||
app.use("/api/external-calls", externalCallRoutes);
|
||||
app.use("/api/external-call-configs", externalCallConfigRoutes);
|
||||
app.use("/api/dataflow", dataflowExecutionRoutes);
|
||||
app.use("/api/admin/reports", reportRoutes);
|
||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||
// app.use("/api/batch", batchRoutes); // 임시 주석
|
||||
// app.use('/api/users', userRoutes);
|
||||
|
||||
333
backend-node/src/controllers/reportController.ts
Normal file
333
backend-node/src/controllers/reportController.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* 리포트 관리 컨트롤러
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import reportService from "../services/reportService";
|
||||
import {
|
||||
CreateReportRequest,
|
||||
UpdateReportRequest,
|
||||
SaveLayoutRequest,
|
||||
CreateTemplateRequest,
|
||||
} from "../types/report";
|
||||
|
||||
export class ReportController {
|
||||
/**
|
||||
* 리포트 목록 조회
|
||||
* GET /api/admin/reports
|
||||
*/
|
||||
async getReports(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const {
|
||||
page = "1",
|
||||
limit = "20",
|
||||
searchText = "",
|
||||
reportType = "",
|
||||
useYn = "Y",
|
||||
sortBy = "created_at",
|
||||
sortOrder = "DESC",
|
||||
} = req.query;
|
||||
|
||||
const result = await reportService.getReports({
|
||||
page: parseInt(page as string, 10),
|
||||
limit: parseInt(limit as string, 10),
|
||||
searchText: searchText as string,
|
||||
reportType: reportType as string,
|
||||
useYn: useYn as string,
|
||||
sortBy: sortBy as string,
|
||||
sortOrder: sortOrder as "ASC" | "DESC",
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리포트 상세 조회
|
||||
* GET /api/admin/reports/:reportId
|
||||
*/
|
||||
async getReportById(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { reportId } = req.params;
|
||||
|
||||
const report = await reportService.getReportById(reportId);
|
||||
|
||||
if (!report) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "리포트를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: report,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리포트 생성
|
||||
* POST /api/admin/reports
|
||||
*/
|
||||
async createReport(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const data: CreateReportRequest = req.body;
|
||||
const userId = (req as any).user?.userId || "SYSTEM";
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!data.reportNameKor || !data.reportType) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "리포트명과 리포트 타입은 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const reportId = await reportService.createReport(data, userId);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
reportId,
|
||||
},
|
||||
message: "리포트가 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리포트 수정
|
||||
* PUT /api/admin/reports/:reportId
|
||||
*/
|
||||
async updateReport(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { reportId } = req.params;
|
||||
const data: UpdateReportRequest = req.body;
|
||||
const userId = (req as any).user?.userId || "SYSTEM";
|
||||
|
||||
const success = await reportService.updateReport(reportId, data, userId);
|
||||
|
||||
if (!success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "수정할 내용이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "리포트가 수정되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리포트 삭제
|
||||
* DELETE /api/admin/reports/:reportId
|
||||
*/
|
||||
async deleteReport(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { reportId } = req.params;
|
||||
|
||||
const success = await reportService.deleteReport(reportId);
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "리포트를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "리포트가 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리포트 복사
|
||||
* POST /api/admin/reports/:reportId/copy
|
||||
*/
|
||||
async copyReport(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { reportId } = req.params;
|
||||
const userId = (req as any).user?.userId || "SYSTEM";
|
||||
|
||||
const newReportId = await reportService.copyReport(reportId, userId);
|
||||
|
||||
if (!newReportId) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "리포트를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
reportId: newReportId,
|
||||
},
|
||||
message: "리포트가 복사되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 조회
|
||||
* GET /api/admin/reports/:reportId/layout
|
||||
*/
|
||||
async getLayout(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { reportId } = req.params;
|
||||
|
||||
const layout = await reportService.getLayout(reportId);
|
||||
|
||||
if (!layout) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "레이아웃을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// components JSON 파싱
|
||||
const layoutData = {
|
||||
...layout,
|
||||
components: layout.components ? JSON.parse(layout.components) : [],
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: layoutData,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 저장
|
||||
* PUT /api/admin/reports/:reportId/layout
|
||||
*/
|
||||
async saveLayout(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { reportId } = req.params;
|
||||
const data: SaveLayoutRequest = req.body;
|
||||
const userId = (req as any).user?.userId || "SYSTEM";
|
||||
|
||||
// 필수 필드 검증
|
||||
if (
|
||||
!data.canvasWidth ||
|
||||
!data.canvasHeight ||
|
||||
!data.pageOrientation ||
|
||||
!data.components
|
||||
) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 레이아웃 정보가 누락되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
await reportService.saveLayout(reportId, data, userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "레이아웃이 저장되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 목록 조회
|
||||
* GET /api/admin/reports/templates
|
||||
*/
|
||||
async getTemplates(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const templates = await reportService.getTemplates();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: templates,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 생성
|
||||
* POST /api/admin/reports/templates
|
||||
*/
|
||||
async createTemplate(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const data: CreateTemplateRequest = req.body;
|
||||
const userId = (req as any).user?.userId || "SYSTEM";
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!data.templateNameKor || !data.templateType) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "템플릿명과 템플릿 타입은 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const templateId = await reportService.createTemplate(data, userId);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
templateId,
|
||||
},
|
||||
message: "템플릿이 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 삭제
|
||||
* DELETE /api/admin/reports/templates/:templateId
|
||||
*/
|
||||
async deleteTemplate(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { templateId } = req.params;
|
||||
|
||||
const success = await reportService.deleteTemplate(templateId);
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "템플릿을 찾을 수 없거나 시스템 템플릿입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "템플릿이 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReportController();
|
||||
|
||||
60
backend-node/src/routes/reportRoutes.ts
Normal file
60
backend-node/src/routes/reportRoutes.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Router } from "express";
|
||||
import reportController from "../controllers/reportController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 모든 리포트 API는 인증이 필요
|
||||
router.use(authenticateToken);
|
||||
|
||||
// 템플릿 관련 라우트 (구체적인 경로를 먼저 배치)
|
||||
router.get("/templates", (req, res, next) =>
|
||||
reportController.getTemplates(req, res, next)
|
||||
);
|
||||
router.post("/templates", (req, res, next) =>
|
||||
reportController.createTemplate(req, res, next)
|
||||
);
|
||||
router.delete("/templates/:templateId", (req, res, next) =>
|
||||
reportController.deleteTemplate(req, res, next)
|
||||
);
|
||||
|
||||
// 리포트 목록
|
||||
router.get("/", (req, res, next) =>
|
||||
reportController.getReports(req, res, next)
|
||||
);
|
||||
|
||||
// 리포트 생성
|
||||
router.post("/", (req, res, next) =>
|
||||
reportController.createReport(req, res, next)
|
||||
);
|
||||
|
||||
// 리포트 복사 (구체적인 경로를 먼저 배치)
|
||||
router.post("/:reportId/copy", (req, res, next) =>
|
||||
reportController.copyReport(req, res, next)
|
||||
);
|
||||
|
||||
// 레이아웃 관련 라우트
|
||||
router.get("/:reportId/layout", (req, res, next) =>
|
||||
reportController.getLayout(req, res, next)
|
||||
);
|
||||
router.put("/:reportId/layout", (req, res, next) =>
|
||||
reportController.saveLayout(req, res, next)
|
||||
);
|
||||
|
||||
// 리포트 상세
|
||||
router.get("/:reportId", (req, res, next) =>
|
||||
reportController.getReportById(req, res, next)
|
||||
);
|
||||
|
||||
// 리포트 수정
|
||||
router.put("/:reportId", (req, res, next) =>
|
||||
reportController.updateReport(req, res, next)
|
||||
);
|
||||
|
||||
// 리포트 삭제
|
||||
router.delete("/:reportId", (req, res, next) =>
|
||||
reportController.deleteReport(req, res, next)
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
610
backend-node/src/services/reportService.ts
Normal file
610
backend-node/src/services/reportService.ts
Normal file
@@ -0,0 +1,610 @@
|
||||
/**
|
||||
* 리포트 관리 서비스
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
import {
|
||||
ReportMaster,
|
||||
ReportLayout,
|
||||
ReportTemplate,
|
||||
ReportDetail,
|
||||
GetReportsParams,
|
||||
GetReportsResponse,
|
||||
CreateReportRequest,
|
||||
UpdateReportRequest,
|
||||
SaveLayoutRequest,
|
||||
GetTemplatesResponse,
|
||||
CreateTemplateRequest,
|
||||
} from "../types/report";
|
||||
|
||||
export class ReportService {
|
||||
/**
|
||||
* 리포트 목록 조회
|
||||
*/
|
||||
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]);
|
||||
|
||||
return {
|
||||
report,
|
||||
layout,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 리포트 생성
|
||||
*/
|
||||
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_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,
|
||||
originalLayout.components,
|
||||
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 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,
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 목록 조회
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReportService();
|
||||
129
backend-node/src/types/report.ts
Normal file
129
backend-node/src/types/report.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 리포트 관리 시스템 타입 정의
|
||||
*/
|
||||
|
||||
// 리포트 템플릿
|
||||
export interface ReportTemplate {
|
||||
template_id: string;
|
||||
template_name_kor: string;
|
||||
template_name_eng: string | null;
|
||||
template_type: string;
|
||||
is_system: string;
|
||||
thumbnail_url: string | null;
|
||||
description: string | null;
|
||||
layout_config: string | null;
|
||||
default_queries: string | null;
|
||||
use_yn: string;
|
||||
sort_order: number;
|
||||
created_at: Date;
|
||||
created_by: string | null;
|
||||
updated_at: Date | null;
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
// 리포트 마스터
|
||||
export interface ReportMaster {
|
||||
report_id: string;
|
||||
report_name_kor: string;
|
||||
report_name_eng: string | null;
|
||||
template_id: string | null;
|
||||
report_type: string;
|
||||
company_code: string | null;
|
||||
description: string | null;
|
||||
use_yn: string;
|
||||
created_at: Date;
|
||||
created_by: string | null;
|
||||
updated_at: Date | null;
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
// 리포트 레이아웃
|
||||
export interface ReportLayout {
|
||||
layout_id: string;
|
||||
report_id: string;
|
||||
canvas_width: number;
|
||||
canvas_height: number;
|
||||
page_orientation: string;
|
||||
margin_top: number;
|
||||
margin_bottom: number;
|
||||
margin_left: number;
|
||||
margin_right: number;
|
||||
components: string | null;
|
||||
created_at: Date;
|
||||
created_by: string | null;
|
||||
updated_at: Date | null;
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
// 리포트 상세 (마스터 + 레이아웃)
|
||||
export interface ReportDetail {
|
||||
report: ReportMaster;
|
||||
layout: ReportLayout | null;
|
||||
}
|
||||
|
||||
// 리포트 목록 조회 파라미터
|
||||
export interface GetReportsParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
searchText?: string;
|
||||
reportType?: string;
|
||||
useYn?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
}
|
||||
|
||||
// 리포트 목록 응답
|
||||
export interface GetReportsResponse {
|
||||
items: ReportMaster[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
// 리포트 생성 요청
|
||||
export interface CreateReportRequest {
|
||||
reportNameKor: string;
|
||||
reportNameEng?: string;
|
||||
templateId?: string;
|
||||
reportType: string;
|
||||
description?: string;
|
||||
companyCode?: string;
|
||||
}
|
||||
|
||||
// 리포트 수정 요청
|
||||
export interface UpdateReportRequest {
|
||||
reportNameKor?: string;
|
||||
reportNameEng?: string;
|
||||
reportType?: string;
|
||||
description?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
// 레이아웃 저장 요청
|
||||
export interface SaveLayoutRequest {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
pageOrientation: string;
|
||||
marginTop: number;
|
||||
marginBottom: number;
|
||||
marginLeft: number;
|
||||
marginRight: number;
|
||||
components: any[];
|
||||
}
|
||||
|
||||
// 템플릿 목록 응답
|
||||
export interface GetTemplatesResponse {
|
||||
system: ReportTemplate[];
|
||||
custom: ReportTemplate[];
|
||||
}
|
||||
|
||||
// 템플릿 생성 요청
|
||||
export interface CreateTemplateRequest {
|
||||
templateNameKor: string;
|
||||
templateNameEng?: string;
|
||||
templateType: string;
|
||||
description?: string;
|
||||
layoutConfig?: any;
|
||||
defaultQueries?: any;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user