@heripo/research-radar 5.0.2 → 5.0.3
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.js +112 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -678,42 +678,126 @@ const parseKhsDetail = async (html) => {
|
|
|
678
678
|
};
|
|
679
679
|
};
|
|
680
680
|
|
|
681
|
+
/**
|
|
682
|
+
* 크롤링 타겟과 API 요청 어댑터가 이 값을 함께 사용한다.
|
|
683
|
+
*/
|
|
684
|
+
const KRAS_SITE_BASE_URL = 'https://www.kras.or.kr';
|
|
685
|
+
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
686
|
+
const isKrasApiPost = (value) => isRecord(value) &&
|
|
687
|
+
(typeof value.id === 'string' || typeof value.id === 'number') &&
|
|
688
|
+
typeof value.board_slug === 'string';
|
|
689
|
+
const parseJson = (html) => {
|
|
690
|
+
try {
|
|
691
|
+
return JSON.parse(html);
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const getKrasPostDate = (post) => getDate((post.publish_date ?? post.created_at ?? '').slice(0, 10));
|
|
698
|
+
const toKrasListItem = (post) => {
|
|
699
|
+
const detailUrl = new URL(`/sub/${encodeURIComponent(post.board_slug)}/detail?id=${encodeURIComponent(String(post.id))}`, KRAS_SITE_BASE_URL);
|
|
700
|
+
return {
|
|
701
|
+
uniqId: String(post.id),
|
|
702
|
+
title: post.title?.trim() ?? '',
|
|
703
|
+
date: getKrasPostDate(post),
|
|
704
|
+
detailUrl: cleanUrl(detailUrl.href),
|
|
705
|
+
dateType: DateType.REGISTERED,
|
|
706
|
+
};
|
|
707
|
+
};
|
|
708
|
+
const parseKrasApiList = (html) => {
|
|
709
|
+
const data = parseJson(html);
|
|
710
|
+
if (!isRecord(data) || !Array.isArray(data.items)) {
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
return data.items.filter(isKrasApiPost).map(toKrasListItem);
|
|
714
|
+
};
|
|
715
|
+
const parseKrasApiDetail = (html) => {
|
|
716
|
+
const data = parseJson(html);
|
|
717
|
+
if (!isRecord(data) || !isKrasApiPost(data.item)) {
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
720
|
+
const item = data.item;
|
|
721
|
+
const content = cheerio.load(item.body ?? '')('body');
|
|
722
|
+
content.find('div.snsbox').remove();
|
|
723
|
+
return {
|
|
724
|
+
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
725
|
+
hasAttachedFile: (item.attachments?.length ?? 0) > 0,
|
|
726
|
+
hasAttachedImage: content.find('img').length > 0,
|
|
727
|
+
};
|
|
728
|
+
};
|
|
729
|
+
/**
|
|
730
|
+
* 새 사이트는 게시글 상세 내용을 API로 렌더링한다.
|
|
731
|
+
* 기사 출처에는 공개 상세 URL을 유지하고, 크롤러가 해당 URL을 요청하면
|
|
732
|
+
* 상세 API 응답을 가져오도록 처리한다.
|
|
733
|
+
*/
|
|
734
|
+
const createKrasFetch = (baseFetch = fetch) => async (input, init) => {
|
|
735
|
+
const requestUrl = typeof input === 'string'
|
|
736
|
+
? input
|
|
737
|
+
: input instanceof URL
|
|
738
|
+
? input.href
|
|
739
|
+
: input.url;
|
|
740
|
+
const url = new URL(requestUrl, KRAS_SITE_BASE_URL);
|
|
741
|
+
const isKrasDetailUrl = url.origin === new URL(KRAS_SITE_BASE_URL).origin &&
|
|
742
|
+
/^\/sub\/[^/]+\/detail$/.test(url.pathname) &&
|
|
743
|
+
url.searchParams.has('id');
|
|
744
|
+
if (isKrasDetailUrl) {
|
|
745
|
+
const id = encodeURIComponent(url.searchParams.get('id') ?? '');
|
|
746
|
+
return baseFetch(`${KRAS_SITE_BASE_URL}/api/boards/detail/${id}`, init);
|
|
747
|
+
}
|
|
748
|
+
return baseFetch(input, init);
|
|
749
|
+
};
|
|
681
750
|
const parseKrasList = (html) => {
|
|
682
751
|
const $ = cheerio.load(html);
|
|
683
752
|
const posts = [];
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
const
|
|
753
|
+
const apiPosts = parseKrasApiList(html);
|
|
754
|
+
if (apiPosts) {
|
|
755
|
+
return apiPosts;
|
|
756
|
+
}
|
|
757
|
+
$('.post-item').each((index, element) => {
|
|
758
|
+
const title = $(element).find('.col-title').text().trim();
|
|
759
|
+
const date = $(element).find('.col-date').text().trim();
|
|
760
|
+
const onclick = $(element).attr('onclick') ?? '';
|
|
761
|
+
const relativeHref = onclick.match(/location\.href\s*=\s*["']([^"']+)["']/)?.[1];
|
|
692
762
|
if (!relativeHref) {
|
|
693
763
|
return;
|
|
694
764
|
}
|
|
695
|
-
const fullUrl = new URL(relativeHref,
|
|
696
|
-
const detailUrl = fullUrl.href;
|
|
697
|
-
const uniqId = fullUrl.searchParams.get('uid') ?? undefined;
|
|
698
|
-
const title = titleElement.text()?.trim() ?? '';
|
|
699
|
-
const date = getDate(columns.eq(4).text().trim());
|
|
765
|
+
const fullUrl = new URL(relativeHref, KRAS_SITE_BASE_URL);
|
|
700
766
|
posts.push({
|
|
701
|
-
uniqId,
|
|
767
|
+
uniqId: fullUrl.searchParams.get('id') ?? undefined,
|
|
702
768
|
title,
|
|
703
|
-
date,
|
|
704
|
-
detailUrl: cleanUrl(
|
|
769
|
+
date: getDate(date),
|
|
770
|
+
detailUrl: cleanUrl(fullUrl.href),
|
|
705
771
|
dateType: DateType.REGISTERED,
|
|
706
772
|
});
|
|
707
773
|
});
|
|
708
774
|
return posts;
|
|
709
775
|
};
|
|
776
|
+
/**
|
|
777
|
+
* KRAS API를 통해 목록이 클라이언트에서 렌더링되는 게시판을 파싱한다.
|
|
778
|
+
*/
|
|
779
|
+
const parseKrasListFromApi = async (_html, boardSlug, customFetch) => {
|
|
780
|
+
const response = await (customFetch ?? fetch)(`${KRAS_SITE_BASE_URL}/api/boards?board=${encodeURIComponent(boardSlug)}&page=1&limit=50`, {
|
|
781
|
+
headers: {
|
|
782
|
+
Accept: 'application/json',
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
if (!response.ok) {
|
|
786
|
+
throw new Error(`KRAS list API returned HTTP ${response.status}`);
|
|
787
|
+
}
|
|
788
|
+
return parseKrasList(await response.text());
|
|
789
|
+
};
|
|
710
790
|
const parseKrasDetail = (html) => {
|
|
791
|
+
const apiDetail = parseKrasApiDetail(html);
|
|
792
|
+
if (apiDetail) {
|
|
793
|
+
return apiDetail;
|
|
794
|
+
}
|
|
711
795
|
const $ = cheerio.load(html);
|
|
712
|
-
const content = $('
|
|
796
|
+
const content = $('.detail-body').first();
|
|
713
797
|
content.find('div.snsbox').remove();
|
|
714
798
|
return {
|
|
715
799
|
detailContent: new TurndownService().turndown(content.html() ?? ''),
|
|
716
|
-
hasAttachedFile: $('
|
|
800
|
+
hasAttachedFile: $('.detail-attachments .attachment-item').length > 0,
|
|
717
801
|
hasAttachedImage: content.find('img').length > 0,
|
|
718
802
|
};
|
|
719
803
|
};
|
|
@@ -1406,29 +1490,29 @@ function createCrawlingTargetGroups(customFetch) {
|
|
|
1406
1490
|
{
|
|
1407
1491
|
id: '한국고고학회_공지사항',
|
|
1408
1492
|
name: '한국고고학회 공지사항',
|
|
1409
|
-
url:
|
|
1410
|
-
parseList:
|
|
1493
|
+
url: `${KRAS_SITE_BASE_URL}/sub/notice`,
|
|
1494
|
+
parseList: (html) => parseKrasListFromApi(html, 'notice', customFetch),
|
|
1411
1495
|
parseDetail: parseKrasDetail,
|
|
1412
1496
|
},
|
|
1413
1497
|
{
|
|
1414
1498
|
id: '한국고고학회_학술대회및행사',
|
|
1415
1499
|
name: '한국고고학회 학술대회 및 행사',
|
|
1416
|
-
url:
|
|
1417
|
-
parseList:
|
|
1500
|
+
url: `${KRAS_SITE_BASE_URL}/sub/symposium`,
|
|
1501
|
+
parseList: (html) => parseKrasListFromApi(html, 'symposium', customFetch),
|
|
1418
1502
|
parseDetail: parseKrasDetail,
|
|
1419
1503
|
},
|
|
1420
1504
|
{
|
|
1421
1505
|
id: '한국고고학회_신간안내_단행본',
|
|
1422
1506
|
name: '한국고고학회 신간안내 - 단행본',
|
|
1423
|
-
url:
|
|
1424
|
-
parseList:
|
|
1507
|
+
url: `${KRAS_SITE_BASE_URL}/sub/books`,
|
|
1508
|
+
parseList: (html) => parseKrasListFromApi(html, 'books', customFetch),
|
|
1425
1509
|
parseDetail: parseKrasDetail,
|
|
1426
1510
|
},
|
|
1427
1511
|
{
|
|
1428
1512
|
id: '한국고고학회_현장소식',
|
|
1429
1513
|
name: '한국고고학회 현장소식',
|
|
1430
|
-
url:
|
|
1431
|
-
parseList:
|
|
1514
|
+
url: `${KRAS_SITE_BASE_URL}/sub/field_news`,
|
|
1515
|
+
parseList: (html) => parseKrasListFromApi(html, 'field_news', customFetch),
|
|
1432
1516
|
parseDetail: parseKrasDetail,
|
|
1433
1517
|
},
|
|
1434
1518
|
{
|
|
@@ -2572,8 +2656,8 @@ class CrawlingProvider {
|
|
|
2572
2656
|
crawlingTargetGroups;
|
|
2573
2657
|
constructor(articleRepository, customFetch) {
|
|
2574
2658
|
this.articleRepository = articleRepository;
|
|
2575
|
-
this.customFetch = customFetch;
|
|
2576
|
-
this.crawlingTargetGroups = createCrawlingTargetGroups(customFetch);
|
|
2659
|
+
this.customFetch = createKrasFetch(customFetch ?? fetch);
|
|
2660
|
+
this.crawlingTargetGroups = createCrawlingTargetGroups(this.customFetch);
|
|
2577
2661
|
}
|
|
2578
2662
|
/**
|
|
2579
2663
|
* Fetch existing articles by URLs to avoid duplicate crawling
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@heripo/research-radar",
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "5.0.
|
|
5
|
+
"version": "5.0.3",
|
|
6
6
|
"description": "AI-driven intelligence for Korean cultural heritage. This package serves as both a ready-to-use newsletter service and a practical implementation example for the LLM-Newsletter-Kit.",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|