/** * 리포트 관리 컨트롤러 */ 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();