사용법
시작하기
시작하기
FreeReport는 외부 라이브러리가 없는 TypeScript 패키지 하나입니다. 설치하고 스타일시트를 한 번 불러온 뒤, 높이가 있는 요소에 뷰어를 만들면 됩니다.
npm i @freereportjs/coreimport { createViewer } from '@freereportjs/core';
import '@freereportjs/core/styles.css';
const definition = await fetch('/reports/sales.json').then(r => r.json());
const viewer = createViewer(document.getElementById('viewer'), {
definition,
data: { rows }, // detail rows; other keys are usable in expressions
lang: 'ko',
});
// <div id="viewer" style="height: 720px"></div>빌드 도구 없이 (CDN)
script 태그로 불러오면 전역 FreeReport 객체가 생깁니다. 운영 환경에서는 v0.1.0처럼 버전을 고정하세요. latest는 항상 최신 버전입니다.
<link rel="stylesheet" href="https://freereport.app/cdn/latest/freereport.css">
<script src="https://freereport.app/cdn/latest/freereport.min.js"></script>
<div id="viewer" style="height: 720px"></div>
<script>
FreeReport.createViewer(document.getElementById('viewer'), { definition, data: { rows } });
</script><link rel="stylesheet" href="https://freereport.app/cdn/v0.1.0/freereport.css">
<script type="module">
import { createViewer } from 'https://freereport.app/cdn/v0.1.0/freereport.esm.js';
createViewer(document.getElementById('viewer'), { definition, data: { rows } });
</script>프레임워크와 전자정부
코어는 어떤 프레임워크에서든 씁니다. 컴포넌트가 붙을 때 뷰어나 디자이너를 만들고 떨어질 때 destroy하면 됩니다. 바로 쓰는 컴포넌트가 npm에 배포되어 있습니다: @freereportjs/react, @freereportjs/vue(Vue 3), @freereportjs/svelte(Svelte 5), @freereportjs/angular. 각각 뷰어와 디자이너를 제공하며, npm i @freereportjs/core @freereportjs/react처럼 코어와 함께 설치합니다.
- React: <FreeReportViewer>, <FreeReportDesigner>. 뷰어 ref는 { viewer, report }이고, 디자이너는 onChange, onSave를 받습니다.
- Vue: 뷰어는 viewer, report를 노출하고, 디자이너는 v-model:definition을 지원하며 change, update:definition, save 이벤트를 보냅니다.
- Svelte: viewer.getViewer() / getReport(), designer.getDesigner() 또는 onReady.
- Angular: <free-report-viewer>, <free-report-designer>(standalone). 디자이너는 (definitionChange)를 보내 [(definition)]이 동작하고, (save)도 보냅니다.
definition, data, params, zoom은 뷰어를 그대로 두고 갱신하고, 나머지 속성은 뷰어를 다시 만듭니다. 두 컴포넌트 모두 기본 높이는 100%, 최소 480px입니다.
// npm i @freereportjs/core @freereportjs/react
import { useRef, useState } from 'react';
import { FreeReportDesigner, FreeReportViewer, type ReportDefinition } from '@freereportjs/react';
import '@freereportjs/core/styles.css';
export function SalesReport({ definition, rows }) {
const ref = useRef(null); // { viewer, report }
return <FreeReportViewer ref={ref} definition={definition} data={{ rows }} lang="ko" style={{ height: 720 }} />;
}
export function SalesDesigner({ initial, rows, save }) {
const [def, setDef] = useState<ReportDefinition>(initial);
return <FreeReportDesigner definition={def} data={{ rows }} onChange={setDef} onSave={save} />;
}<!-- npm i @freereportjs/core @freereportjs/vue -->
<script setup>
import { ref } from 'vue';
import { FreeReportDesigner, FreeReportViewer } from '@freereportjs/vue';
import '@freereportjs/core/styles.css';
const props = defineProps(['initial', 'rows']);
const def = ref(props.initial);
const viewer = ref(null); // viewer.value.viewer / viewer.value.report
const save = d => fetch('/api/reports/sales', { method: 'PUT', body: JSON.stringify(d) });
</script>
<template>
<FreeReportViewer ref="viewer" :definition="def" :data="{ rows }" lang="ko" :height="720" />
<FreeReportDesigner v-model:definition="def" :data="{ rows }" @save="save" />
</template><!-- npm i @freereportjs/core @freereportjs/svelte (Svelte 5) -->
<script>
import { FreeReportDesigner, FreeReportViewer } from '@freereportjs/svelte';
import '@freereportjs/core/styles.css';
let { initial, rows, save } = $props();
let def = $state(initial);
let viewer; // viewer.getViewer(), viewer.getReport()
</script>
<FreeReportViewer bind:this={viewer} definition={def} data={{ rows }} lang="ko" style="height: 720px" />
<FreeReportDesigner definition={def} data={{ rows }} onChange={d => (def = d)} onSave={save} />// npm i @freereportjs/core @freereportjs/angular
// angular.json "styles": ["node_modules/@freereportjs/core/dist/styles.css"]
import { Component } from '@angular/core';
import { FreeReportDesignerComponent, FreeReportViewerComponent } from '@freereportjs/angular';
@Component({
selector: 'app-sales-report',
imports: [FreeReportViewerComponent, FreeReportDesignerComponent],
template: `
<free-report-viewer [definition]="def" [data]="data" lang="ko" [height]="720" />
<free-report-designer [(definition)]="def" [data]="data" (save)="save($event)" />`,
})
export class SalesReportComponent {
def = initialDefinition;
data = { rows: [] };
save(def) { /* PUT the JSON to your server */ }
}<%-- reportView.jsp: the viewer in an eGovFrame (Spring + JSP) page --%>
<link rel="stylesheet" href="<c:url value='/static/freereport/freereport.css'/>">
<div id="viewer" style="height: 760px" data-ctx="${fn:escapeXml(pageContext.request.contextPath)}"></div>
<script src="<c:url value='/static/freereport/freereport.min.js'/>"></script>
<script>
var ctx = document.getElementById('viewer').dataset.ctx;
Promise.all([
fetch(ctx + '/report/def/sales.do').then(function (r) { return r.json(); }), // report JSON
fetch(ctx + '/report/data/sales.do').then(function (r) { return r.json(); }) // { rows: [...] } from MyBatis
]).then(function (res) {
FreeReport.createViewer(document.getElementById('viewer'), {
definition: res[0], data: res[1], lang: 'ko',
onOutput: function (e) { /* POST to the output log controller */ }
});
});
</script>전자정부 표준프레임워크 예제 프로젝트
예제는 전자정부 표준프레임워크 4.3(Spring 5.3, MyBatis, JDK 8 이상) 프로젝트로, 보고서 조회·인쇄·PDF·엑셀 출력과 출력 이력 관리까지 담았습니다. 서버는 보고서 정의와 데이터를 JSON으로만 내려 주고, 화면 구성·인쇄·파일 생성은 브라우저에서 처리하므로 별도 리포트 서버, ActiveX, 설치 프로그램이 필요 없습니다. 3.x 프로젝트는 import 문과 groupId를 org.egovframe.rte에서 egovframework.rte로 바꾸면 됩니다.
| 주소 | 응답 | 설명 |
|---|---|---|
GET /report/view.do?name=sales | JSP | 뷰어 화면 (reportView.jsp) |
GET /report/designer.do?name=sales | JSP | 디자이너 화면 (reportDesigner.jsp) |
GET /report/def/{name}.do | JSON | 보고서 정의: REPORT_DEF 저장본, 없으면 classpath:reports/{name}.json |
POST /report/def/{name}.do | JSON | 디자이너에서 저장한 정의 저장 |
GET /report/data/sales.do | JSON | 보고서 데이터 (Service → MyBatis) |
POST /report/log.do | JSON | 뷰어 onOutput 이벤트를 REPORT_OUTPUT_LOG에 기록 |
GET /report/logs.do | JSP | 출력 이력 최근 100건 |
예제 프로젝트는 요청하시면 보내 드립니다: [email protected]
양식 만들기
보고서 정의
보고서는 JSON 객체입니다. 용지 설정, 기본 글꼴, 필요하면 파라미터와 스타일, 그리고 요소가 배치된 밴드로 이루어집니다. 레이아웃 엔진이 데이터와 묶어 페이지를 만들고, 뷰어·인쇄·PDF·엑셀은 모두 같은 페이지를 그리므로 결과가 똑같습니다.
{
"name": "거래처별 매출",
"page": { "size": "A4", "orientation": "portrait", "margins": 15 },
"font": { "family": "Nanum Gothic", "size": 9 },
"sort": ["customer", "date"],
"bands": {
"title": { "height": 14, "elements": [
{ "type": "text", "x": 0, "y": 0, "w": 180, "h": 10, "text": "거래처별 매출", "style": { "fontSize": 16, "bold": true } }
] },
"columnHeader": { "height": 7, "elements": [
{ "type": "text", "x": 0, "y": 0, "w": 30, "h": 7, "text": "일자", "style": { "bold": true, "border": true } },
{ "type": "text", "x": 30, "y": 0, "w": 110, "h": 7, "text": "거래처", "style": { "bold": true, "border": true } },
{ "type": "text", "x": 140, "y": 0, "w": 40, "h": 7, "text": "금액", "style": { "bold": true, "border": true } }
] },
"detail": { "height": 6, "elements": [
{ "type": "field", "x": 0, "y": 0, "w": 30, "h": 6, "expr": "date", "format": "yyyy-MM-dd", "style": { "border": true } },
{ "type": "field", "x": 30, "y": 0, "w": 110, "h": 6, "expr": "customer", "suppressRepeated": true, "style": { "border": true } },
{ "type": "field", "x": 140, "y": 0, "w": 40, "h": 6, "expr": "amount", "format": "#,##0", "style": { "border": true, "align": "right" } }
] },
"summary": { "height": 8, "elements": [
{ "type": "text", "x": 0, "y": 1, "w": 180, "h": 7, "text": "합계 {sum(amount):#,##0}원 ({koreanAmount(sum(amount))})", "style": { "align": "right", "bold": true } }
] },
"pageFooter": { "height": 8, "elements": [
{ "type": "text", "x": 0, "y": 2, "w": 180, "h": 5, "text": "{page} / {pages}", "style": { "align": "center" } }
] }
}
}| 이름 | 타입 | 설명 |
|---|---|---|
name | string | 보고서 이름. 파일 이름과 출력 이벤트에 쓰입니다. |
page | PageSetup | 용지 크기, 방향, 여백. |
font | { family, size, color } | 보고서 기본 글꼴(기본 나눔고딕 9pt). |
styles | Record<string, Style> | 이름 붙인 스타일. 요소에서 styleName으로 씁니다. |
params | Record<string, ParamDefinition> | 뷰어의 조회 조건 패널에 나오는 파라미터. |
dataSource | string | 데이터에서 상세 행 배열의 이름(기본 "rows"). |
sort | string[] | 상세 행 정렬. 예: ["region", "-amount"] ("-"는 내림차순). |
filter | Expr | 수식이 참인 행만 남깁니다. |
bands | object | title, pageHeader, columnHeader, groups, detail, summary, pageFooter, noData. |
pageHeaderSkipFirst | boolean | 첫 페이지에는 페이지 머리글을 찍지 않습니다. |
fillEmptyRows | boolean | 각 페이지의 남은 공간을 빈 상세 행으로 채웁니다(양식 표). |
watermark | string | Watermark | Watermark[] | 내용 뒤(또는 위)에 찍는 글자·이미지 표시. |
emboss | Emboss | Emboss[] | 종이에 눌러 찍은 듯한 돋움 직인·로고·글자. |
모든 위치와 크기는 밴드 왼쪽 위에서 잰 mm 단위입니다. 글자 크기는 pt입니다.
용지 설정
| 이름 | 타입 | 설명 |
|---|---|---|
size | 'A3' | 'A4' | 'A5' | 'B4' | 'B5' | 'Letter' | 'Legal' | [w, h] | 용지 이름 또는 [가로, 세로] mm. 기본 A4. |
orientation | 'portrait' | 'landscape' | 기본은 세로(portrait). |
margins | number | [top, right, bottom, left] | 여백(mm). 기본 15. |
밴드
밴드는 이 순서로 찍힙니다. 첫 페이지의 제목, 페이지마다 페이지 머리글과 컬럼 머리글, 그룹 머리글, 행마다 상세 밴드, 그룹 바닥글, 마지막 행 다음의 요약, 페이지마다 맨 아래의 페이지 바닥글.
| 이름 | 타입 | 설명 |
|---|---|---|
title | Band | 첫 페이지에 한 번, 맨 앞에 찍힙니다(제목, 머리 양식, 한 장짜리 양식 전체). |
pageHeader | Band | 모든 페이지의 맨 위. |
columnHeader | Band | 상세 행이 있는 페이지마다 페이지 머리글 아래(표 제목 행). |
groups | GroupDefinition[] | 그룹 머리글·바닥글. 바깥 그룹부터. |
detail | Band | 상세 행마다 한 번. |
summary | Band | 마지막 행 다음(총합계, 차트). |
pageFooter | Band | 모든 페이지의 맨 아래(페이지 번호, 페이지 소계). |
noData | Band | 데이터가 없을 때 상세 행 대신 찍힙니다. |
밴드 속성
| 이름 | 타입 | 설명 |
|---|---|---|
height | number | 높이(mm). |
elements | ReportElement[] | 배치된 요소. 밴드 안에서 x, y, w, h(mm). |
canGrow | boolean | 글자가 넘치면 높이를 늘립니다(상세·그룹 밴드는 기본 true). |
visible | Expr | 수식이 참일 때만 밴드를 찍습니다. |
pageBreakBefore / pageBreakAfter | boolean | 밴드 앞/뒤에서 새 페이지를 시작합니다. |
background | string | 밴드 배경색. |
altBackground | string | 상세 밴드: 한 줄 걸러 배경색. |
columns / columnGap | number | 상세 밴드: 행을 여러 열로 나란히 찍습니다(라벨 용지, 카드). 간격은 mm. |
그룹과 소계
그룹은 값이 같은 연속한 행을 묶습니다. 그룹 머리글·바닥글의 집계는 그 그룹 범위이고, 페이지 바닥글에서는 페이지 범위로, 어디서든 보고서 전체 범위로 계산할 수 있습니다.
"groups": [{
"by": "region", "sort": "asc", "repeatHeader": true, "keepWithRows": 2,
"header": { "height": 7, "elements": [
{ "type": "text", "x": 0, "y": 0, "w": 180, "h": 7, "text": "■ {region} ({count()}건)", "style": { "bold": true } }
] },
"footer": { "height": 7, "elements": [
{ "type": "text", "x": 0, "y": 0, "w": 140, "h": 7, "text": "{region} 소계", "style": { "align": "center" } },
{ "type": "field", "x": 140, "y": 0, "w": 40, "h": 7, "expr": "sum(amount)", "format": "#,##0", "style": { "align": "right" } }
] }
}],
"pageFooter": { "height": 7, "elements": [
{ "type": "text", "x": 0, "y": 1, "w": 180, "h": 6, "text": "이 페이지 소계 {sum(amount, 'page'):#,##0}원 · 누계 {sum(amount, 'report'):#,##0}원" }
] }| 이름 | 타입 | 설명 |
|---|---|---|
by | Expr | 연속한 행을 묶는 수식. 예: "region". |
sort | 'asc' | 'desc' | 먼저 그룹 수식으로 정렬합니다(없으면 순서 유지). |
header / footer | Band | 그룹 앞/뒤에 찍힙니다. 집계는 그 그룹 범위입니다. |
newPage | boolean | 그룹마다 새 페이지에서 시작합니다. |
repeatHeader | boolean | 그룹이 다음 페이지로 이어지면 머리글을 다시 찍습니다. |
keepWithRows | number | 머리글을 최소 이만큼의 행과 같은 페이지에 둡니다(기본 1). |
resetPageNumber | boolean | 그룹마다 page / pages 번호를 새로 셉니다. |
요소
| 이름 | 타입 | 설명 |
|---|---|---|
type | 'text' | 'field' | 'line' | 'rect' | 'image' | 'barcode' | 'chart' | 'table' | 'crosstab' | 요소 종류. |
x, y, w, h | number | 밴드 안의 위치와 크기(mm). |
id | string | 선택. 디자이너가 자동으로 붙입니다. |
style / styleName | Style / string | 인라인 스타일. 이름 붙인 스타일 위에 합쳐집니다. |
visible | Expr | 참일 때만 그립니다. |
canGrow | boolean | 글자가 넘치면 아래로 늘어납니다(밴드도 함께). |
stretch | boolean | 밴드가 늘어나면 같이 늘어납니다(밴드 바닥에 닿은 요소는 기본). |
name | string | 엑셀 "데이터" 모드의 열 이름. |
요소 종류
| type | 속성 | 설명 |
|---|---|---|
text | { text } | 고정 글자 또는 템플릿: "합계 {sum(amount):#,##0}원". {{ }}는 중괄호 자체. |
field | { expr, format?, nullText?, suppressRepeated? } | 수식 하나와 서식. suppressRepeated는 위 행과 같은 값을 숨깁니다. |
line | { color?, width?, dash? } | (x, y)에서 (x + w, y + h)까지. 가로선은 h = 0. 두께 mm. |
rect | { fill?, color?, width?, radius? } | 사각형. width 0이면 테두리 없음. |
image | { src? | expr?, fit? } | URL·data: URL 또는 데이터 수식. fit: contain | cover | fill. |
barcode | { expr, format?, showText?, ecc?, color? } | format: 'code128' | 'ean13' | 'qr' | 'datamatrix' | 'pdf417'. ecc: QR 오류 정정 L/M/Q/H. |
chart | { chartType, category, series, source?, title?, legend?, format?, showValues?, stacked? } | chartType: 'bar' | 'hbar' | 'line' | 'area' | 'pie' | 'doughnut'. |
table | { source, columns, rowHeight?, showHeader?, headerStyle?, footerLabel?, altBackground? } | 밴드 안의 목록(마스터–디테일). columns: { header, expr, width?, format?, align?, sum? }. |
crosstab | { rowKey, columnKey, value, aggregate?, format?, totals?, corner?, source?, cellHeight?, rowHeaderWidth?, headerStyle? } | 행 × 열 교차표와 합계. aggregate: sum | count | avg | min | max. |
스타일과 테두리
인라인 스타일은 이름 붙인 스타일(styleName) 위에, 이름 붙인 스타일은 보고서 기본 글꼴 위에 합쳐집니다. 테두리는 변마다 줄여 쓸 수 있어 표의 칸끼리 선을 맞추기 쉽습니다.
"styles": {
"th": { "bold": true, "align": "center", "background": "#eef1f6", "border": "0.2 solid #8a94a6" },
"num": { "align": "right", "border": { "bottom": "0.15 solid #b9c1cd" } }
},
"bands": { "detail": { "height": 6, "elements": [
{ "type": "field", "x": 140, "y": 0, "w": 40, "h": 6, "expr": "amount", "format": "#,##0", "styleName": "num", "style": { "bold": true } }
] } }| 이름 | 타입 | 설명 |
|---|---|---|
fontFamily, fontSize | string, number | 글꼴과 크기(pt, 기본 9). |
bold, italic, underline, strike | boolean | 글꼴 모양. |
color, background | string | 글자색, 배경색. |
align, valign | 'left' | 'center' | 'right' | 'justify'; 'top' | 'middle' | 'bottom' | 가로·세로 정렬. |
padding | number | [t, r, b, l] | 안쪽 여백(mm, 기본 좌우 1, 상하 0.5). |
border | true | "0.3 solid #333" | { top, right, bottom, left } | true는 0.2mm 검은 실선, CSS 비슷한 문자열, 또는 변별 지정. |
lineHeight | number | 글자 크기의 배수(기본 1.25). |
wrap | boolean | 긴 글자 줄바꿈(기본 true). false면 상자 끝에서 자릅니다. |
shrinkToFit | boolean | 한 줄에 들어갈 때까지 글자를 줄입니다. |
letterSpacing | number | 자간(mm). "견 적 서" 같은 제목에. |
데이터와 수식
수식과 함수
필드, 표시 조건, 필터, 텍스트 템플릿은 작고 안전한 수식 언어를 씁니다. 사칙연산, 비교, && || ! ?? ?:, 멤버 접근(customer.name, items[0])과 아래 함수를 쓸 수 있고, 전역 객체에 접근하거나 값을 바꿀 수 없습니다. 텍스트에서는 중괄호 안에 수식을 쓰고 콜론 뒤에 서식을 붙입니다: "{sum(amount):#,##0}원".
price * qty arithmetic
amount >= params.minAmount comparison with a parameter
customer.name ?? '(미지정)' member access and fallback
qty > 10 ? '대량' : '일반' condition
sum(amount) aggregate over the band's scope
sum(amount, 'page') page subtotal
sum(amount) / sum(amount, 'report') share of the grand total
format(date, 'yyyy년 M월 d일 (ddd)') 2026년 10월 15일 (목)
koreanAmount(1230000) 일금 일백이십삼만원정
bizNo('7928703276') 792-87-03276
{page} / {pages} page x / y in a text template변수
| 이름 | 타입 | 설명 |
|---|---|---|
page, pages | number | 페이지 번호와 전체 페이지 수(resetPageNumber면 그룹별). |
rowNum | number | 그룹(또는 보고서) 안의 행 번호. |
rowIndex | number | 보고서 전체의 행 번호. |
groupKey, groupLevel | unknown, number | 현재 그룹 값과 깊이. |
now | Date | 보고서를 만든 시각. |
params | object | 파라미터 값: params.from. |
row, rows, allRows, data | object, array | 현재 행, 범위 안의 행, 모든 행, 데이터 전체. |
집계와 범위
범위는 두 번째 인수입니다. 'group'(현재 그룹), 'page'(현재 페이지의 행 — 페이지 소계), 'report'(모든 행). 생략하면 밴드에 맞는 범위를 씁니다. 그룹 밴드는 그룹, 페이지 밴드는 페이지, 나머지는 보고서 전체입니다.
| 이름 | 타입 | 설명 |
|---|---|---|
sum(expr, scope?) | number | 범위 안의 합계. |
avg / min / max(expr, scope?) | number | 평균, 최소, 최대. |
count(expr?, scope?) | number | 행 수(또는 비어 있지 않은 값의 수). |
distinct(expr, scope?) | number | 서로 다른 값의 수. |
first / last(expr, scope?) | unknown | 첫/마지막 행의 값. |
runningSum(expr, scope?) | number | 현재 행까지의 누계(기본 범위 report). |
함수
| 이름 | 타입 | 설명 |
|---|---|---|
format(v, f) | string | 숫자·날짜 서식: format(date, "yyyy-MM-dd"). |
koreanAmount(n) / korean(n) | string | 1230000 → 일금 일백이십삼만원정. |
bizNo(v), phone(v) | string | 1234567890 → 123-45-67890, 0212345678 → 02-1234-5678. |
round / floor / ceil(v, digits?) | number | 소수점 자리 반올림·내림·올림. |
abs, number, string | — | 변환. |
upper, lower, trim, len, left, right, substr, replace, concat, padStart, join | string | 문자열 함수. |
iif(c, a, b), coalesce(...), isEmpty(v) | — | 조건과 대체 값(?:, ??도 됩니다). |
date, today, now, year, month, day, addDays, dateDiff, ymd | — | 날짜. |
groupSum(rows, key, value), groupCount(rows, key) | array | 키별 합계·개수. 차트 source로 쓰기 좋습니다. |
min2(a, b), max2(a, b) | number | 두 값 중 작은/큰 값. |
서식
필드의 format(또는 템플릿에서 콜론 뒤)은 숫자를 엑셀식 패턴으로, 날짜를 yyyy, MM, dd, HH, mm, ss 패턴으로 표시합니다.
| 서식 | 값 | 설명 |
|---|---|---|
#,##0 #,##0.00 0.0% | number | 엑셀식 숫자 서식. |
₩#,##0 #,##0원 #,##0,"천" | number | 앞뒤 글자와 천 단위 나누기. |
#,##0;(#,##0);- | number | 구역: 양수;음수;0. |
yyyy-MM-dd HH:mm:ss | date | 날짜와 시간. |
yyyy년 M월 d일 (ddd) | date | 요일이 붙은 한국식 날짜. |
a h:mm | date | 오전/오후. |
korean, koreanNumber | number | 일금 ○○원정 / 십이만삼천. |
bizNo, upper, lower | string | 사업자번호, 대소문자. |
조회 조건
파라미터는 뷰어의 조회 조건 패널에 나오고 수식에서는 params.이름으로 씁니다. fetchData를 주면 사용자가 조회를 누를 때마다 서버에서 행을 받아 옵니다.
// definition.params: { from: { label: '시작일', type: 'date', required: true },
// region: { label: '지역', type: 'select', options: [{ value: '' , label: '전체' }, { value: '서울' }] } }
createViewer(el, {
definition,
paramsOpen: true,
fetchData: params => fetch('/api/sales?' + new URLSearchParams(params)).then(r => r.json()),
});| 이름 | 타입 | 설명 |
|---|---|---|
label | string | 조회 조건 패널의 이름. |
type | 'string' | 'number' | 'date' | 'boolean' | 'select' | 입력 종류. |
default | unknown | 초깃값. |
options | { value, label? }[] | select의 선택지. |
required | boolean | 조회 전에 꼭 입력해야 합니다. |
바코드·차트·보안 표시
바코드
바코드는 벡터 도형으로 그려 어떤 배율에서도, 인쇄와 PDF에서도 선명합니다. Code128, EAN-13(12자리를 주면 검사 숫자를 붙임), QR(오류 정정 L/M/Q/H), DataMatrix, PDF417을 지원합니다. 상세 밴드를 여러 열(columns: 3)로 찍으면 한 행이 라벨 용지의 라벨 한 칸이 됩니다. 라벨 데모
"detail": { "height": 38, "columns": 3, "columnGap": 3, "elements": [
{ "type": "field", "x": 2, "y": 2, "w": 36, "h": 6, "expr": "name", "style": { "bold": true } },
{ "type": "barcode", "x": 40, "y": 2, "w": 18, "h": 18, "expr": "'https://example.com/a/' + assetNo", "format": "qr", "ecc": "M" },
{ "type": "barcode", "x": 2, "y": 24, "w": 56, "h": 11, "expr": "assetNo", "format": "code128" }
] }차트·표·교차표
차트는 범위 안의 행(또는 source로 준 배열)에서 행마다 category와 하나 이상의 series 값을 가져옵니다. groupSum()으로 키별 합계를 만들 수 있습니다. table 요소는 밴드 안에 배열을 목록으로 그리고(송장마다 품목), crosstab은 행 × 열 교차표와 합계를 만듭니다. 차트 데모 · 교차 집계 데모
{ "type": "chart", "x": 0, "y": 16, "w": 110, "h": 60, "chartType": "bar", "title": "지역별 매출",
"source": "groupSum(rows, 'region', 'amount')", "category": "region",
"series": [{ "expr": "amount", "label": "매출" }], "format": "#,##0" },
{ "type": "table", "x": 0, "y": 20, "w": 180, "h": 20, "source": "items", "altBackground": "#fafbfd",
"columns": [{ "header": "품목", "expr": "name" }, { "header": "수량", "expr": "qty", "width": 20, "align": "right" },
{ "header": "금액", "expr": "qty * price", "width": 40, "format": "#,##0", "sum": true }] },
{ "type": "crosstab", "x": 0, "y": 0, "w": 180, "h": 50, "rowKey": "region", "columnKey": "category",
"value": "amount", "aggregate": "sum", "format": "#,##0", "corner": "지역 / 품목군" }워터마크와 엠보싱
워터마크는 문자열이나 설정 객체, 또는 그 목록입니다. 텍스트가 템플릿이라 보안 워터마크에 출력자와 출력 일시를 넣고 페이지 전체에 반복할 수 있습니다. 사진을 찍거나 복사해도 남습니다.
"watermark": [
{ "text": "{params.user} · {format(now(), 'yyyy-MM-dd HH:mm')} 출력", "repeat": true, "opacity": 0.07, "fontSize": 10, "gap": 40 },
{ "text": "사 본", "fontSize": 110, "color": "#c8102e", "pages": "all" }
],
"emboss": { "text": "(주)세컨드코리아 대표이사 직인 ·", "centerText": "인증", "x": 140, "y": 240, "w": 34 }| 이름 | 타입 | 설명 |
|---|---|---|
text | string | 템플릿. 예: "{params.user} · {format(now(), 'yyyy-MM-dd HH:mm')}". |
image, imageWidth | string, number | 글자 대신(또는 함께) 쓰는 이미지와 너비(mm). |
repeat, gap | boolean, number | 페이지 전체에 반복(보안 패턴). 간격 mm(기본 45). |
opacity, angle, color, fontSize, bold | — | 기본 투명도 0.12, 각도 -35°. |
pages | 'all' | 'first' | 'last' | 'odd' | 'even' | 찍을 페이지. |
layer | 'back' | 'front' | 내용 뒤(기본) 또는 위. |
웹 엠보싱
엠보싱은 빛과 그림자로 종이에 눌러 찍은 듯한 돋움 직인·로고·글자를 그립니다. 화면, 인쇄, PDF에 모두 나옵니다.
| 이름 | 타입 | 설명 |
|---|---|---|
text, centerText | string | 원형 직인 둘레 글자와 가운데 글자(템플릿 가능). |
image | string | 돋움으로 바꿀 로고 이미지. |
x, y, w, h | number | 위치와 크기(mm, 기본 오른쪽 아래 32mm 직인). |
shape | 'circle' | 'rect' | 'none' | 테두리 모양. |
depth, color, opacity, fontSize, pages | — | 돋움 깊이(mm, 기본 0.3), 종이 색, 투명도, 페이지. |
화면과 출력
뷰어 옵션
뷰어는 화면에 들어오는 페이지만 SVG로 그립니다. 단축키: PageUp/PageDown, Ctrl+Home/End, Ctrl+P(인쇄), Ctrl+휠(확대·축소).
| 이름 | 타입 | 설명 |
|---|---|---|
definition | ReportDefinition | 보고서 정의(JSON). |
data | ReportData | { rows: [...] }와 수식에서 쓰는 다른 값. |
fetchData | (params) => data | Promise | 파라미터로 데이터를 불러옵니다(처음과 조회할 때). |
params | object | 파라미터 초깃값(기본값보다 우선). |
lang | 'ko' | 'en' | 도구 모음 언어(기본 ko). |
toolbar | { pages, zoom, print, pdf, xlsx, xlsxData, csv, params } | 보여 줄 버튼(csv는 기본 꺼짐). |
zoom | number | 'width' | 'page' | 처음 배율(기본 폭 맞춤). |
paramsOpen | boolean | 처음부터 조회 조건 패널을 엽니다. |
fileName | string | 저장 파일 이름. |
licenseKey | string | 라이선스 키(또는 setLicenseKey 한 번). |
onOutput | (e) => boolean | void | Promise | 인쇄·저장 직전마다 호출. false를 돌려주면 취소. |
pdfSecurity | { userPassword?, ownerPassword?, permissions? } | PDF 암호와 권한(AES-256). |
author | string | PDF 문서 정보의 작성자. |
뷰어 메서드
| 이름 | 타입 | 설명 |
|---|---|---|
refresh() | Promise<void> | 보고서를 다시 만듭니다. |
setData(data) / setParams(p) / setDefinition(def) | Promise<void> | 데이터·파라미터·정의를 바꾸고 다시 만듭니다. |
goTo(page) / setZoom(z) | void | 페이지 이동과 배율. |
print() | Promise<boolean> | 인쇄(onOutput이 막으면 false). |
report | Report | 아래의 Report 객체(toPdf, toXlsx…). |
destroy() | void | 뷰어를 없앱니다. |
디자이너
createDesigner는 웹 디자이너를 띄웁니다. 도구 상자, 데이터 필드 목록, 밴드, 속성 창, 미리 보기와 JSON 보기, 실행 취소·다시 실행이 들어 있습니다. 필드 목록과 미리 보기에 보일 수 있게 예제 데이터를 넘겨 주세요. 디자이너 써 보기
import { createDesigner } from '@freereportjs/core';
import '@freereportjs/core/styles.css';
const designer = createDesigner(document.getElementById('designer'), {
definition, // leave out to start with a blank report
data: { rows }, // sample data for the field list and the preview
lang: 'ko',
onChange: def => localStorage.setItem('draft', JSON.stringify(def)),
onSave: def => fetch('/api/reports/sales', { method: 'PUT', body: JSON.stringify(def) }),
});
designer.setMode('preview'); // 'design' | 'preview' | 'json'
const json = designer.getDefinition();인쇄·PDF·엑셀·CSV
인쇄는 보고서의 용지 크기 그대로 브라우저 인쇄를 쓰며 줄바꿈도 화면과 같습니다. PDF는 트루타입 글꼴을 부분 내장해 글자를 검색하고 복사할 수 있습니다. 엑셀 "양식 그대로(form)"는 인쇄 모양을 병합 셀과 테두리로 살리고, "데이터(data)"는 한 행에 한 건씩 써서 이어서 작업하기 좋습니다.
import { createReport } from '@freereportjs/core';
const report = createReport(definition, { rows });
await report.ready(); // wait for web fonts, then lay out
await report.print(); // browser print at the paper size
const pdf = await report.toPdf(); // Blob, fonts embedded
const form = await report.toXlsx('form'); // merged cells and borders, like the printout
const data = await report.toXlsx('data'); // one row per record
await report.download('pdf', '매출 보고서'); // or save directly| 이름 | 타입 | 설명 |
|---|---|---|
createReport(def, data, options?) | Report | 뷰어 없이 정의와 데이터를 묶습니다. |
ready() | Promise<LayoutResult> | 웹 글꼴을 기다린 뒤 레이아웃. 브라우저에서 처음 출력 전에 호출합니다. |
layout() | LayoutResult | 페이지, 행 수, 걸린 시간(캐시됨). |
print({ pages? }) | Promise<boolean> | 정확한 용지 크기로 브라우저 인쇄. |
toPdf() | Promise<Blob | null> | 글꼴을 부분 내장한 PDF. |
toXlsx('form' | 'data') | Promise<Blob | null> | "form"은 병합·테두리 유지, "data"는 한 행에 한 건. |
toHwpx() | Promise<Blob | null> | 인쇄한 페이지와 같은 배치의 한/글 문서(.hwpx, KS X 6101). |
toCsv() | Promise<Blob | null> | CSV(엑셀에서 한글이 깨지지 않도록 BOM 포함). |
download('pdf' | 'xlsx' | 'xlsx-data' | 'csv' | 'hwpx', name?) | Promise<boolean> | 파일을 만들어 저장합니다. |
글꼴
나눔고딕과 나눔명조가 들어 있습니다(SIL OFL). 화면과 인쇄에는 웹 글꼴을, PDF에는 트루타입 파일을 쓰며 처음 필요할 때 jsDelivr CDN에서 받습니다. registerFont로 다른 글꼴을 추가할 수 있고, 사내망에서는 파일을 직접 올려 두고 쓰면 됩니다.
import { registerFont } from '@freereportjs/core';
registerFont('Malgun Gothic', {
css: '/fonts/malgun.css', // @font-face rules for screen and print (optional)
normal: '/fonts/malgun.ttf', // TrueType for the PDF (URL or Uint8Array)
bold: '/fonts/malgunbd.ttf',
});
// then: "font": { "family": "Malgun Gothic" } or "style": { "fontFamily": "Malgun Gothic" }서버·보안·운영
서버에서 생성
renderPdf와 renderXlsx는 브라우저 없이(Node, Deno, Bun, 워커) 레이아웃하고 파일을 씁니다. 글자는 PDF에 내장하는 트루타입 글꼴로 재기 때문에 줄바꿈이 같습니다. 일괄 출력, 예약 보고서, 메일 첨부에 쓰세요.
// Node 20+, Deno or Bun — no browser needed
import { writeFile } from 'node:fs/promises';
import { loadFonts, renderPdf, renderXlsx } from '@freereportjs/core';
const fonts = await loadFonts(['Nanum Gothic']); // TTF bytes (or read your own files)
const pdf = await renderPdf(definition, { rows }, { fonts, params: { user: 'batch' } });
await writeFile('sales.pdf', pdf);
await writeFile('sales.xlsx', await renderXlsx(definition, { rows }, { fonts, mode: 'form' }));PDF 보안
pdfSecurity를 주면 PDF를 AES-256으로 암호화합니다(PDF 2.0 표준 보안). 열기 암호(userPassword)를 주면 열 때 암호를 묻고, permissions로 PDF 뷰어에서의 인쇄·복사·수정을 막습니다. 소유자 암호를 주지 않으면 임의의 값을 씁니다.
createViewer(el, {
definition, data,
pdfSecurity: {
userPassword: '1234', // asked when the PDF is opened (omit: opens freely)
permissions: { print: true, copy: false, modify: false },
},
});출력 이력
onOutput은 인쇄와 저장 직전마다 종류, 보고서 이름, 쪽수, 건수, 파라미터를 넘겨 줍니다. 출력 이력(개인정보 안전성 확보조치)을 남기거나, 출력 사유를 묻거나, false를 돌려줘 막을 수 있습니다. 출력 보안 데모
createViewer(el, {
definition, data, params: { user: '홍길동' },
// e = { kind: 'print' | 'pdf' | 'xlsx' | 'xlsx-data' | 'csv', report, pages, rows, params, fileName }
onOutput: async e => {
const res = await fetch('/api/output-log', { method: 'POST', body: JSON.stringify(e) });
return res.ok; // false cancels the print or download
},
});라이선스 키
개인 사용은 키가 필요 없습니다. 회사는 도메인용 키(FR1.…)를 받아 한 번 설정하거나 licenseKey로 넘깁니다. 공개 도메인에서 유효한 키가 없으면 개발자 콘솔에 안내를 한 번 출력할 뿐, 화면은 바뀌지 않고 기능도 꺼지지 않습니다. 로컬과 사내 네트워크 주소에는 키가 필요 없습니다. 가격
import { setLicenseKey } from '@freereportjs/core';
setLicenseKey('FR1.eyJkb21haW5zIjpb...'); // once, before creating viewers
// or: createViewer(el, { definition, data, licenseKey: 'FR1.eyJkb21haW5zIjpb...' })도움말
AI에게 요청하기
ChatGPT, Claude, Gemini, Cursor에게 FreeReport 보고서 정의를 부탁할 때는 먼저 https://freereport.app/llms-full.txt를 읽게 하세요. 모든 옵션, 요소, 함수, 서식이 한 파일에 정리되어 있습니다. "문서에 없는 옵션은 만들지 말라"고 덧붙이면 틀린 정의가 크게 줄어듭니다.