From bd72f7892b5eebe094ebc2e86a464cb2f7ac7ecd Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 1 Oct 2025 11:34:17 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/app.ts | 2 + .../src/controllers/reportController.ts | 333 ++++++++++ backend-node/src/routes/reportRoutes.ts | 60 ++ backend-node/src/services/reportService.ts | 610 ++++++++++++++++++ backend-node/src/types/report.ts | 129 ++++ 5 files changed, 1134 insertions(+) create mode 100644 backend-node/src/controllers/reportController.ts create mode 100644 backend-node/src/routes/reportRoutes.ts create mode 100644 backend-node/src/services/reportService.ts create mode 100644 backend-node/src/types/report.ts diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 3c8974d4..ba54ab36 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -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); diff --git a/backend-node/src/controllers/reportController.ts b/backend-node/src/controllers/reportController.ts new file mode 100644 index 00000000..6a12df95 --- /dev/null +++ b/backend-node/src/controllers/reportController.ts @@ -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(); + diff --git a/backend-node/src/routes/reportRoutes.ts b/backend-node/src/routes/reportRoutes.ts new file mode 100644 index 00000000..aa4bcb29 --- /dev/null +++ b/backend-node/src/routes/reportRoutes.ts @@ -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; + diff --git a/backend-node/src/services/reportService.ts b/backend-node/src/services/reportService.ts new file mode 100644 index 00000000..f6bf72d3 --- /dev/null +++ b/backend-node/src/services/reportService.ts @@ -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 { + 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(listQuery, [ + ...values, + limit, + offset, + ]); + + return { + items, + total, + page, + limit, + }; + } + + /** + * 리포트 상세 조회 + */ + async getReportById(reportId: string): Promise { + // 리포트 마스터 조회 + 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(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(layoutQuery, [reportId]); + + return { + report, + layout, + }; + } + + /** + * 리포트 생성 + */ + async createReport( + data: CreateReportRequest, + userId: string + ): Promise { + 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 { + 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 { + 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 { + 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 { + 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(layoutQuery, [reportId]); + } + + /** + * 레이아웃 저장 + */ + async saveLayout( + reportId: string, + data: SaveLayoutRequest, + userId: string + ): Promise { + 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 { + 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(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 { + 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 { + 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(); diff --git a/backend-node/src/types/report.ts b/backend-node/src/types/report.ts new file mode 100644 index 00000000..ab471298 --- /dev/null +++ b/backend-node/src/types/report.ts @@ -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; +} +