증명서 · 수료증
한 번의 요청으로 수강생 전원의 수료증을 사람마다 한 장씩 만듭니다. 구역 반복, 항목마다 새 페이지, 로고 이미지, 진위 확인용 QR 을 배웁니다.
여기서 배우는 것
편집기로 만들기
- 사각형을 하나 넣어 페이지 전체를 덮습니다. 이 사각형이 한 사람분의 틀이 됩니다 — 안에 든 것은 전부 이 틀을 따라 복사됩니다.
- 사각형의 속성에서 반복할 배열 경로에 data.students 를 적고, 항목을 부를 이름을 st 로 둡니다. 이제 안에서 {{ st.name }} 처럼 씁니다.
- "항목마다 새 페이지" 를 켭니다. 수료증은 한 사람에 한 장이어야 하기 때문입니다.
- 틀 안에 라벨을 넣어 제목과 이름을 적습니다. 이름은 크게, 가운데 정렬로 둡니다. 문구는 기울임을 켜면 증서다운 느낌이 납니다.
- 이미지를 넣고 로고 파일을 고릅니다. 파일을 고르면 편집기가 data URI 로 바꿔 문서에 담습니다 — 외부 주소는 렌더가 거절하므로, 파일을 고르는 것이 유일한 방법입니다.
- QR·바코드를 넣고 인코딩할 값에 확인 주소를 적습니다. {{ st.code }} 처럼 사람마다 다른 값을 넣으면 벌마다 다른 QR 이 나옵니다.
- QR 의 오류 정정을 Q 로 올립니다. 인쇄물은 접히고 긁히므로, 조금 촘촘해지더라도 더 많이 훼손돼도 읽히는 편이 낫습니다.
- 원과 선으로 도장 자리를 꾸밉니다. 편집 화면에서는 첫 번째 벌만 편집합니다 — 나머지는 데이터가 만드는 것이라 고칠 대상이 아닙니다.
스키마 · API 로 만들기
- 구역 반복은 rect 노드의 props.repeat 하나로 켭니다. 새 노드 타입이 아닙니다.
- 반복 블록은 한 렌더에 100벌까지입니다. props.break 를 "page" 로 두면 벌마다 새 페이지라 그것이 곧 100쪽입니다. 표 행 상한(2,000행)과는 별개 예산이라 서로를 깎지 않습니다 — 회사 100곳에 각각 8줄짜리 문서를 만들면 블록 100 · 행 800 으로 둘 다 안쪽입니다. 넘는 벌은 버리고 warnings 로 알려 드립니다.
- props.as 가 항목을 부를 이름입니다. 비우면 block 이 되므로 {{ block.name }} 이 됩니다.
- props.break 를 "page" 로 두면 벌마다 새 페이지에서 시작합니다.
- 안에 드는 노드는 parent 에 틀의 id 를 적습니다. 좌표는 그 틀 기준이 됩니다.
- image 의 src 는 data: URI 만 받습니다. http · https 주소는 E:render.compile#external-ref 로 렌더가 실패합니다 — 렌더 중에 외부로 요청을 내보내지 않기 때문입니다.
완성 문서
아래 JSON 이 위 두 순서가 만드는 바로 그 문서입니다. 저희 테스트가 이 문서를 검증하고 경고 없이 렌더되는지 확인하므로, 그대로 붙여 넣어 쓰셔도 됩니다.
{
"v": 1,
"unit": "px",
"page": {
"size": "a4",
"orientation": "landscape",
"margin": {
"t": 48,
"r": 48,
"b": 56,
"l": 48
},
"header": "",
"footer": ""
},
"fonts": [],
"styles": [
{
"id": "s_kicker",
"fontFamily": "Liberation Sans",
"fontSize": 13,
"fontWeight": 400,
"italic": false,
"color": "#666666",
"bg": "transparent",
"align": "center",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_name",
"fontFamily": "Liberation Sans",
"fontSize": 40,
"fontWeight": 700,
"italic": false,
"color": "#111111",
"bg": "transparent",
"align": "center",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_cite",
"fontFamily": "Liberation Sans",
"fontSize": 14,
"fontWeight": 400,
"italic": true,
"color": "#333333",
"bg": "transparent",
"align": "center",
"valign": "top",
"lineHeight": 1.8,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_cap",
"fontFamily": "Liberation Sans",
"fontSize": 10,
"fontWeight": 400,
"italic": false,
"color": "#777777",
"bg": "transparent",
"align": "center",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
}
],
"nodes": [
{
"id": "n_frame",
"parent": "",
"order": 0,
"type": "rect",
"x": 0,
"y": 0,
"w": 1026,
"h": 600,
"show": "",
"styleId": "s_cap",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 2,
"stroke": "#c9a227",
"fill": "",
"repeat": "data.students",
"as": "st",
"break": "page",
"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": "One person frame"
},
{
"id": "n_logo",
"parent": "n_frame",
"order": 1,
"type": "image",
"x": 453,
"y": 40,
"w": 120,
"h": 60,
"show": "",
"styleId": "s_cap",
"props": {
"text": "",
"src": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAgNjAiPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNjAiIHJ4PSI4IiBmaWxsPSIjMWYyOTM3Ii8+PHRleHQgeD0iNjAiIHk9IjM4IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIyMCIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+TE9HTzwvdGV4dD48L3N2Zz4=",
"fit": "contain",
"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": "Logo"
},
{
"id": "n_kicker",
"parent": "n_frame",
"order": 2,
"type": "label",
"x": 0,
"y": 130,
"w": 1026,
"h": 20,
"show": "",
"styleId": "s_kicker",
"props": {
"text": "{{ data.org }}",
"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": "Organisation"
},
{
"id": "n_head",
"parent": "n_frame",
"order": 3,
"type": "label",
"x": 0,
"y": 160,
"w": 1026,
"h": 30,
"show": "",
"styleId": "s_kicker",
"props": {
"text": "CERTIFICATE OF COMPLETION",
"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": "Heading"
},
{
"id": "n_name",
"parent": "n_frame",
"order": 4,
"type": "label",
"x": 0,
"y": 210,
"w": 1026,
"h": 56,
"show": "",
"styleId": "s_name",
"props": {
"text": "{{ st.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_cite",
"parent": "n_frame",
"order": 5,
"type": "label",
"x": 163,
"y": 290,
"w": 700,
"h": 0,
"show": "",
"styleId": "s_cite",
"props": {
"text": "has successfully completed the {{ data.course }} course, and is awarded this certificate.",
"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": "Citation"
},
{
"id": "n_seal",
"parent": "n_frame",
"order": 6,
"type": "circle",
"x": 820,
"y": 420,
"w": 110,
"h": 110,
"show": "",
"styleId": "s_cap",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 2,
"stroke": "#c9a227",
"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": "Seal area"
},
{
"id": "n_qr",
"parent": "n_frame",
"order": 7,
"type": "code",
"x": 80,
"y": 420,
"w": 110,
"h": 110,
"show": "",
"styleId": "s_cap",
"props": {
"text": "https://brewmypdf.com/verify/{{ st.code }}",
"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": "qr",
"ecc": "Q",
"fieldKind": "",
"fieldName": "",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Verification QR"
},
{
"id": "n_qr_l",
"parent": "n_frame",
"order": 8,
"type": "label",
"x": 50,
"y": 536,
"w": 170,
"h": 16,
"show": "",
"styleId": "s_cap",
"props": {
"text": "Verify · {{ st.code }}",
"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": "QR caption"
}
]
}요청 본문입니다. template 자리에 위 문서를, data 자리에 아래 데이터를 넣습니다.
{
"template": "(the document above)",
"data": {
"org": "brewmypdf Academy",
"course": "Document Automation in Practice",
"students": [
{
"name": "Do-hyun Kim",
"code": "C-2026-0001"
},
{
"name": "Seo-yeon Lee",
"code": "C-2026-0002"
},
{
"name": "Jun-ho Park",
"code": "C-2026-0003"
}
]
}
}