급여명세서
한 번의 호출로 전 직원의 급여명세서를 만들고, 개인정보이므로 암호를 걸어 내보냅니다. 배치, 암호와 권한, 그리고 월별 추이 막대 차트를 배웁니다.
여기서 배우는 것
편집기로 만들기
- 한 사람분 명세서를 그대로 만듭니다. 배치는 이 템플릿 하나를 사람 수만큼 부르는 것이므로, 템플릿 안에 사람 목록을 넣지 않습니다.
- 지급 항목과 공제 항목을 반복 표 둘로 만듭니다. 배열 경로는 각각 data.earnings 와 data.deductions 입니다.
- 실지급액은 계산해서 씁니다. {{ format(sum(data.earnings, "amount") - sum(data.deductions, "amount"), "number", "0,0", "ko-KR") }} 처럼 두면 두 표와 절대 어긋나지 않습니다.
- 차트를 넣고 종류를 막대로 둡니다. 배열 경로에 data.trend 를, 라벨 식에 {{ item.month }}, 값 식에 {{ item.net }} 을 적습니다.
- 차트는 표와 같은 모양의 값을 씁니다 — 배열 하나에 열 정의 둘입니다. 새 개념을 배우지 않아도 되게 일부러 그렇게 두었습니다.
- 저장하고 template_id 를 확인합니다. 배치는 항목마다 template_id 와 그 사람의 data 를 갖습니다.
스키마 · API 로 만들기
- POST /v1/batch 에 items 배열을 보냅니다. 최대 50건이며, 항목마다 자기 template 또는 template_id 와 자기 data 를 갖습니다.
- merge 를 true 로 두면 한 파일로 이어 붙여 돌려줍니다. 이쪽은 20건이 상한이고, 이때만 기다렸다가 내려받기 주소를 줍니다.
- 합친 파일은 각 문서의 머리글·바닥글을 그대로 지니고, 쪽번호를 다시 매기지 않습니다 — 여러 문서를 한 파일에 담은 것이지 한 문서가 아니기 때문입니다.
- 쿼터는 배치 전체를 놓고 큐에 넣기 전에 한 번 봅니다. 잔여가 모자라면 통째로 거절합니다 — 반쯤 처리된 배치는 환불도 재시도도 애매합니다.
- password 로 열람 암호를, owner_password 로 권한 암호를 겁니다. 둘을 같게 두면 아래 permissions 가 사실상 의미가 없어집니다.
- 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
}
}