@heripo/research-radar 1.1.0 β†’ 1.2.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/README.md CHANGED
@@ -23,7 +23,7 @@ An AI-powered newsletter service for Korean cultural heritage. Built on [`@llm-n
23
23
  **Technical highlights**:
24
24
  - Type-safe TypeScript with strict interfaces
25
25
  - Provider pattern for swapping components (Crawling/Analysis/Content/Email)
26
- - 62 crawling targets across heritage agencies, museums, academic societies
26
+ - 66 crawling targets across heritage agencies, museums, academic societies
27
27
  - LLM-driven analysis (GPT-5 models)
28
28
  - Built-in retries, chain options, preview emails
29
29
 
@@ -131,7 +131,7 @@ Uses the **Provider-Service pattern** from `@llm-newsletter-kit/core`. See [core
131
131
 
132
132
  **Config** (`src/config/`): Brand, language, LLM settings
133
133
 
134
- **Targets** (`src/config/crawling-targets.ts`): 62 sources (News 49, Business 4, Employment 9)
134
+ **Targets** (`src/config/crawling-targets.ts`): 66 sources (News 52, Business 4, Employment 10)
135
135
 
136
136
  **Parsers** (`src/parsers/`): Custom extractors per organization
137
137
 
@@ -152,6 +152,21 @@ npm run typecheck # TypeScript type-check
152
152
  npm run format # Prettier formatting
153
153
  ```
154
154
 
155
+ ### Crawler Debugger
156
+
157
+ A web-based tool for testing crawling parsers during development. Built with Express.js and vanilla HTML/CSS/JS to minimize dependencies.
158
+
159
+ ```bash
160
+ npm run dev:crawler # Start at http://localhost:3333
161
+ ```
162
+
163
+ **Features**:
164
+ - Test `parseList()` and `parseDetail()` parsers via web UI
165
+ - View raw HTML source for debugging
166
+ - Copy parsed results as JSON
167
+ - 5-minute response cache (with skip/clear options)
168
+ - Timing info for fetch and parse operations
169
+
155
170
  ## 🀝 Contributing
156
171
 
157
172
  You can use this project in two ways:
package/dist/index.cjs CHANGED
@@ -970,6 +970,135 @@ function getUniqIdFromNrichMajorEvent(element) {
970
970
  return ((element.attr('onclick') ?? '').match(/fnViewPage\('(.*)'\)/)?.[1] ?? '');
971
971
  }
972
972
 
973
+ const LIST_API_URL = 'http://www.yngogo.or.kr/module/ntt/unity/selectNttListAjax.ink';
974
+ const DETAIL_API_URL = 'http://www.yngogo.or.kr/module/ntt/unity/selectNttDetailAjax.ink';
975
+ const BASE_URL = 'http://www.yngogo.or.kr';
976
+ // User-Agent list used by real browsers
977
+ const USER_AGENTS = [
978
+ // Windows - Chrome, Edge, Firefox
979
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
980
+ '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',
981
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
982
+ // macOS - Chrome, Safari, Firefox
983
+ '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',
984
+ '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',
985
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0',
986
+ // Linux - Chrome, Firefox
987
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
988
+ 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0',
989
+ // Additional common combinations
990
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
991
+ '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',
992
+ ];
993
+ // Pick a random User-Agent
994
+ const getRandomUserAgent = () => USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
995
+ /**
996
+ * Parse list page from μ˜λ‚¨κ³ κ³ ν•™νšŒ (Yeongnam Archaeological Society)
997
+ * Uses internal API since the table is rendered via CSR
998
+ * @see https://www.yngogo.or.kr
999
+ */
1000
+ const parseYngogoList = async (_html, menuSeq, bbsSeq, sitecntntsSeq) => {
1001
+ // Fetch from internal API (CSR workaround)
1002
+ const response = await fetch(LIST_API_URL, {
1003
+ method: 'POST',
1004
+ headers: {
1005
+ 'Content-Type': 'application/x-www-form-urlencoded',
1006
+ 'User-Agent': getRandomUserAgent(),
1007
+ },
1008
+ body: new URLSearchParams({
1009
+ siteSeq: '32000001030',
1010
+ bbsSeq,
1011
+ pageIndex: '1',
1012
+ menuSeq,
1013
+ pageMode: 'B',
1014
+ sitecntntsSeq,
1015
+ tabTyCode: 'dataManage',
1016
+ mngrAt: 'N',
1017
+ searchCondition: '',
1018
+ searchKeyword: '',
1019
+ nttSeq: '',
1020
+ }),
1021
+ });
1022
+ const html = await response.text();
1023
+ const $ = cheerio__namespace.load(html);
1024
+ const posts = [];
1025
+ $('.basic-table01 tr').each((_index, element) => {
1026
+ const columns = $(element).find('td');
1027
+ if (columns.length === 0) {
1028
+ return;
1029
+ }
1030
+ const titleElement = columns.eq(1).find('a');
1031
+ const uniqId = getUniqId(titleElement);
1032
+ const detailUrl = `${BASE_URL}/subList/${menuSeq}?pmode=detail&nttSeq=${uniqId}&bbsSeq=${bbsSeq}&sitecntntsSeq=${sitecntntsSeq}`;
1033
+ const title = titleElement.text()?.trim() ?? '';
1034
+ const date = getDate(columns.eq(3).text().trim());
1035
+ posts.push({
1036
+ uniqId,
1037
+ title,
1038
+ date,
1039
+ detailUrl: cleanUrl(detailUrl),
1040
+ dateType: core.DateType.REGISTERED,
1041
+ });
1042
+ });
1043
+ return posts;
1044
+ };
1045
+ /**
1046
+ * Parse detail page from μ˜λ‚¨κ³ κ³ ν•™νšŒ (Yeongnam Archaeological Society)
1047
+ * Uses internal API since the detail page is rendered via CSR
1048
+ */
1049
+ const parseYngogoDetail = async (_html, menuSeq, bbsSeq, nttSeq, sitecntntsSeq) => {
1050
+ // Fetch from internal API (CSR workaround)
1051
+ const response = await fetch(DETAIL_API_URL, {
1052
+ method: 'POST',
1053
+ headers: {
1054
+ 'Content-Type': 'application/x-www-form-urlencoded',
1055
+ 'User-Agent': getRandomUserAgent(),
1056
+ },
1057
+ body: new URLSearchParams({
1058
+ siteSeq: '32000001030',
1059
+ bbsSeq,
1060
+ nttSeq,
1061
+ pageIndex: '1',
1062
+ ordrSe: 'D',
1063
+ searchCnd: 'frstRegistPnttm',
1064
+ checkNttSeq: '',
1065
+ menuSeq,
1066
+ mngrAt: 'N',
1067
+ parntsNttSeq: '',
1068
+ secretAt: '',
1069
+ searchAt: '',
1070
+ sitecntntsSeq,
1071
+ cmntUseAt: 'N',
1072
+ atchFilePosblAt: 'Y',
1073
+ atchFilePosblCo: '3',
1074
+ listCount: '10',
1075
+ searchCondition: '1',
1076
+ searchKeyword: '',
1077
+ }),
1078
+ });
1079
+ const html = await response.text();
1080
+ const $ = cheerio__namespace.load(html);
1081
+ const content = $('.conM_txt');
1082
+ return {
1083
+ detailContent: new TurndownService().turndown(content.html() ?? ''),
1084
+ hasAttachedFile: $('#atchFile_div').length > 0,
1085
+ hasAttachedImage: content.find('img').length > 0,
1086
+ };
1087
+ };
1088
+ function getUniqId(element) {
1089
+ // fnView('1005200642', 'admin', '', '',''); - extract first param
1090
+ return (element.attr('onclick') ?? '').match(/fnView\('([^']*)'/)?.[1] ?? '';
1091
+ }
1092
+ /**
1093
+ * Extract nttSeq from CSR page HTML
1094
+ * The URL query parameter is embedded in the page script
1095
+ */
1096
+ function extractNttSeq(html) {
1097
+ // Pattern: nttSeq=1005200644 or nttSeq='1005200644'
1098
+ const match = html.match(/nttSeq[=:]['"]?(\d+)/);
1099
+ return match?.[1] ?? '';
1100
+ }
1101
+
973
1102
  const crawlingTargetGroups = [
974
1103
  {
975
1104
  id: 'news',
@@ -1200,6 +1329,27 @@ const crawlingTargetGroups = [
1200
1329
  parseList: parseHsasList,
1201
1330
  parseDetail: parseHsasDetail,
1202
1331
  },
1332
+ {
1333
+ id: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ_곡지사항',
1334
+ name: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ 곡지사항',
1335
+ url: 'http://www.yngogo.or.kr/subList/32000001120',
1336
+ parseList: (html) => parseYngogoList(html, '32000001120', '32000001157', '32000001711'),
1337
+ parseDetail: (html) => parseYngogoDetail(html, '32000001120', '32000001157', extractNttSeq(html), '32000001711'),
1338
+ },
1339
+ {
1340
+ id: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ_ν•™κ³„μ†Œμ‹',
1341
+ name: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ ν•™κ³„μ†Œμ‹',
1342
+ url: 'http://www.yngogo.or.kr/subList/32000001133',
1343
+ parseList: (html) => parseYngogoList(html, '32000001133', '32000001161', '32000001715'),
1344
+ parseDetail: (html) => parseYngogoDetail(html, '32000001133', '32000001161', extractNttSeq(html), '32000001715'),
1345
+ },
1346
+ {
1347
+ id: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ_ν˜„μž₯μ†Œμ‹',
1348
+ name: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ ν˜„μž₯μ†Œμ‹',
1349
+ url: 'http://www.yngogo.or.kr/subList/32000001135',
1350
+ parseList: (html) => parseYngogoList(html, '32000001135', '32000001163', '32000001717'),
1351
+ parseDetail: (html) => parseYngogoDetail(html, '32000001135', '32000001163', extractNttSeq(html), '32000001717'),
1352
+ },
1203
1353
  {
1204
1354
  id: 'ꡭ립쀑앙박물관_μ•Œλ¦Ό',
1205
1355
  name: 'ꡭ립쀑앙박물관 μ•Œλ¦Ό',
@@ -1385,6 +1535,13 @@ const crawlingTargetGroups = [
1385
1535
  parseList: parseKaahList,
1386
1536
  parseDetail: parseKaahDetail,
1387
1537
  },
1538
+ {
1539
+ id: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ_μ±„μš©κ³΅κ³ ',
1540
+ name: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ μ±„μš©κ³΅κ³ ',
1541
+ url: 'http://www.yngogo.or.kr/subList/32000001136',
1542
+ parseList: (html) => parseYngogoList(html, '32000001136', '32000001164', '32000001718'),
1543
+ parseDetail: (html) => parseYngogoDetail(html, '32000001136', '32000001164', extractNttSeq(html), '32000001718'),
1544
+ },
1388
1545
  {
1389
1546
  id: 'ꡭ립쀑앙박물관_μ±„μš©μ•ˆλ‚΄',
1390
1547
  name: 'ꡭ립쀑앙박물관 μ±„μš© μ•ˆλ‚΄',
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.js CHANGED
@@ -949,6 +949,135 @@ function getUniqIdFromNrichMajorEvent(element) {
949
949
  return ((element.attr('onclick') ?? '').match(/fnViewPage\('(.*)'\)/)?.[1] ?? '');
950
950
  }
951
951
 
952
+ const LIST_API_URL = 'http://www.yngogo.or.kr/module/ntt/unity/selectNttListAjax.ink';
953
+ const DETAIL_API_URL = 'http://www.yngogo.or.kr/module/ntt/unity/selectNttDetailAjax.ink';
954
+ const BASE_URL = 'http://www.yngogo.or.kr';
955
+ // User-Agent list used by real browsers
956
+ const USER_AGENTS = [
957
+ // Windows - Chrome, Edge, Firefox
958
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
959
+ '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',
960
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
961
+ // macOS - Chrome, Safari, Firefox
962
+ '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',
963
+ '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',
964
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0',
965
+ // Linux - Chrome, Firefox
966
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
967
+ 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0',
968
+ // Additional common combinations
969
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
970
+ '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',
971
+ ];
972
+ // Pick a random User-Agent
973
+ const getRandomUserAgent = () => USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
974
+ /**
975
+ * Parse list page from μ˜λ‚¨κ³ κ³ ν•™νšŒ (Yeongnam Archaeological Society)
976
+ * Uses internal API since the table is rendered via CSR
977
+ * @see https://www.yngogo.or.kr
978
+ */
979
+ const parseYngogoList = async (_html, menuSeq, bbsSeq, sitecntntsSeq) => {
980
+ // Fetch from internal API (CSR workaround)
981
+ const response = await fetch(LIST_API_URL, {
982
+ method: 'POST',
983
+ headers: {
984
+ 'Content-Type': 'application/x-www-form-urlencoded',
985
+ 'User-Agent': getRandomUserAgent(),
986
+ },
987
+ body: new URLSearchParams({
988
+ siteSeq: '32000001030',
989
+ bbsSeq,
990
+ pageIndex: '1',
991
+ menuSeq,
992
+ pageMode: 'B',
993
+ sitecntntsSeq,
994
+ tabTyCode: 'dataManage',
995
+ mngrAt: 'N',
996
+ searchCondition: '',
997
+ searchKeyword: '',
998
+ nttSeq: '',
999
+ }),
1000
+ });
1001
+ const html = await response.text();
1002
+ const $ = cheerio.load(html);
1003
+ const posts = [];
1004
+ $('.basic-table01 tr').each((_index, element) => {
1005
+ const columns = $(element).find('td');
1006
+ if (columns.length === 0) {
1007
+ return;
1008
+ }
1009
+ const titleElement = columns.eq(1).find('a');
1010
+ const uniqId = getUniqId(titleElement);
1011
+ const detailUrl = `${BASE_URL}/subList/${menuSeq}?pmode=detail&nttSeq=${uniqId}&bbsSeq=${bbsSeq}&sitecntntsSeq=${sitecntntsSeq}`;
1012
+ const title = titleElement.text()?.trim() ?? '';
1013
+ const date = getDate(columns.eq(3).text().trim());
1014
+ posts.push({
1015
+ uniqId,
1016
+ title,
1017
+ date,
1018
+ detailUrl: cleanUrl(detailUrl),
1019
+ dateType: DateType.REGISTERED,
1020
+ });
1021
+ });
1022
+ return posts;
1023
+ };
1024
+ /**
1025
+ * Parse detail page from μ˜λ‚¨κ³ κ³ ν•™νšŒ (Yeongnam Archaeological Society)
1026
+ * Uses internal API since the detail page is rendered via CSR
1027
+ */
1028
+ const parseYngogoDetail = async (_html, menuSeq, bbsSeq, nttSeq, sitecntntsSeq) => {
1029
+ // Fetch from internal API (CSR workaround)
1030
+ const response = await fetch(DETAIL_API_URL, {
1031
+ method: 'POST',
1032
+ headers: {
1033
+ 'Content-Type': 'application/x-www-form-urlencoded',
1034
+ 'User-Agent': getRandomUserAgent(),
1035
+ },
1036
+ body: new URLSearchParams({
1037
+ siteSeq: '32000001030',
1038
+ bbsSeq,
1039
+ nttSeq,
1040
+ pageIndex: '1',
1041
+ ordrSe: 'D',
1042
+ searchCnd: 'frstRegistPnttm',
1043
+ checkNttSeq: '',
1044
+ menuSeq,
1045
+ mngrAt: 'N',
1046
+ parntsNttSeq: '',
1047
+ secretAt: '',
1048
+ searchAt: '',
1049
+ sitecntntsSeq,
1050
+ cmntUseAt: 'N',
1051
+ atchFilePosblAt: 'Y',
1052
+ atchFilePosblCo: '3',
1053
+ listCount: '10',
1054
+ searchCondition: '1',
1055
+ searchKeyword: '',
1056
+ }),
1057
+ });
1058
+ const html = await response.text();
1059
+ const $ = cheerio.load(html);
1060
+ const content = $('.conM_txt');
1061
+ return {
1062
+ detailContent: new TurndownService().turndown(content.html() ?? ''),
1063
+ hasAttachedFile: $('#atchFile_div').length > 0,
1064
+ hasAttachedImage: content.find('img').length > 0,
1065
+ };
1066
+ };
1067
+ function getUniqId(element) {
1068
+ // fnView('1005200642', 'admin', '', '',''); - extract first param
1069
+ return (element.attr('onclick') ?? '').match(/fnView\('([^']*)'/)?.[1] ?? '';
1070
+ }
1071
+ /**
1072
+ * Extract nttSeq from CSR page HTML
1073
+ * The URL query parameter is embedded in the page script
1074
+ */
1075
+ function extractNttSeq(html) {
1076
+ // Pattern: nttSeq=1005200644 or nttSeq='1005200644'
1077
+ const match = html.match(/nttSeq[=:]['"]?(\d+)/);
1078
+ return match?.[1] ?? '';
1079
+ }
1080
+
952
1081
  const crawlingTargetGroups = [
953
1082
  {
954
1083
  id: 'news',
@@ -1179,6 +1308,27 @@ const crawlingTargetGroups = [
1179
1308
  parseList: parseHsasList,
1180
1309
  parseDetail: parseHsasDetail,
1181
1310
  },
1311
+ {
1312
+ id: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ_곡지사항',
1313
+ name: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ 곡지사항',
1314
+ url: 'http://www.yngogo.or.kr/subList/32000001120',
1315
+ parseList: (html) => parseYngogoList(html, '32000001120', '32000001157', '32000001711'),
1316
+ parseDetail: (html) => parseYngogoDetail(html, '32000001120', '32000001157', extractNttSeq(html), '32000001711'),
1317
+ },
1318
+ {
1319
+ id: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ_ν•™κ³„μ†Œμ‹',
1320
+ name: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ ν•™κ³„μ†Œμ‹',
1321
+ url: 'http://www.yngogo.or.kr/subList/32000001133',
1322
+ parseList: (html) => parseYngogoList(html, '32000001133', '32000001161', '32000001715'),
1323
+ parseDetail: (html) => parseYngogoDetail(html, '32000001133', '32000001161', extractNttSeq(html), '32000001715'),
1324
+ },
1325
+ {
1326
+ id: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ_ν˜„μž₯μ†Œμ‹',
1327
+ name: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ ν˜„μž₯μ†Œμ‹',
1328
+ url: 'http://www.yngogo.or.kr/subList/32000001135',
1329
+ parseList: (html) => parseYngogoList(html, '32000001135', '32000001163', '32000001717'),
1330
+ parseDetail: (html) => parseYngogoDetail(html, '32000001135', '32000001163', extractNttSeq(html), '32000001717'),
1331
+ },
1182
1332
  {
1183
1333
  id: 'ꡭ립쀑앙박물관_μ•Œλ¦Ό',
1184
1334
  name: 'ꡭ립쀑앙박물관 μ•Œλ¦Ό',
@@ -1364,6 +1514,13 @@ const crawlingTargetGroups = [
1364
1514
  parseList: parseKaahList,
1365
1515
  parseDetail: parseKaahDetail,
1366
1516
  },
1517
+ {
1518
+ id: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ_μ±„μš©κ³΅κ³ ',
1519
+ name: 'μ˜λ‚¨κ³ κ³ ν•™νšŒ μ±„μš©κ³΅κ³ ',
1520
+ url: 'http://www.yngogo.or.kr/subList/32000001136',
1521
+ parseList: (html) => parseYngogoList(html, '32000001136', '32000001164', '32000001718'),
1522
+ parseDetail: (html) => parseYngogoDetail(html, '32000001136', '32000001164', extractNttSeq(html), '32000001718'),
1523
+ },
1367
1524
  {
1368
1525
  id: 'ꡭ립쀑앙박물관_μ±„μš©μ•ˆλ‚΄',
1369
1526
  name: 'ꡭ립쀑앙박물관 μ±„μš© μ•ˆλ‚΄',
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": "1.1.0",
5
+ "version": "1.2.0",
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.cjs",
8
8
  "module": "dist/index.js",
@@ -36,7 +36,8 @@
36
36
  "lint:fix": "eslint --fix ./src",
37
37
  "lint:ci": "eslint --quiet ./src",
38
38
  "typecheck": "tsc --noEmit",
39
- "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md,mdx}\""
39
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md,mdx}\"",
40
+ "dev:crawler": "tsx watch dev-tools/crawler-debugger/server.ts"
40
41
  },
41
42
  "author": "kimhongyeon",
42
43
  "license": "Apache-2.0",
@@ -52,15 +53,18 @@
52
53
  "@eslint/js": "^9.39.2",
53
54
  "@llm-newsletter-kit/core": "^1.1.0",
54
55
  "@trivago/prettier-plugin-sort-imports": "^6.0.1",
56
+ "@types/express": "^5.0.6",
55
57
  "@types/node": "^25.0.3",
56
58
  "@types/turndown": "^5.0.6",
57
59
  "eslint": "^9.39.2",
58
60
  "eslint-plugin-unused-imports": "^4.3.0",
61
+ "express": "^5.2.1",
59
62
  "prettier": "^3.7.4",
60
63
  "rimraf": "^6.1.2",
61
64
  "rollup": "^4.55.1",
62
65
  "rollup-plugin-dts": "^6.3.0",
63
66
  "rollup-plugin-typescript2": "^0.36.0",
67
+ "tsx": "^4.21.0",
64
68
  "typescript": "^5.9.3",
65
69
  "typescript-eslint": "^8.52.0"
66
70
  },