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 +7 -5
- package/src/dongnelibrary.js +2 -1
- package/src/http.js +109 -0
- package/src/library/gg.js +60 -60
- package/src/library/gunpo.js +29 -37
- package/src/library/hscity.js +47 -48
- package/src/library/osan.js +90 -82
- package/src/library/snlib.js +64 -57
- package/src/library/suwon.js +69 -99
- package/src/library/yongin.js +182 -0
- package/test/helpers/libraryTestSuite.js +19 -6
- package/test/yongin.spec.js +4 -0
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dongnelibrary",
|
|
3
3
|
"engines": {
|
|
4
|
-
"node": ">=
|
|
4
|
+
"node": ">=18.0.0"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.2.
|
|
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
|
-
"
|
|
61
|
+
"undici": "^6.23.0"
|
|
60
62
|
}
|
|
61
63
|
}
|
package/src/dongnelibrary.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
59
|
+
if (statusCode !== 200) {
|
|
60
|
+
if (getBook) {
|
|
61
|
+
getBook({ msg: `HTTP ${statusCode}` });
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 = {
|
package/src/library/gunpo.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
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
|
-
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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 = {
|
package/src/library/hscity.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const getLibraryNames = require("../util.js").getLibraryNames;
|
|
2
2
|
const jquery = require("jquery");
|
|
3
|
-
const
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
72
|
+
if (statusCode !== 200) {
|
|
73
|
+
if (getBook) {
|
|
74
|
+
getBook({ msg: `HTTP ${statusCode}` });
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
88
78
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 = {
|
package/src/library/osan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const getLibraryNames = require("../util.js").getLibraryNames;
|
|
2
|
-
const
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
58
|
+
if (statusCode !== 200) {
|
|
59
|
+
if (getBook) {
|
|
60
|
+
getBook({ msg: `HTTP ${statusCode}` });
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 = {
|
package/src/library/snlib.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const getLibraryNames = require("../util.js").getLibraryNames;
|
|
2
2
|
const jquery = require("jquery");
|
|
3
|
-
const
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
69
|
+
if (statusCode !== 200) {
|
|
70
|
+
if (getBook) {
|
|
71
|
+
getBook({ msg: `HTTP ${statusCode}` });
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 = {
|
package/src/library/suwon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const getLibraryNames = require("../util.js").getLibraryNames;
|
|
2
|
-
const
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
120
|
+
if (statusCode !== 200) {
|
|
121
|
+
if (getBook) {
|
|
122
|
+
getBook({ msg: `HTTP ${statusCode}` });
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
148
126
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
},
|