서버 예약 발송 (@freereportjs/server)
@freereportjs/server로 정해진 시각(매일~cron, 시간대 지정)에 보고서를 만들어 메일·폴더·인쇄로 보냅니다. 받는 사람별 조건, 재시도, 출력 이력을 지원합니다.
@freereportjs/server는 서버 렌더링을 예약 발송으로 만들어 줍니다. 정해진 시각에(작업마다 시간대 지정) 브라우저 없이 PDF·엑셀·워드를 만들어 메일(nodemailer로 SMTP), 폴더, 웹훅, 인쇄로 보냅니다. recipients가 있으면 받는 사람마다 자기 조건으로 따로 만들어, 지점장마다 자기 지점 보고서만 받습니다. 데이터 불러오기와 보내기는 실패하면 간격을 늘려 다시 시도하고, 실행마다 JSONL 출력 이력을 한 줄 남깁니다. Node 18 이상, nodemailer는 메일을 보낼 때만 필요합니다.
import { createScheduler } from '@freereportjs/server';
const scheduler = createScheduler({
timezone: 'Asia/Seoul',
fonts: { 'Nanum Gothic': { normal: 'fonts/NanumGothic.ttf', bold: 'fonts/NanumGothicBold.ttf' } },
outDir: 'out', // files, and out/audit.jsonl
smtp: process.env.SMTP_URL, // smtps://user:pass@smtp.example.com:465
jobs: [{
name: 'branch-sales',
schedule: 'monthly 1 09:00', // or 'weekly mon 09:00', '0 9 * * 1-5'
report: 'reports/branch-sales.report.json',
data: 'https://erp.example.com/api/sales?branch={params.branch}&from={params.from}&to={params.to}',
params: (runDate, { tokens }) => ({ from: tokens.prevMonthStart, to: tokens.prevMonthEnd, month: tokens.prevYearMonth }),
outputs: ['pdf', 'xlsx'],
fileName: '지점매출-{params.month}',
recipients: [ // one render each, with their own params
{ to: 'seoul@example.com', name: '서울', params: { branch: '서울' } },
{ to: 'busan@example.com', name: '부산', params: { branch: '부산' } },
],
deliver: [
{ type: 'email', cc: 'hq@example.com', subject: '[{params.month}] {recipient.name} 지점 매출', body: '{recipient.name} 지점장님, 지난달 보고서입니다.' },
{ type: 'folder', dir: 'archive/{yearMonth}' },
],
}],
});
scheduler.start();
await scheduler.runNow('branch-sales', { dryRun: true }); // files + .eml, nothing sent| 함수 | 돌려주는 값 | 설명 |
|---|---|---|
createScheduler({ jobs, fonts, timezone?, outDir?, auditLog?, smtp?, retry?, log?, onRun? }) | Scheduler | 작업을 검사하고 스케줄러를 돌려줍니다. fonts는 글꼴별 TTF 바이트·경로·URL(없으면 jsDelivr의 나눔고딕). auditLog 기본값은 <outDir>/audit.jsonl. |
scheduler.start() / stop() | void / Promise | 켜진 작업마다 타이머를 겁니다. stop은 진행 중인 실행을 기다립니다. 한 작업의 실행은 겹치지 않습니다. |
scheduler.runNow(name, { now?, dryRun? }) | Promise<AuditRecord> | 작업 하나를 지금 실행(꺼진 작업도). |
scheduler.next(after?) | { name, timezone, next }[] | 켜진 작업마다 다음 실행 시각. |
runJob(job, { now?, dryRun?, ...options }) | Promise<AuditRecord> | 스케줄러 없이 한 번 실행. 실패해도 예외를 던지지 않고 status(ok, partial, error)로 알립니다. |
parseSchedule(text, tz?) / nextRun(s, after?) / dateTokens(date, tz) | … | 일정 해석, 다음 실행 시각 계산, 매개변수·템플릿에 쓰는 날짜 토큰. |
작업
| 이름 | 타입 | 설명 |
|---|---|---|
schedule | string | "daily 09:00", "weekdays 08:30", "weekly mon,thu 09:00", "monthly 1 09:00"(29–31일은 짧은 달에 말일), "monthly last 18:00", 또는 5칸 cron "0 9 * * 1-5"(목록·범위·간격·이름, L=말일). 끝에 IANA 시간대를 붙일 수 있습니다. 서머타임: 없는 시각은 한 시간 뒤, 두 번 오는 시각은 처음 한 번만. |
timezone | string | 작업의 시간대(기본: 스케줄러의 시간대, 그다음 서버의 시간대). |
report | definition | path | 보고서 정의 객체나 JSON 파일 경로. |
data | url | REST | { file } | { module } | (ctx) => data | 행을 가져올 곳. REST는 보고서 정의의 data와 같은 형식({params.x}를 이스케이프해 채움). 없으면 정의의 data. |
params | object | (runDate, ctx) => object | 조회 조건. 문자열에 {prevMonthStart} 같은 날짜 토큰을 쓰거나, 실행 날짜로 계산합니다(예: 지난달). |
outputs | ('pdf' | 'xlsx' | 'xlsx-data' | 'docx')[] | 만들 파일(기본 ['pdf']). |
recipients | { to, cc?, name?, params }[] | 받는 사람마다 자기 조건으로 따로 만듭니다(예: 지점장마다 자기 지점). 메일은 그 사람에게 갑니다. |
deliver | target[] | email(to, cc, subject, body, smtp), folder(dir, filename), webhook(url), put(url), printAgent(token, printer). 템플릿: {yesterday}, {params.x}, {recipient.name}, {title} … |
retry | { retries, delayMs, factor } | 데이터와 보내는 곳마다 간격을 늘려 다시 시도(기본 2번, 30초부터 두 배씩). |
fileName / title / locale / pdf / enabled | … | 파일 이름·제목 템플릿, 숫자·날짜 언어, PDF 옵션(암호), false면 예약 안 함. |
날짜 토큰은 작업의 시간대 기준입니다: {today} {yesterday} {weekAgo} {monthStart} {monthEnd} {prevMonthStart} {prevMonthEnd} {yearMonth} {prevYearMonth} {year} {now}. 미리 실행(dry run)은 파일을 모두 만들고 메일은 첨부가 든 .eml 파일로만 쓰며, 웹훅·업로드·인쇄는 건너뜁니다.
명령줄
jobs.json에는 같은 옵션과 작업을 JSON으로 적고, 경로는 이 파일 기준입니다. 비밀 값은 환경 변수에 두고 ${env:SMTP_PASS}처럼 가리키면 작업을 실행할 때 채우므로, 변수가 없으면 그 작업만 실패합니다. 예약 보고서 예제의 reports.json도 그대로 쓸 수 있습니다.
npm i @freereportjs/core @freereportjs/server nodemailer # nodemailer only for e-mail
npx freereport-server validate --config jobs.json # schedules, targets, checkReport
npx freereport-server next --config jobs.json --count 3 # next run times
npx freereport-server run --config jobs.json --now branch-sales --dry-run
npx freereport-server run --config jobs.json # keep running (NSSM, systemd)| 명령 | 용도 | 설명 |
|---|---|---|
freereport-server run --config jobs.json | service | 계속 실행. 작업마다 일정대로 실행합니다(SIGINT·SIGTERM으로 멈춤). |
run --now <name> [--dry-run] | once | 작업 하나(또는 --now all)를 실행하고 실패하면 종료 코드 1. --dry-run은 파일과 .eml만 쓰고 아무것도 보내지 않습니다. |
validate --config jobs.json | check | 일정, 출력 형식, 보내는 곳, 환경 변수를 검사하고 보고서마다 checkReport를 돌립니다. |
next --config jobs.json [--count n] | list | 작업마다 다음 실행 시각(작업의 시간대로). |
pair-agent | Windows | FreeReport Windows 인쇄 프로그램에서 이 서비스를 한 번 허용하고 토큰을 출력합니다. |
출력 이력
실패한 실행까지 실행마다 JSON 한 줄입니다. status는 ok, partial(일부 받는 사람이나 보내는 곳 실패), error. docHash는 인쇄된 글자의 SHA-256(원본 확인 지문, 같은 내용이면 다시 만들어도 같음), sha256은 보낸 파일 그대로의 해시입니다. 비밀 값과 서명된 URL의 쿼리 문자열은 남기지 않습니다.
{"time":"2026-11-01T00:00:04.120Z","job":"branch-sales","schedule":"monthly 1 09:00","runAt":"2026-11-01T00:00:00.000Z",
"dryRun":false,"status":"ok","recipients":["seoul@example.com","hq@example.com","busan@example.com"],"size":182344,"durationMs":4120,
"renders":[{"recipient":"서울","params":{"branch":"서울"},"pages":3,"rows":120,"docHash":"5c1e…",
"files":[{"name":"지점매출-2026-10-서울.pdf","format":"pdf","size":130484,"sha256":"815f…"}],
"deliveries":[{"type":"email","to":["seoul@example.com"],"cc":["hq@example.com"],"messageId":"<…>"}]}, …]}Windows 서비스(NSSM), 작업 스케줄러(run --now), systemd로 실행할 수 있고, 예약 보고서 예제에 각각의 방법과 Cloudflare Workers Cron 버전이 들어 있습니다. Windows 인쇄 프로그램으로 인쇄하는 것은 그 프로그램이 실행 중인 PC에서, 그 사용자가 로그온해 있을 때만 됩니다. 예제 내려받기 (zip)