← 모든 레시피

발주서

공급자와 납품 조건을 칸에 적어 넣는 발주서를 만듭니다. 손으로 짜는 격자 표, 칸 합치기, 칸별 여백과 정렬, 그리고 값이 없을 때 줄을 통째로 감추는 법을 배웁니다.

이 템플릿 가져오기

여기서 배우는 것

  • table-grid
  • cell-merge
  • cell-pad
  • cell-head
  • cell-bg
  • node-show
  • style-valign
  • node-line
  • page-margin
  • expr-format-date
  • expr-ternary

편집기로 만들기

  1. 표를 넣고 표 속성 맨 위에서 손으로 짜기를 고릅니다. 행을 6, 열을 4 로 맞춥니다.
  2. 왼쪽 두 열을 라벨 열로 쓸 것이므로 좁게 줄입니다. 열 경계를 끌면 폭이 바뀌고, 열 폭의 합은 표 폭과 항상 같게 유지됩니다.
  3. 캔버스에서 칸을 직접 클릭해 고릅니다. 고른 칸에 글을 넣고, 라벨 칸은 머리 칸으로 켜서 배경색이 들어가게 합니다.
  4. 주소처럼 긴 값은 한 칸으로는 좁습니다. 그 칸을 고르고 오른쪽 칸과 합치기를 눌러 두 칸을 하나로 만듭니다.
  5. 값 칸에는 {{ data.supplier }} 처럼 바인딩을 넣습니다. 날짜는 {{ format(data.due, "date", "long", "ko-KR") }} 로 적으면 사람이 읽는 형식으로 나옵니다.
  6. 칸이 답답하면 그 칸만 여백을 늘립니다. 칸을 고르면 표 전체 여백은 숨고 이 칸 여백만 보입니다 — 지금 무엇을 고치는지 화면이 말해 줍니다.
  7. 비고처럼 있을 때만 넣는 줄은, 그 줄을 담은 객체의 조건부 표시에 {{ data.note }} 를 적습니다. 값이 없으면 그 줄이 통째로 빠지고 빈 자리도 남지 않습니다.
  8. 선을 하나 넣어 서명란 위에 긋습니다. 도형은 색과 두께만 있으면 되고, 굳이 표로 만들 필요가 없습니다.

스키마 · API 로 만들기

  1. 격자 표는 props.bind 를 비워 둡니다. 대신 rows 로 행 수를, cells 로 칸을 적습니다.
  2. cells 는 있는 칸만 적으면 됩니다. 없는 자리는 빈 칸이므로 24칸을 다 적지 않아도 됩니다.
  3. span 은 오른쪽으로, rowspan 은 아래로 합칩니다. 합쳐진 자리에 다른 칸을 또 적지 마십시오.
  4. head 를 true 로 두면 머리 칸이 되고 표의 fill 색이 그 칸에만 칠해집니다.
  5. 노드의 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": ""
  }
}