# Pagu.info 구조 결정서

작성: 2026-10-13 · 상태: 확정 (이 문서가 이후 모든 작업 세션의 기준)

이 문서는 "무엇을 만들까"가 아니라 **"어떤 공통 틀 위에 만들까"**를 정한다.
각 기능 세션은 이 문서를 먼저 읽고, 여기서 정한 것을 다시 정하지 않는다.

---

## 1. 확정된 결정 7가지

| # | 항목 | 결정 |
|---|---|---|
| D1 | 데이터 수집 | **GitHub Actions cron → `data/*.json` 커밋 → 정적 사이트가 읽음.** 서버·DB 없음 |
| D2 | 호스팅 | **Vercel** (사용자 유료 플랜), 도메인 `pagu.info` 연결. Vercel은 **정적 파일만** 서빙 |
| D3 | 외부 데이터 | **DeFiLlama 무료 API + Pendle 무료 API.** 유료 플랜 구매 안 함 |
| D4 | 차트 | **TradingView Lightweight Charts** (Apache-2.0 무료). 시계열 전용 |
| D5 | 디자인 | **Stripe 스타일 기반 + 벤토 그리드 + 마스코트**. 아래 §5 |
| D6 | PerpDEX Data Hub | **접근 금지.** 코드·DB·API 어느 것도 참조하지 않는다 |
| D7 | Vault 비상탈출기 | **보류.** 프론트 생존 모니터와 탈출 설명서만 3단계에 흡수 |

---

## 2. 왜 서버와 DB가 필요 없는가

사용자 질문: *"차트도 그렇고 서버나 DB 없이 가능해? Vercel 한도는?"*

**가능하다. 그리고 Vercel 한도와 무관하다.**

```
GitHub Actions (하루 1회, 무료·무제한)
   ↓ DeFiLlama·Pendle·Google Sheet에서 fetch
   ↓ 필요한 것만 추려서 작게 가공
   ↓ data/*.json 으로 저장하고 git commit
GitHub 저장소  ←── 커밋 하나하나가 그날의 스냅샷 = 히스토리
   ↓ push 하면 Vercel이 자동 재배포
Vercel (정적 파일만)
   ↓
브라우저가 작은 JSON 읽어서 차트 그림
```

**Vercel 한도를 안 쓰는 이유**: 데이터를 받아오는 일(cron·서버 함수)을 Vercel이 아니라
GitHub Actions가 한다. Vercel은 HTML·CSS·JS·JSON 파일을 그냥 내려주기만 한다.
그래서 `tokenstock`, `arceco`, `giwaeco`가 쓰는 Vercel 함수 실행량·cron 한도와
**경쟁하지 않는다.** 공유되는 것은 대역폭뿐인데, 정적 JSON 사이트는 Pro 포함량(1TB)에
비해 무시할 수준이다.

**GitHub Actions 비용**: `redmind2/pagu.info`는 **비공개 저장소다**
(2026-10-13 재확인, ran-here: 토큰 없는 요청이 API·웹 모두 404. 확실히 공개인 저장소에
같은 방법을 쓰면 200 이 나온다 — 대조군까지 돌려서 방법 자체를 검증했다).
처음에는 "인증 없이 clone 이 되니 공개"라고 적었는데 **그 검증이 틀렸다**:
`gh` 자격증명 도우미가 붙어 있으면 인증 없는 clone 처럼 보여도 실제로는 인증된 요청이다.

비공개 저장소의 Actions 는 **월 무료 분량에 한도가 있다**(플랜에 따라 월 2,000분 수준).
무제한이 아니다. 다만 실측으로 여유가 크다 — 실행 1회가 **24초**라서
하루 4회(4단계 생존 점검 포함) × 30일이면 **월 1시간 남짓**이다.
1시간 주기 같은 것으로 올리지만 않으면 한도가 문제 되지 않는다.

**히스토리가 공짜인 이유**: 매일 JSON을 커밋하면 git 이력이 곧 시계열 DB가 된다.
"변화 추적"이 목적인 2·3·7번이 DB 없이 해결된다.

### 저장 용량 실측 (2026-10-13, ran-here)

"매일 커밋하면 저장소가 터지지 않나"에 대한 실측 답이다.
DeFiLlama `/pools` 실제 응답을 받아 필요한 13개 필드만 남기고 측정했다.

| 시나리오 | 풀 수 | 하루치(raw) | 하루치(압축) | **1년치 누적** |
|---|---|---|---|---|
| A. 관심 풀 50개 (현실적) | 50 | 12.6 KB | 3.2 KB | **1.1 MB** |
| B. 관심 풀 200개 | 200 | 49.7 KB | 11.1 KB | **4.0 MB** |
| C. TVL $10M 이상 전부 | 818 | 203.5 KB | 45.6 KB | **16.2 MB** |
| D. 전체 16,903개 (최악) | 16,903 | 4.2 MB | 924 KB | **329 MB** |

- 우리는 A~B 구간으로 쓴다. **1년에 1~4MB.** GitHub 권장 상한(1GB)의 0.4% 이하다.
- 위 숫자는 날마다 독립 압축한 상한값이다. git은 비슷한 연속 파일을 델타 압축하므로
  **실제 증가량은 이보다 작다.**
- 금지: D처럼 전체를 그대로 커밋하는 것. 반드시 관심 대상만 추려서 저장한다.

**GitHub 쿼터와 다른 프로젝트의 관계**: 영향 없다.
git 저장소 용량은 **저장소마다 독립**이고 계정 전체 공유 쿼터가 아니다.
계정 단위로 공유되는 것은 Packages 저장소·Actions 아티팩트/캐시·Git LFS인데
**우리는 그중 아무것도 쓰지 않는다.** Actions 실행 시간은 비공개 저장소라 월 한도가 있지만
1회 24초 · 하루 4회라 월 1시간 수준이다 (위 "GitHub Actions 비용" 참고).

**분리 기준 (이 선을 넘으면 데이터 전용 저장소로 뺀다)**
- 단일 JSON 파일이 500KB를 넘을 때
- 연간 증가량이 50MB를 넘을 때
- 그때는 `pagu.info-data` 저장소를 따로 만들고 사이트는 그쪽 raw URL을 읽는다.
  (사이트 저장소를 가볍게 유지해 clone·배포 속도를 지키는 것이 목적)

### 언제 이 구조를 버려야 하나 (업그레이드 신호)

아래 중 하나라도 생기면 그때 백엔드를 붙인다. **그 전에는 만들지 않는다.**

- 1분 단위 같은 실시간성이 필요해질 때
- 알림(텔레그램 푸시)을 보내야 할 때 → 단, 알림은 Actions에서도 가능하므로 먼저 그걸 시도
- 사용자별 저장(로그인, 내 포트폴리오)이 생길 때
- `data/` 가 저장소를 무겁게 만들 때 (커밋당 수 MB 이상이면 신호)

---

## 3. 검증된 사실 (2026-10-13 직접 확인, ran-here)

전부 이 PC에서 실제로 호출해 본 결과다. 추측 아님.

| 확인 대상 | 결과 |
|---|---|
| `yields.llama.fi/pools` (풀 전체) | **HTTP 200, 키 불필요**, 16,903개 풀, 11.6MB, 1.2초 |
| `yields.llama.fi/chart/{poolId}` (풀 과거 이력) | **HTTP 200, 키 불필요** |
| `api.llama.fi/protocols` (프로토콜 TVL) | **HTTP 200, 키 불필요**, 9.3MB |
| `api-v2.pendle.finance/core/v1/1/markets` | **HTTP 200, 키 불필요**, 498개 마켓, PT/YT 실시간 가격·만기 |
| `redmind2/pagu.info` 공개 여부 | 공개 (Actions 무료 무제한) |
| Google Sheet 5개 탭 | 링크 공개(Viewer) → `export?format=csv&gid=` 로 직접 읽힘 |

### DeFiLlama 유료 API($250/월)는 사지 않는다

무료 엔드포인트가 필요한 것을 전부 준다 — 풀 목록, **풀별 과거 이력**, 프로토콜 TVL.
유료 플랜의 가치는 분당 호출 한도(1,000 rpm)와 일부 프리미엄 엔드포인트인데,
우리는 **하루 1회** 호출한다. 사면 100% 낭비다.

주의: `/pools` 응답이 11.6MB다. **이걸 브라우저로 보내면 안 된다.**
GitHub Actions가 받아서 우리가 보는 풀만 추려 수 KB로 줄인 뒤 커밋한다.
이것이 이 구조의 핵심 이점이다.

---

## 4. 차트: TradingView Lightweight Charts

**무료로 된다. 조건은 표기 하나.**

- 라이선스 **Apache-2.0** — 상업적 사용 가능, 무료.
- 단 **TradingView 표기가 의무**다. 차트 옵션 `attributionLogo: true` 를 켜면
  차트 안에 TradingView 링크가 뜨고 그것으로 의무가 충족된다. 이 옵션을 끄지 않는다.
- 혼동 주의: **Advanced Charts**는 다른 제품이다. 별도 승인·비공개 계약이 필요하므로 쓰지 않는다.

**쓰는 곳 / 안 쓰는 곳**

| 용도 | 도구 |
|---|---|
| 시계열 (PerpDEX Vol·OI·TVL 추이, 풀 APY 추이, 토큰 가격) | Lightweight Charts |
| 컨퍼런스 날짜를 가격 차트 위에 점으로 표시 (7번) | Lightweight Charts의 **markers** 기능 |
| 막대·비교표·순위표 | Lightweight Charts는 시계열 전용이라 **부적합**. 일반 HTML 표 또는 CSS 막대로 처리 |

---

## 5. 디자인: Stripe 기반 + 벤토 + 마스코트

"프로페셔널하면서도 친근한"을 UI 스타일 갤러리에서 고른 조합.

### 채택 (5개)

| 갤러리 항목 | 쓰는 곳 |
|---|---|
| **스트라이프 스타일** (기본 톤) | 전체 색·타이포. 핀테크의 "신뢰 + 말 걸기 쉬움" 표준 |
| **벤토 그리드** (배치) | 홈·대시보드. 카드 크기로 중요도를 표현 |
| **마스코트** (친근함) | `assets/pagu-mascot.png`. **빈 상태·에러·로딩에만** 등장 |
| **라인 아이콘** | 메뉴·카드 아이콘. 가볍고 중립적 |
| **마이크로 인터랙션 + 스태거** | 버튼 눌림, 목록 등장. 0.2초 내외로 아주 절제 |

### 색 토큰

```css
--bg:      #f6f9fc;  /* 배경 — 흰색보다 아주 살짝 푸른 회색 */
--panel:   #ffffff;  /* 카드 */
--fg:      #0a2540;  /* 제목 — 네이비 */
--muted:   #425466;  /* 본문·설명 */
--accent:  #635bff;  /* 강조·링크·버튼 */
--up:      #0a966f;  /* 상승 */
--down:    #e0415b;  /* 하락 */
--warn:    #b4762e;  /* 예시 데이터 배지 */
```

- 히어로의 사선 그라디언트 띠는 **홈 최상단에 한 번만**. 나머지 페이지는 평평하게.
- 숫자는 전부 `font-variant-numeric: tabular-nums` + 오른쪽 정렬 + 단위 표시 (AGENTS.md 규칙).
- 터치 영역 최소 44px, 키보드 접근성 유지 (AGENTS.md 규칙).

#### 색을 새로 쓸 때 (2026-10-13 추가, 실제 버그에서 나온 규칙)

**CSS 에 색을 직접 적지 않는다. 반드시 `--pagu-*` 에서 파생시킨다.**

```css
--my-thing: var(--pagu-muted);          /* 좋음 — 다크에서 같이 뒤집힌다 */
--my-thing: #33475b;                    /* 금지 — 다크에서 라이트 값으로 남는다 */
background: rgba(255, 255, 255, 0.8);   /* 금지 — 다크에서 흰 띠가 된다 */
```

이유: `prefers-color-scheme: dark` 는 OS 설정만 보고, `color-scheme: light` 로는
막을 수 없다. 그래서 다크일 때 `--pagu-*` 파생 색만 어두워지고 고정색은 그대로
남아 **어두운 배경 위 어두운 글자**가 된다. 실제로 계산기 5개 페이지의 입력
라벨이 이 이유로 안 보였다 (`styles-v2.css` 의 `--gray-400/500/700/800`,
`--on-accent`, `--blue-100`, 헤더·하단바의 흰색 `rgba`).

반투명 표면이 필요하면 토큰으로 빼고 다크 값을 따로 준다
(`var(--glass, rgba(255,255,255,0.82))` 패턴).

검사 방법: 브라우저를 다크로 두고 각 페이지에서 글자색 대 실제 칠해진 배경색의
대비비를 재서 일반 글자 4.5:1, 큰 글자 3:1 미만이 0건인지 본다.

**예외를 둘 때는 양쪽 실측값을 숫자로 적는다 (2026-10-13 추가, 3단계 세션 제안).**
고정색이 정말 맞는 경우가 있다 — 배경이 양쪽 테마에서 중간 밝기로 유지되는 칸이 그렇다.
그럴 때 주석에 `theme-ok:` 와 함께 **라이트·다크 양쪽 대비값**, 그리고 **테마 토큰을 쓰면
왜 깨지는지**를 적는다. 본보기는 `src/calculator.css:632`:

```css
/* --gray-400 은 라이트 #8fa0b5 / 다크 #7b90a6 으로 양쪽 다 중간 밝기라서,
   이 글자색은 테마를 따라가지 않는 것이 맞다 (라이트 6.7:1 / 다크 5.4:1).
   --gray-900 을 쓰면 다크에서 2.6:1 로 깨지므로 쓰지 말 것. */
.uni-bar > div[data-key="quote"] { background: var(--gray-400); color: #07182b; }
```

**숫자를 적게 하는 것이 요점이다.** 재지 않은 예외는 추측이고, 추측은 틀린다.
실제로 2026-10-13 에 `components.css` 의 `.pagu-btn--primary` 가 "accent 는 양쪽 테마
모두 진한 색이니 흰 글자가 맞다"는 근거로 `color: #ffffff` 를 두고 있었다. 재 보니
`--pagu-accent` 는 다크에서 **연한 라벤더 `#9b95ff`** 로 뒤집혀 흰 글자가 **2.58:1**
(큰 글자 기준 3:1 에도 미달)이었다. 그 클래스를 쓰는 페이지가 0건이어서 화면에는
드러나지 않았을 뿐, 누가 쓰는 순간 깨지는 덫이었다 (`dae1d71` 에서 `--on-accent` 로 고침).
**근거를 숫자 없이 주석에만 적어 두면, 틀린 주장이 검사를 영구히 눈감게 만든다.**

#### 공유 부품의 *기본* 규칙을 바꿀 때 (2026-10-13 추가, 3단계 세션 제안)

**미디어쿼리 밖의 기본 규칙을 바꾸기 전에, 그 셀렉터를 쓰는 곳을 먼저 센다.**

```bash
grep -rl "pagu-source-bar__pick" --include=*.html --include=*.js .
```

이유: 미디어쿼리 안의 추가는 그 폭에서만 영향이 가지만, 기본 규칙은 **그 부품을
쓰는 모든 페이지**를 한꺼번에 바꾼다. 실제로 Pendle 에 네트워크 드롭다운을 넣으면서
`.pagu-source-bar__pick` 의 `grid-template-columns` 를 2칸에서 3칸으로 바꿨다 —
그 부품을 쓰는 페이지가 Pendle 3개뿐이어서 무사했지만, 2칸짜리 페이지가 하나라도
있었으면 그 페이지 선택 바가 조용히 틀어졌다.

**테스트로 잡을 종류가 아니다.** "이 부품은 3곳에서만 쓴다"를 단언하는 테스트는
다른 페이지가 그 부품을 정당하게 채택하는 순간 거짓 실패가 된다. 그래서 테스트를
억지로 만들지 않고 이 문서 규칙으로 둔다. 바꾸는 사람이 먼저 세고, 센 결과를
커밋 메시지에 적는다.

### 배제한 것과 이유

다크네온 · 사이버펑크 · 글리치 · Y2K · 베이퍼웨이브 → "크립토 티"가 나서 AGENTS.md가 금지.
뉴모피즘 · 클레이모피즘 → 숫자 가독성이 떨어짐. 금융 데이터에 부적합.
맥시멀리즘 · 브루탈리즘 → 신뢰감을 깬다.

> **AGENTS.md 수정 필요**: 현재 "흰색/연회색/**파란색**, Toss-like"로 적혀 있다.
> Stripe의 강조색 `#635bff`는 보라빛 파랑이다. 방향(라이트·금융·조용함)은 같지만
> 색 지정이 달라지므로, AGENTS.md의 해당 줄을 이 문서 §5 토큰을 가리키도록 고친다.

---

## 6. 공통 부품 (0단계에서 1번만 만든다)

이걸 먼저 만들지 않으면 페이지마다 다른 모양이 생긴다.

1. **`src/tokens.css`** — §5의 색·간격·반경·그림자 토큰
2. **`<pagu-chart>`** — Lightweight Charts 감싼 공통 차트 (attributionLogo 항상 켬)
3. **데이터 상태 배지** — 모든 숫자 옆에 붙는 3종 표시
   - `실데이터` + 마지막 갱신 시각 + 출처 링크
   - `예시` (노란 테두리) — mock 또는 수동 입력
   - `준비 중` (회색)
   - *AGENTS.md의 "mock과 실데이터를 명확히 구분" 규칙을 부품 수준에서 강제한다*
4. **`data/` JSON 스키마 규칙** — 모든 파일이 아래를 공통으로 가진다
   ```json
   { "updatedAt": "ISO8601", "source": "defillama|pendle|sheet|manual",
     "status": "live|example|stale", "data": [] }
   ```
5. **`.github/workflows/fetch-data.yml`** — 하루 1회 cron, 실패해도 이전 JSON 유지

---

## 7. 단계별 진행 (세션 분리 기준)

**한 세션 = 한 단계.** 각 세션은 이 문서를 먼저 읽는다.

| 단계 | 내용 | 선행 | 병렬 |
|---|---|---|---|
| **0** | Vercel 배포 + 도메인 연결 + §6 공통 부품 5개 | — | — |
| **1A** | 계산기 수식 검증 (APR-APY, CLAMM LP) | 없음 | 0단계와 동시 가능 |
| **1B** | Boros 기회 페이지 | 0단계 | — |
| **2** | PerpDEX 대시보드 (시트 2탭 그래픽화) | 0단계 | — |
| **3** | Pendle 계산기 묶음 (시트 3탭 + 라이브 가격) — **완료, §11** | 0단계 | 2와 동시 가능 |
| **4** | DeFi Pool 추적 + 프론트 생존 모니터 + 탈출 설명서 | 0단계 | 2·3과 동시 가능 |
| **5** | 컨퍼런스 파트너 추적 (Dots 자료 + 마커 차트) | 0단계, 사용자 자료 | — |

### 단계별 메모

**0단계** — `pagu.info`는 현재 아무것도 가리키지 않는다. 저장소에 배포 설정이 전혀 없다
(`vercel.json`, `.github/`, `CNAME` 모두 없음). 여기서 전부 만든다.

**1A** — 저장소에 이미 `calculators/apr-apy.html`, `uniswap-v3.html`이 있다.
새로 만드는 게 아니라 **수식을 검증**하는 일이다. CLAMM LP는 기존 Uniswap V3 계산기를
정밀화하는 방향. 입력·기대출력 테스트 케이스를 같이 남긴다.

**1B** — `pendle-finance/arbitrage-with-crossex`는 **실제 주문을 내는 프로그램**이다
(TypeScript, Gate API 키 필요, README에 자금 손실 경고). 우리는 **기회 숫자만** 보여준다.
주문·서명·API 키 경로는 일절 만들지 않는다. 공개 데이터(Boros 고정 APR, 각 거래소 펀딩률)로
스프레드를 다시 계산해 표로 보여주고, "실제 실행은 Pendle 공식 도구로" 링크만 건다.
목적은 "지금 xxx·yyy에 이런 기회가 있다"를 알려 관심을 만드는 것.

**2단계** — 시트 2개 탭이 소스다.
- `gid=577021711` — PerpDEX Comp. 주간 컬럼으로 Vol / Rev·Fee / OI / TVL / Vol÷OI / OI÷TVL.
  출처는 시트에 "DeFiLlama & Perpetualpulse & Coingecko"로 적혀 있다.
- `gid=2087382385` — TGE Data와 Current Data 대조. Lighter·Backpack·edgeX의
  1M Vol / OI / Funding / FDV / Market Cap. **예상 토큰 밸류에이션의 근거 테이블.**
- 시트 자동화(`automation/PaguSheetAuto.gs`)는 **현재 동작하지 않는다.**
  사본 시트에서 테스트만 하고 본 시트에 적용하지 않았으며 `CG_API_KEY`도 비어 있다.
  → 2단계는 "자동화를 되살릴지" vs "Actions가 시트 대신 직접 수집할지" 중 하나를 먼저 정한다.
  Actions가 직접 하는 쪽이 단순하지만, 예상 밸류에이션 모델은 시트에만 있으므로
  **모델 부분은 시트에서 읽고 원천 데이터는 Actions가 받는** 혼합이 유력하다.

**3단계** — Pendle 시트 3탭이 기존 계산기 3개와 **정확히 1:1 대응**한다.
| 시트 gid | 내용 | 대응 파일 |
|---|---|---|
| `964795995` | YT Profit Simulator (Implied/Underlying/Interest/Reward APY, 만기) | `calculators/pendle-yt.html` |
| `344127195` | PT Leverage·Looping (투자금, 남은일수, Swap PI, SY/YT/PT 가격) | `calculators/pendle-pt-looping.html` |
| `240987043` | YT 가격 × 할인율 → Implied APY 격자 (Day1~7) | `calculators/pendle-yt-implied-apy.html` |

즉 3단계는 신규 제작이 아니라 **시트와 대조해 수식을 맞추고, 만기·가격을
Pendle API 라이브 값으로 채우는** 작업이다.
주의: 시트 값들은 오래됐다 (`964795995`는 남은 일수가 **-117일**, `344127195`는
"Update as of 3/14/2026"). 시트는 **수식의 정답지**로만 쓰고 숫자는 API에서 받는다.

**4단계** — 하루 1회로 시작한다. 그 뒤 측정해서 더 줄일지 판단한다.
- 더 자주 할 수 있나: `/pools` 1회가 11.6MB·1.2초다. 추려서 커밋하면 부담은 "커밋 용량"이지
  API 한도가 아니다. 1시간 간격이면 하루 24커밋 — 가능하지만 저장소가 빨리 커진다.
  **대안: 자주 받되 값이 바뀐 풀만 커밋**하면 용량이 크게 준다. 4단계에서 실측 후 결정.
- 풀 과거 이력은 DeFiLlama `/chart/{poolId}`가 이미 준다. **우리가 쌓을 필요가 없는 부분**이다.
  우리 스냅샷은 "DeFiLlama가 안 주는 것"(우리가 고른 풀 집합, 우리 파생 지표)에만 쓴다.
- **프론트 생존 모니터**를 여기에 합친다. 각 풀 프론트에 Actions가 하루 몇 번 접속해
  응답 코드를 `data/frontend-health.json`에 기록하고, 죽으면 풀 카드에 경고를 띄운다.
  나중에 텔레그램 알림으로 확장할 자리다.
- **풀별 탈출 설명서**를 풀마다 서브페이지로 둔다 (`pools/<slug>/escape.html`).
  컨트랙트 주소, 호출할 함수, Etherscan Write Contract 절차. **정적 문서만. 지갑 연결 없음.**

**5단계** — 컨퍼런스 자료는 사용자가 OpenAI Dots로 수집해 전달한다.
우리는 받은 명단을 `data/conferences.json`으로 정규화하고, 토큰 가격 시계열 위에
컨퍼런스 날짜를 **마커**로 찍는다. 코드보다 자료 정규화가 일의 대부분이다.

---

## 8. 하지 않기로 한 것 (경계)

- **PerpDEX Data Hub 일절 미사용.** 코드·DB·API·파일 어느 것도 읽지 않는다.
  이유: 파밍봇의 운영 시스템이고 "본인 PC 외부로 노출 없음" 정책 아래 있다.
  추가로, 실제 수집 범위가 7개 거래소뿐이고 **TVL은 아예 수집하지 않아** 이 사이트 용도에 맞지도 않는다.
- **지갑 연결·서명·트랜잭션 생성 없음.** 사이트 전체에서. (Vault 탈출기 보류의 핵심 이유)
- **API 키를 브라우저에 두지 않음.** 키가 필요한 소스가 생기면 Actions 비밀값으로만.
- **유료 데이터 플랜 구매 없음.** 무료로 안 되는 게 확인되면 그때 재논의.

---

## 9. 각 세션의 공통 수락 기준

작업이 끝났다고 말하기 전에 전부 통과해야 한다.

1. `node --check src/app.js` 통과
2. 로컬에서 바뀐 페이지가 **200 응답** (`python preview_server.py`, 포트 4174)
3. 데스크톱과 **360px 모바일** 양쪽에서 레이아웃 깨짐 없음
4. 모든 숫자에 **단위 + 오른쪽 정렬 + 데이터 상태 배지**가 있음
5. 예시 데이터와 실데이터가 화면에서 **구분됨**
6. 새로 추가한 외부 호출이 **실패했을 때의 화면**이 정의돼 있음
7. 작업 후 `git status`로 **의도한 파일만** 바뀌었는지 확인

---

## 10. 0단계 완료 (2026-10-13)

**0단계 완료.** Vercel 배포 준비와 §6 공통 부품 5개를 모두 만들었다.
남은 것은 사용자 계정에서만 할 수 있는 작업(Vercel 프로젝트 연결, 도메인 DNS)뿐이며
절차는 `docs/deploy-vercel.md` 에 있다.

### 실제로 만든 파일

| 파일 | 역할 |
|---|---|
| `vercel.json` | 빌드 없는 정적 서빙 설정 + 캐시·보안 헤더 |
| `src/tokens.css` | §5 색 토큰 + 간격·반경·그림자·타이포, 다크모드 |
| `src/components.css` | 배지·차트 카드·표·버튼 등 부품 스타일 |
| `src/pagu-chart.js` | `<pagu-chart>` — Lightweight Charts **5.2.1** 래퍼 |
| `src/pagu-badge.js` | `<pagu-badge>` — 데이터 상태 배지 3종(+stale) |
| `data/README.md` | `data/*.json` 공통 껍데기 규칙 |
| `data/example-timeseries.json` | 스키마 예시 겸 차트 시연용 **가짜** 시계열 60일 |
| `data/defillama-protocols.json` | 실데이터 — 프로토콜 10개 TVL (Actions 가 매일 갱신) |
| `scripts/fetch-data.mjs` | 데이터 수집기 (의존성 없음, Node 18+ 내장 fetch) |
| `.github/workflows/fetch-data.yml` | 매일 00:20 UTC(09:20 KST) cron + 수동 실행 |
| `components.html` | 공통 부품 동작 확인 페이지 (`noindex`) |
| `docs/deploy-vercel.md` | 사용자가 따라 할 Vercel·도메인 연결 절차 |

### 구현하면서 정한 것 (§5 결정을 바꾸지 않는 범위)

1. **토큰 이름에 `--pagu-` 접두사를 붙였다.** 값은 §5 그대로다.
   `src/styles.css`·`src/styles-v2.css` 가 이미 `--bg`·`--text`·`--blue` 를 쓰고 있어
   같은 이름을 다시 정의하면 기존 페이지 색이 전부 바뀐다.
   §5 의 짧은 이름은 `<body data-pagu-tokens>` 안에서 별칭으로 제공한다.
2. **차트 CDN 버전을 5.2.1 로 고정하고 SRI 해시를 걸었다.**
   `attributionLogo: true` 는 래퍼가 강제하며 외부에서 끌 수 없다(라이선스 의무, §4).
   버전을 올릴 때는 `src/pagu-chart.js` 의 `LWC` 상수에서 URL 과 해시를 함께 바꾼다.
3. **배지 상태에 `stale` 을 하나 더 뒀다.** `live` 인데 갱신이 48시간 넘게 멈추면
   자동으로 "실데이터(오래됨)"으로 바뀐다. 수집이 실패한 날 어제 숫자가 실데이터처럼
   보이는 것을 막기 위한 장치다(§9-5·§9-6).
4. **배지·차트 스타일은 `src/components.css` 로 분리했다.** `tokens.css` 는 값만 담는다.

### 검증 결과 (전부 이 PC에서 직접 실행, ran-here)

| 항목 | 결과 |
|---|---|
| `node --check src/app.js` | 통과 (신규 JS 3개도 함께 통과) |
| 로컬 200 응답 | `/`, `/index.html`, `/components.html`, `/calculators/index.html`, `/calculators/apr-apy.html`, `/data/*.json`, `/src/*` 전부 200 |
| 차트 렌더 | Lightweight Charts 5.2.1 로드 확인, 캔버스 생성, TradingView 표기 링크 존재 |
| 배지 렌더 | `live` · `example` · `pending` · `stale` 4종 모두 화면 표시 |
| 실패 화면 | 없는 JSON 을 가리켰을 때 "데이터를 불러오지 못했습니다 · HTTP 404" 안내 표시 |
| 360px 모바일 | 가로 넘침 0px, 표는 자체 영역에서만 가로 스크롤 |
| 데이터 수집기 | 실제 실행 → DeFiLlama 에서 10개 프로토콜 수집·커밋 대상 파일 생성 |

### 전면 재디자인 (2026-10-13, 사용자 승인 후 같은 세션에서 수행)

처음에는 "새 페이지부터 새 토큰을 쓰고 기존 페이지는 천천히 옮긴다"였으나,
사용자가 **기존 사이트 전체를 새 디자인으로 교체**하도록 승인해 그 자리에서 교체했다.

**교체 방식**: 기존 `src/styles-v2.css` 는 색을 거의 전부 변수로 쓰고 있었다
(하드코딩 색은 2,251줄 중 21곳뿐). 그래서 **구조·클래스·HTML 을 다시 쓰지 않고**
그 파일의 `:root` 가 `src/tokens.css` 의 `--pagu-*` 를 가리키게 바꿨다.
한 곳을 고쳐 13개 페이지가 전부 Stripe 톤으로 바뀌었고, `src/app.js` 가 만드는
DOM 과 클래스 이름은 하나도 건드리지 않았다.

| 바뀐 것 | 내용 |
|---|---|
| `src/styles-v2.css` `:root` | 회색 램프를 네이비 톤으로, 파랑을 `#635bff` 로, 상승/하락/예시를 §5 값으로 |
| 페이지 배경 | 흰색 → `#f6f9fc`. 카드는 흰색이라 이제 카드가 떠 보인다 |
| 하드코딩 흰색 17곳 | `var(--surface)` / `var(--on-accent)` 로 교체 |
| 반경·그림자 | `--pagu-radius-*`, `--pagu-shadow-*`(네이비 톤 그림자)로 통일 |
| 홈 히어로 | §5 의 사선 그라디언트 띠 추가. **홈에서 한 번만** |
| 마이크로 인터랙션 | 카드 hover 시 2px 상승. 0.14초, `prefers-reduced-motion` 존중 |
| 숫자 | `tabular-nums` + 오른쪽 정렬 규칙 보강 |
| 터치 영역 | 상단/하단 메뉴 항목 최소 44px 보장 |
| 861~1100px 구간 | 상단 메뉴가 잘리던 문제 수정 (패딩·글자 크기 축소) |
| 13개 페이지 `<head>` | `tokens.css` 링크 추가, 캐시 버전을 `v=20261013-v3` 로 |
| `src/styles.css` (2,954줄) | **삭제.** 어느 페이지도 참조하지 않는 죽은 파일이었다 |

**토큰 이름**: 정식 이름은 `--pagu-*` 하나로 유지했다. `styles-v2.css` 안의
`--bg`·`--text`·`--blue-500` 같은 옛 이름은 `--pagu-*` 를 가리키는 별칭으로 남겼다.
약 2,000줄의 규칙을 다시 쓰지 않고도 색의 출처를 한 곳으로 모으기 위해서다.
**색을 바꾸려면 `src/tokens.css` 만 고치면 된다.**

**다크 모드**: 새 부품(`tokens.css`·`components.css`)과 `components.html` 에만 적용된다.
기존 13개 페이지는 지금도 `color-scheme: light` 고정이다. 반투명 흰색이 남아 있는
헤더·모달이 다크에서 깨지므로, 전체 다크 모드는 별도 작업으로 둔다.

**재디자인 검증 (ran-here)**

| 항목 | 결과 |
|---|---|
| 13개 페이지 전부 | HTTP 200, `tokens.css` 링크 확인, 옛 `styles.css` 참조 0건 |
| 가로 넘침 | 360px·1024px 양쪽에서 **모든 페이지 0px** |
| 계산기 동작 | APR 20% + 일복리 → 화면 22.13%, 수학적 기대값 22.13% 일치. 입력 변경 시 즉시 재계산됨 |
| `src/app.js` | **변경 0줄** (1A 세션의 수식 검증과 충돌 없음) |
| 접근성 | 건너뛰기 링크 숨김 유지, 포커스 링 유지, 메뉴 터치 영역 44px |

### 다음 단계로 넘어가기 전에 사용자가 할 일

1. `docs/deploy-vercel.md` 1번 — Vercel 에서 저장소 Import (약 3분)
2. 같은 문서 2번 — `pagu.info` DNS 레코드 추가 (전파 대기 있음)
3. 같은 문서 3번 — GitHub Actions 에 쓰기 권한 주기 (1분)

이 셋이 끝나면 1B·2·3·4 단계를 (문서 §7 표대로) 시작할 수 있다.

---

## 11. 3단계 완료 (2026-10-13)

**Pendle 계산기 3개를 시트와 대조하고 Pendle 라이브 가격을 붙였다.**
§7 의 3단계 메모대로 신규 제작이 아니라 **대조 + 라이브 연결** 작업이었다.

### 시트 대조 결과 (전체 근거: `docs/pendle-tests.md` §9)

CSV 로 3탭을 받아 **52개 항목 중 49개 일치**. 어긋난 3개는 전부 원인을 특정했고
**모두 시트 쪽이 틀렸다**고 판단해 우리 수식을 유지했다.

| 어긋난 곳 | 시트 | 우리 | 판단 근거 |
|---|---|---|---|
| YT 지급액의 이자 복리 | 이자+보상을 통째로 단리 | 이자만 기간 복리 | Pendle 공식 APY Calculation 문서. 시트도 칸은 나눠 놓고 계산만 섞었다 |
| Auto·Manual 스왑 비용 | 순이익에 **더함** | 뺌 | 비용이 이익을 늘릴 수 없고, 시트의 Mint 탭은 같은 값을 빼고 있다 |
| Mint 의 YT 회차 | 15회 | 16회 | 시트가 PT·차입은 16회로 세면서 YT 만 15회다 (자체 모순) |

GTS 점수(−164.2)가 소수점까지 재현돼, 출처 불명이던 비공식 점수 수식이 시트의
그 수식임이 확인됐다. Implied APY 격자는 **전 칸이 시트와 같았고**, 시트 안의
rsENA 사례에 적힌 **Pendle 화면 실제 값**과도 맞았다.

시트에만 있던 출력 4개를 화면에 추가했다 — Requested ROI / Requested APY /
재투자 가정 APY / Efficiency(PT 단독 대비 배수).

### 라이브 가격

- **저장본 + 라이브 두 갈래.** `data/pendle-markets.json`(Actions 하루 1회)으로 먼저
  그리고, 브라우저가 `api-v2.pendle.finance` 를 한 번 호출해 덮는다. 둘 다 실패하면
  예시 입력값이 남고 실패 사유가 화면에 뜬다. Vercel 함수·Cron 없음 (D1 유지).
- 수집기와 화면이 **`src/pendle-live.js` 하나**를 공유해 모양이 어긋날 수 없다.
- YT 가격 비율은 API 의 `ptDiscount` 다 (PT + YT = 기초자산 1단위). 4체인 61개 재측정:
  61건 중 58건이 Implied APY 역산값과 1e-4 이내이고, 만기가 멀수록 작아진다
  (442일 1.6e-7 / 1.7일 4.0e-4). **1e-6 을 일반 규칙으로 쓰면 안 된다** —
  처음 적은 "47개 전부 1e-6" 은 그때 목록이 장기 마켓 위주였던 탓이다 (`docs/pendle-tests.md` §11.2).
- 거래가 드문 마켓에서는 `ptDiscount`(지금 호가)와 `impliedApy`(마지막 거래)가 실제로
  벌어진다. 온체인 `lastLnImpliedRate` 로 판정해 **어느 쪽도 고치지 않고**, 0.5%p 넘게
  어긋나면 화면에 그렇다고 적는다 (§11.3).
- 남은 기간은 **소수 일수**로 넣는다. 하루로 반올림하면 Implied APY 가 Pendle 화면과
  0.2%p 벌어진다. 소수로 넣으면 우리 화면이 Pendle 과 같은 숫자를 보여 준다.
- 사용자가 칸을 직접 고치면 배지가 "직접 입력한 값"으로 바뀌고 출처 표기가 사라진다.
- **체인 4개**(Ethereum 47 / Base 6 / Arbitrum 4 / BNB 4)를 한 번에 받아 네트워크 선택
  드롭다운으로 고른다. 엔드포인트는 그대로이고 온체인 RPC 도 쓰지 않는다 (§11.1).
  체인 하나가 실패하면 나머지로 계산을 이어 가고 빠진 체인 이름을 화면에 적는다.
  사용자 승인 범위는 여기까지다 — **임의 마켓 주소 입력은 넣지 않았다.**

### 만든·고친 파일

| 파일 | 내용 |
|---|---|
| `src/pendle-live.js` | **신규.** Pendle API·저장본 읽기 + 정규화 (순수 함수는 테스트 대상) |
| `src/pendle-math.js` | `requestedRoi`·`requestedApr`·`efficiency`·`ptAloneGain` 추가 (기존 반환값 불변) |
| `src/app.js` | `initPendleMarkets()` 추가, 계산기 3개에 마켓 선택·배지 연결, 새 결과칸 |
| `src/components.css` | `.pagu-source-bar` 공통 부품 추가 (색은 `--pagu-*` 에서만 파생) |
| `scripts/fetch-data.mjs` | Pendle 수집 작업 + `--only=<파일>` 플래그 (여러 세션이 쓰므로 한 소스만 갱신 가능) |
| `data/pendle-markets.json` | **신규.** 4체인 활성 61개 마켓, 33.3KB (API 원본 63건 중 유동성 $1만 미달 2건 제외) |
| `calculators/pendle-*.html` | 마켓 선택 바 + `<pagu-badge>`, 고정 문구 "수동 입력 예시" 제거, 남은 기간 칸이 소수 허용 |
| `tests/pendle.test.js` | 기존 32개 확장 + 시트 대조·라이브 마켓 2개 추가 |
| `docs/pendle-tests.md` | §9 신설 — 대조 52항목, 판단 근거, 실행 확인 |

### 수락 기준 (§9) 확인 — 전부 ran-here

1. `node --check` 4개 통과 2. 로컬 200 (포트 4176, 4174 는 다른 세션이 사용)
3. 360px·데스크톱 가로 넘침 0 4. 단위·우측 정렬·배지 있음
5. 예시/실데이터/직접입력이 배지로 구분됨 6. 라이브 실패 화면 정의·확인
7. `git status` 로 이 세션이 건드린 파일만 스테이징 (다른 세션의 SEO 작업 제외)

테스트 **88/88 통과** (직전 86 → +2). 기존 단언을 지우거나 약화하지 않았다.

### 남은 것

`docs/pendle-tests.md` §0 의 **열린 질문 5개는 그대로다.** 시트 대조는 "시트와 같은
계산을 하는가"를 확인한 것이지 "그 모델이 실제 수익과 같은가"를 확정한 것이 아니다.
특히 Auto 의 공급 APY 와 PT APY 가 서로 겹치는 수익인지는 시트도 답을 주지 않는다.
차입 APY·LTV 는 Pendle 이 아니라 대출 플랫폼(Euler 등) 값이라 라이브로 채우지 않는다 —
그쪽까지 자동화하려면 대출 플랫폼 API 를 별도로 붙여야 한다.

---

## 12. 2단계 완료 (2026-10-13)

**PerpDEX 대시보드를 기존 `perpdex-dashboard.html` 안에 채웠다.** §7 의 2단계 메모가
남겨 둔 "자동화를 되살릴지 vs Actions 가 시트 대신 직접 수집할지"를 먼저 정했다.

### 결정: 혼합 — 그리고 Apps Script 는 되살리지 않는다

§7 이 유력하다고 본 **혼합**을 그대로 택했다. 다만 "원천 데이터는 Actions 가 받는다"가
**시트의 Vol 열을 재현한다는 뜻은 아니다.** 무료로는 재현이 불가능함을 확인했기 때문이다.

- Actions 가 **시트 2탭을 CSV 로 받아 거울처럼 저장**한다 (주간 이력 + 밸류에이션 모델).
- Actions 가 **"지금 숫자"는 CoinGecko·DeFiLlama 에서 직접** 받는다. 시트를 거치지 않는다.
- `automation/PaguSheetAuto.gs` 는 손대지 않고 그대로 둔다. **되살리지 않는다.**

근거 (전부 2026-10-13 직접 호출, ran-here):

| 확인한 것 | 결과 | 그래서 |
|---|---|---|
| 시트 CSV 내보내기 (`export?format=csv&gid=…`) 2탭 | 키 없이 200 | 시트를 읽는 데 Apps Script 도 API 키도 필요 없다 |
| `api.llama.fi/overview/derivatives` | **HTTP 402** "Upgrade to the paid API plan" | 시트의 주간 Vol 열을 무료로 재현할 수 없다 → 시트를 거울로 둬야 한다 |
| `api.coingecko.com/api/v3/derivatives/exchanges` | 키 없이 200, 우리가 보는 19곳 중 **15곳** 커버 | "지금 거래량·OI"는 시트 없이 매일 받을 수 있다 |
| `api.llama.fi/protocols` | 키 없이 200, **19곳 전부** 커버 | "지금 예치금(TVL)"도 매일 받을 수 있다 |
| 시트의 마지막 주 | **2026-07-26** (약 11주 전) | 시트만 믿으면 화면이 11주 묵은 숫자만 보여 준다 |

**Apps Script 를 되살리지 않는 이유는 "고치기 어려워서"가 아니라 책임 경계다.**
살려 두면 숫자를 받아오는 곳이 (구글 서버 + GitHub Actions) 둘이 되고, 화면이 멈췄을 때
어느 쪽이 멈춘 건지 구분할 수 없다. Actions 한 곳만 두면 실패가 한 군데에서만 나고,
`CG_API_KEY` 를 구글 쪽에 둘 이유도 사라진다 (§8 "브라우저에 키 없음"과 같은 결).
시트는 **사람이 손으로 모델을 고치는 자리**로만 남는다.

### 파일을 4개로 나눈 이유

| 파일 | 상태 | 크기 | 내용 |
|---|---|---|---|
| `data/sheet-perpdex-weekly.json` | `live` | 83KB | 45주(2025-09-21~2026-07-26) × 18곳 × 6지표 |
| `data/sheet-perpdex-valuation.json` | `stale` | 7KB | TGE/Current 벤치마크 + 배수 6개 + 추정 12곳 |
| `data/coingecko-perpdex.json` | `live` | 3KB | 지금 24시간 거래량·OI, 15곳 |
| `data/defillama-perpdex-tvl.json` | `live` | 5KB | 지금 예치금(TVL), 19곳 |

한 파일에 몰면 **배지 하나가 네 출처를 대표하게 되어 거짓말이 된다.** 나눠 두면 CoinGecko 가
죽은 날에도 TVL 은 갱신되고, 화면의 어느 표가 멈췄는지 배지만 보고 알 수 있다.

**"오래됨"이 저절로 뜨게 만든 장치**: 주간 파일의 `updatedAt` 에 *받아온 시각*이 아니라
**시트 마지막 주의 끝 날짜**를 넣었다. 그래서 시트가 멈추면 48시간 뒤 배지가 스스로
"실데이터(오래됨)"으로 바뀐다. 받아온 시각은 `fetchedAt` 에 따로 둔다. 밸류에이션 탭에는
기준일이 아예 없어서 `status: "stale"` 로 고정하고 `sheetAsOf: null` 을 적었다.

### 시트를 읽으면서 걸린 함정 3가지

1. **섹션마다 날짜가 다르다.** OI·TVL 섹션 머리글에 `10/12` 가 두 번 들어간 오타가 있다.
   섹션마다 날짜를 따로 읽으면 그 오타가 주 축을 망가뜨린다.
   → 주 축은 **Vol 섹션 머리글 하나로만** 만들고 나머지 섹션은 열 번호로 맞춘다.
2. **연도가 없다.** 머리글이 `9/21` 처럼 월/일뿐이다. → 가장 오른쪽 열을 "미래가 아닌 가장
   최근 연도"로 잡고 왼쪽으로 가며 역산한다. 날짜가 엄격히 줄지 않으면 예외를 던진다.
3. **△ 칸은 반올림 전 값으로 계산돼 있다.** 그래서 작은 칸(0.1, 2, 3)은 아무리 제대로 읽어도
   맞출 수 없다. 아래 보존 검사가 작은 칸을 빼는 이유다.

### 보존 검사 2개 (제대로 읽었는지 숫자로 확인)

- **주간**: 시트의 △ 칸과, 우리가 값에서 다시 계산한 증감을 맞춰 본다. 50 이상인 칸만
  5%p 허용으로 비교해 **1,352칸 중 1칸(0.1%)** 어긋났다. 일부러 열을 한 칸 밀어 보면
  **1,303칸 중 825칸(63.3%)** 이 어긋난다 — 검사가 실제로 작동한다는 증거다.
  100칸 넘게 비교했는데 10% 를 넘으면 수집기가 **파일을 쓰지 않고 예외를 던진다.**
- **밸류에이션**: 시트에 적힌 배수 6개를 벤치마크 표에서 sum÷sum 으로 다시 계산해 맞춰 본다.
  최대 오차 **1.55%**. 5% 를 넘으면 예외.

### 시트 모델에서 발견한 것 (고치지 않고 각주로 적었다)

시트의 `by Funding` 배수는 **분자(FDV 합)에는 funding 값이 없는 edgeX 를 넣고, 분모(funding
합)에서는 edgeX 를 뺀다.** (1,670 + 514 + 356) ÷ (68 + 37) = 24.19 로 시트의 24.192 와 같다.
분자가 분모보다 넓은 집합이라 **funding 기준 추정 FDV 는 구조적으로 높게 나온다.**
시트는 사용자의 모델이므로 숫자를 바꾸지 않고, 시트 자신의 `*except edgeX` 표기와 함께
화면 각주로 드러냈다.

### 만든·고친 파일

| 파일 | 내용 |
|---|---|
| `src/perpdex-sheet.js` | **신규.** 시트 CSV 파서. 순수 함수·네트워크 없음 — 테스트와 수집기가 같은 파일을 쓴다 |
| `scripts/lib/perpdex-data.mjs` | **신규.** 위 4개 파일을 만드는 수집기 (시트 2탭 + CoinGecko + DeFiLlama) |
| `scripts/fetch-data.mjs` | `...PERPDEX_JOBS` 2줄만 추가 (여러 세션이 같이 쓰는 파일) |
| `.github/workflows/fetch-data.yml` | 수집 단계에 `PAGU_SHEET_ID` 비밀값을 `env` 로 연결 |
| `perpdex-dashboard.html` | `<main>` 교체. **새 페이지를 만들지 않았다** |
| `src/perpdex-dashboard.js` | **신규.** 4개 파일을 읽어 그린다. 파일별로 따로 실패한다 |
| `src/perpdex-dashboard.css` | **신규.** `.pdx-*`. 색은 전부 `--pagu-*` 파생, 고정색·반투명 흰색 없음 |
| `src/app.js` | 손으로 적어 둔 예시 표 3개와 `renderPerpDexDashboard()` 삭제 (−89줄) |
| `src/pagu-badge.js` | `SOURCES` 에 `coingecko` 1줄 추가 |
| `data/README.md` | 새 파일 4개 + `source` 어휘에 `coingecko` 추가 |
| `tests/perpdex.test.js` | **신규 18개** (파싱·연도 역산·보존 검사·색 규칙·id 존재·껍데기 검사) |

차트는 §4 대로 **시계열만** `<pagu-chart>` 로 그린다. 순위·크기 비교는 CSS 막대와 표로 했다.

### 수락 기준 (§9) 확인 — 전부 ran-here

1. 테스트 **106/106 통과** (직전 88 → +18). 기존 테스트를 지우거나 단언을 약화하지 않았다.
2. 로컬 미리보기(포트 4176, 4174 는 다른 세션이 사용) 11개 URL 전부 200, 콘솔 오류 0.
   확인 후 서버 종료.
3. 가로 넘침 **360px·1100px 모두 0**. 긴 표는 카드 안에서만 가로로 스크롤한다.
4. **다크·라이트 양쪽에서 글자 524개 대비 측정, 불합격 0.**
5. 숫자마다 단위(M USD / 배 / %)를 적고 우측 정렬, 파일마다 배지 1개.
6. 파일 하나가 실패하면 그 표에만 실패 사유가 뜨고 나머지는 정상으로 그린다.
7. 이 세션이 건드린 경로만 스테이징. `scripts/fetch-data.mjs`·`src/app.js` 에는 다른 세션의
   4단계(풀 추적) 작업이 같이 들어 있어 **내 hunk 만 골라 넣었다.**

### 남은 것

- **시트 문서 ID 는 저장소에 적지 않았다 — 사용자가 비밀값을 한 번 넣어야 한다.**
  이 시트는 "링크가 있는 사람은 보기"(§3 표) 라서 **ID 가 사실상 열쇠**다. 저장소는 지금
  **비공개**라서(2026-10-13 ran-here: `gh repo view` isPrivate=true, 토큰 없는 요청은 404)
  첫 커밋 80523fb 에 들어갔던 ID 의 실제 노출 범위는 0 이다. 다만 git 이력에서는 지울 수
  없으므로 **저장소를 공개로 바꾸면 그 순간 노출된다** — 그때는 탭을 새 문서로 옮겨 새 ID 를
  비밀값에 넣고 옛 문서의 링크 공유를 끊는 것이 유일한 완전한 해결이다. 그래서 ID 는
  GitHub Actions 비밀값
  `PAGU_SHEET_ID` 로만 넣는다 (워크플로의 "데이터 받기" 단계에 `env` 로 연결해 두었다).
  비밀값을 넣기 전까지는 **시트 2개 작업만 실패**하고 이미 커밋된 JSON 이 그대로 남으며,
  CoinGecko·DeFiLlama 두 작업은 영향을 받지 않는다. 같은 문서에 Pendle 탭 3개가 함께
  들어 있어(§3 "Google Sheet 5개 탭") ID 하나가 5개 탭 전부를 연다는 점이 이유다.
- 시트가 2026-07-26 에서 멈춰 있다. **주간 추이는 사용자가 시트를 갱신해야 움직인다.**
  "지금 숫자" 두 줄은 시트와 무관하게 매일 갱신된다.
- CoinGecko 에 없는 4곳(Hibachi·Ostium·Hotstuff·Ethereal)은 "지금 거래량" 칸이 빈다.
  TVL 은 19곳 전부 있다.
- 밸류에이션 탭은 기준일이 없어 영원히 `stale` 이다. 시트에 날짜 칸이 생기면 그때 `live` 로
  바꿀 수 있다 (`sheetAsOf` 자리를 이미 비워 뒀다).

---

## 13. 4단계 완료 (2026-10-13)

풀 추적 + 프론트 생존 모니터 + 풀별 탈출 설명서. §7 "4단계" 메모의 세 가지를 모두 만들었다.

### 측정해서 정한 것: 하루 1회, 전체 스냅샷

§7 에 숙제가 둘 있었다. "더 자주 받을까"와 "값이 바뀐 풀만 커밋할까".
둘 다 **실제로 재서** 답했다 (2026-10-13, ran-here. 추정값이 아니다).

| 잰 것 | 방법 | 결과 |
|---|---|---|
| 파일 크기 | 실제 수집 1회 | `defi-pools.json` **51.4KB** (gzip 6.5KB), 풀 62개 / 프로토콜 8곳 |
| git 증가량 (풀) | 실제 5일치를 연속 커밋하고 `.git` 크기 차이 | **하루 3.1KB** (풀+이력 합쳐) → 1년 약 1.1MB |
| git 증가량 (생존) | 점검 20회를 연속 커밋 | **1회당 919바이트** → 하루 4회 = 3.7KB → 1년 약 1.3MB |
| 합계 | 위 둘 | **하루 6.8KB, 1년 약 2.4MB** |
| "바뀐 풀만 커밋" 이득 | 5일치 스냅샷의 풀별 차이를 셈 | **없음.** 62개 중 **61~62개가 매일 바뀐다** |

- §2 분리 기준(단일 파일 500KB·연간 50MB) 대비: 파일은 **10분의 1**, 연간은 **5% 미만**.
  데이터 전용 저장소로 뺄 필요가 없다. §2 표의 A~B 구간 안에 들어온다.
- **"바뀐 풀만 커밋"은 기각.** 거의 전부가 매일 바뀌므로 줄어드는 양이 없고 코드만 복잡해진다.
- **더 자주 받을 이유도 없다.** DeFiLlama 의 풀 이력 자체가 **하루 한 줄**이라
  1시간마다 받아도 새 숫자가 생기지 않는다. 받아 오는 쪽에 없는 정밀도를 우리가 만들 수는 없다.
- 생존 점검만 **6시간 간격(하루 4회)**으로 따로 돈다. "지금 열리나"는 시시각각 달라지므로
  자주 보는 값이 맞다. 워크플로가 `github.event.schedule` 로 모드를 가른다.

### 풀별 과거 이력은 우리가 쌓지 않는다

§7 메모대로다. `/chart/{poolId}` 가 풀 하나의 전체 이력을 이미 준다.
우리가 저장하는 것은 **DeFiLlama 가 주지 않는 것**뿐이다 — (1) 우리가 고른 풀 집합,
(2) 우리가 계산한 `TVL 하루 변화`, (3) 우리 집합의 **합계** TVL 시계열.
표의 각 줄에서 DeFiLlama 풀 페이지로 나가는 `과거 이력` 링크를 둬서 사용자는 전체 이력을 볼 수 있다.

운영 사실 하나: `/chart` 를 동시에 4개씩 요청하면 **62개 중 17개가 HTTP 429** 로 막혔다.
순차 + 350ms 간격으로는 62/62 성공했다. 지금 production 은 `/chart` 를 **아예 호출하지 않지만**,
나중에 쓸 일이 생기면 이 간격을 지켜야 한다.

### 프론트 생존 모니터 — 거짓 경보를 내지 않는 쪽으로 설계

`data/frontend-health.json` 에 응답 코드를 기록한다. 핵심은 **"죽었다"고 잘못 말하지 않는 것**이다.

- `401`·`403`·`405`·`429` 는 `blocked` 로 따로 센다. 봇 차단이지 장애가 아니다
  (실제로 점검 대상 일부가 Cloudflare 로 403 을 준다). 경고를 띄우지 않는다.
- 경고는 **연속 2회 `down`** 부터. 한 번 실패로는 띄우지 않는다.
- 분류할 수 없는 코드는 `unknown` 이다. 죽음으로 단정하지 않는다.
- `lastOkAt`(마지막으로 열린 걸 본 시각)은 지우지 않고 누적한다.
- **대상 전부가 네트워크 단계에서 실패하면 수집이 예외를 던진다.** Actions 러너 쪽 네트워크
  문제로 "전원 사망" 기록이 남지 않게 하려는 것이다 → 이전 JSON 이 그대로 남는다.
- 텔레그램 알림 자리: `isAlerting(row)` 가 그 판단을 한곳에 모아 둔 함수다. 여기만 쓰면 된다.

### 풀별 탈출 설명서 8개 — 주소를 온체인으로 검증했다

`pools/<slug>/escape.html` 8개. **정적 문서만. 지갑 연결·서명·트랜잭션 생성 없음** (§8 경계).
Etherscan 에서 사용자가 직접 하도록 절차만 적는다.

적어 둔 주소는 **공개 RPC 로 직접 확인**했다 (ran-here, 검사 38개 통과).
`eth_getCode` 로 컨트랙트 존재, `eth_call` 로 상호 대조(예: 금고의 `asset()` 이 그 토큰을 가리키는지),
EIP-1967 슬롯으로 프록시 여부까지 봤다. 그 과정에서 찾은 것:

- **sUSDe 의 온체인 `cooldownDuration()` 은 1일**이다. DeFiLlama 쪽 설명의 "7일"과 다르다.
  설명서에는 온체인 값을 적고, 쿨다운은 **직접 읽어서 확인하라**고 안내했다.
- **Pendle 은 직접 호출 절차를 적지 않았다.** PT 를 만기 전에 파는 길은 "승인 → 스왑" 2단계인데,
  Etherscan 으로 한 칸씩 하면 그 사이에 가격이 움직인다(프런트런). 만기 후 `redeemPY` 만 안내했다.
  근거: 잔액 0 계정으로 `redeemPY` 를 호출해 0 이 반환되는 것까지 확인했다.
- **GMX V2 는 주소를 아예 싣지 않았다.** 출금이 "주문을 넣고 keeper 가 체결"하는 구조라
  Etherscan 한 번의 호출로 끝나지 않는다. 왜 안 되는지를 설명하는 페이지로 뒀다.

### 만든·고친 파일

- 새로: `scripts/lib/defi-pools.mjs`, `scripts/lib/frontend-health.mjs`,
  `src/pools-page.js`, `src/pools.css`, `pools/index.html`, `pools/*/escape.html` 8개,
  `tests/{defi-pools,frontend-health,pools-pages}.test.js`,
  `data/{defi-pools,defi-pools-history,frontend-health}.json`
- 고침: `scripts/fetch-data.mjs`(수집 작업 3개 추가), `src/app.js`(검색·"더보기"에 풀 추적,
  그리고 하위 폴더 페이지가 맞는 경로를 쓰도록 `data-root`), 공용 HTML 13개 상단 메뉴 한 줄,
  `.github/workflows/fetch-data.yml`(6시간 주기), `data/README.md`
- `src/app.js` 의 경로 처리: 전에는 "계산기면 `../`" 였다. `pools/aave-v3/escape.html` 은 두 단계
  아래라서 그 규칙으로는 깨진다. `<body data-root="...">` 를 읽게 바꿨다 (기존 페이지는 그대로 동작).

### 색 규칙 (§5 "색을 새로 쓸 때")

`src/pools.css` 에 **고정색이 한 개도 없다.** 전부 `--pagu-*` 에서 파생시켰고,
`rgba(255,255,255,...)` 같은 반투명 흰색도 쓰지 않았다. 테스트가 이것을 검사한다
(`tests/pools-pages.test.js`). 2026-10-13 에 계산기 5개가 이 문제로 깨진 것을 되풀이하지 않기 위함이다.

### 남은 것

- 텔레그램 알림은 자리만 만들어 뒀다 (`isAlerting`). 보낼 곳(봇 토큰·채팅 ID)은 Actions 비밀값이 필요하다.
- 탈출 설명서는 8개 프로토콜 기준이다. 추적 풀을 늘리면 `WATCHED_PROJECTS` 의 `escape` 가
  가리키는 페이지도 같이 만들어야 한다 (테스트가 빠진 페이지를 잡아낸다).
