#!/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-pdf-downloader"
cat > "$DEST/paper-pdf-downloader/SKILL.md" <<'SKILL_PAYLOAD_EOF'
---
name: paper-pdf-downloader
description: DOI 또는 PubMed ID를 통해 논문 PDF를 자동으로 다운로드합니다. 6단계 폴백 체인과 봇 감지 우회 기능을 포함합니다.
---

# 논문 PDF 다운로드 스킬

DOI 또는 PubMed ID를 사용하여 논문 PDF를 다운로드합니다.
여러 소스를 순차적으로 시도하며, OA(Open Access) PDF를 우선 검색합니다.

## 사용법

```python
import os, sys
SKILLS_HOME = os.environ.get('SKILLS_HOME', os.path.expanduser('~/projects/skills'))
sys.path.insert(0, SKILLS_HOME)

from paper_pdf_downloader import PaperPDFDownloader, download_pdf

# 방법 1: 편의 함수 사용
result = download_pdf(
    identifier='$ARGUMENTS',  # DOI, PMID, 또는 URL
    download_dir='./papers',
    unpaywall_email='your@email.com'
)

# 방법 2: 클래스 사용
downloader = PaperPDFDownloader(
    download_dir='./papers',
    unpaywall_email='your@email.com'  # 또는 환경변수 UNPAYWALL_EMAIL
)

# DOI로 다운로드
result = downloader.download('10.1371/journal.pone.0200001')

# PMID로 다운로드
result = downloader.download_by_pmid('28800616')

# 결과 확인
if result.success:
    print(f"다운로드 완료: {result.filepath}")
    print(f"소스: {result.source}")
else:
    print(f"실패: {result.error}")
```

---

## 봇 감지 우회 기법

### macOS Python 3.14 + LibreSSL 호환성 (v2026-02)

macOS Python 3.14 + LibreSSL 2.8.3 환경에서는 SSL 인증서 검증 오류가 발생합니다.
래퍼 스크립트(`download-paper-pdf.py`)에서 자동으로 처리됩니다:

```python
# SSL 인증서 검증 비활성화
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

# requests 세션의 verify=False 기본값 설정
import requests as _requests
_orig_request = _requests.Session.request
def _patched_request(self, *args, **kwargs):
    kwargs.setdefault('verify', False)
    return _orig_request(self, *args, **kwargs)
_requests.Session.request = _patched_request

# cloudscraper → 일반 requests.Session 대체
# (cloudscraper의 자체 SSL 핸들링이 LibreSSL과 호환되지 않음)
import cloudscraper as _cloudscraper
def _fake_create_scraper(**kwargs):
    session = _requests.Session()
    session.headers.update({...})
    return session
_cloudscraper.create_scraper = _fake_create_scraper
```

### User-Agent 로테이션

요청마다 랜덤한 User-Agent를 사용합니다.

### Referer 헤더 위장

학술 검색 엔진에서 온 것처럼 위장합니다.

### 자연스러운 요청 간격

```python
min_request_delay: float = 0.5  # 최소 대기
max_request_delay: float = 2.0  # 최대 대기
```

### 차단 감지

봇 차단 응답을 감지하고 적절히 처리합니다.

```python
blocking_indicators = [
    'cloudflare', 'access denied', 'forbidden', 'blocked',
    'captcha', 'verification', 'security check', 'bot detection',
    'just a moment', 'please wait', 'checking your browser',
    'enable javascript', 'browser check'
]
```

---

## 폴백 체인 (6단계)

PDF를 찾을 때까지 순차적으로 시도합니다.

### 1단계: Unpaywall API (Open Access)

```
GET https://api.unpaywall.org/v2/{doi}?email={email}
```
- 오픈 액세스 논문의 PDF URL 반환
- 무료이며 안정적

### 2단계: CrossRef API

```
GET https://api.crossref.org/works/{doi}
```
- 출판사 URL, 라이선스 정보 획득
- PDF 링크가 있으면 직접 다운로드
- 없으면 출판사 URL을 출판사 핸들러로 전달

### 3단계: DOI Resolver

```
GET https://doi.org/{doi}
→ 리다이렉트 → 출판사 페이지
```
- DOI 리다이렉트 추적
- 최종 URL에서 PDF 링크 추출

### 4단계: Wiley 핸들러

Wiley Online Library 전용 처리:
```
원본: https://onlinelibrary.wiley.com/doi/10.xxxx/xxx
시도1: https://onlinelibrary.wiley.com/doi/epdf/10.xxxx/xxx
시도2: https://onlinelibrary.wiley.com/doi/pdf/10.xxxx/xxx
```

### 5단계: Elsevier 핸들러

ScienceDirect 전용 처리:
```
1. PII (Publisher Item Identifier) 추출
2. https://www.sciencedirect.com/science/article/pii/{PII}/pdfft
3. XML/HTML 파싱으로 PDF 링크 추출
```

### 6단계: PMC (PubMed Central)

PMID → PMC ID 변환 후:
```
GET https://www.ncbi.nlm.nih.gov/pmc/articles/{pmcid}/pdf/
```

---

## 출판사별 처리

### Wiley

```python
class WileyHandler:
    domains = ['wiley.com', 'onlinelibrary.wiley.com']
    # URL 패턴 변환: /doi/10.xxx → /doi/epdf/10.xxx, /doi/pdf/10.xxx
```

### Elsevier (ScienceDirect)

```python
class ElsevierHandler:
    domains = ['sciencedirect.com', 'elsevier.com']
    # PII 추출 → /science/article/pii/{PII}/pdfft
```

---

## 환경 변수

| 변수 | 설명 | 필수 |
|------|------|------|
| `UNPAYWALL_EMAIL` | Unpaywall API 이메일 | Yes |
| `ENTREZ_EMAIL` | PubMed Entrez API 이메일 | PMID 사용 시 |

## 의존성 설치

```bash
pip install --break-system-packages -r ~/projects/skills/paper_pdf_downloader/requirements.txt
```

주요 패키지:
- `cloudscraper` - Cloudflare 우회 (macOS에서는 requests.Session으로 자동 대체)
- `beautifulsoup4` - HTML 파싱
- `requests` - HTTP 클라이언트
- `python-dotenv` - 환경 변수

---

## 로깅

### 로그 파일 위치

| 모드 | 로그 파일 |
|------|-----------|
| Content (일반 논문) | `logs/pdf-{jobId}.log` |
| MetaPaper (메타분석) | `logs/meta-pdf-{paperId}.log` |

### 다운로드 실패 로그

```
{download_dir}/download_failures.jsonl
```

```json
{
  "timestamp": "2025-02-03T10:00:00",
  "identifier": "10.1234/example",
  "identifier_type": "doi",
  "doi": "10.1234/example",
  "error_message": "PDF not found from any source",
  "attempts": [
    {"source": "unpaywall", "success": false},
    {"source": "crossref", "success": false},
    {"source": "doi_resolver", "success": false}
  ],
  "download_time": 15.3
}
```

---

## Node.js 연동 (래퍼 스크립트)

프로젝트 `scripts/download-paper-pdf.py`가 Node.js API에서 호출됩니다.

### Content 모드 (일반 논문)

```bash
python3 scripts/download-paper-pdf.py \
  --doi "10.1234/example" \
  --output "/path/to/output.pdf" \
  --job-id "job-id" \
  --content-id "content-id" \
  --db-path "/path/to/dev.db"
```

- `PdfDownloadJob` 테이블로 상태 추적
- 실패 시 자동 브라우저 캡처 fallback (`/browser-pdf-capture`)
- Stuck Job 감지: 10분 이상 PENDING/DOWNLOADING 상태면 FAILED로 자동 전환

### MetaPaper 모드 (메타분석)

```bash
python3 scripts/download-paper-pdf.py \
  --doi "10.1234/example" \
  --output "/path/to/output.pdf" \
  --job-id "meta-paperId" \
  --content-id "paperId" \
  --db-path "/path/to/dev.db" \
  --table "MetaPaper" \
  --pdf-url-prefix "/uploads/meta-papers"
```

- `.status.json` 파일로 상태 추적
- 5분 타임아웃 (DOWNLOADING 상태 초과 시 FAILED)

### 자동 폴백 흐름

```
1. PDF 수집 클릭 → download-paper-pdf.py 실행
2. 6단계 폴백 체인 시도
3. 모두 실패 → PdfDownloadButton이 자동 브라우저 캡처 시작
4. browser-capture-pdf.py 실행 (DOI → 웹페이지 → PDF 캡처)
5. 성공 시 DB 업데이트 + 상태 파일 갱신
```

---

## 트러블슈팅

### 1. SSL 인증서 검증 실패 (macOS Python 3.14)

**원인:** Python 3.14 + LibreSSL 2.8.3의 SSL 인증서 검증 비호환
**해결:** 래퍼 스크립트(`download-paper-pdf.py`)에 SSL 우회 포함됨 (자동)

### 2. 403 Forbidden 에러

**원인:** 봇 감지에 걸림
**해결:**
- 자동으로 재시도 → 실패 시 자동 브라우저 캡처
- 수동: `/browser-pdf-capture` 스킬 사용

### 3. Stuck Job (다운로드 재시도 불가)

**원인:** Python 스크립트 크래시로 PdfDownloadJob이 PENDING/DOWNLOADING에서 stuck
**해결:** v2026-02에서 10분 타임아웃 자동 FAILED 처리 추가됨

### 4. 타임아웃

```python
downloader = PaperPDFDownloader(
    request_timeout=60,    # 기본 30초
    download_timeout=120,  # 기본 60초
)
```

### 5. PDF 검증 실패

**원인:** HTML 페이지가 반환됨 (로그인 필요 등)
**해결:** 기관 VPN 연결 또는 오픈 액세스 버전 확인

### 6. Unpaywall API 실패

```bash
export UNPAYWALL_EMAIL="your@email.com"
```

### 7. 모든 소스에서 실패

**해결 순서:**
1. DOI/PMID가 올바른지 확인
2. 기관 네트워크에서 시도
3. `/browser-pdf-capture` 스킬로 브라우저 캡처 시도

---

## API 응답 구조

### DownloadResult

```python
@dataclass
class DownloadResult:
    success: bool              # 성공 여부
    filepath: str = None       # 저장된 파일 경로
    source: str = None         # 다운로드 소스 (unpaywall, crossref, etc.)
    identifier: str = None     # 입력 식별자
    identifier_type: str = None # doi, pmid, url
    doi: str = None            # 정규화된 DOI
    file_size: int = None      # 파일 크기 (bytes)
    download_time: float = None # 소요 시간 (초)
    error_message: str = None  # 에러 메시지
    attempts: list = None      # 시도 목록
```

---

## 지원하는 소스 요약

| 소스 | 설명 | 비용 |
|------|------|------|
| Unpaywall | Open Access PDF | 무료 |
| CrossRef | 출판사 메타데이터 | 무료 |
| DOI Resolver | 리다이렉트 처리 | 무료 |
| PMC | PubMed Central | 무료 |
| Wiley | URL 패턴 변환 | 무료 (기관 인증 필요) |
| Elsevier | PII 기반 | 무료 (기관 인증 필요) |

---

## 관련 파일

| 파일 | 경로 | 설명 |
|------|------|------|
| Python 패키지 | `~/projects/skills/paper_pdf_downloader/` | 메인 모듈 |
| Content 래퍼 | 프로젝트 `scripts/download-paper-pdf.py` | Node.js 연동 |
| Content API | 프로젝트 `src/app/api/papers/[id]/pdf-download/route.ts` | 일반 논문 API |
| MetaPaper API | 프로젝트 `src/app/api/meta-analysis/[id]/papers/[paperId]/pdf-download/route.ts` | 메타분석 API |
| Skill 정의 | `~/projects/skills/skills/paper-pdf-downloader/SKILL.md` | 이 문서 |

---

## 업데이트 이력

- 2026-02-13: SSL 우회(macOS Python 3.14), cloudscraper 대체, Stuck Job 감지, 로깅, MetaPaper 지원 문서화
- 2025-02-03: 봇 감지 우회 기법, 폴백 체인, 트러블슈팅 가이드 추가
- 2025-01: 초기 버전 작성
SKILL_PAYLOAD_EOF

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