계약서
약관이 흐르고 마지막에 서명란이 오는 계약서를 만듭니다. 서식 있는 본문, 서명·텍스트·체크 입력칸, 그리고 한 항목이 페이지 경계에서 갈리지 않게 하는 법을 배웁니다.
여기서 배우는 것
편집기로 만들기
- 제목 라벨을 놓고, 그 아래에 서식 본문을 넣습니다. 서식 본문은 마크업 한 덩어리를 받아 흐르는 글로 그립니다 — 조항이 늘어도 배치를 다시 잡지 않아도 됩니다.
- 본문에 조항을 씁니다. 허용된 태그만 남고 나머지는 태그만 사라집니다 — script · style · iframe 은 내용까지 버립니다. 붙여 넣기 전에 그 사실을 알고 계십시오.
- 줄 간격을 1.7 쯤으로 올립니다. 계약서는 눈으로 훑는 문서가 아니라 한 줄씩 읽는 문서입니다.
- 입력칸을 넣고 종류를 서명으로 고릅니다. 이름은 signature 로 둡니다 — 받는 쪽 프로그램이 값을 읽을 때 쓰는 열쇠입니다.
- 서명 칸에는 반드시 선을 그으십시오. 서명 칸은 PDF 뷰어조차 아무것도 그리지 않으므로, 인쇄하면 빈 공간만 남습니다.
- 이름과 날짜를 받을 텍스트 입력칸을 둘 더 놓고, 이름 칸은 필수로 표시합니다. 필수는 표시일 뿐 저희가 검사하지 않으며, 강제 여부는 PDF 를 여는 프로그램이 정합니다.
- 동의 확인용 체크 칸을 하나 놓습니다. 옆에는 무엇에 동의하는지 라벨로 적어야 합니다 — 체크 칸 자체에는 글이 없습니다.
- 서명란 전체를 사각형 안에 넣고 그 사각형에 "항목을 쪼개지 않기" 를 켜면, 서명란이 페이지 경계에서 반으로 갈리지 않습니다.
스키마 · API 로 만들기
- html 노드는 props.html 에 마크업을 받습니다. 라벨의 text 와 달리 태그가 살아 있으므로, 검증 규칙도 실패 모드도 다릅니다 — 그래서 키를 나눠 두었습니다.
- {{ }} 는 html 안에서 계산되지 않고 **글자로** 들어갑니다. 값이 필요하면 라벨을 쓰거나, 서버에서 마크업을 만들어 넣으십시오.
- 입력칸은 field 노드 하나에 fieldKind 로 종류를 정합니다 — text · check · select · list · radio · button · sign.
- 값을 미리 채워 둘 수는 없습니다. 서명을 받은 뒤 값을 넣으려면 POST /v1/files 로 올리고 POST /v1/pdf/fields 로 채우십시오.
- 개인정보가 담기므로 password 를 함께 보내 암호를 거는 것을 권합니다. 저희는 암호를 보관하지 않으므로 잃어버리면 저희도 열 수 없습니다.
만든 뒤에
서명받은 뒤 값 채우기
POST /v1/pdf/fields
{
"file": "file_01J…",
"values": {
"signer_name": "Do-hyun Kim",
"signed_at": "2026-09-01",
"agree": true
}
}완성 문서
아래 JSON 이 위 두 순서가 만드는 바로 그 문서입니다. 저희 테스트가 이 문서를 검증하고 경고 없이 렌더되는지 확인하므로, 그대로 붙여 넣어 쓰셔도 됩니다.
{
"v": 1,
"unit": "px",
"page": {
"size": "a4",
"orientation": "portrait",
"margin": {
"t": 48,
"r": 48,
"b": 56,
"l": 48
},
"header": "",
"footer": "{{ data.title }} · [[page]] / [[pages]]"
},
"fonts": [],
"styles": [
{
"id": "s_h1",
"fontFamily": "Liberation Sans",
"fontSize": 22,
"fontWeight": 700,
"italic": false,
"color": "#111111",
"bg": "transparent",
"align": "left",
"valign": "top",
"lineHeight": 1.5,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_body",
"fontFamily": "Liberation Sans",
"fontSize": 12,
"fontWeight": 400,
"italic": false,
"color": "#222222",
"bg": "transparent",
"align": "left",
"valign": "top",
"lineHeight": 1.7,
"border": "none",
"radius": 0,
"opacity": 1
},
{
"id": "s_cap",
"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
},
{
"id": "s_field",
"fontFamily": "Liberation Sans",
"fontSize": 12,
"fontWeight": 400,
"italic": false,
"color": "#111111",
"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": 30,
"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_body",
"parent": "",
"order": 1,
"type": "html",
"x": 0,
"y": 48,
"w": 697,
"h": 0,
"show": "",
"styleId": "s_body",
"props": {
"text": "",
"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": "<p>This agreement is entered into between the Client and the Supplier for the provision of services on the terms below.</p><h3>1. Purpose</h3><p>This agreement sets out the scope of the services the Supplier provides to the Client and the fee payable for them.</p><h3>2. Term</h3><p>The term is six months from the commencement date.</p><h3>3. Payment</h3><ul><li>30% on commencement</li><li>40% at the midpoint</li><li>30% on completion</li></ul>"
},
"name": "Clauses"
},
{
"id": "n_signbox",
"parent": "",
"order": 2,
"type": "rect",
"x": 0,
"y": 420,
"w": 697,
"h": 140,
"show": "",
"styleId": "s_cap",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 0,
"stroke": "",
"fill": "",
"repeat": "",
"as": "",
"break": "",
"keep": "together",
"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 block"
},
{
"id": "n_agree",
"parent": "n_signbox",
"order": 3,
"type": "field",
"x": 12,
"y": 12,
"w": 18,
"h": 18,
"show": "",
"styleId": "s_field",
"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": "check",
"fieldName": "agree",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Consent box"
},
{
"id": "n_agree_l",
"parent": "n_signbox",
"order": 4,
"type": "label",
"x": 38,
"y": 12,
"w": 400,
"h": 18,
"show": "",
"styleId": "s_cap",
"props": {
"text": "I have read and understood the clauses above and agree to them.",
"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": "Consent line"
},
{
"id": "n_name",
"parent": "n_signbox",
"order": 5,
"type": "field",
"x": 12,
"y": 52,
"w": 260,
"h": 28,
"show": "",
"styleId": "s_field",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 1,
"stroke": "#8a93a0",
"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": "text",
"fieldName": "signer_name",
"options": [],
"required": true,
"readonly": false,
"html": ""
},
"name": "Name field"
},
{
"id": "n_name_l",
"parent": "n_signbox",
"order": 6,
"type": "label",
"x": 12,
"y": 84,
"w": 260,
"h": 16,
"show": "",
"styleId": "s_cap",
"props": {
"text": "Full name (required)",
"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 caption"
},
{
"id": "n_date",
"parent": "n_signbox",
"order": 7,
"type": "field",
"x": 292,
"y": 52,
"w": 160,
"h": 28,
"show": "",
"styleId": "s_field",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 1,
"stroke": "#8a93a0",
"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": "text",
"fieldName": "signed_at",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Date field"
},
{
"id": "n_date_l",
"parent": "n_signbox",
"order": 8,
"type": "label",
"x": 292,
"y": 84,
"w": 160,
"h": 16,
"show": "",
"styleId": "s_cap",
"props": {
"text": "Date signed",
"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": "Date caption"
},
{
"id": "n_sign",
"parent": "n_signbox",
"order": 9,
"type": "field",
"x": 472,
"y": 52,
"w": 213,
"h": 56,
"show": "",
"styleId": "s_field",
"props": {
"text": "",
"src": "",
"fit": "",
"thickness": 1,
"stroke": "#8a93a0",
"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": "sign",
"fieldName": "signature",
"options": [],
"required": false,
"readonly": false,
"html": ""
},
"name": "Signature field"
},
{
"id": "n_sign_l",
"parent": "n_signbox",
"order": 10,
"type": "label",
"x": 472,
"y": 112,
"w": 213,
"h": 16,
"show": "",
"styleId": "s_cap",
"props": {
"text": "Signature",
"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": "Signature caption"
}
]
}요청 본문입니다. template 자리에 위 문서를, data 자리에 아래 데이터를 넣습니다.
{
"template": "(the document above)",
"data": {
"title": "Services agreement"
},
"password": "{{open password for this contract}}",
"permissions": {
"modify": false,
"copy": false
}
}