# 도서관 모듈 추가 시 사용하는 Python/Bash 스크립트

> 새 도서관 모듈을 추가할 때 반복적으로 사용되는 분석 스크립트 모음입니다.  
> 대부분의 기능은 `scripts/analyze-library.sh`에 통합되어 있습니다.

---

## 1. 도서관 분기(branch) 코드 수집

검색 결과 응답에서 branch 정보를 자동 수집합니다.

### 사용 시나리오
- pyxis 플랫폼처럼 도서관 목록 전용 API가 있을 경우: `bash scripts/analyze-library.sh libraries {API_URL}`
- 검색 결과에서 branch를 수집해야 하는 경우: 아래 인라인 스크립트 사용

### 인라인 버전 (pyxis 계열 — 검색 결과에서 수집)

```bash
curl -s "{SEARCH_URL}?all=k|a|도서관&max=200" | python3 -c "
import json, sys
data = json.load(sys.stdin)
books = data.get('data', {}).get('list', [])
branches = {}
for b in books:
    for bv in b.get('branchVolumes', []):
        bid = bv.get('id')
        bname = bv.get('name')
        if bid and bname:
            branches[bid] = bname
for bid, bname in sorted(branches.items()):
    print(f'{bid}: {bname}')
print('Total branches:', len(branches))
"
```

### pyxis 전용 branches API (lib.siheung.go.kr 에서 확인)

```bash
curl -s "{BASE_URL}/pyxis-api/1/branches" | python3 -m json.tool
```

pyxis 플랫폼은 `/pyxis-api/1/branches` 엔드포인트에서 전체 분관 목록을 JSON으로 반환합니다.  
(gunpo, siheung 등 `ikc-pyxis-wrap` 클래스를 사용하는 AngularJS 기반 사이트에서 확인됨)

---

## 2. 검색 응답 구조 파악

### 책·도서관 정보 추출 (pyxis 계열)

```bash
curl -s "{SEARCH_URL}?all=k|a|별&max=3" | python3 -c "
import json, sys
data = json.load(sys.stdin)
books = data.get('data', {}).get('list', [])
for b in books[:2]:
    print('title:', b.get('titleStatement'))
    print('id:', b.get('id'))
    print('branches:', b.get('branchVolumes'))
    print()
"
```

### 총 건수 + 책 목록 확인 (범용)

```bash
curl -s "{SEARCH_URL}?{PARAMS}" | python3 -c "
import json, sys
data = json.load(sys.stdin)
d = data.get('data', {})
print('totalCount:', d.get('totalCount'))
books = d.get('list', [])
for b in books[:2]:
    print('title:', b.get('titleStatement'))
    print('branches:', [(bv['name'], bv['cState']) for bv in b.get('branchVolumes', [])])
"
```

---

## 3. 최대 조회 건수(max) 결정

서버 성능에 맞는 안전한 max 값을 찾습니다.

```bash
for max in 50 100 150 200 250 300 500 1000; do
    result=$(curl -s -o /dev/null -w "%{http_code} %{time_total}s" \
      "{SEARCH_URL}?all=k|a|별&branch={FIRST_BRANCH_CODE}&max=$max")
    echo "max=$max: $result"
done
```

**기준**: 단일 요청이 4초 이내여야 테스트 타임아웃 안에 완료됩니다.  
ECONNRESET은 서버 하드 리밋 신호 — 직전 성공값의 85~90%를 안전값으로 설정합니다.

---

## 4. SSL 인증서 체인 확인

```bash
openssl s_client -connect {HOSTNAME}:443 -servername {HOSTNAME} 2>&1 | head -20
```

`unable to get local issuer certificate` 오류가 보이면 중간 CA 누락 — 모듈에서
`undici`의 `Agent({ connect: { rejectUnauthorized: false } })`를 사용해야 합니다.

---

## 5. 다양한 API 엔드포인트 탐색 (pyxis 계열)

```bash
for endpoint in "1/branches" "1/libraries" "settings/branches" "common/branches" "1/collections/1/branches"; do
    result=$(curl -s "https://{HOST}/pyxis-api/$endpoint" 2>/dev/null \
      | python3 -c "import json,sys; d=json.load(sys.stdin); print('OK' if d.get('success') else 'FAIL: '+d.get('message','?'))" 2>/dev/null)
    echo "$endpoint: $result"
done
```

---

## 7. wkcms/KCMS 플랫폼 세션 기반 검색

거제시도서관(`geoje`) 추가 시 발견된 wkcms 플랫폼 패턴입니다.  
**특징**: Java Spring MVC 기반, 포트 9080, 검색 전 JSESSIONID 쿠키 필수.

### JSESSIONID 쿠키 획득 → 검색 (curl 2단계)

```bash
WKCMS="https://lib.geoje.go.kr:9080/wkcms"
# 1단계: 검색 페이지 방문 → 세션 쿠키 저장
curl -sk -c /tmp/wkcms.txt "$WKCMS/KBookSearch/BookSearchPage/MA" -o /dev/null

# 2단계: 쿠키로 검색 (manage_code = 도서관 코드)
curl -sk -b /tmp/wkcms.txt \
  "$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" \
| python3 -c "
import sys, re
html = sys.stdin.read()
total = re.search(r'총\s*([\d,]+)\s*건', html)
print('총 건수:', total.group(1) if total else '없음')
print('응답 크기:', len(html), 'bytes')
available = html.count('loan_o')
print('대출가능 td.loan_o 개수:', available)
titles = re.findall(r'<h4[^>]*>\s*(.*?)\s*</h4>', html, re.DOTALL)[:3]
for t in titles: print(' -', re.sub(r'<[^>]+>', '', t).strip()[:80])
"
```

### HTML에서 도서관 코드 추출 (wkcms 체크박스)

```bash
curl -sk -c /tmp/wkcms.txt "https://HOST:9080/wkcms/KBookSearch/BookSearchPage/MA" | python3 -c "
import sys, re
html = sys.stdin.read()
# 체크박스 name='manage_code' value='MA' 패턴
codes = re.findall(r\"name=['\\\"]manage_code['\\\"][^>]+value=['\\\"]([A-Z]+)['\\\"]\", html)
if not codes:
    # 속성 순서 다를 때
    inputs = re.findall(r'<input[^>]+>', html, re.I)
    codes = [re.search(r'value=[\"']([A-Z]+)[\"']', i).group(1)
             for i in inputs if 'manage_code' in i.lower()
             if re.search(r'value=[\"']([A-Z]+)[\"']', i)]
unique = list(dict.fromkeys(codes))
print(f'{len(unique)}개 코드:')
for c in unique: print(' ', c)
"
```

### 성능 테스트 (display 값 탐색, 세션 필요)

wkcms는 `display` 파라미터로 한 번에 가져올 최대 건수를 조정합니다.  
세션 쿠키 없이는 오류 페이지가 반환되므로 반드시 2단계 방식으로 테스트합니다.

```bash
INIT_URL="https://HOST:9080/wkcms/KBookSearch/BookSearchPage/MA"
for display in 10 20 50 100 200; do
  curl -sk -c /tmp/wkcms.txt "$INIT_URL" -o /dev/null
  printf "display=%-6s " "$display:"
  curl -sk -b /tmp/wkcms.txt -o /dev/null -w "HTTP %{http_code} - %{time_total}s\n" \
    "https://HOST:9080/wkcms/KBookSearch/BookNomalSearch/MA?search_txt=별&book_type=BOOK&pageno=1&display=$display&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"
done
```

범용 버전은 `bash scripts/analyze-html-library.sh perf-test`를 사용합니다.

---

## 8. HTML 기반 사이트 폼/구조 분석 (`scripts/analyze-html-library.sh`)

SPA가 아닌 SSR HTML 도서관 사이트(wkcms, kolaseek, jnet 등) 분석용 스크립트.  
세션 쿠키 획득 + HTML 폼 구조 파악에 특화되어 있습니다.

```bash
# wkcms 도서관 코드 자동 수집
bash scripts/analyze-html-library.sh wkcms-codes "lib.geoje.go.kr"

# 세션 초기화 후 검색 결과 요약
bash scripts/analyze-html-library.sh session-search \
  "https://lib.geoje.go.kr:9080/wkcms/KBookSearch/BookSearchPage/MA" \
  "https://lib.geoje.go.kr:9080/wkcms/KBookSearch/BookNomalSearch/MA?search_txt=별&..."

# HTML 폼의 action, hidden 파라미터, 체크박스 name 목록
bash scripts/analyze-html-library.sh form-info "https://example.library.go.kr/search.do"

# 도서관 코드 체크박스 name 자동 탐지
bash scripts/analyze-html-library.sh list-codes "https://example.go.kr/search.do" searchLibraryArr

# HTML 내 특정 패턴 주변 컨텍스트 출력
bash scripts/analyze-html-library.sh html-context "https://HOST:9080/wkcms/..." "ul.book_info"

# display 값별 응답 시간 측정 (세션 필요)
bash scripts/analyze-html-library.sh perf-test \
  "https://HOST:9080/wkcms/KBookSearch/BookSearchPage/MA" \
  "https://HOST:9080/wkcms/KBookSearch/BookNomalSearch/MA?search_txt=별&display=__DISPLAY__&..." \
  10 20 50 100 200

# SSL 인증서 확인 (포트 지정 가능)
bash scripts/analyze-html-library.sh ssl-check "lib.geoje.go.kr:9080"
```

---

## 6. SPA 사이트 분석 (`scripts/analyze-library.sh` 통합 명령)

> **자세한 내용**: `/add-library-module` 스킬의 "SPA 분석 스크립트" 섹션 참조

```bash
# JS 번들에서 API 엔드포인트 추출
bash scripts/analyze-library.sh endpoints "https://example.go.kr/app.abc123.js"

# 특정 패턴 주변 컨텍스트 확인 (API 파라미터 파악용)
bash scripts/analyze-library.sh context "https://example.go.kr/app.js" "/api/search"

# 도서관 목록 JSON API 파싱
bash scripts/analyze-library.sh libraries "https://example.go.kr/api/common/libraryInfo"

# POST 파라미터 유효값 탐색
bash scripts/analyze-library.sh test-param "https://example.go.kr/api/search" \
  '{"searchKeyword":"별","manageCode":"AA","page":"1","display":"3"}' \
  article TITLE AUTHOR LOANABLE ALL SCORE
```

---

## 플랫폼별 분기 코드 형식 요약

| 플랫폼 | 분기 파라미터 | 코드 형식 | 예시 |
|--------|-------------|---------|------|
| pyxis (gunpo, siheung) | `branch` | 숫자 ID | `1`, `81` |
| alpasq (bcl, nowon) | `manageCode` | 2자리 알파벳 | `"AA"`, `["AG"]` |
| jnet/galib (gwanak, gangnam) | `searchLibraryArr` | 2자리 알파벳 | `MA` |
| dls_le (asan, cbelib) | `manageCode` | 2자리 알파벳 | `MF` |
| wkcms/KCMS (geoje) | `manage_code` | 2자리 알파벳 | `MA`, `MC` |
| nanum (yangcheon) | `manage_code` | 2자리 알파벳 | `MA`, `MH` |
| suwon | `libraryCode` | 6자리 숫자 | `111000` |

---

## 거제시도서관 추가에서 확인된 사항 (wkcms 플랫폼)

- **wkcms 플랫폼 감지**: URL에 `:9080/wkcms/` 경로 또는 `BookSearchPage`/`BookNomalSearch` 패턴이 있으면 wkcms 계열
- **JSESSIONID 필수**: 쿠키 없이 검색하면 "오류가 발생했습니다" 에러 페이지 반환. 반드시 `createSession()`으로 세션 초기화 후 검색
- **필수 파라미터 세트**: `search_txt`, `input_search_text`, `real_search_text`, `now_search_txt` 4개가 모두 동일한 키워드여야 함 — 하나라도 누락되면 오류
- **URLSearchParams 사용**: `http.ts`의 `session.get()`은 `qs` 옵션 미지원 → `new URLSearchParams({...}).toString()`으로 쿼리 스트링 직접 빌드
- **대출 상태**: `td.loan_o` CSS 클래스 존재 여부로 판단 (`loan_o` = 대출가능)
- **도서 제목**: `ul.book_info > li h4` textContent
- **bookUrl 없음**: wkcms는 개별 도서 상세 페이지가 없음 → `bookPrintPopup` URL을 대안으로 사용, `SESSION_REQUIRED_DOMAINS`에 도메인 추가하여 테스트 스킵
- **MB 코드 이상**: 거제시립옥포도서관(MB)은 단독 검색 시 항상 0건 반환 → 제외
- **display=20 안전**: 더 높이면 응답 시간 급증 (100=7초). 20이 1.5초로 안전
- **englishSearchTerm 필요**: MA 도서관은 "javascript" 검색 결과 0건 → `{ englishSearchTerm: "java" }` 지정

## 경상북도교육청통합도서관(gbelib) 추가에서 확인된 사항

- **플랫폼**: Spring MVC + Thymeleaf 기반 SSR 사이트. `gbelib.kr` → `www.gbelib.kr/gbelib/` 리다이렉트
- **통합 검색 엔드포인트**: `GET /gbelib/intro/totalSearch/index.do?menu_idx=150&search_text={title}&search_type=L_TITLE&total_search_type=TOTAL&search_detail_yn=N&book_more_count=1&libraryCodes={code}`
- **총 건수 추출**: `id="bookTotalCnt"` 요소의 textContent (예: `9057`)
- **초기 3건 반환**: `book_more_count=1`이면 항상 3건만 `div.row` 형태로 반환
- **추가 건수 로드**: `GET /gbelib/intro/totalSearch/more.do?more_type=BOOK&...&book_more_count=N` — N=2부터 순서대로 3건씩 반환. 병렬 호출 가능 (~0.4초)
- **도서관 코드 형식**: 8자리 숫자 (`00147046`, `00347034` 등). HTML 체크박스의 `value` 속성에서 추출
- **책 항목 구조**: `div.row > div.box > div.item > div.bif` 안에:
  - `a.name.goDetail` — 책 제목 (textContent에 `<span>` 하이라이트 포함, `textContent`로 추출)
  - `vLoca` 속성 — 8자리 라이브러리 코드
  - `vCtrl` 속성 — 도서 관리번호
  - 두 번째 `<p>` — 도서관명
- **책 상세 URL**: `https://www.gbelib.kr/gbelib/intro/search/detail.do?vLoca={vLoca}&vCtrl={vCtrl}&menu_idx=150` — GET 요청으로 직접 접근 가능, 책 제목이 HTML에 포함됨
- **대출 상태**: `more.do` HTML 조각에 대출 상태 없음. `index_detail.do?vLoca={vLoca}&vCtrl={vCtrl}`로 별도 조회 가능하지만 책마다 추가 HTTP 요청이 필요해 실용적이지 않음 → `exist: false`로 설정
- **더 빠른 개별 도서관 검색**: 각 도서관은 별도 서브사이트 (`/gm/`, `/ad/` 등)를 가지며, `/{prefix}/intro/search/index.do?search_text={title}&rowCount=50`으로 최대 50건 반환. 하지만 전체 네트워크 코드와 매핑이 필요해 통합 검색 사용
- **병렬 more.do 호출로 30건**: 초기 1건 + more.do 9건 병렬 = 총 30건, 약 2-3초 소요

### gbelib 분관 코드 수집 스크립트

```python
import re

html = open('/tmp/gbelib_search.html').read()
libs = re.findall(
    r'<input[^>]+name="libraryCodes"[^>]+value="([^"]+)"[^>]*/><label for="[^"]+">([^<]+)</label>',
    html
)
for code, name in libs:
    if code != 'ALL':
        print(f'  {{ code: "{code}", name: "{name}" }},')
```

또는 bash:
```bash
curl -s "https://www.gbelib.kr/gbelib/intro/totalSearch/index.do?menu_idx=150" | python3 -c "
import sys, re
html = sys.stdin.read()
libs = re.findall(r'name=\"libraryCodes\"[^>]+value=\"([^\"]+)\"[^/]*/><label[^>]+>([^<]+)', html)
for code, name in libs:
    if code != 'ALL':
        print(f'  {{ code: \"{code}\", name: \"{name}\" }},')
"
```

---

## 강원특별자치도교육청도서관(gwe) 추가에서 확인된 사항

- **User-Agent 필수**: `lib.gwe.go.kr`는 User-Agent 없이 curl 하면 503바이트 오류 페이지 반환 (`해당 페이지는 오류가 발생했습니다`). `http.ts`의 `get()`은 자동 포함하지만 curl 분석 시 반드시 `-A "Mozilla/5.0 ..."` 추가
- **`analyze-html-library.sh list-codes` 사용 불가**: User-Agent 없이 실행되어 오류 페이지를 받음 → `curl -A "Mozilla/5.0" URL -o /tmp/file.html` 후 Python으로 직접 파싱
- **도서관 코드 파라미터**: `manageCodes` (체크박스 다중, 2자리 알파벳)
- **검색 파라미터**: `search=true`, `searchInput={title}`, `searchCondition=searchTxt`, `manageCodes={code}`, `size={N}`, `page=1` — GET 방식
- **총 건수**: 정규식 `검색결과\s*총\s*<strong>([\d,]+)<\/strong>건`
- **책 데이터**: `div.bookData`의 `data-title`, `data-reg-no`, `data-lib-name` data-* 속성
- **bookUrl**: `/portal/menu/568/book/view/{regNo}?booktype=` — User-Agent 없이도 접근 가능, 책 제목이 `<h4 class="book_detail__title">` 에 포함됨
- **대출 상태 없음**: 검색 결과에 대출 여부 미포함 (상세 페이지에만 존재) → `exist: false`
- **서버 하드 리밋 약 360건**: size=370 이상 오류 페이지 반환 → 안전값 300 (3.2초)
- **SSL 정상**: 체인 완전 (`*.gwe.go.kr` WISeKey 인증서)

### gwe 도서관 코드 수집 (User-Agent 필수)

```bash
curl -skL "https://lib.gwe.go.kr/portal/menu/568/book/search" \
  -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
  -o /tmp/gwe_search.html
python3 << 'EOF'
with open('/tmp/gwe_search.html', 'rb') as f:
    html = f.read().decode('utf-8', errors='replace')
import re
# 중복 제거 후 출력
seen = set()
items = re.findall(
    r'<input[^>]+value="([A-Z]+)"[^>]+name="manageCodes"[^/]*/>\s*<label[^>]*>([^<]+)</label>',
    html
)
for code, name in items:
    if code not in seen:
        seen.add(code)
        print(f'  {{ code: "{code}", name: "{name.strip()}" }},')
EOF
```

---

## 이번 시흥시도서관 추가에서 확인된 사항

- **pyxis 플랫폼 감지**: HTML에 `class="ikc-pyxis-wrap"` 또는 `ikc-pyxis-wrap` 클래스가 있으면 pyxis API 계열
- **전용 branches API**: `GET /pyxis-api/1/branches` → 전체 분관 목록 (id, name, branchGroup) 반환. 39개 분관 중 무인대출기(id 72, 82)는 제외하고 시립도서관 13개만 포함
- **SSL 인증서 불완전**: `lib.siheung.go.kr`는 중간 CA 누락 → `rejectUnauthorized: false` 필요
- **max 제한**: `max=1000`에서 52초 → `max=200`이 안전 (3초 이내)
- **SPA 처리**: Angular SPA라서 `/#/search/detail/{id}` URL은 JS 없이 렌더링 안 됨 → `SESSION_REQUIRED_DOMAINS`에 추가
- **totalBookCount**: pyxis 응답의 `data.totalCount` 사용 (max 제한으로 실제 결과가 잘리므로)

---

## 양천구도서관(yangcheon) 추가에서 확인된 사항 (nanum 플랫폼)

- **nanum 플랫폼 감지**: JS 경로에 `/nanum/site/` 패턴이 있으면 nanum 계열 (예: `/nanum/site/common/js/jquery-3.3.1.min.js`)
- **메인 리다이렉트**: `https://lib.yangcheon.or.kr` → JS `location.href="/main/main.do"` 로 리다이렉트
- **검색 엔드포인트**: `GET /main/site/search/bookSearch.do`
- **검색 파라미터**: `cmd_name=bookandnonbooksearch`, `manage_code={code}`, `search_key=ALL`, `search_txt={title}`, `rows=10`, `page={N}`
- **페이지 크기 고정**: `rows` 파라미터 무시 — 서버는 항상 10건/페이지 반환
- **총 건수 패턴**: `<span>전체 N</span>개가 검색되었습니다.` 정규식 `전체 ([\d,]+)<\/span>개가 검색되었습니다`
- **책 데이터**: `div.book_info` data-* 속성 — `data-ti`=제목, `data-rk`=species_key, `data-mgc`=도서관코드
- **reckey**: `data-rk`와 다름 — `p.tit > a`의 href에서 URLSearchParams로 `reckey` 파라미터를 추출해야 함
- **대출 상태**: `p.book_status span`의 class 확인 — `activity`=대출가능, `inactive`=불가
- **bookUrl**: `bookSearch.do?manage_code={mgc}&book_type=BOOK&book_type_org=&publish_form_code=MO&species_key={rk}&reckey={reckey}` — species_key와 reckey 모두 필요
- **SSL 정상**: 체인 완전 (Sectigo RSA)
- **WAF 차단**: "javascript" 검색어 → 0바이트 응답. "java" 검색어 사용 필요 → 테스트 spec에서 `{ englishSearchTerm: "java" }` 지정
