FreeReportv0.12.0English

전자 서식 동의서

용지 위의 입력 칸(글자, 날짜, 목록, 라디오, 체크, 서명), 입력 검사, SHA-256 감사 해시와 함께 제출, 채울 수 있는 PDF.

보고서 정의 (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
          }
        }
      ]
    }
  }
}

데이터

긴 목록은 처음 3건만 보여 줍니다.

{
  "applicationNo": "A-2026-01042",
  "applicant": {
    "name": ""
  }
}

이 데모의 코드

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();
}