발주서
공급자와 납품 조건을 칸에 적어 넣는 발주서를 만듭니다. 손으로 짜는 격자 표, 칸 합치기, 칸별 여백과 정렬, 그리고 값이 없을 때 줄을 통째로 감추는 법을 배웁니다.
여기서 배우는 것
편집기로 만들기
- 표를 넣고 표 속성 맨 위에서 손으로 짜기를 고릅니다. 행을 6, 열을 4 로 맞춥니다.
- 왼쪽 두 열을 라벨 열로 쓸 것이므로 좁게 줄입니다. 열 경계를 끌면 폭이 바뀌고, 열 폭의 합은 표 폭과 항상 같게 유지됩니다.
- 캔버스에서 칸을 직접 클릭해 고릅니다. 고른 칸에 글을 넣고, 라벨 칸은 머리 칸으로 켜서 배경색이 들어가게 합니다.
- 주소처럼 긴 값은 한 칸으로는 좁습니다. 그 칸을 고르고 오른쪽 칸과 합치기를 눌러 두 칸을 하나로 만듭니다.
- 값 칸에는 {{ data.supplier }} 처럼 바인딩을 넣습니다. 날짜는 {{ format(data.due, "date", "long", "ko-KR") }} 로 적으면 사람이 읽는 형식으로 나옵니다.
- 칸이 답답하면 그 칸만 여백을 늘립니다. 칸을 고르면 표 전체 여백은 숨고 이 칸 여백만 보입니다 — 지금 무엇을 고치는지 화면이 말해 줍니다.
- 비고처럼 있을 때만 넣는 줄은, 그 줄을 담은 객체의 조건부 표시에 {{ data.note }} 를 적습니다. 값이 없으면 그 줄이 통째로 빠지고 빈 자리도 남지 않습니다.
- 선을 하나 넣어 서명란 위에 긋습니다. 도형은 색과 두께만 있으면 되고, 굳이 표로 만들 필요가 없습니다.
스키마 · API 로 만들기
- 격자 표는 props.bind 를 비워 둡니다. 대신 rows 로 행 수를, cells 로 칸을 적습니다.
- cells 는 있는 칸만 적으면 됩니다. 없는 자리는 빈 칸이므로 24칸을 다 적지 않아도 됩니다.
- span 은 오른쪽으로, rowspan 은 아래로 합칩니다. 합쳐진 자리에 다른 칸을 또 적지 마십시오.
- head 를 true 로 두면 머리 칸이 되고 표의 fill 색이 그 칸에만 칠해집니다.
- 노드의 show 는 표현식입니다. 값이 없거나 빈 문자열이면 그 노드와 안에 든 것이 출력에서 통째로 빠집니다.
완성 문서
아래 JSON 이 위 두 순서가 만드는 바로 그 문서입니다. 저희 테스트가 이 문서를 검증하고 경고 없이 렌더되는지 확인하므로, 그대로 붙여 넣어 쓰셔도 됩니다.
{
"v": 1,
"unit": "px",
"page": {
"size": "a4",
"orientation": "portrait",
"margin": {
"t": 56,
"r": 48,
"b": 56,
"l": 48
},
"header": "",
"footer": ""
},
"fonts": [],
"styles": [
{
"id": "s_h1",
"fontFamily": "Liberation Sans",
"fontSize": 24,
"fontWeight": 700,
"italic": false,
"color": "#111111",
"bg": "transparent",
"align": "left",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_tbl",
"fontFamily": "Liberation Sans",
"fontSize": 12,
"fontWeight": 400,
"italic": false,
"color": "#111111",
"bg": "transparent",
"align": "left",
"valign": "middle",
"lineHeight": 1.4,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_note",
"fontFamily": "Liberation Sans",
"fontSize": 11,
"fontWeight": 400,
"italic": false,
"color": "#555555",
"bg": "transparent",
"align": "left",
"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": 697,
"h": 32,
"show": "",
"styleId": "s_h1",
"props": {
"text": "Purchase order",
"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_head",
"parent": "",
"order": 1,
"type": "table",
"x": 0,
"y": 52,
"w": 697,
"h": 120,
"show": "",
"styleId": "s_tbl",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 1,
"stroke": "#c9ced6",
"fill": "#f3f5f7",
"repeat": "",
"as": "",
"break": "",
"keep": "",
"padding": 0,
"padY": 7,
"padX": 9,
"align": "",
"valign": "middle",
"wrap": "",
"bind": "",
"headerHeight": 0,
"rowHeight": 30,
"max": 0,
"columns": [
{
"header": "",
"cell": "",
"width": 110,
"align": "left"
},
{
"header": "",
"cell": "",
"width": 238,
"align": "left"
},
{
"header": "",
"cell": "",
"width": 110,
"align": "left"
},
{
"header": "",
"cell": "",
"width": 239,
"align": "left"
}
],
"rows": 4,
"cells": [
{
"r": 0,
"c": 0,
"span": 1,
"rowspan": 1,
"text": "Supplier",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": true,
"border": ""
},
{
"r": 0,
"c": 1,
"span": 1,
"rowspan": 1,
"text": "{{ data.supplier }}",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": false,
"border": ""
},
{
"r": 0,
"c": 2,
"span": 1,
"rowspan": 1,
"text": "PO number",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": true,
"border": ""
},
{
"r": 0,
"c": 3,
"span": 1,
"rowspan": 1,
"text": "{{ data.no }}",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": false,
"border": ""
},
{
"r": 1,
"c": 0,
"span": 1,
"rowspan": 1,
"text": "Ship to",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": true,
"border": ""
},
{
"r": 1,
"c": 1,
"span": 3,
"rowspan": 1,
"text": "{{ data.shipTo }}",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": 12,
"src": "",
"fit": "",
"bg": "",
"head": false,
"border": ""
},
{
"r": 2,
"c": 0,
"span": 1,
"rowspan": 1,
"text": "Due",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": true,
"border": ""
},
{
"r": 2,
"c": 1,
"span": 1,
"rowspan": 1,
"text": "{{ format(data.due, \"date\", \"long\", \"en-US\") }}",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": false,
"border": ""
},
{
"r": 2,
"c": 2,
"span": 1,
"rowspan": 1,
"text": "Terms",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": true,
"border": ""
},
{
"r": 2,
"c": 3,
"span": 1,
"rowspan": 1,
"text": "{{ data.terms }}",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": false,
"border": ""
},
{
"r": 3,
"c": 0,
"span": 1,
"rowspan": 1,
"text": "Contact",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": true,
"border": ""
},
{
"r": 3,
"c": 1,
"span": 3,
"rowspan": 1,
"text": "{{ data.contact }}",
"align": "left",
"valign": "",
"wrap": "",
"pad": -1,
"padY": -1,
"padX": -1,
"src": "",
"fit": "",
"bg": "",
"head": false,
"border": ""
}
],
"chartKind": "",
"codeKind": "",
"ecc": "",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Order details"
},
{
"id": "n_note",
"parent": "",
"order": 2,
"type": "label",
"x": 0,
"y": 188,
"w": 697,
"h": 20,
"show": "{{ data.note }}",
"styleId": "s_note",
"props": {
"text": "Note: {{ data.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": "Note"
},
{
"id": "n_rule",
"parent": "",
"order": 3,
"type": "line",
"x": 397,
"y": 256,
"w": 300,
"h": 2,
"show": "",
"styleId": "s_note",
"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_sign",
"parent": "",
"order": 4,
"type": "label",
"x": 397,
"y": 262,
"w": 300,
"h": 18,
"show": "",
"styleId": "s_note",
"props": {
"text": "{{ data.approver ? data.approver : \"(awaiting approval)\" }}",
"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": "Approver"
}
]
}요청 본문입니다. template 자리에 위 문서를, data 자리에 아래 데이터를 넣습니다.
{
"template": "(the document above)",
"data": {
"supplier": "Hanbit Trading Ltd.",
"no": "PO-2026-0117",
"shipTo": "118 Seongsui-ro, Seongdong-gu, Seoul — 3F warehouse",
"due": "2026-09-15",
"terms": "Net 30 after delivery",
"contact": "Do-hyun Kim · +82 10 0000 0000",
"note": "Fragile — please load by pallet.",
"approver": ""
}
}