거래명세서 · 인수증
거래명세서를 백그라운드에서 만들고, 끝나면 알림을 받고, 거래처가 준 계약서 PDF 와 한 파일로 묶습니다. 비동기 렌더 · 웹훅 · 파일 업로드와 병합을 배웁니다.
여기서 배우는 것
편집기로 만들기
- 거래 항목 표와 인수 확인 서명란을 넣습니다. 앞의 청구서·계약서에서 한 것과 같습니다.
- 문서 자체는 편집기에서 끝납니다. 이 레시피가 가르치는 것은 그 다음 — 만든 파일로 무엇을 하느냐입니다.
스키마 · API 로 만들기
- 문서가 크거나 부르는 쪽이 기다릴 수 없으면 POST /v1/create-async 를 씁니다. 작업 id 를 즉시 돌려주고 큐에서 렌더합니다.
- webhook 을 함께 보내면 끝났을 때 그 주소로 POST 를 한 번 보냅니다. https 만 받고, 사설·내부 주소는 큐에 넣기 전에 거절합니다 — 렌더 쿼터를 쓰기 전에 알려 드리기 위해서입니다.
- 알림 본문에는 내려받기 주소가 없습니다. 저희 주소는 그 자체가 권한이라, 알림에 실으면 그 알림을 본 쪽이 문서를 가져갑니다. 주소는 알림을 받은 뒤 여러분의 키로 GET /v1/jobs/{id} 를 불러 얻으십시오.
- 알림은 증거가 아닙니다 — 서명이 없고 누구나 같은 형식으로 POST 할 수 있습니다. "가서 확인하라" 는 신호로만 쓰고, 무엇이 참인지는 언제나 GET /v1/jobs/{id} 로 정하십시오.
- 거래처가 준 PDF 는 POST /v1/files 로 올립니다. 본문에 파일을 그대로 담고 content-type 만 application/pdf 로 주면 됩니다. 10MB 이하이면서 100쪽 이하여야 하고, 24시간 뒤 지워집니다. 쪽수를 따로 막는 이유가 있습니다 — 3,000쪽 PDF 가 600KB 도 안 될 수 있어서 용량만으로는 걸리지 않고, 그렇게 통과한 문서는 받는 분 휴대폰에서 열리지 않습니다.
- 붙이기 전에 GET /v1/files/{id} 로 무엇인지부터 보십시오 — 쪽수, 쪽마다의 크기와 회전, 암호 여부를 알려줍니다. 암호가 걸린 PDF 는 저희가 열 수 없습니다.
- POST /v1/pdf/merge 에 files 를 준 순서대로 이어 붙입니다. 한 번에 20개까지입니다.
만든 뒤에
거래처 PDF 올리기
POST /v1/files
(the body is the PDF itself · content-type: application/pdf)무엇인지 먼저 보기
GET /v1/files/file_01J…
한 파일로 묶기
POST /v1/pdf/merge
{
"files": [
"file_01J…",
"file_01K…"
]
}완성 문서
아래 JSON 이 위 두 순서가 만드는 바로 그 문서입니다. 저희 테스트가 이 문서를 검증하고 경고 없이 렌더되는지 확인하므로, 그대로 붙여 넣어 쓰셔도 됩니다.
{
"v": 1,
"unit": "px",
"page": {
"size": "a4",
"orientation": "portrait",
"margin": {
"t": 48,
"r": 48,
"b": 56,
"l": 48
},
"header": "{{ data.company }}",
"footer": "{{ data.no }} · [[page]] / [[pages]]"
},
"fonts": [],
"styles": [
{
"id": "s_h1",
"fontFamily": "Liberation Sans",
"fontSize": 22,
"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
}
],
"nodes": [
{
"id": "n_title",
"parent": "",
"order": 0,
"type": "label",
"x": 0,
"y": 0,
"w": 400,
"h": 28,
"show": "",
"styleId": "s_h1",
"props": {
"text": "Delivery note",
"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_meta",
"parent": "",
"order": 1,
"type": "label",
"x": 0,
"y": 34,
"w": 697,
"h": 18,
"show": "",
"styleId": "s_l",
"props": {
"text": "{{ data.customer }} · {{ 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": "Customer"
},
{
"id": "n_items",
"parent": "",
"order": 2,
"type": "table",
"x": 0,
"y": 76,
"w": 697,
"h": 180,
"show": "",
"styleId": "s_tbl",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 1,
"stroke": "#c9ced6",
"fill": "#f3f5f7",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 5,
"padX": 8,
"align": "",
"valign": "middle",
"wrap": "",
"bind": "data.items",
"headerHeight": 26,
"rowHeight": 22,
"max": 100,
"columns": [
{
"header": "Item",
"cell": "{{ item.name }}",
"width": 397,
"align": "left"
},
{
"header": "Qty",
"cell": "{{ item.qty }}",
"width": 120,
"align": "right"
},
{
"header": "Amount",
"cell": "{{ format(item.amount, \"currency\", \"USD\", \"en-US\") }}",
"width": 180,
"align": "right"
}
],
"rows": 0,
"cells": [],
"chartKind": "",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Line items"
},
{
"id": "n_rule",
"parent": "",
"order": 3,
"type": "line",
"x": 397,
"y": 300,
"w": 300,
"h": 2,
"show": "",
"styleId": "s_l",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 1,
"stroke": "#111111",
"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": "Signature rule"
},
{
"id": "n_ack",
"parent": "",
"order": 4,
"type": "label",
"x": 397,
"y": 306,
"w": 300,
"h": 18,
"show": "",
"styleId": "s_l",
"props": {
"text": "Received by (signature)",
"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": "Acknowledgement"
}
]
}요청 본문입니다. template 자리에 위 문서를, data 자리에 아래 데이터를 넣습니다.
{
"template": "(the document above)",
"data": {
"company": "BrewMyPDF",
"customer": "Hanbit Corp.",
"no": "DN-2026-0311",
"items": [
{
"name": "Mug",
"qty": 12,
"amount": 180
},
{
"name": "Sticker set",
"qty": 40,
"amount": 120
}
]
},
"webhook": "https://example.com/hooks/BrewMyPDF"
}