← 모든 레시피

증명서 · 수료증

한 번의 요청으로 수강생 전원의 수료증을 사람마다 한 장씩 만듭니다. 구역 반복, 항목마다 새 페이지, 로고 이미지, 진위 확인용 QR 을 배웁니다.

이 템플릿 가져오기

여기서 배우는 것

  • block-repeat
  • block-alias
  • block-break
  • node-image
  • node-code
  • code-qr
  • code-ecc
  • node-circle
  • style-italic
  • style-align
  • page-orientation
  • node-nesting

편집기로 만들기

  1. 사각형을 하나 넣어 페이지 전체를 덮습니다. 이 사각형이 한 사람분의 틀이 됩니다 — 안에 든 것은 전부 이 틀을 따라 복사됩니다.
  2. 사각형의 속성에서 반복할 배열 경로에 data.students 를 적고, 항목을 부를 이름을 st 로 둡니다. 이제 안에서 {{ st.name }} 처럼 씁니다.
  3. "항목마다 새 페이지" 를 켭니다. 수료증은 한 사람에 한 장이어야 하기 때문입니다.
  4. 틀 안에 라벨을 넣어 제목과 이름을 적습니다. 이름은 크게, 가운데 정렬로 둡니다. 문구는 기울임을 켜면 증서다운 느낌이 납니다.
  5. 이미지를 넣고 로고 파일을 고릅니다. 파일을 고르면 편집기가 data URI 로 바꿔 문서에 담습니다 — 외부 주소는 렌더가 거절하므로, 파일을 고르는 것이 유일한 방법입니다.
  6. QR·바코드를 넣고 인코딩할 값에 확인 주소를 적습니다. {{ st.code }} 처럼 사람마다 다른 값을 넣으면 벌마다 다른 QR 이 나옵니다.
  7. QR 의 오류 정정을 Q 로 올립니다. 인쇄물은 접히고 긁히므로, 조금 촘촘해지더라도 더 많이 훼손돼도 읽히는 편이 낫습니다.
  8. 원과 선으로 도장 자리를 꾸밉니다. 편집 화면에서는 첫 번째 벌만 편집합니다 — 나머지는 데이터가 만드는 것이라 고칠 대상이 아닙니다.

스키마 · API 로 만들기

  1. 구역 반복은 rect 노드의 props.repeat 하나로 켭니다. 새 노드 타입이 아닙니다.
  2. 반복 블록은 한 렌더에 100벌까지입니다. props.break 를 "page" 로 두면 벌마다 새 페이지라 그것이 곧 100쪽입니다. 표 행 상한(2,000행)과는 별개 예산이라 서로를 깎지 않습니다 — 회사 100곳에 각각 8줄짜리 문서를 만들면 블록 100 · 행 800 으로 둘 다 안쪽입니다. 넘는 벌은 버리고 warnings 로 알려 드립니다.
  3. props.as 가 항목을 부를 이름입니다. 비우면 block 이 되므로 {{ block.name }} 이 됩니다.
  4. props.break 를 "page" 로 두면 벌마다 새 페이지에서 시작합니다.
  5. 안에 드는 노드는 parent 에 틀의 id 를 적습니다. 좌표는 그 틀 기준이 됩니다.
  6. 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"
      }
    ]
  }
}