e-Form consent
Inputs on the page (text, date, list, radio, check, signature), validation, submit with an audit hash, fillable PDF.
Report definition (JSON)
{
"name": "회원 가입 신청서",
"page": {
"size": "A4",
"margins": [
18,
15,
15,
15
]
},
"font": {
"family": "Nanum Gothic",
"size": 10
},
"bands": {
"title": {
"height": 250,
"elements": [
{
"type": "text",
"x": 0,
"y": 0,
"w": 180,
"h": 12,
"text": "회원 가입 신청서",
"style": {
"fontSize": 20,
"bold": true,
"align": "center",
"letterSpacing": 1.5
}
},
{
"type": "text",
"x": 0,
"y": 14,
"w": 180,
"h": 6,
"text": "신청번호 {applicationNo} · 접수일 {format(today(), \"yyyy-MM-dd\")}",
"style": {
"fontSize": 8.5,
"color": "#555",
"align": "right"
}
},
{
"type": "text",
"x": 0,
"y": 24,
"w": 35,
"h": 9,
"text": "성명",
"style": {
"border": "0.2 solid #8a94a6",
"padding": 1.5,
"valign": "middle",
"background": "#eef1f6",
"bold": true,
"align": "center"
}
},
{
"type": "rect",
"x": 35,
"y": 24,
"w": 145,
"h": 9,
"width": 0.2,
"color": "#8a94a6"
},
{
"id": "in-name",
"type": "input",
"inputType": "text",
"name": "name",
"x": 38,
"y": 25.5,
"w": 60,
"h": 6,
"value": "applicant.name",
"required": true,
"maxLength": 20,
"placeholder": "홍길동",
"style": {
"border": {
"bottom": "0.2 solid #555"
}
}
},
{
"type": "text",
"x": 100,
"y": 25.5,
"w": 25,
"h": 6,
"text": "생년월일",
"style": {
"bold": true,
"align": "right"
}
},
{
"id": "in-birth",
"type": "input",
"inputType": "date",
"name": "birth",
"x": 128,
"y": 25.5,
"w": 48,
"h": 6,
"format": "yyyy년 M월 d일",
"required": true,
"style": {
"border": {
"bottom": "0.2 solid #555"
}
}
},
{
"type": "text",
"x": 0,
"y": 33,
"w": 35,
"h": 9,
"text": "휴대전화",
"style": {
"border": "0.2 solid #8a94a6",
"padding": 1.5,
"valign": "middle",
"background": "#eef1f6",
"bold": true,
"align": "center"
}
},
{
"type": "rect",
"x": 35,
"y": 33,
"w": 145,
"h": 9,
"width": 0.2,
"color": "#8a94a6"
},
{
"id": "in-phone",
"type": "input",
"inputType": "text",
"name": "phone",
"x": 38,
"y": 34.5,
"w": 60,
"h": 6,
"placeholder": "010-0000-0000",
"required": true,
"maxLength": 13,
"style": {
"border": {
"bottom": "0.2 solid #555"
}
}
},
{
"type": "text",
"x": 100,
"y": 34.5,
"w": 25,
"h": 6,
"text": "가입 지점",
"style": {
"bold": true,
"align": "right"
}
},
{
"id": "in-branch",
"type": "input",
"inputType": "select",
"name": "branch",
"x": 128,
"y": 34.5,
"w": 48,
"h": 6,
"required": true,
"options": [
{
"value": "seoul",
"label": "서울 본점"
},
{
"value": "busan",
"label": "부산 지점"
},
{
"value": "daegu",
"label": "대구 지점"
}
],
"style": {
"border": {
"bottom": "0.2 solid #555"
}
}
},
{
"type": "text",
"x": 0,
"y": 42,
"w": 35,
"h": 9,
"text": "이메일",
"style": {
"border": "0.2 solid #8a94a6",
"padding": 1.5,
"valign": "middle",
"background": "#eef1f6",
"bold": true,
"align": "center"
}
},
{
"type": "rect",
"x": 35,
"y": 42,
"w": 145,
"h": 9,
"width": 0.2,
"color": "#8a94a6"
},
{
"id": "in-email",
"type": "input",
"inputType": "text",
"name": "email",
"x": 38,
"y": 43.5,
"w": 138,
"h": 6,
"placeholder": "[email protected]",
"style": {
"border": {
"bottom": "0.2 solid #555"
}
}
},
{
"type": "text",
"x": 0,
"y": 51,
"w": 35,
"h": 9,
"text": "회원 구분",
"style": {
"border": "0.2 solid #8a94a6",
"padding": 1.5,
"valign": "middle",
"background": "#eef1f6",
"bold": true,
"align": "center"
}
},
{
"type": "rect",
"x": 35,
"y": 51,
"w": 145,
"h": 9,
"width": 0.2,
"color": "#8a94a6"
},
{
"id": "in-plan",
"type": "input",
"inputType": "radio",
"name": "plan",
"x": 38,
"y": 52.5,
"w": 120,
"h": 6,
"value": "'basic'",
"options": [
{
"value": "basic",
"label": "일반 회원"
},
{
"value": "premium",
"label": "우수 회원"
},
{
"value": "corp",
"label": "법인 회원"
}
]
},
{
"type": "text",
"x": 0,
"y": 60,
"w": 35,
"h": 9,
"text": "가족 수",
"style": {
"border": "0.2 solid #8a94a6",
"padding": 1.5,
"valign": "middle",
"background": "#eef1f6",
"bold": true,
"align": "center"
}
},
{
"type": "rect",
"x": 35,
"y": 60,
"w": 145,
"h": 9,
"width": 0.2,
"color": "#8a94a6"
},
{
"id": "in-family",
"type": "input",
"inputType": "number",
"name": "family",
"x": 38,
"y": 61.5,
"w": 25,
"h": 6,
"format": "#,##0",
"placeholder": "0",
"style": {
"border": {
"bottom": "0.2 solid #555"
},
"align": "right"
}
},
{
"type": "text",
"x": 66,
"y": 61.5,
"w": 10,
"h": 6,
"text": "명"
},
{
"type": "text",
"x": 0,
"y": 76,
"w": 180,
"h": 8,
"text": "개인정보 수집·이용 동의",
"style": {
"fontSize": 12,
"bold": true,
"border": {
"bottom": "0.4 solid #2340f5"
}
}
},
{
"type": "text",
"x": 0,
"y": 86,
"w": 180,
"h": 34,
"markup": true,
"text": "1. **수집 항목**: 성명, 생년월일, 휴대전화, 이메일\n2. **수집 목적**: 회원 관리, 서비스 안내, 민원 처리\n3. **보유 기간**: 회원 탈퇴 후 5년 (관계 법령에 따라 보관이 필요한 경우 그 기간)\n4. 동의를 거부할 수 있으나, 필수 항목에 동의하지 않으면 가입할 수 없습니다.",
"style": {
"border": "0.2 solid #8a94a6",
"padding": 1.5,
"valign": "top",
"lineHeight": 1.6,
"background": "#fafbfd"
}
},
{
"id": "in-agree",
"type": "input",
"inputType": "check",
"name": "agreePrivacy",
"x": 2,
"y": 123,
"w": 110,
"h": 5.5,
"label": "(필수) 개인정보 수집·이용에 동의합니다",
"required": true
},
{
"id": "in-third",
"type": "input",
"inputType": "check",
"name": "agreeThirdParty",
"x": 2,
"y": 131,
"w": 110,
"h": 5.5,
"label": "(필수) 개인정보 제3자 제공에 동의합니다",
"required": true
},
{
"id": "in-mkt",
"type": "input",
"inputType": "check",
"name": "agreeMarketing",
"x": 2,
"y": 139,
"w": 110,
"h": 5.5,
"label": "(선택) 마케팅 정보 수신에 동의합니다"
},
{
"type": "text",
"x": 0,
"y": 156,
"w": 180,
"h": 8,
"text": "위와 같이 회원 가입을 신청합니다.",
"style": {
"align": "center",
"fontSize": 11
}
},
{
"type": "text",
"x": 0,
"y": 166,
"w": 180,
"h": 7,
"text": "{format(today(), \"yyyy년 M월 d일\")}",
"style": {
"align": "center"
}
},
{
"type": "text",
"x": 70,
"y": 186,
"w": 25,
"h": 18,
"text": "신청인",
"style": {
"bold": true,
"align": "right",
"valign": "middle"
}
},
{
"id": "in-sign",
"type": "input",
"inputType": "signature",
"name": "signature",
"x": 100,
"y": 180,
"w": 70,
"h": 26,
"required": true,
"placeholder": "여기에 서명하세요",
"style": {
"border": {
"bottom": "0.3 solid #333"
}
}
},
{
"type": "text",
"x": 171,
"y": 192,
"w": 9,
"h": 8,
"text": "(인)",
"style": {
"color": "#777"
}
},
{
"type": "text",
"x": 0,
"y": 236,
"w": 180,
"h": 6,
"text": "프리리포트 회원센터 귀중",
"style": {
"align": "center",
"bold": true,
"fontSize": 12
}
}
]
}
}
}Data
Long lists show their first three items.
{
"applicationNo": "A-2026-01042",
"applicant": {
"name": ""
}
}Code for this demo
import type { ReportDefinition, ReportElement, Viewer } from '@freereportjs/core';
import { button, mountViewer, toggle, tr, type DemoContext } from '../ui';
/**
* e-Form: a membership application with a consent section. Inputs sit on the page (text, date,
* select, radio, check boxes and a signature pad); 제출 validates them and hands over the values,
* the signature with its stroke log, and a SHA-256 hash for the audit record. Print and PDF draw
* what was entered; "fillable PDF" writes AcroForm fields instead.
*/
const cell = { border: '0.2 solid #8a94a6', padding: 1.5, valign: 'middle' as const };
const head = { ...cell, background: '#eef1f6', bold: true, align: 'center' as const };
const line = { border: { bottom: '0.2 solid #555' } };
const row = (y: number, label: string, input: ReportElement): ReportElement[] => [
{ type: 'text', x: 0, y, w: 35, h: 9, text: label, style: head },
{ type: 'rect', x: 35, y, w: 145, h: 9, width: 0.2, color: '#8a94a6' },
{ ...input, y: y + 1.5 } as ReportElement,
];
export const definition: ReportDefinition = {
name: '회원 가입 신청서',
page: { size: 'A4', margins: [18, 15, 15, 15] },
font: { family: 'Nanum Gothic', size: 10 },
bands: {
title: {
height: 250, elements: [
{ type: 'text', x: 0, y: 0, w: 180, h: 12, text: '회원 가입 신청서', style: { fontSize: 20, bold: true, align: 'center', letterSpacing: 1.5 } },
{ type: 'text', x: 0, y: 14, w: 180, h: 6, text: '신청번호 {applicationNo} · 접수일 {format(today(), "yyyy-MM-dd")}', style: { fontSize: 8.5, color: '#555', align: 'right' } },
...row(24, '성명', { id: 'in-name', type: 'input', inputType: 'text', name: 'name', x: 38, y: 0, w: 60, h: 6, value: 'applicant.name', required: true, maxLength: 20, placeholder: '홍길동', style: line }),
{ type: 'text', x: 100, y: 25.5, w: 25, h: 6, text: '생년월일', style: { bold: true, align: 'right' } },
{ id: 'in-birth', type: 'input', inputType: 'date', name: 'birth', x: 128, y: 25.5, w: 48, h: 6, format: 'yyyy년 M월 d일', required: true, style: line },
...row(33, '휴대전화', { id: 'in-phone', type: 'input', inputType: 'text', name: 'phone', x: 38, y: 0, w: 60, h: 6, placeholder: '010-0000-0000', required: true, maxLength: 13, style: line }),
{ type: 'text', x: 100, y: 34.5, w: 25, h: 6, text: '가입 지점', style: { bold: true, align: 'right' } },
{ id: 'in-branch', type: 'input', inputType: 'select', name: 'branch', x: 128, y: 34.5, w: 48, h: 6, required: true, options: [{ value: 'seoul', label: '서울 본점' }, { value: 'busan', label: '부산 지점' }, { value: 'daegu', label: '대구 지점' }], style: line },
...row(42, '이메일', { id: 'in-email', type: 'input', inputType: 'text', name: 'email', x: 38, y: 0, w: 138, h: 6, placeholder: '[email protected]', style: line }),
...row(51, '회원 구분', { id: 'in-plan', type: 'input', inputType: 'radio', name: 'plan', x: 38, y: 0, w: 120, h: 6, value: "'basic'", options: [{ value: 'basic', label: '일반 회원' }, { value: 'premium', label: '우수 회원' }, { value: 'corp', label: '법인 회원' }] }),
...row(60, '가족 수', { id: 'in-family', type: 'input', inputType: 'number', name: 'family', x: 38, y: 0, w: 25, h: 6, format: '#,##0', placeholder: '0', style: { ...line, align: 'right' } }),
{ type: 'text', x: 66, y: 61.5, w: 10, h: 6, text: '명' },
{ type: 'text', x: 0, y: 76, w: 180, h: 8, text: '개인정보 수집·이용 동의', style: { fontSize: 12, bold: true, border: { bottom: '0.4 solid #2340f5' } } },
{ type: 'text', x: 0, y: 86, w: 180, h: 34, markup: true, text: '1. **수집 항목**: 성명, 생년월일, 휴대전화, 이메일\n2. **수집 목적**: 회원 관리, 서비스 안내, 민원 처리\n3. **보유 기간**: 회원 탈퇴 후 5년 (관계 법령에 따라 보관이 필요한 경우 그 기간)\n4. 동의를 거부할 수 있으나, 필수 항목에 동의하지 않으면 가입할 수 없습니다.', style: { ...cell, valign: 'top', lineHeight: 1.6, background: '#fafbfd' } },
{ id: 'in-agree', type: 'input', inputType: 'check', name: 'agreePrivacy', x: 2, y: 123, w: 110, h: 5.5, label: '(필수) 개인정보 수집·이용에 동의합니다', required: true },
{ id: 'in-third', type: 'input', inputType: 'check', name: 'agreeThirdParty', x: 2, y: 131, w: 110, h: 5.5, label: '(필수) 개인정보 제3자 제공에 동의합니다', required: true },
{ id: 'in-mkt', type: 'input', inputType: 'check', name: 'agreeMarketing', x: 2, y: 139, w: 110, h: 5.5, label: '(선택) 마케팅 정보 수신에 동의합니다' },
{ type: 'text', x: 0, y: 156, w: 180, h: 8, text: '위와 같이 회원 가입을 신청합니다.', style: { align: 'center', fontSize: 11 } },
{ type: 'text', x: 0, y: 166, w: 180, h: 7, text: '{format(today(), "yyyy년 M월 d일")}', style: { align: 'center' } },
{ type: 'text', x: 70, y: 186, w: 25, h: 18, text: '신청인', style: { bold: true, align: 'right', valign: 'middle' } },
{ id: 'in-sign', type: 'input', inputType: 'signature', name: 'signature', x: 100, y: 180, w: 70, h: 26, required: true, placeholder: '여기에 서명하세요', style: { border: { bottom: '0.3 solid #333' } } },
{ type: 'text', x: 171, y: 192, w: 9, h: 8, text: '(인)', style: { color: '#777' } },
{ type: 'text', x: 0, y: 236, w: 180, h: 6, text: '프리리포트 회원센터 귀중', style: { align: 'center', bold: true, fontSize: 12 } },
],
},
},
};
export const sampleData = { applicationNo: 'A-2026-01042', applicant: { name: '' } };
export default function mount(ctx: DemoContext) {
let fillable = false;
let viewer: Viewer | null = null;
const open = () => {
viewer?.destroy();
viewer = mountViewer(ctx, {
definition,
data: sampleData,
zoom: 'page',
fileName: '회원가입신청서',
onFormChange: e => ctx.log(`${tr(ctx.lang, { en: 'changed', ko: '입력' })} ${e.name} = ${e.value && typeof e.value === 'object' ? tr(ctx.lang, { en: `signature (${(e.value as { strokes: unknown[] }).strokes.length} strokes)`, ko: `서명 (${(e.value as { strokes: unknown[] }).strokes.length}획)` }) : JSON.stringify(e.value)}`),
// a real system would POST this to its server and keep hash + submittedAt as the audit record
onSubmit: async data => {
await new Promise(r => setTimeout(r, 400));
const sign = data.signatures.signature;
ctx.log(`${tr(ctx.lang, { en: 'submitted', ko: '제출' })} ${data.submittedAt} · SHA-256 ${data.hash.slice(0, 16)}… · ${tr(ctx.lang, { en: 'signature', ko: '서명' })} ${sign ? `${sign.strokes.length}${tr(ctx.lang, { en: ' strokes', ko: '획' })}, ${sign.strokes.reduce((a, s) => a + s.points, 0)} ${tr(ctx.lang, { en: 'points', ko: '점' })}` : '-'}`);
ctx.log(JSON.stringify(data.values));
return true;
},
});
};
button(ctx, tr(ctx.lang, { en: 'Fill sample', ko: '예시 채우기' }), () => {
viewer?.setFormData({ values: { name: '김민준', birth: '1990-05-17', phone: '010-1234-5678', branch: 'seoul', email: '[email protected]', plan: 'premium', family: 3, agreePrivacy: true, agreeThirdParty: true } });
});
button(ctx, tr(ctx.lang, { en: 'Validate', ko: '입력 검사' }), () => {
const r = viewer?.validateForm();
if (r) ctx.log(r.valid ? tr(ctx.lang, { en: 'all inputs are valid', ko: '모든 입력이 올바릅니다' }) : r.issues.map(i => `${i.name}: ${i.message}`).join(' · '));
});
toggle(ctx, tr(ctx.lang, { en: 'Fillable PDF', ko: '채울 수 있는 PDF' }), fillable, on => { fillable = on; });
button(ctx, tr(ctx.lang, { en: 'Save PDF', ko: 'PDF 저장' }), () => {
void viewer?.report.toPdf({ formFields: fillable }).then(blob => {
if (!blob) return;
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fillable ? '회원가입신청서-입력용.pdf' : '회원가입신청서.pdf';
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
});
});
open();
}