540 lines
13 KiB
TypeScript
540 lines
13 KiB
TypeScript
/**
|
|
* 리포트 관리 컨트롤러
|
|
*/
|
|
|
|
import { Request, Response, NextFunction } from "express";
|
|
import reportService from "../services/reportService";
|
|
import {
|
|
CreateReportRequest,
|
|
UpdateReportRequest,
|
|
SaveLayoutRequest,
|
|
CreateTemplateRequest,
|
|
} from "../types/report";
|
|
import path from "path";
|
|
import fs from "fs";
|
|
|
|
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",
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: result,
|
|
});
|
|
} catch (error) {
|
|
return 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: "리포트를 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: report,
|
|
});
|
|
} catch (error) {
|
|
return 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);
|
|
|
|
return res.status(201).json({
|
|
success: true,
|
|
data: {
|
|
reportId,
|
|
},
|
|
message: "리포트가 생성되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
return 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: "수정할 내용이 없습니다.",
|
|
});
|
|
}
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: "리포트가 수정되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
return 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: "리포트를 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: "리포트가 삭제되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
return 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: "리포트를 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
return res.status(201).json({
|
|
success: true,
|
|
data: {
|
|
reportId: newReportId,
|
|
},
|
|
message: "리포트가 복사되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
return 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) : [],
|
|
};
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: layoutData,
|
|
});
|
|
} catch (error) {
|
|
return 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);
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: "레이아웃이 저장되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 템플릿 목록 조회
|
|
* GET /api/admin/reports/templates
|
|
*/
|
|
async getTemplates(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const templates = await reportService.getTemplates();
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: templates,
|
|
});
|
|
} catch (error) {
|
|
return 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);
|
|
|
|
return res.status(201).json({
|
|
success: true,
|
|
data: {
|
|
templateId,
|
|
},
|
|
message: "템플릿이 생성되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 현재 리포트를 템플릿으로 저장
|
|
* 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
|
|
*/
|
|
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: "템플릿을 찾을 수 없거나 시스템 템플릿입니다.",
|
|
});
|
|
}
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: "템플릿이 삭제되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 쿼리 실행
|
|
* POST /api/admin/reports/:reportId/queries/:queryId/execute
|
|
*/
|
|
async executeQuery(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const { reportId, queryId } = req.params;
|
|
const { parameters = {}, sqlQuery, externalConnectionId } = req.body;
|
|
|
|
const result = await reportService.executeQuery(
|
|
reportId,
|
|
queryId,
|
|
parameters,
|
|
sqlQuery,
|
|
externalConnectionId
|
|
);
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: result,
|
|
});
|
|
} catch (error: any) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: error.message || "쿼리 실행에 실패했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 외부 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 이미지 파일 업로드
|
|
* POST /api/admin/reports/upload-image
|
|
*/
|
|
async uploadImage(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "이미지 파일이 필요합니다.",
|
|
});
|
|
}
|
|
|
|
const companyCode = req.body.companyCode || "SYSTEM";
|
|
const file = req.file;
|
|
|
|
// 파일 저장 경로 생성
|
|
const uploadDir = path.join(
|
|
process.cwd(),
|
|
"uploads",
|
|
`company_${companyCode}`,
|
|
"reports"
|
|
);
|
|
|
|
// 디렉토리가 없으면 생성
|
|
if (!fs.existsSync(uploadDir)) {
|
|
fs.mkdirSync(uploadDir, { recursive: true });
|
|
}
|
|
|
|
// 고유한 파일명 생성 (타임스탬프 + 원본 파일명)
|
|
const timestamp = Date.now();
|
|
const safeFileName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
const fileName = `${timestamp}_${safeFileName}`;
|
|
const filePath = path.join(uploadDir, fileName);
|
|
|
|
// 파일 저장
|
|
fs.writeFileSync(filePath, file.buffer);
|
|
|
|
// 웹에서 접근 가능한 URL 반환
|
|
const fileUrl = `/uploads/company_${companyCode}/reports/${fileName}`;
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: {
|
|
fileName,
|
|
fileUrl,
|
|
originalName: file.originalname,
|
|
size: file.size,
|
|
mimeType: file.mimetype,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new ReportController();
|