템플릿 문서

템플릿은 JSON 객체 하나입니다. 페이지, 재사용 스타일 목록, 그리고 노드의 평면 배열로 이루어집니다. 노드 목록은 중첩하지 않습니다 — 노드가 parent 에 부모의 id 를 적어 관계를 나타냅니다.

아래 필드는 전부 필수입니다. 노드의 props 는 모든 노드 타입의 props 를 합친 것이라, 그 타입이 쓰지 않는 키도 들고 있어야 합니다. 안 쓰는 것은 비워 두십시오 — 문자열은 "", 숫자는 0, 참·거짓은 false, 배열은 []. 키를 지우면 스키마 위반이지만 비우는 것은 아닙니다.

최상위

v integer

스키마 버전. 현재 1. 버전이 없으면 추측하지 않고 거부합니다.

unit const

좌표 단위. 언제나 "px" 이며 96 dpi 기준입니다 — A4 는 794 × 1123 입니다.

px

lang enum

문서의 언어. CJK 자형을 고르는 데 씁니다. 없으면 글자로 추측하며, 가나나 한글이 있으면 그것으로 충분합니다. 일본어가 한자로만 쓰였을 때 선언하십시오 — 청구서 제목 請求書 은 중국어로도 유효한 글자열이라 추측이 빗나가고, 문서가 중국어 자형으로 인쇄됩니다. "ko" · "ja" · "zh-Hans" · "zh-Hant" 를 받습니다.

kojazh-Hanszh-Hant

page object

용지, 여백, 그리고 모든 페이지에 반복되는 머리글·바닥글.

fonts array

이 문서가 필요로 하는 폰트. 빈 배열이면 기본 폰트를 씁니다.

styles array

재사용하는 글자·상자 스타일. 노드가 id 로 참조합니다.

nodes array

문서의 모든 노드를 담은 평면 배열. 중첩은 parent 로 나타냅니다.

page

size enum

용지 규격. a4 · a5 · letter · legal 중 하나이며, 그 밖의 값은 거부됩니다. 96dpi 기준으로 A4 794×1123, A5 559×794, Letter 816×1056, Legal 816×1344 px 입니다. 생략하면 a4 입니다.

a4a5letterlegal

orientation enum

용지 방향. size 뒤에 적용되므로 a5 + landscape 는 가로로 긴 A5 입니다.

portraitlandscape

margin object

인쇄 여백(px). 노드 좌표는 여백 안쪽에서 시작합니다.

margin.t number

위 여백.

margin.r number

오른쪽 여백.

margin.b number

아래 여백. 바닥글 자리를 위해 보통 위쪽보다 큽니다.

margin.l number

왼쪽 여백.

header string

모든 페이지 상단에 반복되는 문구. 빈 문자열이면 없음. {{ … }} 를 쓸 수 있습니다.

footer string

모든 페이지 하단에 반복되는 문구. 빈 문자열이면 없음. {{ … }} 와 [[page]] · [[pages]] 를 쓸 수 있습니다.

backgrounds array

배경 이미지 목록. 모든 쪽에 깔립니다. 최대 3개이며, 안 쓰면 빈 배열입니다. ⚠ 좌표가 노드와 다릅니다 — 배경은 용지 전체 기준이고 노드는 여백 안쪽 기준입니다. 관공서 서식처럼 용지를 덮는 그림을 깔아야 하기 때문입니다.

backgrounds[].assetId string

POST /v1/assets 로 올린 자산의 id.

backgrounds[].x number

용지 좌상단 기준 가로 위치(px). 노드와 달리 여백 바깥에서 시작합니다.

backgrounds[].y number

용지 좌상단 기준 세로 위치(px).

backgrounds[].w number

너비(px).

backgrounds[].h number

높이(px).

backgrounds[].opacity number

0~1. 1 이 불투명입니다. 서식이 진해서 얹은 값이 안 읽힐 때 낮추십시오.

backgrounds[].rotate number

회전 각도(도). 스캔본이 기울어졌을 때 보정합니다. 0 이 기본.

backgrounds[].print boolean

false 면 편집기에는 보이고 출력에는 없습니다. 원본 용지에 인쇄할 때 칸 위치만 맞추는 용도입니다.

backgrounds[].locked boolean

편집기에서 고정합니다. 렌더에는 영향이 없습니다.

fonts

family string

폰트 이름. styles[].fontFamily 가 찾는 이름과 같아야 합니다.

src enum

폰트의 출처. 지금은 "preinstalled" 만 동작합니다 — 편집기에 나오는 기본 제공 글꼴입니다. "r2" 는 직접 올린 글꼴을 위해 비워 둔 값이며 아직 지원하지 않습니다. 그 값을 쓴 문서는 대체 서체로 그려지고 warnings 로 알려 드립니다.

preinstalledr2

styles

id string

문서 내 유일한 스타일 id. nodes[].styleId 가 가리킵니다.

fontFamily string

폰트 이름. 한글·중국어·일본어가 들어갈 수 있는 글자에는 "Noto Sans CJK KR" 을 쓰십시오 — 라틴 전용 폰트는 그 글자를 통째로 떨어뜨립니다.

fontSize number

글자 크기(px).

fontWeight integer

글자 굵기. 400 이 보통, 700 이 굵게.

italic boolean

기울임.

color string

글자 색 #RRGGBB.

bg string

배경색 #RRGGBB 또는 "transparent".

align enum

가로 정렬.

leftcenterrightjustify

valign enum

노드 상자 안에서의 세로 정렬.

topmiddlebottom

lineHeight number

줄 간격. 글자 크기의 배수입니다.

border string

CSS border 축약형 — 예: "1px solid #ddd". 없으면 "none".

radius number

모서리 둥글기(px).

opacity number

불투명도 0~1.

nodes

id string

문서 내 유일한 노드 id. n_ 접두는 관례일 뿐 규칙은 아닙니다.

name string

편집기 개체 트리에 보일 이름입니다. 식별자가 아니라 라벨이라 유일할 필요가 없고 아무것도 이것을 참조하지 않습니다. 비워 두면 트리에 id 가 보입니다.

parent string

부모 노드의 id. 빈 문자열이면 페이지 직속입니다. 없는 부모를 가리키거나 순환이 생기면 위반입니다.

order integer

같은 부모 안에서의 순서. 겹칠 때 무엇이 위에 그려지는지도 이 값이 정합니다.

type enum

노드의 종류.

labelimagelinerectcircletablechartcodehtmlfield

x number

부모 기준 X 좌표(px).

y number

부모 기준 Y 좌표(px).

w number

너비(px).

h number

높이(px). 0 이면 자동 높이입니다. 배열이 바인딩된 표에서는 예약 높이가 됩니다 — "늘어나는 표" 참조.

show string

표시 조건. 빈 문자열이면 항상 표시합니다. 그 밖의 값은 data 기준으로 계산하는 표현식입니다.

styleId string

styles[] 의 id. 빈 문자열이면 기본 스타일.

props object

타입별 설정. 모든 키가 필수이며 안 쓰는 것은 비워 둡니다.

nodes[].props

text string

label·code. label 은 그릴 텍스트, code 는 인코딩할 값입니다. {{ … }} 가능. 줄바꿈 문자는 그 자리에서 줄을 바꿉니다. 그 밖의 타입은 "".

src string

image 전용. data: URI 만 받습니다 — 렌더 중에 외부로 요청을 내보내지 않기 때문입니다. 그림은 base64 로 바꿔 넣으십시오. 템플릿에 직접 적어 넣은 http · https 주소는 렌더가 E:render.compile#external-ref 로 실패합니다. {{ data.logo }} 같은 바인딩으로 들어온 주소는 그 그림만 비우고 응답에 external-ref 경고를 싣습니다 — 레코드 하나 때문에 배치 전체가 실패하지 않습니다. 그 밖의 타입은 "".

fit enum

image 전용. 이미지를 상자에 채우는 방식. 그 밖의 타입은 "".

""containcoverfill

thickness number

line · rect · circle · table · field. 선 두께(px). 표에서는 격자선, 입력칸에서는 칸 테두리의 두께입니다. 그 밖의 타입은 0.

stroke string

line · rect · circle · table · field. 선 색 #RRGGBB. 표에서는 격자선, 입력칸에서는 칸 테두리의 색입니다. 그 밖의 타입은 "".

fill string

rect · circle · table. 채움 색 #RRGGBB. 표에서는 머리 칸의 배경색입니다. 그 밖의 타입은 "".

repeat string

rect 전용. 이 경로의 배열 항목마다 그룹을 한 벌씩 그립니다 — 예: "data.companies". 안에 든 것이 함께 반복됩니다. 그 밖의 타입은 "".

as string

rect 전용. 반복 그룹 안에서 지금 항목을 부를 이름 — "co" 로 두면 {{ co.name }} 으로 씁니다. 비우면 "block". "item" 은 쓸 수 없습니다: 그 이름은 표의 행이 쓰므로, 나눠 두어야 그룹 안의 표가 둘 다 읽습니다.

break enum

rect 전용. "page" 면 반복할 때마다 새 페이지에서 시작합니다. 비우면 이어 붙습니다.

""page

keep enum

rect 전용. "together" 면 한 벌이 페이지 경계에서 갈리지 않습니다. 한 페이지보다 큰 벌은 어차피 갈립니다.

""together

padding number

table 전용. 칸 안 여백(px). 키가 없으면 선이 있을 때만 기본 여백이 붙습니다. 그 밖의 타입은 0.

padY number

table 전용. 칸 안 상하 여백(px)입니다. -1 이면 padding 을 따릅니다 — 0 은 "여백 없음"이라는 진짜 값이라 "따른다"로 쓸 수 없습니다.

padX number

table 전용. 칸 안 좌우 여백(px)입니다. -1 이면 padding 을 따릅니다.

align enum

table 전용. 격자 표 칸의 가로 정렬 기본값. 칸이 따로 정하면 그쪽이 이깁니다. 반복 표는 열마다(columns[].align) 정합니다.

""leftcenterright

valign enum

table 전용. 칸 안 세로 정렬. 비우면 가운데. 칸이 따로 정하면 그쪽이 이깁니다.

""topmiddlebottom

wrap enum

table 전용. 칸 안 줄바꿈. 비우거나 "on" 이면 넘치는 글이 다음 줄로, "off" 면 한 줄로 두고 넘치는 만큼 잘라 … 를 붙입니다.

""onoff

bind string

table 전용. 반복할 배열의 경로 — 예: "data.items". 그 밖의 타입은 "".

headerHeight number

table 전용. 머리 행 높이(px). 그 밖의 타입은 0.

rowHeight number

table 전용. 본문 행 높이(px). 그 밖의 타입은 0.

max integer

table 전용. 그릴 최대 행 수. 넘는 행은 버립니다. 그 밖의 타입은 0.

columns array

table 전용. 왼쪽부터의 열 정의. 그 밖의 타입은 [].

columns[].header string

머리 셀 문구.

columns[].cell string

본문 셀 템플릿. item 이 현재 행이므로 "{{ item.name }}" 처럼 그 필드를 읽습니다.

columns[].width number

열 너비(px). 합이 표 너비와 맞아야 합니다.

columns[].align enum

셀 정렬.

leftcenterright

rows integer

table 전용. bind 가 빈 격자 표의 행 수. 반복 표와 그 밖의 타입은 0.

cells array

table 전용. 격자 표의 칸. 없는 자리는 빈 칸이므로 다 적지 않아도 됩니다. 반복 표와 그 밖의 타입은 [].

cells[].r integer

행 번호. 0부터.

cells[].c integer

열 번호. 0부터.

cells[].span integer

가로 병합 칸 수. 1 이 기본.

cells[].rowspan integer

세로 병합 칸 수. 1 이 기본.

cells[].text string

칸 내용. "{{ data.x }}" 로 값을 넣을 수 있습니다.

cells[].align enum

칸 글자 정렬.

leftcenterright

cells[].valign enum

이 칸만 세로 정렬. 비우면 표의 값을 따릅니다.

""topmiddlebottom

cells[].wrap enum

이 칸만 줄바꿈. 비우면 표의 값을 따릅니다.

""onoff

cells[].pad number

이 칸만의 안 여백(px)입니다. -1 이거나 없으면 표의 padding 을 따릅니다 — 0 은 "여백 없음"이라는 진짜 값이라 "따른다"로 쓸 수 없기 때문입니다. 표 전체를 넓히지 않고 한 칸만 붙이거나 띄울 때 씁니다.

cells[].padY number

이 칸만의 상하 여백(px)입니다. -1 이거나 없으면 pad → 표 순으로 따릅니다.

cells[].padX number

이 칸만의 좌우 여백(px)입니다. -1 이거나 없으면 pad → 표 순으로 따릅니다.

cells[].src string

칸 안 이미지. data:image/ 로 시작하는 값만 받습니다 — 외부 주소는 요청이 나가므로 렌더가 거부합니다.

cells[].fit enum

칸 이미지 맞춤. 비우면 contain.

""containcoverfill

cells[].bg string

칸 배경색 #RRGGBB. 비우면 배경 없음.

cells[].head boolean

머리 칸. 머리 행은 r=0 줄에, 머리 열은 c=0 줄에 켭니다.

cells[].border enum

"none" 이면 이 칸만 선을 그리지 않습니다.

""none

chartKind enum

chart 전용. "bar"(기본) · "line" · "pie" · "donut". 그 밖의 타입은 "".

""barlinepiedonut

codeKind enum

code 전용. 그릴 코드 종류 — "qr"(기본) · "code128" · "ean13". 그 밖의 타입은 "".

""qrcode128ean13

ecc enum

code 전용이고 QR 에만. 오류정정 수준 L · M · Q · H — 비우면 M 입니다. 높을수록 더 많이 훼손돼도 읽히지만 모듈이 늘어 같은 글자가 더 촘촘한 코드가 됩니다.

""LMQH

html string

html 전용. 서식 있는 본문. 허용목록에 있는 태그만 남습니다. 그 밖의 태그는 글은 남고 태그만 사라지며, script · style · iframe 은 내용까지 사라집니다. 그 밖의 타입은 "".

fieldKind enum

field 전용. 채워 넣을 수 있는 입력의 종류 — "text"(기본) · "check" · "select" · "list" · "radio" · "button" · "sign". "sign" 은 서명할 자리를 만들 뿐 전자서명을 하지는 않습니다. 그 밖의 타입은 "".

""textcheckselectlistradiobuttonsign

fieldName string

field 전용. PDF 안에서 이 입력의 이름입니다 — 받는 쪽 프로그램이 값을 읽을 때 쓰는 열쇠입니다. 비우면 노드 id 를 씁니다. 한 문서 안에서 겹치면 뒤엣것이 앞엣것과 같은 값을 갖게 됩니다. 그 밖의 타입은 "".

options array

field 전용이고 "select" · "list" · "radio" 에만. 고를 수 있는 항목들입니다. 그 밖에는 [].

required boolean

field 전용. 필수 입력으로 표시합니다. 저희가 검사하지는 않습니다 — 표시일 뿐이고, 강제 여부는 PDF 를 여는 프로그램이 정합니다. 그 밖의 타입은 false.

readonly boolean

field 전용. 보이기만 하고 고칠 수 없게 합니다. 그 밖의 타입은 false.

표현식

모든 문자열 필드에 {{ … }} 표현식을 넣을 수 있습니다. 렌더 요청과 함께 보낸 data 를 기준으로 계산됩니다.

표의 셀 안에서 item 은 바인딩된 배열의 현재 행을 가리킵니다. 표 밖에서는 정의되지 않습니다.

쪽번호는 문법이 다릅니다 — [[page]] 와 [[pages]] 이며 page.header · page.footer 에서만 동작합니다. 전체 쪽수가 정해지는 배치 이후에 채워지기 때문입니다.

표시

format(value, kind, pattern?, locale?)
값을 사람이 읽는 형태로 만듭니다. kind 는 number, currency, date 입니다. currency 는 패턴 자리에 통화코드(KRW, USD)를, date 는 short, medium, long, iso 를 받습니다. 통화코드가 틀리면 문서를 실패시키지 않고 코드를 그대로 찍습니다.

숫자

abs(n)
부호를 뗀 거리.
ceil(n)
올림해서 정수로.
floor(n)
내림해서 정수로.
trunc(n)
반올림 없이 소수를 버립니다.
sign(n)
-1, 0, 1 중 하나.
sqrt(n)
제곱근. 음수를 넣으면 오류가 아니라 빈 값입니다.
pow(n, exp)
거듭제곱.
round(n, digits?)
주어진 소수 자리까지 반올림합니다(기본 0).
min(a, b, ...)
인자 중 가장 작은 값.
max(a, b, ...)
인자 중 가장 큰 값.
clamp(n, lo, hi)
범위 안으로 가둡니다. lo 보다 작으면 lo, hi 보다 크면 hi.

글자

upper(s)
대문자로.
lower(s)
소문자로.
trim(s)
양 끝 공백을 없앱니다.
len(s)
글자 수.
sub(s, start, end?)
글자 위치로 잘라낸 조각.
replace(s, find, with)
모두 바꿉니다. 찾는 값은 글자 그대로이며 패턴이 아닙니다.
contains(s, find)
그 글자가 들어 있으면 참.
startsWith(s, find)
그 글자로 시작하면 참.
endsWith(s, find)
그 글자로 끝나면 참.
padStart(s, width, fill?)
너비에 닿을 때까지 왼쪽을 채웁니다. 000042 같은 청구서 번호에 씁니다.
padEnd(s, width, fill?)
너비에 닿을 때까지 오른쪽을 채웁니다.
split(s, sep)
구분자로 잘라 배열로 만듭니다.

배열

count(array)
개수.
sum(array, prop?)
전부 더합니다. prop 을 주면 각 항목의 그 필드를 더합니다.
sumProduct(…)
각 항목의 두 필드를 곱해서 더합니다. 세율이 섞인 청구서의 총 세액(sumProduct(items, "amount", "vat") / 100)이나, 합계용 필드를 따로 저장하지 않는 줄 금액(sumProduct(items, "qty", "price"))에 씁니다. 세금을 아는 함수가 아닙니다 — 세율은 데이터가 나르는 숫자입니다. 나라마다 다르고 법으로 바뀌기 때문입니다. 반올림은 맨 끝에서 한 번만 하므로, 세율 묶음마다 반올림하도록 요구하는 제도에서는 filter 로 묶음을 각각 그리십시오.
avg(array, prop?)
평균. 빈 배열은 오류가 아닙니다 — 자리를 비우고 nan 경고를 냅니다. 계산할 수 없는 다른 숫자와 같은 처리입니다.
minOf(array, prop?)
가장 작은 항목, 또는 그 필드의 최솟값.
maxOf(array, prop?)
가장 큰 항목, 또는 그 필드의 최댓값.
first(array)
첫 항목.
last(array)
마지막 항목.
at(array, index)
0 부터 세어 그 자리의 항목.
join(array, sep?, prop?)
구분자로 이어 하나의 글자로 만듭니다(기본은 쉼표와 공백).
filter(array, prop, value)
그 필드가 값과 같은 항목만 남깁니다.
sortBy(array, prop, desc?)
필드로 정렬합니다. 세 번째 인자에 true (또는 "desc") 를 주면 내림차순입니다.
slice(array, start, end?)
위치로 잘라낸 항목 범위.
unique(array, prop?)
중복을 없앱니다.

고르기

if(test, then, else)
첫 인자가 참이면 둘째를, 아니면 셋째를 고릅니다.
coalesce(a, b, ...)
비어 있지 않은 첫 인자. 비었다는 것은 null, undefined, 빈 글자, 빈 배열입니다 — isEmpty 와 같은 기준입니다. 0 은 비어 있지 않으므로 금액 0 은 남습니다.
ifEmpty(value, fallback)
값을 주되 비어 있으면 대체값을 줍니다. 비었다는 것은 null, undefined, 빈 글자, 빈 배열입니다.
isEmpty(value)
값이 null, undefined, 빈 글자, 빈 배열이면 참.

날짜

dateAdd(date, n, unit)
날짜에 기간을 더해 YYYY-MM-DD 로 돌려줍니다. unit 은 day, week, month, quarter, year 이고 음수를 주면 뺍니다. 달 계산은 말일로 자릅니다 — 2026-01-31 에 1개월을 더하면 2026-03-03 이 아니라 2026-02-28 입니다.
dateDiff(from, to, unit?)
두 날짜 사이를 만 단위로 셉니다. 앞에서 뒤로 세며 기본 단위는 날입니다. 0 방향으로 내리므로 29일은 0개월입니다.
dateStart(date, unit)
그 주, 달, 분기, 해의 첫날. 주는 월요일에 시작합니다.
dateEnd(date, unit)
그 주, 달, 분기, 해의 마지막 날 — 28, 29, 30, 31 을 직접 세지 않고 말일을 얻는 방법입니다.
datePart(date, part)
날짜에서 숫자 하나를 꺼냅니다. part 는 year, month, day, weekday, quarter, week, dayOfYear 입니다. 달은 1~12, 요일은 월요일 1 부터 일요일 7 까지로, 자바스크립트의 0 부터 세는 방식이 아니라 ISO-8601 을 따릅니다.
today(timezone?)
오늘을 YYYY-MM-DD 로 줍니다. 한 번의 렌더 안에서는 고정이라 두 번 불러도 자정을 넘나들지 않습니다. 기본은 UTC 이고, 그것이 어디서나 같은 날은 아닙니다 — 서울의 24일 오전 8시는 UTC 로 아직 23일입니다. 문서에 찍힐 날짜가 읽는 분의 날짜여야 한다면 today("Asia/Seoul") 처럼 IANA 타임존을 주십시오. 날짜만 주고 시각은 주지 않습니다. 시간대가 적히지 않은 시각은 아무도 읽을 수 없기 때문입니다.

서식 있는 글

html 노드는 마크업 한 덩어리를 받습니다. 라벨을 줄지어 놓는 것으로는 못 하는 일입니다 — 줄 하나가 늘면 손으로 잡아 둔 배치가 전부 다시 흐르기 때문입니다. 약관, 안내문, 각주처럼 흘러야 하는 글에 씁니다.

저희는 마크업을 걸러내지 않고 다시 짓습니다. 보내신 HTML 을 읽어서, 목록에 있는 태그와 속성만 새로 씁니다. 이 차이가 중요합니다 — 저희가 이해하지 못한 것은 *저희가 미리 생각해 뒀는지에 기대는 대신* 아예 출력에 존재할 수 없습니다.

허용: p · div · span · br · hr · blockquote · pre · h1~h6 · b · strong · i · em · u · s · small · sub · sup · code · a · ul · ol · li · table 과 그 행·칸 · img. 목록 밖 태그는 태그만 사라지고 글은 남습니다 — 쓰신 글을 잃지 않습니다. script · style · iframe 같은 것은 내용까지 사라집니다. 그 내용은 글이 아니기 때문입니다.

속성은 더 좁습니다: style(선언마다 검사하고 position 은 제외) · 칸의 colspan · rowspan · 링크의 href · 이미지의 src · alt · width · height. class 와 id 는 없습니다 — 페이지를 배치하는 저희 클래스와 부딪힙니다. 이미지는 data: URI 여야 합니다. image 노드와 같은 규칙이고, 원격 이미지를 그리려면 렌더 도중에 가져와야 하기 때문입니다. 링크는 어디든 가리켜도 됩니다. 링크는 가져오기가 아닙니다.

html 안의 {{ … }} 는 데이터가 무엇이든 글자가 되지 마크업이 되지 않습니다. 속성 안에 있으면 그 값도 직접 쓴 값과 똑같이 검사합니다 — 값으로 style 에 url(…) 을 밀어 넣거나 href 에 javascript: 를 넣을 수 없습니다. 상한: 20,000자 · 중첩 32단계 · 태그 2,000개. 넘으면 자르고 warnings 로 알려 드립니다.

차트

chart 노드는 표가 쓰는 두 필드를 그대로 씁니다. bind 가 배열이고, columns 가 각 항목에서 무엇을 읽을지 정합니다 — columns[0].cell 이 라벨, columns[1].cell 이 값, columns[1].header 가 제목입니다. 새 필드는 chartKind 하나입니다: "bar" · "line" · "pie" · "donut".

축은 둥근 최댓값을 고릅니다 — 1 · 2 · 5 에 10의 거듭제곱을 곱한 값 — 그래야 눈금이 사람이 고를 법한 숫자로 읽힙니다. 숫자가 아닌 값은 렌더를 실패시키지 않고 0으로 셉니다. 읽을 수 없는 칸이 빈 칸으로 나오는 것과 같습니다. 파이와 도넛은 절댓값을 씁니다 — 음수 조각은 뜻이 없기 때문입니다.

차트는 SVG 로 그리므로 PDF 안에서 벡터입니다. 항목이 많으면 라벨을 겹쳐 찍는 대신 솎아 냅니다. 범례는 파이·도넛 아래 한 줄에 고정이고 움직이지 않습니다 — 스스로 자리를 찾는 범례는 여러분의 조판을 함께 움직입니다.

QR 코드와 바코드

code 노드는 글자를 스캔되는 그림으로 바꿉니다. 값을 text 에 넣고 codeKind 를 고르면 됩니다 — "qr" · "code128" · "ean13". SVG 로 그리므로 PDF 에 벡터로 들어갑니다. 어떤 인쇄 해상도에서도 선명하고, 막대 경계가 규격이 말하는 자리에 정확히 놓입니다. 마지막이 중요합니다 — 엉뚱한 시점에 래스터로 바뀐 바코드는 스캐너가 잘못 읽는 바코드입니다.

QR 은 정사각형으로 두십시오. 상자를 채우려고 코드를 늘리지 않습니다 — 찌그러지면 스캔이 안 됩니다 — 그래서 직사각형 상자는 긴 쪽에 여백이 남습니다. 코드 둘레의 여백(quiet zone)은 규격의 일부라 저희가 확보해 둡니다. 잘라내려 하지 마십시오.

ean13 은 12자리를 받아 체크digit 을 저희가 계산합니다. 13자리를 주면 마지막 자리를 검사합니다. code128 은 ASCII 32~126 을 담습니다. 인코딩할 수 없는 값이면 — 길이가 틀렸거나, 담을 수 없는 글자거나, QR 용량을 넘겼거나 — 빈 상자를 그리는 대신 오류로 실패합니다. 빈 상자는 라벨이 인쇄된 뒤에야 발견되기 때문입니다.

지원하는 것은 이 셋입니다. 100종을 다루는 라이브러리가 있지만, 가장 작은 것도 저희 워커 전체보다 무겁습니다. 저희가 그리지 않는 것이 필요하면 어떤 것이고 왜 필요한지 알려 주십시오.

표는 두 가지 모드로 씁니다. bind 가 비어 있으면 손으로 짜는 격자 표이고, bind 에 배열 경로를 주면 데이터가 행을 만드는 반복 표입니다. 이 키 하나가 표의 성격을 바꾸며, 두 모드는 서로 다른 필드를 씁니다 — 격자는 rows 와 cells, 반복은 columns 와 max 입니다.

격자 표는 rows 로 행 수를, columns 로 열 수와 너비를 정합니다. 칸 내용은 cells 배열에 r · c 좌표로 넣습니다 — 없는 자리는 빈 칸이므로 모든 칸을 적을 필요는 없습니다. span 과 rowspan 으로 옆칸·아랫칸과 합칩니다. head 를 켜면 머리 칸이 되고 fill 색이 그 칸에만 칠해집니다.

반복 표는 columns 하나가 열 하나입니다. header 가 머리글, cell 이 본문 템플릿이고, 템플릿 안에서 item 이 현재 행을 가리킵니다 — "{{ item.name }}" 처럼 씁니다. 행 수는 데이터가 정하며 max 로 상한을 둘 수 있습니다. 넘는 행은 버리고 응답의 warnings 로 알려 드립니다.

열 너비의 합이 표 너비와 맞아야 합니다. 어긋나면 브라우저가 알아서 늘리거나 줄이는데, 그 결과는 설계한 것과 다릅니다.

여백은 세 단계로 정해집니다: 칸(cells[].padY · padX · pad) → 표(padY · padX · padding) 순으로 찾고, 없으면 선이 있을 때만 기본 여백이 붙습니다. -1 이 "따른다"는 표시입니다 — 0 은 "여백 없음"이라는 진짜 값이라 센티널로 쓸 수 없기 때문입니다.

정렬도 같은 순서입니다. 칸이 정하면 칸이 이기고, 아니면 표의 align · valign 을 따릅니다. wrap 을 "off" 로 두면 그 칸은 한 줄로 두고 넘치는 만큼 잘라 … 를 붙입니다 — 넘친 글자를 옆 칸 위에 겹쳐 인쇄하지 않기 위해서입니다.

채울 수 있는 칸

field 노드는 받는 사람이 PDF 를 열어 직접 채울 수 있는 칸을 만듭니다. 계약서·신청서·설문지처럼 값을 나중에 넣는 문서를 위한 것입니다. 종류는 fieldKind 로 정합니다 — "text"(기본) · "check" · "select" · "list" · "radio" · "button" · "sign".

fieldName 이 PDF 안에서의 이름이고, 받는 쪽 프로그램이 값을 읽을 때 쓰는 열쇠입니다. 비우면 노드 id 를 씁니다. 영문·숫자·밑줄·하이픈 64자까지만 쓸 수 있고, 그 밖의 글자가 섞이면 저희가 고치지 않고 노드 id 로 떨어뜨립니다 — 고쳐 드리면 적으신 이름과 PDF 안의 이름이 달라지고, 그 차이는 받는 쪽에서야 드러나기 때문입니다. 점(.)은 PDF 에서 이름의 계층 구분자라 특히 쓸 수 없습니다.

"select" · "list" · "radio" 는 options 에 선택지를 넣어야 합니다. 비어 있으면 고를 것이 없는 칸이 되므로 그 칸만 건너뛰고 응답의 warnings 로 알려 드립니다. "radio" 는 그린 상자를 선택지 수만큼 세로로 나눠 채웁니다.

선은 stroke 와 thickness 로 그으십시오. 화면에서는 PDF 뷰어가 입력칸 모양을 그려 주지만, 인쇄한 종이에는 뷰어가 없습니다 — 선을 안 그으면 빈 서식을 출력했을 때 채울 자리가 보이지 않습니다. 특히 "sign" 은 뷰어도 아무것도 그리지 않습니다.

값을 미리 채워 둘 수 있습니다 — label 과 같은 text 속성을 쓰십시오. {{ data.payment }} 처럼 바인딩하면 데이터의 값이 그 칸에 들어간 채로 PDF 가 나갑니다(받는 사람은 그 위에서 고칠 수 있습니다). "check" 는 "false"·"0"·"no"·빈 값이면 꺼진 것으로 읽고, "select"·"list"·"radio" 는 options 에 있는 값만 넣습니다 — 없는 값을 넣으면 사람이 고를 수 없는 값이 든 문서가 되기 때문입니다. 한글도 됩니다.

못 하는 것: "sign" 은 서명할 자리를 만들 뿐 전자서명을 하지 않습니다. required 는 표시일 뿐 저희가 검사하지 않으며, 강제 여부는 PDF 를 여는 프로그램이 정합니다. 계산식이나 스크립트가 붙은 칸은 만들지 않습니다. 그림(PNG·JPEG)으로 받으면 칸은 사라집니다 — 그림에는 채울 자리라는 개념이 없습니다. 한 문서에 500칸까지이며, 넘으면 자르고 알려 드립니다.

늘어나는 표

배열이 바인딩된 표는 높이가 정해져 있지 않습니다 — 3행일 수도 300행일 수도 있습니다. 반복 그룹도 마찬가지입니다. 그래서 그런 노드가 처음 나오는 자리부터 그 뒤 전부가 고정 좌표가 아니라 문서 흐름으로 배치되고, 앞의 것이 길어지면 뒤의 내용이 함께 밀려납니다.

표에 적은 h 는 상한이 아니라 예약 높이입니다. 표는 그보다 길어질 수 있습니다. 표 뒤에 놓은 노드는 그 예약 높이의 아래끝을 기준으로, 그려 둔 간격을 그대로 유지합니다.

늘어나는 표에는 현실적인 h 를 주십시오. 0 으로 두면 표 아래에 배치한 노드들이 표 뒤가 아니라 표의 시작 지점에서 그려집니다.

구역 전체를 반복하기

표는 행을 반복합니다. 사각형은 그 안에 든 것을 통째로 반복합니다 — props.repeat 에 배열 경로를 적으면 항목마다 그룹이 한 벌씩, 자식까지 함께 그려집니다. 요청 하나로 여러 레코드를 담은 문서를 만드는 방법입니다. 지점별 명세서, 회사별 요약처럼요. 레코드마다 요청을 나눌 필요가 없습니다.

그룹 안에서 지금 항목은 props.as 에 적은 이름으로 부릅니다. as 가 "co" 면 {{ co.company }} 가 지금 항목의 company 를 읽습니다. 비우면 "block" 입니다. "item" 이 기본값이 아닌 것은 일부러입니다 — 그 이름은 표의 행이 쓰므로, 나눠 두어야 그룹 안의 표가 둘을 함께 읽을 수 있습니다. 레코드는 {{ co.company }}, 행은 {{ item.name }} 입니다. data · item · index 는 이름으로 쓸 수 없습니다.

props.break 를 "page" 로 두면 반복할 때마다 새 페이지에서 시작합니다. 한 페이지보다 큰 그룹은 알아서 나뉘므로 직접 셀 필요가 없습니다. 그룹 안의 표도 흐르기 때문에, 그려 둔 그룹보다 길어지면 그 그룹 안의 나머지를 밀어냅니다.

한 벌 안에서의 쪽번호는 [[bpage]] · [[bpages]] 입니다 — "이 청구서의 1/2쪽"이지 문서 전체의 번호가 아닙니다. page.footer 에서만 됩니다. [[page]] 는 Chromium 이 인쇄하면서 채우는데, 그쪽은 어디서 한 벌이 시작했는지 모릅니다. 그래서 이 바닥글은 인쇄가 끝난 뒤 저희가 직접 그립니다. 같은 이유로 이때의 바닥글은 Latin-1 만 쓸 수 있습니다 — 표준 폰트는 실을 수 있어도 한글 폰트는 못 싣습니다.

경로가 없거나, 배열이 아니거나, 비어 있으면 그룹은 그냥 그려지지 않습니다 — 오류가 아닙니다. 반복은 표의 행과 같은 노드 예산을 쓰므로, 배열이 아주 크면 무한히 늘어나는 대신 잘립니다. 그룹끼리 겹쳐 넣을 수는 없습니다.

편집기가 더 쓰는 키

위 스키마는 렌더에 필요한 것 전부입니다. 다만 편집기에서 저장한 템플릿을 API 로 꺼내 보면 여기 없는 키가 셋 더 붙어 있습니다 — name · hidden · lockOwn. 저희 검증기는 이 셋을 받아들이므로 그대로 두셔도 되고, 지우셔도 렌더 결과는 같습니다.

name 은 객체 목록에 보일 이름이고, lockOwn 은 편집기에서 잠갔는지입니다. 둘 다 렌더에는 아무 영향이 없습니다.

hidden 은 다릅니다 — true 면 그 노드와 그 안에 든 것이 출력에서 통째로 빠집니다. 편집기의 눈 아이콘이 이 값을 씁니다. show 와 헷갈리기 쉬운데, show 는 데이터에 따라 나올지 말지를 정하는 조건식이고 hidden 은 작성자가 지금 안 보겠다는 뜻입니다. API 로 만드는 문서라면 hidden 대신 show 를 쓰십시오.

예시
{
  "v": 1,
  "unit": "px",
  "page": {
    "size": "a4",
    "orientation": "portrait",
    "margin": { "t": 48, "r": 48, "b": 56, "l": 48 },
    "header": "",
    "footer": "{{ data.company }} · [[page]]/[[pages]]"
  },
  "fonts": [],
  "styles": [
    {
      "id": "s_title",
      "fontFamily": "Noto Sans CJK KR",
      "fontSize": 26, "fontWeight": 700, "italic": false,
      "color": "#1a1a1a", "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": 400, "h": 36,
      "show": "", "styleId": "s_title",
      "props": {
        "text": "{{ data.title }}",
        "src": "", "fit": "", "stroke": "", "fill": "",
        "thickness": 0, "bind": "",
        "headerHeight": 0, "rowHeight": 0, "max": 0, "columns": []
      }
    },
    {
      "id": "n_items", "parent": "", "order": 1, "type": "table",
      "x": 0, "y": 168, "w": 499, "h": 300,
      "show": "", "styleId": "",
      "props": {
        "text": "", "src": "", "fit": "", "stroke": "", "fill": "",
        "thickness": 0,
        "bind": "data.items",
        "headerHeight": 32, "rowHeight": 28, "max": 200,
        "columns": [
          { "header": "Item",  "cell": "{{ item.name }}",  "width": 289, "align": "left" },
          { "header": "Qty",   "cell": "{{ item.qty }}",   "width": 60,  "align": "center" },
          { "header": "Price", "cell": "{{ item.price }}", "width": 150, "align": "right" }
        ]
      }
    }
  ]
}
요청
{
  "template": { /* the object on the left */ },
  "data": {
    "company": "ACME Inc.",
    "title": "Invoice #1042",
    "items": [
      { "name": "Design",  "qty": 2, "price": 120 },
      { "name": "Hosting", "qty": 1, "price": 30 }
    ]
  }
}