회사 페이지 중 주요 사안으로 Page 를 자동으로 PDF로 만들어 서버로 전송해달라는 요구 사항이 있었다.
해당 부분을 개발한 기록을 남겨보도록 하겠다
페이지를 서버 단에서 처리하는 방법도 존재했으나, css 가 제한적으로 사용가능하고 웹 페이지와 유리된다는 점이 불편했다.
이에 프론트 페이지와 pdf가 같은 레이아웃을 공유하고 css도 적용시키기 위해서 이 라이브러리를 사용했다
html2canvas는 영역을 지정해주면 캡처를 하는 방식이였다.
이 캡처된 이미지를 가지고 jsPDF를 사용해 PDF파일로 변경하는 방식을 채택했다.
generateReportPageToPdf 함수를 만들어 PDF 생성 로직을 구현했다.
interface generateReportPageToPdfProps {
patrolElements?: HTMLDivElement;
alarmElements: HTMLDivElement[];
startDate: string;
}
export const generateReportPageToPdf = async ({
patrolElements,
alarmElements,
}: generateReportPageToPdfProps) => {
if (!patrolElements) {
console.error('Invalid patrolElements');
return;
}
// patrolElements를 캡처하여 이미지로 변환
const PatrolDetail = await html2canvas(patrolElements, {
allowTaint: true,
useCORS: true,
logging: false,
scale: 2, // 해상도를 높이기 위해 scale 값을 2로 설정
});
const PatrolDetailImg = PatrolDetail.toDataURL('image/png', 1.0);
const imgWidth = 210; // A4 기준 가로 길이(mm)
const imgHeight = (PatrolDetail.height * imgWidth) / PatrolDetail.width;
const padding = 5;
// PDF 문서 생성
const doc = new jsPDF('p', 'mm', 'a4', true);
doc.addFont(hl, 'HyundaiHarmonyL', 'normal');
doc.setFont('HyundaiHarmonyL');
doc.setFontSize(8);
// 첫 페이지에 PatrolDetail 이미지 추가
doc.addImage(PatrolDetailImg, 'PNG', 0, 10, imgWidth, imgHeight);
let curHeight = imgHeight + padding;
// alarmElements를 순회하며 각각을 이미지로 변환하여 PDF에 추가
for (let i = 0; i < alarmElements.length; i++) {
const canvas = await html2canvas(alarmElements[i], {
allowTaint: true,
useCORS: true,
logging: false,
scale: 2,
});
const img = canvas.toDataURL('image/png', 1.0);
const imageHeight = (canvas.height * imgWidth) / canvas.width;
// 현재 페이지의 높이와 이미지의 높이를 비교하여 페이지를 추가
if (curHeight + imageHeight > doc.internal.pageSize.height - padding) {
doc.addPage();
curHeight = padding;
}
doc.addImage(img, 'PNG', padding, curHeight, 200, imageHeight);
curHeight += imageHeight + padding;
}
addWaterMark(doc);
return doc;
};
생성된 파일은 doc라는 이름으로 리턴되고 있다.
저장된 페이지에 검정색 부분이 발생함
-> scale 값을 2로 두어 해결함. 해당 부분의 경우 화면 크기에 비해서 작은 scale이 문제가 되는 것 같음
board가 표현되지 않는다.
-> 내부적으로 MUI를 사용 중인데, Paper의 값을 읽지 못하는 듯하다. 이에 div를 참조하는 Box를 사용하고, 해당 부분에서 border를 주어 해결했다.
페이지가 마음대로 넘어가는 문제
-> 원하는대로 이미지가 배치되지 못하고 넘어가는 문제가 있었다. 이에 잘리지 않게 만드는 법을 고민했다.
참조: https://tryncatch.tistory.com/16
각 report div에 대해서 ref를 저장하고 싶었음.
const alarmRef = useRef([]);
{AlarmReportData.map((data, index) => (
<Box ref={(el) => (alarmRef.current[index] = el)} key={index}>
<AlarmReport data={data} />
</Box>
))}
map 안에서 돌면서 해당 ref가 index를 따라 저장되도록 해주었다. 이 ref를 props 로 내려주었고, 안에는 배열형태로 된 ref가 저장되어 있었음.
const alramlists = [];
for (let i = 0; i < alarmRef.current.length; i++) {
const canvas = await html2canvas(alarmRef.current[i], {
allowTaint: true,
useCORS: true,
logging: false,
scale: 2,
});
const img = canvas.toDataURL('image/png', 1.0);
const imageHeight = (canvas.height * imgWidth) / canvas.width;
alramlists.push({ image: img, height: imageHeight });
}
받은 alarmRef를 각각 canvas라는 이름으로 변수 지정을 한 다음, lists 배열에 객체 형식으로 저장시켰다. 각각의 div가 높이가 다를 수도 있다는 점을 고려하여 height까지 포함하여 진행했다.
이를 통해 alarmLists에는 각 ref를 이미지로 변환한 값을 저장할 수 있었다.
그렇다면 이제 이 값을 통해 PDF에 뿌려주고, 길이가 더 길다면 다음 페이지로 넘기면 되는 것이었다.
let curHeight = padding;
for (let i = 0; i < alramlists.length; i++) {
const image = alramlists[i].image;
const imgHeight = alramlists[i].height;
if (curHeight + imgHeight > 297 - padding * 2) {
doc.addPage();
curHeight = padding;
doc.addImage(image, 'PNG', padding, curHeight, 200, imgHeight);
curHeight += imgHeight;
} else {
doc.addImage(image, 'PNG', padding, curHeight, 200, imgHeight);
curHeight += imgHeight;
}
}
if를 통해 297 (A4 세로)를 가지고 어떤 값이 더 큰지를 계속 체크해주었다. 각각의 이미지들을 가지고 쭉 이어주면 완성.
디렉토리 구조 안에 ttf 파일이 있는 상황에서 굳이 그렇게 해야 할까 의문이었기 때문에 doc를 참조하여 다양한 설정값을 주어본 결과 import된 ttf 값을 사용 가능함을 파악했다.
import hl from '../font/HyundaiHarmonyL.ttf';
const doc = new jsPDF('p', 'mm', 'a4', true);
doc.addFont(hl, 'HyundaiHarmonyL', 'normal');
doc.setFont('HyundaiHarmonyL');
doc.setFontSize(8);
폰트를 추가하여 정상 표현되었다.
const addFooters = (doc) => {
const pageCount = doc.internal.getNumberOfPages();
doc.addFont(hl, 'HyundaiHarmonyL', 'normal');
doc.setFont('HyundaiHarmonyL');
doc.setFontSize(8);
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.text(
'Page ' + String(i) + ' of ' + String(pageCount),
doc.internal.pageSize.width / 2,
287,
{
align: 'center',
}
);
doc.text(
`레포트 생성 일자 : ${getToday()}`,
doc.internal.pageSize.width / 2 + 40,
287,
{
align: 'center',
}
);
}
};
실제 개발 단계의 코드와는 조금 변경이 있었다. 기능을 먼저 개발하고 개선에 참여했기 때문이다.
그래서 결론은?
특정 URL에 접속하면 해당 페이지가 출력되고 PDF를 저장하는 코드가 돌아가도록 했다.
저장 후 API를 통해 서버로 들어가는 것까지 확인했다.
현재 사용중인 최종코드( pdf 생성 )
interface generateReportPageToPdfProps {
patrolElements?: HTMLDivElement;
alarmElements: HTMLDivElement[];
startDate: string;
}
export const generateReportPageToPdf = async ({
patrolElements,
alarmElements,
}: generateReportPageToPdfProps) => {
if (!patrolElements) {
console.error('Invalid patrolElements');
return;
}
const PatrolDetail = await html2canvas(patrolElements, {
allowTaint: true,
useCORS: true,
logging: false,
scale: 2,
});
const PatrolDetailImg = PatrolDetail.toDataURL('image/png', 1.0);
const imgWidth = 210; // 이미지 가로 길이(mm) / A4 기준 210mm
const imgHeight = (PatrolDetail.height * imgWidth) / PatrolDetail.width;
const padding = 5;
// PDF 문서 생성 및 폰트 설정
const doc = new jsPDF('p', 'mm', 'a4', true);
doc.addFont(hl, 'HyundaiHarmonyL', 'normal');
doc.setFont('HyundaiHarmonyL');
doc.setFontSize(8);
// 첫 페이지에 PatrolDetail 이미지 추가
doc.addImage(PatrolDetailImg, 'PNG', 0, 10, imgWidth, imgHeight);
let curHeight = imgHeight + padding;
// alarmElements를 순회하며 각각을 이미지로 변환하여 PDF에 추가
for (let i = 0; i < alarmElements.length; i++) {
const canvas = await html2canvas(alarmElements[i], {
allowTaint: true,
useCORS: true,
logging: false,
scale: 2,
});
const img = canvas.toDataURL('image/png', 1.0);
const imageHeight = (canvas.height * imgWidth) / canvas.width;
// 현재 페이지의 높이와 이미지의 높이를 비교하여 필요시 새 페이지 추가
if (curHeight + imageHeight > doc.internal.pageSize.height - padding) {
doc.addPage();
curHeight = padding;
}
doc.addImage(img, 'PNG', padding, curHeight, 200, imageHeight);
curHeight += imageHeight + padding;
}
addWaterMark(doc); // 워터마크 추가
return doc;
};
해당 기능을 구성할때 정말 많은 포스팅들에게 도움을 받았는데, 내 포스팅도 누군가에게 힌트가 되었으면 좋겠다.