dongnelibrary 0.3.17 → 0.3.21
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/.claude/settings.json +30 -1
- package/.claude/settings.local.json +26 -1
- package/.playwright-cli/console-2026-05-15T13-42-50-460Z.log +1 -0
- package/.playwright-cli/console-2026-05-15T13-43-40-832Z.log +10 -0
- package/.playwright-cli/console-2026-05-18T12-47-04-141Z.log +14 -0
- package/.playwright-cli/page-2026-05-07T11-53-37-538Z.yml +836 -0
- package/.playwright-cli/page-2026-05-07T11-54-02-314Z.yml +658 -0
- package/.playwright-cli/page-2026-05-07T12-03-12-317Z.yml +310 -0
- package/.playwright-cli/page-2026-05-08T13-54-43-061Z.yml +276 -0
- package/.playwright-cli/page-2026-05-08T13-54-45-094Z.yml +276 -0
- package/.playwright-cli/page-2026-05-08T13-54-53-014Z.yml +201 -0
- package/.playwright-cli/page-2026-05-08T13-56-02-097Z.yml +726 -0
- package/.playwright-cli/page-2026-05-08T13-56-33-697Z.yml +226 -0
- package/.playwright-cli/page-2026-05-15T13-42-51-601Z.yml +47 -0
- package/.playwright-cli/page-2026-05-15T13-43-40-955Z.yml +47 -0
- package/.playwright-cli/page-2026-05-15T13-43-43-563Z.yml +47 -0
- package/.playwright-cli/page-2026-05-18T12-47-05-318Z.yml +135 -0
- package/README.md +59 -2
- package/dist/dongnelibrary.d.ts.map +1 -1
- package/dist/dongnelibrary.js +36 -0
- package/dist/dongnelibrary.js.map +1 -1
- package/dist/localLibraryModule/ansan.d.ts +6 -0
- package/dist/localLibraryModule/ansan.d.ts.map +1 -0
- package/dist/localLibraryModule/ansan.js +76 -0
- package/dist/localLibraryModule/ansan.js.map +1 -0
- package/dist/localLibraryModule/bcl.d.ts +6 -0
- package/dist/localLibraryModule/bcl.d.ts.map +1 -0
- package/dist/localLibraryModule/bcl.js +75 -0
- package/dist/localLibraryModule/bcl.js.map +1 -0
- package/dist/localLibraryModule/gangnam.d.ts +6 -0
- package/dist/localLibraryModule/gangnam.d.ts.map +1 -0
- package/dist/localLibraryModule/gangnam.js +100 -0
- package/dist/localLibraryModule/gangnam.js.map +1 -0
- package/dist/localLibraryModule/gangseo.d.ts +6 -0
- package/dist/localLibraryModule/gangseo.d.ts.map +1 -0
- package/dist/localLibraryModule/gangseo.js +90 -0
- package/dist/localLibraryModule/gangseo.js.map +1 -0
- package/dist/localLibraryModule/gbelib.d.ts +6 -0
- package/dist/localLibraryModule/gbelib.d.ts.map +1 -0
- package/dist/localLibraryModule/gbelib.js +122 -0
- package/dist/localLibraryModule/gbelib.js.map +1 -0
- package/dist/localLibraryModule/geoje.d.ts +6 -0
- package/dist/localLibraryModule/geoje.d.ts.map +1 -0
- package/dist/localLibraryModule/geoje.js +75 -0
- package/dist/localLibraryModule/geoje.js.map +1 -0
- package/dist/localLibraryModule/gimhae.d.ts +6 -0
- package/dist/localLibraryModule/gimhae.d.ts.map +1 -0
- package/dist/localLibraryModule/gimhae.js +81 -0
- package/dist/localLibraryModule/gimhae.js.map +1 -0
- package/dist/localLibraryModule/gunsan.d.ts +6 -0
- package/dist/localLibraryModule/gunsan.d.ts.map +1 -0
- package/dist/localLibraryModule/gunsan.js +89 -0
- package/dist/localLibraryModule/gunsan.js.map +1 -0
- package/dist/localLibraryModule/gwanak.d.ts +6 -0
- package/dist/localLibraryModule/gwanak.d.ts.map +1 -0
- package/dist/localLibraryModule/gwanak.js +103 -0
- package/dist/localLibraryModule/gwanak.js.map +1 -0
- package/dist/localLibraryModule/gwe.d.ts +6 -0
- package/dist/localLibraryModule/gwe.d.ts.map +1 -0
- package/dist/localLibraryModule/gwe.js +84 -0
- package/dist/localLibraryModule/gwe.js.map +1 -0
- package/dist/localLibraryModule/junggulib.d.ts +6 -0
- package/dist/localLibraryModule/junggulib.d.ts.map +1 -0
- package/dist/localLibraryModule/junggulib.js +96 -0
- package/dist/localLibraryModule/junggulib.js.map +1 -0
- package/dist/localLibraryModule/mokpolib.d.ts +6 -0
- package/dist/localLibraryModule/mokpolib.d.ts.map +1 -0
- package/dist/localLibraryModule/mokpolib.js +104 -0
- package/dist/localLibraryModule/mokpolib.js.map +1 -0
- package/dist/localLibraryModule/nowon.d.ts +6 -0
- package/dist/localLibraryModule/nowon.d.ts.map +1 -0
- package/dist/localLibraryModule/nowon.js +95 -0
- package/dist/localLibraryModule/nowon.js.map +1 -0
- package/dist/localLibraryModule/paju.d.ts +6 -0
- package/dist/localLibraryModule/paju.d.ts.map +1 -0
- package/dist/localLibraryModule/paju.js +94 -0
- package/dist/localLibraryModule/paju.js.map +1 -0
- package/dist/localLibraryModule/pohang.d.ts +6 -0
- package/dist/localLibraryModule/pohang.d.ts.map +1 -0
- package/dist/localLibraryModule/pohang.js +83 -0
- package/dist/localLibraryModule/pohang.js.map +1 -0
- package/dist/localLibraryModule/siheung.d.ts +6 -0
- package/dist/localLibraryModule/siheung.d.ts.map +1 -0
- package/dist/localLibraryModule/siheung.js +65 -0
- package/dist/localLibraryModule/siheung.js.map +1 -0
- package/dist/localLibraryModule/yangcheon.d.ts +6 -0
- package/dist/localLibraryModule/yangcheon.d.ts.map +1 -0
- package/dist/localLibraryModule/yangcheon.js +90 -0
- package/dist/localLibraryModule/yangcheon.js.map +1 -0
- package/dist/localLibraryModule/ydplib.d.ts +6 -0
- package/dist/localLibraryModule/ydplib.d.ts.map +1 -0
- package/dist/localLibraryModule/ydplib.js +101 -0
- package/dist/localLibraryModule/ydplib.js.map +1 -0
- package/docs/library-module-python-scripts.md +381 -0
- package/docs//355/206/265/355/225/251/353/217/204/354/204/234/302/240/353/204/244/355/212/270/354/233/214/355/201/254-/353/252/251/353/241/235.md +184 -0
- package/docs//355/206/265/355/225/251/353/217/204/354/204/234/352/264/200-/353/204/244/355/212/270/354/233/214/355/201/254-/353/252/251/353/241/235.md +19 -19
- package/package.json +44 -6
- package/scripts/analyze-html-library.sh +300 -0
- package/scripts/analyze-library.sh +261 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dongnelibrary",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"description": "책을 빌릴 수 있는지 확인한다.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.22.0"
|
|
@@ -16,22 +16,40 @@
|
|
|
16
16
|
"prepublishOnly": "npm run build",
|
|
17
17
|
"test": "npm run build && node --test test/*.spec.js",
|
|
18
18
|
"dongne": "npm run build && node --test test/dongne.spec.js",
|
|
19
|
+
"ansan": "npm run build && node --test test/ansan.spec.js",
|
|
19
20
|
"asan": "npm run build && node --test test/asan.spec.js",
|
|
21
|
+
"bcl": "npm run build && node --test test/bcl.spec.js",
|
|
20
22
|
"cbelib": "npm run build && node --test test/cbelib.spec.js",
|
|
21
23
|
"daegu": "npm run build && node --test test/daegu.spec.js",
|
|
24
|
+
"gangnam": "npm run build && node --test test/gangnam.spec.js",
|
|
25
|
+
"gangseo": "npm run build && node --test test/gangseo.spec.js",
|
|
26
|
+
"gbelib": "npm run build && node --test test/gbelib.spec.js",
|
|
27
|
+
"geoje": "npm run build && node --test test/geoje.spec.js",
|
|
22
28
|
"gg": "npm run build && node --test test/gg.spec.js",
|
|
23
29
|
"gjcity": "npm run build && node --test test/gjcity.spec.js",
|
|
30
|
+
"gimhae": "npm run build && node --test test/gimhae.spec.js",
|
|
24
31
|
"gunpo": "npm run build && node --test test/gunpo.spec.js",
|
|
32
|
+
"gunsan": "npm run build && node --test test/gunsan.spec.js",
|
|
33
|
+
"gwanak": "npm run build && node --test test/gwanak.spec.js",
|
|
34
|
+
"gwe": "npm run build && node --test test/gwe.spec.js",
|
|
25
35
|
"hanamlib": "npm run build && node --test test/hanamlib.spec.js",
|
|
26
36
|
"hscity": "npm run build && node --test test/hscity.spec.js",
|
|
37
|
+
"nowon": "npm run build && node --test test/nowon.spec.js",
|
|
27
38
|
"ice": "npm run build && node --test test/ice.spec.js",
|
|
39
|
+
"junggulib": "npm run build && node --test test/junggulib.spec.js",
|
|
40
|
+
"mokpolib": "npm run build && node --test test/mokpolib.spec.js",
|
|
41
|
+
"paju": "npm run build && node --test test/paju.spec.js",
|
|
28
42
|
"osan": "npm run build && node --test test/osan.spec.js",
|
|
43
|
+
"pohang": "npm run build && node --test test/pohang.spec.js",
|
|
29
44
|
"ptlib": "npm run build && node --test test/ptlib.spec.js",
|
|
45
|
+
"siheung": "npm run build && node --test test/siheung.spec.js",
|
|
30
46
|
"snlib": "npm run build && node --test test/snlib.spec.js",
|
|
31
47
|
"suwon": "npm run build && node --test test/suwon.spec.js",
|
|
32
48
|
"uwlib": "npm run build && node --test test/uwlib.spec.js",
|
|
49
|
+
"yangcheon": "npm run build && node --test test/yangcheon.spec.js",
|
|
33
50
|
"yjlib": "npm run build && node --test test/yjlib.spec.js",
|
|
34
51
|
"yongin": "npm run build && node --test test/yongin.spec.js",
|
|
52
|
+
"ydplib": "npm run build && node --test test/ydplib.spec.js",
|
|
35
53
|
"yplib": "npm run build && node --test test/yplib.spec.js",
|
|
36
54
|
"jeju": "npm run build && node --test test/jeju.spec.js",
|
|
37
55
|
"wonju": "npm run build && node --test test/wonju.spec.js",
|
|
@@ -48,16 +66,30 @@
|
|
|
48
66
|
"book",
|
|
49
67
|
"public library",
|
|
50
68
|
"도서 검색",
|
|
69
|
+
"안산시도서관",
|
|
51
70
|
"아산시도서관",
|
|
71
|
+
"부천시립도서관",
|
|
72
|
+
"강남구통합도서관",
|
|
73
|
+
"강서구통합도서관",
|
|
52
74
|
"충청북도교육도서관",
|
|
53
75
|
"대구광역시통합도서관",
|
|
76
|
+
"경상북도교육청통합도서관",
|
|
54
77
|
"경기교육통합도서관",
|
|
78
|
+
"거제시도서관",
|
|
79
|
+
"군산시도서관",
|
|
55
80
|
"경기광주시도서관",
|
|
81
|
+
"김해통합도서관",
|
|
56
82
|
"군포시도서관",
|
|
83
|
+
"관악구통합도서관",
|
|
84
|
+
"강원특별자치도교육청도서관",
|
|
57
85
|
"하남시도서관",
|
|
86
|
+
"노원구립도서관",
|
|
58
87
|
"성남시도서관",
|
|
88
|
+
"파주시도서관",
|
|
59
89
|
"오산시도서관",
|
|
60
90
|
"평택시도서관",
|
|
91
|
+
"포항시립도서관",
|
|
92
|
+
"시흥시도서관",
|
|
61
93
|
"화성시립도서관",
|
|
62
94
|
"수원시도서관",
|
|
63
95
|
"의왕시도서관",
|
|
@@ -66,8 +98,12 @@
|
|
|
66
98
|
"양평군도서관",
|
|
67
99
|
"용인시도서관",
|
|
68
100
|
"인천광역시교육청통합공공도서관",
|
|
101
|
+
"중구구립도서관",
|
|
102
|
+
"목포시통합도서관",
|
|
69
103
|
"제주시도서관",
|
|
70
|
-
"원주시립통합도서관"
|
|
104
|
+
"원주시립통합도서관",
|
|
105
|
+
"양천구도서관",
|
|
106
|
+
"영등포구립도서관"
|
|
71
107
|
],
|
|
72
108
|
"author": "<autoscripts@gmail.com>",
|
|
73
109
|
"license": "MIT",
|
|
@@ -82,9 +118,9 @@
|
|
|
82
118
|
"commander": "^14.0.3",
|
|
83
119
|
"configstore": "^4.0.0",
|
|
84
120
|
"figlet": "^1.2.1",
|
|
85
|
-
"jsdom": "^21.1.
|
|
86
|
-
"lodash": "^4.
|
|
87
|
-
"undici": "^6.
|
|
121
|
+
"jsdom": "^21.1.2",
|
|
122
|
+
"lodash": "^4.18.1",
|
|
123
|
+
"undici": "^6.25.0"
|
|
88
124
|
},
|
|
89
125
|
"devDependencies": {
|
|
90
126
|
"@types/configstore": "^6.0.2",
|
|
@@ -96,7 +132,9 @@
|
|
|
96
132
|
},
|
|
97
133
|
"overrides": {
|
|
98
134
|
"acorn": ">=5.7.4",
|
|
99
|
-
"minimist": ">=1.2.2"
|
|
135
|
+
"minimist": ">=1.2.2",
|
|
136
|
+
"@tootallnate/once": "^2.0.1",
|
|
137
|
+
"ws": "^8.20.1"
|
|
100
138
|
},
|
|
101
139
|
"tonicExampleFilename": "src/example.js"
|
|
102
140
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# HTML 기반 도서관 웹사이트 분석 도구
|
|
3
|
+
# SSR HTML 사이트(wkcms, kolaseek, jnet 등) — 세션 쿠키·폼 구조 분석에 특화
|
|
4
|
+
# SPA/JSON API 분석은 scripts/analyze-library.sh 사용
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
cmd=${1:-help}
|
|
9
|
+
shift 2>/dev/null || true
|
|
10
|
+
|
|
11
|
+
COOKIE_FILE=$(mktemp)
|
|
12
|
+
trap "rm -f '$COOKIE_FILE'" EXIT
|
|
13
|
+
|
|
14
|
+
_usage() {
|
|
15
|
+
cat <<'EOF'
|
|
16
|
+
사용법: bash scripts/analyze-html-library.sh <명령> [인수...]
|
|
17
|
+
|
|
18
|
+
명령:
|
|
19
|
+
session-init URL 세션 쿠키(JSESSIONID) 획득 및 출력
|
|
20
|
+
session-search INIT_URL SEARCH_URL 세션 초기화 후 검색 요약 출력
|
|
21
|
+
list-codes URL [INPUT_NAME] HTML 폼에서 도서관 코드 추출 (기본: manage_code)
|
|
22
|
+
form-info URL 폼 action, hidden 입력값, 체크박스 name 목록
|
|
23
|
+
html-context URL PATTERN [N] HTML에서 패턴 주변 컨텍스트 출력 (기본 N=300자)
|
|
24
|
+
perf-test INIT_URL SEARCH_TPL VALUES.. display 값별 응답 시간 측정 (세션 필요)
|
|
25
|
+
wkcms-codes HOST wkcms 플랫폼 도서관 코드 자동 수집 (포트 9080)
|
|
26
|
+
ssl-check HOST[:PORT] SSL 인증서 체인 확인
|
|
27
|
+
|
|
28
|
+
예시:
|
|
29
|
+
bash scripts/analyze-html-library.sh wkcms-codes "lib.geoje.go.kr"
|
|
30
|
+
|
|
31
|
+
bash scripts/analyze-html-library.sh session-init \
|
|
32
|
+
"https://lib.geoje.go.kr:9080/wkcms/KBookSearch/BookSearchPage/MA"
|
|
33
|
+
|
|
34
|
+
bash scripts/analyze-html-library.sh session-search \
|
|
35
|
+
"https://lib.geoje.go.kr:9080/wkcms/KBookSearch/BookSearchPage/MA" \
|
|
36
|
+
"https://lib.geoje.go.kr:9080/wkcms/KBookSearch/BookNomalSearch/MA?search_txt=별&book_type=BOOK&pageno=1&display=20&detail_search_type=Nomal&manage_code=MA&option=nomal&libcode=ALL&input_search_text=별&real_search_text=별&now_search_txt=별&hidden_book_type=BOOK&orderby=ASC&orderby_item=TITLE_INFO_SORT"
|
|
37
|
+
|
|
38
|
+
bash scripts/analyze-html-library.sh list-codes \
|
|
39
|
+
"https://example.library.go.kr/search.do" searchLibraryArr
|
|
40
|
+
|
|
41
|
+
bash scripts/analyze-html-library.sh form-info \
|
|
42
|
+
"https://example.library.go.kr/searchResultList.do"
|
|
43
|
+
|
|
44
|
+
bash scripts/analyze-html-library.sh html-context \
|
|
45
|
+
"https://lib.geoje.go.kr:9080/wkcms/KBookSearch/BookNomalSearch/MA?..." "ul.book_info"
|
|
46
|
+
|
|
47
|
+
bash scripts/analyze-html-library.sh perf-test \
|
|
48
|
+
"https://HOST:9080/wkcms/KBookSearch/BookSearchPage/MA" \
|
|
49
|
+
"https://HOST:9080/wkcms/KBookSearch/BookNomalSearch/MA?search_txt=별&display=__DISPLAY__&book_type=BOOK&pageno=1&detail_search_type=Nomal&manage_code=MA&option=nomal&libcode=ALL&input_search_text=별&real_search_text=별&now_search_txt=별&hidden_book_type=BOOK&orderby=ASC&orderby_item=TITLE_INFO_SORT" \
|
|
50
|
+
10 20 50 100 200
|
|
51
|
+
|
|
52
|
+
bash scripts/analyze-html-library.sh ssl-check "lib.geoje.go.kr:9080"
|
|
53
|
+
|
|
54
|
+
EOF
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case "$cmd" in
|
|
58
|
+
|
|
59
|
+
session-init)
|
|
60
|
+
url="${1:?오류: URL 필요}"
|
|
61
|
+
echo "[세션 초기화: $url]"
|
|
62
|
+
status=$(curl -sk -o /dev/null -w "%{http_code}" -c "$COOKIE_FILE" "$url")
|
|
63
|
+
echo "HTTP 상태: $status"
|
|
64
|
+
echo ""
|
|
65
|
+
echo "[획득한 쿠키]"
|
|
66
|
+
grep -v '^#' "$COOKIE_FILE" | grep -v '^$' || echo "(쿠키 없음)"
|
|
67
|
+
;;
|
|
68
|
+
|
|
69
|
+
session-search)
|
|
70
|
+
init_url="${1:?오류: INIT_URL 필요}"
|
|
71
|
+
search_url="${2:?오류: SEARCH_URL 필요}"
|
|
72
|
+
tmpfile=$(mktemp)
|
|
73
|
+
trap "rm -f '$tmpfile' '$COOKIE_FILE'" EXIT
|
|
74
|
+
echo "[세션 초기화: $init_url]"
|
|
75
|
+
curl -sk -o /dev/null -c "$COOKIE_FILE" "$init_url"
|
|
76
|
+
echo "[검색 요청: $search_url]"
|
|
77
|
+
curl -sk -b "$COOKIE_FILE" "$search_url" > "$tmpfile"
|
|
78
|
+
python3 - "$tmpfile" <<'PYEOF'
|
|
79
|
+
import sys, re
|
|
80
|
+
with open(sys.argv[1]) as f:
|
|
81
|
+
html = f.read()
|
|
82
|
+
total_match = re.search(r'총\s*([\d,]+)\s*건', html)
|
|
83
|
+
print('총 건수:', total_match.group(1) if total_match else '확인 안 됨')
|
|
84
|
+
print('응답 크기:', f'{len(html):,}바이트')
|
|
85
|
+
# 책 제목 샘플: wkcms는 h4, 기타는 dt.tit a
|
|
86
|
+
titles = re.findall(r'<h4[^>]*>\s*(.*?)\s*</h4>', html, re.DOTALL)[:3]
|
|
87
|
+
if not titles:
|
|
88
|
+
titles = re.findall(r'<dt[^>]*class="tit"[^>]*>.*?<a[^>]*>(.*?)</a>', html, re.DOTALL)[:3]
|
|
89
|
+
if titles:
|
|
90
|
+
print('\n[책 제목 샘플]')
|
|
91
|
+
for t in titles:
|
|
92
|
+
print(' -', re.sub(r'<[^>]+>', '', t).strip()[:80])
|
|
93
|
+
else:
|
|
94
|
+
print('\n책 제목을 찾지 못했습니다. html-context 명령으로 구조 확인 권장')
|
|
95
|
+
PYEOF
|
|
96
|
+
;;
|
|
97
|
+
|
|
98
|
+
list-codes)
|
|
99
|
+
url="${1:?오류: URL 필요}"
|
|
100
|
+
input_name="${2:-manage_code}"
|
|
101
|
+
tmpfile=$(mktemp)
|
|
102
|
+
trap "rm -f '$tmpfile' '$COOKIE_FILE'" EXIT
|
|
103
|
+
echo "[도서관 코드 추출: $url]"
|
|
104
|
+
echo "[파라미터 name: $input_name]"
|
|
105
|
+
echo ""
|
|
106
|
+
curl -skL "$url" > "$tmpfile"
|
|
107
|
+
python3 - "$input_name" "$tmpfile" <<'PYEOF'
|
|
108
|
+
import sys, re
|
|
109
|
+
input_name = sys.argv[1]
|
|
110
|
+
with open(sys.argv[2]) as f:
|
|
111
|
+
html = f.read()
|
|
112
|
+
|
|
113
|
+
print(f'[응답 크기: {len(html):,}바이트]')
|
|
114
|
+
|
|
115
|
+
# 방법 1: checkbox 또는 radio (name 앞 또는 뒤, 값은 영문+숫자 모두 허용)
|
|
116
|
+
p1 = rf"name=['\"]?{re.escape(input_name)}['\"]?[^>]+value=['\"]?([A-Za-z0-9]+)['\"]?"
|
|
117
|
+
p2 = rf"value=['\"]?([A-Za-z0-9]+)['\"]?[^>]+name=['\"]?{re.escape(input_name)}['\"]?"
|
|
118
|
+
codes = [m for m in re.findall(p1, html)] + [m for m in re.findall(p2, html)]
|
|
119
|
+
|
|
120
|
+
# 방법 2: <select> option
|
|
121
|
+
sel_pat = rf'(?s)<select[^>]+name=["\']?{re.escape(input_name)}["\']?.*?</select>'
|
|
122
|
+
sel = re.search(sel_pat, html)
|
|
123
|
+
if sel:
|
|
124
|
+
codes = re.findall(r'<option[^>]+value="([^"]+)"', sel.group())
|
|
125
|
+
|
|
126
|
+
unique = [c for c in dict.fromkeys(codes) if c not in ('ALL', '', '0')]
|
|
127
|
+
if unique:
|
|
128
|
+
print(f'{len(unique)}개 코드 발견:')
|
|
129
|
+
for c in unique:
|
|
130
|
+
print(f' {c}')
|
|
131
|
+
else:
|
|
132
|
+
print(f'"{input_name}" 파라미터를 찾을 수 없습니다.')
|
|
133
|
+
names = list(dict.fromkeys(re.findall(r'name=["\']([^"\']+)["\']', html)))
|
|
134
|
+
print('힌트 — 페이지의 파라미터 name 목록:', names[:20])
|
|
135
|
+
if len(html) < 500:
|
|
136
|
+
print('[응답 내용 (짧음)]')
|
|
137
|
+
print(html[:500])
|
|
138
|
+
PYEOF
|
|
139
|
+
;;
|
|
140
|
+
|
|
141
|
+
form-info)
|
|
142
|
+
url="${1:?오류: URL 필요}"
|
|
143
|
+
tmpfile=$(mktemp)
|
|
144
|
+
trap "rm -f '$tmpfile' '$COOKIE_FILE'" EXIT
|
|
145
|
+
echo "[폼 정보 추출: $url]"
|
|
146
|
+
echo ""
|
|
147
|
+
curl -skL "$url" > "$tmpfile"
|
|
148
|
+
python3 - "$tmpfile" <<'PYEOF'
|
|
149
|
+
import sys, re
|
|
150
|
+
with open(sys.argv[1]) as f:
|
|
151
|
+
html = f.read()
|
|
152
|
+
forms = re.findall(r'(?s)<form[^>]*>.*?</form>', html)
|
|
153
|
+
print(f'총 {len(forms)}개 폼')
|
|
154
|
+
print()
|
|
155
|
+
for i, form in enumerate(forms[:5]):
|
|
156
|
+
action = re.search(r'action=["\']([^"\']+)["\']', form)
|
|
157
|
+
method = re.search(r'method=["\']([^"\']+)["\']', form, re.I)
|
|
158
|
+
print(f'--- 폼 {i+1} ---')
|
|
159
|
+
print(f' action : {action.group(1) if action else "(없음)"}')
|
|
160
|
+
print(f' method : {method.group(1).upper() if method else "GET"}')
|
|
161
|
+
hiddens = re.findall(r'<input[^>]+type=["\']hidden["\'][^>]+>', form, re.I)
|
|
162
|
+
if hiddens:
|
|
163
|
+
print(f' hidden ({len(hiddens)}개):')
|
|
164
|
+
for h in hiddens[:10]:
|
|
165
|
+
n = re.search(r'name=["\']([^"\']+)["\']', h)
|
|
166
|
+
v = re.search(r'value=["\']([^"\']*)["\']', h)
|
|
167
|
+
if n:
|
|
168
|
+
print(f' {n.group(1)} = {v.group(1) if v else ""}')
|
|
169
|
+
checkboxes = re.findall(r'<input[^>]+type=["\']checkbox["\'][^>]+>', form, re.I)
|
|
170
|
+
if checkboxes:
|
|
171
|
+
cb_names = list(dict.fromkeys(
|
|
172
|
+
re.search(r'name=["\']([^"\']+)["\']', c).group(1)
|
|
173
|
+
for c in checkboxes if re.search(r'name=["\']', c)
|
|
174
|
+
))
|
|
175
|
+
print(f' checkbox names : {cb_names[:8]}')
|
|
176
|
+
print()
|
|
177
|
+
PYEOF
|
|
178
|
+
;;
|
|
179
|
+
|
|
180
|
+
html-context)
|
|
181
|
+
url="${1:?오류: URL 필요}"
|
|
182
|
+
pattern="${2:?오류: PATTERN 필요}"
|
|
183
|
+
n="${3:-300}"
|
|
184
|
+
tmpfile=$(mktemp)
|
|
185
|
+
trap "rm -f '$tmpfile'" EXIT
|
|
186
|
+
curl -sk "$url" > "$tmpfile"
|
|
187
|
+
python3 - "$tmpfile" "$pattern" "$n" <<'PYEOF'
|
|
188
|
+
import sys, re
|
|
189
|
+
path, pattern, n = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
190
|
+
with open(path) as f:
|
|
191
|
+
content = f.read()
|
|
192
|
+
escaped = re.escape(pattern)
|
|
193
|
+
matches = re.findall(fr'.{{0,{n}}}{escaped}.{{0,{n}}}', content, re.DOTALL)
|
|
194
|
+
if not matches:
|
|
195
|
+
print(f'"{pattern}" 주변 컨텍스트를 찾을 수 없습니다.')
|
|
196
|
+
print(f'응답 크기: {len(content):,}바이트')
|
|
197
|
+
sys.exit(0)
|
|
198
|
+
for i, m in enumerate(matches[:3]):
|
|
199
|
+
print(f'--- match {i+1}/{min(len(matches),3)} ---')
|
|
200
|
+
print(m[:1000])
|
|
201
|
+
print()
|
|
202
|
+
PYEOF
|
|
203
|
+
;;
|
|
204
|
+
|
|
205
|
+
perf-test)
|
|
206
|
+
init_url="${1:?오류: INIT_URL 필요}"
|
|
207
|
+
search_tpl="${2:?오류: SEARCH_TPL (display=__DISPLAY__ 포함) 필요}"
|
|
208
|
+
shift 2
|
|
209
|
+
if [ $# -eq 0 ]; then
|
|
210
|
+
echo "오류: 테스트할 display 값 목록이 필요합니다" >&2
|
|
211
|
+
echo "예시: bash scripts/analyze-html-library.sh perf-test INIT_URL TPL 10 20 50 100 200" >&2
|
|
212
|
+
exit 1
|
|
213
|
+
fi
|
|
214
|
+
echo "[성능 테스트]"
|
|
215
|
+
echo "세션 초기화: $init_url"
|
|
216
|
+
echo "검색 템플릿: ${search_tpl:0:80}..."
|
|
217
|
+
echo ""
|
|
218
|
+
for val in "$@"; do
|
|
219
|
+
search_url="${search_tpl/__DISPLAY__/$val}"
|
|
220
|
+
curl -sk -o /dev/null -c "$COOKIE_FILE" "$init_url"
|
|
221
|
+
printf "display=%-6s " "$val:"
|
|
222
|
+
curl -sk -b "$COOKIE_FILE" -o /dev/null -w "HTTP %{http_code} %{time_total}s\n" "$search_url"
|
|
223
|
+
done
|
|
224
|
+
;;
|
|
225
|
+
|
|
226
|
+
wkcms-codes)
|
|
227
|
+
host="${1:?오류: HOST 필요}"
|
|
228
|
+
host="${host%/}"
|
|
229
|
+
wkcms_url="https://$host:9080/wkcms/KBookSearch/BookSearchPage/MA"
|
|
230
|
+
tmpfile=$(mktemp)
|
|
231
|
+
trap "rm -f '$tmpfile' '$COOKIE_FILE'" EXIT
|
|
232
|
+
echo "[wkcms 도서관 코드 수집: $host]"
|
|
233
|
+
echo "URL: $wkcms_url"
|
|
234
|
+
echo ""
|
|
235
|
+
curl -sk -c "$COOKIE_FILE" "$wkcms_url" -o /dev/null
|
|
236
|
+
curl -sk -b "$COOKIE_FILE" "$wkcms_url" > "$tmpfile"
|
|
237
|
+
python3 - "$host" "$tmpfile" <<'PYEOF'
|
|
238
|
+
import sys, re
|
|
239
|
+
host = sys.argv[1]
|
|
240
|
+
with open(sys.argv[2]) as f:
|
|
241
|
+
html = f.read()
|
|
242
|
+
|
|
243
|
+
# manage_code 체크박스 추출 (속성 순서 무관)
|
|
244
|
+
inputs = re.findall(r'<input[^>]+>', html, re.I)
|
|
245
|
+
codes = []
|
|
246
|
+
for inp in inputs:
|
|
247
|
+
if 'manage_code' in inp.lower():
|
|
248
|
+
val = re.search(r'value=["\']([A-Z]+)["\']', inp)
|
|
249
|
+
if val and val.group(1) not in ('ALL', ''):
|
|
250
|
+
codes.append(val.group(1))
|
|
251
|
+
|
|
252
|
+
unique = list(dict.fromkeys(codes))
|
|
253
|
+
if unique:
|
|
254
|
+
print(f'총 {len(unique)}개 도서관 코드:')
|
|
255
|
+
for c in unique:
|
|
256
|
+
print(f' {c}')
|
|
257
|
+
print()
|
|
258
|
+
print('[libraryList 템플릿]')
|
|
259
|
+
print('const libraryList: LibraryInfo[] = [')
|
|
260
|
+
for c in unique:
|
|
261
|
+
print(f' {{ code: "{c}", name: "TODO" }},')
|
|
262
|
+
print('];')
|
|
263
|
+
else:
|
|
264
|
+
print('코드를 찾지 못했습니다.')
|
|
265
|
+
print(f'응답 크기: {len(html):,}바이트')
|
|
266
|
+
if len(html) < 1000:
|
|
267
|
+
print('[응답 내용]')
|
|
268
|
+
print(html[:500])
|
|
269
|
+
else:
|
|
270
|
+
names = list(dict.fromkeys(re.findall(r'name=["\']([^"\']+)["\']', html)))
|
|
271
|
+
print('발견된 name 속성 목록:', names[:20])
|
|
272
|
+
print()
|
|
273
|
+
print('힌트: wkcms 플랫폼이 아닐 수 있습니다.')
|
|
274
|
+
print(' 포트 9080이 맞는지, /wkcms/ 경로가 맞는지 확인하세요.')
|
|
275
|
+
PYEOF
|
|
276
|
+
;;
|
|
277
|
+
|
|
278
|
+
ssl-check)
|
|
279
|
+
host="${1:?오류: HOST[:PORT] 필요}"
|
|
280
|
+
if [[ "$host" == *:* ]]; then
|
|
281
|
+
hostname="${host%:*}"
|
|
282
|
+
port="${host##*:}"
|
|
283
|
+
else
|
|
284
|
+
hostname="$host"
|
|
285
|
+
port="443"
|
|
286
|
+
fi
|
|
287
|
+
echo "[SSL 인증서 체인 확인: $hostname:$port]"
|
|
288
|
+
openssl s_client -connect "$hostname:$port" -servername "$hostname" 2>&1 | head -20
|
|
289
|
+
;;
|
|
290
|
+
|
|
291
|
+
help|--help|-h)
|
|
292
|
+
_usage
|
|
293
|
+
;;
|
|
294
|
+
|
|
295
|
+
*)
|
|
296
|
+
echo "알 수 없는 명령: $cmd" >&2
|
|
297
|
+
_usage >&2
|
|
298
|
+
exit 1
|
|
299
|
+
;;
|
|
300
|
+
esac
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPA 기반 도서관 웹사이트 분석 도구
|
|
3
|
+
# 새 도서관 모듈 추가 시 API 엔드포인트·파라미터 탐색에 사용
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
cmd=${1:-help}
|
|
8
|
+
shift 2>/dev/null || true
|
|
9
|
+
|
|
10
|
+
_usage() {
|
|
11
|
+
cat <<'EOF'
|
|
12
|
+
사용법: bash scripts/analyze-library.sh <명령> [인수...]
|
|
13
|
+
|
|
14
|
+
명령:
|
|
15
|
+
endpoints BUNDLE_URL JS 번들에서 /api/* 경로 목록 추출
|
|
16
|
+
context BUNDLE_URL PATTERN 패턴 주변 컨텍스트(±200자) 출력 (최대 5개)
|
|
17
|
+
symbol BUNDLE_URL SYMBOL 심볼/함수 정의 위치 출력 (앞 200자, 뒤 500자)
|
|
18
|
+
libraries API_URL 도서관 목록 JSON API 파싱
|
|
19
|
+
pattern BUNDLE_URL REGEX 정규식 매칭 결과 추출 (최대 30개)
|
|
20
|
+
test-param API_URL BASE_JSON PARAM VAL.. POST 파라미터 유효값 순차 탐색
|
|
21
|
+
pyxis BASE_URL pyxis 플랫폼 분관 목록 조회 (ikc-pyxis-wrap 계열)
|
|
22
|
+
ssl-check HOSTNAME SSL 인증서 체인 확인 (중간 CA 누락 감지)
|
|
23
|
+
|
|
24
|
+
예시:
|
|
25
|
+
bash scripts/analyze-library.sh endpoints "https://alpasq.bcl.go.kr/app.abc123.js"
|
|
26
|
+
|
|
27
|
+
bash scripts/analyze-library.sh context "https://example.go.kr/app.js" "/api/search"
|
|
28
|
+
|
|
29
|
+
bash scripts/analyze-library.sh symbol "https://example.go.kr/app.js" "convertToDetailPath:function"
|
|
30
|
+
|
|
31
|
+
bash scripts/analyze-library.sh libraries "https://example.go.kr/api/common/libraryInfo"
|
|
32
|
+
|
|
33
|
+
bash scripts/analyze-library.sh pattern "https://example.go.kr/app.js" '"(TITLE|AUTHOR|ALL|LOANABLE)"'
|
|
34
|
+
|
|
35
|
+
bash scripts/analyze-library.sh test-param "https://example.go.kr/api/search" \
|
|
36
|
+
'{"searchKeyword":"별","manageCode":"AA","pubFormCode":"ALL","page":"1","display":"3","order":"DESC"}' \
|
|
37
|
+
article TITLE AUTHOR LOANABLE ALL SCORE
|
|
38
|
+
|
|
39
|
+
bash scripts/analyze-library.sh pyxis "https://lib.siheung.go.kr"
|
|
40
|
+
|
|
41
|
+
bash scripts/analyze-library.sh ssl-check "lib.siheung.go.kr"
|
|
42
|
+
|
|
43
|
+
EOF
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case "$cmd" in
|
|
47
|
+
|
|
48
|
+
endpoints)
|
|
49
|
+
url="${1:?오류: BUNDLE_URL 필요}"
|
|
50
|
+
curl -s "$url" | grep -oE '"/api/[^"]{3,80}"' | sort -u
|
|
51
|
+
;;
|
|
52
|
+
|
|
53
|
+
context)
|
|
54
|
+
url="${1:?오류: BUNDLE_URL 필요}"
|
|
55
|
+
pattern="${2:?오류: PATTERN 필요}"
|
|
56
|
+
tmpfile=$(mktemp)
|
|
57
|
+
trap "rm -f '$tmpfile'" EXIT
|
|
58
|
+
curl -s "$url" > "$tmpfile"
|
|
59
|
+
python3 - "$tmpfile" "$pattern" <<'PYEOF'
|
|
60
|
+
import sys, re
|
|
61
|
+
path, pattern = sys.argv[1], sys.argv[2]
|
|
62
|
+
with open(path) as f:
|
|
63
|
+
content = f.read()
|
|
64
|
+
escaped = re.escape(pattern)
|
|
65
|
+
matches = re.findall(fr'.{{0,200}}{escaped}.{{0,400}}', content)
|
|
66
|
+
if not matches:
|
|
67
|
+
print(f'"{pattern}" 주변 컨텍스트를 찾을 수 없습니다.')
|
|
68
|
+
sys.exit(0)
|
|
69
|
+
for i, m in enumerate(matches[:5]):
|
|
70
|
+
print(f'--- match {i+1}/{min(len(matches),5)} ---')
|
|
71
|
+
print(m)
|
|
72
|
+
print()
|
|
73
|
+
PYEOF
|
|
74
|
+
;;
|
|
75
|
+
|
|
76
|
+
symbol)
|
|
77
|
+
url="${1:?오류: BUNDLE_URL 필요}"
|
|
78
|
+
symbol="${2:?오류: SYMBOL 필요}"
|
|
79
|
+
tmpfile=$(mktemp)
|
|
80
|
+
trap "rm -f '$tmpfile'" EXIT
|
|
81
|
+
curl -s "$url" > "$tmpfile"
|
|
82
|
+
python3 - "$tmpfile" "$symbol" <<'PYEOF'
|
|
83
|
+
import sys
|
|
84
|
+
path, symbol = sys.argv[1], sys.argv[2]
|
|
85
|
+
with open(path) as f:
|
|
86
|
+
content = f.read()
|
|
87
|
+
idx = content.find(symbol)
|
|
88
|
+
if idx == -1:
|
|
89
|
+
print(f'"{symbol}" 를 찾을 수 없습니다.')
|
|
90
|
+
sys.exit(0)
|
|
91
|
+
print(content[max(0, idx - 200):idx + 500])
|
|
92
|
+
PYEOF
|
|
93
|
+
;;
|
|
94
|
+
|
|
95
|
+
libraries)
|
|
96
|
+
url="${1:?오류: API_URL 필요}"
|
|
97
|
+
tmpfile=$(mktemp)
|
|
98
|
+
trap "rm -f '$tmpfile'" EXIT
|
|
99
|
+
curl -s "$url" > "$tmpfile"
|
|
100
|
+
python3 - "$tmpfile" <<'PYEOF'
|
|
101
|
+
import json, sys
|
|
102
|
+
|
|
103
|
+
path = sys.argv[1]
|
|
104
|
+
with open(path) as f:
|
|
105
|
+
try:
|
|
106
|
+
data = json.load(f)
|
|
107
|
+
except json.JSONDecodeError as e:
|
|
108
|
+
print(f'JSON 파싱 오류: {e}', file=sys.stderr)
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
def find_list(obj, depth=0):
|
|
112
|
+
if depth > 5:
|
|
113
|
+
return None
|
|
114
|
+
if isinstance(obj, list) and len(obj) > 0 and isinstance(obj[0], dict):
|
|
115
|
+
return obj
|
|
116
|
+
if isinstance(obj, dict):
|
|
117
|
+
for k in ('libList', 'libraryList', 'list', 'data', 'contents', 'result', 'items'):
|
|
118
|
+
if k in obj:
|
|
119
|
+
found = find_list(obj[k], depth + 1)
|
|
120
|
+
if found:
|
|
121
|
+
return found
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
libs = find_list(data)
|
|
125
|
+
if not libs:
|
|
126
|
+
print('[원본 JSON (처음 2000자)]')
|
|
127
|
+
print(json.dumps(data, ensure_ascii=False, indent=2)[:2000])
|
|
128
|
+
sys.exit(0)
|
|
129
|
+
|
|
130
|
+
print(f'총 {len(libs)}개 항목')
|
|
131
|
+
print()
|
|
132
|
+
for lib in libs:
|
|
133
|
+
code = lib.get('manageCode') or lib.get('libCode') or lib.get('code') or '?'
|
|
134
|
+
name = lib.get('libName') or lib.get('name') or lib.get('libraryName') or '?'
|
|
135
|
+
group = lib.get('groupName') or lib.get('group') or ''
|
|
136
|
+
if code == 'ALL':
|
|
137
|
+
continue
|
|
138
|
+
suffix = f' (그룹: {group})' if group else ''
|
|
139
|
+
print(f'{code}: {name}{suffix}')
|
|
140
|
+
PYEOF
|
|
141
|
+
;;
|
|
142
|
+
|
|
143
|
+
pattern)
|
|
144
|
+
url="${1:?오류: BUNDLE_URL 필요}"
|
|
145
|
+
regex="${2:?오류: REGEX 필요}"
|
|
146
|
+
tmpfile=$(mktemp)
|
|
147
|
+
trap "rm -f '$tmpfile'" EXIT
|
|
148
|
+
curl -s "$url" > "$tmpfile"
|
|
149
|
+
python3 - "$tmpfile" "$regex" <<'PYEOF'
|
|
150
|
+
import sys, re
|
|
151
|
+
path, regex = sys.argv[1], sys.argv[2]
|
|
152
|
+
with open(path) as f:
|
|
153
|
+
content = f.read()
|
|
154
|
+
try:
|
|
155
|
+
matches = re.findall(regex, content)
|
|
156
|
+
except re.error as e:
|
|
157
|
+
print(f'정규식 오류: {e}', file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
unique = list(dict.fromkeys(matches))
|
|
160
|
+
if not unique:
|
|
161
|
+
print('매칭 결과 없음')
|
|
162
|
+
sys.exit(0)
|
|
163
|
+
print(f'총 {len(unique)}개 (상위 30개):')
|
|
164
|
+
for m in unique[:30]:
|
|
165
|
+
print(m)
|
|
166
|
+
PYEOF
|
|
167
|
+
;;
|
|
168
|
+
|
|
169
|
+
test-param)
|
|
170
|
+
url="${1:?오류: API_URL 필요}"
|
|
171
|
+
base_json="${2:?오류: BASE_JSON 필요}"
|
|
172
|
+
param="${3:?오류: PARAM 필요}"
|
|
173
|
+
shift 3
|
|
174
|
+
if [ $# -eq 0 ]; then
|
|
175
|
+
echo "오류: 테스트할 값 목록이 필요합니다" >&2
|
|
176
|
+
exit 1
|
|
177
|
+
fi
|
|
178
|
+
echo "[$param 유효값 탐색] → $url"
|
|
179
|
+
echo
|
|
180
|
+
for val in "$@"; do
|
|
181
|
+
body=$(python3 -c "
|
|
182
|
+
import json, sys
|
|
183
|
+
d = json.loads(sys.argv[1])
|
|
184
|
+
d[sys.argv[2]] = sys.argv[3]
|
|
185
|
+
print(json.dumps(d, ensure_ascii=False))
|
|
186
|
+
" "$base_json" "$param" "$val")
|
|
187
|
+
printf "%s=%-12s " "$param" "$val:"
|
|
188
|
+
curl -s -X POST "$url" \
|
|
189
|
+
-H "Content-Type: application/json" \
|
|
190
|
+
-d "$body" | python3 -c "
|
|
191
|
+
import json, sys
|
|
192
|
+
try:
|
|
193
|
+
d = json.load(sys.stdin)
|
|
194
|
+
r = d
|
|
195
|
+
for k in ('result', 'data', 'contents', 'response'):
|
|
196
|
+
if isinstance(r, dict) and k in r:
|
|
197
|
+
r = r[k]
|
|
198
|
+
if isinstance(r, dict):
|
|
199
|
+
out = r.get('debug') or r.get('message') or r.get('code') or r.get('totalCount')
|
|
200
|
+
print(str(out if out is not None else r)[:80])
|
|
201
|
+
elif isinstance(r, list):
|
|
202
|
+
print(f'{len(r)}건')
|
|
203
|
+
else:
|
|
204
|
+
print(str(d)[:80])
|
|
205
|
+
except Exception as e:
|
|
206
|
+
print(f'파싱 오류: {e}')
|
|
207
|
+
"
|
|
208
|
+
done
|
|
209
|
+
;;
|
|
210
|
+
|
|
211
|
+
pyxis)
|
|
212
|
+
base="${1:?오류: BASE_URL 필요}"
|
|
213
|
+
base="${base%/}"
|
|
214
|
+
echo "[$base pyxis 분관 목록]"
|
|
215
|
+
tmpfile=$(mktemp)
|
|
216
|
+
trap "rm -f '$tmpfile'" EXIT
|
|
217
|
+
curl -sk "$base/pyxis-api/1/branches" > "$tmpfile"
|
|
218
|
+
python3 - "$tmpfile" <<'PYEOF'
|
|
219
|
+
import json, sys
|
|
220
|
+
path = sys.argv[1]
|
|
221
|
+
with open(path) as f:
|
|
222
|
+
content = f.read().strip()
|
|
223
|
+
if not content:
|
|
224
|
+
print('오류: 응답이 비어 있습니다. URL을 확인하거나 pyxis 플랫폼이 아닐 수 있습니다.')
|
|
225
|
+
sys.exit(1)
|
|
226
|
+
try:
|
|
227
|
+
data = json.loads(content)
|
|
228
|
+
except json.JSONDecodeError as e:
|
|
229
|
+
print(f'JSON 파싱 오류: {e}')
|
|
230
|
+
sys.exit(1)
|
|
231
|
+
if not data.get('success'):
|
|
232
|
+
print(f'API 오류: {data.get("message","?")}\npyxis 플랫폼이 아닐 수 있습니다.')
|
|
233
|
+
sys.exit(1)
|
|
234
|
+
branches = data.get('data', {}).get('list', [])
|
|
235
|
+
print(f'총 {len(branches)}개 분관')
|
|
236
|
+
print()
|
|
237
|
+
for b in branches:
|
|
238
|
+
bid = b.get('id')
|
|
239
|
+
bname = b.get('name', '?')
|
|
240
|
+
group = b.get('branchGroup', {}).get('name', '')
|
|
241
|
+
suffix = f' (그룹: {group})' if group and group != bname else ''
|
|
242
|
+
print(f' {bid}: {bname}{suffix}')
|
|
243
|
+
PYEOF
|
|
244
|
+
;;
|
|
245
|
+
|
|
246
|
+
ssl-check)
|
|
247
|
+
host="${1:?오류: HOSTNAME 필요}"
|
|
248
|
+
echo "[SSL 인증서 체인 확인: $host]"
|
|
249
|
+
openssl s_client -connect "$host:443" -servername "$host" 2>&1 | head -15
|
|
250
|
+
;;
|
|
251
|
+
|
|
252
|
+
help|--help|-h)
|
|
253
|
+
_usage
|
|
254
|
+
;;
|
|
255
|
+
|
|
256
|
+
*)
|
|
257
|
+
echo "알 수 없는 명령: $cmd" >&2
|
|
258
|
+
_usage >&2
|
|
259
|
+
exit 1
|
|
260
|
+
;;
|
|
261
|
+
esac
|