@heripo/research-radar 2.1.0 → 2.3.0

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/dist/index.cjs CHANGED
@@ -5,6 +5,10 @@ var openai = require('@ai-sdk/openai');
5
5
  var core = require('@llm-newsletter-kit/core');
6
6
  var cheerio = require('cheerio');
7
7
  var TurndownService = require('turndown');
8
+ var jsdom = require('jsdom');
9
+ var safeMarkdown2Html = require('safe-markdown2html');
10
+ var DOMPurify = require('dompurify');
11
+ var juice = require('juice');
8
12
 
9
13
  function _interopNamespaceDefault(e) {
10
14
  var n = Object.create(null);
@@ -648,6 +652,33 @@ const parseKhsLawList = (html) => {
648
652
  });
649
653
  return posts;
650
654
  };
655
+ const parseKhsTenderList = (html) => {
656
+ const $ = cheerio__namespace.load(html);
657
+ const posts = [];
658
+ const baseUrl = 'https://www.khs.go.kr';
659
+ $('table.b_list tbody tr').each((index, element) => {
660
+ const columns = $(element).find('td');
661
+ if (columns.length === 0)
662
+ return;
663
+ const titleElement = columns.eq(1).find('a.b_tit');
664
+ const relativeHref = titleElement.attr('href');
665
+ if (!relativeHref)
666
+ return;
667
+ const fullUrl = new URL(relativeHref, baseUrl);
668
+ const detailUrl = fullUrl.href;
669
+ const uniqId = fullUrl.searchParams.get('id') ?? undefined;
670
+ const title = titleElement.text().trim();
671
+ const date = getDate(columns.eq(4).text().trim());
672
+ posts.push({
673
+ uniqId,
674
+ title,
675
+ date,
676
+ detailUrl: cleanUrl(detailUrl),
677
+ dateType: core.DateType.REGISTERED,
678
+ });
679
+ });
680
+ return posts;
681
+ };
651
682
  const parseKhsDetail = async (html) => {
652
683
  const $ = cheerio__namespace.load(html);
653
684
  const content = $('div.b_content');
@@ -992,9 +1023,9 @@ const getRandomUserAgent = () => USER_AGENTS[Math.floor(Math.random() * USER_AGE
992
1023
  * Uses internal API since the table is rendered via CSR
993
1024
  * @see https://www.yngogo.or.kr
994
1025
  */
995
- const parseYngogoList = async (_html, menuSeq, bbsSeq, sitecntntsSeq) => {
1026
+ const parseYngogoList = async (_html, menuSeq, bbsSeq, sitecntntsSeq, customFetch) => {
996
1027
  // Fetch from internal API (CSR workaround)
997
- const response = await fetch(LIST_API_URL, {
1028
+ const response = await (customFetch ?? fetch)(LIST_API_URL, {
998
1029
  method: 'POST',
999
1030
  headers: {
1000
1031
  'Content-Type': 'application/x-www-form-urlencoded',
@@ -1041,9 +1072,9 @@ const parseYngogoList = async (_html, menuSeq, bbsSeq, sitecntntsSeq) => {
1041
1072
  * Parse detail page from 영남고고학회 (Yeongnam Archaeological Society)
1042
1073
  * Uses internal API since the detail page is rendered via CSR
1043
1074
  */
1044
- const parseYngogoDetail = async (_html, menuSeq, bbsSeq, nttSeq, sitecntntsSeq) => {
1075
+ const parseYngogoDetail = async (_html, menuSeq, bbsSeq, nttSeq, sitecntntsSeq, customFetch) => {
1045
1076
  // Fetch from internal API (CSR workaround)
1046
- const response = await fetch(DETAIL_API_URL, {
1077
+ const response = await (customFetch ?? fetch)(DETAIL_API_URL, {
1047
1078
  method: 'POST',
1048
1079
  headers: {
1049
1080
  'Content-Type': 'application/x-www-form-urlencoded',
@@ -1094,496 +1125,498 @@ function extractNttSeq(html) {
1094
1125
  return match?.[1] ?? '';
1095
1126
  }
1096
1127
 
1097
- const crawlingTargetGroups = [
1098
- {
1099
- id: 'news',
1100
- name: 'News',
1101
- targets: [
1102
- {
1103
- id: '국가유산청_공지사항',
1104
- name: '국가유산청 공지사항',
1105
- url: 'https://www.khs.go.kr/multiBbz/selectMultiBbzList.do?bbzId=newpublic&mn=NS_01_01',
1106
- parseList: parseKhsList,
1107
- parseDetail: parseKhsDetail,
1108
- },
1109
- {
1110
- id: '국가유산청_보도설명',
1111
- name: '국가유산청 보도/설명',
1112
- url: 'https://www.khs.go.kr/newsBbz/selectNewsBbzList.do?sectionId=all_sec_1&mn=NS_01_02',
1113
- parseList: parseKhsList,
1114
- parseDetail: parseKhsDetail,
1115
- },
1116
- {
1117
- id: '국가유산청_사진뉴스',
1118
- name: '국가유산청 사진뉴스',
1119
- url: 'https://www.khs.go.kr/cop/bbs/selectBoardList.do?bbsId=BBSMSTR_1002&mn=NS_01_03',
1120
- parseList: parseKhsGalleryList,
1121
- parseDetail: parseKhsDetail,
1122
- },
1123
- {
1124
- id: '국가유산청_입법예고',
1125
- name: '국가유산청 입법예고',
1126
- url: 'https://www.khs.go.kr/lawBbz/selectLawBbzList.do?mn=NS_03_01_01',
1127
- parseList: parseKhsLawList,
1128
- parseDetail: parseKhsDetail,
1129
- },
1130
- // NOTE: Parsing logic is implemented, but too much fragmented data with little value for newsletter
1131
- // {
1132
- // id: '국가유산청_발굴조사_현황공개',
1133
- // name: '국가유산청 발굴조사 현황공개',
1134
- // url: 'https://www.e-minwon.go.kr/ge/ee/getListEcexmPrmsnAply.do',
1135
- // parseList: parseExcavationStatusList,
1136
- // parseDetail: parseExcavationStatusDetail,
1137
- // },
1138
- {
1139
- id: '국가유산청_발굴조사_보고서',
1140
- name: '국가유산청 발굴조사 보고서',
1141
- url: 'https://www.e-minwon.go.kr/ge/ee/getListEcexmRptp.do',
1142
- parseList: parseExcavationReportList,
1143
- parseDetail: parseExcavationReportDetail,
1144
- },
1145
- {
1146
- id: '국가유산청_발굴조사_현장공개',
1147
- name: '국가유산청 발굴조사 현장공개',
1148
- url: 'https://www.e-minwon.go.kr/ge/ee/getListLinkGrndsRls.do',
1149
- parseList: parseExcavationSiteList,
1150
- parseDetail: parseExcavationSiteDetail,
1151
- },
1152
- {
1153
- id: '국립문화유산연구원_공지사항',
1154
- name: '국립문화유산연구원 공지사항',
1155
- url: 'https://www.nrich.go.kr/kor/boardList.do?menuIdx=282&bbscd=32',
1156
- parseList: parseNrichNoticeList,
1157
- parseDetail: parseNrichNoticeDetail,
1158
- },
1159
- {
1160
- id: '국립문화유산연구원_주요행사',
1161
- name: '국립문화유산연구원 주요행사',
1162
- url: 'https://www.nrich.go.kr/kor/majorList.do?menuIdx=286',
1163
- parseList: parseNrichMajorEventList,
1164
- parseDetail: parseNrichMajorEventDetail,
1165
- },
1166
- {
1167
- id: '국립문화유산연구원_학술지_헤리티지',
1168
- name: '국립문화유산연구원 헤리티지:역사와 과학 학술지',
1169
- url: 'https://www.nrich.go.kr/kor/subscriptionDataUsrList.do?menuIdx=1651&gubun=J',
1170
- parseList: parseNrichJournalList,
1171
- parseDetail: parseNrichJournalDetail,
1172
- },
1173
- {
1174
- id: '국립문화유산연구원_학술지_보존과학연구',
1175
- name: '국립문화유산연구원 보존과학연구 학술지',
1176
- url: 'https://www.nrich.go.kr/kor/subscriptionDataUsrList.do?menuIdx=2065&gubun=K',
1177
- parseList: parseNrichJournalList,
1178
- parseDetail: parseNrichJournalDetail,
1179
- },
1180
- {
1181
- id: '국가유산지식이음_공지사항',
1182
- name: '국가유산 지식이음 공지사항',
1183
- url: 'https://portal.nrich.go.kr/kor/boardList.do?menuIdx=1058&bbscd=9',
1184
- parseList: parseNrichPortalList,
1185
- parseDetail: parseNrichPortalDetail,
1186
- },
1187
- {
1188
- id: '국립고궁박물관_공지사항',
1189
- name: '국립고궁박물관 공지사항',
1190
- url: 'https://www.gogung.go.kr/gogung/bbs/BMSR00022/list.do?gubunCd=B22_001&menuNo=800088',
1191
- parseList: parseGogungList,
1192
- parseDetail: parseGogungDetail,
1193
- },
1194
- {
1195
- id: '국가유산진흥원_공지사항',
1196
- name: '국가유산진흥원 공지사항',
1197
- url: 'https://www.kh.or.kr/brd/board/644/L/SITES/100/menu/371',
1198
- parseList: parseHeritageAgencyList,
1199
- parseDetail: parseHeritageAgencyDetail,
1200
- },
1201
- {
1202
- id: '국가유산진흥원_보도자료',
1203
- name: '국가유산진흥원 보도자료',
1204
- url: 'https://www.kh.or.kr/brd/board/715/L/menu/373',
1205
- parseList: parseHeritageAgencyList,
1206
- parseDetail: parseHeritageAgencyDetail,
1207
- },
1208
- {
1209
- id: '국가유산진흥원_매장유산국비발굴단_공지사항',
1210
- name: '국가유산진흥원 매장유산국비발굴단 공지사항',
1211
- url: 'https://www.kh.or.kr/brd/board/644/L/SITES/201/menu/506',
1212
- parseList: parseHeritageAgencyList,
1213
- parseDetail: parseHeritageAgencyDetail,
1214
- },
1215
- {
1216
- id: '국가유산진흥원_매장유산국비발굴단_현장설명회',
1217
- name: '국가유산진흥원 매장유산국비발굴단 현장설명회',
1218
- url: 'https://www.kh.or.kr/brd/board/631/L/menu/504',
1219
- parseList: parseHeritageAgencyList,
1220
- parseDetail: parseHeritageAgencyDetail,
1221
- },
1222
- {
1223
- id: '한국문화유산협회_공지사항',
1224
- name: '한국문화유산협회 공지사항',
1225
- url: 'https://www.kaah.kr/notice',
1226
- parseList: parseKaahList,
1227
- parseDetail: parseKaahDetail,
1228
- },
1229
- {
1230
- id: '한국문화유산협회_협회소식',
1231
- name: '한국문화유산협회 협회소식',
1232
- url: 'https://www.kaah.kr/news',
1233
- parseList: parseKaahList,
1234
- parseDetail: parseKaahDetail,
1235
- },
1236
- {
1237
- id: '한국문화유산협회_보도자료',
1238
- name: '한국문화유산협회 보도자료',
1239
- url: 'https://www.kaah.kr/mass',
1240
- parseList: parseKaahList,
1241
- parseDetail: parseKaahDetail,
1242
- },
1243
- {
1244
- id: '한국문화유산협회_회원기관소식',
1245
- name: '한국문화유산협회 회원기관소식',
1246
- url: 'https://www.kaah.kr/assnews',
1247
- parseList: parseKaahList,
1248
- parseDetail: parseKaahDetail,
1249
- },
1250
- {
1251
- id: '한국문화유산협회_유관기관소식',
1252
- name: '한국문화유산협회 유관기관소식',
1253
- url: 'https://www.kaah.kr/ralnews',
1254
- parseList: parseKaahList,
1255
- parseDetail: parseKaahDetail,
1256
- },
1257
- {
1258
- id: '한국문화유산협회_발굴현장공개',
1259
- name: '한국문화유산협회 발굴현장공개',
1260
- url: 'https://www.kaah.kr/placeopen',
1261
- parseList: parseKaahPlaceList,
1262
- parseDetail: parseKaahPlaceDetail,
1263
- },
1264
- {
1265
- id: '한국고고학회_공지사항',
1266
- name: '한국고고학회 공지사항',
1267
- url: 'https://www.kras.or.kr/?r=kras&m=bbs&bid=notice',
1268
- parseList: parseKrasList,
1269
- parseDetail: parseKrasDetail,
1270
- },
1271
- {
1272
- id: '한국고고학회_학술대회및행사',
1273
- name: '한국고고학회 학술대회 및 행사',
1274
- url: 'https://www.kras.or.kr/?r=kras&m=bbs&bid=sympo',
1275
- parseList: parseKrasList,
1276
- parseDetail: parseKrasDetail,
1277
- },
1278
- {
1279
- id: '한국고고학회_신간안내_단행본',
1280
- name: '한국고고학회 신간안내 - 단행본',
1281
- url: 'https://www.kras.or.kr/?c=61/101/105',
1282
- parseList: parseKrasList,
1283
- parseDetail: parseKrasDetail,
1284
- },
1285
- {
1286
- id: '한국고고학회_현장소식',
1287
- name: '한국고고학회 현장소식',
1288
- url: 'https://www.kras.or.kr/?c=61/73',
1289
- parseList: parseKrasList,
1290
- parseDetail: parseKrasDetail,
1291
- },
1292
- {
1293
- id: '중부고고학회_공지사항',
1294
- name: '중부고고학회 공지사항',
1295
- url: 'https://www.jbgogo.or.kr/bbs/notice',
1296
- parseList: parseJbgogoList,
1297
- parseDetail: parseJbgogoDetail,
1298
- },
1299
- {
1300
- id: '중부고고학회_학계소식',
1301
- name: '중부고고학회 학계소식',
1302
- url: 'https://www.jbgogo.or.kr/bbs/news',
1303
- parseList: parseJbgogoList,
1304
- parseDetail: parseJbgogoDetail,
1305
- },
1306
- {
1307
- id: '중부고고학회_발굴현장소식',
1308
- name: '중부고고학회 발굴현장소식',
1309
- url: 'https://www.jbgogo.or.kr/bbs/spotnews',
1310
- parseList: parseJbgogoList,
1311
- parseDetail: parseJbgogoDetail,
1312
- },
1313
- {
1314
- id: '호서고고학회_공지사항',
1315
- name: '호서고고학회 공지사항',
1316
- url: 'http://www.hsas.or.kr/flow/?ref=board/board.emt&menu_table=m2_00&bbs_table=notice&menu_idx=010000',
1317
- parseList: parseHsasList,
1318
- parseDetail: parseHsasDetail,
1319
- },
1320
- {
1321
- id: '호서고고학회_학회소식',
1322
- name: '호서고고학회 학회소식',
1323
- url: 'http://www.hsas.or.kr/flow/?ref=board/board.emt&menu_table=m2_00&bbs_table=m2_01&menu_idx=020000',
1324
- parseList: parseHsasList,
1325
- parseDetail: parseHsasDetail,
1326
- },
1327
- {
1328
- id: '영남고고학회_공지사항',
1329
- name: '영남고고학회 공지사항',
1330
- url: 'http://www.yngogo.or.kr/subList/32000001120',
1331
- parseList: (html) => parseYngogoList(html, '32000001120', '32000001157', '32000001711'),
1332
- parseDetail: (html) => parseYngogoDetail(html, '32000001120', '32000001157', extractNttSeq(html), '32000001711'),
1333
- },
1334
- {
1335
- id: '영남고고학회_학계소식',
1336
- name: '영남고고학회 학계소식',
1337
- url: 'http://www.yngogo.or.kr/subList/32000001133',
1338
- parseList: (html) => parseYngogoList(html, '32000001133', '32000001161', '32000001715'),
1339
- parseDetail: (html) => parseYngogoDetail(html, '32000001133', '32000001161', extractNttSeq(html), '32000001715'),
1340
- },
1341
- {
1342
- id: '영남고고학회_현장소식',
1343
- name: '영남고고학회 현장소식',
1344
- url: 'http://www.yngogo.or.kr/subList/32000001135',
1345
- parseList: (html) => parseYngogoList(html, '32000001135', '32000001163', '32000001717'),
1346
- parseDetail: (html) => parseYngogoDetail(html, '32000001135', '32000001163', extractNttSeq(html), '32000001717'),
1347
- },
1348
- {
1349
- id: '국립중앙박물관_알림',
1350
- name: '국립중앙박물관 알림',
1351
- url: 'https://www.museum.go.kr/MUSEUM/contents/M0701010000.do?catCustomType=united&catId=128',
1352
- parseList: (html) => parseMuseumList(html, '/MUSEUM/contents/M0701010000.do'),
1353
- parseDetail: parseMuseumDetail,
1354
- },
1355
- {
1356
- id: '국립중앙박물관_고시공고',
1357
- name: '국립중앙박물관 고시/공고',
1358
- url: 'https://www.museum.go.kr/MUSEUM/contents/M0701020000.do',
1359
- parseList: (html) => parseMuseumList(html, '/MUSEUM/contents/M0701020000.do'),
1360
- parseDetail: parseMuseumDetail,
1361
- },
1362
- {
1363
- id: '국립중앙박물관_보도자료',
1364
- name: '국립중앙박물관 보도 자료',
1365
- url: 'https://www.museum.go.kr/MUSEUM/contents/M0701040000.do?catCustomType=post&catId=93',
1366
- parseList: parseMuseumPressList,
1367
- parseDetail: parseMuseumDetail,
1368
- },
1369
- {
1370
- id: '국립전주박물관_새소식',
1371
- name: '국립전주박물관 새소식',
1372
- url: 'https://jeonju.museum.go.kr/board.es?mid=a10105010000&bid=0001',
1373
- parseList: parseJeonjuMuseumList,
1374
- parseDetail: parseJeonjuMuseumDetail,
1375
- },
1376
- {
1377
- id: '국립전주박물관_보도자료',
1378
- name: '국립전주박물관 보도자료',
1379
- url: 'https://jeonju.museum.go.kr/board.es?mid=a10105050000&bid=0004',
1380
- parseList: parseJeonjuMuseumList,
1381
- parseDetail: parseJeonjuMuseumDetail,
1382
- },
1383
- {
1384
- id: '국립부여박물관_공지사항',
1385
- name: '국립부여박물관 공지사항',
1386
- url: 'https://buyeo.museum.go.kr/bbs/list.do?key=2301250005',
1387
- parseList: (html) => parseBuyeoMuseumList(html, '2301250005'),
1388
- parseDetail: parseBuyeoMuseumDetail,
1389
- },
1390
- {
1391
- id: '국립부여박물관_보도자료',
1392
- name: '국립부여박물관 보도자료',
1393
- url: 'https://buyeo.museum.go.kr/bbs/list.do?key=2302150024',
1394
- parseList: (html) => parseBuyeoMuseumList(html, '2302150024'),
1395
- parseDetail: parseBuyeoMuseumDetail,
1396
- },
1397
- {
1398
- id: '국립진주박물관_새소식',
1399
- name: '국립진주박물관 새소식',
1400
- url: 'https://jinju.museum.go.kr/kor/html/sub06/0601.html',
1401
- parseList: parseJinjuMuseumList,
1402
- parseDetail: parseJinjuMuseumDetail,
1403
- },
1404
- // NOTE: Parsing logic is implemented, but crawling is restricted by robots.txt policy
1405
- // {
1406
- // id: '국립경주박물관_새소식',
1407
- // name: '국립경주박물관 새소식',
1408
- // url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0701.html',
1409
- // parseList: (html) =>
1410
- // parseGyeongjuMuseumList(html, '/kor/html/sub07/0701.html'),
1411
- // parseDetail: parseGyeongjuMuseumDetail,
1412
- // },
1413
- // {
1414
- // id: '국립경주박물관_고시공고',
1415
- // name: '국립경주박물관 고시/공고',
1416
- // url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0703.html',
1417
- // parseList: parseGyeongjuMuseumNoticeList,
1418
- // parseDetail: parseGyeongjuMuseumDetail,
1419
- // },
1420
- // {
1421
- // id: '국립경주박물관_보도자료',
1422
- // name: '국립경주박물관 보도자료',
1423
- // url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0705.html',
1424
- // parseList: (html) =>
1425
- // parseGyeongjuMuseumList(html, '/kor/html/sub07/0705.html'),
1426
- // parseDetail: parseGyeongjuMuseumDetail,
1427
- // },
1428
- // {
1429
- // id: '국립청주박물관_새소식',
1430
- // name: '국립청주박물관 새소식',
1431
- // url: 'https://cheongju.museum.go.kr/www/selectBbsNttList.do?bbsNo=1&key=482&nbar=s',
1432
- // parseList: parseCheongjuMuseumList,
1433
- // parseDetail: parseCheongjuMuseumDetail,
1434
- // },
1435
- // {
1436
- // id: '국립청주박물관_언론보도자료',
1437
- // name: '국립청주박물관 언론보도자료',
1438
- // url: 'https://cheongju.museum.go.kr/www/selectBbsNttList.do?bbsNo=20&key=31&nbar=s',
1439
- // parseList: parseCheongjuMuseumList,
1440
- // parseDetail: parseCheongjuMuseumDetail,
1441
- // },
1442
- // {
1443
- // id: '국립김해박물관_새소식',
1444
- // name: '국립김해박물관 새소식',
1445
- // url: 'https://gimhae.museum.go.kr/kr/html/sub04/0401.html',
1446
- // parseList: (html) =>
1447
- // parseGimhaeMuseumList(html, '/kr/html/sub04/0401.html'),
1448
- // parseDetail: parseGimhaeMuseumDetail,
1449
- // },
1450
- // {
1451
- // id: '국립김해박물관_보도자료',
1452
- // name: '국립김해박물관 언론보도자료',
1453
- // url: 'https://gimhae.museum.go.kr/kr/html/sub04/0402.html',
1454
- // parseList: (html) =>
1455
- // parseGimhaeMuseumList(html, '/kr/html/sub04/0402.html'),
1456
- // parseDetail: parseGimhaeMuseumDetail,
1457
- // },
1458
- // {
1459
- // id: '국립제주박물관_새소식',
1460
- // name: '국립제주박물관 새소식',
1461
- // url: 'https://jeju.museum.go.kr/_prog/_board/?code=sub02_0201&site_dvs_cd=kr&menu_dvs_cd=050101&ntt_tag=1',
1462
- // parseList: parseJejuMuseumList,
1463
- // parseDetail: parseJejuMuseumDetail,
1464
- // },
1465
- // {
1466
- // id: '국립익산박물관_공지사항',
1467
- // name: '국립익산박물관 공지사항',
1468
- // url: 'https://iksan.museum.go.kr/kor/html/sub05/0501.html',
1469
- // parseList: parseIksanMuseumList,
1470
- // parseDetail: parseIksanMuseumDetail,
1471
- // },
1472
- ],
1473
- },
1474
- {
1475
- id: 'business',
1476
- name: 'Business',
1477
- targets: [
1478
- {
1479
- id: '국가유산청_입찰정보',
1480
- name: '국가유산청 입찰정보',
1481
- url: 'https://www.khs.go.kr/tenderBbz/selectTenderBbzList.do?mn=NS_01_05',
1482
- parseList: parseKhsList,
1483
- parseDetail: parseKhsDetail,
1484
- },
1485
- {
1486
- id: '국가유산진흥원_입찰정보',
1487
- name: '국가유산진흥원 입찰정보',
1488
- url: 'https://www.kh.or.kr/brd/board/717/L/menu/375',
1489
- parseList: parseHeritageAgencyList,
1490
- parseDetail: parseHeritageAgencyDetail,
1491
- },
1492
- {
1493
- id: '한국문화유산협회_사업공고',
1494
- name: '한국문화유산협회 사업공고',
1495
- url: 'https://www.kaah.kr/bussopen',
1496
- parseList: parseKaahList,
1497
- parseDetail: parseKaahDetail,
1498
- },
1499
- {
1500
- id: '한국문화유산협회_입찰공고',
1501
- name: '한국문화유산협회 입찰공고',
1502
- url: 'https://www.kaah.kr/ipcopen',
1503
- parseList: parseKaahList,
1504
- parseDetail: parseKaahDetail,
1505
- },
1506
- ],
1507
- },
1508
- {
1509
- id: 'employment',
1510
- name: 'Employment',
1511
- targets: [
1512
- {
1513
- id: '국가유산청_시험채용',
1514
- name: '국가유산청 시험/채용',
1515
- url: 'https://www.khs.go.kr/multiBbz/selectMultiBbzList.do?bbzId=newexam&mn=NS_01_06',
1516
- parseList: parseKhsList,
1517
- parseDetail: parseKhsDetail,
1518
- },
1519
- {
1520
- id: '국가유산진흥원_인재채용',
1521
- name: '국가유산진흥원 인재채용',
1522
- url: 'https://www.kh.or.kr/brd/board/721/L/CATEGORY/719/menu/377',
1523
- parseList: parseHeritageAgencyList,
1524
- parseDetail: parseHeritageAgencyDetail,
1525
- },
1526
- {
1527
- id: '한국문화유산협회_채용공고',
1528
- name: '한국문화유산협회 채용공고',
1529
- url: 'https://www.kaah.kr/reqopen',
1530
- parseList: parseKaahList,
1531
- parseDetail: parseKaahDetail,
1532
- },
1533
- {
1534
- id: '영남고고학회_채용공고',
1535
- name: '영남고고학회 채용공고',
1536
- url: 'http://www.yngogo.or.kr/subList/32000001136',
1537
- parseList: (html) => parseYngogoList(html, '32000001136', '32000001164', '32000001718'),
1538
- parseDetail: (html) => parseYngogoDetail(html, '32000001136', '32000001164', extractNttSeq(html), '32000001718'),
1539
- },
1540
- {
1541
- id: '국립중앙박물관_채용안내',
1542
- name: '국립중앙박물관 채용 안내',
1543
- url: 'https://www.museum.go.kr/MUSEUM/contents/M0701030000.do?catCustomType=post&catId=54&recruitYn=Y',
1544
- parseList: parseMuseumRecruitList,
1545
- parseDetail: parseMuseumDetail,
1546
- },
1547
- {
1548
- id: '국립전주박물관_채용',
1549
- name: '국립전주박물관 채용',
1550
- url: 'https://jeonju.museum.go.kr/board.es?mid=a10105020000&bid=0002',
1551
- parseList: parseJeonjuMuseumRecruitList,
1552
- parseDetail: parseJeonjuMuseumDetail,
1553
- },
1554
- {
1555
- id: '국립부여박물관_채용공고',
1556
- name: '국립부여박물관 채용공고',
1557
- url: 'https://buyeo.museum.go.kr/bbs/list.do?key=2301270001',
1558
- parseList: (html) => parseBuyeoMuseumList(html, '2301270001'),
1559
- parseDetail: parseBuyeoMuseumDetail,
1560
- },
1561
- // NOTE: Parsing logic is implemented, but crawling is restricted by robots.txt policy
1562
- // {
1563
- // id: '국립경주박물관_채용안내',
1564
- // name: '국립경주박물관 채용안내',
1565
- // url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0704.html',
1566
- // parseList: (html) =>
1567
- // parseGyeongjuMuseumList(html, '/kor/html/sub07/0704.html'),
1568
- // parseDetail: parseGyeongjuMuseumDetail,
1569
- // },
1570
- // {
1571
- // id: '국립청주박물관_채용및공고',
1572
- // name: '국립청주박물관 채용 및 공고',
1573
- // url: 'https://cheongju.museum.go.kr/www/selectBbsNttList.do?bbsNo=29&key=476&nbar=s',
1574
- // parseList: parseCheongjuMuseumList,
1575
- // parseDetail: parseCheongjuMuseumDetail,
1576
- // },
1577
- // {
1578
- // id: '국립제주박물관_채용정보',
1579
- // name: '국립제주박물관 채용정보',
1580
- // url: 'https://jeju.museum.go.kr/_prog/_board/?code=sub02_0201&site_dvs_cd=kr&menu_dvs_cd=050102&ntt_tag=2',
1581
- // parseList: parseJejuMuseumList,
1582
- // parseDetail: parseJejuMuseumDetail,
1583
- // },
1584
- ],
1585
- },
1586
- ];
1128
+ function createCrawlingTargetGroups(customFetch) {
1129
+ return [
1130
+ {
1131
+ id: 'news',
1132
+ name: 'News',
1133
+ targets: [
1134
+ {
1135
+ id: '국가유산청_공지사항',
1136
+ name: '국가유산청 공지사항',
1137
+ url: 'https://www.khs.go.kr/multiBbz/selectMultiBbzList.do?bbzId=newpublic&mn=NS_01_01',
1138
+ parseList: parseKhsList,
1139
+ parseDetail: parseKhsDetail,
1140
+ },
1141
+ {
1142
+ id: '국가유산청_보도설명',
1143
+ name: '국가유산청 보도/설명',
1144
+ url: 'https://www.khs.go.kr/newsBbz/selectNewsBbzList.do?sectionId=all_sec_1&mn=NS_01_02',
1145
+ parseList: parseKhsList,
1146
+ parseDetail: parseKhsDetail,
1147
+ },
1148
+ {
1149
+ id: '국가유산청_사진뉴스',
1150
+ name: '국가유산청 사진뉴스',
1151
+ url: 'https://www.khs.go.kr/cop/bbs/selectBoardList.do?bbsId=BBSMSTR_1002&mn=NS_01_03',
1152
+ parseList: parseKhsGalleryList,
1153
+ parseDetail: parseKhsDetail,
1154
+ },
1155
+ {
1156
+ id: '국가유산청_입법예고',
1157
+ name: '국가유산청 입법예고',
1158
+ url: 'https://www.khs.go.kr/lawBbz/selectLawBbzList.do?mn=NS_03_01_01',
1159
+ parseList: parseKhsLawList,
1160
+ parseDetail: parseKhsDetail,
1161
+ },
1162
+ // NOTE: Parsing logic is implemented, but too much fragmented data with little value for newsletter
1163
+ // {
1164
+ // id: '국가유산청_발굴조사_현황공개',
1165
+ // name: '국가유산청 발굴조사 현황공개',
1166
+ // url: 'https://www.e-minwon.go.kr/ge/ee/getListEcexmPrmsnAply.do',
1167
+ // parseList: parseExcavationStatusList,
1168
+ // parseDetail: parseExcavationStatusDetail,
1169
+ // },
1170
+ {
1171
+ id: '국가유산청_발굴조사_보고서',
1172
+ name: '국가유산청 발굴조사 보고서',
1173
+ url: 'https://www.e-minwon.go.kr/ge/ee/getListEcexmRptp.do',
1174
+ parseList: parseExcavationReportList,
1175
+ parseDetail: parseExcavationReportDetail,
1176
+ },
1177
+ {
1178
+ id: '국가유산청_발굴조사_현장공개',
1179
+ name: '국가유산청 발굴조사 현장공개',
1180
+ url: 'https://www.e-minwon.go.kr/ge/ee/getListLinkGrndsRls.do',
1181
+ parseList: parseExcavationSiteList,
1182
+ parseDetail: parseExcavationSiteDetail,
1183
+ },
1184
+ {
1185
+ id: '국립문화유산연구원_공지사항',
1186
+ name: '국립문화유산연구원 공지사항',
1187
+ url: 'https://www.nrich.go.kr/kor/boardList.do?menuIdx=282&bbscd=32',
1188
+ parseList: parseNrichNoticeList,
1189
+ parseDetail: parseNrichNoticeDetail,
1190
+ },
1191
+ {
1192
+ id: '국립문화유산연구원_주요행사',
1193
+ name: '국립문화유산연구원 주요행사',
1194
+ url: 'https://www.nrich.go.kr/kor/majorList.do?menuIdx=286',
1195
+ parseList: parseNrichMajorEventList,
1196
+ parseDetail: parseNrichMajorEventDetail,
1197
+ },
1198
+ {
1199
+ id: '국립문화유산연구원_학술지_헤리티지',
1200
+ name: '국립문화유산연구원 헤리티지:역사와 과학 학술지',
1201
+ url: 'https://www.nrich.go.kr/kor/subscriptionDataUsrList.do?menuIdx=1651&gubun=J',
1202
+ parseList: parseNrichJournalList,
1203
+ parseDetail: parseNrichJournalDetail,
1204
+ },
1205
+ {
1206
+ id: '국립문화유산연구원_학술지_보존과학연구',
1207
+ name: '국립문화유산연구원 보존과학연구 학술지',
1208
+ url: 'https://www.nrich.go.kr/kor/subscriptionDataUsrList.do?menuIdx=2065&gubun=K',
1209
+ parseList: parseNrichJournalList,
1210
+ parseDetail: parseNrichJournalDetail,
1211
+ },
1212
+ {
1213
+ id: '국가유산지식이음_공지사항',
1214
+ name: '국가유산 지식이음 공지사항',
1215
+ url: 'https://portal.nrich.go.kr/kor/boardList.do?menuIdx=1058&bbscd=9',
1216
+ parseList: parseNrichPortalList,
1217
+ parseDetail: parseNrichPortalDetail,
1218
+ },
1219
+ {
1220
+ id: '국립고궁박물관_공지사항',
1221
+ name: '국립고궁박물관 공지사항',
1222
+ url: 'https://www.gogung.go.kr/gogung/bbs/BMSR00022/list.do?gubunCd=B22_001&menuNo=800088',
1223
+ parseList: parseGogungList,
1224
+ parseDetail: parseGogungDetail,
1225
+ },
1226
+ {
1227
+ id: '국가유산진흥원_공지사항',
1228
+ name: '국가유산진흥원 공지사항',
1229
+ url: 'https://www.kh.or.kr/brd/board/644/L/SITES/100/menu/371',
1230
+ parseList: parseHeritageAgencyList,
1231
+ parseDetail: parseHeritageAgencyDetail,
1232
+ },
1233
+ {
1234
+ id: '국가유산진흥원_보도자료',
1235
+ name: '국가유산진흥원 보도자료',
1236
+ url: 'https://www.kh.or.kr/brd/board/715/L/menu/373',
1237
+ parseList: parseHeritageAgencyList,
1238
+ parseDetail: parseHeritageAgencyDetail,
1239
+ },
1240
+ {
1241
+ id: '국가유산진흥원_매장유산국비발굴단_공지사항',
1242
+ name: '국가유산진흥원 매장유산국비발굴단 공지사항',
1243
+ url: 'https://www.kh.or.kr/brd/board/644/L/SITES/201/menu/506',
1244
+ parseList: parseHeritageAgencyList,
1245
+ parseDetail: parseHeritageAgencyDetail,
1246
+ },
1247
+ {
1248
+ id: '국가유산진흥원_매장유산국비발굴단_현장설명회',
1249
+ name: '국가유산진흥원 매장유산국비발굴단 현장설명회',
1250
+ url: 'https://www.kh.or.kr/brd/board/631/L/menu/504',
1251
+ parseList: parseHeritageAgencyList,
1252
+ parseDetail: parseHeritageAgencyDetail,
1253
+ },
1254
+ {
1255
+ id: '한국문화유산협회_공지사항',
1256
+ name: '한국문화유산협회 공지사항',
1257
+ url: 'https://www.kaah.kr/notice',
1258
+ parseList: parseKaahList,
1259
+ parseDetail: parseKaahDetail,
1260
+ },
1261
+ {
1262
+ id: '한국문화유산협회_협회소식',
1263
+ name: '한국문화유산협회 협회소식',
1264
+ url: 'https://www.kaah.kr/news',
1265
+ parseList: parseKaahList,
1266
+ parseDetail: parseKaahDetail,
1267
+ },
1268
+ {
1269
+ id: '한국문화유산협회_보도자료',
1270
+ name: '한국문화유산협회 보도자료',
1271
+ url: 'https://www.kaah.kr/mass',
1272
+ parseList: parseKaahList,
1273
+ parseDetail: parseKaahDetail,
1274
+ },
1275
+ {
1276
+ id: '한국문화유산협회_회원기관소식',
1277
+ name: '한국문화유산협회 회원기관소식',
1278
+ url: 'https://www.kaah.kr/assnews',
1279
+ parseList: parseKaahList,
1280
+ parseDetail: parseKaahDetail,
1281
+ },
1282
+ {
1283
+ id: '한국문화유산협회_유관기관소식',
1284
+ name: '한국문화유산협회 유관기관소식',
1285
+ url: 'https://www.kaah.kr/ralnews',
1286
+ parseList: parseKaahList,
1287
+ parseDetail: parseKaahDetail,
1288
+ },
1289
+ {
1290
+ id: '한국문화유산협회_발굴현장공개',
1291
+ name: '한국문화유산협회 발굴현장공개',
1292
+ url: 'https://www.kaah.kr/placeopen',
1293
+ parseList: parseKaahPlaceList,
1294
+ parseDetail: parseKaahPlaceDetail,
1295
+ },
1296
+ {
1297
+ id: '한국고고학회_공지사항',
1298
+ name: '한국고고학회 공지사항',
1299
+ url: 'https://www.kras.or.kr/?r=kras&m=bbs&bid=notice',
1300
+ parseList: parseKrasList,
1301
+ parseDetail: parseKrasDetail,
1302
+ },
1303
+ {
1304
+ id: '한국고고학회_학술대회및행사',
1305
+ name: '한국고고학회 학술대회 및 행사',
1306
+ url: 'https://www.kras.or.kr/?r=kras&m=bbs&bid=sympo',
1307
+ parseList: parseKrasList,
1308
+ parseDetail: parseKrasDetail,
1309
+ },
1310
+ {
1311
+ id: '한국고고학회_신간안내_단행본',
1312
+ name: '한국고고학회 신간안내 - 단행본',
1313
+ url: 'https://www.kras.or.kr/?c=61/101/105',
1314
+ parseList: parseKrasList,
1315
+ parseDetail: parseKrasDetail,
1316
+ },
1317
+ {
1318
+ id: '한국고고학회_현장소식',
1319
+ name: '한국고고학회 현장소식',
1320
+ url: 'https://www.kras.or.kr/?c=61/73',
1321
+ parseList: parseKrasList,
1322
+ parseDetail: parseKrasDetail,
1323
+ },
1324
+ {
1325
+ id: '중부고고학회_공지사항',
1326
+ name: '중부고고학회 공지사항',
1327
+ url: 'https://www.jbgogo.or.kr/bbs/notice',
1328
+ parseList: parseJbgogoList,
1329
+ parseDetail: parseJbgogoDetail,
1330
+ },
1331
+ {
1332
+ id: '중부고고학회_학계소식',
1333
+ name: '중부고고학회 학계소식',
1334
+ url: 'https://www.jbgogo.or.kr/bbs/news',
1335
+ parseList: parseJbgogoList,
1336
+ parseDetail: parseJbgogoDetail,
1337
+ },
1338
+ {
1339
+ id: '중부고고학회_발굴현장소식',
1340
+ name: '중부고고학회 발굴현장소식',
1341
+ url: 'https://www.jbgogo.or.kr/bbs/spotnews',
1342
+ parseList: parseJbgogoList,
1343
+ parseDetail: parseJbgogoDetail,
1344
+ },
1345
+ {
1346
+ id: '호서고고학회_공지사항',
1347
+ name: '호서고고학회 공지사항',
1348
+ url: 'http://www.hsas.or.kr/flow/?ref=board/board.emt&menu_table=m2_00&bbs_table=notice&menu_idx=010000',
1349
+ parseList: parseHsasList,
1350
+ parseDetail: parseHsasDetail,
1351
+ },
1352
+ {
1353
+ id: '호서고고학회_학회소식',
1354
+ name: '호서고고학회 학회소식',
1355
+ url: 'http://www.hsas.or.kr/flow/?ref=board/board.emt&menu_table=m2_00&bbs_table=m2_01&menu_idx=020000',
1356
+ parseList: parseHsasList,
1357
+ parseDetail: parseHsasDetail,
1358
+ },
1359
+ {
1360
+ id: '영남고고학회_공지사항',
1361
+ name: '영남고고학회 공지사항',
1362
+ url: 'http://www.yngogo.or.kr/subList/32000001120',
1363
+ parseList: (html) => parseYngogoList(html, '32000001120', '32000001157', '32000001711', customFetch),
1364
+ parseDetail: (html) => parseYngogoDetail(html, '32000001120', '32000001157', extractNttSeq(html), '32000001711', customFetch),
1365
+ },
1366
+ {
1367
+ id: '영남고고학회_학계소식',
1368
+ name: '영남고고학회 학계소식',
1369
+ url: 'http://www.yngogo.or.kr/subList/32000001133',
1370
+ parseList: (html) => parseYngogoList(html, '32000001133', '32000001161', '32000001715', customFetch),
1371
+ parseDetail: (html) => parseYngogoDetail(html, '32000001133', '32000001161', extractNttSeq(html), '32000001715', customFetch),
1372
+ },
1373
+ {
1374
+ id: '영남고고학회_현장소식',
1375
+ name: '영남고고학회 현장소식',
1376
+ url: 'http://www.yngogo.or.kr/subList/32000001135',
1377
+ parseList: (html) => parseYngogoList(html, '32000001135', '32000001163', '32000001717', customFetch),
1378
+ parseDetail: (html) => parseYngogoDetail(html, '32000001135', '32000001163', extractNttSeq(html), '32000001717', customFetch),
1379
+ },
1380
+ {
1381
+ id: '국립중앙박물관_알림',
1382
+ name: '국립중앙박물관 알림',
1383
+ url: 'https://www.museum.go.kr/MUSEUM/contents/M0701010000.do?catCustomType=united&catId=128',
1384
+ parseList: (html) => parseMuseumList(html, '/MUSEUM/contents/M0701010000.do'),
1385
+ parseDetail: parseMuseumDetail,
1386
+ },
1387
+ {
1388
+ id: '국립중앙박물관_고시공고',
1389
+ name: '국립중앙박물관 고시/공고',
1390
+ url: 'https://www.museum.go.kr/MUSEUM/contents/M0701020000.do',
1391
+ parseList: (html) => parseMuseumList(html, '/MUSEUM/contents/M0701020000.do'),
1392
+ parseDetail: parseMuseumDetail,
1393
+ },
1394
+ {
1395
+ id: '국립중앙박물관_보도자료',
1396
+ name: '국립중앙박물관 보도 자료',
1397
+ url: 'https://www.museum.go.kr/MUSEUM/contents/M0701040000.do?catCustomType=post&catId=93',
1398
+ parseList: parseMuseumPressList,
1399
+ parseDetail: parseMuseumDetail,
1400
+ },
1401
+ {
1402
+ id: '국립전주박물관_새소식',
1403
+ name: '국립전주박물관 새소식',
1404
+ url: 'https://jeonju.museum.go.kr/board.es?mid=a10105010000&bid=0001',
1405
+ parseList: parseJeonjuMuseumList,
1406
+ parseDetail: parseJeonjuMuseumDetail,
1407
+ },
1408
+ {
1409
+ id: '국립전주박물관_보도자료',
1410
+ name: '국립전주박물관 보도자료',
1411
+ url: 'https://jeonju.museum.go.kr/board.es?mid=a10105050000&bid=0004',
1412
+ parseList: parseJeonjuMuseumList,
1413
+ parseDetail: parseJeonjuMuseumDetail,
1414
+ },
1415
+ {
1416
+ id: '국립부여박물관_공지사항',
1417
+ name: '국립부여박물관 공지사항',
1418
+ url: 'https://buyeo.museum.go.kr/bbs/list.do?key=2301250005',
1419
+ parseList: (html) => parseBuyeoMuseumList(html, '2301250005'),
1420
+ parseDetail: parseBuyeoMuseumDetail,
1421
+ },
1422
+ {
1423
+ id: '국립부여박물관_보도자료',
1424
+ name: '국립부여박물관 보도자료',
1425
+ url: 'https://buyeo.museum.go.kr/bbs/list.do?key=2302150024',
1426
+ parseList: (html) => parseBuyeoMuseumList(html, '2302150024'),
1427
+ parseDetail: parseBuyeoMuseumDetail,
1428
+ },
1429
+ {
1430
+ id: '국립진주박물관_새소식',
1431
+ name: '국립진주박물관 새소식',
1432
+ url: 'https://jinju.museum.go.kr/kor/html/sub06/0601.html',
1433
+ parseList: parseJinjuMuseumList,
1434
+ parseDetail: parseJinjuMuseumDetail,
1435
+ },
1436
+ // NOTE: Parsing logic is implemented, but crawling is restricted by robots.txt policy
1437
+ // {
1438
+ // id: '국립경주박물관_새소식',
1439
+ // name: '국립경주박물관 새소식',
1440
+ // url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0701.html',
1441
+ // parseList: (html) =>
1442
+ // parseGyeongjuMuseumList(html, '/kor/html/sub07/0701.html'),
1443
+ // parseDetail: parseGyeongjuMuseumDetail,
1444
+ // },
1445
+ // {
1446
+ // id: '국립경주박물관_고시공고',
1447
+ // name: '국립경주박물관 고시/공고',
1448
+ // url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0703.html',
1449
+ // parseList: parseGyeongjuMuseumNoticeList,
1450
+ // parseDetail: parseGyeongjuMuseumDetail,
1451
+ // },
1452
+ // {
1453
+ // id: '국립경주박물관_보도자료',
1454
+ // name: '국립경주박물관 보도자료',
1455
+ // url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0705.html',
1456
+ // parseList: (html) =>
1457
+ // parseGyeongjuMuseumList(html, '/kor/html/sub07/0705.html'),
1458
+ // parseDetail: parseGyeongjuMuseumDetail,
1459
+ // },
1460
+ // {
1461
+ // id: '국립청주박물관_새소식',
1462
+ // name: '국립청주박물관 새소식',
1463
+ // url: 'https://cheongju.museum.go.kr/www/selectBbsNttList.do?bbsNo=1&key=482&nbar=s',
1464
+ // parseList: parseCheongjuMuseumList,
1465
+ // parseDetail: parseCheongjuMuseumDetail,
1466
+ // },
1467
+ // {
1468
+ // id: '국립청주박물관_언론보도자료',
1469
+ // name: '국립청주박물관 언론보도자료',
1470
+ // url: 'https://cheongju.museum.go.kr/www/selectBbsNttList.do?bbsNo=20&key=31&nbar=s',
1471
+ // parseList: parseCheongjuMuseumList,
1472
+ // parseDetail: parseCheongjuMuseumDetail,
1473
+ // },
1474
+ // {
1475
+ // id: '국립김해박물관_새소식',
1476
+ // name: '국립김해박물관 새소식',
1477
+ // url: 'https://gimhae.museum.go.kr/kr/html/sub04/0401.html',
1478
+ // parseList: (html) =>
1479
+ // parseGimhaeMuseumList(html, '/kr/html/sub04/0401.html'),
1480
+ // parseDetail: parseGimhaeMuseumDetail,
1481
+ // },
1482
+ // {
1483
+ // id: '국립김해박물관_보도자료',
1484
+ // name: '국립김해박물관 언론보도자료',
1485
+ // url: 'https://gimhae.museum.go.kr/kr/html/sub04/0402.html',
1486
+ // parseList: (html) =>
1487
+ // parseGimhaeMuseumList(html, '/kr/html/sub04/0402.html'),
1488
+ // parseDetail: parseGimhaeMuseumDetail,
1489
+ // },
1490
+ // {
1491
+ // id: '국립제주박물관_새소식',
1492
+ // name: '국립제주박물관 새소식',
1493
+ // url: 'https://jeju.museum.go.kr/_prog/_board/?code=sub02_0201&site_dvs_cd=kr&menu_dvs_cd=050101&ntt_tag=1',
1494
+ // parseList: parseJejuMuseumList,
1495
+ // parseDetail: parseJejuMuseumDetail,
1496
+ // },
1497
+ // {
1498
+ // id: '국립익산박물관_공지사항',
1499
+ // name: '국립익산박물관 공지사항',
1500
+ // url: 'https://iksan.museum.go.kr/kor/html/sub05/0501.html',
1501
+ // parseList: parseIksanMuseumList,
1502
+ // parseDetail: parseIksanMuseumDetail,
1503
+ // },
1504
+ ],
1505
+ },
1506
+ {
1507
+ id: 'business',
1508
+ name: 'Business',
1509
+ targets: [
1510
+ {
1511
+ id: '국가유산청_입찰정보',
1512
+ name: '국가유산청 입찰정보',
1513
+ url: 'https://www.khs.go.kr/tenderBbz/selectTenderBbzList.do?mn=NS_01_05',
1514
+ parseList: parseKhsTenderList,
1515
+ parseDetail: parseKhsDetail,
1516
+ },
1517
+ {
1518
+ id: '국가유산진흥원_입찰정보',
1519
+ name: '국가유산진흥원 입찰정보',
1520
+ url: 'https://www.kh.or.kr/brd/board/717/L/menu/375',
1521
+ parseList: parseHeritageAgencyList,
1522
+ parseDetail: parseHeritageAgencyDetail,
1523
+ },
1524
+ {
1525
+ id: '한국문화유산협회_사업공고',
1526
+ name: '한국문화유산협회 사업공고',
1527
+ url: 'https://www.kaah.kr/bussopen',
1528
+ parseList: parseKaahList,
1529
+ parseDetail: parseKaahDetail,
1530
+ },
1531
+ {
1532
+ id: '한국문화유산협회_입찰공고',
1533
+ name: '한국문화유산협회 입찰공고',
1534
+ url: 'https://www.kaah.kr/ipcopen',
1535
+ parseList: parseKaahList,
1536
+ parseDetail: parseKaahDetail,
1537
+ },
1538
+ ],
1539
+ },
1540
+ {
1541
+ id: 'employment',
1542
+ name: 'Employment',
1543
+ targets: [
1544
+ {
1545
+ id: '국가유산청_시험채용',
1546
+ name: '국가유산청 시험/채용',
1547
+ url: 'https://www.khs.go.kr/multiBbz/selectMultiBbzList.do?bbzId=newexam&mn=NS_01_06',
1548
+ parseList: parseKhsList,
1549
+ parseDetail: parseKhsDetail,
1550
+ },
1551
+ {
1552
+ id: '국가유산진흥원_인재채용',
1553
+ name: '국가유산진흥원 인재채용',
1554
+ url: 'https://www.kh.or.kr/brd/board/721/L/CATEGORY/719/menu/377',
1555
+ parseList: parseHeritageAgencyList,
1556
+ parseDetail: parseHeritageAgencyDetail,
1557
+ },
1558
+ {
1559
+ id: '한국문화유산협회_채용공고',
1560
+ name: '한국문화유산협회 채용공고',
1561
+ url: 'https://www.kaah.kr/reqopen',
1562
+ parseList: parseKaahList,
1563
+ parseDetail: parseKaahDetail,
1564
+ },
1565
+ {
1566
+ id: '영남고고학회_채용공고',
1567
+ name: '영남고고학회 채용공고',
1568
+ url: 'http://www.yngogo.or.kr/subList/32000001136',
1569
+ parseList: (html) => parseYngogoList(html, '32000001136', '32000001164', '32000001718', customFetch),
1570
+ parseDetail: (html) => parseYngogoDetail(html, '32000001136', '32000001164', extractNttSeq(html), '32000001718', customFetch),
1571
+ },
1572
+ {
1573
+ id: '국립중앙박물관_채용안내',
1574
+ name: '국립중앙박물관 채용 안내',
1575
+ url: 'https://www.museum.go.kr/MUSEUM/contents/M0701030000.do?catCustomType=post&catId=54&recruitYn=Y',
1576
+ parseList: parseMuseumRecruitList,
1577
+ parseDetail: parseMuseumDetail,
1578
+ },
1579
+ {
1580
+ id: '국립전주박물관_채용',
1581
+ name: '국립전주박물관 채용',
1582
+ url: 'https://jeonju.museum.go.kr/board.es?mid=a10105020000&bid=0002',
1583
+ parseList: parseJeonjuMuseumRecruitList,
1584
+ parseDetail: parseJeonjuMuseumDetail,
1585
+ },
1586
+ {
1587
+ id: '국립부여박물관_채용공고',
1588
+ name: '국립부여박물관 채용공고',
1589
+ url: 'https://buyeo.museum.go.kr/bbs/list.do?key=2301270001',
1590
+ parseList: (html) => parseBuyeoMuseumList(html, '2301270001'),
1591
+ parseDetail: parseBuyeoMuseumDetail,
1592
+ },
1593
+ // NOTE: Parsing logic is implemented, but crawling is restricted by robots.txt policy
1594
+ // {
1595
+ // id: '국립경주박물관_채용안내',
1596
+ // name: '국립경주박물관 채용안내',
1597
+ // url: 'https://gyeongju.museum.go.kr/kor/html/sub07/0704.html',
1598
+ // parseList: (html) =>
1599
+ // parseGyeongjuMuseumList(html, '/kor/html/sub07/0704.html'),
1600
+ // parseDetail: parseGyeongjuMuseumDetail,
1601
+ // },
1602
+ // {
1603
+ // id: '국립청주박물관_채용및공고',
1604
+ // name: '국립청주박물관 채용 및 공고',
1605
+ // url: 'https://cheongju.museum.go.kr/www/selectBbsNttList.do?bbsNo=29&key=476&nbar=s',
1606
+ // parseList: parseCheongjuMuseumList,
1607
+ // parseDetail: parseCheongjuMuseumDetail,
1608
+ // },
1609
+ // {
1610
+ // id: '국립제주박물관_채용정보',
1611
+ // name: '국립제주박물관 채용정보',
1612
+ // url: 'https://jeju.museum.go.kr/_prog/_board/?code=sub02_0201&site_dvs_cd=kr&menu_dvs_cd=050102&ntt_tag=2',
1613
+ // parseList: parseJejuMuseumList,
1614
+ // parseDetail: parseJejuMuseumDetail,
1615
+ // },
1616
+ ],
1617
+ },
1618
+ ];
1619
+ }
1587
1620
 
1588
1621
  /**
1589
1622
  * Newsletter content configuration
@@ -1727,6 +1760,74 @@ class AnalysisProvider {
1727
1760
  }
1728
1761
  }
1729
1762
 
1763
+ /**
1764
+ * Shared HTML fragments used by both newsletter and welcome email templates.
1765
+ *
1766
+ * These helpers extract identical HTML blocks to avoid duplication
1767
+ * while keeping template-specific styling separate.
1768
+ */
1769
+ const purify = DOMPurify(new jsdom.JSDOM('').window);
1770
+ /**
1771
+ * Sanitize user-supplied strings for safe HTML insertion.
1772
+ * Strips all HTML tags, leaving only plain text.
1773
+ */
1774
+ const sanitizeText = (str) => purify.sanitize(str, { ALLOWED_TAGS: [] });
1775
+ /**
1776
+ * Heripo light/dark logo block.
1777
+ * @param imgMarginBottom - Margin below the logo image (e.g., '8px', '12px')
1778
+ */
1779
+ const heripoLogoHtml = (imgMarginBottom) => `
1780
+ <div style="margin-bottom: 32px;">
1781
+ <div style="text-align: left; display: block;" class="light-logo">
1782
+ <img src="https://heripo.com/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">
1783
+ </div>
1784
+ <!--[if !mso]><!-->
1785
+ <div style="text-align: left; display: none;" class="dark-logo">
1786
+ <img src="https://heripo.com/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">
1787
+ </div>
1788
+ <!--<![endif]-->
1789
+ </div>`;
1790
+ /**
1791
+ * KRAS dual-logo header block.
1792
+ * Left: KRAS logo, Right: heripo lab logo (light/dark) + "제공" text.
1793
+ */
1794
+ const krasHeaderHtml = () => `
1795
+ <table cellpadding="0" cellspacing="0" width="100%" role="presentation" style="width: 100%; border-collapse: collapse; margin: 0 0 18px 0; mso-table-lspace: 0pt; mso-table-rspace: 0pt; margin-bottom: 20px; border: none;">
1796
+ <tr>
1797
+ <td align="left" valign="middle" width="50%" style="text-align: left; font-size: 15px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; padding: 0; border: none;">
1798
+ <img src="https://heripo.com/kras-logo.jpeg" width="200" alt="한국고고학회" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; display: block;" height="auto">
1799
+ </td>
1800
+ <td align="right" valign="middle" width="50%" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; padding: 0; border: none; text-align: right; font-size: 0; line-height: 0; white-space: nowrap;">
1801
+ <div style="text-align: left; display: inline-block; vertical-align: middle; line-height: 0;" class="light-logo">
1802
+ <img src="https://heripo.com/heripolab-logo.png" width="120" alt="heripo lab" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; display: inline-block; vertical-align: middle;" height="auto">
1803
+ </div><!--[if !mso]><!--><div style="text-align: left; display: none; vertical-align: middle; line-height: 0;" class="dark-logo dark-logo-inline">
1804
+ <img src="https://heripo.com/heripolab-logo-dark.png" width="120" alt="heripo lab" style="-ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; display: inline-block; vertical-align: middle;" height="auto">
1805
+ </div><!--<![endif]--><span class="header-dark-text" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; color: #666666; line-height: 1; vertical-align: middle; padding-left: 4px;">제공</span>
1806
+ </td>
1807
+ </tr>
1808
+ </table>`;
1809
+ /**
1810
+ * Heripo platform introduction section.
1811
+ * Shared between newsletter and welcome email templates.
1812
+ *
1813
+ * Note: Each template may append its own additional paragraph after this block
1814
+ * (e.g., newsletter adds a line about source requests via GitHub Issues).
1815
+ */
1816
+ const platformIntroHtml = () => `
1817
+ <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>
1818
+ <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>
1819
+ <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
1820
+ 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>
1821
+ <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.com" target="_blank">데모 사이트</a></strong>에서 직접 체험해 보실 수 있으며, 플랫폼 프로토타입 출시 시 구독자분들께 우선 안내해 드리겠습니다.</p>`;
1822
+ /**
1823
+ * "Powered by LLM Newsletter Kit · View Source" footer line.
1824
+ */
1825
+ const poweredByFooterHtml = () => `
1826
+ <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">
1827
+ 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> ·
1828
+ <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>
1829
+ </p>`;
1830
+
1730
1831
  /**
1731
1832
  * Creates an HTML template for the newsletter email
1732
1833
  *
@@ -1739,16 +1840,18 @@ class AnalysisProvider {
1739
1840
  * - Platform introduction
1740
1841
  *
1741
1842
  * @param targets - Array of crawling targets to be listed in the newsletter footer
1843
+ * @param options - Optional template customization options
1742
1844
  * @returns Complete HTML string for the newsletter email
1743
1845
  *
1744
1846
  * @example
1745
1847
  * ```typescript
1746
- * const html = createNewsletterHtmlTemplate([
1747
- * { id: '1', name: 'Source 1', url: 'https://example.com', ... }
1748
- * ]);
1848
+ * const html = createNewsletterHtmlTemplate(
1849
+ * [{ id: '1', name: 'Source 1', url: 'https://example.com', ... }],
1850
+ * { isKrasNewsletter: true, krasNewsMarkdown: '## News...' },
1851
+ * );
1749
1852
  * ```
1750
1853
  */
1751
- const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
1854
+ const createNewsletterHtmlTemplate = (targets, options) => `<!DOCTYPE html>
1752
1855
  <html lang="ko" style="color-scheme: light dark; supported-color-schemes: light dark;">
1753
1856
  <head>
1754
1857
  <meta charset="UTF-8">
@@ -2126,6 +2229,18 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2126
2229
  background-color: #23201c !important;
2127
2230
  }
2128
2231
 
2232
+
2233
+ .header-dark-text {
2234
+ color: #eeeeee !important;
2235
+ }
2236
+
2237
+ .header-title-border {
2238
+ border-bottom-color: #E59866 !important;
2239
+ }
2240
+
2241
+ .dark-logo-inline {
2242
+ display: inline-block !important;
2243
+ }
2129
2244
  }
2130
2245
  </style>
2131
2246
  </head>
@@ -2141,16 +2256,50 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2141
2256
  <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: 800px;" class="container" role="presentation">
2142
2257
  <tr>
2143
2258
  <td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg" 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);">
2144
- <div style="margin-bottom: 32px;">
2145
- <div style="text-align: left; display: block;" class="light-logo">
2146
- <img src="https://heripo.com/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: 12px;" height="auto">
2147
- </div>
2148
- <!--[if !mso]><!-->
2149
- <div style="text-align: left; display: none;" class="dark-logo">
2150
- <img src="https://heripo.com/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: 12px;" height="auto">
2151
- </div>
2152
- <!--<![endif]-->
2153
- </div>
2259
+ ${options?.isKrasNewsletter
2260
+ ? `${krasHeaderHtml()}
2261
+ <!-- 헤더: 제목/날짜 -->
2262
+ <table cellpadding="0" cellspacing="0" width="100%" role="presentation" class="header-title-border" style="width: 100%; border-collapse: collapse; margin: 0 0 18px 0; mso-table-lspace: 0pt; mso-table-rspace: 0pt; margin-bottom: 32px; border: none; border-bottom: 3px solid #D2691E;">
2263
+ <tr>
2264
+ <td align="left" valign="baseline" class="header-dark-text" style="text-align: left; padding: 0 0 14px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 26px; font-weight: bold; color: #111111; line-height: 1.2; border: none;">
2265
+ 한국고고학회 뉴스레터
2266
+ </td>
2267
+ <td align="left" valign="baseline" class="header-dark-text" style="text-align: left; padding: 0 0 14px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; color: #333333; white-space: nowrap; border: none; width: 1px;" width="1">
2268
+ ${options?.displayDate ?? ''}
2269
+ </td>
2270
+ </tr>
2271
+ </table>
2272
+ `
2273
+ : `${heripoLogoHtml('12px')}
2274
+ `}
2275
+
2276
+ ${options?.krasNewsMarkdown
2277
+ ? safeMarkdown2Html(`## 학회 소식
2278
+ ${options.krasNewsMarkdown}
2279
+
2280
+ ---
2281
+ `, {
2282
+ window: new jsdom.JSDOM('').window,
2283
+ linkTargetBlank: true,
2284
+ fixMalformedUrls: true,
2285
+ fixBoldSyntax: true,
2286
+ convertStrikethrough: true,
2287
+ })
2288
+ : ''}
2289
+
2290
+ ${options?.heripolabNewsMarkdown
2291
+ ? safeMarkdown2Html(`## heripo lab 소식
2292
+ ${options.heripolabNewsMarkdown}
2293
+
2294
+ ---
2295
+ `, {
2296
+ window: new jsdom.JSDOM('').window,
2297
+ linkTargetBlank: true,
2298
+ fixMalformedUrls: true,
2299
+ fixBoldSyntax: true,
2300
+ convertStrikethrough: true,
2301
+ })
2302
+ : ''}
2154
2303
 
2155
2304
  {{NEWSLETTER_CONTENT}}
2156
2305
 
@@ -2166,7 +2315,7 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2166
2315
  </ul>
2167
2316
  <hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
2168
2317
  <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>
2169
- <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>heripo 리서치 레이더</strong>는 매일 발행을 원칙으로 하되, 독자분들께 의미 있는 정보를 제공하기 위해 다음과 같은 발행 기준을 적용합니다:</p>
2318
+ <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>
2170
2319
  <ul style="padding-left: 24px; margin: 0 0 18px 0;">
2171
2320
  <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>
2172
2321
  <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>
@@ -2175,11 +2324,7 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2175
2324
  <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>
2176
2325
  <hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
2177
2326
  <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>
2178
- <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>
2179
- <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>
2180
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
2181
- 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>
2182
- <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.com" target="_blank">데모 사이트</a></strong>에서 직접 체험해 보실 수 있으며, 플랫폼 프로토타입 출시 시 구독자분들께 우선 안내해 드리겠습니다.</p>
2327
+ ${platformIntroHtml()}
2183
2328
  <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7;
2184
2329
  color: #444444; margin: 0 0 18px 0;">보고 계신 뉴스레터(리서치 레이더)는 heripo의 초기 선행 기능 중 하나입니다. 뉴스레터 소스 추가 요청은 <a href="https://github.com/heripo-lab/heripo-research-radar/issues" target="_blank">GitHub 이슈</a>를 통해 언제든 환영합니다.</p>
2185
2330
  <hr style="border: 0; border-top: 2px solid #D2691E; margin: 32px 0;">
@@ -2192,12 +2337,9 @@ const createNewsletterHtmlTemplate = (targets) => `<!DOCTYPE html>
2192
2337
  <tr>
2193
2338
  <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;">
2194
2339
  <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.com</p>
2195
- <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.com에서 리서치 레이더를 구독하신 분들에게 발송됩니다.<br>
2340
+ <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.com에서 뉴스레터를 구독하신 분들과 한국고고학회 회원에게 발송됩니다.' : '이 메일은 heripo.com에서 리서치 레이더를 구독하신 분들에게 발송됩니다.'}<br>
2196
2341
  더 이상 이메일을 받고 싶지 않으시면 <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>
2197
- <p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #999999; margin: 0;" class="footer-text">
2198
- 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> ·
2199
- <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>
2200
- </p>
2342
+ ${poweredByFooterHtml()}
2201
2343
  </td>
2202
2344
  </tr>
2203
2345
  </table>
@@ -2224,16 +2366,26 @@ class ContentGenerateProvider {
2224
2366
  newsletterRepository;
2225
2367
  _issueOrder = null;
2226
2368
  model;
2227
- constructor(google, articleRepository, newsletterRepository) {
2369
+ /** HTML template with markers for title and content injection */
2370
+ htmlTemplate;
2371
+ /** Newsletter brand name (defaults to config, can be overridden via constructor) */
2372
+ newsletterBrandName;
2373
+ constructor(google, articleRepository, newsletterRepository, templateOptions, brandName) {
2228
2374
  this.google = google;
2229
2375
  this.articleRepository = articleRepository;
2230
2376
  this.newsletterRepository = newsletterRepository;
2231
2377
  this.model = this.google('gemini-3-pro-preview');
2378
+ this.newsletterBrandName = brandName ?? newsletterConfig.brandName;
2379
+ this.htmlTemplate = {
2380
+ html: createNewsletterHtmlTemplate(createCrawlingTargetGroups().flatMap((group) => group.targets), templateOptions),
2381
+ markers: {
2382
+ title: 'NEWSLETTER_TITLE',
2383
+ content: 'NEWSLETTER_CONTENT',
2384
+ },
2385
+ };
2232
2386
  }
2233
2387
  /** LLM temperature setting for content generation */
2234
2388
  temperature = llmConfig.generation.temperature;
2235
- /** Newsletter brand name */
2236
- newsletterBrandName = newsletterConfig.brandName;
2237
2389
  /** Subscribe page URL */
2238
2390
  subscribePageUrl = newsletterConfig.subscribePageUrl;
2239
2391
  /** Publication criteria (minimum article count, priority score threshold) */
@@ -2261,14 +2413,6 @@ class ContentGenerateProvider {
2261
2413
  async fetchArticleCandidates() {
2262
2414
  return this.articleRepository.findCandidatesForNewsletter();
2263
2415
  }
2264
- /** HTML template with markers for title and content injection */
2265
- htmlTemplate = {
2266
- html: createNewsletterHtmlTemplate(crawlingTargetGroups.flatMap((group) => group.targets)),
2267
- markers: {
2268
- title: 'NEWSLETTER_TITLE',
2269
- content: 'NEWSLETTER_CONTENT',
2270
- },
2271
- };
2272
2416
  /**
2273
2417
  * Save generated newsletter to the repository
2274
2418
  * @param input - Newsletter data and used articles
@@ -2289,11 +2433,15 @@ class CrawlingProvider {
2289
2433
  articleRepository;
2290
2434
  /** Maximum number of concurrent crawling operations */
2291
2435
  maxConcurrency = 5;
2292
- constructor(articleRepository) {
2436
+ /** Optional custom fetch function (e.g., proxy-based fetch) */
2437
+ customFetch;
2438
+ /** Crawling target groups configuration */
2439
+ crawlingTargetGroups;
2440
+ constructor(articleRepository, customFetch) {
2293
2441
  this.articleRepository = articleRepository;
2442
+ this.customFetch = customFetch;
2443
+ this.crawlingTargetGroups = createCrawlingTargetGroups(customFetch);
2294
2444
  }
2295
- /** Crawling target groups configuration */
2296
- crawlingTargetGroups = crawlingTargetGroups;
2297
2445
  /**
2298
2446
  * Fetch existing articles by URLs to avoid duplicate crawling
2299
2447
  * @param articleUrls - URLs to check
@@ -2447,11 +2595,29 @@ function createNewsletterGenerator(dependencies) {
2447
2595
  });
2448
2596
  const dateService = new DateService(dependencies.publishDate);
2449
2597
  const taskService = new TaskService(dependencies.taskRepository);
2450
- const crawlingProvider = new CrawlingProvider(dependencies.articleRepository);
2598
+ const crawlingProvider = new CrawlingProvider(dependencies.articleRepository, dependencies.customFetch);
2451
2599
  const analysisProvider = new AnalysisProvider(openai$1, dependencies.articleRepository, dependencies.tagRepository);
2452
- const contentGenerateProvider = new ContentGenerateProvider(google$1, dependencies.articleRepository, dependencies.newsletterRepository);
2600
+ // Inject display date from DateService into template options
2601
+ const templateOptions = dependencies.templateOptions
2602
+ ? {
2603
+ ...dependencies.templateOptions,
2604
+ displayDate: dateService.getDisplayDateString(),
2605
+ }
2606
+ : undefined;
2607
+ let resolvedContentOptions = { ...contentOptions };
2608
+ let resolvedBrandName = newsletterConfig.brandName;
2609
+ if (templateOptions?.isKrasNewsletter) {
2610
+ resolvedContentOptions = {
2611
+ ...resolvedContentOptions,
2612
+ expertField: ['고고학 우선적 문화유산'],
2613
+ freeFormIntro: true,
2614
+ titleContext: templateOptions.titleContext || undefined,
2615
+ };
2616
+ resolvedBrandName = '한국고고학회 뉴스레터';
2617
+ }
2618
+ const contentGenerateProvider = new ContentGenerateProvider(google$1, dependencies.articleRepository, dependencies.newsletterRepository, templateOptions, resolvedBrandName);
2453
2619
  return new core.GenerateNewsletter({
2454
- contentOptions,
2620
+ contentOptions: resolvedContentOptions,
2455
2621
  dateService,
2456
2622
  taskService,
2457
2623
  crawlingProvider,
@@ -2494,14 +2660,268 @@ async function generateNewsletter(dependencies) {
2494
2660
  return generator.generate();
2495
2661
  }
2496
2662
 
2663
+ /**
2664
+ * Generates a welcome email HTML string with CSS inlined via juice.
2665
+ *
2666
+ * API is designed to match the original heripo-web `generateWelcomeHTML(id, name)` usage,
2667
+ * with an optional third parameter for KRAS mode and site URL override.
2668
+ *
2669
+ * @param id - Subscriber ID (used for unsubscribe links in default mode)
2670
+ * @param name - Subscriber display name
2671
+ * @param options - Optional configuration for KRAS mode and site URL
2672
+ * @returns Complete HTML string with CSS inlined (ready to send as email)
2673
+ *
2674
+ * @example
2675
+ * ```typescript
2676
+ * // Default heripo branding (same as original heripo-web usage):
2677
+ * const html = generateWelcomeHTML('subscriber-123', '홍길동');
2678
+ *
2679
+ * // KRAS mode:
2680
+ * const krasHtml = generateWelcomeHTML('subscriber-123', '홍길동', {
2681
+ * isKrasNewsletter: true,
2682
+ * });
2683
+ * ```
2684
+ */
2685
+ function generateWelcomeHTML(id, name, options) {
2686
+ const isKras = options?.isKrasNewsletter ?? false;
2687
+ const siteUrl = options?.siteUrl ?? 'https://heripo.com';
2688
+ const safeName = sanitizeText(name);
2689
+ const unsubscribeUrl = isKras
2690
+ ? '{{{RESEND_UNSUBSCRIBE_URL}}}'
2691
+ : `${siteUrl}/research-radar/unsubscribe?id=${id}`;
2692
+ return juice(createWelcomeHtmlRaw(safeName, isKras, siteUrl, unsubscribeUrl));
2693
+ }
2694
+ function createWelcomeHtmlRaw(name, isKras, siteUrl, unsubscribeUrl) {
2695
+ const title = isKras
2696
+ ? '한국고고학회 뉴스레터 구독 완료'
2697
+ : 'heripo 리서치 레이더 구독 완료';
2698
+ const headerHtml = isKras
2699
+ ? `${krasHeaderHtml()}
2700
+ <h1 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.2; margin: 0 0
2701
+ 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}님, 한국고고학회 뉴스레터를 구독해주셔서 감사합니다.</h1>`
2702
+ : `${heripoLogoHtml('8px')}
2703
+
2704
+ <h1 style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.2; margin: 0 0
2705
+ 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>`;
2706
+ const feedbackHeading = `${name}님의 목소리가 heripo의 미래를 만듭니다`;
2707
+ const feedbackText = 'heripo';
2708
+ const newsletterLine = isKras
2709
+ ? `<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>`
2710
+ : `<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>`;
2711
+ const warningHtml = isKras
2712
+ ? `
2713
+ <blockquote style="background-color: #fef2f2; border-left: 5px solid #dc2626; margin: 24px 0; padding: 20px; border-radius: 4px;">
2714
+ <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>
2715
+ <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>
2716
+ <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>
2717
+ </blockquote>`
2718
+ : `
2719
+ <blockquote style="background-color: #fef2f2; border-left: 5px solid #dc2626; margin: 24px 0; padding: 20px; border-radius: 4px;">
2720
+ <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>
2721
+ <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>
2722
+ <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>
2723
+ </blockquote>`;
2724
+ const footerDisclaimerText = isKras
2725
+ ? '이 이메일은 heripo.com에서 한국고고학회 뉴스레터를 구독하신 분들에게 발송됩니다.'
2726
+ : '이 이메일은 heripo.com에서 리서치 레이더를 구독하신 분들에게 발송됩니다.';
2727
+ const footerUnsubscribeHtml = isKras
2728
+ ? `<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>`
2729
+ : `<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>`;
2730
+ return `<!DOCTYPE html>
2731
+ <html lang="ko" style="color-scheme: light dark; supported-color-schemes: light dark;">
2732
+ <head>
2733
+ <meta charset="UTF-8">
2734
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2735
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
2736
+ <meta name="color-scheme" content="light dark">
2737
+ <meta name="supported-color-schemes" content="light dark">
2738
+ <title>${title}</title>
2739
+ <style type="text/css">
2740
+ a:hover {
2741
+ color: #D2691E;
2742
+ }
2743
+ .button-link {
2744
+ color: #fff !important;
2745
+ text-decoration: none !important;
2746
+ font-weight: bold;
2747
+ font-size: 16px;
2748
+ display: inline-block;
2749
+ padding: 10px 28px;
2750
+ border-radius: 4px;
2751
+ background: #D2691E;
2752
+ border: none;
2753
+ }
2754
+ .button-link:hover {
2755
+ background: #b85a1a;
2756
+ }
2757
+ @media screen and (max-width: 800px) {
2758
+ .container {
2759
+ width: 100% !important;
2760
+ max-width: 100% !important;
2761
+ padding: 0 !important;
2762
+ }
2763
+
2764
+ .content-cell {
2765
+ padding: 20px !important;
2766
+ }
2767
+ }
2768
+ @media screen and (max-width: 600px) {
2769
+ h1 {
2770
+ font-size: 24px !important;
2771
+ }
2772
+
2773
+ h2 {
2774
+ font-size: 20px !important;
2775
+ }
2776
+ }
2777
+ @media (prefers-color-scheme: dark) {
2778
+ body,
2779
+ .dark-mode-bg {
2780
+ background-color: #121212 !important;
2781
+ }
2782
+
2783
+ .dark-mode-content-bg {
2784
+ background-color: #1e1e1e !important;
2785
+ box-shadow: 0 4px 10px rgba(0,0,0,0.25) !important;
2786
+ }
2787
+
2788
+ h1,
2789
+ h2,
2790
+ h3,
2791
+ h4,
2792
+ h5,
2793
+ h6 {
2794
+ color: #FFFFFF !important;
2795
+ }
2796
+
2797
+ h2,
2798
+ h3,
2799
+ h4 {
2800
+ background: #1e1e1e !important;
2801
+ }
2802
+
2803
+ p,
2804
+ li {
2805
+ color: #FFFFFF !important;
2806
+ }
2807
+
2808
+ a:not(.button-link) {
2809
+ color: #4da6ff !important;
2810
+ text-decoration: underline !important;
2811
+ }
2812
+
2813
+ a.button-link {
2814
+ color: #fff !important;
2815
+ }
2816
+
2817
+ blockquote {
2818
+ background-color: #2b2b2b !important;
2819
+ }
2820
+
2821
+ blockquote p {
2822
+ color: #bbbbbb !important;
2823
+ }
2824
+
2825
+ .footer-text {
2826
+ color: #999999 !important;
2827
+ }
2828
+
2829
+ .footer-link {
2830
+ color: #999999 !important;
2831
+ text-decoration: underline !important;
2832
+ }
2833
+
2834
+ .dark-logo {
2835
+ display: block !important;
2836
+ }
2837
+
2838
+ .light-logo {
2839
+ display: none !important;
2840
+ }
2841
+
2842
+ .welcome-notice {
2843
+ background-color: #2b2b2b !important;
2844
+ }
2845
+
2846
+ .header-dark-text {
2847
+ color: #eeeeee !important;
2848
+ }
2849
+
2850
+ .dark-logo-inline {
2851
+ display: inline-block !important;
2852
+ }
2853
+ }
2854
+ </style>
2855
+ </head>
2856
+ <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;">
2857
+ <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;">
2858
+ <tr>
2859
+ <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">
2860
+ <!--[if (gte mso 9)|(IE)]>
2861
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="800">
2862
+ <tr>
2863
+ <td align="center" valign="top" width="800">
2864
+ <![endif]-->
2865
+ <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: 800px;" class="container" role="presentation">
2866
+ <tr>
2867
+ <td bgcolor="#ffffff" align="left" class="content-cell dark-mode-content-bg" 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);">
2868
+ ${headerHtml}
2869
+
2870
+ <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>
2871
+
2872
+ <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>
2873
+
2874
+ <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>
2875
+
2876
+ <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>
2877
+ ${isKras
2878
+ ? `
2879
+ <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>
2880
+ `
2881
+ : ''}
2882
+ <hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 28px 0 20px;">
2883
+
2884
+ <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>
2885
+ ${platformIntroHtml()}
2886
+ ${newsletterLine}
2887
+ ${warningHtml}
2888
+
2889
+ <hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 32px 0;">
2890
+
2891
+ <div style="color: #6b7280; font-size: 14px;" class="footer-text">
2892
+ <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>
2893
+ ${footerUnsubscribeHtml}
2894
+
2895
+ <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>
2896
+
2897
+ ${poweredByFooterHtml()}
2898
+
2899
+ <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>
2900
+ </div>
2901
+ </td>
2902
+ </tr>
2903
+ </table>
2904
+ <!--[if (gte mso 9)|(IE)]>
2905
+ </td>
2906
+ </tr>
2907
+ </table>
2908
+ <![endif]-->
2909
+ </td>
2910
+ </tr>
2911
+ </table>
2912
+ </body>
2913
+ </html>`;
2914
+ }
2915
+
2497
2916
  exports.AnalysisProvider = AnalysisProvider;
2498
2917
  exports.ContentGenerateProvider = ContentGenerateProvider;
2499
2918
  exports.CrawlingProvider = CrawlingProvider;
2500
2919
  exports.DateService = DateService;
2501
2920
  exports.TaskService = TaskService;
2502
2921
  exports.contentOptions = contentOptions;
2503
- exports.crawlingTargetGroups = crawlingTargetGroups;
2922
+ exports.createCrawlingTargetGroups = createCrawlingTargetGroups;
2504
2923
  exports.generateNewsletter = generateNewsletter;
2924
+ exports.generateWelcomeHTML = generateWelcomeHTML;
2505
2925
  exports.llmConfig = llmConfig;
2506
2926
  exports.newsletterConfig = newsletterConfig;
2507
2927
  //# sourceMappingURL=index.cjs.map