반응형
저번에 Spring Boot와 JasperReport를 연동하여 스웨거 테스트를 진행했었는데 React 연동까지 해볼라한다..

1. 우선 useReportPopupBridge라는 ts파일을 생성했다. MessageEvent를 사용하였다.
// useReportPopupBridge.ts (간단 훅으로 만들어 재사용 권장)
import { useEffect } from "react";
const ORIGIN = window.location.origin;
type OnDownload = (docType: "pdf" | "excel" | "word", context: any) => Promise<{ url: string; filename: string }>;
export default function useReportPopupBridge(
getInitial: () => { blobUrl: string; params: any },
onDownload: OnDownload
) {
useEffect(() => {
const handler = async (e: MessageEvent) => {
if (e.origin !== ORIGIN) return;
const win = e.source as Window | null;
if (!win) return;
const { type, docType, context } = e.data || {};
// 팝업이 READY면 초기 데이터 전달
if (type === "REPORT_VIEWER_READY") {
const { blobUrl, params } = getInitial();
win.postMessage({ type: "REPORT_VIEWER_DATA", blobUrl, params }, ORIGIN);
return;
}
// 팝업에서 다운로드 요청
if (type === "REPORT_DOWNLOAD_REQUEST" && docType) {
try {
const { url, filename } = await onDownload(docType, context);
win.postMessage({ type: "REPORT_DOWNLOAD_URL", url, filename }, ORIGIN);
} catch (err) {
console.error(err);
win.postMessage({ type: "REPORT_DOWNLOAD_URL", url: "", filename: "" }, ORIGIN);
}
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [getInitial, onDownload]);
}
2.ReportViewrWrapper.tsx 파일을 생성한다.
import React, { useEffect, useState, useCallback } from "react";
import ReportViewer from "./ReportViewer";
const ReportViewerWrapper = () => {
const [blobUrl, setBlobUrl] = useState("");
const [params, setParams] = useState<Record<string, any>>({});
// opener에게 "READY" 알림
useEffect(() => {
try {
window.opener?.postMessage({ type: "REPORT_VIEWER_READY" }, window.location.origin);
} catch (e) {
// opener가 없을 수도 있으므로 무시
}
}, []);
// 부모로부터 DATA 수신
useEffect(() => {
const handler = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
const { type, blobUrl, params } = event.data || {};
if (type === "REPORT_VIEWER_DATA" && blobUrl) {
setBlobUrl(blobUrl);
setParams(params || {});
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, []);
return <ReportViewer blobUrl={blobUrl} params={params} />;
};
export default ReportViewerWrapper;
3.ReportViewer.tsx파일을 생성한다 .
// ReportViewer.tsx (API 호출 제거 버전)
import React, { useCallback, useEffect } from "react";
import pdfIcon from "../../../src/assets/images/pdfIcon.png";
import excelIcon from "../../../src/assets/images/excelIcon.png";
import wordIcon from "../../../src/assets/images/wordIcon.png";
import ButtonComponent from "../common/ButtonComponent";
import { useTranslation } from "react-i18next";
interface ReportViewerProps {
blobUrl: string; // 부모가 전달한 PDF 미리보기 blob URL
params: Record<string, any>; // 부모가 전달한 컨텍스트(보고서 타입/검색조건 등)
}
type DocType = "pdf" | "excel" | "word";
const ORIGIN = window.location.origin;
const ReportViewer: React.FC<ReportViewerProps> = ({ blobUrl, params }) => {
const { t } = useTranslation();
// 부모가 보내준 "다운로드 URL" 수신 → 이 창에서 실제 다운로드 실행
useEffect(() => {
const onMsg = (e: MessageEvent) => {
if (e.origin !== ORIGIN) return;
const { type, url, filename } = e.data || {};
if (type === "REPORT_DOWNLOAD_URL" && url) {
const a = document.createElement("a");
a.href = url;
a.download = filename || "report";
document.body.appendChild(a);
a.click();
a.remove();
// URL.revokeObjectURL(url)는 부모가 적절한 타이밍에 처리
}
};
window.addEventListener("message", onMsg);
return () => window.removeEventListener("message", onMsg);
}, []);
// 다운로드 “요청”만 부모(opener)에게 보냄 (부모가 API 호출/Blob 생성/URL 회신)
const requestDownload = useCallback(
(docType: DocType) => {
if (!window.opener) {
alert("다운로드 요청을 보낼 부모 창이 없습니다.");
return;
}
window.opener.postMessage({ type: "REPORT_DOWNLOAD_REQUEST", docType, context: params }, ORIGIN);
},
[params]
);
// 미리보기는 PDF만
const isPdf = (params?.docType ?? "pdf") === "pdf";
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
{/* 버튼 영역 */}
<div className="print-modal-div-button-class">
<button onClick={() => requestDownload("pdf")} className="print-modal-down-button" title="PDF 다운로드">
<img src={pdfIcon} alt="PDF" className="print-modal-pdf-icon" />
</button>
<button onClick={() => requestDownload("excel")} className="print-modal-down-button" title="Excel 다운로드">
<img src={excelIcon} alt="Excel" className="print-modal-excel-icon" />
</button>
<button onClick={() => requestDownload("word")} className="print-modal-down-button" title="Word 다운로드">
<img src={wordIcon} alt="Word" className="print-modal-word-icon" />
</button>
<ButtonComponent
type="button"
className="print-modal-close-button"
iClassName="fe-x"
txt={t("common.close.btn")}
onClick={() => window.close()}
/>
</div>
{/* PDF 미리보기(iframe) */}
{isPdf ? (
blobUrl ? (
<iframe src={blobUrl} title="PDF Viewer" style={{ width: "100%", height: "100%", border: "none" }} />
) : (
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#666" }}>
로딩 중...
</div>
)
) : (
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#666" }}>
선택한 문서는 미리보기가 제공되지 않습니다. (다운로드로 확인하세요)
</div>
)}
</div>
);
};
export default ReportViewer;
4.출력 버튼을 클릭하게되면은 처음 이미지와 같이 팝업창이 표출되기위한 소스이다.
const [reportPayload, setReportPayload] = useState<{
blobUrl: string;
params: any;
} | null>(null);
// 2) 브리지 훅: READY 오면 reportPayload를 전달, 다운로드 요청 오면 API 호출
useReportPopupBridge(
() => reportPayload ?? { blobUrl: "", params: {} },
async (docType, ctx) => {
// 여기서만 공통 API 호출
const res = await dispatch(exportRndArticleReport({ ...ctx.request, docType })).unwrap();
const mime =
docType === "pdf"
? "application/pdf"
: docType === "excel"
? "application/vnd.ms-excel"
: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
const ext = docType === "pdf" ? "pdf" : docType === "excel" ? "xls" : "docx";
const blob = new Blob([res.data], { type: mime });
const url = URL.createObjectURL(blob);
return { url, filename: `report.${ext}` };
}
);
// 3) 버튼 핸들러: 초기 PDF만 받아 미리보기용 blobUrl/state 세팅 → 팝업 열기
const onPrintButtonClick = async () => {
try {
// 이부분은 각자 API를 호출하자.
const res = await dispatch(
exportRndArticleReport()
).unwrap();
const blobUrl = URL.createObjectURL(new Blob([res.data], { type: "application/pdf" }));
// 팝업에 넘길 컨텍스트(공통)
setReportPayload({
blobUrl,
params: {
docType: "pdf",
reportType: "rndArticleReportGenerator",
request: {
...searchParams,
seqArticle: seqArticle,
cdCompany: user?.companyId || "1000",
},
},
});
// 메시지 전송은 훅이 처리하므로, 여기선 팝업만 오픈
openReportPopup(); // 인자 없이 열기
} catch (err) {
showAlert("리포트 출력에 실패했습니다.");
}
};
// 버튼 클릭부분
<PageTitleBar
onPrintButtonClick={onPrintButtonClick}
/>
// PageTiteleBar 컴포넌트
interface Props {
onPrintButtonClick?: () => void;
}
const PageTitleBar = memo(
({
onPrintButtonClick,
}: Props) => {
const { t } = useTranslation();
return (
<Row className="mb-2">
<Col>
{/* isFabricLibraryStatus가 false일 때만 저장 버튼 표시 */}
<ButtonComponent
type="button"
className="system-page-title-button"
iClassName="mdi mdi-printer"
txt={t("common.print.btn")}
onClick={onPrintButtonClick}
/>
</Col>
</Row>
);
}
);
export default PageTitleBar;
2025.08.11 - [Report] - [Jaspersoft Studio] Jasper Report InteliJ 연동 Gradle (5)
[Jaspersoft Studio] Jasper Report InteliJ 연동 Gradle (5)
환경부터 소개하겠다 간단하게.Spring Boot - InteliJ 툴빌드 GradleSwagger 테스트 ms-sql 1. 우선은 build.gradle 에 들어가서 dependecies에 해당 외부 라이브러리를 추가한다 . // 나머지 라이브러리는 word, excel ,
ycds.tistory.com
반응형
'Report' 카테고리의 다른 글
| [Jaspersoft Studio] JAVA 한글 폰트 설정 -(8) (0) | 2025.10.21 |
|---|---|
| [Jaspersoft Studio] JAVA 연동 QR CODE 출력 안됌 해결 - (7) (0) | 2025.10.21 |
| [Jaspersoft Studio] Jasper Report InteliJ 연동 Gradle (5) (2) | 2025.08.11 |
| [Jaspersoft Studio] jxrml 파일 생성 및 DB SELECT(4) (3) | 2025.08.11 |
| [Jaspersoft Studio] Data Adapters DBMS 연결 ClassNotFoundException 해결 (3) (4) | 2025.08.06 |