일정에 따라 PDF 보고서를 자동으로 생성하고 이메일로 보내는 방법
Source: Dev.to
PDF 보고서를 수동으로 생성하는 것은 쉽습니다. 실시간 데이터를 가져오고, 브랜드가 적용된 HTML 템플릿으로 렌더링한 뒤, 픽셀 단위로 정확한 PDF로 캡처하고, 이를 이해관계자에게 이메일로 자동 전송하는 과정—이 부분에서 대부분의 설정이 무너집니다.
보통의 장애 요인: 크론 작업에서 Puppeteer 또는 Playwright 사용. 스케줄된 작업에서 헤드리스 브라우저가 조용히 실패하고, 메모리를 많이 소모하며, 컨테이너 재시작 시 살아남지 못합니다.
견고한 파이프라인
cron → fetch data → render HTML → PageBolt PDF → email
헤드리스 브라우저 의존성이 없습니다. PageBolt이 PDF 렌더링을 담당합니다.
npm install node-cron nodemailer
1. 스케줄러 (node‑cron)
import cron from "node-cron";
import nodemailer from "nodemailer";
// Runs every Monday at 8 am
cron.schedule("0 8 * * 1", async () => {
console.log("Generating weekly report...");
await generateAndSendReport();
});
2. 주요 워크플로우
async function generateAndSendReport() {
// 1️⃣ Fetch your data
const data = await fetchReportData();
// 2️⃣ Render HTML
const html = renderReportHtml(data);
// 3️⃣ Capture as PDF (PageBolt)
const pdfRes = await fetch("https://pagebolt.dev/api/v1/pdf", {
method: "POST",
headers: {
"x-api-key": process.env.PAGEBOLT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
});
const pdfBuffer = Buffer.from(await pdfRes.arrayBuffer());
// 4️⃣ Email it
await emailReport(pdfBuffer, data.weekLabel);
}
3. 데이터 가져오기
import { db } from "./db.js"; // your DB client
async function fetchReportData() {
const weekStart = new Date();
weekStart.setDate(weekStart.getDate() - 7);
const [revenue, signups, churn] = await Promise.all([
db.payments.sum({ where: { createdAt: { gte: weekStart } } }),
db.users.count({ where: { createdAt: { gte: weekStart } } }),
db.subscriptions.count({
where: { canceledAt: { gte: weekStart }, status: "canceled" },
}),
]);
const topPages = await db.pageViews.groupBy({
by: ["path"],
_sum: { views: true },
orderBy: { _sum: { views: "desc" } },
take: 5,
where: { createdAt: { gte: weekStart } },
});
return {
weekLabel: `Week of ${weekStart.toLocaleDateString()}`,
revenue: (revenue._sum.amount / 100).toFixed(2),
signups,
churn,
topPages: topPages.map((p) => ({
path: p.path,
views: p._sum.views,
})),
};
}
4. HTML 렌더링
function renderReportHtml(data) {
const topPagesRows = data.topPages
.map(
(p, i) => `
${i + 1}
${p.path}
${p.views.toLocaleString()}
`
)
.join("");
return `
<style>
body { font-family: -apple-system, sans-serif; color: #111; max-width: 720px; margin: 40px auto; padding: 0 24px; }
h1 { font-size: 22px; margin-bottom: 4px; }
.subtitle { color: #666; margin-bottom: 32px; }
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 40px; }
.metric { background: #f5f5f5; border-radius: 8px; padding: 20px; }
.metric-value { font-size: 28px; font-weight: 700; }
.metric-label { color: #666; font-size: 13px; margin-top: 4px; }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; border-bottom: 2px solid #111; padding: 8px 0; font-size: 13px; }
td { padding: 10px 0; border-bottom: 1px solid #eee; font-size: 14px; }
h2 { font-size: 16px; margin: 32px 0 12px; }
</style>
<h2>Weekly Report</h2>
<p>${data.weekLabel}</p>
<div class="metrics">
<div class="metric">
<div class="metric-value">$${data.revenue}</div>
<div class="metric-label">Revenue</div>
</div>
<div class="metric">
<div class="metric-value">${data.signups}</div>
<div class="metric-label">New signups</div>
</div>
<div class="metric">
<div class="metric-value">${data.churn}</div>
<div class="metric-label">Cancellations</div>
</div>
</div>
<h2>Top pages</h2>
<table>
<thead>
<tr><th>#</th><th>Page</th><th>Views</th></tr>
</thead>
<tbody>
${topPagesRows}
</tbody>
</table>
`;
}
5. PDF 이메일 보내기
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
async function emailReport(pdfBuffer, weekLabel) {
const recipients = process.env.REPORT_RECIPIENTS.split(",");
await transporter.sendMail({
from: "reports@yourapp.com",
to: recipients,
subject: `Weekly Report — ${weekLabel}`,
text: "Your weekly report is attached.",
attachments: [
{
filename: `weekly-report-${Date.now()}.pdf`,
content: pdfBuffer,
contentType: "application/pdf",
},
],
});
console.log(`Report sent to ${recipients.join(", ")}`);
}
6. 각 사용자의 청구 주기에 맞춘 개인화 보고서
// 매일 자정에 실행되는 cron에 의해 호출됨
cron.schedule("0 0 * * *", async () => {
const today = new Date().getDate();
// 오늘과 일치하는 청구일을 가진 사용자를 찾음
const users = await db.users.findMany({
where: { billingDay: today, reportEnabled: true },
});
await Promise.allSettled(
users.map(async (user) => {
const data = await fetchUserReportData(user.id);
const html = renderUserReportHtml(user, data);
const pdfRes = await fetch("https://pagebolt.dev/api/v1/pdf", {
method: "POST",
headers: {
"x-api-key": process.env.PAGEBOLT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
});
const pdf = Buffer.from(await pdfRes.arrayBuffer());
await emailReport(pdf, user.email, "Your monthly summary");
})
);
});
Promise.allSettled는 하나의 실패가 배치를 중단시키지 않도록 보장합니다—각 사용자의 보고서는 독립적입니다.
7. Production 환경에서 스케줄러 실행하기
# Using PM2 (or any process manager)
pm2 start report-scheduler.js --name weekly-reports
그게 전부입니다! 이제 완전 자동화된, 헤드리스 브라우저 없이 동작하는 파이프라인을 갖게 됩니다:
- 스케줄에 따라 실시간 데이터를 가져옵니다.
- 세련된 HTML 보고서를 렌더링합니다.
- PageBolt API를 통해 PDF로 변환합니다.
- PDF를 적절한 수신자에게 이메일로 전송합니다.
HTML, 데이터 소스, 혹은 이메일 레이아웃을 여러분의 브랜드에 맞게 자유롭게 수정하세요. 즐거운 보고 되세요!
// handler.js — triggered by EventBridge rule: cron(0 8 ? * MON *)
export const handler = async () => {
await generateAndSendReport();
return { statusCode: 200 };
};
Lambda 패키지에 브라우저 바이너리가 없습니다. PDF 생성은 단일 외부 HTTP 요청으로 이루어지며, Lambda의 메모리 및 타임아웃 제한 안에서 충분히 동작합니다.
무료로 사용해 보세요 — 월 100건 요청, 신용카드 필요 없음. → 2분 안에 시작하기