← 모든 레시피

청구서

품목이 데이터에서 오고 합계가 그 데이터에서 계산되는 청구서를 만듭니다. 반복 표·통화 서식·합계 계산·머리글과 쪽번호를 여기서 배웁니다.

이 템플릿 가져오기

여기서 배우는 것

  • node-label
  • node-table
  • table-repeat
  • expr-format-currency
  • expr-array
  • page-header
  • page-footer
  • style-font-cjk
  • style-align
  • api-sync
  • api-template-id
  • node-autoheight

편집기로 만들기

  1. 새 템플릿을 만들고 편집기를 엽니다. 종이는 A4 세로, 여백은 기본값 그대로 둡니다.
  2. 라벨을 넣고 텍스트에 {{ data.title }} 를 적습니다. 크기 26, 굵기 700 으로 올려 제목처럼 보이게 합니다. 글꼴은 Noto Sans CJK KR 을 고릅니다 — 한글이 들어갈 자리이기 때문입니다.
  3. 라벨을 둘 더 넣어 받는 곳({{ data.customer }} 귀하)과 문서번호(번호 {{ data.no }})를 적습니다. 문서번호는 상자 정렬로 오른쪽에 붙입니다.
  4. 표를 넣습니다. 표 속성 맨 위에서 데이터 반복을 고르고, 배열 경로에 data.items 를 적습니다.
  5. 열을 셋으로 만듭니다. 머리글과 셀 템플릿을 각각 항목/{{ item.name }}, 수량/{{ item.qty }}, 금액/{{ format(item.amount, "currency", "KRW", "ko-KR") }} 으로 넣습니다. 수량과 금액 열은 오른쪽 정렬로 둡니다 — 숫자는 자릿수가 맞아야 읽힙니다.
  6. 표 아래에 합계 라벨을 놓고 {{ format(sum(data.items, "amount"), "currency", "KRW", "ko-KR") }} 를 적습니다. 합계를 데이터로 받지 않고 계산하면, 품목을 고쳤을 때 합계가 어긋나는 일이 없습니다.
  7. 표가 길어지면 합계가 밀려야 합니다. 표 아래에 있는 것들은 흐름 구간에 들어가므로, 편집 화면의 주황 안내선이 어디까지 함께 밀리는지 보여줍니다.
  8. 데이터 탭에서 JSON 을 실제 값으로 바꿔 미리보기를 확인합니다. 자동 샘플은 숫자 자리에 문자열을 넣으므로 통화가 ₩NaN 으로 보입니다 — 진짜 숫자를 넣어야 자릿수와 넘침이 제대로 보입니다.
  9. 저장합니다. 저장하면 버전이 쌓이고, API 에서 template_id 로 부를 수 있습니다.

스키마 · API 로 만들기

  1. 문서는 노드의 평면 배열입니다. 제목·받는 곳·번호는 label, 품목은 table 하나입니다.
  2. 표를 반복 모드로 두는 것은 props.bind 하나입니다. 이 값이 있으면 rows 와 cells 는 쓰지 않고 columns 가 열을 정합니다.
  3. h 를 0 으로 두면 자동 높이입니다. 반복 표에서는 예약 높이가 되어, 그보다 늘어나면 아래 노드가 밀립니다.
  4. page.header 와 page.footer 는 모든 페이지에 반복됩니다. 바닥글에서만 [[page]] 와 [[pages]] 를 쓸 수 있습니다.
  5. 저장해 둔 템플릿이라면 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
      }
    ]
  }
}