@promr-acorda/core
v0.6.8
Published
Framework-agnostic electronic contract document engine
Maintainers
Readme
@promr-acorda/core
전자계약 문서를 다루는 프레임워크 독립적인 엔진입니다. 불변(immutable) 문서/필드
조작, 좌표 변환, 검증, 공유(source/mirror) 필드 처리, PDF 내보내기 기능을 제공합니다.
모든 조작 함수는 문서를 직접 변경하지 않고 다음 상태의 새 ContractDocument를 반환합니다.
설치
npm install @promr-acorda/core빠른 시작
import {
createDocument,
createField,
setFieldValue,
validateDocument,
getResolvedValues,
exportToPdf,
type ContractDocument,
} from '@promr-acorda/core';
let document: ContractDocument = createDocument({
id: 'contract-001',
title: '근로계약서',
pdfData: pdfBytes,
pageCount: 1,
pages: [{ index: 0, width: 612, height: 792 }],
});
document = createField(document, {
id: 'employeeName',
name: '직원명',
type: 'text',
page: 0,
x: 0.1,
y: 0.2,
width: 0.35,
height: 0.05,
placeholder: '이름 입력',
required: true,
});
document = setFieldValue(document, 'employeeName', '홍길동');
const validation = validateDocument(document);
const values = getResolvedValues(document);
const pdfBytes2 = await exportToPdf(document);ContractDocument 구조
interface ContractDocument {
readonly id: string;
readonly title: string;
readonly pdfData: Uint8Array;
readonly pageImages?: readonly Uint8Array[];
readonly pageCount: number;
readonly pages: readonly PageInfo[];
readonly fields: readonly ContractField[];
readonly fieldValues: FieldValueMap;
readonly sharedValues: SharedValueMap;
readonly createdAt: string;
readonly updatedAt: string;
}pdfData는 PDF 내보내기와 PDF 기반 렌더링에 사용되는 원본 PDF 바이트입니다.pageImages가 있으면(@promr-acorda/react사용 시) 이미지 기반 렌더링을 우선 사용합니다.fieldValues는 각 필드에 직접 입력된 값,sharedValues는 공유 필드 그룹의 값입니다. 둘 다 직접 다루기보다는 아래 함수들을 통해 조작/조회하는 것을 권장합니다.
필드 좌표
필드의 위치와 크기는 픽셀이 아니라 페이지 대비 비율(0~1)로 저장됩니다.
| 필드 | 설명 |
| --- | --- |
| page | 0부터 시작하는 페이지 인덱스 |
| x, y | 페이지 너비/높이 대비 좌측·상단 기준 시작 위치 비율 |
| width, height | 페이지 너비/높이 대비 크기 비율 |
import { toAbsoluteRect, toNormalizedRect, normalizeRect } from '@promr-acorda/core';
const abs = toAbsoluteRect({ page: 0, x: 0.1, y: 0.2, width: 0.35, height: 0.05 }, 612, 792);
const normalized = toNormalizedRect(
{ page: 0, x: 61.2, y: 158.4, width: 214.2, height: 39.6 },
612,
792
);
// 범위를 벗어난 좌표/크기를 유효 범위로 보정
const safeRect = normalizeRect(
{ page: 0, x: -1, y: 0.2, width: 0.3, height: 0.05 },
document.pageCount
);그 외 좌표 유틸리티: clampToPage(value, pageCount), clampCoord(value),
ensureMinSize(size).
필드 타입
공통 속성:
| 필드 | 타입 | 설명 |
| --- | --- | --- |
| id | string | 필드 고유 ID |
| name | string | 필드 이름 |
| type | ContractFieldType | 필드 종류 |
| page | number | 페이지 번호 |
| x, y, width, height | number | 정규화 좌표 |
| label | string | 표시 라벨 |
| required | boolean | 필수 여부 |
| placeholder | string | 입력 안내 문구 |
| textAlign | 'left' \| 'center' \| 'right' | 텍스트 계열 필드의 정렬. 기본값 'left' |
| textSize | number | 텍스트 계열 필드의 글자 크기. 기본값 10. React 뷰어와 PDF export 양쪽에 적용 |
| highlightColor | string | 필드를 강조할 반투명 오버레이 색상(CSS color). React 뷰어의 모든 모드에 적용되며 PDF export에는 반영되지 않음 |
| readonly | boolean | 값만 표시하고 수정은 금지 |
| disabled | boolean | 필드 UI(테두리, placeholder)는 유지하면서 입력/이동/크기조절 상호작용만 차단 |
| hidden | boolean | true면 PDF export에서 제외 |
| defaultValue | - | 값이 없을 때 사용할 기본값 |
| validation | FieldValidation | 검증 규칙 |
| sharedKey | string | 공유 필드 그룹 키 |
| sharedMode | 'source' \| 'mirror' | 공유 필드 역할 |
타입별 값과 추가 속성:
| 타입 | 값 타입 | 추가 속성 |
| --- | --- | --- |
| text | string | maxLength? |
| textarea | string | maxLength?, rows? |
| date | string(yyyy-mm-dd) | dateFormat?(표시/출력용 포맷) |
| checkbox | boolean | 없음 |
| signature | SignatureValue | signatureMode?, signatureActions? |
| email | string | 없음 |
| phone | string | 없음 |
| number | number | min?, max?, step? |
textSize, textAlign은 text, textarea, date, email, phone, number
필드에서 사용할 수 있습니다.
document = createField(document, {
id: 'employeeName',
name: '직원명',
type: 'text',
page: 0,
x: 0.1,
y: 0.2,
width: 0.35,
height: 0.05,
textSize: 14,
textAlign: 'center',
});readonly는 값을 문서 위에 표시하기만 할 때, disabled는 필드 테두리와
placeholder 같은 UI는 유지하되 사용자가 입력하거나 빌더에서 이동/크기조절하지
못하게 할 때 사용합니다.
document = createField(document, {
id: 'lockedDepartment',
name: '부서',
type: 'text',
page: 0,
x: 0.1,
y: 0.32,
width: 0.35,
height: 0.05,
placeholder: '부서명',
disabled: true,
});서명 필드는 signatureMode('all' | 'sign-only' | 'stamp-only')와
signatureActions({ edit?: boolean; remove?: boolean })로 표시 방식과
기존 값 위의 액션 버튼을 제어합니다. 기본값은 액션을 표시하지 않는 동작입니다.
document = createField(document, {
id: 'employeeSignature',
name: '직원 서명',
type: 'signature',
page: 0,
x: 0.1,
y: 0.72,
width: 0.3,
height: 0.1,
signatureMode: 'all',
signatureActions: { edit: true, remove: true },
});문서/필드 조작
| 함수 | 설명 |
| --- | --- |
| createDocument(input) | 빈 문서 생성 |
| createDocumentFromPdfData(input) | 원본 PDF에서 페이지 크기를 읽어 문서 생성 (비동기) |
| getPdfPageInfo(pdfData) | PDF에서 페이지별 크기 정보만 조회 (비동기) |
| createField(document, field) | 필드 추가 |
| updateField(document, fieldId, patch) | 필드 속성 수정 (id, type은 수정 대상 제외) |
| removeField(document, fieldId) | 필드 삭제 (해당 필드 값도 함께 제거) |
| moveField(document, fieldId, position) | 필드 위치(page, x, y) 이동 |
| resizeField(document, fieldId, size) | 필드 크기(width, height) 변경 |
const nextDocument = updateField(document, 'employeeName', {
label: 'Employee Name',
required: true,
});
const moved = moveField(document, 'employeeName', { page: 0, x: 0.2, y: 0.3 });
const resized = resizeField(document, 'employeeName', { width: 0.4, height: 0.06 });필드 정의 조회
getField/getFields는 값이 아니라 필드 정의를 조회합니다.
import { getField, getFields } from '@promr-acorda/core';
const field = getField(document, 'employeeName'); // 없으면 undefined
const fields = getFields(document);
const requiredFields = getFields(document, { required: true });
const visibleFields = getFields(document, { hidden: false });
const disabledFields = getFields(document, { disabled: true });
const pageOneFields = getFields(document, { page: 0 });
const textFields = getFields(document, { type: 'text' });
const sharedMirrors = getFields(document, { sharedKey: 'partyName', sharedMode: 'mirror' });getFields 필터 옵션: page, type, required, hidden, readonly,
disabled, sharedKey, sharedMode.
필드 값 조작 및 조회
| 함수 | 설명 |
| --- | --- |
| setFieldValue(document, fieldId, value) | 필드 값 설정 (source 필드면 sharedValues도 갱신) |
| clearFieldValue(document, fieldId) | 필드 값 제거 |
| getResolvedFieldValue(document, fieldId) | 공유값/defaultValue를 반영한 단일 필드 값 조회 |
| getResolvedValues(document, options?) | 공유값/defaultValue를 반영한 전체 필드 값 조회 |
const values = getResolvedValues(document); // 기본: 값이 있는 필드만 반환
const valuesWithEmpty = getResolvedValues(document, { includeEmpty: true });
if (valuesWithEmpty.employeeName === undefined) {
console.log('employeeName is empty.');
}공유(source/mirror) 필드
document = createField(document, {
id: 'partyNameSource',
name: '회사명 원본',
type: 'text',
page: 0,
x: 0.1,
y: 0.15,
width: 0.35,
height: 0.05,
sharedKey: 'partyName',
sharedMode: 'source',
});
document = createField(document, {
id: 'partyNameMirror',
name: '회사명 복제',
type: 'text',
page: 0,
x: 0.1,
y: 0.25,
width: 0.35,
height: 0.05,
sharedKey: 'partyName',
sharedMode: 'mirror',
});
document = setFieldValue(document, 'partyNameSource', '주식회사 프로엠알');
// partyNameMirror도 같은 값을 갖게 됩니다.저수준 공유 필드 유틸리티도 함께 제공됩니다:
| 함수 | 설명 |
| --- | --- |
| getSourceField(fields, sharedKey) | 공유 키의 source 필드 조회 |
| getMirrorFields(fields, sharedKey) | 공유 키의 mirror 필드 목록 조회 |
| resolveFieldValue(field, fieldValues, sharedValues) | 단일 필드 값 해석 |
| setSharedFieldValue(fields, sharedValues, sharedKey, value) | 공유 값 설정 후 새 SharedValueMap 반환 |
| resolveAllSharedValues(fields, fieldValues, sharedValues) | 공유 필드를 포함한 전체 값 해석 |
검증
import { validateField, validateDocument, validateSharedFieldGroup } from '@promr-acorda/core';
const fieldResult = validateField(field, value);
const result = validateDocument(document);
if (!result.valid) {
console.log(result.errors); // FieldValidationError[]
}
const groupResult = validateSharedFieldGroup(
document.fields,
document.fieldValues,
document.sharedValues,
'partyName'
);날짜 포맷
날짜 필드는 내부적으로 yyyy-mm-dd 형식을 값으로 사용하고, 표시나 PDF
출력에는 필드의 dateFormat을 적용합니다.
import {
formatDateValue,
isIsoDateString,
matchesDateFormat,
dateFormatToRegexPattern,
} from '@promr-acorda/core';
formatDateValue('2026-04-22', 'yyyy.mm.dd'); // '2026.04.22'
isIsoDateString('2026-04-22'); // true
matchesDateFormat('yyyy.mm.dd', '2026.04.22'); // true
dateFormatToRegexPattern('yyyy.mm.dd'); // 검증용 정규식 패턴값 타입 가드
import { isSignatureValue, isPrimitiveValue } from '@promr-acorda/core';
if (isSignatureValue(value)) {
console.log(value.mimeType);
}
isPrimitiveValue(value); // string | number | boolean 여부PDF 내보내기
import { exportToPdf } from '@promr-acorda/core';
const pdfBytes = await exportToPdf(document);document.pdfData를 원본 PDF로 사용해 각 필드의 해석된 값을 페이지 위치에 그립니다.hidden: true인 필드는 출력에서 제외됩니다.- 체크박스는 값이
true일 때만 원본 PDF 위에 체크 표시를 그립니다. - 기본적으로 표준 Helvetica 폰트를 사용하며, 이 폰트는 라틴(WinAnsi) 문자만 지원합니다.
한글/일본어/중국어 등 비 라틴 문자
options.fontBytes로 TrueType/OpenType 폰트 데이터를 전달하면 (내부적으로
@pdf-lib/fontkit을 사용해) Helvetica 대신 유니코드 폰트를 임베드합니다.
const fontBytes = await fetch('/fonts/NotoSansKR.ttf').then((r) => r.arrayBuffer());
const pdfBytes = await exportToPdf(document, {
fontBytes: new Uint8Array(fontBytes),
});에러 처리:
fontBytes없이 Helvetica가 인코딩할 수 없는 값(예: 한글)을 내보내려 하면options.fontBytes가 필요하다는 것을 안내하는 에러를 던집니다.fontBytes로 유효하지 않은 폰트 데이터를 전달하면 폰트를 임베드할 수 없다는 것을 안내하는 에러를 던집니다.- 두 경우 모두 pdf-lib의 원본(저수준) 에러 메시지를 그대로 노출하지 않고, 원인과 해결 방법을 포함한 메시지로 감싸서 던집니다.
라이선스
MIT
