html2canvas , jspdf 활용 pdf 다운로드 구현

Jayden ·2023년 12월 20일

1. View에서 페이지 나누기 (테이블 Row 나누기)

html2canvas는 컴포넌트(html 요소)를 이미지로 캡쳐하는 라이브러리입니다.

jspdf 캡처한 이미지 파일을 pdf 형태로 다운로드 가능하게 해주는 라이브러리 입니다.

npm install html2canvas jspdf

  1. html2canvas, jspdf는 페이지 나누기 기능이 따로 없습니다.
    따라서 , View에서 부터 1페이지, 2페이지,..... 로 각 페이지에 저장할 영역을 id나 className으로 지정합니다.`

  2. jspdf는 출력물의 용지 사이즈(A4 등), 마진, 가로, 높이를 설정할 수 있습니다. 따라서 원하는 결과물에 맞춰서 적절하게 크기를 설정합니다.

  3. margin-bottom을 설정합니다. 마지막 페이지일 경우, 하단의 printToPdf 설정에 맞춰 테이블의 세로가 늘어나는 현상이 발생합니다. (align-item : stretch와 유사한 결과) 이를 방지하기 위해 적용합니다.

import { useRecoilValue } from "recoil"
import { appTestEnrollState, middleType } from "./store/stores"
import { AlertComponent, Button, H1, H2, SelectBox, Wrapper } from "./common/Style"
import styled from "styled-components"
import { reportTypeOption } from "./common/selectBoxOptions"
import i18next, { t } from "i18next"
import { useEffect, useRef, useState } from "react"
import html2canvas from "html2canvas"
import jsPDF from "jspdf"

// (이하 import 생략)

// (컴포넌트 스타일 적용 생략)

export default function CompatiFuncTestReport({setModalOpen}: {[key : string] : Function}) {


    
      const tableRows = Object.keys(scoresByCombination).map((combinationKey) => {
        const { questionStr, 0: score1 = 0, 1: score2 = 0, 2: score3 = 0, 3: score4 = 0, 4: score5 = 0 } = scoresByCombination[combinationKey];
      
        return (
          <Tr key={combinationKey}>
            <Td style={{ width: "390px" }}>{t(questionStr!)}</Td>
            <Td>{score1}</Td>
            <Td>{score2}</Td>
            <Td>{score3}</Td>
            <Td>{score4}</Td>
            <Td>{score5}</Td>
          </Tr>
        );
      });


      /* 결과 리포트 pdf 파일로 저장 */

      const printToPDF = async () => {
        const pdf = new jsPDF("p", "mm", "a4");

        const addPageContent = async (element : any) => {
          const canvas = await html2canvas(element);
          const imgData = canvas.toDataURL("image/jpeg");
          pdf.addImage(imgData, "JPEG", 10, 10, 190, 277);
          pdf.addPage();
        };

        const reportContents = [
          document.getElementById("report-contents-1"),
          document.getElementById("report-contents-2"),
          document.getElementById("report-contents-3"),
          document.getElementById("report-contents-4"),
        ];

        for (const reportContent of reportContents) {
          if (reportContent) {
            await addPageContent(reportContent);
          }
        }
        pdf.save("test_report.pdf");
};
  
    const nowDate = getDateHourMinuteSecond(appTestInfo.test_endtime)?.substring(0,11)

    const tableRowsFirst = tableRows.slice(0, 5);
    const tableRowsSecond = tableRows.slice(5, 28);
    const tableRowsThird = tableRows.slice(28, 51);
    const tableRowsFourth = tableRows.slice(51);
  

    return(
    <ModalBackground>
            <ModalComponent>
                ...
                <TestReportContents id ="report-contents-1">
                    <TestSummaryWrapper>
                        <TestSummaryItem>
                            <p>Test App</p>
                            <p>{appTestInfo.tname}</p>
                        </TestSummaryItem>
                        <TestSummaryItem>
                            <p>Test Period</p>
                            <p>{nowDate}</p>
                        </TestSummaryItem>
                        <TestSummaryItem>
                            <p>No.of Tester</p>
                            <p>{data?.report_mchoice_list?.length}</p>
                        </TestSummaryItem>
                        <TestSummaryItem>
                            <p>Male / Female</p>
                            <p>{maleCount} / {femaleCount}</p>
                        </TestSummaryItem>
                    </TestSummaryWrapper>
                    <DeviceInfo>
                        <DeviceInfoContainer>        
                            <DonutChart data={deviceInfo} type ="device_manuf"/>
                            <DonutChart data={deviceInfo} type= "device_os"/>
                            <DonutChart data={deviceInfo} type= "device_memory"/>
                        </DeviceInfoContainer>  
                    </DeviceInfo>
                    <Detail>
                        <H2 style={{fontSize : "20px", fontWeight : 700, marginTop : "24px"}}>{t('상세 결과')}</H2>
                        <DetailResultWrapper>
                          <DetailDonutChart data={result}/>
                              <div style={{marginTop : "87px", 
                                           marginLeft : "100px", 
                                           display : "flex", 
                                           flexDirection : "column", 
                                           gap : "20px"}}>
                                      <p>{participantString} {totalQuestionString}
                                      </p>
                                      <Wrapper display="flex" gap={10}>  
                                          <Wrapper>
                                              <ResultStatusText>{t('정상(Pass)')}</ResultStatusText>
                                              <ResultStatusText style={{backgroundColor : "#EAF2FF",
                                                                        borderRadius: "0px 0px 10px 10px"}}>
                                                      {result.pass} {t('건')}
                                              </ResultStatusText>
                                          </Wrapper>
                                          <Wrapper>
                                              <ResultStatusText>{t('에러(Fail)')}</ResultStatusText>
                                              <ResultStatusText style={{backgroundColor : "#EAF2FF",
                                                                        borderRadius: "0px 0px 10px 10px"}}>
                                                      {result.fail} {t('건')}
                                              </ResultStatusText>
                                          </Wrapper>
                                          <Wrapper>
                                              <ResultStatusText>{t('확인불가(N/A)')}</ResultStatusText>
                                              <ResultStatusText style={{backgroundColor : "#EAF2FF",
                                                                        borderRadius: "0px 0px 10px 10px"}}>
                                                      {result.notApplicable} {t('건')}
                                              </ResultStatusText>
                                          </Wrapper>
                                      </Wrapper>      
                                  <p>{t('이슈가 확인 됩니다.')}</p>
                              </div> 
                        </DetailResultWrapper>   
                    </Detail> 
                    <DetailTable style={{ marginBottom: !tableRowsSecond.length  ? `${1200 - 36 * tableRowsFirst.length}px` :"" }}>
                        <Tr>           
                            <Th style={{width : "390px"}}>{t('문항')}</Th>
                            <Th>{t('확인불가')}</Th>
                            <Th>{t('발생하지 않음')}</Th>
                            <Th>{t('드물게 발생')}</Th>
                            <Th>{t('빈번하게 발생')}</Th>
                            <Th>{t('100% 발생')}</Th>
                        </Tr>
                        {tableRowsFirst}
                    </DetailTable>
                </TestReportContents>
                {tableRowsSecond.length > 0 && 
                <TestReportContents id ="report-contents-2">
                    <DetailTable style={{ marginBottom: !tableRowsThird.length ? `${1200 - 36 * tableRowsSecond.length}px` :"" }}>
                        {tableRowsSecond}             
                    </DetailTable>     
                 </TestReportContents>
                } 
               {tableRowsThird.length > 0 && (
                  <TestReportContents id="report-contents-3">
                    <DetailTable style={{ marginBottom:  !tableRowsFourth.length ? `${1200 - 36 * tableRowsThird.length}px` :"" }}>
                      {tableRowsThird}
                    </DetailTable>
                  </TestReportContents>
              )}
              {tableRowsFourth.length > 0 && (
                  <TestReportContents id="report-contents-4">
                    <DetailTable style={{ marginBottom: `${1200 - 36 * tableRowsFourth.length}px` }}>
                      {tableRowsFourth}
                    </DetailTable>
                  </TestReportContents>
              )}
            </ModalComponent>
            {isPopupOpen && <AlertComponent message={t('데이터 조회에 실패하였습니다.')} popupClose = {setIsPopupOpen} callback = {handleRefetch} buttonText={t('다시 조회하기')}/>}
        </ModalBackground>  
            
    )
}
profile
프론트엔드 개발자

0개의 댓글