@heripo/research-radar 4.0.2 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +1 -1
- package/package.json +23 -16
- package/dist/index.cjs +0 -3122
- package/dist/index.cjs.map +0 -1
package/dist/index.cjs
DELETED
|
@@ -1,3122 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var anthropic = require('@ai-sdk/anthropic');
|
|
4
|
-
var google = require('@ai-sdk/google');
|
|
5
|
-
var openai = require('@ai-sdk/openai');
|
|
6
|
-
var core = require('@llm-newsletter-kit/core');
|
|
7
|
-
var cheerio = require('cheerio');
|
|
8
|
-
var TurndownService = require('turndown');
|
|
9
|
-
var jsdom = require('jsdom');
|
|
10
|
-
var safeMarkdown2Html = require('safe-markdown2html');
|
|
11
|
-
var DOMPurify = require('dompurify');
|
|
12
|
-
|
|
13
|
-
function _interopNamespaceDefault(e) {
|
|
14
|
-
var n = Object.create(null);
|
|
15
|
-
if (e) {
|
|
16
|
-
Object.keys(e).forEach(function (k) {
|
|
17
|
-
if (k !== 'default') {
|
|
18
|
-
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
-
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
-
enumerable: true,
|
|
21
|
-
get: function () { return e[k]; }
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
n.default = e;
|
|
27
|
-
return Object.freeze(n);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
var cheerio__namespace = /*#__PURE__*/_interopNamespaceDefault(cheerio);
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Formats a date string by replacing dots with dashes.
|
|
34
|
-
* If the string contains a newline (indicating a date range),
|
|
35
|
-
* it formats it as "start ~ end".
|
|
36
|
-
*
|
|
37
|
-
* Examples:
|
|
38
|
-
* - "2024.01.15" → "2024-01-15"
|
|
39
|
-
* - "2024.01.15\n2024.01.20" → "2024-01-15 ~ 2024-01-20"
|
|
40
|
-
*
|
|
41
|
-
* @param str The date string to format
|
|
42
|
-
* @returns The formatted date string with dashes instead of dots
|
|
43
|
-
*/
|
|
44
|
-
function getDate(str) {
|
|
45
|
-
if (str.includes('\n')) {
|
|
46
|
-
const textArr = str.split('\n');
|
|
47
|
-
return `${textArr[0]} ~ ${textArr[1].trim()}`.replaceAll('.', '-');
|
|
48
|
-
}
|
|
49
|
-
return str.replaceAll('.', '-');
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Removes unnecessary parts from a URL such as session IDs and pagination parameters.
|
|
53
|
-
* The following items are removed:
|
|
54
|
-
* - Session IDs like ;jsessionid=xxx
|
|
55
|
-
* - Query parameters: pageIndex, pageUnit, strWhere, searchWrd, sdate, edate
|
|
56
|
-
*
|
|
57
|
-
* Example: https://example.com/path;jsessionid=123456?id=123&pageIndex=1 → https://example.com/path?id=123
|
|
58
|
-
*
|
|
59
|
-
* @param url The URL string to clean
|
|
60
|
-
* @returns The cleaned URL with unnecessary parts removed
|
|
61
|
-
*/
|
|
62
|
-
function cleanUrl(url) {
|
|
63
|
-
if (!url) {
|
|
64
|
-
return url;
|
|
65
|
-
}
|
|
66
|
-
try {
|
|
67
|
-
// Remove all session ID information from semicolon (;) to before question mark (?) or hash (#)
|
|
68
|
-
let cleaned = url.replace(/;[^?#]*/g, '');
|
|
69
|
-
// Create URL object for parsing
|
|
70
|
-
const urlObj = new URL(cleaned);
|
|
71
|
-
// List of parameters to remove
|
|
72
|
-
const paramsToRemove = [
|
|
73
|
-
'pageIndex',
|
|
74
|
-
'pageUnit',
|
|
75
|
-
'strWhere',
|
|
76
|
-
'searchWrd',
|
|
77
|
-
'sdate',
|
|
78
|
-
'edate',
|
|
79
|
-
];
|
|
80
|
-
// Remove each unnecessary parameter
|
|
81
|
-
paramsToRemove.forEach((param) => {
|
|
82
|
-
urlObj.searchParams.delete(param);
|
|
83
|
-
});
|
|
84
|
-
// Return the cleaned URL string
|
|
85
|
-
cleaned = urlObj.toString();
|
|
86
|
-
return cleaned;
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return url; // Return original URL if an error occurs
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const parseBuyeoMuseumList = (html, listId) => {
|
|
94
|
-
const $ = cheerio__namespace.load(html);
|
|
95
|
-
const posts = [];
|
|
96
|
-
const baseUrl = 'https://buyeo.museum.go.kr';
|
|
97
|
-
$('table.tableType1 tbody tr').each((index, element) => {
|
|
98
|
-
const columns = $(element).find('td');
|
|
99
|
-
if (columns.length === 0) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const titleElement = columns.eq(1).find('a');
|
|
103
|
-
const uniqId = getUniqIdFromBuyeoMuseum(columns.eq(1));
|
|
104
|
-
const detailUrl = `${baseUrl}/bbs/view.do?pstSn=${uniqId}&key=${listId}`;
|
|
105
|
-
const title = titleElement
|
|
106
|
-
.clone() // 원본을 수정하지 않도록 복사
|
|
107
|
-
.find('span, i') // span과 i 태그 선택
|
|
108
|
-
.remove() // 제거
|
|
109
|
-
.end() // 원래 요소로 돌아감
|
|
110
|
-
.text() // 텍스트 추출
|
|
111
|
-
.trim() ||
|
|
112
|
-
titleElement.attr('title')?.trim() ||
|
|
113
|
-
'';
|
|
114
|
-
const date = getDate(columns.eq(4).text().trim());
|
|
115
|
-
posts.push({
|
|
116
|
-
uniqId,
|
|
117
|
-
title,
|
|
118
|
-
date,
|
|
119
|
-
detailUrl: cleanUrl(detailUrl),
|
|
120
|
-
dateType: core.DateType.REGISTERED,
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
return posts;
|
|
124
|
-
};
|
|
125
|
-
const parseBuyeoMuseumDetail = (html) => {
|
|
126
|
-
const $ = cheerio__namespace.load(html);
|
|
127
|
-
const content = $('div.cont');
|
|
128
|
-
return {
|
|
129
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
130
|
-
hasAttachedFile: $('div.attachmentWrap ul.noList').length === 0,
|
|
131
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
132
|
-
};
|
|
133
|
-
};
|
|
134
|
-
function getUniqIdFromBuyeoMuseum(element) {
|
|
135
|
-
return (element.attr('onclick') ?? '').match(/goView\('(.*)'\)/)?.[1] ?? '';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const parseExcavationReportList = (html) => {
|
|
139
|
-
const $ = cheerio__namespace.load(html);
|
|
140
|
-
const posts = [];
|
|
141
|
-
const baseUrl = 'https://www.e-minwon.go.kr';
|
|
142
|
-
$('table.td_left tbody tr.list_tr').each((index, element) => {
|
|
143
|
-
const columns = $(element).find('td');
|
|
144
|
-
if (columns.length === 0) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
const titleElement = columns.eq(2).find('a');
|
|
148
|
-
const relativeHref = titleElement.attr('href');
|
|
149
|
-
if (!relativeHref) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const uniqId = getUniqIdFromExcavationItem(titleElement);
|
|
153
|
-
const title = titleElement.attr('title')?.trim() ?? titleElement.text().trim() ?? '';
|
|
154
|
-
const date = getDate(columns.eq(6).text().trim());
|
|
155
|
-
const detailUrl = `${baseUrl}/ge/ee/getEcexmRptp.do?ecexmRcno=${uniqId}`;
|
|
156
|
-
posts.push({
|
|
157
|
-
uniqId,
|
|
158
|
-
title,
|
|
159
|
-
date,
|
|
160
|
-
detailUrl,
|
|
161
|
-
dateType: core.DateType.REGISTERED,
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
return posts;
|
|
165
|
-
};
|
|
166
|
-
const parseExcavationSiteList = (html) => {
|
|
167
|
-
const $ = cheerio__namespace.load(html);
|
|
168
|
-
const posts = [];
|
|
169
|
-
const baseUrl = 'https://www.e-minwon.go.kr';
|
|
170
|
-
$('table.td_left tbody tr.list_tr').each((index, element) => {
|
|
171
|
-
const columns = $(element).find('td');
|
|
172
|
-
if (columns.length === 0) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const titleElement = columns.eq(1).find('a');
|
|
176
|
-
const relativeHref = titleElement.attr('href');
|
|
177
|
-
if (!relativeHref) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const uniqId = getUniqIdFromExcavationItem(titleElement);
|
|
181
|
-
const title = titleElement.attr('title')?.trim() ?? titleElement.text().trim() ?? '';
|
|
182
|
-
const date = getDate(columns.eq(4).text().trim());
|
|
183
|
-
const detailUrl = `${baseUrl}/ge/ee/getLinkGrndsRls.do?grndsRlsSeqc=${uniqId}`;
|
|
184
|
-
posts.push({
|
|
185
|
-
uniqId,
|
|
186
|
-
title,
|
|
187
|
-
date,
|
|
188
|
-
detailUrl,
|
|
189
|
-
dateType: core.DateType.DURATION,
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
return posts;
|
|
193
|
-
};
|
|
194
|
-
const parseExcavationReportDetail = (html) => {
|
|
195
|
-
const $ = cheerio__namespace.load(html);
|
|
196
|
-
const content = $('table.td_left').parent();
|
|
197
|
-
const trList = content.find('table tbody tr');
|
|
198
|
-
trList.each((index, element) => {
|
|
199
|
-
if (trList.length === index + 1) {
|
|
200
|
-
$(element).remove();
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
return {
|
|
204
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
205
|
-
hasAttachedFile: true,
|
|
206
|
-
hasAttachedImage: false,
|
|
207
|
-
};
|
|
208
|
-
};
|
|
209
|
-
const parseExcavationSiteDetail = (html) => {
|
|
210
|
-
const $ = cheerio__namespace.load(html);
|
|
211
|
-
const content = $('div.board_view').parent();
|
|
212
|
-
const dlList = content.find('div dl');
|
|
213
|
-
dlList.each((index, element) => {
|
|
214
|
-
if (dlList.length === index + 1) {
|
|
215
|
-
$(element).remove();
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
return {
|
|
219
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
220
|
-
hasAttachedFile: true,
|
|
221
|
-
hasAttachedImage: false,
|
|
222
|
-
};
|
|
223
|
-
};
|
|
224
|
-
function getUniqIdFromExcavationItem(element) {
|
|
225
|
-
return ((element.attr('onclick') ?? '').match(/dataSelected\('(.*)'\)/)?.[1] ?? '');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const parseGogungList = (html) => {
|
|
229
|
-
const $ = cheerio__namespace.load(html);
|
|
230
|
-
const posts = [];
|
|
231
|
-
const baseUrl = 'https://www.gogung.go.kr';
|
|
232
|
-
$('table.board-list tbody tr').each((index, element) => {
|
|
233
|
-
const columns = $(element).find('td');
|
|
234
|
-
if (columns.length === 0) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const titleElement = columns.eq(1).find('a');
|
|
238
|
-
const relativeHref = titleElement.attr('href');
|
|
239
|
-
if (!relativeHref) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const fullUrl = new URL(`/gogung/bbs/BMSR00022/${relativeHref}`, baseUrl);
|
|
243
|
-
const detailUrl = fullUrl.href;
|
|
244
|
-
const uniqId = fullUrl.searchParams.get('boardId') ?? undefined;
|
|
245
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
246
|
-
const date = getDate(columns.eq(3).text().trim());
|
|
247
|
-
posts.push({
|
|
248
|
-
uniqId,
|
|
249
|
-
title,
|
|
250
|
-
date,
|
|
251
|
-
detailUrl: cleanUrl(detailUrl),
|
|
252
|
-
dateType: core.DateType.REGISTERED,
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
return posts;
|
|
256
|
-
};
|
|
257
|
-
const parseGogungDetail = (html) => {
|
|
258
|
-
const $ = cheerio__namespace.load(html);
|
|
259
|
-
const content = $('div.content-wrap');
|
|
260
|
-
return {
|
|
261
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
262
|
-
hasAttachedFile: $('div.file-list').length > 0,
|
|
263
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
264
|
-
};
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const parseHeritageAgencyList = (html) => {
|
|
268
|
-
const $ = cheerio__namespace.load(html);
|
|
269
|
-
const posts = [];
|
|
270
|
-
const baseUrl = 'https://www.kh.or.kr';
|
|
271
|
-
$('table.board_list tbody tr').each((index, element) => {
|
|
272
|
-
const columns = $(element).find('td');
|
|
273
|
-
if (columns.length === 0) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const titleElement = columns.eq(1).find('a');
|
|
277
|
-
const relativeHref = titleElement.attr('href');
|
|
278
|
-
if (!relativeHref) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
const fullUrl = new URL(relativeHref, baseUrl);
|
|
282
|
-
const detailUrl = fullUrl.href;
|
|
283
|
-
const uniqId = fullUrl.searchParams.get('bbIdx') ?? undefined;
|
|
284
|
-
const title = titleElement.attr('title')?.trim() ?? titleElement.text()?.trim() ?? '';
|
|
285
|
-
const date = getDate(columns.eq(4).text().trim());
|
|
286
|
-
if (!$(element).hasClass('top')) {
|
|
287
|
-
posts.push({
|
|
288
|
-
uniqId,
|
|
289
|
-
title,
|
|
290
|
-
date,
|
|
291
|
-
detailUrl: cleanUrl(detailUrl),
|
|
292
|
-
dateType: core.DateType.REGISTERED,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
return posts;
|
|
297
|
-
};
|
|
298
|
-
const parseHeritageAgencyDetail = (html) => {
|
|
299
|
-
const $ = cheerio__namespace.load(html);
|
|
300
|
-
const content = $('div.view_con');
|
|
301
|
-
return {
|
|
302
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
303
|
-
hasAttachedFile: $('div.tbl_file p').length > 0,
|
|
304
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
305
|
-
};
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const parseHsasList = (html) => {
|
|
309
|
-
const $ = cheerio__namespace.load(html);
|
|
310
|
-
const posts = [];
|
|
311
|
-
const baseUrl = 'http://www.hsas.or.kr';
|
|
312
|
-
$('table#bbs_list tbody tr').each((index, element) => {
|
|
313
|
-
const columns = $(element).find('td');
|
|
314
|
-
if (columns.length === 0) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
const titleElement = columns.eq(1).find('a');
|
|
318
|
-
const relativeHref = titleElement.attr('href');
|
|
319
|
-
if (!relativeHref) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
const fullUrl = new URL(relativeHref.replace('./?ref', '/flow/?ref'), baseUrl);
|
|
323
|
-
const detailUrl = fullUrl.href;
|
|
324
|
-
const uniqId = fullUrl.searchParams.get('eb_idx') ?? undefined;
|
|
325
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
326
|
-
const date = getDate(`20${columns.eq(3).text().trim()}`);
|
|
327
|
-
posts.push({
|
|
328
|
-
uniqId,
|
|
329
|
-
title,
|
|
330
|
-
date,
|
|
331
|
-
detailUrl: cleanUrl(detailUrl),
|
|
332
|
-
dateType: core.DateType.REGISTERED,
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
return posts;
|
|
336
|
-
};
|
|
337
|
-
const parseHsasDetail = (html) => {
|
|
338
|
-
const $ = cheerio__namespace.load(html);
|
|
339
|
-
const content = $('div#view_content');
|
|
340
|
-
return {
|
|
341
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
342
|
-
hasAttachedFile: false,
|
|
343
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
344
|
-
};
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
const parseJbgogoList = (html) => {
|
|
348
|
-
const $ = cheerio__namespace.load(html);
|
|
349
|
-
const posts = [];
|
|
350
|
-
const baseUrl = 'https://www.jbgogo.or.kr';
|
|
351
|
-
$('table.table_list tbody tr').each((index, element) => {
|
|
352
|
-
const columns = $(element).find('td');
|
|
353
|
-
if (columns.length === 0) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
const titleElement = columns.eq(1).find('div a');
|
|
357
|
-
const relativeHref = titleElement.attr('href');
|
|
358
|
-
if (!relativeHref) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
const fullUrl = new URL(relativeHref, baseUrl);
|
|
362
|
-
const detailUrl = fullUrl.href;
|
|
363
|
-
const uniqId = fullUrl.pathname.split('/').pop() ?? undefined;
|
|
364
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
365
|
-
const date = getDate(columns.eq(3).text().trim());
|
|
366
|
-
posts.push({
|
|
367
|
-
uniqId,
|
|
368
|
-
title,
|
|
369
|
-
date,
|
|
370
|
-
detailUrl: cleanUrl(detailUrl),
|
|
371
|
-
dateType: core.DateType.REGISTERED,
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
return posts;
|
|
375
|
-
};
|
|
376
|
-
const parseJbgogoDetail = (html) => {
|
|
377
|
-
const $ = cheerio__namespace.load(html);
|
|
378
|
-
const content = $('div.upload_contents');
|
|
379
|
-
content.find('div.snsbox').remove();
|
|
380
|
-
return {
|
|
381
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
382
|
-
hasAttachedFile: $('div.item_box a.btn_file').length > 0,
|
|
383
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
384
|
-
};
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const parseJeonjuMuseumList = (html) => {
|
|
388
|
-
const $ = cheerio__namespace.load(html);
|
|
389
|
-
const posts = [];
|
|
390
|
-
const baseUrl = 'https://jeonju.museum.go.kr';
|
|
391
|
-
$('table.tstyle_list tbody tr').each((index, element) => {
|
|
392
|
-
const columns = $(element).find('td');
|
|
393
|
-
if (columns.length === 0) {
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const titleElement = columns.eq(1).find('a');
|
|
397
|
-
const relativeHref = titleElement.attr('href');
|
|
398
|
-
if (!relativeHref) {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const fullUrl = new URL(relativeHref, baseUrl);
|
|
402
|
-
const detailUrl = fullUrl.href;
|
|
403
|
-
const uniqId = fullUrl.searchParams.get('list_no') ?? undefined;
|
|
404
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
405
|
-
const date = getDate(columns.eq(3).text().trim());
|
|
406
|
-
posts.push({
|
|
407
|
-
uniqId,
|
|
408
|
-
title,
|
|
409
|
-
date,
|
|
410
|
-
detailUrl: cleanUrl(detailUrl),
|
|
411
|
-
dateType: core.DateType.REGISTERED,
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
return posts;
|
|
415
|
-
};
|
|
416
|
-
const parseJeonjuMuseumRecruitList = (html) => {
|
|
417
|
-
const $ = cheerio__namespace.load(html);
|
|
418
|
-
const posts = [];
|
|
419
|
-
const baseUrl = 'https://jeonju.museum.go.kr';
|
|
420
|
-
$('table.tstyle_list tbody tr').each((index, element) => {
|
|
421
|
-
const columns = $(element).find('td');
|
|
422
|
-
if (columns.length === 0) {
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
const titleElement = columns.eq(1).find('a');
|
|
426
|
-
const relativeHref = titleElement.attr('href');
|
|
427
|
-
if (!relativeHref) {
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
const fullUrl = new URL(relativeHref, baseUrl);
|
|
431
|
-
const detailUrl = fullUrl.href;
|
|
432
|
-
const uniqId = fullUrl.searchParams.get('list_no') ?? undefined;
|
|
433
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
434
|
-
const date = getDate(columns.eq(2).text().trim());
|
|
435
|
-
posts.push({
|
|
436
|
-
uniqId,
|
|
437
|
-
title,
|
|
438
|
-
date,
|
|
439
|
-
detailUrl: cleanUrl(detailUrl),
|
|
440
|
-
dateType: core.DateType.DURATION,
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
return posts;
|
|
444
|
-
};
|
|
445
|
-
const parseJeonjuMuseumDetail = (html) => {
|
|
446
|
-
const $ = cheerio__namespace.load(html);
|
|
447
|
-
const content = $('div.contents');
|
|
448
|
-
return {
|
|
449
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
450
|
-
hasAttachedFile: $('div.file ul.list').length > 0,
|
|
451
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
452
|
-
};
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
const parseJinjuMuseumList = (html) => {
|
|
456
|
-
const $ = cheerio__namespace.load(html);
|
|
457
|
-
const posts = [];
|
|
458
|
-
const baseUrl = 'https://jinju.museum.go.kr';
|
|
459
|
-
$('table tbody tr').each((index, element) => {
|
|
460
|
-
const columns = $(element).find('td');
|
|
461
|
-
if (columns.length === 0) {
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
const titleElement = columns.eq(1).find('a');
|
|
465
|
-
const relativeHref = titleElement.attr('href');
|
|
466
|
-
if (!relativeHref) {
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
const fullUrl = new URL(`/kor/html/sub06/0601.html${relativeHref}`, baseUrl);
|
|
470
|
-
const detailUrl = fullUrl.href;
|
|
471
|
-
const uniqId = fullUrl.searchParams.get('no') ?? undefined;
|
|
472
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
473
|
-
const date = getDate(columns.eq(4).text().trim());
|
|
474
|
-
posts.push({
|
|
475
|
-
uniqId,
|
|
476
|
-
title,
|
|
477
|
-
date,
|
|
478
|
-
detailUrl: cleanUrl(detailUrl),
|
|
479
|
-
dateType: core.DateType.REGISTERED,
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
return posts;
|
|
483
|
-
};
|
|
484
|
-
const parseJinjuMuseumDetail = (html) => {
|
|
485
|
-
const $ = cheerio__namespace.load(html);
|
|
486
|
-
const content = $('div.bbs--view--content');
|
|
487
|
-
return {
|
|
488
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
489
|
-
hasAttachedFile: $('div.bbs--view--file').length > 0,
|
|
490
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
491
|
-
};
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
const parseKaahList = (html) => {
|
|
495
|
-
const $ = cheerio__namespace.load(html);
|
|
496
|
-
const posts = [];
|
|
497
|
-
$('table.board-table-list tbody tr').each((index, element) => {
|
|
498
|
-
const columns = $(element).find('td');
|
|
499
|
-
if (columns.length === 0) {
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
const titleElement = columns.eq(1).find('a');
|
|
503
|
-
const relativeHref = titleElement.attr('href');
|
|
504
|
-
if (!relativeHref) {
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
const fullUrl = new URL(relativeHref);
|
|
508
|
-
const detailUrl = fullUrl.href;
|
|
509
|
-
const uniqId = fullUrl.pathname.split('/').pop() ?? undefined;
|
|
510
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
511
|
-
const date = getDate(columns
|
|
512
|
-
.eq(columns.length - 2)
|
|
513
|
-
.text()
|
|
514
|
-
.trim());
|
|
515
|
-
posts.push({
|
|
516
|
-
uniqId,
|
|
517
|
-
title,
|
|
518
|
-
date,
|
|
519
|
-
detailUrl: cleanUrl(detailUrl),
|
|
520
|
-
dateType: core.DateType.REGISTERED,
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
return posts;
|
|
524
|
-
};
|
|
525
|
-
const parseKaahPlaceList = (html) => {
|
|
526
|
-
const $ = cheerio__namespace.load(html);
|
|
527
|
-
const posts = [];
|
|
528
|
-
$('div.page-con-box div.data-list').each((index, element) => {
|
|
529
|
-
const titleEl = $(element).find('div.con');
|
|
530
|
-
const dateEl = $(element).find('div.title');
|
|
531
|
-
const relativeHref = titleEl.find('div a').attr('href');
|
|
532
|
-
if (!relativeHref) {
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
const fullUrl = new URL(relativeHref);
|
|
536
|
-
const detailUrl = fullUrl.href;
|
|
537
|
-
const uniqId = fullUrl.pathname.split('/').pop() ?? undefined;
|
|
538
|
-
const title = titleEl.find('div a').text()?.trim() ?? '';
|
|
539
|
-
const date = getDate(`${dateEl.find('div.year').text().trim()}-${dateEl.find('div.date').text().trim()}`);
|
|
540
|
-
posts.push({
|
|
541
|
-
uniqId,
|
|
542
|
-
title,
|
|
543
|
-
date,
|
|
544
|
-
detailUrl: cleanUrl(detailUrl),
|
|
545
|
-
dateType: core.DateType.REGISTERED,
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
return posts;
|
|
549
|
-
};
|
|
550
|
-
const parseKaahDetail = (html) => {
|
|
551
|
-
const $ = cheerio__namespace.load(html);
|
|
552
|
-
const content = $('div.content');
|
|
553
|
-
return {
|
|
554
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
555
|
-
hasAttachedFile: $('div.board-view-file div.file-list').length > 0,
|
|
556
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
557
|
-
};
|
|
558
|
-
};
|
|
559
|
-
const parseKaahPlaceDetail = (html) => {
|
|
560
|
-
const $ = cheerio__namespace.load(html);
|
|
561
|
-
const content = $('div.board-view-top');
|
|
562
|
-
return {
|
|
563
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
564
|
-
hasAttachedFile: $('div.board-view-file div.file-list').length > 0,
|
|
565
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
566
|
-
};
|
|
567
|
-
};
|
|
568
|
-
|
|
569
|
-
const parseKhsList = (html) => {
|
|
570
|
-
const $ = cheerio__namespace.load(html);
|
|
571
|
-
const posts = [];
|
|
572
|
-
const baseUrl = 'https://www.khs.go.kr';
|
|
573
|
-
$('table.tbl tbody tr').each((index, element) => {
|
|
574
|
-
const columns = $(element).find('td');
|
|
575
|
-
if (columns.length === 0) {
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
const titleElement = columns.eq(1).find('a.b_tit');
|
|
579
|
-
const relativeHref = titleElement.attr('href');
|
|
580
|
-
if (!relativeHref) {
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
const fullUrl = new URL(relativeHref, baseUrl);
|
|
584
|
-
const detailUrl = fullUrl.href;
|
|
585
|
-
const uniqId = fullUrl.searchParams.get('id') ?? undefined;
|
|
586
|
-
const title = titleElement.find('span').first().text()?.trim() ??
|
|
587
|
-
titleElement.text()?.trim() ??
|
|
588
|
-
'';
|
|
589
|
-
const date = getDate(columns.eq(3).text().trim());
|
|
590
|
-
posts.push({
|
|
591
|
-
uniqId,
|
|
592
|
-
title,
|
|
593
|
-
date,
|
|
594
|
-
detailUrl: cleanUrl(detailUrl),
|
|
595
|
-
dateType: core.DateType.REGISTERED,
|
|
596
|
-
});
|
|
597
|
-
});
|
|
598
|
-
return posts;
|
|
599
|
-
};
|
|
600
|
-
const parseKhsGalleryList = (html) => {
|
|
601
|
-
const $ = cheerio__namespace.load(html);
|
|
602
|
-
const posts = [];
|
|
603
|
-
const baseUrl = 'https://www.khs.go.kr';
|
|
604
|
-
$('ul.board-list > li').each((index, element) => {
|
|
605
|
-
const linkElement = $(element).find('a').first();
|
|
606
|
-
const relativeHref = linkElement.attr('href');
|
|
607
|
-
if (!relativeHref) {
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
const fullUrl = new URL(relativeHref, baseUrl);
|
|
611
|
-
const detailUrl = fullUrl.href;
|
|
612
|
-
const uniqId = fullUrl.searchParams.get('nttId') ?? undefined;
|
|
613
|
-
const imgElement = $(element).find('img');
|
|
614
|
-
const titleElement = $(element).find('strong');
|
|
615
|
-
const dateElement = $(element).find('dl dd');
|
|
616
|
-
const title = imgElement.attr('alt')?.replace(/\s*이미지$/, '') ||
|
|
617
|
-
titleElement.text()?.trim() ||
|
|
618
|
-
'';
|
|
619
|
-
const date = getDate(dateElement.first().text().trim());
|
|
620
|
-
posts.push({
|
|
621
|
-
uniqId,
|
|
622
|
-
title,
|
|
623
|
-
date,
|
|
624
|
-
detailUrl: cleanUrl(detailUrl),
|
|
625
|
-
dateType: core.DateType.REGISTERED,
|
|
626
|
-
});
|
|
627
|
-
});
|
|
628
|
-
return posts;
|
|
629
|
-
};
|
|
630
|
-
const parseKhsLawList = (html) => {
|
|
631
|
-
const $ = cheerio__namespace.load(html);
|
|
632
|
-
const posts = [];
|
|
633
|
-
const baseUrl = 'https://www.khs.go.kr';
|
|
634
|
-
$('table.tbl tbody tr').each((index, element) => {
|
|
635
|
-
const columns = $(element).find('td');
|
|
636
|
-
if (columns.length === 0) {
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
const titleElement = columns.eq(1).find('a.b_tit');
|
|
640
|
-
const relativeHref = titleElement.attr('href');
|
|
641
|
-
if (!relativeHref) {
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
const fullUrl = new URL(relativeHref, baseUrl);
|
|
645
|
-
const detailUrl = fullUrl.href;
|
|
646
|
-
const uniqId = fullUrl.searchParams.get('id') ?? undefined;
|
|
647
|
-
const title = titleElement.find('span').first().text()?.trim() ??
|
|
648
|
-
titleElement.text().trim() ??
|
|
649
|
-
'';
|
|
650
|
-
const date = getDate(columns.eq(4).text().trim());
|
|
651
|
-
const hasEndDate = date.includes('~');
|
|
652
|
-
posts.push({
|
|
653
|
-
uniqId,
|
|
654
|
-
title,
|
|
655
|
-
date,
|
|
656
|
-
detailUrl: cleanUrl(detailUrl),
|
|
657
|
-
dateType: hasEndDate ? core.DateType.DURATION : core.DateType.REGISTERED,
|
|
658
|
-
});
|
|
659
|
-
});
|
|
660
|
-
return posts;
|
|
661
|
-
};
|
|
662
|
-
const parseKhsTenderList = (html) => {
|
|
663
|
-
const $ = cheerio__namespace.load(html);
|
|
664
|
-
const posts = [];
|
|
665
|
-
const baseUrl = 'https://www.khs.go.kr';
|
|
666
|
-
$('table.tbl tbody tr').each((index, element) => {
|
|
667
|
-
const columns = $(element).find('td');
|
|
668
|
-
if (columns.length === 0)
|
|
669
|
-
return;
|
|
670
|
-
const titleElement = columns.eq(1).find('a.b_tit');
|
|
671
|
-
const relativeHref = titleElement.attr('href');
|
|
672
|
-
if (!relativeHref)
|
|
673
|
-
return;
|
|
674
|
-
const fullUrl = new URL(relativeHref, baseUrl);
|
|
675
|
-
const detailUrl = fullUrl.href;
|
|
676
|
-
const uniqId = fullUrl.searchParams.get('id') ?? undefined;
|
|
677
|
-
const title = titleElement.find('span').first().text()?.trim() ||
|
|
678
|
-
titleElement.text().trim();
|
|
679
|
-
const date = getDate(columns.eq(4).text().trim());
|
|
680
|
-
posts.push({
|
|
681
|
-
uniqId,
|
|
682
|
-
title,
|
|
683
|
-
date,
|
|
684
|
-
detailUrl: cleanUrl(detailUrl),
|
|
685
|
-
dateType: core.DateType.REGISTERED,
|
|
686
|
-
});
|
|
687
|
-
});
|
|
688
|
-
return posts;
|
|
689
|
-
};
|
|
690
|
-
const parseKhsDetail = async (html) => {
|
|
691
|
-
const $ = cheerio__namespace.load(html);
|
|
692
|
-
const content = $('div.board-view-content');
|
|
693
|
-
const fileCount = $('.file-container ul.box-group-area li').length;
|
|
694
|
-
return {
|
|
695
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
696
|
-
hasAttachedFile: fileCount > 0,
|
|
697
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
698
|
-
};
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
const parseKrasList = (html) => {
|
|
702
|
-
const $ = cheerio__namespace.load(html);
|
|
703
|
-
const posts = [];
|
|
704
|
-
const baseUrl = 'https://www.kras.or.kr';
|
|
705
|
-
$('table tbody tr').each((index, element) => {
|
|
706
|
-
const columns = $(element).find('td');
|
|
707
|
-
if (columns.length === 0) {
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
const titleElement = columns.eq(1).find('a');
|
|
711
|
-
const relativeHref = titleElement.attr('href');
|
|
712
|
-
if (!relativeHref) {
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
const fullUrl = new URL(relativeHref, baseUrl);
|
|
716
|
-
const detailUrl = fullUrl.href;
|
|
717
|
-
const uniqId = fullUrl.searchParams.get('uid') ?? undefined;
|
|
718
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
719
|
-
const date = getDate(columns.eq(4).text().trim());
|
|
720
|
-
posts.push({
|
|
721
|
-
uniqId,
|
|
722
|
-
title,
|
|
723
|
-
date,
|
|
724
|
-
detailUrl: cleanUrl(detailUrl),
|
|
725
|
-
dateType: core.DateType.REGISTERED,
|
|
726
|
-
});
|
|
727
|
-
});
|
|
728
|
-
return posts;
|
|
729
|
-
};
|
|
730
|
-
const parseKrasDetail = (html) => {
|
|
731
|
-
const $ = cheerio__namespace.load(html);
|
|
732
|
-
const content = $('#vContent');
|
|
733
|
-
content.find('div.snsbox').remove();
|
|
734
|
-
return {
|
|
735
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
736
|
-
hasAttachedFile: $('div.attach ul li').length > 0,
|
|
737
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
738
|
-
};
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
const parseMuseumList = (html, hrefPrefix) => {
|
|
742
|
-
const $ = cheerio__namespace.load(html);
|
|
743
|
-
const posts = [];
|
|
744
|
-
const baseUrl = 'https://www.museum.go.kr';
|
|
745
|
-
$('div.board-list-tbody ul').each((index, element) => {
|
|
746
|
-
const columns = $(element).find('li');
|
|
747
|
-
if (columns.length === 0) {
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
const titleElement = columns.eq(3).find('a');
|
|
751
|
-
const relativeHref = titleElement.attr('href');
|
|
752
|
-
if (!relativeHref) {
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
const fullUrl = new URL(`${hrefPrefix}${relativeHref}`, baseUrl);
|
|
756
|
-
const detailUrl = fullUrl.href;
|
|
757
|
-
const uniqId = fullUrl.searchParams.get('arcId') ?? undefined;
|
|
758
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
759
|
-
const date = getDate(columns.eq(5).text().trim());
|
|
760
|
-
posts.push({
|
|
761
|
-
uniqId,
|
|
762
|
-
title,
|
|
763
|
-
date,
|
|
764
|
-
detailUrl: cleanUrl(detailUrl),
|
|
765
|
-
dateType: core.DateType.REGISTERED,
|
|
766
|
-
});
|
|
767
|
-
});
|
|
768
|
-
return posts;
|
|
769
|
-
};
|
|
770
|
-
const parseMuseumPressList = (html) => {
|
|
771
|
-
const $ = cheerio__namespace.load(html);
|
|
772
|
-
const posts = [];
|
|
773
|
-
const baseUrl = 'https://www.museum.go.kr';
|
|
774
|
-
$('div.board-list-tbody ul').each((index, element) => {
|
|
775
|
-
const columns = $(element).find('li');
|
|
776
|
-
if (columns.length === 0) {
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
const titleElement = columns.eq(1).find('a');
|
|
780
|
-
const relativeHref = titleElement.attr('href');
|
|
781
|
-
if (!relativeHref) {
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
const fullUrl = new URL(`/MUSEUM/contents/M0701040000.do${relativeHref}`, baseUrl);
|
|
785
|
-
const detailUrl = fullUrl.href;
|
|
786
|
-
const uniqId = fullUrl.searchParams.get('arcId') ?? undefined;
|
|
787
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
788
|
-
const date = getDate(columns.eq(3).text().trim());
|
|
789
|
-
posts.push({
|
|
790
|
-
uniqId,
|
|
791
|
-
title,
|
|
792
|
-
date,
|
|
793
|
-
detailUrl: cleanUrl(detailUrl),
|
|
794
|
-
dateType: core.DateType.REGISTERED,
|
|
795
|
-
});
|
|
796
|
-
});
|
|
797
|
-
return posts;
|
|
798
|
-
};
|
|
799
|
-
const parseMuseumRecruitList = (html) => {
|
|
800
|
-
const $ = cheerio__namespace.load(html);
|
|
801
|
-
const posts = [];
|
|
802
|
-
const baseUrl = 'https://www.museum.go.kr';
|
|
803
|
-
$('div.board-list-tbody ul').each((index, element) => {
|
|
804
|
-
const columns = $(element).find('li');
|
|
805
|
-
if (columns.length === 0) {
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
const titleElement = columns.eq(2).find('a');
|
|
809
|
-
const relativeHref = titleElement.attr('href');
|
|
810
|
-
if (!relativeHref) {
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
const fullUrl = new URL(`/MUSEUM/contents/M0701030000.do${relativeHref}`, baseUrl);
|
|
814
|
-
const detailUrl = fullUrl.href;
|
|
815
|
-
const uniqId = fullUrl.searchParams.get('arcId') ?? undefined;
|
|
816
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
817
|
-
const date = getDate(columns.eq(3).text().trim().replace('~ ', ' ~ '));
|
|
818
|
-
posts.push({
|
|
819
|
-
uniqId,
|
|
820
|
-
title,
|
|
821
|
-
date,
|
|
822
|
-
detailUrl: cleanUrl(detailUrl),
|
|
823
|
-
dateType: core.DateType.DURATION,
|
|
824
|
-
});
|
|
825
|
-
});
|
|
826
|
-
return posts;
|
|
827
|
-
};
|
|
828
|
-
const parseMuseumDetail = (html) => {
|
|
829
|
-
const $ = cheerio__namespace.load(html);
|
|
830
|
-
const content = $('div.viewStyle1');
|
|
831
|
-
return {
|
|
832
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
833
|
-
hasAttachedFile: $('div.flie-down-area ul li').length > 0,
|
|
834
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
835
|
-
};
|
|
836
|
-
};
|
|
837
|
-
|
|
838
|
-
const parseNrichNoticeList = (html) => {
|
|
839
|
-
const $ = cheerio__namespace.load(html);
|
|
840
|
-
const posts = [];
|
|
841
|
-
const baseUrl = 'https://www.nrich.go.kr';
|
|
842
|
-
$('ul.list-body li').each((index, element) => {
|
|
843
|
-
if ($(element).hasClass('bg-notice')) {
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
const titleElement = $(element).find('.col2 a.cont-link');
|
|
847
|
-
const relativeHref = titleElement.attr('href');
|
|
848
|
-
if (!relativeHref) {
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
const fullUrl = new URL(`/kor/${relativeHref}`, baseUrl);
|
|
852
|
-
const detailUrl = fullUrl.href;
|
|
853
|
-
const uniqId = fullUrl.searchParams.get('bbs_idx') ?? undefined;
|
|
854
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
855
|
-
const date = getDate($(element).find('.col5 .cont-txt').text().trim());
|
|
856
|
-
if (posts.length < 10) {
|
|
857
|
-
posts.push({
|
|
858
|
-
uniqId,
|
|
859
|
-
title,
|
|
860
|
-
date,
|
|
861
|
-
detailUrl: cleanUrl(detailUrl),
|
|
862
|
-
dateType: core.DateType.REGISTERED,
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
});
|
|
866
|
-
return posts;
|
|
867
|
-
};
|
|
868
|
-
const parseNrichMajorEventList = (html) => {
|
|
869
|
-
const $ = cheerio__namespace.load(html);
|
|
870
|
-
const posts = [];
|
|
871
|
-
$('ul.list-body li').each((index, element) => {
|
|
872
|
-
const linkElement = $(element).find('a[onclick]').first();
|
|
873
|
-
const uniqId = getUniqIdFromNrichMajorEvent(linkElement);
|
|
874
|
-
const detailUrl = `https://www.nrich.go.kr/kor/majorView.do?menuIdx=286&bbs_idx=${uniqId}`;
|
|
875
|
-
const title = $(element).find('.info-tit strong.tit').text().trim();
|
|
876
|
-
let dateText = '';
|
|
877
|
-
$(element)
|
|
878
|
-
.find('.info-detail .detail-box')
|
|
879
|
-
.each((i, box) => {
|
|
880
|
-
const boxTitle = $(box).find('span.tit').text().trim();
|
|
881
|
-
if (boxTitle === '행사기간') {
|
|
882
|
-
dateText = $(box).find('span.cont').text().trim();
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
const dateSplit = getDate(dateText).split(' ~ ');
|
|
886
|
-
const startDate = dateSplit[0];
|
|
887
|
-
const endDate = dateSplit[1];
|
|
888
|
-
const hasEndDate = startDate !== endDate;
|
|
889
|
-
if (uniqId) {
|
|
890
|
-
posts.push({
|
|
891
|
-
uniqId,
|
|
892
|
-
title,
|
|
893
|
-
date: hasEndDate ? `${startDate} ~ ${endDate}` : startDate,
|
|
894
|
-
detailUrl: cleanUrl(detailUrl),
|
|
895
|
-
dateType: hasEndDate ? core.DateType.DURATION : core.DateType.REGISTERED,
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
});
|
|
899
|
-
return posts;
|
|
900
|
-
};
|
|
901
|
-
const parseNrichJournalList = (html) => {
|
|
902
|
-
const $ = cheerio__namespace.load(html);
|
|
903
|
-
const posts = [];
|
|
904
|
-
const baseUrl = 'https://www.nrich.go.kr';
|
|
905
|
-
$('ul.list-body li').each((index, element) => {
|
|
906
|
-
const titleElement = $(element).find('.col2 a.cont-link');
|
|
907
|
-
const relativeHref = titleElement.attr('href');
|
|
908
|
-
if (!relativeHref) {
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
const fullUrl = new URL(`${relativeHref}`, baseUrl);
|
|
912
|
-
const detailUrl = fullUrl.href;
|
|
913
|
-
const uniqId = fullUrl.searchParams.get('idx') ?? undefined;
|
|
914
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
915
|
-
const date = getDate($(element).find('.col3 .cont-txt').text().trim());
|
|
916
|
-
posts.push({
|
|
917
|
-
uniqId,
|
|
918
|
-
title,
|
|
919
|
-
date,
|
|
920
|
-
detailUrl: cleanUrl(detailUrl),
|
|
921
|
-
dateType: core.DateType.REGISTERED,
|
|
922
|
-
});
|
|
923
|
-
});
|
|
924
|
-
return posts;
|
|
925
|
-
};
|
|
926
|
-
const parseNrichPortalList = (html) => {
|
|
927
|
-
const $ = cheerio__namespace.load(html);
|
|
928
|
-
const posts = [];
|
|
929
|
-
const baseUrl = 'https://portal.nrich.go.kr';
|
|
930
|
-
$('table.tbl02 tbody tr').each((index, element) => {
|
|
931
|
-
const columns = $(element).find('td');
|
|
932
|
-
if (columns.length === 0) {
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
const titleElement = columns.eq(1).find('a');
|
|
936
|
-
const relativeHref = titleElement.attr('href');
|
|
937
|
-
if (!relativeHref) {
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
const fullUrl = new URL(`/kor/${relativeHref}`, baseUrl);
|
|
941
|
-
const detailUrl = fullUrl.href;
|
|
942
|
-
const uniqId = fullUrl.searchParams.get('bbs_idx') ?? undefined;
|
|
943
|
-
const title = titleElement.attr('title')?.trim() ?? titleElement.text()?.trim() ?? '';
|
|
944
|
-
const date = getDate(columns.eq(2).text().trim());
|
|
945
|
-
posts.push({
|
|
946
|
-
uniqId,
|
|
947
|
-
title,
|
|
948
|
-
date,
|
|
949
|
-
detailUrl: cleanUrl(detailUrl),
|
|
950
|
-
dateType: core.DateType.REGISTERED,
|
|
951
|
-
});
|
|
952
|
-
});
|
|
953
|
-
return posts;
|
|
954
|
-
};
|
|
955
|
-
const parseNrichNoticeDetail = (html) => {
|
|
956
|
-
const $ = cheerio__namespace.load(html);
|
|
957
|
-
const content = $('.view-content .info-txt');
|
|
958
|
-
return {
|
|
959
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
960
|
-
hasAttachedFile: $('.board-file').length > 0,
|
|
961
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
962
|
-
};
|
|
963
|
-
};
|
|
964
|
-
const parseNrichMajorEventDetail = (html) => {
|
|
965
|
-
const $ = cheerio__namespace.load(html);
|
|
966
|
-
const content = $('.view-content');
|
|
967
|
-
return {
|
|
968
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
969
|
-
hasAttachedFile: false,
|
|
970
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
971
|
-
};
|
|
972
|
-
};
|
|
973
|
-
const parseNrichJournalDetail = (html) => {
|
|
974
|
-
const $ = cheerio__namespace.load(html);
|
|
975
|
-
$('script, style').remove();
|
|
976
|
-
const articles = [];
|
|
977
|
-
// 리스트의 각 항목을 순회하면서 논문 정보 추출
|
|
978
|
-
$('ul.list-body li').each((index, element) => {
|
|
979
|
-
const number = $(element).find('.col1 .cont-txt').text().trim();
|
|
980
|
-
const titleElement = $(element).find('.col2 a.cont-link');
|
|
981
|
-
const title = titleElement.text().trim();
|
|
982
|
-
const author = $(element).find('.col3 .cont-txt').text().trim();
|
|
983
|
-
if (title && author) {
|
|
984
|
-
articles.push(`${number}. **${title}**\n 저자: ${author}`);
|
|
985
|
-
}
|
|
986
|
-
});
|
|
987
|
-
const content = articles.length > 0 ? `## 논문 목록\n\n${articles.join('\n\n')}` : '';
|
|
988
|
-
return {
|
|
989
|
-
detailContent: content,
|
|
990
|
-
hasAttachedFile: true,
|
|
991
|
-
hasAttachedImage: false,
|
|
992
|
-
};
|
|
993
|
-
};
|
|
994
|
-
const parseNrichPortalDetail = (html) => {
|
|
995
|
-
const $ = cheerio__namespace.load(html);
|
|
996
|
-
const content = $('div.detail_Area2');
|
|
997
|
-
return {
|
|
998
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
999
|
-
hasAttachedFile: false,
|
|
1000
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
1001
|
-
};
|
|
1002
|
-
};
|
|
1003
|
-
function getUniqIdFromNrichMajorEvent(element) {
|
|
1004
|
-
return ((element.attr('onclick') ?? '').match(/fnViewPage\('(.*)'\)/)?.[1] ?? '');
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
const BASE_URL$1 = 'https://www.seamuse.go.kr';
|
|
1008
|
-
/**
|
|
1009
|
-
* Extracts the nttId from seamuse.go.kr detail page HTML.
|
|
1010
|
-
* The page contains a JS init call like: noticeFunc.infoInit(4394)
|
|
1011
|
-
* or og:url meta tag: https://www.seamuse.go.kr/news/notice/info/4394
|
|
1012
|
-
*/
|
|
1013
|
-
const extractNttId = (html) => {
|
|
1014
|
-
// Try og:url meta tag first
|
|
1015
|
-
const ogUrlMatch = html.match(/property="og:url"\s+content="[^"]*\/info\/(\d+)"/);
|
|
1016
|
-
if (ogUrlMatch) {
|
|
1017
|
-
return ogUrlMatch[1];
|
|
1018
|
-
}
|
|
1019
|
-
// Fallback: JS init call pattern like infoInit(4394)
|
|
1020
|
-
const initMatch = html.match(/\.infoInit\((\d+)\)/);
|
|
1021
|
-
if (initMatch) {
|
|
1022
|
-
return initMatch[1];
|
|
1023
|
-
}
|
|
1024
|
-
return '';
|
|
1025
|
-
};
|
|
1026
|
-
/**
|
|
1027
|
-
* Parses the list page from seamuse.go.kr (국립해양유산연구소).
|
|
1028
|
-
* The site uses client-side rendering — list data is fetched from a JSON API.
|
|
1029
|
-
* URL pattern: /news/notice/list/1 → API: POST /news/notice/listData/1
|
|
1030
|
-
*
|
|
1031
|
-
* @param listDataPath - API path segment (e.g., "/news/notice" or "/resources/academiccultural")
|
|
1032
|
-
* @param infoPathPrefix - Detail page path prefix (e.g., "/news/notice/info")
|
|
1033
|
-
*/
|
|
1034
|
-
const parseSeamuseList = async (_html, listDataPath, infoPathPrefix, customFetch, searchCategory) => {
|
|
1035
|
-
const body = searchCategory ? `searchCategory=${searchCategory}` : '';
|
|
1036
|
-
const response = await (customFetch ?? fetch)(`${BASE_URL$1}${listDataPath}/listData/1`, {
|
|
1037
|
-
method: 'POST',
|
|
1038
|
-
headers: {
|
|
1039
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1040
|
-
},
|
|
1041
|
-
body,
|
|
1042
|
-
});
|
|
1043
|
-
const data = await response.json();
|
|
1044
|
-
const posts = [];
|
|
1045
|
-
for (const item of data.resultList) {
|
|
1046
|
-
posts.push({
|
|
1047
|
-
uniqId: String(item.nttId),
|
|
1048
|
-
title: item.nttSj?.trim() ?? '',
|
|
1049
|
-
date: item.frstRegisterPnttm ?? '',
|
|
1050
|
-
detailUrl: `${BASE_URL$1}${infoPathPrefix}/${item.nttId}`,
|
|
1051
|
-
dateType: core.DateType.REGISTERED,
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
return posts;
|
|
1055
|
-
};
|
|
1056
|
-
/**
|
|
1057
|
-
* Parses the detail page from seamuse.go.kr.
|
|
1058
|
-
* The site uses client-side rendering — detail data is fetched from a JSON API.
|
|
1059
|
-
* URL pattern: /news/notice/info/4394 → API: GET /news/notice/infoData/4394?nttId=4394
|
|
1060
|
-
*
|
|
1061
|
-
* @param infoDataPath - API path segment (e.g., "/news/notice")
|
|
1062
|
-
*/
|
|
1063
|
-
const parseSeamuseDetail = async (html, infoDataPath, customFetch) => {
|
|
1064
|
-
const nttId = extractNttId(html);
|
|
1065
|
-
if (!nttId) {
|
|
1066
|
-
return {
|
|
1067
|
-
detailContent: '',
|
|
1068
|
-
hasAttachedFile: false,
|
|
1069
|
-
hasAttachedImage: false,
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
const response = await (customFetch ?? fetch)(`${BASE_URL$1}${infoDataPath}/infoData/${nttId}?nttId=${nttId}`, {
|
|
1073
|
-
method: 'GET',
|
|
1074
|
-
headers: {
|
|
1075
|
-
'Content-Type': 'application/json',
|
|
1076
|
-
},
|
|
1077
|
-
});
|
|
1078
|
-
const data = await response.json();
|
|
1079
|
-
const content = data.result.nttCn ?? '';
|
|
1080
|
-
return {
|
|
1081
|
-
detailContent: new TurndownService().turndown(content),
|
|
1082
|
-
hasAttachedFile: (data.files?.length ?? 0) > 0,
|
|
1083
|
-
hasAttachedImage: content.includes('<img'),
|
|
1084
|
-
};
|
|
1085
|
-
};
|
|
1086
|
-
|
|
1087
|
-
const LIST_API_URL = 'http://www.yngogo.or.kr/module/ntt/unity/selectNttListAjax.ink';
|
|
1088
|
-
const DETAIL_API_URL = 'http://www.yngogo.or.kr/module/ntt/unity/selectNttDetailAjax.ink';
|
|
1089
|
-
const BASE_URL = 'http://www.yngogo.or.kr';
|
|
1090
|
-
// User-Agent list used by real browsers
|
|
1091
|
-
const USER_AGENTS = [
|
|
1092
|
-
// Windows - Chrome, Edge, Firefox
|
|
1093
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
|
1094
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
|
|
1095
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
|
|
1096
|
-
// macOS - Chrome, Safari, Firefox
|
|
1097
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
|
1098
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15',
|
|
1099
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0',
|
|
1100
|
-
// Linux - Chrome, Firefox
|
|
1101
|
-
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
|
1102
|
-
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0',
|
|
1103
|
-
// Additional common combinations
|
|
1104
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
|
1105
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
1106
|
-
];
|
|
1107
|
-
// Pick a random User-Agent
|
|
1108
|
-
const getRandomUserAgent = () => USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
|
|
1109
|
-
/**
|
|
1110
|
-
* Parse list page from 영남고고학회 (Yeongnam Archaeological Society)
|
|
1111
|
-
* Uses internal API since the table is rendered via CSR
|
|
1112
|
-
* @see https://www.yngogo.or.kr
|
|
1113
|
-
*/
|
|
1114
|
-
const parseYngogoList = async (_html, menuSeq, bbsSeq, sitecntntsSeq, customFetch) => {
|
|
1115
|
-
// Fetch from internal API (CSR workaround)
|
|
1116
|
-
const response = await (customFetch ?? fetch)(LIST_API_URL, {
|
|
1117
|
-
method: 'POST',
|
|
1118
|
-
headers: {
|
|
1119
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1120
|
-
'User-Agent': getRandomUserAgent(),
|
|
1121
|
-
},
|
|
1122
|
-
body: new URLSearchParams({
|
|
1123
|
-
siteSeq: '32000001030',
|
|
1124
|
-
bbsSeq,
|
|
1125
|
-
pageIndex: '1',
|
|
1126
|
-
menuSeq,
|
|
1127
|
-
pageMode: 'B',
|
|
1128
|
-
sitecntntsSeq,
|
|
1129
|
-
tabTyCode: 'dataManage',
|
|
1130
|
-
mngrAt: 'N',
|
|
1131
|
-
searchCondition: '',
|
|
1132
|
-
searchKeyword: '',
|
|
1133
|
-
nttSeq: '',
|
|
1134
|
-
}),
|
|
1135
|
-
});
|
|
1136
|
-
const html = await response.text();
|
|
1137
|
-
const $ = cheerio__namespace.load(html);
|
|
1138
|
-
const posts = [];
|
|
1139
|
-
$('.basic-table01 tr').each((_index, element) => {
|
|
1140
|
-
const columns = $(element).find('td');
|
|
1141
|
-
if (columns.length === 0) {
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
const titleElement = columns.eq(1).find('a');
|
|
1145
|
-
const uniqId = getUniqId(titleElement);
|
|
1146
|
-
const detailUrl = `${BASE_URL}/subList/${menuSeq}?pmode=detail&nttSeq=${uniqId}&bbsSeq=${bbsSeq}&sitecntntsSeq=${sitecntntsSeq}`;
|
|
1147
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
1148
|
-
const date = getDate(columns.eq(3).text().trim());
|
|
1149
|
-
posts.push({
|
|
1150
|
-
uniqId,
|
|
1151
|
-
title,
|
|
1152
|
-
date,
|
|
1153
|
-
detailUrl: cleanUrl(detailUrl),
|
|
1154
|
-
dateType: core.DateType.REGISTERED,
|
|
1155
|
-
});
|
|
1156
|
-
});
|
|
1157
|
-
return posts;
|
|
1158
|
-
};
|
|
1159
|
-
/**
|
|
1160
|
-
* Parse detail page from 영남고고학회 (Yeongnam Archaeological Society)
|
|
1161
|
-
* Uses internal API since the detail page is rendered via CSR
|
|
1162
|
-
*/
|
|
1163
|
-
const parseYngogoDetail = async (_html, menuSeq, bbsSeq, nttSeq, sitecntntsSeq, customFetch) => {
|
|
1164
|
-
// Fetch from internal API (CSR workaround)
|
|
1165
|
-
const response = await (customFetch ?? fetch)(DETAIL_API_URL, {
|
|
1166
|
-
method: 'POST',
|
|
1167
|
-
headers: {
|
|
1168
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1169
|
-
'User-Agent': getRandomUserAgent(),
|
|
1170
|
-
},
|
|
1171
|
-
body: new URLSearchParams({
|
|
1172
|
-
siteSeq: '32000001030',
|
|
1173
|
-
bbsSeq,
|
|
1174
|
-
nttSeq,
|
|
1175
|
-
pageIndex: '1',
|
|
1176
|
-
ordrSe: 'D',
|
|
1177
|
-
searchCnd: 'frstRegistPnttm',
|
|
1178
|
-
checkNttSeq: '',
|
|
1179
|
-
menuSeq,
|
|
1180
|
-
mngrAt: 'N',
|
|
1181
|
-
parntsNttSeq: '',
|
|
1182
|
-
secretAt: '',
|
|
1183
|
-
searchAt: '',
|
|
1184
|
-
sitecntntsSeq,
|
|
1185
|
-
cmntUseAt: 'N',
|
|
1186
|
-
atchFilePosblAt: 'Y',
|
|
1187
|
-
atchFilePosblCo: '3',
|
|
1188
|
-
listCount: '10',
|
|
1189
|
-
searchCondition: '1',
|
|
1190
|
-
searchKeyword: '',
|
|
1191
|
-
}),
|
|
1192
|
-
});
|
|
1193
|
-
const html = await response.text();
|
|
1194
|
-
const $ = cheerio__namespace.load(html);
|
|
1195
|
-
const content = $('.conM_txt');
|
|
1196
|
-
return {
|
|
1197
|
-
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
1198
|
-
hasAttachedFile: $('#atchFile_div').length > 0,
|
|
1199
|
-
hasAttachedImage: content.find('img').length > 0,
|
|
1200
|
-
};
|
|
1201
|
-
};
|
|
1202
|
-
function getUniqId(element) {
|
|
1203
|
-
// fnView('1005200642', 'admin', '', '',''); - extract first param
|
|
1204
|
-
return (element.attr('onclick') ?? '').match(/fnView\('([^']*)'/)?.[1] ?? '';
|
|
1205
|
-
}
|
|
1206
|
-
/**
|
|
1207
|
-
* Extract nttSeq from CSR page HTML
|
|
1208
|
-
* The URL query parameter is embedded in the page script
|
|
1209
|
-
*/
|
|
1210
|
-
function extractNttSeq(html) {
|
|
1211
|
-
// Pattern: nttSeq=1005200644 or nttSeq='1005200644'
|
|
1212
|
-
const match = html.match(/nttSeq[=:]['"]?(\d+)/);
|
|
1213
|
-
return match?.[1] ?? '';
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
function createCrawlingTargetGroups(customFetch) {
|
|
1217
|
-
return [
|
|
1218
|
-
{
|
|
1219
|
-
id: 'news',
|
|
1220
|
-
name: 'News',
|
|
1221
|
-
targets: [
|
|
1222
|
-
{
|
|
1223
|
-
id: '국가유산청_공지사항',
|
|
1224
|
-
name: '국가유산청 공지사항',
|
|
1225
|
-
url: 'https://www.khs.go.kr/multiBbz/selectMultiBbzList.do?bbzId=newpublic&mn=NS_01_01',
|
|
1226
|
-
parseList: parseKhsList,
|
|
1227
|
-
parseDetail: parseKhsDetail,
|
|
1228
|
-
},
|
|
1229
|
-
{
|
|
1230
|
-
id: '국가유산청_보도설명',
|
|
1231
|
-
name: '국가유산청 보도/설명',
|
|
1232
|
-
url: 'https://www.khs.go.kr/newsBbz/selectNewsBbzList.do?sectionId=all_sec_1&mn=NS_01_02',
|
|
1233
|
-
parseList: parseKhsList,
|
|
1234
|
-
parseDetail: parseKhsDetail,
|
|
1235
|
-
},
|
|
1236
|
-
{
|
|
1237
|
-
id: '국가유산청_사진뉴스',
|
|
1238
|
-
name: '국가유산청 사진뉴스',
|
|
1239
|
-
url: 'https://www.khs.go.kr/cop/bbs/selectBoardList.do?bbsId=BBSMSTR_1002&mn=NS_01_03',
|
|
1240
|
-
parseList: parseKhsGalleryList,
|
|
1241
|
-
parseDetail: parseKhsDetail,
|
|
1242
|
-
},
|
|
1243
|
-
{
|
|
1244
|
-
id: '국가유산청_입법예고',
|
|
1245
|
-
name: '국가유산청 입법예고',
|
|
1246
|
-
url: 'https://www.khs.go.kr/lawBbz/selectLawBbzList.do?mn=NS_03_01_01',
|
|
1247
|
-
parseList: parseKhsLawList,
|
|
1248
|
-
parseDetail: parseKhsDetail,
|
|
1249
|
-
},
|
|
1250
|
-
// NOTE: Parsing logic is implemented, but too much fragmented data with little value for newsletter
|
|
1251
|
-
// {
|
|
1252
|
-
// id: '국가유산청_발굴조사_현황공개',
|
|
1253
|
-
// name: '국가유산청 발굴조사 현황공개',
|
|
1254
|
-
// url: 'https://www.e-minwon.go.kr/ge/ee/getListEcexmPrmsnAply.do',
|
|
1255
|
-
// parseList: parseExcavationStatusList,
|
|
1256
|
-
// parseDetail: parseExcavationStatusDetail,
|
|
1257
|
-
// },
|
|
1258
|
-
{
|
|
1259
|
-
id: '국가유산청_발굴조사_보고서',
|
|
1260
|
-
name: '국가유산청 발굴조사 보고서',
|
|
1261
|
-
url: 'https://www.e-minwon.go.kr/ge/ee/getListEcexmRptp.do',
|
|
1262
|
-
parseList: parseExcavationReportList,
|
|
1263
|
-
parseDetail: parseExcavationReportDetail,
|
|
1264
|
-
},
|
|
1265
|
-
{
|
|
1266
|
-
id: '국가유산청_발굴조사_현장공개',
|
|
1267
|
-
name: '국가유산청 발굴조사 현장공개',
|
|
1268
|
-
url: 'https://www.e-minwon.go.kr/ge/ee/getListLinkGrndsRls.do',
|
|
1269
|
-
parseList: parseExcavationSiteList,
|
|
1270
|
-
parseDetail: parseExcavationSiteDetail,
|
|
1271
|
-
},
|
|
1272
|
-
{
|
|
1273
|
-
id: '국립문화유산연구원_공지사항',
|
|
1274
|
-
name: '국립문화유산연구원 공지사항',
|
|
1275
|
-
url: 'https://www.nrich.go.kr/kor/boardList.do?menuIdx=282&bbscd=32',
|
|
1276
|
-
parseList: parseNrichNoticeList,
|
|
1277
|
-
parseDetail: parseNrichNoticeDetail,
|
|
1278
|
-
},
|
|
1279
|
-
{
|
|
1280
|
-
id: '국립문화유산연구원_주요행사',
|
|
1281
|
-
name: '국립문화유산연구원 주요행사',
|
|
1282
|
-
url: 'https://www.nrich.go.kr/kor/majorList.do?menuIdx=286',
|
|
1283
|
-
parseList: parseNrichMajorEventList,
|
|
1284
|
-
parseDetail: parseNrichMajorEventDetail,
|
|
1285
|
-
},
|
|
1286
|
-
{
|
|
1287
|
-
id: '국립문화유산연구원_학술지_헤리티지',
|
|
1288
|
-
name: '국립문화유산연구원 헤리티지:역사와 과학 학술지',
|
|
1289
|
-
url: 'https://www.nrich.go.kr/kor/subscriptionDataUsrList.do?menuIdx=1651&gubun=J',
|
|
1290
|
-
parseList: parseNrichJournalList,
|
|
1291
|
-
parseDetail: parseNrichJournalDetail,
|
|
1292
|
-
},
|
|
1293
|
-
{
|
|
1294
|
-
id: '국립문화유산연구원_학술지_보존과학연구',
|
|
1295
|
-
name: '국립문화유산연구원 보존과학연구 학술지',
|
|
1296
|
-
url: 'https://www.nrich.go.kr/kor/subscriptionDataUsrList.do?menuIdx=2065&gubun=K',
|
|
1297
|
-
parseList: parseNrichJournalList,
|
|
1298
|
-
parseDetail: parseNrichJournalDetail,
|
|
1299
|
-
},
|
|
1300
|
-
{
|
|
1301
|
-
id: '국가유산지식이음_공지사항',
|
|
1302
|
-
name: '국가유산 지식이음 공지사항',
|
|
1303
|
-
url: 'https://portal.nrich.go.kr/kor/boardList.do?menuIdx=1058&bbscd=9',
|
|
1304
|
-
parseList: parseNrichPortalList,
|
|
1305
|
-
parseDetail: parseNrichPortalDetail,
|
|
1306
|
-
},
|
|
1307
|
-
{
|
|
1308
|
-
id: '국립해양유산연구소_공지사항',
|
|
1309
|
-
name: '국립해양유산연구소 공지사항',
|
|
1310
|
-
url: 'https://www.seamuse.go.kr/news/notice/list/1',
|
|
1311
|
-
parseList: (html) => parseSeamuseList(html, '/news/notice', '/news/notice/info', customFetch),
|
|
1312
|
-
parseDetail: (html) => parseSeamuseDetail(html, '/news/notice', customFetch),
|
|
1313
|
-
},
|
|
1314
|
-
{
|
|
1315
|
-
id: '국립해양유산연구소_학술지',
|
|
1316
|
-
name: '국립해양유산연구소 학술지 해양유산연구',
|
|
1317
|
-
url: 'https://www.seamuse.go.kr/resources/academiccultural/list/1',
|
|
1318
|
-
parseList: (html) => parseSeamuseList(html, '/resources/academiccultural', '/resources/academiccultural/info', customFetch),
|
|
1319
|
-
parseDetail: (html) => parseSeamuseDetail(html, '/resources/academiccultural', customFetch),
|
|
1320
|
-
},
|
|
1321
|
-
{
|
|
1322
|
-
id: '국립해양유산연구소_학술보고서_수중유산조사',
|
|
1323
|
-
name: '국립해양유산연구소 학술보고서 수중유산 조사',
|
|
1324
|
-
url: 'https://www.seamuse.go.kr/resources/academicreport/list/1',
|
|
1325
|
-
parseList: (html) => parseSeamuseList(html, '/resources/academicreport', '/resources/academicreport/info', customFetch, 'ACARPT001'),
|
|
1326
|
-
parseDetail: (html) => parseSeamuseDetail(html, '/resources/academicreport', customFetch),
|
|
1327
|
-
},
|
|
1328
|
-
{
|
|
1329
|
-
id: '국립해양유산연구소_학술보고서_수중유산보존',
|
|
1330
|
-
name: '국립해양유산연구소 학술보고서 수중유산 보존',
|
|
1331
|
-
url: 'https://www.seamuse.go.kr/resources/academicreport/list/1#tab2',
|
|
1332
|
-
parseList: (html) => parseSeamuseList(html, '/resources/academicreport', '/resources/academicreport/info', customFetch, 'ACARPT002'),
|
|
1333
|
-
parseDetail: (html) => parseSeamuseDetail(html, '/resources/academicreport', customFetch),
|
|
1334
|
-
},
|
|
1335
|
-
{
|
|
1336
|
-
id: '국립해양유산연구소_학술보고서_전통선박',
|
|
1337
|
-
name: '국립해양유산연구소 학술보고서 전통 선박',
|
|
1338
|
-
url: 'https://www.seamuse.go.kr/resources/academicreport/list/1#tab3',
|
|
1339
|
-
parseList: (html) => parseSeamuseList(html, '/resources/academicreport', '/resources/academicreport/info', customFetch, 'ACARPT003'),
|
|
1340
|
-
parseDetail: (html) => parseSeamuseDetail(html, '/resources/academicreport', customFetch),
|
|
1341
|
-
},
|
|
1342
|
-
{
|
|
1343
|
-
id: '국립해양유산연구소_학술보고서_해양문화역사',
|
|
1344
|
-
name: '국립해양유산연구소 학술보고서 해양문화역사',
|
|
1345
|
-
url: 'https://www.seamuse.go.kr/resources/academicreport/list/1#tab4',
|
|
1346
|
-
parseList: (html) => parseSeamuseList(html, '/resources/academicreport', '/resources/academicreport/info', customFetch, 'ACARPT004'),
|
|
1347
|
-
parseDetail: (html) => parseSeamuseDetail(html, '/resources/academicreport', customFetch),
|
|
1348
|
-
},
|
|
1349
|
-
{
|
|
1350
|
-
id: '국립고궁박물관_공지사항',
|
|
1351
|
-
name: '국립고궁박물관 공지사항',
|
|
1352
|
-
url: 'https://www.gogung.go.kr/gogung/bbs/BMSR00022/list.do?gubunCd=B22_001&menuNo=800088',
|
|
1353
|
-
parseList: parseGogungList,
|
|
1354
|
-
parseDetail: parseGogungDetail,
|
|
1355
|
-
},
|
|
1356
|
-
{
|
|
1357
|
-
id: '국가유산진흥원_공지사항',
|
|
1358
|
-
name: '국가유산진흥원 공지사항',
|
|
1359
|
-
url: 'https://www.kh.or.kr/brd/board/644/L/SITES/100/menu/371',
|
|
1360
|
-
parseList: parseHeritageAgencyList,
|
|
1361
|
-
parseDetail: parseHeritageAgencyDetail,
|
|
1362
|
-
},
|
|
1363
|
-
{
|
|
1364
|
-
id: '국가유산진흥원_보도자료',
|
|
1365
|
-
name: '국가유산진흥원 보도자료',
|
|
1366
|
-
url: 'https://www.kh.or.kr/brd/board/715/L/menu/373',
|
|
1367
|
-
parseList: parseHeritageAgencyList,
|
|
1368
|
-
parseDetail: parseHeritageAgencyDetail,
|
|
1369
|
-
},
|
|
1370
|
-
{
|
|
1371
|
-
id: '국가유산진흥원_매장유산국비발굴단_공지사항',
|
|
1372
|
-
name: '국가유산진흥원 매장유산국비발굴단 공지사항',
|
|
1373
|
-
url: 'https://www.kh.or.kr/brd/board/644/L/SITES/201/menu/506',
|
|
1374
|
-
parseList: parseHeritageAgencyList,
|
|
1375
|
-
parseDetail: parseHeritageAgencyDetail,
|
|
1376
|
-
},
|
|
1377
|
-
{
|
|
1378
|
-
id: '국가유산진흥원_매장유산국비발굴단_현장설명회',
|
|
1379
|
-
name: '국가유산진흥원 매장유산국비발굴단 현장설명회',
|
|
1380
|
-
url: 'https://www.kh.or.kr/brd/board/631/L/menu/504',
|
|
1381
|
-
parseList: parseHeritageAgencyList,
|
|
1382
|
-
parseDetail: parseHeritageAgencyDetail,
|
|
1383
|
-
},
|
|
1384
|
-
{
|
|
1385
|
-
id: '한국문화유산협회_공지사항',
|
|
1386
|
-
name: '한국문화유산협회 공지사항',
|
|
1387
|
-
url: 'https://www.kaah.kr/notice',
|
|
1388
|
-
parseList: parseKaahList,
|
|
1389
|
-
parseDetail: parseKaahDetail,
|
|
1390
|
-
},
|
|
1391
|
-
{
|
|
1392
|
-
id: '한국문화유산협회_협회소식',
|
|
1393
|
-
name: '한국문화유산협회 협회소식',
|
|
1394
|
-
url: 'https://www.kaah.kr/news',
|
|
1395
|
-
parseList: parseKaahList,
|
|
1396
|
-
parseDetail: parseKaahDetail,
|
|
1397
|
-
},
|
|
1398
|
-
{
|
|
1399
|
-
id: '한국문화유산협회_보도자료',
|
|
1400
|
-
name: '한국문화유산협회 보도자료',
|
|
1401
|
-
url: 'https://www.kaah.kr/mass',
|
|
1402
|
-
parseList: parseKaahList,
|
|
1403
|
-
parseDetail: parseKaahDetail,
|
|
1404
|
-
},
|
|
1405
|
-
{
|
|
1406
|
-
id: '한국문화유산협회_회원기관소식',
|
|
1407
|
-
name: '한국문화유산협회 회원기관소식',
|
|
1408
|
-
url: 'https://www.kaah.kr/assnews',
|
|
1409
|
-
parseList: parseKaahList,
|
|
1410
|
-
parseDetail: parseKaahDetail,
|
|
1411
|
-
},
|
|
1412
|
-
{
|
|
1413
|
-
id: '한국문화유산협회_유관기관소식',
|
|
1414
|
-
name: '한국문화유산협회 유관기관소식',
|
|
1415
|
-
url: 'https://www.kaah.kr/ralnews',
|
|
1416
|
-
parseList: parseKaahList,
|
|
1417
|
-
parseDetail: parseKaahDetail,
|
|
1418
|
-
},
|
|
1419
|
-
{
|
|
1420
|
-
id: '한국문화유산협회_발굴현장공개',
|
|
1421
|
-
name: '한국문화유산협회 발굴현장공개',
|
|
1422
|
-
url: 'https://www.kaah.kr/placeopen',
|
|
1423
|
-
parseList: parseKaahPlaceList,
|
|
1424
|
-
parseDetail: parseKaahPlaceDetail,
|
|
1425
|
-
},
|
|
1426
|
-
{
|
|
1427
|
-
id: '한국고고학회_공지사항',
|
|
1428
|
-
name: '한국고고학회 공지사항',
|
|
1429
|
-
url: 'https://www.kras.or.kr/?r=kras&m=bbs&bid=notice',
|
|
1430
|
-
parseList: parseKrasList,
|
|
1431
|
-
parseDetail: parseKrasDetail,
|
|
1432
|
-
},
|
|
1433
|
-
{
|
|
1434
|
-
id: '한국고고학회_학술대회및행사',
|
|
1435
|
-
name: '한국고고학회 학술대회 및 행사',
|
|
1436
|
-
url: 'https://www.kras.or.kr/?r=kras&m=bbs&bid=sympo',
|
|
1437
|
-
parseList: parseKrasList,
|
|
1438
|
-
parseDetail: parseKrasDetail,
|
|
1439
|
-
},
|
|
1440
|
-
{
|
|
1441
|
-
id: '한국고고학회_신간안내_단행본',
|
|
1442
|
-
name: '한국고고학회 신간안내 - 단행본',
|
|
1443
|
-
url: 'https://www.kras.or.kr/?c=61/101/105',
|
|
1444
|
-
parseList: parseKrasList,
|
|
1445
|
-
parseDetail: parseKrasDetail,
|
|
1446
|
-
},
|
|
1447
|
-
{
|
|
1448
|
-
id: '한국고고학회_현장소식',
|
|
1449
|
-
name: '한국고고학회 현장소식',
|
|
1450
|
-
url: 'https://www.kras.or.kr/?c=61/73',
|
|
1451
|
-
parseList: parseKrasList,
|
|
1452
|
-
parseDetail: parseKrasDetail,
|
|
1453
|
-
},
|
|
1454
|
-
{
|
|
1455
|
-
id: '중부고고학회_공지사항',
|
|
1456
|
-
name: '중부고고학회 공지사항',
|
|
1457
|
-
url: 'https://www.jbgogo.or.kr/bbs/notice',
|
|
1458
|
-
parseList: parseJbgogoList,
|
|
1459
|
-
parseDetail: parseJbgogoDetail,
|
|
1460
|
-
},
|
|
1461
|
-
{
|
|
1462
|
-
id: '중부고고학회_학계소식',
|
|
1463
|
-
name: '중부고고학회 학계소식',
|
|
1464
|
-
url: 'https://www.jbgogo.or.kr/bbs/news',
|
|
1465
|
-
parseList: parseJbgogoList,
|
|
1466
|
-
parseDetail: parseJbgogoDetail,
|
|
1467
|
-
},
|
|
1468
|
-
{
|
|
1469
|
-
id: '중부고고학회_발굴현장소식',
|
|
1470
|
-
name: '중부고고학회 발굴현장소식',
|
|
1471
|
-
url: 'https://www.jbgogo.or.kr/bbs/spotnews',
|
|
1472
|
-
parseList: parseJbgogoList,
|
|
1473
|
-
parseDetail: parseJbgogoDetail,
|
|
1474
|
-
},
|
|
1475
|
-
{
|
|
1476
|
-
id: '호서고고학회_공지사항',
|
|
1477
|
-
name: '호서고고학회 공지사항',
|
|
1478
|
-
url: 'http://www.hsas.or.kr/flow/?ref=board/board.emt&menu_table=m2_00&bbs_table=notice&menu_idx=010000',
|
|
1479
|
-
parseList: parseHsasList,
|
|
1480
|
-
parseDetail: parseHsasDetail,
|
|
1481
|
-
},
|
|
1482
|
-
{
|
|
1483
|
-
id: '호서고고학회_학회소식',
|
|
1484
|
-
name: '호서고고학회 학회소식',
|
|
1485
|
-
url: 'http://www.hsas.or.kr/flow/?ref=board/board.emt&menu_table=m2_00&bbs_table=m2_01&menu_idx=020000',
|
|
1486
|
-
parseList: parseHsasList,
|
|
1487
|
-
parseDetail: parseHsasDetail,
|
|
1488
|
-
},
|
|
1489
|
-
{
|
|
1490
|
-
id: '영남고고학회_공지사항',
|
|
1491
|
-
name: '영남고고학회 공지사항',
|
|
1492
|
-
url: 'http://www.yngogo.or.kr/subList/32000001120',
|
|
1493
|
-
parseList: (html) => parseYngogoList(html, '32000001120', '32000001157', '32000001711', customFetch),
|
|
1494
|
-
parseDetail: (html) => parseYngogoDetail(html, '32000001120', '32000001157', extractNttSeq(html), '32000001711', customFetch),
|
|
1495
|
-
},
|
|
1496
|
-
{
|
|
1497
|
-
id: '영남고고학회_학계소식',
|
|
1498
|
-
name: '영남고고학회 학계소식',
|
|
1499
|
-
url: 'http://www.yngogo.or.kr/subList/32000001133',
|
|
1500
|
-
parseList: (html) => parseYngogoList(html, '32000001133', '32000001161', '32000001715', customFetch),
|
|
1501
|
-
parseDetail: (html) => parseYngogoDetail(html, '32000001133', '32000001161', extractNttSeq(html), '32000001715', customFetch),
|
|
1502
|
-
},
|
|
1503
|
-
{
|
|
1504
|
-
id: '영남고고학회_현장소식',
|
|
1505
|
-
name: '영남고고학회 현장소식',
|
|
1506
|
-
url: 'http://www.yngogo.or.kr/subList/32000001135',
|
|
1507
|
-
parseList: (html) => parseYngogoList(html, '32000001135', '32000001163', '32000001717', customFetch),
|
|
1508
|
-
parseDetail: (html) => parseYngogoDetail(html, '32000001135', '32000001163', extractNttSeq(html), '32000001717', customFetch),
|
|
1509
|
-
},
|
|
1510
|
-
{
|
|
1511
|
-
id: '국립중앙박물관_알림',
|
|
1512
|
-
name: '국립중앙박물관 알림',
|
|
1513
|
-
url: 'https://www.museum.go.kr/MUSEUM/contents/M0701010000.do?catCustomType=united&catId=128',
|
|
1514
|
-
parseList: (html) => parseMuseumList(html, '/MUSEUM/contents/M0701010000.do'),
|
|
1515
|
-
parseDetail: parseMuseumDetail,
|
|
1516
|
-
},
|
|
1517
|
-
{
|
|
1518
|
-
id: '국립중앙박물관_고시공고',
|
|
1519
|
-
name: '국립중앙박물관 고시/공고',
|
|
1520
|
-
url: 'https://www.museum.go.kr/MUSEUM/contents/M0701020000.do',
|
|
1521
|
-
parseList: (html) => parseMuseumList(html, '/MUSEUM/contents/M0701020000.do'),
|
|
1522
|
-
parseDetail: parseMuseumDetail,
|
|
1523
|
-
},
|
|
1524
|
-
{
|
|
1525
|
-
id: '국립중앙박물관_보도자료',
|
|
1526
|
-
name: '국립중앙박물관 보도 자료',
|
|
1527
|
-
url: 'https://www.museum.go.kr/MUSEUM/contents/M0701040000.do?catCustomType=post&catId=93',
|
|
1528
|
-
parseList: parseMuseumPressList,
|
|
1529
|
-
parseDetail: parseMuseumDetail,
|
|
1530
|
-
},
|
|
1531
|
-
{
|
|
1532
|
-
id: '국립전주박물관_새소식',
|
|
1533
|
-
name: '국립전주박물관 새소식',
|
|
1534
|
-
url: 'https://jeonju.museum.go.kr/board.es?mid=a10105010000&bid=0001',
|
|
1535
|
-
parseList: parseJeonjuMuseumList,
|
|
1536
|
-
parseDetail: parseJeonjuMuseumDetail,
|
|
1537
|
-
},
|
|
1538
|
-
{
|
|
1539
|
-
id: '국립전주박물관_보도자료',
|
|
1540
|
-
name: '국립전주박물관 보도자료',
|
|
1541
|
-
url: 'https://jeonju.museum.go.kr/board.es?mid=a10105050000&bid=0004',
|
|
1542
|
-
parseList: parseJeonjuMuseumList,
|
|
1543
|
-
parseDetail: parseJeonjuMuseumDetail,
|
|
1544
|
-
},
|
|
1545
|
-
{
|
|
1546
|
-
id: '국립부여박물관_공지사항',
|
|
1547
|
-
name: '국립부여박물관 공지사항',
|
|
1548
|
-
url: 'https://buyeo.museum.go.kr/bbs/list.do?key=2301250005',
|
|
1549
|
-
parseList: (html) => parseBuyeoMuseumList(html, '2301250005'),
|
|
1550
|
-
parseDetail: parseBuyeoMuseumDetail,
|
|
1551
|
-
},
|
|
1552
|
-
{
|
|
1553
|
-
id: '국립부여박물관_보도자료',
|
|
1554
|
-
name: '국립부여박물관 보도자료',
|
|
1555
|
-
url: 'https://buyeo.museum.go.kr/bbs/list.do?key=2302150024',
|
|
1556
|
-
parseList: (html) => parseBuyeoMuseumList(html, '2302150024'),
|
|
1557
|
-
parseDetail: parseBuyeoMuseumDetail,
|
|
1558
|
-
},
|
|
1559
|
-
{
|
|
1560
|
-
id: '국립진주박물관_새소식',
|
|
1561
|
-
name: '국립진주박물관 새소식',
|
|
1562
|
-
url: 'https://jinju.museum.go.kr/kor/html/sub06/0601.html',
|
|
1563
|
-
parseList: parseJinjuMuseumList,
|
|
1564
|
-
parseDetail: parseJinjuMuseumDetail,
|
|
1565
|
-
},
|
|
1566
|
-
// NOTE: Parsing logic is implemented, but crawling is restricted by robots.txt policy
|
|
1567
|
-
// {
|
|
1568
|
-
// id: '국립경주박물관_새소식',
|
|
1569
|
-
// name: '국립경주박물관 새소식',
|
|
1570
|
-
// url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0701.html',
|
|
1571
|
-
// parseList: (html) =>
|
|
1572
|
-
// parseGyeongjuMuseumList(html, '/kor/html/sub07/0701.html'),
|
|
1573
|
-
// parseDetail: parseGyeongjuMuseumDetail,
|
|
1574
|
-
// },
|
|
1575
|
-
// {
|
|
1576
|
-
// id: '국립경주박물관_고시공고',
|
|
1577
|
-
// name: '국립경주박물관 고시/공고',
|
|
1578
|
-
// url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0703.html',
|
|
1579
|
-
// parseList: parseGyeongjuMuseumNoticeList,
|
|
1580
|
-
// parseDetail: parseGyeongjuMuseumDetail,
|
|
1581
|
-
// },
|
|
1582
|
-
// {
|
|
1583
|
-
// id: '국립경주박물관_보도자료',
|
|
1584
|
-
// name: '국립경주박물관 보도자료',
|
|
1585
|
-
// url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0705.html',
|
|
1586
|
-
// parseList: (html) =>
|
|
1587
|
-
// parseGyeongjuMuseumList(html, '/kor/html/sub07/0705.html'),
|
|
1588
|
-
// parseDetail: parseGyeongjuMuseumDetail,
|
|
1589
|
-
// },
|
|
1590
|
-
// {
|
|
1591
|
-
// id: '국립청주박물관_새소식',
|
|
1592
|
-
// name: '국립청주박물관 새소식',
|
|
1593
|
-
// url: 'https://cheongju.museum.go.kr/www/selectBbsNttList.do?bbsNo=1&key=482&nbar=s',
|
|
1594
|
-
// parseList: parseCheongjuMuseumList,
|
|
1595
|
-
// parseDetail: parseCheongjuMuseumDetail,
|
|
1596
|
-
// },
|
|
1597
|
-
// {
|
|
1598
|
-
// id: '국립청주박물관_언론보도자료',
|
|
1599
|
-
// name: '국립청주박물관 언론보도자료',
|
|
1600
|
-
// url: 'https://cheongju.museum.go.kr/www/selectBbsNttList.do?bbsNo=20&key=31&nbar=s',
|
|
1601
|
-
// parseList: parseCheongjuMuseumList,
|
|
1602
|
-
// parseDetail: parseCheongjuMuseumDetail,
|
|
1603
|
-
// },
|
|
1604
|
-
// {
|
|
1605
|
-
// id: '국립김해박물관_새소식',
|
|
1606
|
-
// name: '국립김해박물관 새소식',
|
|
1607
|
-
// url: 'https://gimhae.museum.go.kr/kr/html/sub04/0401.html',
|
|
1608
|
-
// parseList: (html) =>
|
|
1609
|
-
// parseGimhaeMuseumList(html, '/kr/html/sub04/0401.html'),
|
|
1610
|
-
// parseDetail: parseGimhaeMuseumDetail,
|
|
1611
|
-
// },
|
|
1612
|
-
// {
|
|
1613
|
-
// id: '국립김해박물관_보도자료',
|
|
1614
|
-
// name: '국립김해박물관 언론보도자료',
|
|
1615
|
-
// url: 'https://gimhae.museum.go.kr/kr/html/sub04/0402.html',
|
|
1616
|
-
// parseList: (html) =>
|
|
1617
|
-
// parseGimhaeMuseumList(html, '/kr/html/sub04/0402.html'),
|
|
1618
|
-
// parseDetail: parseGimhaeMuseumDetail,
|
|
1619
|
-
// },
|
|
1620
|
-
// {
|
|
1621
|
-
// id: '국립제주박물관_새소식',
|
|
1622
|
-
// name: '국립제주박물관 새소식',
|
|
1623
|
-
// url: 'https://jeju.museum.go.kr/_prog/_board/?code=sub02_0201&site_dvs_cd=kr&menu_dvs_cd=050101&ntt_tag=1',
|
|
1624
|
-
// parseList: parseJejuMuseumList,
|
|
1625
|
-
// parseDetail: parseJejuMuseumDetail,
|
|
1626
|
-
// },
|
|
1627
|
-
// {
|
|
1628
|
-
// id: '국립익산박물관_공지사항',
|
|
1629
|
-
// name: '국립익산박물관 공지사항',
|
|
1630
|
-
// url: 'https://iksan.museum.go.kr/kor/html/sub05/0501.html',
|
|
1631
|
-
// parseList: parseIksanMuseumList,
|
|
1632
|
-
// parseDetail: parseIksanMuseumDetail,
|
|
1633
|
-
// },
|
|
1634
|
-
],
|
|
1635
|
-
},
|
|
1636
|
-
{
|
|
1637
|
-
id: 'business',
|
|
1638
|
-
name: 'Business',
|
|
1639
|
-
targets: [
|
|
1640
|
-
{
|
|
1641
|
-
id: '국가유산청_입찰정보',
|
|
1642
|
-
name: '국가유산청 입찰정보',
|
|
1643
|
-
url: 'https://www.khs.go.kr/tenderBbz/selectTenderBbzList.do?mn=NS_01_05',
|
|
1644
|
-
parseList: parseKhsTenderList,
|
|
1645
|
-
parseDetail: parseKhsDetail,
|
|
1646
|
-
},
|
|
1647
|
-
{
|
|
1648
|
-
id: '국가유산진흥원_입찰정보',
|
|
1649
|
-
name: '국가유산진흥원 입찰정보',
|
|
1650
|
-
url: 'https://www.kh.or.kr/brd/board/717/L/menu/375',
|
|
1651
|
-
parseList: parseHeritageAgencyList,
|
|
1652
|
-
parseDetail: parseHeritageAgencyDetail,
|
|
1653
|
-
},
|
|
1654
|
-
{
|
|
1655
|
-
id: '한국문화유산협회_사업공고',
|
|
1656
|
-
name: '한국문화유산협회 사업공고',
|
|
1657
|
-
url: 'https://www.kaah.kr/bussopen',
|
|
1658
|
-
parseList: parseKaahList,
|
|
1659
|
-
parseDetail: parseKaahDetail,
|
|
1660
|
-
},
|
|
1661
|
-
{
|
|
1662
|
-
id: '한국문화유산협회_입찰공고',
|
|
1663
|
-
name: '한국문화유산협회 입찰공고',
|
|
1664
|
-
url: 'https://www.kaah.kr/ipcopen',
|
|
1665
|
-
parseList: parseKaahList,
|
|
1666
|
-
parseDetail: parseKaahDetail,
|
|
1667
|
-
},
|
|
1668
|
-
],
|
|
1669
|
-
},
|
|
1670
|
-
{
|
|
1671
|
-
id: 'employment',
|
|
1672
|
-
name: 'Employment',
|
|
1673
|
-
targets: [
|
|
1674
|
-
{
|
|
1675
|
-
id: '국가유산청_시험채용',
|
|
1676
|
-
name: '국가유산청 시험/채용',
|
|
1677
|
-
url: 'https://www.khs.go.kr/multiBbz/selectMultiBbzList.do?bbzId=newexam&mn=NS_01_06',
|
|
1678
|
-
parseList: parseKhsList,
|
|
1679
|
-
parseDetail: parseKhsDetail,
|
|
1680
|
-
},
|
|
1681
|
-
{
|
|
1682
|
-
id: '국가유산진흥원_인재채용',
|
|
1683
|
-
name: '국가유산진흥원 인재채용',
|
|
1684
|
-
url: 'https://www.kh.or.kr/brd/board/721/L/CATEGORY/719/menu/377',
|
|
1685
|
-
parseList: parseHeritageAgencyList,
|
|
1686
|
-
parseDetail: parseHeritageAgencyDetail,
|
|
1687
|
-
},
|
|
1688
|
-
{
|
|
1689
|
-
id: '한국문화유산협회_채용공고',
|
|
1690
|
-
name: '한국문화유산협회 채용공고',
|
|
1691
|
-
url: 'https://www.kaah.kr/reqopen',
|
|
1692
|
-
parseList: parseKaahList,
|
|
1693
|
-
parseDetail: parseKaahDetail,
|
|
1694
|
-
},
|
|
1695
|
-
{
|
|
1696
|
-
id: '영남고고학회_채용공고',
|
|
1697
|
-
name: '영남고고학회 채용공고',
|
|
1698
|
-
url: 'http://www.yngogo.or.kr/subList/32000001136',
|
|
1699
|
-
parseList: (html) => parseYngogoList(html, '32000001136', '32000001164', '32000001718', customFetch),
|
|
1700
|
-
parseDetail: (html) => parseYngogoDetail(html, '32000001136', '32000001164', extractNttSeq(html), '32000001718', customFetch),
|
|
1701
|
-
},
|
|
1702
|
-
{
|
|
1703
|
-
id: '국립중앙박물관_채용안내',
|
|
1704
|
-
name: '국립중앙박물관 채용 안내',
|
|
1705
|
-
url: 'https://www.museum.go.kr/MUSEUM/contents/M0701030000.do?catCustomType=post&catId=54&recruitYn=Y',
|
|
1706
|
-
parseList: parseMuseumRecruitList,
|
|
1707
|
-
parseDetail: parseMuseumDetail,
|
|
1708
|
-
},
|
|
1709
|
-
{
|
|
1710
|
-
id: '국립전주박물관_채용',
|
|
1711
|
-
name: '국립전주박물관 채용',
|
|
1712
|
-
url: 'https://jeonju.museum.go.kr/board.es?mid=a10105020000&bid=0002',
|
|
1713
|
-
parseList: parseJeonjuMuseumRecruitList,
|
|
1714
|
-
parseDetail: parseJeonjuMuseumDetail,
|
|
1715
|
-
},
|
|
1716
|
-
{
|
|
1717
|
-
id: '국립부여박물관_채용공고',
|
|
1718
|
-
name: '국립부여박물관 채용공고',
|
|
1719
|
-
url: 'https://buyeo.museum.go.kr/bbs/list.do?key=2301270001',
|
|
1720
|
-
parseList: (html) => parseBuyeoMuseumList(html, '2301270001'),
|
|
1721
|
-
parseDetail: parseBuyeoMuseumDetail,
|
|
1722
|
-
},
|
|
1723
|
-
// NOTE: Parsing logic is implemented, but crawling is restricted by robots.txt policy
|
|
1724
|
-
// {
|
|
1725
|
-
// id: '국립경주박물관_채용안내',
|
|
1726
|
-
// name: '국립경주박물관 채용안내',
|
|
1727
|
-
// url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0704.html',
|
|
1728
|
-
// parseList: (html) =>
|
|
1729
|
-
// parseGyeongjuMuseumList(html, '/kor/html/sub07/0704.html'),
|
|
1730
|
-
// parseDetail: parseGyeongjuMuseumDetail,
|
|
1731
|
-
// },
|
|
1732
|
-
// {
|
|
1733
|
-
// id: '국립청주박물관_채용및공고',
|
|
1734
|
-
// name: '국립청주박물관 채용 및 공고',
|
|
1735
|
-
// url: 'https://cheongju.museum.go.kr/www/selectBbsNttList.do?bbsNo=29&key=476&nbar=s',
|
|
1736
|
-
// parseList: parseCheongjuMuseumList,
|
|
1737
|
-
// parseDetail: parseCheongjuMuseumDetail,
|
|
1738
|
-
// },
|
|
1739
|
-
// {
|
|
1740
|
-
// id: '국립제주박물관_채용정보',
|
|
1741
|
-
// name: '국립제주박물관 채용정보',
|
|
1742
|
-
// url: 'https://jeju.museum.go.kr/_prog/_board/?code=sub02_0201&site_dvs_cd=kr&menu_dvs_cd=050102&ntt_tag=2',
|
|
1743
|
-
// parseList: parseJejuMuseumList,
|
|
1744
|
-
// parseDetail: parseJejuMuseumDetail,
|
|
1745
|
-
// },
|
|
1746
|
-
],
|
|
1747
|
-
},
|
|
1748
|
-
];
|
|
1749
|
-
}
|
|
1750
|
-
function getSourceList() {
|
|
1751
|
-
return createCrawlingTargetGroups().map((group) => ({
|
|
1752
|
-
id: group.id,
|
|
1753
|
-
name: group.name,
|
|
1754
|
-
sources: group.targets.map((target) => ({
|
|
1755
|
-
id: target.id,
|
|
1756
|
-
name: target.name,
|
|
1757
|
-
url: target.url,
|
|
1758
|
-
})),
|
|
1759
|
-
}));
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
/**
|
|
1763
|
-
* Newsletter content configuration
|
|
1764
|
-
*/
|
|
1765
|
-
const contentOptions = {
|
|
1766
|
-
outputLanguage: '한국어',
|
|
1767
|
-
expertField: ['문화유산'],
|
|
1768
|
-
};
|
|
1769
|
-
/**
|
|
1770
|
-
* Newsletter brand configuration
|
|
1771
|
-
*/
|
|
1772
|
-
const newsletterConfig = {
|
|
1773
|
-
brandName: '문화유산 리서치 레이더',
|
|
1774
|
-
subscribePageUrl: 'https://heripo.app/research-radar/subscribe',
|
|
1775
|
-
publicationCriteria: {
|
|
1776
|
-
minimumArticleCountForIssue: 5,
|
|
1777
|
-
priorityArticleScoreThreshold: 8,
|
|
1778
|
-
},
|
|
1779
|
-
};
|
|
1780
|
-
/**
|
|
1781
|
-
* LLM configuration
|
|
1782
|
-
*/
|
|
1783
|
-
const llmConfig = {
|
|
1784
|
-
maxRetries: 5,
|
|
1785
|
-
chainStopAfterAttempt: 3,
|
|
1786
|
-
generation: {
|
|
1787
|
-
temperature: 0.3,
|
|
1788
|
-
},
|
|
1789
|
-
};
|
|
1790
|
-
|
|
1791
|
-
/**
|
|
1792
|
-
* Analysis provider implementation
|
|
1793
|
-
* - LLM-based article analysis
|
|
1794
|
-
* - Tag classification, image analysis, importance scoring
|
|
1795
|
-
*/
|
|
1796
|
-
class AnalysisProvider {
|
|
1797
|
-
openai;
|
|
1798
|
-
articleRepository;
|
|
1799
|
-
tagRepository;
|
|
1800
|
-
classifyTagOptions;
|
|
1801
|
-
analyzeImagesOptions;
|
|
1802
|
-
determineScoreOptions;
|
|
1803
|
-
constructor(openai, articleRepository, tagRepository) {
|
|
1804
|
-
this.openai = openai;
|
|
1805
|
-
this.articleRepository = articleRepository;
|
|
1806
|
-
this.tagRepository = tagRepository;
|
|
1807
|
-
this.classifyTagOptions = {
|
|
1808
|
-
model: this.openai('gpt-5-mini'),
|
|
1809
|
-
};
|
|
1810
|
-
this.analyzeImagesOptions = {
|
|
1811
|
-
model: this.openai('gpt-5.1'),
|
|
1812
|
-
};
|
|
1813
|
-
this.determineScoreOptions = {
|
|
1814
|
-
model: this.openai('gpt-5.1'),
|
|
1815
|
-
minimumImportanceScoreRules: [
|
|
1816
|
-
// Korean Archaeological Society news: minimum score 6
|
|
1817
|
-
{
|
|
1818
|
-
targetUrl: 'https://www.kras.or.kr/?r=kras&m=bbs&bid=notice',
|
|
1819
|
-
minScore: 6,
|
|
1820
|
-
},
|
|
1821
|
-
{
|
|
1822
|
-
targetUrl: 'https://www.kras.or.kr/?r=kras&m=bbs&bid=sympo',
|
|
1823
|
-
minScore: 6,
|
|
1824
|
-
},
|
|
1825
|
-
{
|
|
1826
|
-
targetUrl: 'https://www.kras.or.kr/?c=61/101/105',
|
|
1827
|
-
minScore: 6,
|
|
1828
|
-
},
|
|
1829
|
-
{
|
|
1830
|
-
targetUrl: 'https://www.kaah.kr/notice',
|
|
1831
|
-
minScore: 6,
|
|
1832
|
-
},
|
|
1833
|
-
{
|
|
1834
|
-
targetUrl: 'https://www.kaah.kr/news',
|
|
1835
|
-
minScore: 6,
|
|
1836
|
-
},
|
|
1837
|
-
{
|
|
1838
|
-
targetUrl: 'https://www.kaah.kr/mass',
|
|
1839
|
-
minScore: 6,
|
|
1840
|
-
},
|
|
1841
|
-
{
|
|
1842
|
-
targetUrl: 'https://www.kaah.kr/assnews',
|
|
1843
|
-
minScore: 6,
|
|
1844
|
-
},
|
|
1845
|
-
{
|
|
1846
|
-
targetUrl: 'https://www.kaah.kr/ralnews',
|
|
1847
|
-
minScore: 6,
|
|
1848
|
-
},
|
|
1849
|
-
{
|
|
1850
|
-
targetUrl: 'https://www.kaah.kr/notice',
|
|
1851
|
-
minScore: 6,
|
|
1852
|
-
},
|
|
1853
|
-
{
|
|
1854
|
-
targetUrl: 'https://www.kaah.kr/placeopen',
|
|
1855
|
-
minScore: 6,
|
|
1856
|
-
},
|
|
1857
|
-
{
|
|
1858
|
-
targetUrl: 'https://www.kaah.kr/bussopen',
|
|
1859
|
-
minScore: 6,
|
|
1860
|
-
},
|
|
1861
|
-
{
|
|
1862
|
-
targetUrl: 'https://www.kaah.kr/ipcopen',
|
|
1863
|
-
minScore: 6,
|
|
1864
|
-
},
|
|
1865
|
-
{
|
|
1866
|
-
targetUrl: 'https://www.seamuse.go.kr/resources/academiccultural/list/1',
|
|
1867
|
-
minScore: 6,
|
|
1868
|
-
},
|
|
1869
|
-
{
|
|
1870
|
-
targetUrl: 'https://www.seamuse.go.kr/resources/academicreport/list/1',
|
|
1871
|
-
minScore: 6,
|
|
1872
|
-
},
|
|
1873
|
-
{
|
|
1874
|
-
targetUrl: 'https://www.seamuse.go.kr/resources/academicreport/list/1#tab2',
|
|
1875
|
-
minScore: 6,
|
|
1876
|
-
},
|
|
1877
|
-
{
|
|
1878
|
-
targetUrl: 'https://www.seamuse.go.kr/resources/academicreport/list/1#tab3',
|
|
1879
|
-
minScore: 6,
|
|
1880
|
-
},
|
|
1881
|
-
{
|
|
1882
|
-
targetUrl: 'https://www.seamuse.go.kr/resources/academicreport/list/1#tab4',
|
|
1883
|
-
minScore: 6,
|
|
1884
|
-
},
|
|
1885
|
-
// Excavation report news: minimum score 2
|
|
1886
|
-
{
|
|
1887
|
-
targetUrl: 'https://www.e-minwon.go.kr/ge/ee/getListEcexmPrmsnAply.do',
|
|
1888
|
-
minScore: 2,
|
|
1889
|
-
},
|
|
1890
|
-
{
|
|
1891
|
-
targetUrl: 'https://www.e-minwon.go.kr/ge/ee/getListEcexmRptp.do',
|
|
1892
|
-
minScore: 2,
|
|
1893
|
-
},
|
|
1894
|
-
{
|
|
1895
|
-
targetUrl: 'https://www.e-minwon.go.kr/ge/ee/getListLinkGrndsRls.do',
|
|
1896
|
-
minScore: 2,
|
|
1897
|
-
},
|
|
1898
|
-
],
|
|
1899
|
-
};
|
|
1900
|
-
}
|
|
1901
|
-
/**
|
|
1902
|
-
* Fetch articles that haven't been scored yet
|
|
1903
|
-
* @returns Unscored articles awaiting analysis
|
|
1904
|
-
*/
|
|
1905
|
-
async fetchUnscoredArticles() {
|
|
1906
|
-
return this.articleRepository.findUnscoredArticles();
|
|
1907
|
-
}
|
|
1908
|
-
/**
|
|
1909
|
-
* Fetch all existing tags for classification
|
|
1910
|
-
* @returns List of tag names
|
|
1911
|
-
*/
|
|
1912
|
-
async fetchTags() {
|
|
1913
|
-
return this.tagRepository.findAllTags();
|
|
1914
|
-
}
|
|
1915
|
-
/**
|
|
1916
|
-
* Update article with analysis results (tags, image analysis, importance score)
|
|
1917
|
-
* @param article - Article with analysis data
|
|
1918
|
-
*/
|
|
1919
|
-
async update(article) {
|
|
1920
|
-
await this.articleRepository.updateAnalysis(article);
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
/**
|
|
1925
|
-
* Shared HTML fragments used by both newsletter and welcome email templates.
|
|
1926
|
-
*
|
|
1927
|
-
* These helpers extract identical HTML blocks to avoid duplication
|
|
1928
|
-
* while keeping template-specific styling separate.
|
|
1929
|
-
*/
|
|
1930
|
-
const purify = DOMPurify(new jsdom.JSDOM('').window);
|
|
1931
|
-
/**
|
|
1932
|
-
* Sanitize user-supplied strings for safe HTML insertion.
|
|
1933
|
-
* Strips all HTML tags, leaving only plain text.
|
|
1934
|
-
*/
|
|
1935
|
-
const sanitizeText = (str) => purify.sanitize(str, { ALLOWED_TAGS: [] });
|
|
1936
|
-
/**
|
|
1937
|
-
* Heripo light/dark logo block.
|
|
1938
|
-
* @param imgMarginBottom - Margin below the logo image (e.g., '8px', '12px')
|
|
1939
|
-
*/
|
|
1940
|
-
const heripoLogoHtml = (imgMarginBottom) => `
|
|
1941
|
-
<div style="margin-bottom: 32px;">
|
|
1942
|
-
<div style="text-align: left; display: block;" class="light-logo">
|
|
1943
|
-
<img src="https://heripo.app/heripo-logo.png" width="150" alt="로고" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; margin-bottom: ${imgMarginBottom};" height="auto">
|
|
1944
|
-
</div>
|
|
1945
|
-
<!--[if !mso]><!-->
|
|
1946
|
-
<div style="text-align: left; display: none;" class="dark-logo">
|
|
1947
|
-
<img src="https://heripo.app/heripo-logo-dark.png" width="150" alt="다크모드 로고" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; margin-bottom: ${imgMarginBottom};" height="auto">
|
|
1948
|
-
</div>
|
|
1949
|
-
<!--<![endif]-->
|
|
1950
|
-
</div>`;
|
|
1951
|
-
/**
|
|
1952
|
-
* Heripo platform introduction section.
|
|
1953
|
-
* Shared between newsletter and welcome email templates.
|
|
1954
|
-
*
|
|
1955
|
-
* Note: Each template may append its own additional paragraph after this block
|
|
1956
|
-
* (e.g., newsletter adds a line about source requests via GitHub Issues).
|
|
1957
|
-
*/
|
|
1958
|
-
const platformIntroHtml = () => `
|
|
1959
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">heripo는 고고학 연구 환경의 실질적인 디지털 전환을 지향하는 연구 플랫폼입니다.</p>
|
|
1960
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">발굴조사보고서(PDF) 속에 갇힌 텍스트와 도면을 분석 가능한 구조화된 데이터로 전환하여, 연구자가 자료를 보다 체계적으로 탐색하고 재사용할 수 있는 인프라를 구축하고 있습니다.</p>
|
|
1961
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
|
|
1962
|
-
color: #444444; margin: 0 0 18px 0;">현재는 소프트웨어 엔지니어와 고고학 연구자가 함께하는 <strong><a href="https://github.com/heripo-lab" target="_blank">heripo lab</a></strong>으로 운영 중이며, 2026년 1월 28일 핵심 엔진을 <strong><a href="https://github.com/heripo-lab/heripo-engine" target="_blank">오픈소스로 공개</a></strong>했습니다.</p>
|
|
1963
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">오픈소스로 공개된 핵심 기능은 <strong><a href="https://engine-demo.heripo.org" target="_blank">데모 사이트</a></strong>에서 직접 체험해 보실 수 있으며, 플랫폼 프로토타입 출시 시 구독자분들께 우선 안내해 드리겠습니다.</p>`;
|
|
1964
|
-
/**
|
|
1965
|
-
* "Powered by LLM Newsletter Kit · View Source" footer line.
|
|
1966
|
-
*/
|
|
1967
|
-
const poweredByFooterHtml = () => `
|
|
1968
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #999999; margin: 0 0 12px 0;" class="footer-text">
|
|
1969
|
-
Powered by <a href="https://github.com/heripo-lab/llm-newsletter-kit-core" target="_blank" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; transition: color 0.2s; color: #999999; text-decoration: underline;" class="footer-link">LLM Newsletter Kit</a> ·
|
|
1970
|
-
<a href="https://github.com/heripo-lab/heripo-research-radar" target="_blank" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; transition: color 0.2s; color: #999999; text-decoration: underline;" class="footer-link">View Source</a>
|
|
1971
|
-
</p>`;
|
|
1972
|
-
|
|
1973
|
-
/**
|
|
1974
|
-
* Creates an HTML template for the newsletter email
|
|
1975
|
-
*
|
|
1976
|
-
* This function generates a responsive email template with:
|
|
1977
|
-
* - Light/dark mode support
|
|
1978
|
-
* - Mobile-friendly design
|
|
1979
|
-
* - Brand-specific styling
|
|
1980
|
-
* - Link to crawling sources page
|
|
1981
|
-
* - Publication policy information
|
|
1982
|
-
* - Platform introduction
|
|
1983
|
-
*
|
|
1984
|
-
* @param options - Optional template customization options
|
|
1985
|
-
* @returns Complete HTML string for the newsletter email
|
|
1986
|
-
*
|
|
1987
|
-
* @example
|
|
1988
|
-
* ```typescript
|
|
1989
|
-
* const html = createNewsletterHtmlTemplate(
|
|
1990
|
-
* { isKrasNewsletter: true, krasNewsMarkdown: '## News...' },
|
|
1991
|
-
* );
|
|
1992
|
-
* ```
|
|
1993
|
-
*/
|
|
1994
|
-
const createNewsletterHtmlTemplate = (options) => `<!DOCTYPE html>
|
|
1995
|
-
<html lang="ko" style="color-scheme: light dark; supported-color-schemes: light dark;">
|
|
1996
|
-
<head>
|
|
1997
|
-
<meta charset="UTF-8">
|
|
1998
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1999
|
-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
2000
|
-
<meta name="color-scheme" content="light dark">
|
|
2001
|
-
<meta name="supported-color-schemes" content="light dark">
|
|
2002
|
-
<style type="text/css">
|
|
2003
|
-
a:hover {
|
|
2004
|
-
color: #D2691E !important;
|
|
2005
|
-
}
|
|
2006
|
-
.button-link {
|
|
2007
|
-
color: #fff !important;
|
|
2008
|
-
background: #D2691E;
|
|
2009
|
-
padding: 10px 28px;
|
|
2010
|
-
border-radius: 5px;
|
|
2011
|
-
text-decoration: none !important;
|
|
2012
|
-
font-weight: bold;
|
|
2013
|
-
font-size: 16px;
|
|
2014
|
-
display: inline-block;
|
|
2015
|
-
letter-spacing: 0.5px;
|
|
2016
|
-
}
|
|
2017
|
-
.button-link:hover {
|
|
2018
|
-
background: #b85a1a !important;
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
html {
|
|
2022
|
-
color-scheme: light dark;
|
|
2023
|
-
supported-color-schemes: light dark;
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
body {
|
|
2027
|
-
-webkit-text-size-adjust: 100%;
|
|
2028
|
-
-ms-text-size-adjust: 100%;
|
|
2029
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
2030
|
-
background-color: #f4f4f4;
|
|
2031
|
-
font-size: 16px;
|
|
2032
|
-
line-height: 1.7;
|
|
2033
|
-
letter-spacing: 0.01em;
|
|
2034
|
-
height: 100%;
|
|
2035
|
-
width: 100%;
|
|
2036
|
-
margin: 0;
|
|
2037
|
-
padding: 0;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
.main-table {
|
|
2041
|
-
-webkit-text-size-adjust: 100%;
|
|
2042
|
-
-ms-text-size-adjust: 100%;
|
|
2043
|
-
mso-table-lspace: 0pt;
|
|
2044
|
-
mso-table-rspace: 0pt;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
.outer-cell {
|
|
2048
|
-
-webkit-text-size-adjust: 100%;
|
|
2049
|
-
-ms-text-size-adjust: 100%;
|
|
2050
|
-
mso-table-lspace: 0pt;
|
|
2051
|
-
mso-table-rspace: 0pt;
|
|
2052
|
-
padding: 20px 0;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
.container {
|
|
2056
|
-
-webkit-text-size-adjust: 100%;
|
|
2057
|
-
-ms-text-size-adjust: 100%;
|
|
2058
|
-
mso-table-lspace: 0pt;
|
|
2059
|
-
mso-table-rspace: 0pt;
|
|
2060
|
-
max-width: 700px;
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
.content-cell {
|
|
2064
|
-
-webkit-text-size-adjust: 100%;
|
|
2065
|
-
-ms-text-size-adjust: 100%;
|
|
2066
|
-
mso-table-lspace: 0pt;
|
|
2067
|
-
mso-table-rspace: 0pt;
|
|
2068
|
-
padding: 44px 44px 36px 44px;
|
|
2069
|
-
border-radius: 12px;
|
|
2070
|
-
box-shadow: 0 4px 18px rgba(0,0,0,0.07);
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
.logo-container {
|
|
2074
|
-
margin-bottom: 32px;
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
.light-logo {
|
|
2078
|
-
text-align: left;
|
|
2079
|
-
display: block;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
.dark-logo {
|
|
2083
|
-
text-align: left;
|
|
2084
|
-
display: none;
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
.logo-img {
|
|
2088
|
-
-ms-interpolation-mode: bicubic;
|
|
2089
|
-
border: 0;
|
|
2090
|
-
height: auto;
|
|
2091
|
-
line-height: 100%;
|
|
2092
|
-
outline: none;
|
|
2093
|
-
text-decoration: none;
|
|
2094
|
-
display: block;
|
|
2095
|
-
margin-bottom: 12px;
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
h1 {
|
|
2099
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
2100
|
-
line-height: 1.2;
|
|
2101
|
-
margin: 0 0 18px 0;
|
|
2102
|
-
letter-spacing: -0.5px;
|
|
2103
|
-
margin-top: 0;
|
|
2104
|
-
font-size: 28px;
|
|
2105
|
-
font-weight: bold;
|
|
2106
|
-
color: #111111;
|
|
2107
|
-
border-bottom: 3px solid #D2691E;
|
|
2108
|
-
padding-bottom: 8px;
|
|
2109
|
-
margin-bottom: 24px;
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
p {
|
|
2113
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
2114
|
-
font-size: 16px;
|
|
2115
|
-
line-height: 1.7;
|
|
2116
|
-
color: #444444;
|
|
2117
|
-
margin: 0 0 18px 0;
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
.strong-text {
|
|
2121
|
-
color: #D2691E;
|
|
2122
|
-
font-weight: bold;
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
hr {
|
|
2126
|
-
border: 0;
|
|
2127
|
-
border-top: 2px solid #D2691E;
|
|
2128
|
-
margin: 32px 0;
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
h2 {
|
|
2132
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
2133
|
-
font-size: 24px;
|
|
2134
|
-
font-weight: bold;
|
|
2135
|
-
line-height: 1.3;
|
|
2136
|
-
color: #D2691E;
|
|
2137
|
-
margin: 0 0 16px 0;
|
|
2138
|
-
letter-spacing: -0.2px;
|
|
2139
|
-
border-left: 5px solid #D2691E;
|
|
2140
|
-
padding-left: 12px;
|
|
2141
|
-
background: none;
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
ul {
|
|
2145
|
-
padding-left: 24px;
|
|
2146
|
-
margin: 0 0 18px 0;
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
ol {
|
|
2150
|
-
padding-left: 24px;
|
|
2151
|
-
margin: 0 0 18px 0;
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
li {
|
|
2155
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
2156
|
-
font-size: 16px;
|
|
2157
|
-
line-height: 1.7;
|
|
2158
|
-
color: #444444;
|
|
2159
|
-
margin: 0 0 18px 0;
|
|
2160
|
-
margin-bottom: 8px;
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
h3 {
|
|
2164
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
2165
|
-
font-size: 18px;
|
|
2166
|
-
font-weight: bold;
|
|
2167
|
-
line-height: 1.3;
|
|
2168
|
-
color: #D2691E;
|
|
2169
|
-
margin: 0 0 12px 0;
|
|
2170
|
-
letter-spacing: -0.1px;
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
.link-style {
|
|
2174
|
-
-webkit-text-size-adjust: 100%;
|
|
2175
|
-
-ms-text-size-adjust: 100%;
|
|
2176
|
-
color: #0056b3;
|
|
2177
|
-
text-decoration: underline;
|
|
2178
|
-
font-weight: bold;
|
|
2179
|
-
transition: color 0.2s;
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
.data-table {
|
|
2183
|
-
-webkit-text-size-adjust: 100%;
|
|
2184
|
-
-ms-text-size-adjust: 100%;
|
|
2185
|
-
mso-table-lspace: 0pt;
|
|
2186
|
-
mso-table-rspace: 0pt;
|
|
2187
|
-
width: 100%;
|
|
2188
|
-
border-collapse: collapse;
|
|
2189
|
-
margin: 0 0 18px 0;
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
.table-th {
|
|
2193
|
-
border: 1px solid #e5e5e5;
|
|
2194
|
-
padding: 12px 8px;
|
|
2195
|
-
text-align: left;
|
|
2196
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
2197
|
-
font-size: 15px;
|
|
2198
|
-
background-color: #f2e6dd;
|
|
2199
|
-
font-weight: bold;
|
|
2200
|
-
color: #D2691E;
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
.content-cell table {
|
|
2204
|
-
width: 100%;
|
|
2205
|
-
border-collapse: collapse;
|
|
2206
|
-
margin: 0 0 18px 0;
|
|
2207
|
-
}
|
|
2208
|
-
.content-cell th,
|
|
2209
|
-
.content-cell td {
|
|
2210
|
-
border: 1px solid #e5e5e5;
|
|
2211
|
-
padding: 12px 8px;
|
|
2212
|
-
text-align: left;
|
|
2213
|
-
font-size: 15px;
|
|
2214
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
2215
|
-
}
|
|
2216
|
-
.content-cell thead th {
|
|
2217
|
-
background-color: #f2e6dd;
|
|
2218
|
-
color: #D2691E;
|
|
2219
|
-
font-weight: bold;
|
|
2220
|
-
}
|
|
2221
|
-
.content-cell tbody tr:nth-child(even) td {
|
|
2222
|
-
background-color: #faf7f3;
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
@media screen and (max-width: 700px) {
|
|
2226
|
-
.container {
|
|
2227
|
-
width: 100% !important;
|
|
2228
|
-
max-width: 100% !important;
|
|
2229
|
-
padding: 0 !important;
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
.content-cell {
|
|
2233
|
-
padding: 20px !important;
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
@media screen and (max-width: 600px) {
|
|
2237
|
-
h1 {
|
|
2238
|
-
font-size: 22px !important;
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
h2 {
|
|
2242
|
-
font-size: 18px !important;
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
.content-cell {
|
|
2246
|
-
padding: 12px !important;
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
@media (prefers-color-scheme: dark) {
|
|
2250
|
-
body,
|
|
2251
|
-
.dark-mode-bg {
|
|
2252
|
-
background-color: #121212 !important;
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
.dark-mode-content-bg {
|
|
2256
|
-
background-color: #1e1e1e !important;
|
|
2257
|
-
box-shadow: 0 4px 10px rgba(0,0,0,0.25) !important;
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
h1 {
|
|
2261
|
-
color: #ffffff !important;
|
|
2262
|
-
border-bottom: 3px solid #E59866 !important;
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
h2,
|
|
2266
|
-
h3 {
|
|
2267
|
-
color: #E59866 !important;
|
|
2268
|
-
border-left-color: #E59866 !important;
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
p,
|
|
2272
|
-
li {
|
|
2273
|
-
color: #eeeeee !important;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
a:not(.button-link) {
|
|
2277
|
-
color: #4da6ff !important;
|
|
2278
|
-
text-decoration: underline !important;
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
a.button-link {
|
|
2282
|
-
background: #E59866 !important;
|
|
2283
|
-
color: #222 !important;
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
strong {
|
|
2287
|
-
color: #E59866 !important;
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
hr,
|
|
2291
|
-
.section-divider {
|
|
2292
|
-
border-top-color: #E59866 !important;
|
|
2293
|
-
background: linear-gradient(90deg, #E59866 0%, #121212 100%) !important;
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
blockquote {
|
|
2297
|
-
background-color: #2b2b2b !important;
|
|
2298
|
-
border-left-color: #E59866 !important;
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
blockquote p {
|
|
2302
|
-
color: #E59866 !important;
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
code {
|
|
2306
|
-
background-color: #333333 !important;
|
|
2307
|
-
color: #E59866 !important;
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
pre {
|
|
2311
|
-
background-color: #2d2d2d !important;
|
|
2312
|
-
color: #f2f2f2 !important;
|
|
2313
|
-
border: 1px solid #444 !important;
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
.container table th,
|
|
2317
|
-
.container table td {
|
|
2318
|
-
border-color: #444444 !important;
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
.container table th {
|
|
2322
|
-
background-color: #333333 !important;
|
|
2323
|
-
color: #E59866 !important;
|
|
2324
|
-
}
|
|
2325
|
-
|
|
2326
|
-
.container table td strong {
|
|
2327
|
-
color: #E59866 !important;
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
.container table tr:nth-child(even) td {
|
|
2331
|
-
background-color: #23201c !important;
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
.button-cell {
|
|
2335
|
-
background-color: #E59866 !important;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
.button-link {
|
|
2339
|
-
color: #222 !important;
|
|
2340
|
-
background: #E59866 !important;
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
.footer-text {
|
|
2344
|
-
color: #999999 !important;
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
.footer-link {
|
|
2348
|
-
color: #999999 !important;
|
|
2349
|
-
text-decoration: underline !important;
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
.dark-logo {
|
|
2353
|
-
display: block !important;
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
.light-logo {
|
|
2357
|
-
display: none !important;
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
.content-cell th,
|
|
2361
|
-
.content-cell td {
|
|
2362
|
-
border-color: #444444 !important;
|
|
2363
|
-
}
|
|
2364
|
-
.content-cell thead th {
|
|
2365
|
-
background-color: #333333 !important;
|
|
2366
|
-
color: #E59866 !important;
|
|
2367
|
-
}
|
|
2368
|
-
.content-cell tbody tr:nth-child(even) td {
|
|
2369
|
-
background-color: #23201c !important;
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
.header-dark-text {
|
|
2374
|
-
color: #eeeeee !important;
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
.header-title-border {
|
|
2378
|
-
border-bottom-color: #E59866 !important;
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
.dark-logo-inline {
|
|
2382
|
-
display: inline-block !important;
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
.kras-newsletter .kras-header-title {
|
|
2386
|
-
color: #eeeeee !important;
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
.kras-newsletter .kras-header-date {
|
|
2390
|
-
color: #bbbbbb !important;
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
.kras-newsletter .kras-header-divider {
|
|
2394
|
-
border-top-color: #E59866 !important;
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
</style>
|
|
2398
|
-
</head>
|
|
2399
|
-
<body style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f4; font-size: 16px; line-height: 1.7; letter-spacing: 0.01em; height: 100%; width: 100%; margin: 0; padding: 0;">
|
|
2400
|
-
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
|
|
2401
|
-
<tr>
|
|
2402
|
-
<td bgcolor="#f4f4f4" align="center" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 20px 0;" class="dark-mode-bg">
|
|
2403
|
-
<!--[if (gte mso 9)|(IE)]>
|
|
2404
|
-
<table align="center" border="0" cellspacing="0" cellpadding="0" width="700">
|
|
2405
|
-
<tr>
|
|
2406
|
-
<td align="center" valign="top" width="700">
|
|
2407
|
-
<![endif]-->
|
|
2408
|
-
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 700px;" class="container" role="presentation">
|
|
2409
|
-
<tr>
|
|
2410
|
-
<td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg${options?.isKrasNewsletter ? ' kras-newsletter' : ''}" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 44px 44px 36px 44px; border-radius: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.07);">
|
|
2411
|
-
${options?.isKrasNewsletter
|
|
2412
|
-
? `<!-- KRAS 50주년 헤더 -->
|
|
2413
|
-
<div style="text-align: center; margin-bottom: 28px;">
|
|
2414
|
-
<div style="width: 180px; min-height: 141px; display: inline-block; margin-bottom: 20px;"><img src="https://heripo.app/kras-50.png" width="180" alt="한국고고학회 50주년" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block;" height="auto"></div>
|
|
2415
|
-
<div class="kras-header-title" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 30px; font-weight: bold; color: #111111; line-height: 1.2; margin-bottom: 8px;">한국고고학회 뉴스레터</div>
|
|
2416
|
-
<div class="kras-header-date" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; color: #666666; line-height: 1.5;">${options?.displayDate ?? ''}</div>
|
|
2417
|
-
</div>
|
|
2418
|
-
<hr class="kras-header-divider" style="border: 0; border-top: 2px solid #D2691E; margin: 0 0 32px 0;">
|
|
2419
|
-
`
|
|
2420
|
-
: `${heripoLogoHtml('12px')}
|
|
2421
|
-
`}
|
|
2422
|
-
|
|
2423
|
-
${options?.krasNewsMarkdown
|
|
2424
|
-
? `<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;"><span style="display: inline-block; width: 26px; height: 26px; vertical-align: -4px; margin-right: 6px;"><img src="https://heripo.app/kras-symbol.png" width="26" height="26" alt="" style="border: 0; display: block;"></span>학회 소식</h2>` +
|
|
2425
|
-
safeMarkdown2Html(`${options.krasNewsMarkdown}
|
|
2426
|
-
|
|
2427
|
-
---
|
|
2428
|
-
`, {
|
|
2429
|
-
window: new jsdom.JSDOM('').window,
|
|
2430
|
-
linkTargetBlank: true,
|
|
2431
|
-
fixMalformedUrls: true,
|
|
2432
|
-
fixBoldSyntax: true,
|
|
2433
|
-
convertStrikethrough: true,
|
|
2434
|
-
}).replaceAll('%7B%7B%7BRESEND_UNSUBSCRIBE_URL%7D%7D%7D', '{{{RESEND_UNSUBSCRIBE_URL}}}')
|
|
2435
|
-
: ''}
|
|
2436
|
-
|
|
2437
|
-
${options?.heripolabNewsMarkdown
|
|
2438
|
-
? safeMarkdown2Html(`## heripo lab 소식
|
|
2439
|
-
${options.heripolabNewsMarkdown}
|
|
2440
|
-
|
|
2441
|
-
---
|
|
2442
|
-
`, {
|
|
2443
|
-
window: new jsdom.JSDOM('').window,
|
|
2444
|
-
linkTargetBlank: true,
|
|
2445
|
-
fixMalformedUrls: true,
|
|
2446
|
-
fixBoldSyntax: true,
|
|
2447
|
-
convertStrikethrough: true,
|
|
2448
|
-
})
|
|
2449
|
-
: ''}
|
|
2450
|
-
|
|
2451
|
-
{{NEWSLETTER_CONTENT}}
|
|
2452
|
-
|
|
2453
|
-
<hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
|
|
2454
|
-
|
|
2455
|
-
${options?.krasNoticeMarkdown
|
|
2456
|
-
? `<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;"><span style="display: inline-block; width: 26px; height: 26px; vertical-align: -4px; margin-right: 6px;"><img src="https://heripo.app/kras-symbol.png" width="26" height="26" alt="" style="border: 0; display: block;"></span>학회 안내</h2>` +
|
|
2457
|
-
safeMarkdown2Html(`${options.krasNoticeMarkdown}
|
|
2458
|
-
|
|
2459
|
-
---
|
|
2460
|
-
`, {
|
|
2461
|
-
window: new jsdom.JSDOM('').window,
|
|
2462
|
-
linkTargetBlank: true,
|
|
2463
|
-
fixMalformedUrls: true,
|
|
2464
|
-
fixBoldSyntax: true,
|
|
2465
|
-
convertStrikethrough: true,
|
|
2466
|
-
}).replaceAll('%7B%7B%7BRESEND_UNSUBSCRIBE_URL%7D%7D%7D', '{{{RESEND_UNSUBSCRIBE_URL}}}')
|
|
2467
|
-
: ''}
|
|
2468
|
-
|
|
2469
|
-
<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;">🔍 뉴스레터 출처</h2>
|
|
2470
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">모든 소식의 출처는 <a href="https://heripo.app/research-radar/sources" target="_blank">이곳에서</a> 확인할 수 있습니다.</p>
|
|
2471
|
-
<hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
|
|
2472
|
-
<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;">📅 발행 정책</h2>
|
|
2473
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong>${options?.isKrasNewsletter ? '한국고고학회 뉴스레터' : 'heripo 리서치 레이더'}</strong>는 매일 발행을 원칙으로 하되, 독자분들께 의미 있는 정보를 제공하기 위해 다음과 같은 발행 기준을 적용합니다:</p>
|
|
2474
|
-
<ul style="padding-left: 24px; margin: 0 0 18px 0;">
|
|
2475
|
-
<li style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0; margin-bottom: 8px;"><strong>정상 발행</strong>: 새로운 소식이 ${newsletterConfig.publicationCriteria.minimumArticleCountForIssue + 1}개 이상이거나, ${newsletterConfig.publicationCriteria.minimumArticleCountForIssue}개 이하여도 중요도 ${newsletterConfig.publicationCriteria.priorityArticleScoreThreshold}점 이상의 핵심 소식이 포함된 경우</li>
|
|
2476
|
-
<li style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0; margin-bottom: 8px;"><strong>이월 발행</strong>: 새로운 소식이 ${newsletterConfig.publicationCriteria.minimumArticleCountForIssue}개 이하이면서 중요한 내용(${newsletterConfig.publicationCriteria.priorityArticleScoreThreshold}점 이상)이 없을 경우, 다음 호로 이월하여 더 풍성한 내용으로 제공</li>
|
|
2477
|
-
<li style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0; margin-bottom: 8px;"><strong>통합 발행</strong>: 이월된 소식과 새로운 소식을 함께 발행하여 보다 종합적인 업계 동향을 전달</li>
|
|
2478
|
-
</ul>
|
|
2479
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">이러한 정책을 통해 매일 의미 없는 소식으로 독자분들의 시간을 낭비하지 않고, 정말 중요한 정보를 적절한 타이밍에 제공하고자 합니다.</p>
|
|
2480
|
-
<hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
|
|
2481
|
-
<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;">🔍 heripo(헤리포) 플랫폼 소개</h2>
|
|
2482
|
-
${platformIntroHtml()}
|
|
2483
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
|
|
2484
|
-
color: #444444; margin: 0 0 18px 0;">보고 계신 뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>
|
|
2485
|
-
<hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
|
|
2486
|
-
<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 16px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: none;">⚠️ 중요 안내</h2>
|
|
2487
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">본 뉴스레터는 국가유산청 공지사항, 관련 기관 입찰 정보 등 특정 웹 게시판의 모든 신규 소식을 빠짐없이 수집하여 제공합니다. 수집된 모든 정보는 정확한 크롤링 로직에 기반하므로 원본과 일치하여 신뢰할 수 있습니다.</p>
|
|
2488
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">다만, 수집된 정보를 바탕으로 한 <strong>분류, 요약, 분석, 중요도 판단</strong>은 LLM에 의해 수행되었습니다. LLM은 고도로 발전된 기술이지만, 정보를 해석하고 판단하는 과정에서 오류가 발생할 수 있습니다.</p>
|
|
2489
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">따라서 중요한 의사결정 시에는 <strong>반드시 원문 또는 원본 정보 출처를 직접 확인</strong>하시기를 권고합니다.</p>
|
|
2490
|
-
</td>
|
|
2491
|
-
</tr>
|
|
2492
|
-
<tr>
|
|
2493
|
-
<td align="center" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 30px 20px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 1.5; color: #888888;">
|
|
2494
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 10px 0;" class="footer-text">heripo lab | newsletter@heripo.org</p>
|
|
2495
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 10px 0;" class="footer-text">${options?.isKrasNewsletter ? '이 메일은 heripo.app에서 뉴스레터를 구독하신 분들과 한국고고학회 회원에게 발송됩니다.' : '이 메일은 heripo.app에서 리서치 레이더를 구독하신 분들에게 발송됩니다.'}<br>
|
|
2496
|
-
더 이상 이메일을 받고 싶지 않으시면 <a href="{{{RESEND_UNSUBSCRIBE_URL}}}" target="_blank" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-weight: bold; transition: color 0.2s; color: #888888; text-decoration: underline;" class="footer-link">여기에서 수신 거부</a>하세요.</p>
|
|
2497
|
-
${poweredByFooterHtml()}
|
|
2498
|
-
</td>
|
|
2499
|
-
</tr>
|
|
2500
|
-
</table>
|
|
2501
|
-
<!--[if (gte mso 9)|(IE)]>
|
|
2502
|
-
</td>
|
|
2503
|
-
</tr>
|
|
2504
|
-
</table>
|
|
2505
|
-
<![endif]-->
|
|
2506
|
-
</td>
|
|
2507
|
-
</tr>
|
|
2508
|
-
</table>
|
|
2509
|
-
</body>
|
|
2510
|
-
</html>`;
|
|
2511
|
-
|
|
2512
|
-
/**
|
|
2513
|
-
* Content generation provider implementation
|
|
2514
|
-
* - LLM-based newsletter content generation
|
|
2515
|
-
* - HTML template provisioning
|
|
2516
|
-
* - Newsletter persistence
|
|
2517
|
-
*/
|
|
2518
|
-
class ContentGenerateProvider {
|
|
2519
|
-
articleRepository;
|
|
2520
|
-
newsletterRepository;
|
|
2521
|
-
_issueOrder = null;
|
|
2522
|
-
model;
|
|
2523
|
-
/** HTML template with markers for title and content injection */
|
|
2524
|
-
htmlTemplate;
|
|
2525
|
-
/** Newsletter brand name (defaults to config, can be overridden via constructor) */
|
|
2526
|
-
newsletterBrandName;
|
|
2527
|
-
constructor(model, articleRepository, newsletterRepository, templateOptions, brandName) {
|
|
2528
|
-
this.articleRepository = articleRepository;
|
|
2529
|
-
this.newsletterRepository = newsletterRepository;
|
|
2530
|
-
this.model = model;
|
|
2531
|
-
this.newsletterBrandName = brandName ?? newsletterConfig.brandName;
|
|
2532
|
-
this.htmlTemplate = {
|
|
2533
|
-
html: createNewsletterHtmlTemplate(templateOptions),
|
|
2534
|
-
markers: {
|
|
2535
|
-
title: 'NEWSLETTER_TITLE',
|
|
2536
|
-
content: 'NEWSLETTER_CONTENT',
|
|
2537
|
-
},
|
|
2538
|
-
};
|
|
2539
|
-
}
|
|
2540
|
-
/** LLM temperature setting for content generation */
|
|
2541
|
-
temperature = llmConfig.generation.temperature;
|
|
2542
|
-
/** Subscribe page URL */
|
|
2543
|
-
subscribePageUrl = newsletterConfig.subscribePageUrl;
|
|
2544
|
-
/** Publication criteria (minimum article count, priority score threshold) */
|
|
2545
|
-
publicationCriteria = newsletterConfig.publicationCriteria;
|
|
2546
|
-
/**
|
|
2547
|
-
* Get current issue order
|
|
2548
|
-
* @throws Error if issue order not initialized
|
|
2549
|
-
*/
|
|
2550
|
-
get issueOrder() {
|
|
2551
|
-
if (this._issueOrder === null) {
|
|
2552
|
-
throw new Error('issueOrder not initialized. Call initializeIssueOrder() first.');
|
|
2553
|
-
}
|
|
2554
|
-
return this._issueOrder;
|
|
2555
|
-
}
|
|
2556
|
-
/**
|
|
2557
|
-
* Initialize issue order before newsletter generation
|
|
2558
|
-
*/
|
|
2559
|
-
async initializeIssueOrder() {
|
|
2560
|
-
this._issueOrder = await this.newsletterRepository.getNextIssueOrder();
|
|
2561
|
-
}
|
|
2562
|
-
/**
|
|
2563
|
-
* Fetch candidate articles for newsletter generation
|
|
2564
|
-
* @returns Articles eligible for inclusion in the newsletter
|
|
2565
|
-
*/
|
|
2566
|
-
async fetchArticleCandidates() {
|
|
2567
|
-
return this.articleRepository.findCandidatesForNewsletter();
|
|
2568
|
-
}
|
|
2569
|
-
/**
|
|
2570
|
-
* Save generated newsletter to the repository
|
|
2571
|
-
* @param input - Newsletter data and used articles
|
|
2572
|
-
* @returns Saved newsletter ID
|
|
2573
|
-
*/
|
|
2574
|
-
async saveNewsletter(input) {
|
|
2575
|
-
return this.newsletterRepository.saveNewsletter(input);
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
/**
|
|
2580
|
-
* Crawling provider implementation
|
|
2581
|
-
* - Defines crawling targets
|
|
2582
|
-
* - Saves crawling results
|
|
2583
|
-
* - Fetches existing articles
|
|
2584
|
-
*/
|
|
2585
|
-
class CrawlingProvider {
|
|
2586
|
-
articleRepository;
|
|
2587
|
-
/** Maximum number of concurrent crawling operations */
|
|
2588
|
-
maxConcurrency = 5;
|
|
2589
|
-
/** Optional custom fetch function (e.g., proxy-based fetch) */
|
|
2590
|
-
customFetch;
|
|
2591
|
-
/** Crawling target groups configuration */
|
|
2592
|
-
crawlingTargetGroups;
|
|
2593
|
-
constructor(articleRepository, customFetch) {
|
|
2594
|
-
this.articleRepository = articleRepository;
|
|
2595
|
-
this.customFetch = customFetch;
|
|
2596
|
-
this.crawlingTargetGroups = createCrawlingTargetGroups(customFetch);
|
|
2597
|
-
}
|
|
2598
|
-
/**
|
|
2599
|
-
* Fetch existing articles by URLs to avoid duplicate crawling
|
|
2600
|
-
* @param articleUrls - URLs to check
|
|
2601
|
-
* @returns Existing articles
|
|
2602
|
-
*/
|
|
2603
|
-
async fetchExistingArticlesByUrls(articleUrls) {
|
|
2604
|
-
return this.articleRepository.findByUrls(articleUrls);
|
|
2605
|
-
}
|
|
2606
|
-
/**
|
|
2607
|
-
* Save crawled articles to the repository
|
|
2608
|
-
* @param articles - Articles to save
|
|
2609
|
-
* @param context - Task context (task ID, target group, target)
|
|
2610
|
-
* @returns Number of saved articles
|
|
2611
|
-
*/
|
|
2612
|
-
async saveCrawledArticles(articles, context) {
|
|
2613
|
-
return this.articleRepository.saveCrawledArticles(articles, context);
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
/**
|
|
2618
|
-
* Date service implementation
|
|
2619
|
-
* - Provides newsletter publication date strings (ISO and localized display form)
|
|
2620
|
-
* - Always returns Korea Standard Time (KST, Asia/Seoul) regardless of server timezone
|
|
2621
|
-
* - Accepts optional publishDate to override "today" (e.g., generating today's run for tomorrow's publication)
|
|
2622
|
-
*/
|
|
2623
|
-
class DateService {
|
|
2624
|
-
targetDate;
|
|
2625
|
-
/**
|
|
2626
|
-
* @param publishDate - Optional ISO date string (YYYY-MM-DD) representing the newsletter
|
|
2627
|
-
* publication date. If omitted, the current wall-clock time (KST)
|
|
2628
|
-
* is used. Pass tomorrow's date when building the issue the night before.
|
|
2629
|
-
* @throws {Error} If publishDate is not in YYYY-MM-DD format or is not a real calendar date.
|
|
2630
|
-
*/
|
|
2631
|
-
constructor(publishDate) {
|
|
2632
|
-
if (publishDate !== undefined) {
|
|
2633
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(publishDate)) {
|
|
2634
|
-
throw new Error(`Invalid publishDate format: "${publishDate}". Expected YYYY-MM-DD (e.g., "2025-02-12").`);
|
|
2635
|
-
}
|
|
2636
|
-
const date = new Date(publishDate + 'T00:00:00+09:00');
|
|
2637
|
-
// Round-trip check: format back to YYYY-MM-DD in KST and compare.
|
|
2638
|
-
// Catches invalid dates like "2025-02-30" that JS silently normalizes to "2025-03-02".
|
|
2639
|
-
const roundTrip = date.toLocaleDateString('en-CA', {
|
|
2640
|
-
timeZone: 'Asia/Seoul',
|
|
2641
|
-
});
|
|
2642
|
-
if (roundTrip !== publishDate) {
|
|
2643
|
-
throw new Error(`Invalid publishDate: "${publishDate}" is not a real calendar date.`);
|
|
2644
|
-
}
|
|
2645
|
-
this.targetDate = date;
|
|
2646
|
-
}
|
|
2647
|
-
else {
|
|
2648
|
-
this.targetDate = new Date();
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
/**
|
|
2652
|
-
* Get the newsletter publication date in ISO format (YYYY-MM-DD).
|
|
2653
|
-
* - Always returns date in Korea Standard Time (UTC+9)
|
|
2654
|
-
* @returns ISO date string (e.g., "2024-10-15")
|
|
2655
|
-
*/
|
|
2656
|
-
getPublicationISODateString() {
|
|
2657
|
-
// Use Intl.DateTimeFormat to get date in Korea timezone
|
|
2658
|
-
// 'en-CA' locale returns YYYY-MM-DD format by default
|
|
2659
|
-
const kstDate = this.targetDate.toLocaleDateString('en-CA', {
|
|
2660
|
-
timeZone: 'Asia/Seoul',
|
|
2661
|
-
});
|
|
2662
|
-
return kstDate;
|
|
2663
|
-
}
|
|
2664
|
-
getCurrentISODateString() {
|
|
2665
|
-
return this.getPublicationISODateString();
|
|
2666
|
-
}
|
|
2667
|
-
/**
|
|
2668
|
-
* Get the newsletter publication date as a localized display string.
|
|
2669
|
-
* - Always returns date in Korea Standard Time (UTC+9)
|
|
2670
|
-
* @returns Korean formatted date (e.g., "2024년 10월 15일")
|
|
2671
|
-
*/
|
|
2672
|
-
getPublicationDisplayDateString() {
|
|
2673
|
-
const formatter = new Intl.DateTimeFormat('ko-KR', {
|
|
2674
|
-
timeZone: 'Asia/Seoul',
|
|
2675
|
-
year: 'numeric',
|
|
2676
|
-
month: 'long',
|
|
2677
|
-
day: 'numeric',
|
|
2678
|
-
});
|
|
2679
|
-
return formatter.format(this.targetDate);
|
|
2680
|
-
}
|
|
2681
|
-
getDisplayDateString() {
|
|
2682
|
-
return this.getPublicationDisplayDateString();
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
/**
|
|
2687
|
-
* Task service implementation
|
|
2688
|
-
* - Manages newsletter generation task lifecycle (start/end)
|
|
2689
|
-
* - Prevents duplicate execution
|
|
2690
|
-
*/
|
|
2691
|
-
class TaskService {
|
|
2692
|
-
taskRepository;
|
|
2693
|
-
currentTaskId = null;
|
|
2694
|
-
constructor(taskRepository) {
|
|
2695
|
-
this.taskRepository = taskRepository;
|
|
2696
|
-
}
|
|
2697
|
-
/**
|
|
2698
|
-
* Start a new task
|
|
2699
|
-
* @throws Error if a task is already running
|
|
2700
|
-
* @returns Task ID
|
|
2701
|
-
*/
|
|
2702
|
-
async start() {
|
|
2703
|
-
if (this.currentTaskId !== null) {
|
|
2704
|
-
throw new Error('Task is already running');
|
|
2705
|
-
}
|
|
2706
|
-
const taskId = await this.taskRepository.createTask();
|
|
2707
|
-
this.currentTaskId = taskId;
|
|
2708
|
-
return taskId;
|
|
2709
|
-
}
|
|
2710
|
-
/**
|
|
2711
|
-
* End the current task
|
|
2712
|
-
* @throws Error if no task is running
|
|
2713
|
-
*/
|
|
2714
|
-
async end() {
|
|
2715
|
-
if (this.currentTaskId === null) {
|
|
2716
|
-
throw new Error('No task is running');
|
|
2717
|
-
}
|
|
2718
|
-
await this.taskRepository.completeTask(this.currentTaskId);
|
|
2719
|
-
this.currentTaskId = null;
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
/**
|
|
2724
|
-
* Newsletter generator factory function
|
|
2725
|
-
*
|
|
2726
|
-
* @param dependencies - Repository implementations and options
|
|
2727
|
-
* @returns Configured newsletter generator instance
|
|
2728
|
-
*
|
|
2729
|
-
* @example
|
|
2730
|
-
* ```typescript
|
|
2731
|
-
* const generator = createNewsletterGenerator({
|
|
2732
|
-
* openAIApiKey: process.env.OPENAI_API_KEY,
|
|
2733
|
-
* contentGeneration: {
|
|
2734
|
-
* provider: 'anthropic',
|
|
2735
|
-
* apiKey: process.env.ANTHROPIC_API_KEY,
|
|
2736
|
-
* },
|
|
2737
|
-
* taskRepository: new PrismaTaskRepository(prisma),
|
|
2738
|
-
* articleRepository: new PrismaArticleRepository(prisma),
|
|
2739
|
-
* tagRepository: new PrismaTagRepository(prisma),
|
|
2740
|
-
* newsletterRepository: new PrismaNewsletterRepository(prisma),
|
|
2741
|
-
* logger: customLogger, // optional
|
|
2742
|
-
* previewNewsletter: { // optional
|
|
2743
|
-
* fetchNewsletterForPreview: async () => { ... },
|
|
2744
|
-
* emailService: emailService,
|
|
2745
|
-
* emailMessage: { from: '...', to: '...' },
|
|
2746
|
-
* },
|
|
2747
|
-
* });
|
|
2748
|
-
*
|
|
2749
|
-
* const newsletterId = await generator.generate();
|
|
2750
|
-
* ```
|
|
2751
|
-
*/
|
|
2752
|
-
function createContentGenerationModel(config) {
|
|
2753
|
-
switch (config.provider) {
|
|
2754
|
-
case 'openai': {
|
|
2755
|
-
const provider = openai.createOpenAI({ apiKey: config.apiKey });
|
|
2756
|
-
return provider(config.model ?? 'gpt-5.4');
|
|
2757
|
-
}
|
|
2758
|
-
case 'anthropic': {
|
|
2759
|
-
const provider = anthropic.createAnthropic({ apiKey: config.apiKey });
|
|
2760
|
-
return provider(config.model ?? 'claude-sonnet-4-6');
|
|
2761
|
-
}
|
|
2762
|
-
case 'google': {
|
|
2763
|
-
const provider = google.createGoogleGenerativeAI({ apiKey: config.apiKey });
|
|
2764
|
-
return provider(config.model ?? 'gemini-3.1-pro-preview');
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
function createNewsletterGenerator(dependencies) {
|
|
2769
|
-
const openai$1 = openai.createOpenAI({
|
|
2770
|
-
apiKey: dependencies.openAIApiKey,
|
|
2771
|
-
});
|
|
2772
|
-
const dateService = new DateService(dependencies.publishDate);
|
|
2773
|
-
const taskService = new TaskService(dependencies.taskRepository);
|
|
2774
|
-
const crawlingProvider = new CrawlingProvider(dependencies.articleRepository, dependencies.customFetch);
|
|
2775
|
-
const analysisProvider = new AnalysisProvider(openai$1, dependencies.articleRepository, dependencies.tagRepository);
|
|
2776
|
-
// Inject display date from DateService into template options
|
|
2777
|
-
const templateOptions = dependencies.templateOptions
|
|
2778
|
-
? {
|
|
2779
|
-
...dependencies.templateOptions,
|
|
2780
|
-
displayDate: dateService.getPublicationDisplayDateString(),
|
|
2781
|
-
}
|
|
2782
|
-
: undefined;
|
|
2783
|
-
let resolvedContentOptions = { ...contentOptions };
|
|
2784
|
-
let resolvedBrandName = newsletterConfig.brandName;
|
|
2785
|
-
if (templateOptions?.isKrasNewsletter) {
|
|
2786
|
-
resolvedContentOptions = {
|
|
2787
|
-
...resolvedContentOptions,
|
|
2788
|
-
expertField: ['고고학 우선적 문화유산'],
|
|
2789
|
-
freeFormIntro: true,
|
|
2790
|
-
titleContext: templateOptions.titleContext || undefined,
|
|
2791
|
-
};
|
|
2792
|
-
resolvedBrandName = '한국고고학회 뉴스레터';
|
|
2793
|
-
}
|
|
2794
|
-
const contentModel = createContentGenerationModel(dependencies.contentGeneration);
|
|
2795
|
-
const contentGenerateProvider = new ContentGenerateProvider(contentModel, dependencies.articleRepository, dependencies.newsletterRepository, templateOptions, resolvedBrandName);
|
|
2796
|
-
return new core.GenerateNewsletter({
|
|
2797
|
-
contentOptions: resolvedContentOptions,
|
|
2798
|
-
dateService,
|
|
2799
|
-
taskService,
|
|
2800
|
-
crawlingProvider,
|
|
2801
|
-
analysisProvider,
|
|
2802
|
-
contentGenerateProvider,
|
|
2803
|
-
options: {
|
|
2804
|
-
llm: {
|
|
2805
|
-
maxRetries: llmConfig.maxRetries,
|
|
2806
|
-
},
|
|
2807
|
-
chain: {
|
|
2808
|
-
stopAfterAttempt: llmConfig.chainStopAfterAttempt,
|
|
2809
|
-
},
|
|
2810
|
-
logger: dependencies.logger,
|
|
2811
|
-
previewNewsletter: dependencies.previewNewsletter,
|
|
2812
|
-
},
|
|
2813
|
-
});
|
|
2814
|
-
}
|
|
2815
|
-
/**
|
|
2816
|
-
* Newsletter generation execution function
|
|
2817
|
-
*
|
|
2818
|
-
* @param dependencies - Repository implementations and options
|
|
2819
|
-
* @returns Generated newsletter ID
|
|
2820
|
-
*
|
|
2821
|
-
* @example
|
|
2822
|
-
* ```typescript
|
|
2823
|
-
* const newsletterId = await generateNewsletter({
|
|
2824
|
-
* openAIApiKey: process.env.OPENAI_API_KEY,
|
|
2825
|
-
* contentGeneration: {
|
|
2826
|
-
* provider: 'anthropic',
|
|
2827
|
-
* apiKey: process.env.ANTHROPIC_API_KEY,
|
|
2828
|
-
* },
|
|
2829
|
-
* taskRepository: new PrismaTaskRepository(prisma),
|
|
2830
|
-
* articleRepository: new PrismaArticleRepository(prisma),
|
|
2831
|
-
* tagRepository: new PrismaTagRepository(prisma),
|
|
2832
|
-
* newsletterRepository: new PrismaNewsletterRepository(prisma),
|
|
2833
|
-
* });
|
|
2834
|
-
* ```
|
|
2835
|
-
*/
|
|
2836
|
-
async function generateNewsletter(dependencies) {
|
|
2837
|
-
const generator = createNewsletterGenerator(dependencies);
|
|
2838
|
-
// Initialize issueOrder right before calling generate()
|
|
2839
|
-
await generator['contentGenerateProvider'].initializeIssueOrder();
|
|
2840
|
-
return generator.generate();
|
|
2841
|
-
}
|
|
2842
|
-
|
|
2843
|
-
/**
|
|
2844
|
-
* Generates a welcome email HTML string with CSS inlined via juice.
|
|
2845
|
-
*
|
|
2846
|
-
* API is designed to match the original heripo-web `generateWelcomeHTML(id, name)` usage,
|
|
2847
|
-
* with an optional third parameter for KRAS mode and site URL override.
|
|
2848
|
-
*
|
|
2849
|
-
* @param id - Subscriber ID (used for unsubscribe links in default mode)
|
|
2850
|
-
* @param name - Subscriber display name
|
|
2851
|
-
* @param options - Optional configuration for KRAS mode and site URL
|
|
2852
|
-
* @returns Complete HTML string with CSS inlined (ready to send as email)
|
|
2853
|
-
*
|
|
2854
|
-
* @example
|
|
2855
|
-
* ```typescript
|
|
2856
|
-
* // Default heripo branding (same as original heripo-web usage):
|
|
2857
|
-
* const html = await generateWelcomeHTML('subscriber-123', '홍길동');
|
|
2858
|
-
*
|
|
2859
|
-
* // KRAS mode:
|
|
2860
|
-
* const krasHtml = await generateWelcomeHTML('subscriber-123', '홍길동', {
|
|
2861
|
-
* isKrasNewsletter: true,
|
|
2862
|
-
* });
|
|
2863
|
-
* ```
|
|
2864
|
-
*/
|
|
2865
|
-
async function generateWelcomeHTML(id, name, options) {
|
|
2866
|
-
const isKras = options?.isKrasNewsletter ?? false;
|
|
2867
|
-
const siteUrl = options?.siteUrl ?? 'https://heripo.app';
|
|
2868
|
-
const safeName = sanitizeText(name);
|
|
2869
|
-
const unsubscribeUrl = `${siteUrl}/research-radar/unsubscribe?id=${id}`;
|
|
2870
|
-
const { default: juice } = await import('juice');
|
|
2871
|
-
return juice(createWelcomeHtmlRaw(safeName, isKras, siteUrl, unsubscribeUrl));
|
|
2872
|
-
}
|
|
2873
|
-
function createWelcomeHtmlRaw(name, isKras, siteUrl, unsubscribeUrl) {
|
|
2874
|
-
const title = isKras
|
|
2875
|
-
? '한국고고학회 뉴스레터 구독 완료'
|
|
2876
|
-
: 'heripo 리서치 레이더 구독 완료';
|
|
2877
|
-
const headerHtml = isKras
|
|
2878
|
-
? `<!-- KRAS 50주년 헤더 -->
|
|
2879
|
-
<div style="text-align: center; margin-bottom: 36px;">
|
|
2880
|
-
<div style="width: 180px; min-height: 141px; display: inline-block; margin-bottom: 20px;"><img src="https://heripo.app/kras-50.png" width="180" alt="한국고고학회 50주년" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block;" height="auto"></div>
|
|
2881
|
-
<div class="kras-header-title" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 30px; font-weight: bold; color: #111111; line-height: 1.2; margin-bottom: 0;">한국고고학회 뉴스레터</div>
|
|
2882
|
-
</div>
|
|
2883
|
-
<hr class="kras-header-divider" style="border: 0; border-top: 2px solid #D2691E; margin: 0 0 32px 0;">`
|
|
2884
|
-
: `${heripoLogoHtml('8px')}
|
|
2885
|
-
|
|
2886
|
-
<h1 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.2; margin: 0 0
|
|
2887
|
-
18px 0; letter-spacing: -0.5px; margin-top: 0; font-size: 32px; font-weight: bold; color: #111111; border-bottom: 3px solid #D2691E; padding-bottom: 8px;">${name}님, heripo 리서치 레이더에 오신 것을 환영합니다!</h1>`;
|
|
2888
|
-
const feedbackHeading = `${name}님의 목소리가 heripo의 미래를 만듭니다`;
|
|
2889
|
-
const feedbackText = 'heripo';
|
|
2890
|
-
const newsletterLine = isKras
|
|
2891
|
-
? `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>`
|
|
2892
|
-
: `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>`;
|
|
2893
|
-
const warningHtml = isKras
|
|
2894
|
-
? `
|
|
2895
|
-
<blockquote style="background-color: #fef2f2; border-left: 5px solid #dc2626; margin: 24px 0; padding: 20px; border-radius: 4px;">
|
|
2896
|
-
<h3 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: bold; line-height: 1.3; color: #dc2626; margin: 0 0 10px 0; letter-spacing: -0.1px;">⚠️ 본인이 신청하지 않으셨다면</h3>
|
|
2897
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0; margin-bottom: 10px;">만약 본인이 직접 구독 신청을 하지 않으셨다면, 다른 분이 실수로 이메일 주소를 입력했을 가능성이 있습니다. 이 경우 아래 링크를 통해 즉시 수신을 거부하실 수 있습니다.</p>
|
|
2898
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0;"><a href="${unsubscribeUrl}" style="color: #dc2626; font-weight: bold;">🚫 수신 거부하기</a></p>
|
|
2899
|
-
</blockquote>`
|
|
2900
|
-
: `
|
|
2901
|
-
<blockquote style="background-color: #fef2f2; border-left: 5px solid #dc2626; margin: 24px 0; padding: 20px; border-radius: 4px;">
|
|
2902
|
-
<h3 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: bold; line-height: 1.3; color: #dc2626; margin: 0 0 10px 0; letter-spacing: -0.1px;">⚠️ 본인이 신청하지 않으셨다면</h3>
|
|
2903
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0; margin-bottom: 10px;">만약 본인이 직접 구독 신청을 하지 않으셨다면, 다른 분이 실수로 이메일 주소를 입력했을 가능성이 있습니다. 이 경우 아래 링크를 통해 즉시 수신을 거부하실 수 있습니다.</p>
|
|
2904
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0;"><a href="${unsubscribeUrl}" style="color: #dc2626; font-weight: bold;">🚫 수신 거부하기</a></p>
|
|
2905
|
-
</blockquote>`;
|
|
2906
|
-
const footerDisclaimerText = isKras
|
|
2907
|
-
? '이 이메일은 heripo.app에서 한국고고학회 뉴스레터를 구독하신 분들에게 발송됩니다.'
|
|
2908
|
-
: '이 이메일은 heripo.app에서 리서치 레이더를 구독하신 분들에게 발송됩니다.';
|
|
2909
|
-
const footerUnsubscribeHtml = isKras
|
|
2910
|
-
? `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 8px;">📱 구독 관리: <a href="${unsubscribeUrl}" class="footer-link">구독 해지</a></p>`
|
|
2911
|
-
: `<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 8px;">📱 구독 관리: <a href="${unsubscribeUrl}" class="footer-link">구독 해지</a></p>`;
|
|
2912
|
-
return `<!DOCTYPE html>
|
|
2913
|
-
<html lang="ko" style="color-scheme: light dark; supported-color-schemes: light dark;">
|
|
2914
|
-
<head>
|
|
2915
|
-
<meta charset="UTF-8">
|
|
2916
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2917
|
-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
2918
|
-
<meta name="color-scheme" content="light dark">
|
|
2919
|
-
<meta name="supported-color-schemes" content="light dark">
|
|
2920
|
-
<title>${title}</title>
|
|
2921
|
-
<style type="text/css">
|
|
2922
|
-
a:hover {
|
|
2923
|
-
color: #D2691E;
|
|
2924
|
-
}
|
|
2925
|
-
.button-link {
|
|
2926
|
-
color: #fff !important;
|
|
2927
|
-
text-decoration: none !important;
|
|
2928
|
-
font-weight: bold;
|
|
2929
|
-
font-size: 16px;
|
|
2930
|
-
display: inline-block;
|
|
2931
|
-
padding: 10px 28px;
|
|
2932
|
-
border-radius: 4px;
|
|
2933
|
-
background: #D2691E;
|
|
2934
|
-
border: none;
|
|
2935
|
-
}
|
|
2936
|
-
.button-link:hover {
|
|
2937
|
-
background: #b85a1a;
|
|
2938
|
-
}
|
|
2939
|
-
@media screen and (max-width: 700px) {
|
|
2940
|
-
.container {
|
|
2941
|
-
width: 100% !important;
|
|
2942
|
-
max-width: 100% !important;
|
|
2943
|
-
padding: 0 !important;
|
|
2944
|
-
}
|
|
2945
|
-
|
|
2946
|
-
.content-cell {
|
|
2947
|
-
padding: 20px !important;
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
@media screen and (max-width: 600px) {
|
|
2951
|
-
h1 {
|
|
2952
|
-
font-size: 24px !important;
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
h2 {
|
|
2956
|
-
font-size: 20px !important;
|
|
2957
|
-
}
|
|
2958
|
-
}
|
|
2959
|
-
@media (prefers-color-scheme: dark) {
|
|
2960
|
-
body,
|
|
2961
|
-
.dark-mode-bg {
|
|
2962
|
-
background-color: #121212 !important;
|
|
2963
|
-
}
|
|
2964
|
-
|
|
2965
|
-
.dark-mode-content-bg {
|
|
2966
|
-
background-color: #1e1e1e !important;
|
|
2967
|
-
box-shadow: 0 4px 10px rgba(0,0,0,0.25) !important;
|
|
2968
|
-
}
|
|
2969
|
-
|
|
2970
|
-
h1,
|
|
2971
|
-
h2,
|
|
2972
|
-
h3,
|
|
2973
|
-
h4,
|
|
2974
|
-
h5,
|
|
2975
|
-
h6 {
|
|
2976
|
-
color: #FFFFFF !important;
|
|
2977
|
-
}
|
|
2978
|
-
|
|
2979
|
-
h2,
|
|
2980
|
-
h3,
|
|
2981
|
-
h4 {
|
|
2982
|
-
background: #1e1e1e !important;
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
p,
|
|
2986
|
-
li {
|
|
2987
|
-
color: #FFFFFF !important;
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
a:not(.button-link) {
|
|
2991
|
-
color: #4da6ff !important;
|
|
2992
|
-
text-decoration: underline !important;
|
|
2993
|
-
}
|
|
2994
|
-
|
|
2995
|
-
a.button-link {
|
|
2996
|
-
color: #fff !important;
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
blockquote {
|
|
3000
|
-
background-color: #2b2b2b !important;
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
blockquote p {
|
|
3004
|
-
color: #bbbbbb !important;
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
.footer-text {
|
|
3008
|
-
color: #999999 !important;
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
.footer-link {
|
|
3012
|
-
color: #999999 !important;
|
|
3013
|
-
text-decoration: underline !important;
|
|
3014
|
-
}
|
|
3015
|
-
|
|
3016
|
-
.dark-logo {
|
|
3017
|
-
display: block !important;
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
.light-logo {
|
|
3021
|
-
display: none !important;
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
.welcome-notice {
|
|
3025
|
-
background-color: #2b2b2b !important;
|
|
3026
|
-
}
|
|
3027
|
-
|
|
3028
|
-
.header-dark-text {
|
|
3029
|
-
color: #eeeeee !important;
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
.dark-logo-inline {
|
|
3033
|
-
display: inline-block !important;
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
.kras-newsletter .kras-header-title {
|
|
3037
|
-
color: #eeeeee !important;
|
|
3038
|
-
}
|
|
3039
|
-
|
|
3040
|
-
.kras-newsletter .kras-header-date {
|
|
3041
|
-
color: #bbbbbb !important;
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
.kras-newsletter .kras-header-divider {
|
|
3045
|
-
border-top-color: #E59866 !important;
|
|
3046
|
-
}
|
|
3047
|
-
}
|
|
3048
|
-
</style>
|
|
3049
|
-
</head>
|
|
3050
|
-
<body style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f4; font-size: 16px; line-height: 1.7; letter-spacing: 0.01em; height: 100%; width: 100%; margin: 0; padding: 0;">
|
|
3051
|
-
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
|
|
3052
|
-
<tr>
|
|
3053
|
-
<td bgcolor="#f4f4f4" align="center" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 20px 0;" class="dark-mode-bg">
|
|
3054
|
-
<!--[if (gte mso 9)|(IE)]>
|
|
3055
|
-
<table align="center" border="0" cellspacing="0" cellpadding="0" width="700">
|
|
3056
|
-
<tr>
|
|
3057
|
-
<td align="center" valign="top" width="700">
|
|
3058
|
-
<![endif]-->
|
|
3059
|
-
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 700px;" class="container" role="presentation">
|
|
3060
|
-
<tr>
|
|
3061
|
-
<td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg${isKras ? ' kras-newsletter' : ''}" style="-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt; padding: 48px 44px 44px 44px; border-radius: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.07);">
|
|
3062
|
-
${headerHtml}
|
|
3063
|
-
|
|
3064
|
-
<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 15px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: #fff7f2;">💬 ${feedbackHeading}</h2>
|
|
3065
|
-
|
|
3066
|
-
${isKras ? ` <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">${name}님, 한국고고학회 뉴스레터를 구독해주셔서 감사합니다.</p>` : ''}
|
|
3067
|
-
|
|
3068
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong style="color: #D2691E; font-weight: bold;">"이런 기능이 있다면 좋겠다"</strong> 혹은 <strong style="color: #D2691E; font-weight: bold;">"이런 점은 불편하다"</strong>와 같은 의견을 언제든 보내주세요.</p>
|
|
3069
|
-
|
|
3070
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;">여러분의 피드백 하나하나가 ${feedbackText}의 다음 발걸음을 결정합니다.</p>
|
|
3071
|
-
${isKras
|
|
3072
|
-
? `
|
|
3073
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #444444; margin: 0 0 18px 0;"><strong><a href="https://github.com/heripo-lab" target="_blank">heripo lab</a></strong>은 한국고고학회와 함께 뉴스레터 발행 및 고고학의 디지털 전환을 추진하고 있습니다. 앞으로도 연구 현장에 실질적으로 도움이 되는 정보와 기술을 제공해 드리겠습니다.</p>
|
|
3074
|
-
`
|
|
3075
|
-
: ''}
|
|
3076
|
-
<hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 28px 0 20px;">
|
|
3077
|
-
|
|
3078
|
-
<h2 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; font-weight: bold; line-height: 1.3; color: #D2691E; margin: 0 0 15px 0; letter-spacing: -0.2px; border-left: 5px solid #D2691E; padding-left: 12px; background: #fff7f2;">🔍 heripo(헤리포) 플랫폼 소개</h2>
|
|
3079
|
-
${platformIntroHtml()}
|
|
3080
|
-
${newsletterLine}
|
|
3081
|
-
${warningHtml}
|
|
3082
|
-
|
|
3083
|
-
<hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 32px 0;">
|
|
3084
|
-
|
|
3085
|
-
<div style="color: #6b7280; font-size: 14px;" class="footer-text">
|
|
3086
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 8px;">📧 피드백 및 문의: <a href="${siteUrl}/contact" class="footer-link">문의하기</a></p>
|
|
3087
|
-
${footerUnsubscribeHtml}
|
|
3088
|
-
|
|
3089
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 18px 0; margin-bottom: 15px;"><em>정보 검색에 쏟던 시간을 연구와 창의적 기획에 집중하는 시간으로 바꿔보세요.</em></p>
|
|
3090
|
-
|
|
3091
|
-
${poweredByFooterHtml()}
|
|
3092
|
-
|
|
3093
|
-
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 1.7; color: #9ca3af; margin: 0;">${footerDisclaimerText}<br>더 이상 이메일을 받고 싶지 않으시면 <a href="${unsubscribeUrl}" target="_blank" style="color: #888888; text-decoration: underline;" class="footer-link">여기에서 수신 거부</a>하세요.</p>
|
|
3094
|
-
</div>
|
|
3095
|
-
</td>
|
|
3096
|
-
</tr>
|
|
3097
|
-
</table>
|
|
3098
|
-
<!--[if (gte mso 9)|(IE)]>
|
|
3099
|
-
</td>
|
|
3100
|
-
</tr>
|
|
3101
|
-
</table>
|
|
3102
|
-
<![endif]-->
|
|
3103
|
-
</td>
|
|
3104
|
-
</tr>
|
|
3105
|
-
</table>
|
|
3106
|
-
</body>
|
|
3107
|
-
</html>`;
|
|
3108
|
-
}
|
|
3109
|
-
|
|
3110
|
-
exports.AnalysisProvider = AnalysisProvider;
|
|
3111
|
-
exports.ContentGenerateProvider = ContentGenerateProvider;
|
|
3112
|
-
exports.CrawlingProvider = CrawlingProvider;
|
|
3113
|
-
exports.DateService = DateService;
|
|
3114
|
-
exports.TaskService = TaskService;
|
|
3115
|
-
exports.contentOptions = contentOptions;
|
|
3116
|
-
exports.createCrawlingTargetGroups = createCrawlingTargetGroups;
|
|
3117
|
-
exports.generateNewsletter = generateNewsletter;
|
|
3118
|
-
exports.generateWelcomeHTML = generateWelcomeHTML;
|
|
3119
|
-
exports.getSourceList = getSourceList;
|
|
3120
|
-
exports.llmConfig = llmConfig;
|
|
3121
|
-
exports.newsletterConfig = newsletterConfig;
|
|
3122
|
-
//# sourceMappingURL=index.cjs.map
|