FreeReportv0.1.0한국어

Output control

PDF password and permissions, an output log written before every print or export, and cancelling outputs.

Report definition (JSON)

{
  "name": "재직증명서",
  "page": {
    "size": "A4",
    "margins": 20
  },
  "fingerprint": true,
  "font": {
    "family": "Nanum Gothic",
    "size": 10.5
  },
  "watermark": [
    {
      "text": "{params.user} · {format(now(), \"yyyy-MM-dd HH:mm\")} 출력",
      "repeat": true,
      "opacity": 0.07,
      "fontSize": 9,
      "gap": 34,
      "angle": -30
    },
    {
      "text": "원 본",
      "opacity": 0.06,
      "fontSize": 120,
      "color": "#2340f5"
    }
  ],
  "emboss": {
    "text": "(주)세컨드코리아 대표이사 직인 ·",
    "centerText": "인증",
    "x": 128,
    "y": 232,
    "w": 36,
    "depth": 0.35
  },
  "bands": {
    "title": {
      "height": 240,
      "elements": [
        {
          "type": "text",
          "x": 0,
          "y": 0,
          "w": 85,
          "h": 7,
          "text": "{docNo}",
          "style": {
            "fontSize": 9,
            "color": "#555"
          }
        },
        {
          "type": "text",
          "x": 0,
          "y": 14,
          "w": 170,
          "h": 18,
          "text": "재 직 증 명 서",
          "style": {
            "fontSize": 26,
            "bold": true,
            "align": "center",
            "letterSpacing": 1.5
          }
        },
        {
          "type": "text",
          "x": 0,
          "y": 44,
          "w": 10,
          "h": 33,
          "text": "인\n적\n사\n항",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333",
            "lineHeight": 1.5
          }
        },
        {
          "type": "text",
          "x": 10,
          "y": 44,
          "w": 30,
          "h": 11,
          "text": "성    명",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333"
          }
        },
        {
          "type": "text",
          "x": 40,
          "y": 44,
          "w": 50,
          "h": 11,
          "text": "{person.name}",
          "style": {
            "border": "0.25 solid #333",
            "padding": [
              0.5,
              2,
              0.5,
              2
            ]
          }
        },
        {
          "type": "text",
          "x": 90,
          "y": 44,
          "w": 30,
          "h": 11,
          "text": "생년월일",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333"
          }
        },
        {
          "type": "text",
          "x": 120,
          "y": 44,
          "w": 50,
          "h": 11,
          "text": "{format(person.birth, \"yyyy년 M월 d일\")}",
          "style": {
            "border": "0.25 solid #333",
            "padding": [
              0.5,
              2,
              0.5,
              2
            ]
          }
        },
        {
          "type": "text",
          "x": 10,
          "y": 55,
          "w": 30,
          "h": 22,
          "text": "주    소",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333"
          }
        },
        {
          "type": "text",
          "x": 40,
          "y": 55,
          "w": 130,
          "h": 22,
          "text": "{person.address}",
          "style": {
            "border": "0.25 solid #333",
            "padding": [
              0.5,
              2,
              0.5,
              2
            ]
          }
        },
        {
          "type": "text",
          "x": 0,
          "y": 77,
          "w": 10,
          "h": 33,
          "text": "재\n직\n사\n항",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333",
            "lineHeight": 1.5
          }
        },
        {
          "type": "text",
          "x": 10,
          "y": 77,
          "w": 30,
          "h": 11,
          "text": "회 사 명",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333"
          }
        },
        {
          "type": "text",
          "x": 40,
          "y": 77,
          "w": 130,
          "h": 11,
          "text": "{company.name}",
          "style": {
            "border": "0.25 solid #333",
            "padding": [
              0.5,
              2,
              0.5,
              2
            ]
          }
        },
        {
          "type": "text",
          "x": 10,
          "y": 88,
          "w": 30,
          "h": 11,
          "text": "부    서",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333"
          }
        },
        {
          "type": "text",
          "x": 40,
          "y": 88,
          "w": 50,
          "h": 11,
          "text": "{person.dept}",
          "style": {
            "border": "0.25 solid #333",
            "padding": [
              0.5,
              2,
              0.5,
              2
            ]
          }
        },
        {
          "type": "text",
          "x": 90,
          "y": 88,
          "w": 30,
          "h": 11,
          "text": "직    위",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333"
          }
        },
        {
          "type": "text",
          "x": 120,
          "y": 88,
          "w": 50,
          "h": 11,
          "text": "{person.position}",
          "style": {
            "border": "0.25 solid #333",
            "padding": [
              0.5,
              2,
              0.5,
              2
            ]
          }
        },
        {
          "type": "text",
          "x": 10,
          "y": 99,
          "w": 30,
          "h": 11,
          "text": "재직기간",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333"
          }
        },
        {
          "type": "text",
          "x": 40,
          "y": 99,
          "w": 130,
          "h": 11,
          "text": "{format(person.joined, \"yyyy년 M월 d일\")} ~ 현재  ({floor(dateDiff(person.joined, issued) / 365)}년 {floor((dateDiff(person.joined, issued) % 365) / 30)}개월)",
          "style": {
            "border": "0.25 solid #333",
            "padding": [
              0.5,
              2,
              0.5,
              2
            ]
          }
        },
        {
          "type": "text",
          "x": 0,
          "y": 110,
          "w": 40,
          "h": 11,
          "text": "용    도",
          "style": {
            "background": "#f6f7f9",
            "bold": true,
            "align": "center",
            "border": "0.25 solid #333"
          }
        },
        {
          "type": "text",
          "x": 40,
          "y": 110,
          "w": 130,
          "h": 11,
          "text": "{purpose}",
          "style": {
            "border": "0.25 solid #333",
            "padding": [
              0.5,
              2,
              0.5,
              2
            ]
          }
        },
        {
          "type": "text",
          "x": 0,
          "y": 136,
          "w": 170,
          "h": 10,
          "text": "위와 같이 재직하고 있음을 증명합니다.",
          "style": {
            "align": "center",
            "fontSize": 13
          }
        },
        {
          "type": "text",
          "x": 0,
          "y": 160,
          "w": 170,
          "h": 10,
          "text": "{format(issued, \"yyyy년 M월 d일\")}",
          "style": {
            "align": "center",
            "fontSize": 12
          }
        },
        {
          "type": "text",
          "x": 0,
          "y": 190,
          "w": 170,
          "h": 12,
          "text": "{company.name}  대표이사  {company.ceo}",
          "style": {
            "align": "center",
            "fontSize": 15,
            "bold": true,
            "letterSpacing": 0.6
          }
        },
        {
          "type": "image",
          "x": 131,
          "y": 184,
          "w": 22,
          "h": 22,
          "src": "data:image/svg+xml;base64,… (1,866 chars)"
        },
        {
          "type": "text",
          "x": 0,
          "y": 203,
          "w": 170,
          "h": 7,
          "text": "{company.address}",
          "style": {
            "align": "center",
            "fontSize": 9,
            "color": "#555"
          }
        },
        {
          "type": "barcode",
          "x": 0,
          "y": 226,
          "w": 22,
          "h": 22,
          "expr": "\"https://freereport.app/verify/\" + replace(docNo, \" \", \"\") + \"?h=\" + docHashShort",
          "format": "qr",
          "ecc": "Q"
        },
        {
          "type": "text",
          "x": 24,
          "y": 230,
          "w": 90,
          "h": 14,
          "text": "이 증명서는 위·변조 방지 처리가 되어 있습니다.\nQR 코드로 원본 여부를 확인할 수 있습니다.\n확인코드 {docHashShort}",
          "style": {
            "fontSize": 7.5,
            "color": "#555",
            "valign": "top"
          }
        }
      ]
    }
  },
  "params": {
    "user": {
      "label": "출력자",
      "type": "string",
      "default": "김민준 (개발팀)"
    }
  }
}

Data

Long lists show their first three items.

{
  "docNo": "제 2026-0412 호",
  "person": {
    "name": "홍길동",
    "birth": "1990-03-15",
    "address": "서울특별시 중구 세종대로 110",
    "dept": "개발팀",
    "position": "책임연구원",
    "joined": "2019-03-04"
  },
  "purpose": "금융기관 제출용",
  "company": {
    "name": "(주)세컨드코리아",
    "ceo": "구명석",
    "address": "경기도 파주시 와석순환로 507, 701호"
  },
  "issued": "2026-10-15"
}

Code for this demo

import type { ReportDefinition, Viewer } from '@freereportjs/core';
import { certificateData } from '../../samples/data';
import { certificateReport } from '../../samples/reports';
import { mountViewer, outputLine, textInput, toggle, tr, type DemoContext } from '../ui';

/**
 * Output control: the watermark names who printed the page and when, onOutput writes an output log
 * (or cancels the output), and the PDF gets an open password and copy/print permissions.
 */
const base = certificateReport();
export const definition: ReportDefinition = {
  ...base,
  params: { user: { label: '출력자', type: 'string', default: '김민준 (개발팀)' } },
  watermark: [
    { text: '{params.user} · {format(now(), "yyyy-MM-dd HH:mm")} 출력', repeat: true, opacity: 0.07, fontSize: 9, gap: 34, angle: -30 },
    { text: '원 본', opacity: 0.06, fontSize: 120, color: '#2340f5' },
  ],
};
export const sampleData = certificateData;

export default function mount(ctx: DemoContext) {
  const state = { user: '김민준 (개발팀)', password: '', noCopy: true, noPrint: false, block: false };
  let viewer: Viewer | null = null;

  // pdfSecurity is a report option, so the viewer is rebuilt when it changes
  const open = () => {
    viewer?.destroy();
    viewer = mountViewer(ctx, {
      definition,
      data: sampleData,
      params: { user: state.user },
      zoom: 'page',
      fileName: '재직증명서',
      pdfSecurity: { userPassword: state.password || undefined, permissions: { copy: !state.noCopy, print: !state.noPrint } },
      onOutput: e => {
        // a real system would POST this to its output log (개인정보 출력 이력) before allowing it
        ctx.log(`${outputLine(ctx.lang, e)} · ${tr(ctx.lang, { en: 'user', ko: '출력자' })} ${String(e.params.user ?? '')}${state.block ? tr(ctx.lang, { en: ' → blocked', ko: ' → 차단' }) : ''}`);
        return !state.block;
      },
    });
  };

  textInput(ctx, tr(ctx.lang, { en: 'User', ko: '출력자' }), state.user, v => { state.user = v; void viewer?.setParams({ user: v }); });
  textInput(ctx, tr(ctx.lang, { en: 'PDF password', ko: 'PDF 열기 암호' }), '', v => { state.password = v; open(); });
  toggle(ctx, tr(ctx.lang, { en: 'PDF: no copying', ko: 'PDF 복사 금지' }), state.noCopy, on => { state.noCopy = on; open(); });
  toggle(ctx, tr(ctx.lang, { en: 'PDF: no printing', ko: 'PDF 인쇄 금지' }), state.noPrint, on => { state.noPrint = on; open(); });
  toggle(ctx, tr(ctx.lang, { en: 'Block outputs', ko: '출력 막기' }), state.block, on => { state.block = on; });
  open();
}