๋“ค์–ด๊ฐ€๋ฉฐ

ํšŒ์‚ฌ ํ”„๋กœ์ ํŠธ์—์„œ ์›น ํŽ˜์ด์ง€๋ฅผ ์ž๋™์œผ๋กœ PDF๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์„œ๋ฒ„๋กœ ์ „์†กํ•ด์•ผ ํ•˜๋Š” ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์ธก์—์„œ PDF๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์—ˆ์ง€๋งŒ, CSS ์‚ฌ์šฉ์ด ์ œํ•œ์ ์ด๊ณ  ์›น ํŽ˜์ด์ง€์™€ ๋ ˆ์ด์•„์›ƒ์ด ๋‹ฌ๋ผ์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ง์ ‘ ํŽ˜์ด์ง€๋ฅผ PDF๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

์›น ํŽ˜์ด์ง€๋ฅผ PDF๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ๋‘ ๊ฐ€์ง€ ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. html2canvas: HTML ์š”์†Œ๋ฅผ ์บก์ฒ˜ํ•˜์—ฌ ์บ”๋ฒ„์Šค๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  2. jsPDF: ์บ”๋ฒ„์Šค ์ด๋ฏธ์ง€๋ฅผ PDF ๋ฌธ์„œ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์ด ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์กฐํ•ฉํ•˜๋ฉด ์›น ํŽ˜์ด์ง€์˜ ๋ชจ์–‘์„ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๋ฉด์„œ PDF๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

PDF ์ƒ์„ฑ ํ•จ์ˆ˜ ๊ตฌํ˜„ํ•˜๊ธฐ

๊ธฐ๋ณธ ๊ตฌ์กฐ ์„ค๊ณ„

๋จผ์ € PDF ์ƒ์„ฑ์„ ์œ„ํ•œ ํ•จ์ˆ˜์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค:

interface generateReportPageToPdfProps {
  patrolElements?: HTMLDivElement;
  alarmElements: HTMLDivElement[];
  startDate: string;
}

export const generateReportPageToPdf = async ({
  patrolElements,
  alarmElements,
}: generateReportPageToPdfProps) => {
  // PDF ์ƒ์„ฑ ๋กœ์ง
};

ํ•จ์ˆ˜๋Š” ๋‘ ์ข…๋ฅ˜์˜ HTML ์š”์†Œ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค:

  • patrolElements: ๋ณด๊ณ ์„œ์˜ ์ฃผ์š” ๋‚ด์šฉ
  • alarmElements: ์•Œ๋žŒ ๋ณด๊ณ ์„œ ๋ชฉ๋ก (์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์— ๊ฑธ์ณ ํ‘œ์‹œ๋  ์ˆ˜ ์žˆ์Œ)

HTML ์š”์†Œ๋ฅผ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ

์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๋Š” HTML ์š”์†Œ๋ฅผ ์บ”๋ฒ„์Šค๋กœ ๋ณ€ํ™˜ํ•œ ํ›„ ์ด๋ฏธ์ง€๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค:

// patrolElements๋ฅผ ์บก์ฒ˜ํ•˜์—ฌ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜
const PatrolDetail = await html2canvas(patrolElements, {
  allowTaint: true,
  useCORS: true,
  logging: false,
  scale: 2, // ํ•ด์ƒ๋„๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•ด scale ๊ฐ’์„ 2๋กœ ์„ค์ •
});

const PatrolDetailImg = PatrolDetail.toDataURL('image/png', 1.0);

์—ฌ๊ธฐ์„œ ์ฃผ๋ชฉํ•  ์˜ต์…˜๋“ค:

  • allowTaint: ์™ธ๋ถ€ ๋„๋ฉ”์ธ์˜ ์ด๋ฏธ์ง€ ์‚ฌ์šฉ ํ—ˆ์šฉ
  • useCORS: ํฌ๋กœ์Šค ๋„๋ฉ”์ธ ์ด๋ฏธ์ง€ ์‚ฌ์šฉ
  • scale: 2: ํ•ด์ƒ๋„๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•œ ์„ค์ • (์ด ๋ถ€๋ถ„์ด ์ค‘์š”!)

PDF ๋ฌธ์„œ ์ƒ์„ฑ ๋ฐ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€

๋‹ค์Œ์œผ๋กœ jsPDF๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ PDF ๋ฌธ์„œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ด๋ฏธ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

const imgWidth = 210; // A4 ๊ธฐ์ค€ ๊ฐ€๋กœ ๊ธธ์ด(mm)
const imgHeight = (PatrolDetail.height * imgWidth) / PatrolDetail.width;
const padding = 5;

// PDF ๋ฌธ์„œ ์ƒ์„ฑ
const doc = new jsPDF('p', 'mm', 'a4', true);

// ์ฒซ ํŽ˜์ด์ง€์— PatrolDetail ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
doc.addImage(PatrolDetailImg, 'PNG', 0, 10, imgWidth, imgHeight);

์—ฌ๋Ÿฌ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌํ•˜๊ธฐ

alarmElements๋ฅผ ์ˆœํšŒํ•˜๋ฉด์„œ ๊ฐ๊ฐ์˜ ์š”์†Œ๋ฅผ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  PDF์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ํŽ˜์ด์ง€ ํฌ๊ธฐ๋ฅผ ๋„˜์–ด๊ฐ€๋ฉด ์ƒˆ ํŽ˜์ด์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋กœ์ง์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

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;
}

๋ฐœ์ƒํ•œ ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. ๊ฒ€์ •์ƒ‰ ๋ถ€๋ถ„ ๋ฐœ์ƒ ๋ฌธ์ œ

์บก์ฒ˜๋œ ์ด๋ฏธ์ง€์— ๊ฒ€์ •์ƒ‰ ๋ถ€๋ถ„์ด ๋‚˜ํƒ€๋‚˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•: scale ๊ฐ’์„ 2๋กœ ์„ค์ •ํ•˜์—ฌ ํ•ด์ƒ๋„๋ฅผ ๋†’์˜€์Šต๋‹ˆ๋‹ค. ํ™”๋ฉด ํฌ๊ธฐ์— ๋น„ํ•ด ์ž‘์€ scale ๊ฐ’์ด ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค.

2. MUI Board ํ‘œํ˜„ ๋ฌธ์ œ

Material-UI์˜ Paper ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ์บก์ฒ˜๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•: div๋ฅผ ์ฐธ์กฐํ•˜๋Š” Box ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ์ง์ ‘ border ์Šคํƒ€์ผ์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

3. ํŽ˜์ด์ง€ ์ž๋™ ๋ถ„ํ•  ๋ฌธ์ œ

์ด๋ฏธ์ง€๊ฐ€ ์›ํ•˜๋Š” ๋Œ€๋กœ ๋ฐฐ์น˜๋˜์ง€ ์•Š๊ณ  ํŽ˜์ด์ง€๊ฐ€ ์ž„์˜๋กœ ๋„˜์–ด๊ฐ€๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•: ๊ฐ ์š”์†Œ์˜ ref๋ฅผ ๋ฐฐ์—ด๋กœ ์ €์žฅํ•˜๊ณ , ๊ฐ ์š”์†Œ์˜ ๋†’์ด๋ฅผ ๊ณ„์‚ฐํ•˜์—ฌ ํŽ˜์ด์ง€ ๋ถ„ํ• ์„ ์ •ํ™•ํ•˜๊ฒŒ ์ œ์–ดํ–ˆ์Šต๋‹ˆ๋‹ค:

const alarmRef = useRef([]);

// ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ JSX์—์„œ
{AlarmReportData.map((data, index) => (
  <Box ref={(el) => (alarmRef.current[index] = el)} key={index}>
    <AlarmReport data={data} />
  </Box>
))}

// ๊ฐ ์š”์†Œ๋ฅผ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜
const alramlists = [];
for (let i = 0; i < alarmRef.current.length; i++) {
  const canvas = await html2canvas(alarmRef.current[i], {...});
  const img = canvas.toDataURL('image/png', 1.0);
  const imageHeight = (canvas.height * imgWidth) / canvas.width;
  alramlists.push({ image: img, height: imageHeight });
}

// ํŽ˜์ด์ง€ ๋ถ„ํ•  ๋กœ์ง
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) { // A4 ์„ธ๋กœ ๊ธธ์ด (297mm)
    doc.addPage();
    curHeight = padding;
  }
  doc.addImage(image, 'PNG', padding, curHeight, 200, imgHeight);
  curHeight += imgHeight;
}

4. ํ•œ๊ธ€ ํฐํŠธ ๊นจ์ง ๋ฌธ์ œ

PDF์— ํ•œ๊ธ€ ํ…์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ํฐํŠธ๊ฐ€ ๊นจ์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•: ํฐํŠธ ํŒŒ์ผ์„ ์ง์ ‘ importํ•˜์—ฌ jsPDF์— ๋“ฑ๋กํ–ˆ์Šต๋‹ˆ๋‹ค:

import hl from '../font/HyundaiHarmonyL.ttf';

// PDF ์ƒ์„ฑ ์‹œ
const doc = new jsPDF('p', 'mm', 'a4', true);
doc.addFont(hl, 'HyundaiHarmonyL', 'normal');
doc.setFont('HyundaiHarmonyL');
doc.setFontSize(8);

5. ํ‘ธํ„ฐ ์ถ”๊ฐ€ํ•˜๊ธฐ

๊ฐ ํŽ˜์ด์ง€์— ์ผ๊ด€๋œ ํ‘ธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค:

const addFooters = (doc) => {
  const pageCount = doc.internal.getNumberOfPages();
  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' }
    );
  }
};

์ตœ์ข… ์ฝ”๋“œ

์ตœ์ข…์ ์œผ๋กœ ์™„์„ฑ๋œ PDF ์ƒ์„ฑ ํ•จ์ˆ˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

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;
};

๋งˆ์น˜๋ฉฐ

์ด ๋ฐฉ๋ฒ•์„ ํ†ตํ•ด ์›น ํŽ˜์ด์ง€๋ฅผ ์›๋ณธ ๋ ˆ์ด์•„์›ƒ๊ณผ ์Šคํƒ€์ผ ๊ทธ๋Œ€๋กœ PDF๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , ์„œ๋ฒ„๋กœ ์ „์†กํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์„ฑ๊ณต์ ์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  ๋ณต์žกํ•œ ํŽ˜์ด์ง€๋‚˜ ๋™์  ์ปจํ…์ธ ๊ฐ€ ๋งŽ์€ ๊ฒฝ์šฐ ์ถ”๊ฐ€์ ์ธ ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ธฐ๋ณธ์ ์ธ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ถฉ์กฑํ•˜๋Š” ์†”๋ฃจ์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๋ฆฌ์•กํŠธ ํ”„๋กœ์ ํŠธ์—์„œ PDF ๋ณ€ํ™˜ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•˜์‹  ๋ถ„๋“ค๊ป˜ ์ด ๊ธ€์ด ๋„์›€์ด ๋˜๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค. ๋•๋ถ„์— ์ €๋„ ๋งŽ์€ ๊ฒƒ์„ ๋ฐฐ์› ๊ณ , ์ด ์ง€์‹์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์–ด ๊ธฐ์ฉ๋‹ˆ๋‹ค.

profile
ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ ์‚ด์•„๊ฐ€๊ธฐ

0๊ฐœ์˜ ๋Œ“๊ธ€

๊ด€๋ จ ์ฑ„์šฉ ์ •๋ณด

Powered by GraphCDN, the GraphQL CDN