Merge branch 'kwshin-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
/**
|
||||
* 리포트 관리 컨트롤러
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { Response, NextFunction } from "express";
|
||||
import reportService from "../services/reportService";
|
||||
import {
|
||||
CreateReportRequest,
|
||||
UpdateReportRequest,
|
||||
SaveLayoutRequest,
|
||||
CreateTemplateRequest,
|
||||
GetReportsParams,
|
||||
} from "../types/report";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { logger } from "../utils/logger";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import {
|
||||
@@ -35,92 +34,91 @@ import {
|
||||
import { WatermarkConfig } from "../types/report";
|
||||
import bwipjs from "bwip-js";
|
||||
|
||||
function getUserInfo(req: AuthenticatedRequest) {
|
||||
return {
|
||||
userId: req.user?.userId || "SYSTEM",
|
||||
companyCode: req.user?.companyCode || "*",
|
||||
};
|
||||
}
|
||||
|
||||
export class ReportController {
|
||||
/**
|
||||
* 리포트 목록 조회
|
||||
* GET /api/admin/reports
|
||||
*/
|
||||
async getReports(req: Request, res: Response, next: NextFunction) {
|
||||
async getReports(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { companyCode } = getUserInfo(req);
|
||||
const {
|
||||
page = "1",
|
||||
limit = "20",
|
||||
searchText = "",
|
||||
reportType = "",
|
||||
useYn = "Y",
|
||||
sortBy = "created_at",
|
||||
sortOrder = "DESC",
|
||||
page = "1", limit = "20", searchText = "", searchField,
|
||||
startDate, endDate, 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,
|
||||
searchField: searchField as GetReportsParams["searchField"],
|
||||
startDate: startDate as string | undefined,
|
||||
endDate: endDate as string | undefined,
|
||||
reportType: reportType as string,
|
||||
useYn: useYn as string,
|
||||
sortBy: sortBy as string,
|
||||
sortOrder: sortOrder as "ASC" | "DESC",
|
||||
});
|
||||
}, companyCode);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
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) {
|
||||
async getReportById(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { companyCode } = getUserInfo(req);
|
||||
const { reportId } = req.params;
|
||||
|
||||
const report = await reportService.getReportById(reportId);
|
||||
const report = await reportService.getReportById(reportId, companyCode);
|
||||
|
||||
if (!report) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "리포트를 찾을 수 없습니다.",
|
||||
});
|
||||
return res.status(404).json({ success: false, message: "리포트를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: report,
|
||||
});
|
||||
return res.json({ success: true, data: report });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리포트 생성
|
||||
* POST /api/admin/reports
|
||||
*/
|
||||
async createReport(req: Request, res: Response, next: NextFunction) {
|
||||
async getReportsByMenuObjid(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const data: CreateReportRequest = req.body;
|
||||
const userId = (req as any).user?.userId || "SYSTEM";
|
||||
const { companyCode } = getUserInfo(req);
|
||||
const { menuObjid } = req.params;
|
||||
const menuObjidNum = parseInt(menuObjid, 10);
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!data.reportNameKor || !data.reportType) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "리포트명과 리포트 타입은 필수입니다.",
|
||||
});
|
||||
if (isNaN(menuObjidNum)) {
|
||||
return res.status(400).json({ success: false, message: "menuObjid는 숫자여야 합니다." });
|
||||
}
|
||||
|
||||
const result = await reportService.getReportsByMenuObjid(menuObjidNum, companyCode);
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async createReport(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { userId, companyCode } = getUserInfo(req);
|
||||
const data: CreateReportRequest = req.body;
|
||||
|
||||
if (!data.reportNameKor || !data.reportType) {
|
||||
return res.status(400).json({ success: false, message: "리포트명과 리포트 타입은 필수입니다." });
|
||||
}
|
||||
|
||||
data.companyCode = companyCode;
|
||||
const reportId = await reportService.createReport(data, userId);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
reportId,
|
||||
},
|
||||
data: { reportId },
|
||||
message: "리포트가 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -128,83 +126,56 @@ export class ReportController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리포트 수정
|
||||
* PUT /api/admin/reports/:reportId
|
||||
*/
|
||||
async updateReport(req: Request, res: Response, next: NextFunction) {
|
||||
async updateReport(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { userId, companyCode } = getUserInfo(req);
|
||||
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);
|
||||
const success = await reportService.updateReport(reportId, data, userId, companyCode);
|
||||
|
||||
if (!success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "수정할 내용이 없습니다.",
|
||||
});
|
||||
return res.status(400).json({ success: false, message: "수정할 내용이 없습니다." });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
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) {
|
||||
async deleteReport(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { companyCode } = getUserInfo(req);
|
||||
const { reportId } = req.params;
|
||||
|
||||
const success = await reportService.deleteReport(reportId);
|
||||
const success = await reportService.deleteReport(reportId, companyCode);
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "리포트를 찾을 수 없습니다.",
|
||||
});
|
||||
return res.status(404).json({ success: false, message: "리포트를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
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) {
|
||||
async copyReport(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { userId, companyCode } = getUserInfo(req);
|
||||
const { reportId } = req.params;
|
||||
const userId = (req as any).user?.userId || "SYSTEM";
|
||||
const { newName } = req.body;
|
||||
|
||||
const newReportId = await reportService.copyReport(reportId, userId);
|
||||
const newReportId = await reportService.copyReport(reportId, userId, companyCode, newName);
|
||||
|
||||
if (!newReportId) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "리포트를 찾을 수 없습니다.",
|
||||
});
|
||||
return res.status(404).json({ success: false, message: "리포트를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
reportId: newReportId,
|
||||
},
|
||||
data: { reportId: newReportId },
|
||||
message: "리포트가 복사되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -212,132 +183,92 @@ export class ReportController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 조회
|
||||
* GET /api/admin/reports/:reportId/layout
|
||||
*/
|
||||
async getLayout(req: Request, res: Response, next: NextFunction) {
|
||||
async getLayout(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { companyCode } = getUserInfo(req);
|
||||
const { reportId } = req.params;
|
||||
|
||||
const layout = await reportService.getLayout(reportId);
|
||||
const layout = await reportService.getLayout(reportId, companyCode);
|
||||
|
||||
if (!layout) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "레이아웃을 찾을 수 없습니다.",
|
||||
});
|
||||
return res.status(404).json({ success: false, message: "레이아웃을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
// components 컬럼에서 JSON 파싱
|
||||
const parsedComponents = layout.components
|
||||
? JSON.parse(layout.components)
|
||||
: null;
|
||||
|
||||
const storedData = layout.components;
|
||||
let layoutData;
|
||||
// 새 구조 (layoutConfig.pages)인지 확인
|
||||
|
||||
if (
|
||||
parsedComponents &&
|
||||
parsedComponents.pages &&
|
||||
Array.isArray(parsedComponents.pages)
|
||||
storedData &&
|
||||
typeof storedData === "object" &&
|
||||
!Array.isArray(storedData) &&
|
||||
Array.isArray((storedData as Record<string, unknown>).pages)
|
||||
) {
|
||||
// pages 배열을 직접 포함하여 반환
|
||||
const parsed = storedData as Record<string, unknown>;
|
||||
layoutData = {
|
||||
...layout,
|
||||
pages: parsedComponents.pages,
|
||||
components: [], // 호환성을 위해 빈 배열
|
||||
pages: parsed.pages,
|
||||
watermark: parsed.watermark,
|
||||
components: storedData,
|
||||
};
|
||||
} else {
|
||||
// 기존 구조: components 배열
|
||||
layoutData = {
|
||||
...layout,
|
||||
components: parsedComponents || [],
|
||||
};
|
||||
layoutData = { ...layout, components: storedData || [] };
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: layoutData,
|
||||
});
|
||||
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) {
|
||||
async saveLayout(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { userId, companyCode } = getUserInfo(req);
|
||||
const { reportId } = req.params;
|
||||
const data: SaveLayoutRequest = req.body;
|
||||
const userId = (req as any).user?.userId || "SYSTEM";
|
||||
|
||||
// 필수 필드 검증 (페이지 기반 구조)
|
||||
if (
|
||||
!data.layoutConfig ||
|
||||
!data.layoutConfig.pages ||
|
||||
data.layoutConfig.pages.length === 0
|
||||
) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "레이아웃 설정이 필요합니다.",
|
||||
});
|
||||
if (!data.layoutConfig?.pages?.length) {
|
||||
return res.status(400).json({ success: false, message: "레이아웃 설정이 필요합니다." });
|
||||
}
|
||||
|
||||
await reportService.saveLayout(reportId, data, userId);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "레이아웃이 저장되었습니다.",
|
||||
});
|
||||
await reportService.saveLayout(reportId, data, userId, companyCode);
|
||||
return res.json({ success: true, message: "레이아웃이 저장되었습니다." });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 목록 조회
|
||||
* GET /api/admin/reports/templates
|
||||
*/
|
||||
async getTemplates(req: Request, res: Response, next: NextFunction) {
|
||||
async getTemplates(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const templates = await reportService.getTemplates();
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: templates,
|
||||
});
|
||||
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) {
|
||||
async getCategories(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const data: CreateTemplateRequest = req.body;
|
||||
const userId = (req as any).user?.userId || "SYSTEM";
|
||||
const categories = await reportService.getCategories();
|
||||
return res.json({ success: true, data: categories });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async createTemplate(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { userId } = getUserInfo(req);
|
||||
const data: CreateTemplateRequest = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!data.templateNameKor || !data.templateType) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "템플릿명과 템플릿 타입은 필수입니다.",
|
||||
});
|
||||
return res.status(400).json({ success: false, message: "템플릿명과 템플릿 타입은 필수입니다." });
|
||||
}
|
||||
|
||||
const templateId = await reportService.createTemplate(data, userId);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
templateId,
|
||||
},
|
||||
data: { templateId },
|
||||
message: "템플릿이 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -345,37 +276,23 @@ export class ReportController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 리포트를 템플릿으로 저장
|
||||
* POST /api/admin/reports/:reportId/save-as-template
|
||||
*/
|
||||
async saveAsTemplate(req: Request, res: Response, next: NextFunction) {
|
||||
async saveAsTemplate(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { userId } = getUserInfo(req);
|
||||
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: "템플릿명은 필수입니다.",
|
||||
});
|
||||
return res.status(400).json({ success: false, message: "템플릿명은 필수입니다." });
|
||||
}
|
||||
|
||||
const templateId = await reportService.saveAsTemplate(
|
||||
reportId,
|
||||
templateNameKor,
|
||||
templateNameEng,
|
||||
description,
|
||||
userId
|
||||
reportId, templateNameKor, templateNameEng, description, userId
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
templateId,
|
||||
},
|
||||
data: { templateId },
|
||||
message: "템플릿이 저장되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -383,39 +300,20 @@ export class ReportController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 데이터로 직접 템플릿 생성 (리포트 저장 불필요)
|
||||
* POST /api/admin/reports/templates/create-from-layout
|
||||
*/
|
||||
async createTemplateFromLayout(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
async createTemplateFromLayout(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { userId } = getUserInfo(req);
|
||||
const {
|
||||
templateNameKor,
|
||||
templateNameEng,
|
||||
templateType,
|
||||
description,
|
||||
layoutConfig,
|
||||
defaultQueries = [],
|
||||
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: "템플릿명은 필수입니다.",
|
||||
});
|
||||
return res.status(400).json({ success: false, message: "템플릿명은 필수입니다." });
|
||||
}
|
||||
|
||||
if (!layoutConfig) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "레이아웃 설정은 필수입니다.",
|
||||
});
|
||||
return res.status(400).json({ success: false, message: "레이아웃 설정은 필수입니다." });
|
||||
}
|
||||
|
||||
const templateId = await reportService.createTemplateFromLayout(
|
||||
@@ -440,78 +338,47 @@ export class ReportController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 삭제
|
||||
* DELETE /api/admin/reports/templates/:templateId
|
||||
*/
|
||||
async deleteTemplate(req: Request, res: Response, next: NextFunction) {
|
||||
async deleteTemplate(req: AuthenticatedRequest, 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.status(404).json({ success: false, message: "템플릿을 찾을 수 없거나 시스템 템플릿입니다." });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
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) {
|
||||
async executeQuery(req: AuthenticatedRequest, 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
|
||||
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 || "쿼리 실행에 실패했습니다.",
|
||||
});
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "쿼리 실행에 실패했습니다.";
|
||||
return res.status(400).json({ success: false, message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 목록 조회 (활성화된 것만)
|
||||
* GET /api/admin/reports/external-connections
|
||||
*/
|
||||
async getExternalConnections(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
async getExternalConnections(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { companyCode } = getUserInfo(req);
|
||||
const { ExternalDbConnectionService } = await import(
|
||||
"../services/externalDbConnectionService"
|
||||
);
|
||||
|
||||
const result = await ExternalDbConnectionService.getConnections({
|
||||
is_active: "Y",
|
||||
company_code: req.body.companyCode || "",
|
||||
company_code: companyCode,
|
||||
});
|
||||
|
||||
return res.json(result);
|
||||
@@ -520,52 +387,34 @@ export class ReportController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 파일 업로드
|
||||
* POST /api/admin/reports/upload-image
|
||||
*/
|
||||
async uploadImage(req: Request, res: Response, next: NextFunction) {
|
||||
async uploadImage(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "이미지 파일이 필요합니다.",
|
||||
});
|
||||
return res.status(400).json({ success: false, message: "이미지 파일이 필요합니다." });
|
||||
}
|
||||
|
||||
const companyCode = req.body.companyCode || "SYSTEM";
|
||||
const { companyCode } = getUserInfo(req);
|
||||
const file = req.file;
|
||||
|
||||
// 파일 저장 경로 생성
|
||||
const uploadDir = path.join(
|
||||
process.cwd(),
|
||||
"uploads",
|
||||
`company_${companyCode}`,
|
||||
"reports"
|
||||
);
|
||||
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,
|
||||
fileName, fileUrl,
|
||||
originalName: file.originalname,
|
||||
size: file.size,
|
||||
mimeType: file.mimetype,
|
||||
@@ -576,11 +425,7 @@ export class ReportController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 데이터를 WORD(DOCX)로 변환
|
||||
* POST /api/admin/reports/export-word
|
||||
*/
|
||||
async exportToWord(req: Request, res: Response, next: NextFunction) {
|
||||
async exportToWord(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { layoutConfig, queryResults, fileName = "리포트" } = req.body;
|
||||
|
||||
@@ -591,22 +436,15 @@ export class ReportController {
|
||||
});
|
||||
}
|
||||
|
||||
// mm를 twip으로 변환
|
||||
const mmToTwip = (mm: number) => convertMillimetersToTwip(mm);
|
||||
|
||||
// 프론트엔드와 동일한 MM_TO_PX 상수 (캔버스에서 mm를 px로 변환할 때 사용하는 값)
|
||||
const MM_TO_PX = 4;
|
||||
// 1mm = 56.692913386 twip (docx 라이브러리 기준)
|
||||
// px를 twip으로 변환: px -> mm -> twip
|
||||
const MM_TO_PX = 4; // 프론트엔드와 동일, 1mm = 56.692913386 twip (docx)
|
||||
const pxToTwip = (px: number) => Math.round((px / MM_TO_PX) * 56.692913386);
|
||||
|
||||
// 쿼리 결과 맵
|
||||
const queryResultsMap: Record<
|
||||
string,
|
||||
{ fields: string[]; rows: Record<string, unknown>[] }
|
||||
> = queryResults || {};
|
||||
|
||||
// 컴포넌트 값 가져오기
|
||||
const getComponentValue = (component: any): string => {
|
||||
if (component.queryId && component.fieldName) {
|
||||
const queryResult = queryResultsMap[component.queryId];
|
||||
@@ -621,11 +459,9 @@ export class ReportController {
|
||||
return component.defaultValue || "";
|
||||
};
|
||||
|
||||
// px → half-point 변환 (1px = 0.75pt, Word는 half-pt 단위 사용)
|
||||
// px * 0.75 * 2 = px * 1.5
|
||||
// px → half-point (1px = 0.75pt, px * 1.5)
|
||||
const pxToHalfPt = (px: number) => Math.round(px * 1.5);
|
||||
|
||||
// 셀 내용 생성 헬퍼 함수 (가로 배치용)
|
||||
const createCellContent = (
|
||||
component: any,
|
||||
displayValue: string,
|
||||
@@ -1557,7 +1393,7 @@ export class ReportController {
|
||||
const base64 = png.toString("base64");
|
||||
return `data:image/png;base64,${base64}`;
|
||||
} catch (error) {
|
||||
console.error("바코드 생성 오류:", error);
|
||||
logger.error("바코드 생성 오류:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1891,7 +1727,7 @@ export class ReportController {
|
||||
children.push(paragraph);
|
||||
lastBottomY = adjustedY + component.height;
|
||||
} catch (imgError) {
|
||||
console.error("이미지 처리 오류:", imgError);
|
||||
logger.error("이미지 처리 오류:", imgError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2005,7 +1841,7 @@ export class ReportController {
|
||||
});
|
||||
children.push(paragraph);
|
||||
} catch (imgError) {
|
||||
console.error("서명 이미지 오류:", imgError);
|
||||
logger.error("서명 이미지 오류:", imgError);
|
||||
textRuns.push(
|
||||
new TextRun({
|
||||
text: "_".repeat(20),
|
||||
@@ -2083,7 +1919,7 @@ export class ReportController {
|
||||
});
|
||||
children.push(paragraph);
|
||||
} catch (imgError) {
|
||||
console.error("도장 이미지 오류:", imgError);
|
||||
logger.error("도장 이미지 오류:", imgError);
|
||||
textRuns.push(
|
||||
new TextRun({
|
||||
text: "(인)",
|
||||
@@ -2886,7 +2722,7 @@ export class ReportController {
|
||||
})
|
||||
);
|
||||
} catch (imgError) {
|
||||
console.error("바코드 이미지 오류:", imgError);
|
||||
logger.error("바코드 이미지 오류:", imgError);
|
||||
// 바코드 이미지 생성 실패 시 텍스트로 대체
|
||||
const barcodeValue = component.barcodeValue || "BARCODE";
|
||||
children.push(
|
||||
@@ -3164,13 +3000,57 @@ export class ReportController {
|
||||
|
||||
return res.send(docxBuffer);
|
||||
} catch (error: any) {
|
||||
console.error("WORD 변환 오류:", error);
|
||||
logger.error("WORD 변환 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "WORD 변환에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 비주얼 쿼리 빌더 API ─────────────────────────────────────────────────────
|
||||
|
||||
async getSchemaTables(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const tables = await reportService.getSchemaTables();
|
||||
return res.json({ success: true, data: tables });
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "테이블 목록 조회에 실패했습니다.";
|
||||
logger.error("스키마 테이블 조회 오류:", { error: message });
|
||||
return res.status(500).json({ success: false, message });
|
||||
}
|
||||
}
|
||||
|
||||
async getSchemaTableColumns(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
if (!tableName) {
|
||||
return res.status(400).json({ success: false, message: "테이블명이 필요합니다." });
|
||||
}
|
||||
const columns = await reportService.getSchemaTableColumns(tableName);
|
||||
return res.json({ success: true, data: columns });
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "컬럼 목록 조회에 실패했습니다.";
|
||||
logger.error("테이블 컬럼 조회 오류:", { error: message });
|
||||
return res.status(500).json({ success: false, message });
|
||||
}
|
||||
}
|
||||
|
||||
async previewVisualQuery(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { visualQuery } = req.body;
|
||||
if (!visualQuery || !visualQuery.tableName) {
|
||||
return res.status(400).json({ success: false, message: "visualQuery 정보가 필요합니다." });
|
||||
}
|
||||
const result = await reportService.executeVisualQuery(visualQuery);
|
||||
const generatedSql = reportService.buildVisualQuerySql(visualQuery);
|
||||
return res.json({ success: true, data: { ...result, sql: generatedSql } });
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "쿼리 실행에 실패했습니다.";
|
||||
logger.error("비주얼 쿼리 미리보기 오류:", { error: message });
|
||||
return res.status(500).json({ success: false, message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReportController();
|
||||
|
||||
@@ -43,6 +43,11 @@ router.get("/templates", (req, res, next) =>
|
||||
router.post("/templates", (req, res, next) =>
|
||||
reportController.createTemplate(req, res, next)
|
||||
);
|
||||
|
||||
// 카테고리(report_type) 목록 조회
|
||||
router.get("/categories", (req, res, next) =>
|
||||
reportController.getCategories(req, res, next)
|
||||
);
|
||||
// 레이아웃 데이터로 직접 템플릿 생성 (리포트 저장 불필요)
|
||||
router.post("/templates/create-from-layout", (req, res, next) =>
|
||||
reportController.createTemplateFromLayout(req, res, next)
|
||||
@@ -61,6 +66,17 @@ router.post("/export-word", (req, res, next) =>
|
||||
reportController.exportToWord(req, res, next)
|
||||
);
|
||||
|
||||
// 비주얼 쿼리 빌더 — 스키마 조회 (/:reportId 패턴보다 반드시 먼저 등록)
|
||||
router.get("/schema/tables", (req, res, next) =>
|
||||
reportController.getSchemaTables(req, res, next)
|
||||
);
|
||||
router.get("/schema/tables/:tableName/columns", (req, res, next) =>
|
||||
reportController.getSchemaTableColumns(req, res, next)
|
||||
);
|
||||
router.post("/schema/preview", (req, res, next) =>
|
||||
reportController.previewVisualQuery(req, res, next)
|
||||
);
|
||||
|
||||
// 리포트 목록
|
||||
router.get("/", (req, res, next) =>
|
||||
reportController.getReports(req, res, next)
|
||||
@@ -71,6 +87,11 @@ router.post("/", (req, res, next) =>
|
||||
reportController.createReport(req, res, next)
|
||||
);
|
||||
|
||||
// 메뉴별 리포트 목록 (/:reportId 보다 반드시 먼저 등록)
|
||||
router.get("/by-menu/:menuObjid", (req, res, next) =>
|
||||
reportController.getReportsByMenuObjid(req, res, next)
|
||||
);
|
||||
|
||||
// 리포트 복사 (구체적인 경로를 먼저 배치)
|
||||
router.post("/:reportId/copy", (req, res, next) =>
|
||||
reportController.copyReport(req, res, next)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* 리포트 관리 시스템 타입 정의
|
||||
*/
|
||||
|
||||
// 리포트 템플릿
|
||||
export interface ReportTemplate {
|
||||
template_id: string;
|
||||
template_name_kor: string;
|
||||
@@ -21,12 +16,12 @@ export interface ReportTemplate {
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
// 리포트 마스터
|
||||
export interface ReportMaster {
|
||||
report_id: string;
|
||||
report_name_kor: string;
|
||||
report_name_eng: string | null;
|
||||
template_id: string | null;
|
||||
template_name: string | null;
|
||||
report_type: string;
|
||||
company_code: string | null;
|
||||
description: string | null;
|
||||
@@ -37,7 +32,6 @@ export interface ReportMaster {
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
// 리포트 레이아웃
|
||||
export interface ReportLayout {
|
||||
layout_id: string;
|
||||
report_id: string;
|
||||
@@ -55,7 +49,6 @@ export interface ReportLayout {
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
// 리포트 쿼리
|
||||
export interface ReportQuery {
|
||||
query_id: string;
|
||||
report_id: string;
|
||||
@@ -63,7 +56,7 @@ export interface ReportQuery {
|
||||
query_type: "MASTER" | "DETAIL";
|
||||
sql_query: string;
|
||||
parameters: string[] | null;
|
||||
external_connection_id: number | null; // 외부 DB 연결 ID (NULL이면 내부 DB)
|
||||
external_connection_id: number | null;
|
||||
display_order: number;
|
||||
created_at: Date;
|
||||
created_by: string | null;
|
||||
@@ -71,34 +64,37 @@ export interface ReportQuery {
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
// 리포트 상세 (마스터 + 레이아웃 + 쿼리 + 연결된 메뉴)
|
||||
export interface ReportDetail {
|
||||
report: ReportMaster;
|
||||
layout: ReportLayout | null;
|
||||
queries: ReportQuery[];
|
||||
menuObjids?: number[]; // 연결된 메뉴 ID 목록
|
||||
menuObjids?: number[];
|
||||
}
|
||||
|
||||
// 리포트 목록 조회 파라미터
|
||||
export interface GetReportsParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
searchText?: string;
|
||||
searchField?: "report_name" | "created_by" | "report_type" | "updated_at" | "created_at";
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
reportType?: string;
|
||||
useYn?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
}
|
||||
|
||||
// 리포트 목록 응답
|
||||
export interface GetReportsResponse {
|
||||
items: ReportMaster[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
typeSummary: Array<{ type: string; count: number }>;
|
||||
allTypes: string[];
|
||||
recentActivity: Array<{ date: string; count: number }>;
|
||||
recentTotal: number;
|
||||
}
|
||||
|
||||
// 리포트 생성 요청
|
||||
export interface CreateReportRequest {
|
||||
reportNameKor: string;
|
||||
reportNameEng?: string;
|
||||
@@ -108,7 +104,6 @@ export interface CreateReportRequest {
|
||||
companyCode?: string;
|
||||
}
|
||||
|
||||
// 리포트 수정 요청
|
||||
export interface UpdateReportRequest {
|
||||
reportNameKor?: string;
|
||||
reportNameEng?: string;
|
||||
@@ -117,23 +112,18 @@ export interface UpdateReportRequest {
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
// 워터마크 설정
|
||||
export interface WatermarkConfig {
|
||||
enabled: boolean;
|
||||
type: "text" | "image";
|
||||
// 텍스트 워터마크
|
||||
text?: string;
|
||||
fontSize?: number;
|
||||
fontColor?: string;
|
||||
// 이미지 워터마크
|
||||
imageUrl?: string;
|
||||
// 공통 설정
|
||||
opacity: number; // 0~1
|
||||
opacity: number;
|
||||
style: "diagonal" | "center" | "tile";
|
||||
rotation?: number; // 대각선일 때 각도 (기본 -45)
|
||||
rotation?: number;
|
||||
}
|
||||
|
||||
// 페이지 설정
|
||||
export interface PageConfig {
|
||||
page_id: string;
|
||||
page_name: string;
|
||||
@@ -147,30 +137,29 @@ export interface PageConfig {
|
||||
left: number;
|
||||
right: number;
|
||||
};
|
||||
components: any[];
|
||||
components: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
// 레이아웃 설정
|
||||
export interface ReportLayoutConfig {
|
||||
pages: PageConfig[];
|
||||
watermark?: WatermarkConfig; // 전체 페이지 공유 워터마크
|
||||
watermark?: WatermarkConfig;
|
||||
}
|
||||
|
||||
export interface SaveLayoutQueryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "MASTER" | "DETAIL";
|
||||
sqlQuery: string;
|
||||
parameters: string[];
|
||||
externalConnectionId?: number | null;
|
||||
}
|
||||
|
||||
// 레이아웃 저장 요청
|
||||
export interface SaveLayoutRequest {
|
||||
layoutConfig: ReportLayoutConfig;
|
||||
queries?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: "MASTER" | "DETAIL";
|
||||
sqlQuery: string;
|
||||
parameters: string[];
|
||||
externalConnectionId?: number;
|
||||
}>;
|
||||
menuObjids?: number[]; // 연결할 메뉴 ID 목록
|
||||
queries?: SaveLayoutQueryItem[];
|
||||
menuObjids?: number[];
|
||||
}
|
||||
|
||||
// 리포트-메뉴 매핑
|
||||
export interface ReportMenuMapping {
|
||||
mapping_id: number;
|
||||
report_id: string;
|
||||
@@ -180,23 +169,20 @@ export interface ReportMenuMapping {
|
||||
created_by: string | null;
|
||||
}
|
||||
|
||||
// 템플릿 목록 응답
|
||||
export interface GetTemplatesResponse {
|
||||
system: ReportTemplate[];
|
||||
custom: ReportTemplate[];
|
||||
}
|
||||
|
||||
// 템플릿 생성 요청
|
||||
export interface CreateTemplateRequest {
|
||||
templateNameKor: string;
|
||||
templateNameEng?: string;
|
||||
templateType: string;
|
||||
description?: string;
|
||||
layoutConfig?: any;
|
||||
defaultQueries?: any;
|
||||
layoutConfig?: Record<string, unknown>;
|
||||
defaultQueries?: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
// 컴포넌트 설정 (프론트엔드와 동기화)
|
||||
export interface ComponentConfig {
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -224,21 +210,16 @@ export interface ComponentConfig {
|
||||
conditional?: string;
|
||||
locked?: boolean;
|
||||
groupId?: string;
|
||||
// 이미지 전용
|
||||
imageUrl?: string;
|
||||
objectFit?: "contain" | "cover" | "fill" | "none";
|
||||
// 구분선 전용
|
||||
orientation?: "horizontal" | "vertical";
|
||||
lineStyle?: "solid" | "dashed" | "dotted" | "double";
|
||||
lineWidth?: number;
|
||||
lineColor?: string;
|
||||
// 서명/도장 전용
|
||||
showLabel?: boolean;
|
||||
labelText?: string;
|
||||
labelPosition?: "top" | "left" | "bottom" | "right";
|
||||
showUnderline?: boolean;
|
||||
personName?: string;
|
||||
// 테이블 전용
|
||||
tableColumns?: Array<{
|
||||
field: string;
|
||||
header: string;
|
||||
@@ -249,9 +230,7 @@ export interface ComponentConfig {
|
||||
headerTextColor?: string;
|
||||
showBorder?: boolean;
|
||||
rowHeight?: number;
|
||||
// 페이지 번호 전용
|
||||
pageNumberFormat?: "number" | "numberTotal" | "koreanNumber";
|
||||
// 카드 컴포넌트 전용
|
||||
cardTitle?: string;
|
||||
cardItems?: Array<{
|
||||
label: string;
|
||||
@@ -267,7 +246,6 @@ export interface ComponentConfig {
|
||||
titleColor?: string;
|
||||
labelColor?: string;
|
||||
valueColor?: string;
|
||||
// 계산 컴포넌트 전용
|
||||
calcItems?: Array<{
|
||||
label: string;
|
||||
value: number | string;
|
||||
@@ -280,7 +258,6 @@ export interface ComponentConfig {
|
||||
showCalcBorder?: boolean;
|
||||
numberFormat?: "none" | "comma" | "currency";
|
||||
currencySuffix?: string;
|
||||
// 바코드 컴포넌트 전용
|
||||
barcodeType?: "CODE128" | "CODE39" | "EAN13" | "EAN8" | "UPC" | "QR";
|
||||
barcodeValue?: string;
|
||||
barcodeFieldName?: string;
|
||||
@@ -289,19 +266,118 @@ export interface ComponentConfig {
|
||||
barcodeBackground?: string;
|
||||
barcodeMargin?: number;
|
||||
qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H";
|
||||
// QR코드 다중 필드 (JSON 형식)
|
||||
qrDataFields?: Array<{
|
||||
fieldName: string;
|
||||
label: string;
|
||||
}>;
|
||||
qrUseMultiField?: boolean;
|
||||
qrIncludeAllRows?: boolean;
|
||||
// 체크박스 컴포넌트 전용
|
||||
checkboxChecked?: boolean; // 체크 상태 (고정값)
|
||||
checkboxFieldName?: string; // 쿼리 필드 바인딩 (truthy/falsy 값)
|
||||
checkboxLabel?: string; // 체크박스 옆 레이블 텍스트
|
||||
checkboxSize?: number; // 체크박스 크기 (px)
|
||||
checkboxColor?: string; // 체크 색상
|
||||
checkboxBorderColor?: string; // 테두리 색상
|
||||
checkboxLabelPosition?: "left" | "right"; // 레이블 위치
|
||||
checkboxChecked?: boolean;
|
||||
checkboxFieldName?: string;
|
||||
checkboxLabel?: string;
|
||||
checkboxSize?: number;
|
||||
checkboxColor?: string;
|
||||
checkboxBorderColor?: string;
|
||||
checkboxLabelPosition?: "left" | "right";
|
||||
visualQuery?: VisualQuery;
|
||||
// 카드 레이아웃 설정 (card 컴포넌트 전용 - v3)
|
||||
cardLayoutConfig?: CardLayoutConfig;
|
||||
}
|
||||
|
||||
export interface VisualQueryFormulaColumn {
|
||||
alias: string;
|
||||
header: string;
|
||||
expression: string;
|
||||
}
|
||||
|
||||
export interface VisualQuery {
|
||||
tableName: string;
|
||||
limit?: number;
|
||||
columns: string[];
|
||||
formulaColumns: VisualQueryFormulaColumn[];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 카드 레이아웃 v3 타입 정의
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type CardElementType = "header" | "dataCell" | "divider" | "badge";
|
||||
export type CellDirection = "vertical" | "horizontal";
|
||||
|
||||
export interface CardElementBase {
|
||||
id: string;
|
||||
type: CardElementType;
|
||||
colspan?: number;
|
||||
rowspan?: number;
|
||||
}
|
||||
|
||||
export interface CardHeaderElement extends CardElementBase {
|
||||
type: "header";
|
||||
icon?: string;
|
||||
iconColor?: string;
|
||||
title: string;
|
||||
titleColor?: string;
|
||||
titleFontSize?: number;
|
||||
}
|
||||
|
||||
export interface CardDataCellElement extends CardElementBase {
|
||||
type: "dataCell";
|
||||
direction: CellDirection;
|
||||
label: string;
|
||||
columnName?: string;
|
||||
inputType?: "text" | "date" | "number" | "select" | "readonly";
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
selectOptions?: string[];
|
||||
labelWidth?: number;
|
||||
labelFontSize?: number;
|
||||
labelColor?: string;
|
||||
valueFontSize?: number;
|
||||
valueColor?: string;
|
||||
}
|
||||
|
||||
export interface CardDividerElement extends CardElementBase {
|
||||
type: "divider";
|
||||
style?: "solid" | "dashed" | "dotted";
|
||||
color?: string;
|
||||
thickness?: number;
|
||||
}
|
||||
|
||||
export interface CardBadgeElement extends CardElementBase {
|
||||
type: "badge";
|
||||
label?: string;
|
||||
columnName?: string;
|
||||
colorMap?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type CardElement =
|
||||
| CardHeaderElement
|
||||
| CardDataCellElement
|
||||
| CardDividerElement
|
||||
| CardBadgeElement;
|
||||
|
||||
export interface CardLayoutRow {
|
||||
id: string;
|
||||
gridColumns: number;
|
||||
elements: CardElement[];
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export interface CardLayoutConfig {
|
||||
tableName?: string;
|
||||
primaryKey?: string;
|
||||
rows: CardLayoutRow[];
|
||||
padding?: string;
|
||||
gap?: string;
|
||||
borderStyle?: string;
|
||||
borderColor?: string;
|
||||
backgroundColor?: string;
|
||||
headerTitleFontSize?: number;
|
||||
headerTitleColor?: string;
|
||||
labelFontSize?: number;
|
||||
labelColor?: string;
|
||||
valueFontSize?: number;
|
||||
valueColor?: string;
|
||||
dividerThickness?: number;
|
||||
dividerColor?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user