본문 바로가기
Report

[Jaspersoft Studio] Jasper Report - React 연동 (6)

by 승븐지 2025. 8. 25.
반응형

 

저번에 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

 

반응형