dongnelibrary 0.2.7 → 0.2.9

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/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "dongnelibrary",
3
3
  "engines": {
4
- "node": ">=16.0.0"
4
+ "node": ">=18.0.0"
5
5
  },
6
- "version": "0.2.7",
6
+ "version": "0.2.9",
7
7
  "description": "책을 빌릴 수 있는지 확인한다.",
8
8
  "main": "src/dongnelibrary.js",
9
9
  "bin": {
@@ -17,7 +17,8 @@
17
17
  "hscity": "mocha test/hscity.spec.js",
18
18
  "osan": "mocha test/osan.spec.js",
19
19
  "snlib": "mocha test/snlib.spec.js",
20
- "suwon": "mocha test/suwon.spec.js"
20
+ "suwon": "mocha test/suwon.spec.js",
21
+ "yongin": "mocha test/yongin.spec.js"
21
22
  },
22
23
  "repository": {
23
24
  "type": "git",
@@ -32,7 +33,8 @@
32
33
  "성남시도서관",
33
34
  "오산시도서관",
34
35
  "화성시립도서관",
35
- "수원시도서관"
36
+ "수원시도서관",
37
+ "용인시도서관"
36
38
  ],
37
39
  "author": "<autoscripts@gmail.com>",
38
40
  "license": "MIT",
@@ -56,6 +58,6 @@
56
58
  "jquery": "^3.7.1",
57
59
  "jsdom": "^21.1.1",
58
60
  "lodash": "^4.17.20",
59
- "request": "^2.73.0"
61
+ "undici": "^6.23.0"
60
62
  }
61
63
  }
@@ -6,6 +6,7 @@ const hscity = require("./library/hscity");
6
6
  const osan = require("./library/osan");
7
7
  const snlib = require("./library/snlib");
8
8
  const suwon = require("./library/suwon");
9
+ const yongin = require("./library/yongin");
9
10
  const async = require("async");
10
11
  const util = require("./util.js");
11
12
 
@@ -14,7 +15,7 @@ const libraryList = [];
14
15
  const getLibraryNames = () => util.getLibraryNames(libraryList);
15
16
 
16
17
  function makeLibraryList() {
17
- const library = [gg, gunpo, hscity, osan, snlib, suwon];
18
+ const library = [gg, gunpo, hscity, osan, snlib, suwon, yongin];
18
19
 
19
20
  _.each(library, (library) => {
20
21
  _.each(library.getLibraryNames(), (name) => {
package/src/http.js ADDED
@@ -0,0 +1,109 @@
1
+ const { request } = require("undici");
2
+
3
+ const DEFAULT_TIMEOUT = 20000;
4
+ const DEFAULT_USER_AGENT =
5
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
6
+
7
+ async function get(url, options = {}) {
8
+ const { qs, headers = {}, timeout = DEFAULT_TIMEOUT } = options;
9
+
10
+ let fullUrl = url;
11
+ if (qs && Object.keys(qs).length > 0) {
12
+ const params = new URLSearchParams();
13
+ for (const [key, value] of Object.entries(qs)) {
14
+ params.append(key, String(value));
15
+ }
16
+ fullUrl = `${url}?${params}`;
17
+ }
18
+
19
+ const res = await request(fullUrl, {
20
+ method: "GET",
21
+ headersTimeout: timeout,
22
+ bodyTimeout: timeout,
23
+ headers: { "User-Agent": DEFAULT_USER_AGENT, ...headers },
24
+ });
25
+
26
+ return { statusCode: res.statusCode, body: await res.body.text() };
27
+ }
28
+
29
+ async function post(url, options = {}) {
30
+ const { form, headers = {}, timeout = DEFAULT_TIMEOUT } = options;
31
+
32
+ const formData = new URLSearchParams();
33
+ if (form) {
34
+ for (const [key, value] of Object.entries(form)) {
35
+ formData.append(key, String(value));
36
+ }
37
+ }
38
+
39
+ const res = await request(url, {
40
+ method: "POST",
41
+ headersTimeout: timeout,
42
+ bodyTimeout: timeout,
43
+ headers: {
44
+ "User-Agent": DEFAULT_USER_AGENT,
45
+ "Content-Type": "application/x-www-form-urlencoded",
46
+ ...headers,
47
+ },
48
+ body: formData.toString(),
49
+ });
50
+
51
+ return { statusCode: res.statusCode, body: await res.body.text() };
52
+ }
53
+
54
+ function createSession() {
55
+ let cookies = [];
56
+
57
+ return {
58
+ async get(url, options = {}) {
59
+ const { headers = {}, timeout = DEFAULT_TIMEOUT } = options;
60
+
61
+ const res = await request(url, {
62
+ method: "GET",
63
+ headersTimeout: timeout,
64
+ bodyTimeout: timeout,
65
+ headers: {
66
+ "User-Agent": DEFAULT_USER_AGENT,
67
+ Cookie: cookies.join("; "),
68
+ ...headers,
69
+ },
70
+ });
71
+
72
+ const setCookie = res.headers["set-cookie"];
73
+ if (setCookie) {
74
+ const newCookies = Array.isArray(setCookie) ? setCookie : [setCookie];
75
+ cookies = [...cookies, ...newCookies.map((c) => c.split(";")[0])];
76
+ }
77
+
78
+ return { statusCode: res.statusCode, body: await res.body.text() };
79
+ },
80
+
81
+ async post(url, options = {}) {
82
+ const { form, headers = {}, timeout = DEFAULT_TIMEOUT } = options;
83
+
84
+ const formData = new URLSearchParams();
85
+ if (form) {
86
+ for (const [key, value] of Object.entries(form)) {
87
+ formData.append(key, String(value));
88
+ }
89
+ }
90
+
91
+ const res = await request(url, {
92
+ method: "POST",
93
+ headersTimeout: timeout,
94
+ bodyTimeout: timeout,
95
+ headers: {
96
+ "User-Agent": DEFAULT_USER_AGENT,
97
+ "Content-Type": "application/x-www-form-urlencoded",
98
+ Cookie: cookies.join("; "),
99
+ ...headers,
100
+ },
101
+ body: formData.toString(),
102
+ });
103
+
104
+ return { statusCode: res.statusCode, body: await res.body.text() };
105
+ },
106
+ };
107
+ }
108
+
109
+ module.exports = { get, post, createSession };
package/src/library/gg.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const getLibraryNames = require("../util.js").getLibraryNames;
2
2
  const jquery = require("jquery");
3
- const req = require("request");
3
+ const { get } = require("../http");
4
4
  const { JSDOM } = require("jsdom");
5
5
 
6
6
  const libraryList = [
@@ -22,7 +22,7 @@ function getLibraryCode(libraryName) {
22
22
  return found ? found.code : "";
23
23
  }
24
24
 
25
- function search(opt, getBook) {
25
+ async function search(opt, getBook) {
26
26
  let title = opt.title;
27
27
  let libraryName = opt.libraryName;
28
28
 
@@ -40,70 +40,70 @@ function search(opt, getBook) {
40
40
  return;
41
41
  }
42
42
 
43
- // 'https://lib.goe.go.kr/gg/intro/search/index.do?viewPage=1&search_text=javascript&booktype=BOOKANDNONBOOK&libraryCodes=MB&rowCount=1000',
44
43
  const lcode = getLibraryCode(libraryName);
45
- req.get(
46
- {
47
- url: `https://lib.goe.go.kr/gg/intro/search/index.do`,
48
- timeout: 20000,
49
- qs: {
50
- booktype: "BOOKANDNONBOOK",
51
- libraryCodes: lcode,
52
- rowCount: 1000,
53
- search_text: title,
54
- viewPage: 1,
55
- },
56
- },
57
- function (err, res, body) {
58
- if (err || (res && res.statusCode !== 200)) {
59
- let msg = "";
60
44
 
61
- if (err) {
62
- msg = err;
63
- }
45
+ try {
46
+ const { statusCode, body } = await get(
47
+ `https://lib.goe.go.kr/gg/intro/search/index.do`,
48
+ {
49
+ qs: {
50
+ booktype: "BOOKANDNONBOOK",
51
+ libraryCodes: lcode,
52
+ rowCount: 1000,
53
+ search_text: title,
54
+ viewPage: 1,
55
+ },
56
+ },
57
+ );
64
58
 
65
- if (res && res.statusCode) {
66
- msg = msg + " " + res.statusCode;
67
- }
59
+ if (statusCode !== 200) {
60
+ if (getBook) {
61
+ getBook({ msg: `HTTP ${statusCode}` });
62
+ }
63
+ return;
64
+ }
68
65
 
69
- if (getBook) {
70
- getBook({ msg: msg });
71
- }
72
- } else {
73
- const dom = new JSDOM(body);
74
- const $counter = dom.window.document.querySelector(
75
- "#search_result > div.research-box > div.search-info > b",
76
- );
77
- const count = $counter ? Number($counter.innerHTML) : 0;
78
- // const $row = dom.window.document.querySelectorAll('.row .book-title')
79
- const $ = jquery(dom.window);
80
- const booklist = [];
81
- $(".bif").each((_, a) => {
82
- const title = $(a).find(".book-title > span").text().trim();
83
- const rented = $(a).find(".state.typeC").text().trim();
84
- const libraryName = $(a)
85
- .find("span:contains('도서관')")
86
- .next()
87
- .text()
88
- .split("|")[0]
89
- .trim();
90
- if (title) {
91
- booklist.push({
92
- libraryName,
93
- title,
94
- maxoffset: count,
95
- exist: rented === "대출가능",
96
- });
97
- }
98
- });
99
- getBook(null, {
100
- startPage: opt.startPage,
101
- totalBookCount: count,
102
- booklist,
66
+ const dom = new JSDOM(body);
67
+ const $counter = dom.window.document.querySelector(
68
+ "#search_result > div.research-box > div.search-info > b",
69
+ );
70
+ const count = $counter ? Number($counter.innerHTML) : 0;
71
+ const $ = jquery(dom.window);
72
+ const booklist = [];
73
+ $(".bif").each((_, a) => {
74
+ const titleElement = $(a).find(".book-title");
75
+ const title = titleElement.find("> span").text().trim();
76
+ const bookPath = titleElement.attr("href");
77
+ const bookUrl = bookPath
78
+ ? "https://lib.goe.go.kr/gg/intro/search/" + bookPath
79
+ : "";
80
+ const rented = $(a).find(".state.typeC").text().trim();
81
+ const libraryName = $(a)
82
+ .find("span:contains('도서관')")
83
+ .next()
84
+ .text()
85
+ .split("|")[0]
86
+ .trim();
87
+ if (title) {
88
+ booklist.push({
89
+ libraryName,
90
+ title,
91
+ bookUrl,
92
+ maxoffset: count,
93
+ exist: rented === "대출가능",
103
94
  });
104
95
  }
105
- },
106
- );
96
+ });
97
+ getBook(null, {
98
+ startPage: opt.startPage,
99
+ totalBookCount: count,
100
+ booklist,
101
+ });
102
+ } catch (err) {
103
+ if (getBook) {
104
+ getBook({ msg: err.toString() });
105
+ }
106
+ }
107
107
  }
108
108
 
109
109
  module.exports = {
@@ -1,4 +1,4 @@
1
- const req = require("request");
1
+ const { get } = require("../http");
2
2
  const _ = require("lodash");
3
3
  const getLibraryNames = require("../util.js").getLibraryNames;
4
4
 
@@ -40,11 +40,12 @@ function getBookList(json) {
40
40
  title: book.titleStatement,
41
41
  exist: book.branchVolumes.some((vol) => vol.cState.includes("대출가능")),
42
42
  libraryName: book.branchVolumes.map((vol) => vol.name).join(","),
43
+ bookUrl: book.id ? `https://www.gunpolib.go.kr/#/book/${book.id}` : "",
43
44
  };
44
45
  });
45
46
  }
46
47
 
47
- function search(opt, getBook) {
48
+ async function search(opt, getBook) {
48
49
  let title = opt.title;
49
50
  let libraryName = opt.libraryName;
50
51
 
@@ -64,44 +65,35 @@ function search(opt, getBook) {
64
65
 
65
66
  const branch = getLibraryCode(libraryName);
66
67
 
67
- req.get(
68
- {
69
- url: "https://www.gunpolib.go.kr/pyxis-api/1/collections/1/search",
70
- timeout: 20000,
71
- headers: {
72
- "User-Agent":
73
- "User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
68
+ try {
69
+ const { statusCode, body } = await get(
70
+ "https://www.gunpolib.go.kr/pyxis-api/1/collections/1/search",
71
+ {
72
+ qs: {
73
+ all: `k|a|${title}`,
74
+ branch,
75
+ max: 1000,
76
+ },
74
77
  },
75
- qs: {
76
- all: `k|a|${title}`,
77
- branch,
78
- max: 1000,
79
- },
80
- },
81
- function (err, res, body) {
82
- if (err || (res && res.statusCode !== 200)) {
83
- let msg = "";
84
-
85
- if (err) {
86
- msg = err;
87
- }
78
+ );
88
79
 
89
- if (res && res.statusCode) {
90
- msg = msg + " " + res.statusCode;
91
- }
92
-
93
- if (getBook) {
94
- getBook({ msg: msg });
95
- }
96
- } else {
97
- const booklist = getBookList(JSON.parse(body));
98
- getBook(null, {
99
- totalBookCount: booklist.length,
100
- booklist,
101
- });
80
+ if (statusCode !== 200) {
81
+ if (getBook) {
82
+ getBook({ msg: `HTTP ${statusCode}` });
102
83
  }
103
- },
104
- );
84
+ return;
85
+ }
86
+
87
+ const booklist = getBookList(JSON.parse(body));
88
+ getBook(null, {
89
+ totalBookCount: booklist.length,
90
+ booklist,
91
+ });
92
+ } catch (err) {
93
+ if (getBook) {
94
+ getBook({ msg: err.toString() });
95
+ }
96
+ }
105
97
  }
106
98
 
107
99
  module.exports = {
@@ -1,6 +1,6 @@
1
1
  const getLibraryNames = require("../util.js").getLibraryNames;
2
2
  const jquery = require("jquery");
3
- const req = require("request");
3
+ const { post } = require("../http");
4
4
  const { JSDOM } = require("jsdom");
5
5
 
6
6
  const libraryList = [
@@ -38,7 +38,7 @@ function getLibraryCode(libraryName) {
38
38
  return found ? found.code : "";
39
39
  }
40
40
 
41
- function search(opt, getBook) {
41
+ async function search(opt, getBook) {
42
42
  let title = opt.title;
43
43
  let libraryName = opt.libraryName;
44
44
 
@@ -57,62 +57,61 @@ function search(opt, getBook) {
57
57
  }
58
58
 
59
59
  const lcode = getLibraryCode(libraryName);
60
- // const url=`https://hscitylib.or.kr/intro/menu/10008/program/30001/searchResultList.do?searchType=SIMPLE&searchManageCodeArr=MK&searchKeyword=javascript`
61
60
  const url = `https://hscitylib.or.kr/intro/menu/10008/program/30001/searchResultList.do`;
62
- req.post(
63
- {
64
- url,
65
- timeout: 20000,
66
- headers: {
67
- "User-Agent":
68
- "User-Agent:Mozilla/5.0 (X11 Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
69
- },
61
+
62
+ try {
63
+ const { statusCode, body } = await post(url, {
70
64
  form: {
71
65
  searchType: "SIMPLE",
72
66
  searchKeyword: title,
73
67
  searchManageCodeArr: lcode,
74
68
  searchDisplay: 1000,
75
69
  },
76
- },
77
- function (err, res, body) {
78
- if (err || (res && res.statusCode !== 200)) {
79
- let msg = "";
70
+ });
80
71
 
81
- if (err) {
82
- msg = err;
83
- }
84
-
85
- if (res && res.statusCode) {
86
- msg = msg + " " + res.statusCode;
87
- }
72
+ if (statusCode !== 200) {
73
+ if (getBook) {
74
+ getBook({ msg: `HTTP ${statusCode}` });
75
+ }
76
+ return;
77
+ }
88
78
 
89
- if (getBook) {
90
- getBook({ msg });
91
- }
92
- } else {
93
- const dom = new JSDOM(body);
94
- const $ = jquery(dom.window);
95
- const count = $("#totalCnt").text().match(/\d+/)[0];
96
- const booklist = [];
97
- $(".bookArea").each((_, a) => {
98
- const title = $(a).find("p.book_name.kor.on > a").attr("title");
99
- const rented = $(a).find("span.emp8").text().trim();
100
- const libraryName = $(a).find("b.themeFC").text().trim();
101
- booklist.push({
102
- libraryName: libraryName.replace(/[\[\]]/g, ""),
103
- title,
104
- maxoffset: count,
105
- exist: rented.includes("대출가능"),
106
- });
107
- });
108
- getBook(null, {
109
- startPage: opt.startPage,
110
- totalBookCount: count,
111
- booklist,
112
- });
79
+ const dom = new JSDOM(body);
80
+ const $ = jquery(dom.window);
81
+ const count = $("#totalCnt").text().match(/\d+/)[0];
82
+ const booklist = [];
83
+ $(".bookArea").each((_, a) => {
84
+ const titleElement = $(a).find("p.book_name.kor.on > a");
85
+ const title = titleElement.attr("title");
86
+ const onclick = titleElement.attr("onclick") || "";
87
+ const match = onclick.match(
88
+ /fnDetail\('(\d+)',\s*'(\d+)',\s*'([^']*)',\s*'(\w+)'\)/,
89
+ );
90
+ let bookUrl = "";
91
+ if (match) {
92
+ const [, bookKey, speciesKey, isbn, pubFormCode] = match;
93
+ bookUrl = `https://hscitylib.or.kr/intro/menu/10008/program/30001/searchResultDetail.do?bookKey=${bookKey}&speciesKey=${speciesKey}&isbn=${isbn}&pubFormCode=${pubFormCode}`;
113
94
  }
114
- },
115
- );
95
+ const rented = $(a).find("span.emp8").text().trim();
96
+ const libraryName = $(a).find("b.themeFC").text().trim();
97
+ booklist.push({
98
+ libraryName: libraryName.replace(/[\[\]]/g, ""),
99
+ title,
100
+ bookUrl,
101
+ maxoffset: count,
102
+ exist: rented.includes("대출가능"),
103
+ });
104
+ });
105
+ getBook(null, {
106
+ startPage: opt.startPage,
107
+ totalBookCount: count,
108
+ booklist,
109
+ });
110
+ } catch (err) {
111
+ if (getBook) {
112
+ getBook({ msg: err.toString() });
113
+ }
114
+ }
116
115
  }
117
116
 
118
117
  module.exports = {
@@ -1,5 +1,5 @@
1
1
  const getLibraryNames = require("../util.js").getLibraryNames;
2
- const req = require("request");
2
+ const { get } = require("../http");
3
3
  const { JSDOM } = require("jsdom");
4
4
 
5
5
  const libraryList = [
@@ -20,7 +20,7 @@ function getLibraryCode(libraryName) {
20
20
  return found ? found.code : "";
21
21
  }
22
22
 
23
- function search(opt, getBook) {
23
+ async function search(opt, getBook) {
24
24
  let title = opt.title;
25
25
  let libraryName = opt.libraryName;
26
26
 
@@ -38,95 +38,103 @@ function search(opt, getBook) {
38
38
  return;
39
39
  }
40
40
 
41
- // https://www.osanlibrary.go.kr/intro/program/plusSearchResultList.do?searchType=SIMPLE&searchCategory=ALL&searchLibraryArr=MA&searchKey=ALL&searchKeyword=javascript&searchRecordCount=20
42
41
  const lcode = getLibraryCode(libraryName);
43
- req.get(
44
- {
45
- url: `https://www.osanlibrary.go.kr/intro/program/plusSearchResultList.do`,
46
- timeout: 20000,
47
- qs: {
48
- searchType: "SIMPLE",
49
- searchCategory: "ALL",
50
- searchLibraryArr: lcode,
51
- searchKey: "ALL",
52
- searchKeyword: title,
53
- searchRecordCount: 1000,
42
+
43
+ try {
44
+ const { statusCode, body } = await get(
45
+ `https://www.osanlibrary.go.kr/intro/program/plusSearchResultList.do`,
46
+ {
47
+ qs: {
48
+ searchType: "SIMPLE",
49
+ searchCategory: "ALL",
50
+ searchLibraryArr: lcode,
51
+ searchKey: "ALL",
52
+ searchKeyword: title,
53
+ searchRecordCount: 1000,
54
+ },
54
55
  },
55
- },
56
- function (err, res, body) {
57
- if (err || (res && res.statusCode !== 200)) {
58
- let msg = "";
56
+ );
59
57
 
60
- if (err) {
61
- msg = err;
62
- }
58
+ if (statusCode !== 200) {
59
+ if (getBook) {
60
+ getBook({ msg: `HTTP ${statusCode}` });
61
+ }
62
+ return;
63
+ }
63
64
 
64
- if (res && res.statusCode) {
65
- msg = msg + " " + res.statusCode;
66
- }
65
+ const dom = new JSDOM(body);
66
+ const document = dom.window.document;
67
+
68
+ // Extract total count from "총 <span class="highlight">44</span>건"
69
+ const highlightSpans = document.querySelectorAll("span.highlight");
70
+ let count = "0";
71
+ for (const span of highlightSpans) {
72
+ const text = span.textContent.trim();
73
+ if (/^\d+$/.test(text)) {
74
+ count = text;
75
+ break;
76
+ }
77
+ }
67
78
 
68
- if (getBook) {
69
- getBook({ msg: msg });
70
- }
71
- } else {
72
- const dom = new JSDOM(body);
73
- const document = dom.window.document;
74
-
75
- // Extract total count from "총 <span class="highlight">44</span>건"
76
- const highlightSpans = document.querySelectorAll("span.highlight");
77
- let count = "0";
78
- for (const span of highlightSpans) {
79
- const text = span.textContent.trim();
80
- if (/^\d+$/.test(text)) {
81
- count = text;
82
- break;
83
- }
84
- }
79
+ const booklist = [];
80
+ const bookItems = document.querySelectorAll(".bookList .listWrap > li");
81
+ bookItems.forEach((li) => {
82
+ // Get title and book URL from .book_name link
83
+ const titleLink = li.querySelector(".book_name");
84
+ const titleEl = titleLink ? titleLink.querySelector("span") : null;
85
+ const bookTitle = titleEl ? titleEl.textContent.trim() : "";
86
+
87
+ // Extract book URL from onclick handler
88
+ let bookUrl = "";
89
+ const onclick = titleLink ? titleLink.getAttribute("onclick") || "" : "";
90
+ const urlMatch = onclick.match(
91
+ /fnSearchResultDetail\((\d+),(\d+),'(\w+)'\)/,
92
+ );
93
+ if (urlMatch) {
94
+ const [, recKey, bookKey, publishFormCode] = urlMatch;
95
+ bookUrl = `https://www.osanlibrary.go.kr/intro/menu/10003/program/30004/plusSearchResultDetail.do?recKey=${recKey}&bookKey=${bookKey}&publishFormCode=${publishFormCode}`;
96
+ }
85
97
 
86
- const booklist = [];
87
- const bookItems = document.querySelectorAll(".bookList .listWrap > li");
88
- bookItems.forEach((li) => {
89
- // Get title from .book_name span
90
- const titleEl = li.querySelector(".book_name span");
91
- const bookTitle = titleEl ? titleEl.textContent.trim() : "";
92
-
93
- // Get availability status from .status p
94
- const statusEl = li.querySelector(".status p");
95
- const statusText = statusEl ? statusEl.textContent.trim() : "";
96
- const exist = statusText.includes("대출가능");
97
-
98
- // Get library name from ".book_info .fb p" containing "소장도서관"
99
- let libName = "";
100
- const fbParagraphs = li.querySelectorAll(".book_info .fb p");
101
- fbParagraphs.forEach((p) => {
102
- const text = p.textContent;
103
- if (text.includes("소장도서관")) {
104
- // Format: "[공공]오산시중앙도서관" - extract library name after "]"
105
- const match = text.match(/\](.+)$/);
106
- if (match) {
107
- libName = match[1].trim();
108
- }
109
- }
110
- });
111
-
112
- if (bookTitle) {
113
- booklist.push({
114
- libraryName: libName,
115
- title: bookTitle,
116
- maxoffset: count,
117
- exist: exist,
118
- });
98
+ // Get availability status from .status p
99
+ const statusEl = li.querySelector(".status p");
100
+ const statusText = statusEl ? statusEl.textContent.trim() : "";
101
+ const exist = statusText.includes("대출가능");
102
+
103
+ // Get library name from ".book_info .fb p" containing "소장도서관"
104
+ let libName = "";
105
+ const fbParagraphs = li.querySelectorAll(".book_info .fb p");
106
+ fbParagraphs.forEach((p) => {
107
+ const text = p.textContent;
108
+ if (text.includes("소장도서관")) {
109
+ // Format: "[공공]오산시중앙도서관" - extract library name after "]"
110
+ const match = text.match(/\](.+)$/);
111
+ if (match) {
112
+ libName = match[1].trim();
119
113
  }
120
- });
121
-
122
- getBook(null, {
123
- startPage: opt.startPage,
124
- totalBookCount: count,
125
- booklist,
114
+ }
115
+ });
116
+
117
+ if (bookTitle) {
118
+ booklist.push({
119
+ libraryName: libName,
120
+ title: bookTitle,
121
+ bookUrl,
122
+ maxoffset: count,
123
+ exist: exist,
126
124
  });
127
125
  }
128
- },
129
- );
126
+ });
127
+
128
+ getBook(null, {
129
+ startPage: opt.startPage,
130
+ totalBookCount: count,
131
+ booklist,
132
+ });
133
+ } catch (err) {
134
+ if (getBook) {
135
+ getBook({ msg: err.toString() });
136
+ }
137
+ }
130
138
  }
131
139
 
132
140
  module.exports = {
@@ -1,6 +1,6 @@
1
1
  const getLibraryNames = require("../util.js").getLibraryNames;
2
2
  const jquery = require("jquery");
3
- const req = require("request");
3
+ const { get } = require("../http");
4
4
  const { JSDOM } = require("jsdom");
5
5
 
6
6
  const libraryList = [
@@ -27,7 +27,7 @@ function getLibraryCode(libraryName) {
27
27
  return found ? found.code : "";
28
28
  }
29
29
 
30
- function search(opt, getBook) {
30
+ async function search(opt, getBook) {
31
31
  let title = opt.title;
32
32
  let libraryName = opt.libraryName;
33
33
 
@@ -46,67 +46,74 @@ function search(opt, getBook) {
46
46
  }
47
47
 
48
48
  const lcode = getLibraryCode(libraryName);
49
- req.get(
50
- {
51
- url: "https://www.snlib.go.kr/intro/menu/10041/program/30009/plusSearchResultList.do",
52
- timeout: 20000,
53
- qs: {
54
- currentPageNo: 1,
55
- searchBookClass: "ALL",
56
- searchCategory: "BOOK",
57
- searchKey: "ALL",
58
- searchKeyword: title,
59
- searchLibraryArr: lcode,
60
- searchOrder: "DESC",
61
- searchRecordCount: 1000,
62
- searchSort: "SIMILAR",
63
- searchType: "SIMPLE",
64
- },
65
- },
66
- function (err, res, body) {
67
- if (err || (res && res.statusCode !== 200)) {
68
- let msg = "";
69
49
 
70
- if (err) {
71
- msg = err;
72
- }
50
+ try {
51
+ const { statusCode, body } = await get(
52
+ "https://www.snlib.go.kr/intro/menu/10041/program/30009/plusSearchResultList.do",
53
+ {
54
+ qs: {
55
+ currentPageNo: 1,
56
+ searchBookClass: "ALL",
57
+ searchCategory: "BOOK",
58
+ searchKey: "ALL",
59
+ searchKeyword: title,
60
+ searchLibraryArr: lcode,
61
+ searchOrder: "DESC",
62
+ searchRecordCount: 1000,
63
+ searchSort: "SIMILAR",
64
+ searchType: "SIMPLE",
65
+ },
66
+ },
67
+ );
73
68
 
74
- if (res && res.statusCode) {
75
- msg = msg + " " + res.statusCode;
76
- }
69
+ if (statusCode !== 200) {
70
+ if (getBook) {
71
+ getBook({ msg: `HTTP ${statusCode}` });
72
+ }
73
+ return;
74
+ }
77
75
 
78
- if (getBook) {
79
- getBook({ msg: msg });
76
+ const dom = new JSDOM(body);
77
+ const $ = jquery(dom.window);
78
+ const count = $("strong.themeFC").text().match(/\d+/)[0];
79
+ const booklist = [];
80
+ if (count) {
81
+ $(".resultList > li").each((_, a) => {
82
+ const titleElement = $(a).find(".tit a");
83
+ const title = titleElement.text().trim();
84
+ const onclick = titleElement.attr("onclick") || "";
85
+ const match = onclick.match(
86
+ /fnSearchResultDetail\((\d+),(\d+),'(\w+)'\)/,
87
+ );
88
+ let bookUrl = "";
89
+ if (match) {
90
+ const [, recKey, bookKey, publishFormCode] = match;
91
+ bookUrl = `https://www.snlib.go.kr/intro/menu/10041/program/30009/plusSearchResultDetail.do?recKey=${recKey}&bookKey=${bookKey}&publishFormCode=${publishFormCode}`;
80
92
  }
81
- } else {
82
- const dom = new JSDOM(body);
83
- const $ = jquery(dom.window);
84
- const count = $("strong.themeFC").text().match(/\d+/)[0];
85
- const booklist = [];
86
- if (count) {
87
- $(".resultList > li").each((_, a) => {
88
- const title = $(a).find(".tit a").text().trim();
89
- const rented = $(a).find(".bookStateBar .txt b").text();
90
- const b = $(a).find(".site > span:first-child").text().split(":");
91
- const libraryName = b && b[1] ? b[1].trim() : "";
92
- if (title) {
93
- booklist.push({
94
- libraryName,
95
- title,
96
- maxoffset: count,
97
- exist: rented.includes("대출가능"),
98
- });
99
- }
93
+ const rented = $(a).find(".bookStateBar .txt b").text();
94
+ const b = $(a).find(".site > span:first-child").text().split(":");
95
+ const libraryName = b && b[1] ? b[1].trim() : "";
96
+ if (title) {
97
+ booklist.push({
98
+ libraryName,
99
+ title,
100
+ bookUrl,
101
+ maxoffset: count,
102
+ exist: rented.includes("대출가능"),
100
103
  });
101
104
  }
102
- getBook(null, {
103
- startPage: opt.startPage,
104
- totalBookCount: count,
105
- booklist,
106
- });
107
- }
108
- },
109
- );
105
+ });
106
+ }
107
+ getBook(null, {
108
+ startPage: opt.startPage,
109
+ totalBookCount: count,
110
+ booklist,
111
+ });
112
+ } catch (err) {
113
+ if (getBook) {
114
+ getBook({ msg: err.toString() });
115
+ }
116
+ }
110
117
  }
111
118
 
112
119
  module.exports = {
@@ -1,5 +1,5 @@
1
1
  const getLibraryNames = require("../util.js").getLibraryNames;
2
- const req = require("request");
2
+ const { createSession } = require("../http");
3
3
 
4
4
  const libraryList = [
5
5
  { code: "141025", name: "선경도서관" },
@@ -32,10 +32,6 @@ function getLibraryCode(libraryName) {
32
32
  return found ? found.code : "";
33
33
  }
34
34
 
35
- function getAllLibraryCodes() {
36
- return libraryList.map((lib) => lib.code).join(",");
37
- }
38
-
39
35
  function stripHtml(str) {
40
36
  return str ? str.replace(/<[^>]*>/g, "") : "";
41
37
  }
@@ -45,15 +41,20 @@ function getBookList(data) {
45
41
  return [];
46
42
  }
47
43
  return data.SEARCH_RESULT.SEARCH_LIST.map(function (book) {
44
+ let bookUrl = "";
45
+ if (book.MANAGE_CODE && book.ISBN && book.BOOK_KEY) {
46
+ bookUrl = `https://search.suwonlib.go.kr/detail/${book.MANAGE_CODE}/${book.ISBN}/${book.BOOK_KEY}`;
47
+ }
48
48
  return {
49
49
  title: stripHtml(book.TITLE_INFO || ""),
50
50
  exist: book.LOAN_CODE === "OK",
51
51
  libraryName: book.LIB_NAME || "",
52
+ bookUrl,
52
53
  };
53
54
  });
54
55
  }
55
56
 
56
- function search(opt, getBook) {
57
+ async function search(opt, getBook) {
57
58
  let title = opt.title;
58
59
  let libraryName = opt.libraryName;
59
60
 
@@ -73,102 +74,71 @@ function search(opt, getBook) {
73
74
 
74
75
  const lcode = getLibraryCode(libraryName);
75
76
 
76
- // Create a cookie jar to maintain session
77
- const jar = req.jar();
78
-
79
- // First, visit the search page to initialize session
80
- req.get(
81
- {
82
- url: "https://search.suwonlib.go.kr/search",
83
- jar: jar,
84
- timeout: 20000,
85
- headers: {
86
- "User-Agent":
87
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
88
- },
89
- },
90
- function (err1, res1, body1) {
91
- if (err1) {
92
- if (getBook) {
93
- getBook({ msg: err1.toString() });
94
- }
95
- return;
96
- }
97
-
98
- // Now make the API call with the session cookies
99
- // Requires 'ajax: true' header and full parameter set including empty facet parameters
100
- req.post(
101
- {
102
- url: "https://search.suwonlib.go.kr/getSearchResult/normal",
103
- jar: jar,
104
- timeout: 20000,
105
- headers: {
106
- "User-Agent":
107
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
108
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
109
- Referer: "https://search.suwonlib.go.kr/search",
110
- "X-Requested-With": "XMLHttpRequest",
111
- ajax: "true",
112
- },
113
- form: {
114
- searchTxt: title,
115
- kCid: "",
116
- kdcValue: "",
117
- searchKind: "book",
118
- manageCode: lcode,
119
- isInnerSearch: "F",
120
- innerSearchTxt: "",
121
- keywordSearch: false,
122
- displayNo: "1000",
123
- orderbyItem: "ACCURACY_SORT",
124
- orderby: "DESC",
125
- pageNo: "1",
126
- facetLib: "",
127
- facetLibName: "",
128
- facetAuthor: "",
129
- facetPublisher: "",
130
- facetPubYear: "",
131
- facetSubject: "",
132
- facetSubjectName: "",
133
- facetMedia: "",
134
- facetMediaName: "",
135
- },
77
+ try {
78
+ // Create a session to maintain cookies
79
+ const session = createSession();
80
+
81
+ // First, visit the search page to initialize session
82
+ await session.get("https://search.suwonlib.go.kr/search");
83
+
84
+ // Now make the API call with the session cookies
85
+ const { statusCode, body } = await session.post(
86
+ "https://search.suwonlib.go.kr/getSearchResult/normal",
87
+ {
88
+ form: {
89
+ searchTxt: title,
90
+ kCid: "",
91
+ kdcValue: "",
92
+ searchKind: "book",
93
+ manageCode: lcode,
94
+ isInnerSearch: "F",
95
+ innerSearchTxt: "",
96
+ keywordSearch: false,
97
+ displayNo: "1000",
98
+ orderbyItem: "ACCURACY_SORT",
99
+ orderby: "DESC",
100
+ pageNo: "1",
101
+ facetLib: "",
102
+ facetLibName: "",
103
+ facetAuthor: "",
104
+ facetPublisher: "",
105
+ facetPubYear: "",
106
+ facetSubject: "",
107
+ facetSubjectName: "",
108
+ facetMedia: "",
109
+ facetMediaName: "",
136
110
  },
137
- function (err, res, body) {
138
- if (err || (res && res.statusCode !== 200)) {
139
- let msg = "";
140
-
141
- if (err) {
142
- msg = err;
143
- }
111
+ headers: {
112
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
113
+ Referer: "https://search.suwonlib.go.kr/search",
114
+ "X-Requested-With": "XMLHttpRequest",
115
+ ajax: "true",
116
+ },
117
+ },
118
+ );
144
119
 
145
- if (res && res.statusCode) {
146
- msg = msg + " " + res.statusCode;
147
- }
120
+ if (statusCode !== 200) {
121
+ if (getBook) {
122
+ getBook({ msg: `HTTP ${statusCode}` });
123
+ }
124
+ return;
125
+ }
148
126
 
149
- if (getBook) {
150
- getBook({ msg: msg });
151
- }
152
- } else {
153
- try {
154
- const data = JSON.parse(body);
155
- const booklist = getBookList(data);
156
- const totalCount =
157
- data.SEARCH_RESULT && data.SEARCH_RESULT.SEARCH_COUNT
158
- ? data.SEARCH_RESULT.SEARCH_COUNT
159
- : booklist.length;
160
- getBook(null, {
161
- totalBookCount: totalCount,
162
- booklist,
163
- });
164
- } catch (e) {
165
- getBook({ msg: "Failed to parse response: " + e.message });
166
- }
167
- }
168
- },
169
- );
170
- },
171
- );
127
+ const data = JSON.parse(body);
128
+ const booklist = getBookList(data);
129
+ const totalCount =
130
+ data.SEARCH_RESULT && data.SEARCH_RESULT.SEARCH_COUNT
131
+ ? data.SEARCH_RESULT.SEARCH_COUNT
132
+ : booklist.length;
133
+ getBook(null, {
134
+ totalBookCount: totalCount,
135
+ booklist,
136
+ });
137
+ } catch (err) {
138
+ if (getBook) {
139
+ getBook({ msg: err.toString() });
140
+ }
141
+ }
172
142
  }
173
143
 
174
144
  module.exports = {
@@ -0,0 +1,182 @@
1
+ const getLibraryNames = require("../util.js").getLibraryNames;
2
+ const { get } = require("../http");
3
+ const { JSDOM } = require("jsdom");
4
+
5
+ const libraryList = [
6
+ // Public libraries (시립도서관)
7
+ { code: "MB", name: "수지도서관" },
8
+ { code: "MI", name: "구갈희망누리도서관" },
9
+ { code: "MD", name: "구성도서관" },
10
+ { code: "MK", name: "기흥도서관" },
11
+ { code: "MY", name: "남사도서관" },
12
+ { code: "MF", name: "동백도서관" },
13
+ { code: "NA", name: "동천도서관" },
14
+ { code: "ML", name: "모현도서관" },
15
+ { code: "MM", name: "보라도서관" },
16
+ { code: "MO", name: "상현도서관" },
17
+ { code: "MZ", name: "서농도서관" },
18
+ { code: "NB", name: "성복도서관" },
19
+ { code: "MA", name: "용인중앙도서관" },
20
+ { code: "MJ", name: "양지해밀도서관" },
21
+ { code: "NN", name: "영덕도서관" },
22
+ { code: "MX", name: "이동꿈틀도서관" },
23
+ { code: "ME", name: "죽전도서관" },
24
+ { code: "MP", name: "청덕도서관" },
25
+ { code: "MC", name: "포곡도서관" },
26
+ { code: "MN", name: "흥덕도서관" },
27
+ // Smart libraries (스마트도서관)
28
+ { code: "NO", name: "기흥동행정복지센터스마트도서관" },
29
+ { code: "NJ", name: "기흥역스마트도서관" },
30
+ { code: "NF", name: "동천동행정복지센터스마트도서관" },
31
+ { code: "NS", name: "마북동행정복지센터스마트도서관" },
32
+ { code: "NG", name: "보정동행정복지센터스마트도서관" },
33
+ { code: "NP", name: "상갈동행정복지센터스마트도서관" },
34
+ { code: "NT", name: "상하동행정복지센터스마트도서관" },
35
+ { code: "NH", name: "성복역스마트도서관" },
36
+ { code: "NL", name: "시청스마트도서관" },
37
+ { code: "NI", name: "신봉동행정복지센터스마트도서관" },
38
+ { code: "NR", name: "역북동행정복지센터스마트도서관" },
39
+ { code: "NM", name: "용인중앙시장역스마트도서관" },
40
+ { code: "ND", name: "원삼면스마트도서관" },
41
+ { code: "NQ", name: "유방어린이공원스마트도서관" },
42
+ { code: "NK", name: "죽전역스마트도서관" },
43
+ // Small libraries (작은도서관)
44
+ { code: "NC", name: "고림다온작은도서관" },
45
+ { code: "MS", name: "남사맑은누리작은도서관" },
46
+ { code: "MT", name: "백암면작은도서관" },
47
+ { code: "MW", name: "상현1동작은도서관" },
48
+ { code: "MQ", name: "상현2동작은도서관" },
49
+ { code: "MV", name: "이동천리작은도서관" },
50
+ ];
51
+
52
+ function getLibraryCode(libraryName) {
53
+ const found = libraryList.find((lib) => lib.name === libraryName);
54
+ return found ? found.code : "";
55
+ }
56
+
57
+ async function search(opt, getBook) {
58
+ let title = opt.title;
59
+ let libraryName = opt.libraryName;
60
+
61
+ if (!title) {
62
+ if (getBook) {
63
+ getBook({ msg: "Need a book name" });
64
+ }
65
+ return;
66
+ }
67
+
68
+ if (!libraryName) {
69
+ if (getBook) {
70
+ getBook({ msg: "Need a library name" });
71
+ }
72
+ return;
73
+ }
74
+
75
+ const lcode = getLibraryCode(libraryName);
76
+
77
+ try {
78
+ const { statusCode, body } = await get(
79
+ `https://lib.yongin.go.kr/intro/menu/10003/program/30012/plusSearchResultList.do`,
80
+ {
81
+ qs: {
82
+ searchType: "SIMPLE",
83
+ searchCategory: "ALL",
84
+ searchLibraryArr: lcode,
85
+ searchKey: "ALL",
86
+ searchKeyword: title,
87
+ searchRecordCount: 1000,
88
+ },
89
+ },
90
+ );
91
+
92
+ if (statusCode !== 200) {
93
+ if (getBook) {
94
+ getBook({ msg: `HTTP ${statusCode}` });
95
+ }
96
+ return;
97
+ }
98
+
99
+ const dom = new JSDOM(body);
100
+ const document = dom.window.document;
101
+
102
+ // Extract total count from "총<strong class="highlight">44</strong> 건"
103
+ const highlightElems = document.querySelectorAll(".highlight");
104
+ let count = "0";
105
+ for (const elem of highlightElems) {
106
+ const text = elem.textContent.trim();
107
+ if (/^\d+$/.test(text)) {
108
+ count = text;
109
+ break;
110
+ }
111
+ }
112
+
113
+ const booklist = [];
114
+ const bookItems = document.querySelectorAll(".bookList .listWrap > li");
115
+ bookItems.forEach((li) => {
116
+ // Get title and book URL from .book_name link
117
+ const titleLink = li.querySelector(".book_name a");
118
+ // Title is the text content of the link, excluding the book_kind badge
119
+ let bookTitle = "";
120
+ if (titleLink) {
121
+ // Clone the node and remove the book_kind element to get clean title
122
+ const clone = titleLink.cloneNode(true);
123
+ const bookKind = clone.querySelector(".book_kind");
124
+ if (bookKind) bookKind.remove();
125
+ bookTitle = clone.textContent.trim();
126
+ }
127
+
128
+ // Extract book URL from onclick handler
129
+ let bookUrl = "";
130
+ const onclick = titleLink ? titleLink.getAttribute("onclick") || "" : "";
131
+ const urlMatch = onclick.match(
132
+ /fnSearchResultDetail\((\d+),(\d+),'(\w+)'\)/,
133
+ );
134
+ if (urlMatch) {
135
+ const [, recKey, bookKey, publishFormCode] = urlMatch;
136
+ bookUrl = `https://lib.yongin.go.kr/intro/menu/10003/program/30012/plusSearchResultDetail.do?recKey=${recKey}&bookKey=${bookKey}&publishFormCode=${publishFormCode}`;
137
+ }
138
+
139
+ // Get availability status from .status p
140
+ const statusEl = li.querySelector(".status p");
141
+ const statusText = statusEl ? statusEl.textContent.trim() : "";
142
+ const exist = statusText.includes("대출가능");
143
+
144
+ // Get library name from ".book_info.info03 p" (first p contains library name)
145
+ let libName = "";
146
+ const info03 = li.querySelector(".book_info.info03");
147
+ if (info03) {
148
+ const firstP = info03.querySelector("p");
149
+ if (firstP) {
150
+ libName = firstP.textContent.trim();
151
+ }
152
+ }
153
+
154
+ if (bookTitle) {
155
+ booklist.push({
156
+ libraryName: libName,
157
+ title: bookTitle,
158
+ bookUrl,
159
+ maxoffset: count,
160
+ exist: exist,
161
+ });
162
+ }
163
+ });
164
+
165
+ getBook(null, {
166
+ startPage: opt.startPage,
167
+ totalBookCount: count,
168
+ booklist,
169
+ });
170
+ } catch (err) {
171
+ if (getBook) {
172
+ getBook({ msg: err.toString() });
173
+ }
174
+ }
175
+ }
176
+
177
+ module.exports = {
178
+ search,
179
+ getLibraryNames: function () {
180
+ return getLibraryNames(libraryList);
181
+ },
182
+ };
@@ -97,6 +97,7 @@ function createLibraryTestSuite(lib, description) {
97
97
  this.timeout(60000);
98
98
  let completed = 0;
99
99
  const failures = [];
100
+ let successCount = 0;
100
101
 
101
102
  libraryNames.forEach(function (libraryName) {
102
103
  lib.search(
@@ -116,19 +117,31 @@ function createLibraryTestSuite(lib, description) {
116
117
  book.booklist !== undefined,
117
118
  `${libraryName} should return a booklist`,
118
119
  );
119
- console.log(
120
- ` ✓ ${libraryName}: ${book.totalBookCount} books found`,
121
- );
120
+ if (book.totalBookCount > 0) {
121
+ console.log(
122
+ ` ✓ ${libraryName}: ${book.totalBookCount} books found`,
123
+ );
124
+ successCount++;
125
+ } else {
126
+ // Some libraries (smart/small) may legitimately have 0 results
127
+ console.log(
128
+ ` - ${libraryName}: 0 books found (may be expected for small collections)`,
129
+ );
130
+ }
122
131
  }
123
132
 
124
133
  if (completed === libraryNames.length) {
125
- if (failures.length > 0) {
126
- console.log(` Warning: ${failures.length} libraries failed`);
127
- }
134
+ console.log(
135
+ ` Summary: ${successCount} libraries with results, ${failures.length} errors`,
136
+ );
128
137
  assert.ok(
129
138
  failures.length < libraryNames.length,
130
139
  "At least one library should be searchable",
131
140
  );
141
+ assert.ok(
142
+ successCount > 0,
143
+ "At least one library should return results",
144
+ );
132
145
  done();
133
146
  }
134
147
  },
@@ -0,0 +1,4 @@
1
+ const lib = require("../src/library/yongin");
2
+ const { createLibraryTestSuite } = require("./helpers/libraryTestSuite");
3
+
4
+ createLibraryTestSuite(lib, "용인시 도서관");