청구서
품목이 데이터에서 오고 합계가 그 데이터에서 계산되는 청구서를 만듭니다. 반복 표·통화 서식·합계 계산·머리글과 쪽번호를 여기서 배웁니다.
여기서 배우는 것
편집기로 만들기
- 새 템플릿을 만들고 편집기를 엽니다. 종이는 A4 세로, 여백은 기본값 그대로 둡니다.
- 라벨을 넣고 텍스트에 {{ data.title }} 를 적습니다. 크기 26, 굵기 700 으로 올려 제목처럼 보이게 합니다. 글꼴은 Noto Sans CJK KR 을 고릅니다 — 한글이 들어갈 자리이기 때문입니다.
- 라벨을 둘 더 넣어 받는 곳({{ data.customer }} 귀하)과 문서번호(번호 {{ data.no }})를 적습니다. 문서번호는 상자 정렬로 오른쪽에 붙입니다.
- 표를 넣습니다. 표 속성 맨 위에서 데이터 반복을 고르고, 배열 경로에 data.items 를 적습니다.
- 열을 셋으로 만듭니다. 머리글과 셀 템플릿을 각각 항목/{{ item.name }}, 수량/{{ item.qty }}, 금액/{{ format(item.amount, "currency", "KRW", "ko-KR") }} 으로 넣습니다. 수량과 금액 열은 오른쪽 정렬로 둡니다 — 숫자는 자릿수가 맞아야 읽힙니다.
- 표 아래에 합계 라벨을 놓고 {{ format(sum(data.items, "amount"), "currency", "KRW", "ko-KR") }} 를 적습니다. 합계를 데이터로 받지 않고 계산하면, 품목을 고쳤을 때 합계가 어긋나는 일이 없습니다.
- 표가 길어지면 합계가 밀려야 합니다. 표 아래에 있는 것들은 흐름 구간에 들어가므로, 편집 화면의 주황 안내선이 어디까지 함께 밀리는지 보여줍니다.
- 데이터 탭에서 JSON 을 실제 값으로 바꿔 미리보기를 확인합니다. 자동 샘플은 숫자 자리에 문자열을 넣으므로 통화가 ₩NaN 으로 보입니다 — 진짜 숫자를 넣어야 자릿수와 넘침이 제대로 보입니다.
- 저장합니다. 저장하면 버전이 쌓이고, API 에서 template_id 로 부를 수 있습니다.
스키마 · API 로 만들기
- 문서는 노드의 평면 배열입니다. 제목·받는 곳·번호는 label, 품목은 table 하나입니다.
- 표를 반복 모드로 두는 것은 props.bind 하나입니다. 이 값이 있으면 rows 와 cells 는 쓰지 않고 columns 가 열을 정합니다.
- h 를 0 으로 두면 자동 높이입니다. 반복 표에서는 예약 높이가 되어, 그보다 늘어나면 아래 노드가 밀립니다.
- page.header 와 page.footer 는 모든 페이지에 반복됩니다. 바닥글에서만 [[page]] 와 [[pages]] 를 쓸 수 있습니다.
- 저장해 둔 템플릿이라면 template 대신 template_id 를 보냅니다. 둘 다 보내면 어느 것이 참인지 알 수 없어 거절합니다.
완성 문서
아래 JSON 이 위 두 순서가 만드는 바로 그 문서입니다. 저희 테스트가 이 문서를 검증하고 경고 없이 렌더되는지 확인하므로, 그대로 붙여 넣어 쓰셔도 됩니다.
{
"v": 1,
"unit": "px",
"page": {
"size": "a4",
"orientation": "portrait",
"margin": {
"t": 48,
"r": 48,
"b": 56,
"l": 48
},
"header": "{{ data.company }}",
"footer": "{{ data.company }} · [[page]] / [[pages]]"
},
"fonts": [],
"styles": [
{
"id": "s_h1",
"fontFamily": "Liberation Sans",
"fontSize": 26,
"fontWeight": 700,
"italic": false,
"color": "#111111",
"bg": "transparent",
"align": "left",
"valign": "top",
"lineHeight": 1.3,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_body",
"fontFamily": "Liberation Sans",
"fontSize": 13,
"fontWeight": 400,
"italic": false,
"color": "#333333",
"bg": "transparent",
"align": "left",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_right",
"fontFamily": "Liberation Sans",
"fontSize": 13,
"fontWeight": 400,
"italic": false,
"color": "#333333",
"bg": "transparent",
"align": "right",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_tbl",
"fontFamily": "Liberation Sans",
"fontSize": 12,
"fontWeight": 400,
"italic": false,
"color": "#222222",
"bg": "transparent",
"align": "left",
"valign": "middle",
"lineHeight": 1.4,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_total",
"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": 380,
"h": 34,
"show": "",
"styleId": "s_h1",
"props": {
"text": "{{ data.title }}",
"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": "Title"
},
{
"id": "n_to",
"parent": "",
"order": 1,
"type": "label",
"x": 0,
"y": 44,
"w": 380,
"h": 0,
"show": "",
"styleId": "s_body",
"props": {
"text": "To {{ data.customer }}",
"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": "Bill to"
},
{
"id": "n_no",
"parent": "",
"order": 2,
"type": "label",
"x": 420,
"y": 6,
"w": 277,
"h": 20,
"show": "",
"styleId": "s_right",
"props": {
"text": "No. {{ data.no }}",
"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": "Number"
},
{
"id": "n_items",
"parent": "",
"order": 3,
"type": "table",
"x": 0,
"y": 96,
"w": 697,
"h": 160,
"show": "",
"styleId": "s_tbl",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 1,
"stroke": "#c9ced6",
"fill": "#f3f5f7",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 6,
"padX": 8,
"align": "",
"valign": "middle",
"wrap": "",
"bind": "data.items",
"headerHeight": 30,
"rowHeight": 26,
"max": 200,
"columns": [
{
"header": "Item",
"cell": "{{ item.name }}",
"width": 397,
"align": "left"
},
{
"header": "Qty",
"cell": "{{ item.qty }}",
"width": 100,
"align": "right"
},
{
"header": "Amount",
"cell": "{{ format(item.amount, \"currency\", \"USD\", \"en-US\") }}",
"width": 200,
"align": "right"
}
],
"rows": 0,
"cells": [],
"chartKind": "",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Line items"
},
{
"id": "n_total",
"parent": "",
"order": 4,
"type": "label",
"x": 0,
"y": 268,
"w": 697,
"h": 26,
"show": "",
"styleId": "s_total",
"props": {
"text": "Total {{ format(sum(data.items, \"amount\"), \"currency\", \"USD\", \"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": "Total"
}
]
}요청 본문입니다. template 자리에 위 문서를, data 자리에 아래 데이터를 넣습니다.
{
"template": "(the document above)",
"data": {
"company": "brewmypdf",
"title": "Invoice",
"customer": "Hanbit Corp.",
"no": "INV-2026-0042",
"items": [
{
"name": "Brand design",
"qty": 1,
"amount": 2400
},
{
"name": "Website build",
"qty": 1,
"amount": 1500
},
{
"name": "Maintenance (3 months)",
"qty": 3,
"amount": 280
}
]
}
}