← 모든 레시피

급여명세서

한 번의 호출로 전 직원의 급여명세서를 만들고, 개인정보이므로 암호를 걸어 내보냅니다. 배치, 암호와 권한, 그리고 월별 추이 막대 차트를 배웁니다.

이 템플릿 가져오기

여기서 배우는 것

  • node-chart
  • chart-bar
  • api-batch
  • api-batch-merge
  • api-password
  • api-permissions
  • expr-math
  • expr-format-number
  • cell-align

편집기로 만들기

  1. 한 사람분 명세서를 그대로 만듭니다. 배치는 이 템플릿 하나를 사람 수만큼 부르는 것이므로, 템플릿 안에 사람 목록을 넣지 않습니다.
  2. 지급 항목과 공제 항목을 반복 표 둘로 만듭니다. 배열 경로는 각각 data.earnings 와 data.deductions 입니다.
  3. 실지급액은 계산해서 씁니다. {{ format(sum(data.earnings, "amount") - sum(data.deductions, "amount"), "number", "0,0", "ko-KR") }} 처럼 두면 두 표와 절대 어긋나지 않습니다.
  4. 차트를 넣고 종류를 막대로 둡니다. 배열 경로에 data.trend 를, 라벨 식에 {{ item.month }}, 값 식에 {{ item.net }} 을 적습니다.
  5. 차트는 표와 같은 모양의 값을 씁니다 — 배열 하나에 열 정의 둘입니다. 새 개념을 배우지 않아도 되게 일부러 그렇게 두었습니다.
  6. 저장하고 template_id 를 확인합니다. 배치는 항목마다 template_id 와 그 사람의 data 를 갖습니다.

스키마 · API 로 만들기

  1. POST /v1/batch 에 items 배열을 보냅니다. 최대 50건이며, 항목마다 자기 template 또는 template_id 와 자기 data 를 갖습니다.
  2. merge 를 true 로 두면 한 파일로 이어 붙여 돌려줍니다. 이쪽은 20건이 상한이고, 이때만 기다렸다가 내려받기 주소를 줍니다.
  3. 합친 파일은 각 문서의 머리글·바닥글을 그대로 지니고, 쪽번호를 다시 매기지 않습니다 — 여러 문서를 한 파일에 담은 것이지 한 문서가 아니기 때문입니다.
  4. 쿼터는 배치 전체를 놓고 큐에 넣기 전에 한 번 봅니다. 잔여가 모자라면 통째로 거절합니다 — 반쯤 처리된 배치는 환불도 재시도도 애매합니다.
  5. password 로 열람 암호를, owner_password 로 권한 암호를 겁니다. 둘을 같게 두면 아래 permissions 가 사실상 의미가 없어집니다.
  6. permissions 로 print · copy · modify · annotate · fill 을 각각 끕니다. 화면 읽기 프로그램을 위한 텍스트 추출은 저희가 막지 않습니다.

완성 문서

아래 JSON 이 위 두 순서가 만드는 바로 그 문서입니다. 저희 테스트가 이 문서를 검증하고 경고 없이 렌더되는지 확인하므로, 그대로 붙여 넣어 쓰셔도 됩니다.

예시
{
  "v": 1,
  "unit": "px",
  "page": {
    "size": "a4",
    "orientation": "portrait",
    "margin": {
      "t": 48,
      "r": 48,
      "b": 56,
      "l": 48
    },
    "header": "{{ data.company }} payslip",
    "footer": "{{ data.period }} · [[page]] / [[pages]]"
  },
  "fonts": [],
  "styles": [
    {
      "id": "s_h1",
      "fontFamily": "Liberation Sans",
      "fontSize": 20,
      "fontWeight": 700,
      "italic": false,
      "color": "#111111",
      "bg": "transparent",
      "align": "left",
      "valign": "top",
      "lineHeight": 1.5,
      "border": "none",
      "radius": 0,
      "opacity": 1
    },
    {
      "id": "s_l",
      "fontFamily": "Liberation Sans",
      "fontSize": 12,
      "fontWeight": 400,
      "italic": false,
      "color": "#333333",
      "bg": "transparent",
      "align": "left",
      "valign": "top",
      "lineHeight": 1.5,
      "border": "none",
      "radius": 0,
      "opacity": 1
    },
    {
      "id": "s_tbl",
      "fontFamily": "Liberation Sans",
      "fontSize": 11,
      "fontWeight": 400,
      "italic": false,
      "color": "#111111",
      "bg": "transparent",
      "align": "left",
      "valign": "middle",
      "lineHeight": 1.5,
      "border": "none",
      "radius": 0,
      "opacity": 1
    },
    {
      "id": "s_net",
      "fontFamily": "Liberation Sans",
      "fontSize": 18,
      "fontWeight": 700,
      "italic": false,
      "color": "#111111",
      "bg": "transparent",
      "align": "right",
      "valign": "top",
      "lineHeight": 1.5,
      "border": "none",
      "radius": 0,
      "opacity": 1
    }
  ],
  "nodes": [
    {
      "id": "n_title",
      "parent": "",
      "order": 0,
      "type": "label",
      "x": 0,
      "y": 0,
      "w": 400,
      "h": 26,
      "show": "",
      "styleId": "s_h1",
      "props": {
        "text": "{{ data.employee.name }}",
        "src": "",
        "fit": "",
        "thickness": 0,
        "stroke": "",
        "fill": "",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 0,
        "padX": 0,
        "align": "",
        "valign": "",
        "wrap": "",
        "bind": "",
        "headerHeight": 0,
        "rowHeight": 0,
        "max": 0,
        "columns": [],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Name"
    },
    {
      "id": "n_meta",
      "parent": "",
      "order": 1,
      "type": "label",
      "x": 0,
      "y": 30,
      "w": 400,
      "h": 18,
      "show": "",
      "styleId": "s_l",
      "props": {
        "text": "{{ data.employee.dept }} · ID {{ data.employee.no }} · {{ data.period }}",
        "src": "",
        "fit": "",
        "thickness": 0,
        "stroke": "",
        "fill": "",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 0,
        "padX": 0,
        "align": "",
        "valign": "",
        "wrap": "",
        "bind": "",
        "headerHeight": 0,
        "rowHeight": 0,
        "max": 0,
        "columns": [],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Department"
    },
    {
      "id": "n_earn",
      "parent": "",
      "order": 2,
      "type": "table",
      "x": 0,
      "y": 76,
      "w": 340,
      "h": 150,
      "show": "",
      "styleId": "s_tbl",
      "props": {
        "text": "",
        "src": "",
        "fit": "",
        "thickness": 1,
        "stroke": "#c9ced6",
        "fill": "#eef4ff",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 5,
        "padX": 8,
        "align": "",
        "valign": "middle",
        "wrap": "",
        "bind": "data.earnings",
        "headerHeight": 26,
        "rowHeight": 22,
        "max": 30,
        "columns": [
          {
            "header": "Earnings",
            "cell": "{{ item.name }}",
            "width": 200,
            "align": "left"
          },
          {
            "header": "Amount",
            "cell": "{{ format(item.amount, \"number\", \"0,0\", \"en-US\") }}",
            "width": 140,
            "align": "right"
          }
        ],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Earnings"
    },
    {
      "id": "n_ded",
      "parent": "",
      "order": 3,
      "type": "table",
      "x": 357,
      "y": 76,
      "w": 340,
      "h": 150,
      "show": "",
      "styleId": "s_tbl",
      "props": {
        "text": "",
        "src": "",
        "fit": "",
        "thickness": 1,
        "stroke": "#c9ced6",
        "fill": "#fff1f0",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 5,
        "padX": 8,
        "align": "",
        "valign": "middle",
        "wrap": "",
        "bind": "data.deductions",
        "headerHeight": 26,
        "rowHeight": 22,
        "max": 30,
        "columns": [
          {
            "header": "Deductions",
            "cell": "{{ item.name }}",
            "width": 200,
            "align": "left"
          },
          {
            "header": "Amount",
            "cell": "{{ format(item.amount, \"number\", \"0,0\", \"en-US\") }}",
            "width": 140,
            "align": "right"
          }
        ],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Deductions"
    },
    {
      "id": "n_net",
      "parent": "",
      "order": 4,
      "type": "label",
      "x": 0,
      "y": 244,
      "w": 697,
      "h": 26,
      "show": "",
      "styleId": "s_net",
      "props": {
        "text": "Net pay {{ format(sum(data.earnings, \"amount\") - sum(data.deductions, \"amount\"), \"number\", \"0,0\", \"en-US\") }}",
        "src": "",
        "fit": "",
        "thickness": 0,
        "stroke": "",
        "fill": "",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 0,
        "padX": 0,
        "align": "",
        "valign": "",
        "wrap": "",
        "bind": "",
        "headerHeight": 0,
        "rowHeight": 0,
        "max": 0,
        "columns": [],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Net pay"
    },
    {
      "id": "n_trend_l",
      "parent": "",
      "order": 5,
      "type": "label",
      "x": 0,
      "y": 296,
      "w": 697,
      "h": 18,
      "show": "",
      "styleId": "s_l",
      "props": {
        "text": "Recent net pay",
        "src": "",
        "fit": "",
        "thickness": 0,
        "stroke": "",
        "fill": "",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 0,
        "padX": 0,
        "align": "",
        "valign": "",
        "wrap": "",
        "bind": "",
        "headerHeight": 0,
        "rowHeight": 0,
        "max": 0,
        "columns": [],
        "rows": 0,
        "cells": [],
        "chartKind": "",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Trend heading"
    },
    {
      "id": "n_trend",
      "parent": "",
      "order": 6,
      "type": "chart",
      "x": 0,
      "y": 320,
      "w": 697,
      "h": 200,
      "show": "",
      "styleId": "s_l",
      "props": {
        "text": "",
        "src": "",
        "fit": "",
        "thickness": 0,
        "stroke": "",
        "fill": "",
        "repeat": "",
        "as": "",
        "break": "",
        "keep": "",
        "padding": 0,
        "padY": 0,
        "padX": 0,
        "align": "",
        "valign": "",
        "wrap": "",
        "bind": "data.trend",
        "headerHeight": 0,
        "rowHeight": 0,
        "max": 0,
        "columns": [
          {
            "header": "Month",
            "cell": "{{ item.month }}",
            "width": 0,
            "align": "left"
          },
          {
            "header": "Net",
            "cell": "{{ item.net }}",
            "width": 0,
            "align": "right"
          }
        ],
        "rows": 0,
        "cells": [],
        "chartKind": "bar",
        "codeKind": "",
        "ecc": "",
        "fieldKind": "",
        "fieldName": "",
        "options": [],
        "required": false,
        "readonly": false,
        "html": ""
      },
      "name": "Trend chart"
    }
  ]
}

요청 본문입니다. template 자리에 위 문서를, data 자리에 아래 데이터를 넣습니다.

요청
{
  "template": "(the document above)",
  "data": {
    "company": "brewmypdf",
    "period": "2026-08",
    "employee": {
      "name": "Do-hyun Kim",
      "dept": "Engineering",
      "no": "E-2041"
    },
    "earnings": [
      {
        "name": "Base salary",
        "amount": 4200
      },
      {
        "name": "Role allowance",
        "amount": 300
      },
      {
        "name": "Meal allowance",
        "amount": 200
      }
    ],
    "deductions": [
      {
        "name": "Pension",
        "amount": 189
      },
      {
        "name": "Health insurance",
        "amount": 148
      },
      {
        "name": "Income tax",
        "amount": 262
      }
    ],
    "trend": [
      {
        "month": "May",
        "net": 4050
      },
      {
        "month": "Jun",
        "net": 4090
      },
      {
        "month": "Jul",
        "net": 4010
      },
      {
        "month": "Aug",
        "net": 4101
      }
    ]
  },
  "password": "{{employee date of birth, 6 digits}}",
  "owner_password": "{{your owner password}}",
  "permissions": {
    "print": true,
    "copy": false,
    "modify": false,
    "annotate": false,
    "fill": false
  }
}