-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcore_utils.js
More file actions
236 lines (205 loc) · 7.36 KB
/
core_utils.js
File metadata and controls
236 lines (205 loc) · 7.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// core_utils.js
import { PDFDocument, rgb } from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 기본 경로 (필요 시 호출부에서 override 가능)
const DEFAULT_TEMPLATE_PATH = path.join(__dirname, "templates", "template2.pdf");
const DEFAULT_FONT_PATH = path.join(__dirname, "fonts", "NotoSansKR-Regular.ttf");
const DEFAULT_FIELDS_PATH = path.join(__dirname, "fields.json");
// 주민등록번호 마스킹
function maskRRN(v) {
if (!v) return "";
const clean = String(v).replace(/[^0-9]/g, "");
if (clean.length >= 7) return clean.slice(0, 6) + "-" + clean.slice(6, 7) + "******";
return v;
}
/**
* PDF 채우기 코어 함수
* @param {Object} params
* @param {Object} params.data - 필드에 채울 데이터
* @param {Object|null} params.fieldsOverride - 필드 좌표 설정 override
* @param {string} [params.templatePath] - 템플릿 경로 override
* @param {string} [params.fontPath] - 폰트 경로 override
* @param {string} [params.fieldsPath] - 필드 설정 파일 경로 override
* @returns {Promise<Buffer>}
*/
export async function fillPdf({
data = {},
fieldsOverride = null,
templatePath = DEFAULT_TEMPLATE_PATH,
fontPath = DEFAULT_FONT_PATH,
fieldsPath = DEFAULT_FIELDS_PATH,
} = {}) {
// 1) 템플릿 & 폰트 로드
const [templateBytes, fontBytes] = await Promise.all([
fs.readFile(templatePath),
fs.readFile(fontPath),
]);
const pdfDoc = await PDFDocument.load(templateBytes);
pdfDoc.registerFontkit(fontkit);
// 폰트 서브셋 비활성화
const font = await pdfDoc.embedFont(fontBytes, { subset: false });
// 2) 좌표 맵 로드
const fieldsJson = fieldsOverride ?? JSON.parse(await fs.readFile(fieldsPath, "utf8"));
// 3) 각 필드 쓰기
for (const [key, cfg] of Object.entries(fieldsJson)) {
const valRaw = data[key];
if (valRaw == null) continue;
const pageIndex = cfg.p ?? 0;
const page = pdfDoc.getPages()[pageIndex];
if (!page) continue;
const size = cfg.size ?? 12;
const color = rgb(0, 0, 0);
// 박스 타입이 지정된 경우 fillBoxes 사용
if (cfg.boxType) {
fillBoxes({
data: valRaw,
type: cfg.boxType,
startX: cfg.x,
startY: cfg.y,
size,
page,
font,
color,
radioOptions: cfg.radioOptions || [] // 라디오 버튼 옵션 추가
});
} else if (cfg.letterSpacing && cfg.letterSpacing > 0) {
// 기존 letterSpacing 로직
let x = cfg.x;
const y = cfg.y;
const text = cfg.mask ? maskRRN(valRaw) : String(valRaw);
for (const ch of text) {
page.drawText(ch, { x, y, size, font, color });
x += cfg.letterSpacing;
}
} else {
// 기존 일반 텍스트 로직
const text = cfg.mask ? maskRRN(valRaw) : String(valRaw);
page.drawText(text, { x: cfg.x, y: cfg.y, size, font, color });
}
}
// 4) 결과 반환
const out = await pdfDoc.save();
return Buffer.from(out);
}
/**
* 박스 형태로 글자를 그리는 함수
* @param {Object} params
* @param {Object} params.data - 필드에 채울 데이터
* @param {string} params.type - 'dash', 'date', 또는 'radio'
* @param {number} params.startX - 첫 번째 박스의 x 좌표
* @param {number} params.startY - 첫 번째 박스의 y 좌표
* @param {number} params.size - 폰트 크기 (기본값: 12)
* @param {Object} params.page - PDF 페이지 객체
* @param {Object} params.font - 폰트 객체
* @param {Object} params.color - 색상 객체 (기본값: 검정색)
* @param {Array} params.radioOptions - 라디오 버튼 옵션들 (x, y 좌표 배열)
*/
export function fillBoxes({
data,
type,
startX,
startY,
size = 12,
page,
font,
color = rgb(0, 0, 0),
radioOptions = []
}) {
if (data == null || !type || !page || !font) {
throw new Error('필수 파라미터가 누락되었습니다.');
}
let currentX = startX;
const y = startY;
if (type === 'dash') {
// Box Dash: 입력 데이터의 대시 위치를 기준으로 처리
const inputStr = String(data);
// 거리 설정
const boxSpacing = 17; // 박스 간 거리
const dashSpacing = 4; // 대시를 지난 후 거리
// 문자열을 한 글자씩 처리
for (let i = 0; i < inputStr.length; i++) {
const char = inputStr[i];
if (char === '-') {
// 대시면 출력하지 않고 간격만 추가
currentX += dashSpacing;
} else {
// 숫자면 출력하고 박스 간격 추가
page.drawText(char, { x: currentX, y, size, font, color });
currentX += boxSpacing;
}
}
} else if (type === 'date') {
// Box Date: 년/월/일/시/분 형태
const dateStr = String(data);
const digits = dateStr.replace(/[^0-9]/g, '').split('');
// 거리 설정
const boxSpacing = 17; // 박스 간 거리
const separatorSpacing = 6; // 년/월/일/시/분 구분 후 거리
let digitIndex = 0;
// 년도 (4자리)
for (let i = 0; i < 4 && digitIndex < digits.length; i++) {
page.drawText(digits[digitIndex], { x: currentX, y, size, font, color });
currentX += boxSpacing;
digitIndex++;
}
// 월 (2자리)
if (digitIndex < digits.length) {
currentX += separatorSpacing;
for (let i = 0; i < 2 && digitIndex < digits.length; i++) {
page.drawText(digits[digitIndex], { x: currentX, y, size, font, color });
currentX += boxSpacing;
digitIndex++;
}
}
// 일 (2자리)
if (digitIndex < digits.length) {
currentX += separatorSpacing;
for (let i = 0; i < 2 && digitIndex < digits.length; i++) {
page.drawText(digits[digitIndex], { x: currentX, y, size, font, color });
currentX += boxSpacing;
digitIndex++;
}
}
// 시 (2자리)
if (digitIndex < digits.length) {
currentX += separatorSpacing;
for (let i = 0; i < 2 && digitIndex < digits.length; i++) {
page.drawText(digits[digitIndex], { x: currentX, y, size, font, color });
currentX += boxSpacing;
digitIndex++;
}
}
// 분 (2자리)
if (digitIndex < digits.length) {
currentX += separatorSpacing;
for (let i = 0; i < 2 && digitIndex < digits.length; i++) {
page.drawText(digits[digitIndex], { x: currentX, y, size, font, color });
currentX += boxSpacing;
digitIndex++;
}
}
} else if (type === 'radio') {
// Radio: 여러 옵션 중 하나만 선택
const selectedIndex = parseInt(data);
if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= radioOptions.length) {
throw new Error(`라디오 버튼 선택 인덱스가 유효하지 않습니다. 0-${radioOptions.length - 1} 범위의 숫자를 입력하세요.`);
}
// 선택된 옵션에만 체크 표시 (X 마크)
const selectedOption = radioOptions[selectedIndex];
const checkMark = '✓'; // 또는 'X' 사용 가능
page.drawText(checkMark, {
x: selectedOption.x,
y: selectedOption.y,
size,
font,
color
});
} else {
throw new Error('지원하지 않는 타입입니다. "dash", "date", 또는 "radio"를 사용하세요.');
}
}