#!/bin/sh
# Claude Code Skill 설치 스크립트
# 생성: 2026-08-13 · Skill 1개 · 파일 1개
#
#   ./install-skills.sh            ~/.claude/skills/ 에 설치 (모든 프로젝트에서 사용)
#   ./install-skills.sh ./myrepo   ./myrepo/.claude/skills/ 에 설치 (그 저장소 전용)
set -e

DEST="${1:+$1/.claude/skills}"
DEST="${DEST:-$HOME/.claude/skills}"
mkdir -p "$DEST"
echo "설치 위치: $DEST"


mkdir -p "$DEST/paper-source-collect"
cat > "$DEST/paper-source-collect/SKILL.md" <<'SKILL_PAYLOAD_EOF'
---
name: paper-source-collect
description: PubMed·OpenAlex·KoreaMed에서 논문을 대량 수집한다. DB마다 다른 페이지네이션·호출한도·응답형식을 다루는 법과, 공식 API가 없는 KoreaMed를 폼 재전송으로 긁는 법을 담았다. TRIGGER - "논문 수집", "PubMed 긁기", "OpenAlex", "KoreaMed 수집", "검색식으로 논문 목록 만들기", 서지 코퍼스를 처음부터 만들어야 할 때.
---

# 논문 다중소스 수집

## 언제 쓰나

검색식 하나로 여러 DB에서 논문 코퍼스를 만들 때. DB마다 제약이 달라서 **한 방식으로 셋을 다 긁을 수 없다.**

## 정본 코드

```
~/Documents/혜연/BTC_KOL_PoC/
  collect_pubmed.py    194줄  esearch → efetch 배치
  collect_openalex.py  198줄  OR 필터 배치 + 캐시
  collect_koreamed.py  426줄  API 없음 — 폼 재전송으로 긁는다
  topics/                     검색식 정의
  out/                        수집 결과 JSON
```

## DB별 제약 — 여기가 전부다

| DB | 인증 | 한도 | 페이지네이션 | 응답 |
|---|---|---|---|---|
| PubMed E-utilities | 키 선택 | 무키 3 req/s | `retmax` 최대 10000, efetch는 100건씩 | XML |
| OpenAlex | **키 없음** | `mailto=` 넣으면 10/s · 10만/일 | OR 필터 한 번에 50개 | JSON |
| KoreaMed | 없음 | 명시 없음 | **아래 함정 참조** | XML(요청해야) |

### PubMed — 2단계

`esearch`로 PMID 목록을 받고(`retmax: 10000`), `efetch`로 100건씩 상세를 받는다.

```python
def efetch(pmids, batch=100, delay=1.0):
    ...
    time.sleep(delay)   # NCBI 부하 배려. 무키 제한은 3req/s 이지만 넉넉히 둔다
```

**한도보다 여유 있게 잡는다.** 3 req/s가 상한이라고 1초에 3번 때리면 간헐적으로 막힌다. 재시도는 `time.sleep(3 * (i + 1))`로 점점 늘린다.

### OpenAlex — 키가 아예 없다

> API 키는 존재하지 않는다. `mailto=` 만 붙이면 polite pool(초당 10건 / 일 10만건).

`mailto`는 **인증이 아니라 예의**다. 안 넣으면 공용 풀에서 훨씬 느리다.

`BATCH = 50` — OpenAlex OR 필터 상한이다. 그 이상 넣으면 조용히 잘린다.
`select=` 로 필요한 필드만 받는다(`ids,doi,authorships,cited_by_count,publication_year`). 전체를 받으면 응답이 수십 배 커진다.

결과는 `openalex_cache.json`에 쌓는다. 병합 단계를 여러 번 다시 돌리게 되므로 캐시가 없으면 매번 다시 긁는다.

### KoreaMed — 공식 API가 없다

이 파일 426줄의 대부분이 여기서 나온 함정 대응이다.

```
s_display_type=xml     → PubMed 스타일 XML (저자별 소속 포함). 안 주면 HTML 파싱해야 한다
s_num_per_page         → 최대 500
```

**가장 큰 함정 — 2페이지 이후:**

> 2페이지 이후는 `search_type=page_search` + 결과페이지의 `sub_search_frm` 폼 전체 재전송이라야 나온다. `sub_search` 로 `s_direct_page` 만 바꾸면 **조용히 1페이지가 다시 온다** (실측 중복 500/500).

에러가 아니라 **같은 데이터가 성공처럼 돌아온다.** 중복 제거를 안 하면 500건을 N번 받고도 모른다. 그래서 `form_fields(body)`로 결과 페이지의 input/select를 전부 긁어 되보낸다.

**검색식이 길면 나눈다.** `QUERY_BUDGET = 900`자 어림으로 최상위 OR 기준 분할(`split_top_level_or`), 실패하면 자동 이분할. 주석에 "정확할 필요 없다"고 적힌 이유다 — 실패가 분할을 유도한다.

## 공통 규칙

- **검색식을 코드에 박지 마라.** `topics/` 에 두고 `load_topic(topic_id)` 로 읽는다. 검색식은 계속 바뀌고, 어떤 식으로 뽑았는지가 논문에 들어간다.
- 재시도는 지수 백오프(`3 * (i+1)`). 4~5회.
- 수집 결과는 원본 그대로 `out/` 에 남긴다. 가공은 병합 단계에서.

## 연관 skill

`[[paper-merge-provenance]]`(다음 단계), `[[biblio-normalize]]`, `[[lit-relevance-classify]]`
SKILL_PAYLOAD_EOF

echo ""
echo "완료 — Skill 1개를 설치했습니다."
echo "Claude Code를 다시 시작하면 인식됩니다."
