Merge branch 'main' into feature/screen-management

This commit is contained in:
kjs
2025-12-24 18:38:08 +09:00
37 changed files with 6132 additions and 1159 deletions

View File

@@ -30,6 +30,7 @@ import {
Header,
Footer,
HeadingLevel,
TableLayoutType,
} from "docx";
import { WatermarkConfig } from "../types/report";
import bwipjs from "bwip-js";
@@ -592,8 +593,12 @@ export class ReportController {
// mm를 twip으로 변환
const mmToTwip = (mm: number) => convertMillimetersToTwip(mm);
// px를 twip으로 변환 (1px = 15twip at 96DPI)
const pxToTwip = (px: number) => Math.round(px * 15);
// 프론트엔드와 동일한 MM_TO_PX 상수 (캔버스에서 mm를 px로 변환할 때 사용하는 값)
const MM_TO_PX = 4;
// 1mm = 56.692913386 twip (docx 라이브러리 기준)
// px를 twip으로 변환: px -> mm -> twip
const pxToTwip = (px: number) => Math.round((px / MM_TO_PX) * 56.692913386);
// 쿼리 결과 맵
const queryResultsMap: Record<
@@ -726,6 +731,9 @@ export class ReportController {
const base64Data =
component.imageBase64.split(",")[1] || component.imageBase64;
const imageBuffer = Buffer.from(base64Data, "base64");
// 서명 이미지 크기: 라벨 옆에 인라인으로 표시될 수 있도록 적절한 크기로 조정
const sigImageHeight = 30; // 고정 높이 (약 40px)
const sigImageWidth = Math.round((component.width / component.height) * sigImageHeight) || 80;
result.push(
new ParagraphRef({
children: [
@@ -733,8 +741,8 @@ export class ReportController {
new ImageRunRef({
data: imageBuffer,
transformation: {
width: Math.round(component.width * 0.75),
height: Math.round(component.height * 0.75),
width: sigImageWidth,
height: sigImageHeight,
},
type: "png",
}),
@@ -1443,7 +1451,11 @@ export class ReportController {
try {
const barcodeType = component.barcodeType || "CODE128";
const barcodeColor = (component.barcodeColor || "#000000").replace("#", "");
const barcodeBackground = (component.barcodeBackground || "#ffffff").replace("#", "");
// transparent는 bwip-js에서 지원하지 않으므로 흰색으로 변환
let barcodeBackground = (component.barcodeBackground || "#ffffff").replace("#", "");
if (barcodeBackground === "transparent" || barcodeBackground === "") {
barcodeBackground = "ffffff";
}
// 바코드 값 결정 (쿼리 바인딩 또는 고정값)
let barcodeValue = component.barcodeValue || "SAMPLE123";
@@ -1739,6 +1751,7 @@ export class ReportController {
const rowTable = new Table({
rows: [new TableRow({ children: cells })],
width: { size: 100, type: WidthType.PERCENTAGE },
layout: TableLayoutType.FIXED, // 셀 너비 고정
borders: {
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
@@ -1821,6 +1834,7 @@ export class ReportController {
const textTable = new Table({
rows: [new TableRow({ children: [textCell] })],
width: { size: pxToTwip(component.width), type: WidthType.DXA },
layout: TableLayoutType.FIXED, // 셀 너비 고정
indent: { size: indentLeft, type: WidthType.DXA },
borders: {
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
@@ -1970,6 +1984,10 @@ export class ReportController {
component.imageBase64.split(",")[1] || component.imageBase64;
const imageBuffer = Buffer.from(base64Data, "base64");
// 서명 이미지 크기: 라벨 옆에 인라인으로 표시될 수 있도록 적절한 크기로 조정
const sigImageHeight = 30; // 고정 높이
const sigImageWidth = Math.round((component.width / component.height) * sigImageHeight) || 80;
const paragraph = new Paragraph({
spacing: { before: spacingBefore, after: 0 },
indent: { left: indentLeft },
@@ -1978,8 +1996,8 @@ export class ReportController {
new ImageRun({
data: imageBuffer,
transformation: {
width: Math.round(component.width * 0.75),
height: Math.round(component.height * 0.75),
width: sigImageWidth,
height: sigImageHeight,
},
type: "png",
}),

View File

@@ -234,10 +234,23 @@ export class ReportService {
`;
const queries = await query<ReportQuery>(queriesQuery, [reportId]);
// 메뉴 매핑 조회
const menuMappingQuery = `
SELECT menu_objid
FROM report_menu_mapping
WHERE report_id = $1
ORDER BY created_at
`;
const menuMappings = await query<{ menu_objid: number }>(menuMappingQuery, [
reportId,
]);
const menuObjids = menuMappings?.map((m) => Number(m.menu_objid)) || [];
return {
report,
layout,
queries: queries || [],
menuObjids,
};
}
@@ -696,6 +709,43 @@ export class ReportService {
}
}
// 3. 메뉴 매핑 저장 (있는 경우)
if (data.menuObjids !== undefined) {
// 기존 메뉴 매핑 모두 삭제
await client.query(
`DELETE FROM report_menu_mapping WHERE report_id = $1`,
[reportId]
);
// 새 메뉴 매핑 삽입
if (data.menuObjids.length > 0) {
// 리포트의 company_code 조회
const reportResult = await client.query(
`SELECT company_code FROM report_master WHERE report_id = $1`,
[reportId]
);
const companyCode = reportResult.rows[0]?.company_code || "*";
const insertMappingSql = `
INSERT INTO report_menu_mapping (
report_id,
menu_objid,
company_code,
created_by
) VALUES ($1, $2, $3, $4)
`;
for (const menuObjid of data.menuObjids) {
await client.query(insertMappingSql, [
reportId,
menuObjid,
companyCode,
userId,
]);
}
}
}
return true;
});
}

View File

@@ -71,11 +71,12 @@ export interface ReportQuery {
updated_by: string | null;
}
// 리포트 상세 (마스터 + 레이아웃 + 쿼리)
// 리포트 상세 (마스터 + 레이아웃 + 쿼리 + 연결된 메뉴)
export interface ReportDetail {
report: ReportMaster;
layout: ReportLayout | null;
queries: ReportQuery[];
menuObjids?: number[]; // 연결된 메뉴 ID 목록
}
// 리포트 목록 조회 파라미터
@@ -166,6 +167,17 @@ export interface SaveLayoutRequest {
parameters: string[];
externalConnectionId?: number;
}>;
menuObjids?: number[]; // 연결할 메뉴 ID 목록
}
// 리포트-메뉴 매핑
export interface ReportMenuMapping {
mapping_id: number;
report_id: string;
menu_objid: number;
company_code: string;
created_at: Date;
created_by: string | null;
}
// 템플릿 목록 응답