# PDF 딸깍 — 패치 노트

이 도구의 버전별 변경 이력입니다. 최신 버전이 위에 옵니다.

> **표기 규칙**
> - 🆕 **Added** — 신규 기능
> - 🔧 **Changed** — 기능 변경·개선
> - 🐛 **Fixed** — 버그 수정
> - 🗑 **Removed** — 제거된 기능
> - 📚 **Documentation** — 문서·설계 변경
>
> **마일스톤 정책** (2026-05-23 사용자 확정): Phase 1~5 까지는 **내부 개발 버전 (v0.1.0 ~ v0.4.0)** 으로 진행, 랜딩 카드 비공개. **Phase 5 완료 시점에 일괄 v1.0.0 정식 출시 + 랜딩 카드 활성**.

---

## 🚧 v2.0.0-dev5.3 — extract-text 다국어 자동 감지 (원본/오버레이 폐기) (2026-05-25)

> v2.0.0-dev5.2 사용자 피드백: *"KR, US는 잘됨. 원본 오버레이 말고 언어 감지로 가자. 이건 자동인가? 영어 한글 말고 일본어 독일어 이런거 나와도?"* → fontName 휴리스틱 완전 폐기 + 다국어 확장 + PDF 분석 후 감지 언어 안내.

### 🗑 Removed

- 원본/오버레이 (자동) 모드 — fontName 휴리스틱 폐기 (페이지마다 분류 뒤집힘 문제 + 같은 폰트로 덮어쓴 케이스 분리 X 한계)
- core.js 의 fontName 통계 로직 제거

### 🆕 Added

- **언어 감지 4종 + 묶음** (모드 5개):
  - 🇰🇷 **한글** — `[가-힯]` (자음·모음·완성형)
  - 🇺🇸 **영어/라틴** — `[a-zA-ZÀ-ɏ]` (영어·독일어·프랑스어·스페인어 등 라틴 기반)
  - 🇯🇵 **일본어** — `[ぁ-ゟ゠-ヿ]` (히라가나/가타카나, 한자도 함께 분류)
  - 🇨🇳 **중국어** — `[一-鿿]` CJK 통합 한자 (일본어 detect 시 zh 자동 제외 — 한자 겹침 처리)
- `countLangChars(s)` / `detectLang(s)` — 단순 regex 기반, 가장 많은 언어 1개로 분류 (일본어 한자 겹침 처리 포함)
- `detectLanguages(core, sampleSize=10)` — panel mount 시 첫 10쪽 분석해 발견된 언어 set 반환
- **panel mount 시 자동 안내**: `🔍 감지된 언어 (첫 10쪽): 🇰🇷 한글 · 🇺🇸 영어/라틴` 같이 즉시 표시

### 🔧 panel.js

- 모드 옵션 격자 5개로 단순화 + 국기 아이콘
- 감지 결과 안내 박스 (`detectedNotice`) 추가 — 사용자가 어떤 언어가 있는지 즉시 파악
- 안내 박스 갱신: "원본/오버레이 휴리스틱" 설명 제거, 라틴 통합 안내 추가

### 라틴 베이스 (영어/독일어/프랑스어 등) 통합 안내

단순 regex 로는 라틴 알파벳을 영어·독일어·프랑스어 등으로 세분화 X. 모두 "영어/라틴" 한 모드로 묶음. 정확한 언어 구분 필요하면 별도 NLP 라이브러리 (예: franc) 필요 — 후속 검토.

### cache-bust

`?v=2.0.13 → 2.0.14` 일괄. 버전 배지 `v2.0.0-dev5.2 → v2.0.0-dev5.3`.

### 라이브 검증 시나리오

1. 한↔영 Marker 번역 PDF → 📝 텍스트 추출 → mount 즉시 `🔍 감지된 언어: 🇰🇷 한글 · 🇺🇸 영어/라틴` 표시
2. 🇰🇷 한글 / 🇺🇸 영어/라틴 → 깔끔히 분리
3. 일본어 PDF → `🔍 감지된 언어: 🇯🇵 일본어` 안내 + 🇯🇵 일본어 모드 = 일본어만
4. 중국어 PDF → 🇨🇳 중국어 모드
5. 독일어 PDF → 영어/라틴 모드 (단순 regex 한계)

---

## 🚧 v2.0.0-dev5.2 — extract-text 분리 정확도 개선 (전 문서 통계 + 언어 감지) (2026-05-25)

> v2.0.0-dev5.1 검증 피드백: 사용자가 Marker 번역 PDF 의 원본/오버레이 추출 결과 공유 — *"깔끔하게 원본(영어)이나 오버레이(한글)만 추출되지 않고 일부가 묻어나옴"*. 결과 분석 결과 **fontName 통계가 페이지별** 로 동작해 페이지마다 분류가 뒤집힘 (1쪽 한글이 많으면 한글 = 원본, 2쪽 영어가 많으면 영어 = 원본).

### 🐛 Fixed — 전 문서 fontName 통계

- `core.js` — `original/overlay` 모드일 때 **모든 페이지의 items 를 합산** 해 fontName 통계 1회 → 가장 많이 사용된 fontName 1개 = 원본 (전 문서 일관)
- 페이지마다 원본/오버레이 분류 뒤집히던 버그 해결

### 🆕 Added — 언어 감지 모드 (Marker 한↔영 직접 매칭)

- `core.js` — `detectLang(s)` helper (한글 vs 영어 글자 수 비교)
- 모드 2개 신규:
  - **🇰🇷 한글만**: 한글이 영어보다 많은 items
  - **🇺🇸 영어만**: 영어가 한글보다 많은 items
- fontName 휴리스틱이 안 통하는 케이스 (원본·번역이 같은 폰트 사용) 도 정확

### 🔧 panel.js

- 모드 옵션 격자 5개로 확장: 묶음 / 원본 (자동) / 오버레이 (자동) / 🇰🇷 한글만 / 🇺🇸 영어만
- 안내 박스: 자동 휴리스틱 한계 + 언어 모드의 정확성 명시

### cache-bust

`?v=2.0.12 → 2.0.13` 일괄. 버전 배지 `v2.0.0-dev5.1 → v2.0.0-dev5.2`.

### 라이브 검증 시나리오

1. Marker 번역 PDF (한글) → 📝 텍스트 추출
2. **🇰🇷 한글만**: 한글 번역만 깔끔히 .txt (영어 묻어남 X)
3. **🇺🇸 영어만**: 영어 원본만 깔끔히 .txt (한글 묻어남 X)
4. **원본 (자동) / 오버레이 (자동)**: 전 문서 통계로 페이지 일관 — 1쪽도 정상 (이전엔 1쪽 거꾸로)

---

## 🚧 v2.0.0-dev5.1 — unlock 라이브 적용 + extract-text 원본/오버레이 분리 (2026-05-25)

> v2.0.0-dev5 검증 피드백 2건:
> 🔧 *"잠금풀기를 적용하면 그냥 풀린파일 다운로드 받아버리고 마는데 라이브로 적용되면 좋겠어. 그래야 바로 수정이 가능하게"*
> 🆕 *"텍스트 추출하면 AI 번역으로 오버레이된 경우 원본과 오버레이가 같이 묶여서 나오네... 원본, 오버레이, 묶음 이런식으로 선택해서 추출가능하게"*

### 🔧 unlock 라이브 적용 (결과 반환 → 즉시 적용)

- **`tools/unlock/panel.js`** — `downloadPDF` 호출 제거, `api.applyToPdf(bytes, '잠금 풀기')` 로 교체
- 잠금 풀린 PDF 가 currentCore 로 누적 → 회전·삭제·워터마크 등 후속 도구 즉시 적용 가능
- 메인 ↶ Undo 로 원본 복귀, 💾 다운로드로 모든 누적 결과 한 번 저장
- 안내 문구 갱신: "결과는 별도 다운로드" → "viewer 에 라이브 반영"
- 카테고리 표 (dev.md §14.4) 도 unlock 을 "즉시 적용" 으로 분류 변경

### 🆕 extract-text 원본/오버레이 분리 (fontName 휴리스틱)

- **`tools/extract-text/core.js`** — `extractText(core, pages, { mode }, onProgress)` 시그니처 추가
  - **휴리스틱**: 페이지 안 fontName 별 글자수 통계 → 가장 많이 사용된 fontName = "원본" 추정, 나머지 = "오버레이"
  - 모드 3개: `'all'` (현재 동작) · `'original'` (원본만) · `'overlay'` (오버레이만)
  - 하위 호환: `extractText(core, pages, onProgress)` 도 작동 (typeof opts === 'function' 검사)
- **`tools/extract-text/panel.js`** — "추출 대상" 옵션 격자 추가 (📝 묶음 / 📄 원본만 / 🎨 오버레이만)
  - 파일명 suffix 도 모드별: `텍스트` / `텍스트-원본` / `텍스트-오버레이`
  - 안내 박스에 휴리스틱 한계 명시 (원본/번역 폰트가 같으면 분리 X)
- **단독 페이지 `extract-text.js`** — 호환만 (opts 미전달 → 'all' 기본). 후속 PR 에서 단독 UI 통일 검토

### cache-bust

`?v=2.0.11 → 2.0.12` 일괄. 버전 배지 `v2.0.0-dev5 → v2.0.0-dev5.1`.

### inline 도구 카테고리 갱신 (dev.md §14.4)

| 카테고리 | 도구 |
|---|---|
| page 즉시 적용 | 🔄 회전 · 🗑️ 삭제 · ↔️ 재정렬 · 📐 크기조정 |
| page 결과 반환 | 📤 추출 · ✂️ 분할 |
| extract 결과 반환 | 📝 텍스트 추출 · 🖼️ 페이지→이미지 |
| misc **즉시 적용** | 🔓 **잠금 풀기** (NEW — currentCore 갱신) |

### 라이브 검증 시나리오

1. **🔓 잠금 풀기 라이브**: 권한 제한 PDF 드롭 → 잠금 풀기 실행 → viewer PDF 가 풀린 결과로 자동 갱신 → 회전·삭제 등 후속 도구 적용 가능 → ↶ Undo 로 잠금 PDF 복귀 → 💾 다운로드
2. **📝 텍스트 추출 모드**: Marker 번역 PDF → 추출 대상 "원본만" = 원본 영문만 .txt / "오버레이만" = 번역 한글만 / "묶음" = 둘 다 섞임 (기존 동작)
3. 단독 페이지 회귀 X (`/pdftools/tools/unlock/`, `/pdftools/tools/extract-text/`)

---

## 🚧 v2.0.0-dev5 — 결과 반환 도구 3개 panel 통합 (extract-text · to-image · unlock) (2026-05-25)

> v2.0.0-dev4.2 검증 통과 → 결과 반환 카테고리 3개 통합. inline 도구 6 → 9개. 페이지 작업 6 + 결과 반환 3 (텍스트 추출, 페이지→이미지, 잠금 풀기).

### 🆕 신규 panel 모듈

- **tools/extract-text/core.js** — `extractText(core, pages, onProgress) → [{page, text}]`
- **tools/extract-text/panel.js** — 좌측 선택 페이지 활용 + 형식 옵션 (단일 .txt / 페이지별 zip) → 별도 다운로드
- **tools/to-image/core.js** — `renderPagesToImages(core, pages, { dpi, format, quality }, onProgress) → [{page, blob}]` + `safeName` 유틸
- **tools/to-image/panel.js** — 형식 (PNG/JPG) + DPI 격자 (72/150/200/300) + 좌측 선택 → 단일 이미지 또는 zip
- **tools/unlock/core.js** — `unlockPDF(bytes, fileName, onProgress)` — 백엔드 qpdf-wasm cascade + 클라이언트 fallback 추출
- **tools/unlock/panel.js** — 단일 액션 ("잠금 풀기 실행") — currentCore.bytes 를 API 로 → 잠금 풀린 PDF 다운로드

### 🔧 단독 페이지 리팩토링 (core.js 공유 import)

- **extract-text.js** — `getPageText` 가 core 의 `extractText` 단일 페이지 호출, 추출 본문은 `extractText` 일괄 호출
- **to-image.js** — `renderPagesToImages` 호출로 변환 본문 + 내부 `renderPageToBlob` 제거 (core 로 이동). `safeName` 도 core 에서 import
- **unlock.js** — cascade 로직 전체 core 의 `unlockPDF` 로 위임 (단독 페이지는 file → bytes → core 호출만)

### 🔧 viewer 갱신

- `viewer.js` — PANEL_MODULES 에 3개 추가 (총 9개 inline)
- `index.html` — 사이드바 도구 링크 3개 (extract-text · to-image · unlock) `target=_blank` 제거 + inline 마크 + 안내 갱신 ("9개 도구 inline 작동")

### inline 도구 현황 (v2.0.0-dev5 시점)

| 카테고리 | 도구 | 동작 |
|---|---|---|
| page (즉시 적용) | 🔄 회전·반전 · 🗑️ 삭제 · ↔️ 재정렬 · 📐 크기조정 | applyToPdf 누적 + Undo |
| page (결과 반환) | 📤 추출 · ✂️ 분할 | 별도 다운로드 |
| extract (결과 반환) | 📝 **텍스트 추출** · 🖼️ **페이지→이미지** | .txt/zip · png/jpg/zip |
| misc (결과 반환) | 🔓 **잠금 풀기** | 백엔드 qpdf 우선 + 클라 fallback |

### cache-bust

`?v=2.0.10 → 2.0.11` 일괄. 버전 배지 `v2.0.0-dev4.2 → v2.0.0-dev5`.

### 라이브 검증 시나리오

1. PDF 드롭 → 사이드바 **9개 inline 마크** 확인
2. **📝 텍스트 추출**: 좌측 페이지 선택 (또는 전체) → 형식 선택 → 추출 = .txt 또는 zip 다운로드
3. **🖼️ 페이지→이미지**: PNG 200dpi 기본 → 이미지 변환 = 1쪽=단일 / 여러쪽=zip
4. **🔓 잠금 풀기**: 권한 제한 PDF 열고 → 잠금 풀기 실행 = 풀린 PDF 다운로드
5. 단독 페이지 (`/pdftools/tools/{extract-text,to-image,unlock}/`) 회귀 X

### 🚀 다음 PR (v2.0.0-dev6)

- AI 도구 (📄 PDF→MD · ✨ 요약 · 🌐 번역) — Marker/Gemini/DeepL API. shared/api-keys 활용
- 또는 특수 도구 (compress-images · compare · from-image · merge)
- WYSIWYG (워터마크·페이지번호·서명·텍스트추가·자르기) — `api.requestPageOverlay` 인프라 신규

---

## 🚧 v2.0.0-dev4.2 — 인쇄 버튼 추가 (메인 toolbar) (2026-05-25)

> 사용자: *"명색이 pdf 뷰어인데 인쇄가 되어야 하지 않겠어? 위의 툴바에 인쇄버튼 추가해줘"*. 합당. 메인 toolbar 에 🖨️ 인쇄 버튼 추가. 누적 결과 PDF (currentCore) 가 hidden iframe 으로 로드되어 브라우저 인쇄 다이얼로그 호출.

### 🆕 Added

- **메인 toolbar 🖨️ 인쇄 버튼** (`viewer.html`) — `↶ Undo`/`↷ Redo` 와 `💾 다운로드` 사이. title "현재 누적 결과 인쇄 (Ctrl+P)"
- **`handlePrint()`** (`viewer.js`) —
  - `currentCore.bytes` → Blob → ObjectURL
  - hidden iframe (`#viewerPrintIframe`, fixed/0×0) 에 PDF 로드
  - `iframe.contentWindow.print()` 호출 → 브라우저 인쇄 다이얼로그
  - 실패 시 `window.open(url, '_blank')` fallback (PDF 인쇄 가능한 새 탭)
  - 60초 후 ObjectURL revoke + iframe 제거 (cleanup)
- btnPrint enable/disable: `openInitial` 시 enable, `reset()` 시 disable

### cache-bust

`?v=2.0.9 → 2.0.10` 일괄. 버전 배지 `v2.0.0-dev4.1 → v2.0.0-dev4.2`.

### 라이브 검증 시나리오

1. PDF 드롭 → 메인 toolbar 의 🖨️ 인쇄 버튼 활성
2. 회전·삭제 등 변경 적용 → 🖨️ 인쇄 클릭 = 브라우저 인쇄 다이얼로그가 **누적 결과** 로 띄움
3. 인쇄 다이얼로그에서 프린터 선택 or PDF 로 저장
4. 다른 PDF 열기 → 🖨️ 인쇄 disabled → 새 PDF 로딩 후 재활성

---

## 🚧 v2.0.0-dev4.1 — resize 개선 3건 (활성 강조 · anchor 9방향 · 선택 페이지) (2026-05-25)

> v2.0.0-dev4 검증 피드백: ✅ 재정렬 잘됨 / 🔧 **활성 옵션 강조 없음** + **'원본' 9방향 위치 패널** + **선택 페이지만 처리** 요청.

### 🐛 Fixed

- **활성 옵션 강조 누락** — `.panel-option-btn.active` CSS 가 정의되어 있지 않아 선택 표시 X. JS 는 active 클래스 추가 하지만 스타일 없음
  - 수정: `viewer.css` 에 `.panel-option-btn.active` 추가 (rgba(74,158,255,0.28) bg + 1.5px 진한 파란 border + inset shadow)

### 🆕 Added

- **shared/viewer-panel.js — `createAnchorGrid(initial, onChange)`** — 9방향 anchor 격자 helper (좌상/상/우상/좌/중앙/우/좌하/하/우하). 워터마크 위치·resize keep 모드 anchor 등 공용
  - 중앙은 동그라미 dot, 나머지는 화살표 (↖↑↗←→↙↓↘)
  - `viewer.css` 에 `.panel-anchor-grid` / `.panel-anchor-btn` 스타일
- **resize core.js — anchor 9방향 지원**
  - `getAnchorXY(anchor, targetW, targetH, contentW, contentH)` — PDF 좌표계 (좌하 원점) 으로 변환
  - keep 모드일 때 `drawPage` 의 x/y 가 anchor 에 따라 결정
- **resize core.js — pages 인자 (선택 페이지만 처리)**
  - `resizePages(core, { pageSize, orient, scaleMode, anchor, pages }, onProgress)` 시그니처 변경 (opts 객체)
  - pages = null/undefined → 전체. pages = [1,3,5] → 그 페이지만 새 사이즈, 나머지는 원본 그대로 (copyPage)
- **resize panel.js — keep 모드 시 9방향 위치 패널 노출**
  - scaleMode === 'keep' 일 때만 `anchorGroup` 표시 (다른 모드면 hide)
  - 좌측 페이지 선택이 있으면 그 페이지만, 없으면 전체
  - 안내 박스: `▶ 3쪽 선택 → A4 세로 (210 × 297 mm) — 원본 크기 유지 · 위치 좌상`

### 🔧 Changed

- **resize.js (단독 페이지)** — 새 opts 객체 시그니처 호환만 (UI 변경 X, 후속 PR 에서 anchor/페이지 선택 UI 추가 검토)

### cache-bust

`?v=2.0.8 → 2.0.9` 일괄. 버전 배지 `v2.0.0-dev4 → v2.0.0-dev4.1`.

### 라이브 검증 시나리오

1. PDF 드롭 → 📐 크기 조정 → 사이즈 격자: **A4 가 진한 파란 배경 + 굵은 border** 로 활성 표시
2. 콘텐츠 처리 → **"원본" 클릭** → 9방향 위치 격자 노출 → 좌상/중앙/우하 등 선택 → 적용
3. 좌측 썸네일 2-3쪽 체크 → 안내 박스 `▶ 3쪽 선택 → A4 세로 ...` → 적용 = 선택 페이지만 새 사이즈, 나머지는 원본 그대로
4. 선택 0개 + 적용 = 전체 페이지 처리 (기존 동작)
5. 회귀 X: 단독 페이지 `/pdftools/tools/resize/` 그대로 작동

---

## 🚧 v2.0.0-dev4 — 페이지 작업 panel 2개 추가 (reorder · resize, 6/7개 완료) (2026-05-25)

> v2.0.0-dev3.2 검증 통과 → 페이지 작업 카테고리 남은 3개 중 **reorder + resize** 통합. crop 은 WYSIWYG 인프라 (page overlay) 가 필요해 Step 5c (워터마크 등 WYSIWYG 도구) 와 같이 처리. 페이지 작업 6/7 inline 완료.

### 🆕 신규 panel 모듈

- **tools/reorder/core.js** — `reorderPages(core, newOrder, onProgress)`
- **tools/reorder/panel.js** — `thumbnailMode: 'drag'` 신규 + `ctx.api.onThumbnailReorder(cb)` 사용. 좌측 썸네일 직접 드래그 = 즉시 applyToPdf 누적
- **tools/resize/core.js** — `resizePages(core, pageSize, orient, scaleMode, onProgress)` + `SIZES` / `SCALE_MODES` / `getTargetSize` export
- **tools/resize/panel.js** — 옵션 격자 3개 (사이즈 6종 · 방향 세로/가로 · 콘텐츠 처리 fit/stretch/keep) + 적용 버튼 → applyToPdf

### 🔧 viewer 인프라 확장

- **viewer.js** — `thumbnailMode: 'drag' | 'select'` (기본 select) 지원
  - `renderLeftThumbs()` = 상위 라우터, mountedPanel 의 mode 보고 분기
  - `renderLeftThumbsSelectable()` (기존 동작) + `renderLeftThumbsDraggable()` (Sortable.js + onReorder)
  - `api.onThumbnailReorder(cb)` 신규 — reorder panel 이 등록한 콜백을 thumbnail 의 onReorder 에 연결
  - `mountToolPanel()` — `mod.thumbnailMode === 'drag'` 면 좌측을 draggable 모드로 재구성
  - `unmountToolPanel()` — drag 모드였으면 selectable 로 복원
  - `loadFromBytes()` — 활성 panel mode 자동 유지 (`renderLeftThumbs` 가 라우팅)
- **index.html** — Sortable.js CDN 정적 로드 (`sortablejs@1.15.2`)
- **단독 도구 페이지** (reorder.js, resize.js) — core.js 호출로 단순화 (변환 본문 제거)

### 도구별 동작 (v2.0.0-dev4 시점, page 카테고리)

| 도구 | 카테고리 | inline | 동작 |
|---|---|---|---|
| 🔄 회전·반전 | 즉시 적용 | ✅ | 옵션 = 즉시 회전 + applyToPdf |
| 🗑️ 페이지 삭제 | 즉시 적용 | ✅ | 선택 + 삭제 클릭 = applyToPdf |
| ↔️ **페이지 재정렬** | 즉시 적용 (drag) | ✅ NEW | 좌측 썸네일 드래그 = 즉시 applyToPdf |
| 📐 **페이지 크기 조정** | 즉시 적용 | ✅ NEW | 사이즈·방향·스케일 선택 + 적용 = applyToPdf |
| 📤 페이지 추출 | 결과 반환 | ✅ | 별도 PDF 다운로드 |
| ✂️ 분할 | 결과 반환 | ✅ | 모드별 zip 다운로드 |
| ✂️ 자르기 | (WYSIWYG) | ⏭ | Step 5c 와 함께 처리 (페이지 위 드래그 영역) |

### cache-bust

`?v=2.0.7 → 2.0.8` 일괄. 버전 배지 `v2.0.0-dev3.2 → v2.0.0-dev4`. Sortable CDN script 신규 추가.

### 🚀 다음 PR

- **v2.0.0-dev5**: 결과 반환 도구 (extract-text · to-image · to-markdown · summary · translate · compare · unlock 등 7개 — `currentCore` 누적 X)
- **v2.0.0-dev6**: WYSIWYG 6+1개 (watermark-text/image · page-number · header-footer · add-text · signature · crop) — 인프라 신규 (`api.requestPageOverlay` 등)
- **v2.0.0 출시**: PATCHNOTE / dev.md / CLAUDE.md / README

### 라이브 검증 시나리오

1. PDF 드롭 → 사이드바 6개 inline 마크 확인 (회전·삭제·재정렬·크기조정·추출·분할)
2. **↔️ 재정렬**: 좌측 썸네일 직접 드래그·드롭 → 즉시 좌·중 반영, ↶ Undo 가능
3. **📐 크기 조정**: A4 + 가로 + fit → 적용 = 모든 페이지가 A4 가로 (여백 추가). 다른 사이즈 또 적용 = 누적
4. 회귀 X: 단독 페이지 (`/pdftools/tools/{reorder,resize}/`) 모두 그대로 작동

---

## 🚧 v2.0.0-dev3.2 — split panel 2개 fix (JSZip 미로드 · group input 숨김) (2026-05-25)

> v2.0.0-dev3.1 검증 피드백 (사용자, viewer panel 한정): **JSZip 미로드 오류** + **N쪽씩 모드 input 입력 안됨** (실은 input 자체가 안 보임). 단독 페이지는 모두 정상.

### 🐛 Fixed

- **JSZip 미로드**: viewer panel 에서 분할 시 `downloadZip` 호출 → "JSZip 가 로드되지 않음" 오류
  - 원인: 단독 페이지 `tools/split/index.html` 은 `<script src=".../jszip.min.js">` 정적 로드. viewer panel 환경 (`tools/viewer/index.html`) 에는 JSZip script 없음
  - 수정: `split/panel.js` 가 zip 다운로드 직전 `loadJSZip()` 호출 (이미 있던 `shared/jszip-lazy.js` 활용 — 한 번만 로드, 캐시)
- **N쪽씩 모드 input 숨김**: 사용자가 "숫자 입력 안됨" 으로 보고 — 실은 input UI 자체가 안 보임
  - 원인: `groupRow.style.display = 'none'` 인라인 스타일 영구 적용. `updateGroupRow()` 가 wrap (groupGroup) 의 display 만 toggle 하므로 자식 groupRow 의 display:none 우선 → 안 보임
  - 수정: `groupRow` 의 인라인 `style.display = 'none'` 제거 (wrap toggle 만으로 충분)

### cache-bust

`?v=2.0.5 → 2.0.7` + `?v=2.0.6 → 2.0.7` 일괄 (split 관련 + viewer). 버전 배지 `v2.0.0-dev3 → v2.0.0-dev3.2`.

---

## 🚧 v2.0.0-dev3.1 — 분할 모드 재정의 ('range' 폐기 → '분할점' 추가) (2026-05-25)

> v2.0.0-dev3 검증 피드백: 사용자 *"분할에서 선택페이지를 분할하는건 추출과 같은 기능. 분할이니 분할점을 지정해서 n개로 자르는 기능으로 바꾸는게 좋을듯"* — 합당한 지적. 분할의 본질에 맞춰 모드 교체. **단독 도구 페이지에도 일괄 적용**.

### 🔧 Changed

- **tools/split/core.js** — `splitRange` 폐기 → `splitByPoints(core, splitPoints, onProgress)` 신규
  - **알고리즘**: 분할점 = "해당 페이지 끝에서 자르기". 예: 100쪽 + `[25, 74]` → `1-25` · `26-74` · `75-100` (3개)
  - 양 끝 자동 제외 (1 이상, 마지막 페이지 미만만 유효)
  - 정렬·중복 제거 후 ranges 계산
- **tools/split/panel.js** — viewer panel 모드 라벨 `선택 페이지` → `분할점` (✂️ 아이콘)
  - 좌측 썸네일 체크 = 분할점 (페이지 N 체크 = N 끝에서 자르기)
  - 안내 미리보기: `▶ 3개 파일: 1-25 · 26-74 · 75-100`
- **tools/split/split.js (단독 페이지)** — 같은 패러다임 적용
  - 변수 `selectedRange` → `splitPoints`, `pageRangeCtrl` → `pointsCtrl`
  - 모드 라디오 `range` → `points`, 라벨 "지정한 범위만" → "분할점 지정 (예: 25, 74)"
  - page-range.js 그대로 활용 (콤마 입력 / range 확장 모두 지원)
- **tools/split/index.html** — 모드 라디오 라벨 + `rangeWrap` → `pointsWrap` + 안내 박스 추가 ("📌 분할점으로 사용할 페이지를 콤마로 입력")
- **cache-bust** — split 관련 모듈 `?v=2.0.5 → 2.0.6`, viewer 의 split panel import 도 갱신. 단독 페이지 `split.js?v=0.5.12 → 2.0.6`

### 도구 분리 명확화

| 도구 | 입력 | 출력 |
|---|---|---|
| 📤 **페이지 추출** | 선택한 페이지들 | 그 페이지만 모은 1개 PDF |
| ✂️ **분할 — 분할점** | 분할점 페이지들 | 분할점으로 나뉜 N+1개 PDF (zip) |
| ✂️ **분할 — 각 1장씩** | (없음) | 각 페이지 1개씩 → N개 zip |
| ✂️ **분할 — N쪽씩** | 묶음 크기 | N쪽씩 묶음 → 여러 zip |

### 라이브 검증 시나리오

1. **panel**: PDF 드롭 → ✂️ 분할 → 모드 "분할점" 선택 → 좌측에서 25, 74쪽 체크 → 안내 박스에 `▶ 3개 파일: 1-25 · 26-74 · 75-끝` 표시 → 분할 시작 → 3개 zip
2. **단독**: `/pdftools/tools/split/` → 모드 "분할점 지정" → 입력 "25, 74" → 결과 미리보기 같이 → 분할 시작

---

## 🚧 v2.0.0-dev3 — 페이지 작업 panel 3개 inline 통합 (2026-05-25)

> v2.0.0-dev2.3 검증 통과 (사용자 "OK 잘됨") → 같은 라이브 적용 패러다임으로 페이지 작업 도구 일괄 통합 시작. **3-column + 라이브 적용 + Undo + TextLayer 인프라 그대로 활용**, 도구별 core.js + panel.js 추가만으로 점진 통합. PATCHNOTE.md dev.md §14 단일 진실 출처 그대로.

### 🆕 Added

- **shared/viewer-panel.js** — `createActionButton({ label, icon, enabled, danger, onClick })` 추가. 단일 액션 버튼 (즉시 삭제·실행·다운로드 등). danger=true 면 빨간 강조 (삭제 등 destructive 액션)
- **tools/extract-pages/core.js** — `extractPages(core, pages, onProgress) → Uint8Array`
- **tools/extract-pages/panel.js** — viewer 결과 반환 panel (currentCore 누적 X, 별도 다운로드)
- **tools/split/core.js** — 3개 모드 함수: `splitEach` / `splitRange` / `splitGroup`
- **tools/split/panel.js** — 모드 격자 (각 1장씩 / 선택 페이지 / N쪽씩) + group size 입력 + 결과 zip 다운로드
- **tools/remove-pages/panel.js** — 라이브 적용 panel (선택 페이지 삭제 → applyToPdf 누적)

### 🔧 Changed

- **tools/viewer/viewer.js** — `PANEL_MODULES` 에 3개 추가 (rotate · remove-pages · extract-pages · split)
- **tools/viewer/index.html** — 사이드바 도구 링크 4개에 `inline` 마크 + target=_blank 제거 (rotate · remove-pages · extract-pages · split), 안내 박스 갱신
- **tools/viewer/viewer.css** — `.panel-action-btn` 큰 단일 액션 버튼 + `.panel-action-danger` 빨간 변형 추가
- **tools/remove-pages/remove-pages.js** — `removePages` core 함수 호출로 단순화 (변환 본문 → 1줄)
- **tools/extract-pages/extract-pages.js** — `extractPages` core 함수 호출
- **tools/split/split.js** — `splitEach/splitRange/splitGroup` core 함수 호출

### 도구별 동작 모델 (dev.md §14.4)

| 도구 | 카테고리 | viewer 동작 |
|---|---|---|
| 🔄 회전·반전 | 즉시 적용 | 옵션 클릭 = 즉시 currentCore 누적, ↶ Undo 가능, 💾 메인 다운로드 |
| 🗑️ 페이지 삭제 | 즉시 적용 | 좌측 선택 + 삭제 클릭 = 즉시 누적, ↶ Undo 가능 |
| 📤 페이지 추출 | 결과 반환 | 좌측 선택 + 추출 클릭 = **별도 PDF 즉시 다운로드** (원본 그대로) |
| ✂️ 분할 | 결과 반환 | 모드별 결과 즉시 다운로드 (1개 PDF 또는 zip), currentCore 영향 X |

### 🚀 다음 PR (v2.0.0-dev4)

페이지 작업 남은 3개: reorder (Sortable.js 드래그) · resize (페이지 크기) · crop (WYSIWYG 드래그 영역). reorder 는 좌측 썸네일 직접 드래그라 viewer 인프라 수정 필요 (api 신규 함수 `reorderPages(newOrder)`).

### 라이브 검증 시나리오

1. PDF 드롭 → 좌측 썸네일 / 중앙 뷰어 / 우측 도구창 4개 inline 마크 확인
2. **🗑️ 페이지 삭제**: 좌측 2-3쪽 체크 → 우측 "선택 페이지 삭제" 클릭 → 즉시 좌·중 반영 + ↶ Undo 활성. 💾 다운로드로 최종 결과 (-편집됨.pdf)
3. **📤 페이지 추출**: 좌측 1, 5쪽 체크 → "선택 페이지 추출" → 별도 PDF 다운로드 (viewer 의 메인은 그대로)
4. **✂️ 분할**: 모드 "각 1장씩" → 분할 시작 → N개 zip / 모드 "N쪽씩" + 입력 3 → 묶음 zip
5. **회귀 X**: 단독 페이지 (`/pdftools/tools/remove-pages/`, `/extract-pages/`, `/split/`) 모두 그대로 작동 (core.js 공유)

### cache-bust

`?v=2.0.4 → 2.0.5` 일괄. 버전 배지 `v2.0.0-dev2.3 → v2.0.0-dev3`.

---

## 🚧 v2.0.0-dev2.3 — hotfix3 (썸네일 토글 잠김 근본 원인 fix: listener 누적) (2026-05-25)

> v2.0.0-dev2.2 라이브 재검증 + 사용자 콘솔 진단 결과: DOM 과 내부 Set 일치 (1,1). 즉 hotfix1 의 initialSelected fix 가 작동했지만 토글이 여전히 잠김 + 다른 PDF 열어도 잠긴 상태 유지 → **다른 근본 원인 발견: 이벤트 리스너 누적**.

### 🐛 근본 원인

`shared/thumbnail.js` 가 매 호출마다 `container.addEventListener('click', ...)` / `('change', ...)` 익명 함수로 등록. viewer 의 `thumbsContainer` 는 같은 DOM element 라 회전 적용 후 새 thumbCtrl 생성 시 listener 가 누적:

- 회전 1번 → listener 2개 → click 시 토글 2번 발생 → 원상복귀 = **"잠김"**
- 회전 2번 → listener 3개 → 토글 3번 = 1번 토글 효과 = **"풀림"**
- 회전 3번 → listener 4개 → 토글 4번 = 잠김 ... 반복

→ 사용자 보고 패턴 "회전 한 번 더 해야 풀림" 과 정확히 일치. "다른 PDF 열어도 잠긴 상태" = listener 가 같은 DOM 에 남아있어서.

### 🔧 Fix

- **`shared/thumbnail.js`** — click/change handler 를 named function 으로 분리 (`changeHandler`, `clickHandler`). Sortable instance 도 변수 저장. **`destroy()` 메서드 추가** — listener 제거 + IntersectionObserver disconnect + Sortable destroy + class 정리
- **`tools/viewer/viewer.js`** —
  - `renderLeftThumbs` 시작에 `leftThumbCtrl?.destroy?.()` 호출
  - `reset()` 도 `leftThumbCtrl?.destroy?.()` 호출
- **backward-compatible** — 기존 단독 도구 페이지 (rotate, remove-pages 등) 는 `destroy()` 호출 안 함. 단독 도구는 한 번만 생성하고 같은 페이지 내 재생성 시 페이지 전체가 reset 되므로 listener 누적 영향 미미.

### cache-bust

`?v=2.0.3 → 2.0.4` 일괄. 버전 배지 `v2.0.0-dev2.2 → v2.0.0-dev2.3`.

---

## 🚧 v2.0.0-dev2.2 — hotfix2 (sticky 간격 제거 · 줌 라벨 중복 fix) (2026-05-25)

> v2.0.0-dev2.1 라이브 재검증 피드백 3건:
> 🔧 **툴바와 탑바 사이 간격 8px 어색** → 0 간격으로 / 🔧 **"너비 맞춤" 텍스트 중복** (zoomLabel + btnFitWidth) → 라벨 "자동" / ⚠ **썸네일 토글 여전히 잠김** → cache 의심 (강제 새로고침 요청 + 코드 변경 없음).

### 🔧 Changed

- **메인 toolbar sticky top** (`viewer.css`) — `calc(var(--tph) + 8px)` (52px, 8px 간격) → `var(--tph)` (44px, 0 간격). topbar 와 한 덩어리로 붙음
- **좌·우 sidebar sticky top** — `calc(var(--tph) + 78px)` → `calc(var(--tph) + 64px)` (메인 toolbar 의 sticky 가 0 간격으로 올라간 만큼)
- **zoomLabel 라벨 "자동"** (`viewer.js`) — `zoomScale == null` 일 때 "너비 맞춤" → "자동" (옆 별도 btnFitWidth 가 "너비 맞춤" 텍스트라 중복 방지)
- **btnFitWidth 활성 표시** (`viewer.css` + `viewer.js`) — `zoomScale == null` 일 때 `vbtn-active` 클래스 (파란 강조). 현재 모드가 어떤 줌인지 한눈에

### ⚠ 미해결 (사용자 재검증 필요)

- **썸네일 토글 잠김 여전히 발생** — v2.0.0-dev2.1 의 thumbnail.js `initialSelected` 옵션 추가 + viewer.js 외부 DOM 복원 제거 fix 가 적용됐는데 사용자 보고 "여전히 잠김". 가능성:
  1. **브라우저 캐시** — 사용자가 강제 새로고침 (Ctrl+Shift+R / Cmd+Shift+R) 안 했을 가능성 (가장 큰 의심). PR #237 의 ?v=2.0.2 가 viewer.html 자체 캐시면 import 가 옛 ?v=2.0.1 그대로 → 옛 thumbnail.js / viewer.js
  2. **다른 코드 버그** — 강제 새로고침 후에도 잠김이면 추가 디버깅 (개발자 도구 console 에서 thumbnail.js 의 selected Set 상태 확인 필요)
- **텍스트 선택 작동 여부 미보고** — v2.0.0-dev2.1 의 3단계 fallback (TextLayer class → renderTextLayer 함수 → manual placement) 작동 여부 명시 X. 사용자 확인 요청

### cache-bust

`?v=2.0.2 → 2.0.3` 일괄. 버전 배지 `v2.0.0-dev2.1 → v2.0.0-dev2.2`.

---

## 🚧 v2.0.0-dev2.1 — hotfix1 (텍스트 선택 fallback · 토글 잠김 fix · sticky 가림 fix) (2026-05-25)

> v2.0.0-dev2 라이브 검증 피드백 4건 중 3건 코드 수정 + 1건 답변:
> ✅ 3-column / ❌ **텍스트 선택 안됨** → fallback / 🐛 **썸네일 토글 잠김** → fix / ✅ 회전·Undo / ⏱ 회전 딜레이 (PDF 재인코딩 본질) / 🔧 **메인 toolbar sticky 헤더에 가려짐** → fix.

### 🐛 Fixed

- **🔧 sticky toolbar 가려짐** ([viewer.css](tools/viewer/viewer.css))
  - `.topbar` = `position: fixed; height: var(--tph)=44px; z-index: 100` 이라 sticky `top: 8px` 가 그 아래로 들어감
  - 메인 toolbar `top: calc(var(--tph) + 8px)` 로 변경, z-index 30 → 50, 배경 불투명도 ↑ (스크롤 시 뒤 콘텐츠 안 비침)
  - 좌·우 sidebar 도 `top: calc(var(--tph) + 78px)` (메인 toolbar 아래) + max-height 도 같은 식으로 조정
- **🔧 썸네일 토글 잠김 (회전 적용 후)** ([thumbnail.js](shared/thumbnail.js))
  - 원인: viewer 가 `loadFromBytes` 후 새 thumbCtrl 생성 → 이전 selectedPages 를 외부에서 DOM (`checkbox.checked = true`) 만 set → **내부 selected Set 은 비어있음** → 다음 click 토글이 Set 과 불일치
  - 수정: `renderThumbnails(container, core, { initialSelected: [1, 3, 5] })` 옵션 추가 → 내부 Set 초기화 + 렌더 시 `checked` 속성 + `.thumb-selected` 클래스 함께 set
  - viewer.js 의 외부 DOM 복원 코드 제거 → `initialSelected: validInitial` 옵션 사용
- **🔧 텍스트 선택 안됨 fallback** ([text-layer.js](shared/text-layer.js))
  - pdf.js v4.0.379 의 `pdf.min.mjs` ESM 빌드가 환경에 따라 `TextLayer` class 노출 안 할 수 있음
  - mountTextLayer 가 fallback 체인으로 3단계 시도:
    1. `window.pdfjsLib.TextLayer` class (pdf.js v4 공식)
    2. `window.pdfjsLib.renderTextLayer()` 함수 (v3·v4 호환 deprecated)
    3. **manual placement** — `getTextContent().items[]` 의 `transform` 행렬을 직접 적용해 span 절대 배치 (Util.transform 식 6-element affine 행렬 직접 구현)
  - 어떤 환경이든 텍스트 드래그 선택 + Ctrl+C 작동 보장. console.warn 으로 어떤 단계가 작동했는지 확인 가능

### 📚 회전 딜레이 (코드 변경 X)

사용자 질문: "회전 적용에 몇 초 딜레이 — 적용 버튼 방식이라도 동일?"

**답변: 동일함**. pdf-lib 가 PDF 를 `copyPages` (원본 보존) + `setRotation` + `save()` (full re-encode) 하므로 PDF 크기·페이지 수에 비례한 시간 소모. WYSIWYG vs 적용 버튼 차이는 X — 변환 자체가 무거움.

**향후 최적화 옵션** (별도 PR 후보):
- 회전만은 `src.setRotation(...)` 으로 **메타데이터만 변경 후 save** (copyPages 생략) — 90·180·270 만 가능, flip 은 X. 빠르지만 원본 mutate 위험
- 미리보기는 가벼운 canvas rotation 으로 즉시 표시, 실제 PDF 변환은 background — 복잡

### cache-bust

`viewer.css?v=2.0.1 → 2.0.2`, `viewer.js?v=2.0.1 → 2.0.2`, viewer.js 안 모든 shared import `?v=2.0.1 → 2.0.2` 일괄. 버전 배지 `v2.0.0-dev2 → v2.0.0-dev2.1`.

---

## 🚧 v2.0.0-dev2 — 3-column 패러다임 + 라이브 적용 + Undo + TextLayer (2026-05-25)

> **사용자 큰 패러다임 전환** (v2.0.0-dev1 직후 같은 날): 좌+우 2-column + viewMode 자동 전환 + 도구마다 다운로드 → **3-column (썸네일/뷰어/도구) + 옵션 즉시 라이브 적용 + 메인 다운로드 한 번 + Undo + 텍스트 선택**. dev.md §14 통째로 갱신 (단일 진실 출처). 즉 PDF 뷰어 + 편집기 통합 = Adobe Acrobat 패러다임.

### 🆕 Added

- **dev.md §14 (큰 갱신)** — 새 3-column 패러다임 단일 진실 출처. 사용자 결정 8개 (§14.1), 레이아웃 (§14.2), 라이브 적용+Undo 모델 (§14.3), 새 mountPanel 시그니처 (§14.4), 도구 분류 표 (즉시 적용/WYSIWYG/결과 반환), 작업 순서 (v2.0.0-dev2~dev6 → 출시)
- **shared/text-layer.js** — pdf.js TextLayer mount helper (canvas 위 텍스트 레이어 → 드래그 선택 + Ctrl+C native 복사)
- **shared/history-stack.js** — Undo/Redo 누적 stack (maxSize 20, label 보존, future/past 양방향)

### 🔧 Changed (큰 변경)

- **tools/viewer/viewer.js (전면 재작성)** — v2.0.0-dev1 의 viewMode 자동 전환 모델 폐기, 3-column + 라이브 적용 + Undo + TextLayer 모두 메인 뷰어 본체에 흡수
  - 상태: `currentCore` · `selectedPages` · `history` · `selectionSubscribers`
  - `openInitial(file)` — 최초 PDF 로딩 (history clear)
  - `loadFromBytes(bytes)` — bytes 로부터 currentCore 재로딩 + 좌·중 갱신 (Undo/Redo/applyToPdf 공통)
  - `renderLeftThumbs()` — 좌측 selectable 썸네일 (shared/thumbnail.js)
  - `renderCenterPages()` + `renderPage()` — 중앙 lazy 페이지 + `mountTextLayer` 호출
  - `mountToolPanel(toolId)` — 새 ctx 시그니처 (`getCore`/`getSelectedPages`/`container`/`api`)
  - api: `applyToPdf(bytes, label)` (history.push + 미리보기 갱신) · `onSelectionChange(cb)` · `onProgress(msg, pct)`
  - Ctrl+Z / Ctrl+Shift+Z 키보드 단축키 (입력 필드에서는 무시)
- **tools/viewer/index.html (전면 재작성)** — 3-column 마크업
  - 메인 toolbar (위 전체 가로): 파일 정보 · 줌 · ↶ Undo · ↷ Redo · 💾 다운로드 · 📂 다른 PDF
  - 3-column body: 좌측 썸네일 (220px) + 중앙 lazy (1fr) + 우측 도구창 (320px)
  - 좌측에 전체 선택 체크박스 + 선택 카운트
  - 우측 사이드바 도구 리스트 ↔ 패널 오버레이 swap (기존 구조)
- **tools/viewer/viewer.css (전면 재작성)** — 3-column 그리드, 메인 toolbar 스타일, `.viewer-col-left/center/right`, `.viewer-thumbs-left` (한 컬럼 override), TextLayer 스타일 (`.pdf-text-layer` + `::selection`)
- **tools/rotate/panel.js (재작성)** — 새 mountPanel 시그니처. 옵션 버튼 = 즉시 액션 (`applyRotation` → `api.applyToPdf(bytes, '90° 회전 (3쪽)')`). 적용 버튼 / 다운로드 호출 모두 제거.

### 🗑 Removed (v2.0.0-dev1 모델)

- viewMode 자동 전환 (preview/thumbnails/wysiwyg) — 항상 3-column
- panel 의 `viewMode` / `takeoverLeft` / `requestPageSelect` / `requestPageOverlay` / `releaseOverlays` / `onResult` API
- 도구마다 다운로드 호출 (panel.js 가 downloadPDF 직접 호출) — 메인 toolbar 가 일임
- `createApplyRow` 사용 (즉시 적용 패러다임에서는 사용 빈도 낮음, helper 자체는 keep — 결과 반환 도구가 향후 사용 가능)

### 🚀 다음 PR (v2.0.0-dev3)

페이지 작업 5개 (remove-pages · extract-pages · reorder · split-each · split-group) 같은 라이브 적용 패턴으로 통합. core.js 가 이미 추출됐거나 (remove-pages 부분 작성됨) 같은 패턴으로 빠르게 진행 예상.

### 라이브 검증 시나리오

1. https://sobjil-gdi-apps.mycafe24.ai/pdftools/tools/viewer/ → PDF 드롭
2. **3-column 확인**: 좌측 썸네일 / 중앙 뷰어 (텍스트 드래그 선택 가능) / 우측 도구창
3. 좌측 썸네일에서 페이지 체크 → "n/m" 카운트 갱신
4. 중앙 뷰어에서 텍스트 드래그 → 파란 하이라이트 + Ctrl+C 복사 가능
5. 우측 🔄 회전·반전 클릭 → 패널 오버레이, "회전할 페이지 선택" 또는 "n쪽 선택됨"
6. 90° 버튼 클릭 = **즉시** 좌측 썸네일 + 중앙 뷰어에 회전 반영, 메인 ↶ 되돌리기 활성
7. 다른 변환 (180°) 또 클릭 = 누적 적용
8. ↶ Undo (Ctrl+Z) → 직전 상태 복귀, ↷ Redo (Ctrl+Shift+Z) → 다시 적용
9. 💾 다운로드 → 모든 누적 결과 한 번 다운로드 ("xxx-편집됨.pdf")

---

## 🚧 v2.0.0-dev1 — viewer hub 인프라 + 회전 inline 검증 (2026-05-25)

> **v2.0 풀 리팩토링 시작점**. PATCHNOTE v2.0 박제 + [dev.md §14](doc/dev.md) 설계 박제 → 코드 첫 PR. 첫 PR 에서는 인프라 + rotate 1개 도구만 inline 검증. 후속 PR 에서 나머지 22개 도구 + TextLayer/검색/이미지 복사 점진 통합. **사용자 결정**: UI A (좌+우 패널 mount), v2.0 풀 한 번에 (v1.1 분할 X), 기존 23개 단독 도구 페이지 모두 유지.

### 🆕 Added

- **dev.md §14** — v2.0 viewer hub 리팩토링 설계 단일 진실 출처 (UI A · viewMode preview/thumbnails/wysiwyg · mountPanel 시그니처 · 공용 로직 추출 패턴 · 신규 shared 모듈)
- **shared/viewer-panel.js** (신규) — 패널 helper (`createSidebarGroup`, `createOptionGrid`, `createApplyRow`, `createNotice`, `clearChildren`)
- **tools/rotate/core.js** (신규) — `applyRotation(core, op, pages, onProgress)` 순수 PDF 변환 로직. 단독 페이지 (rotate.js) ↔ panel.js 양쪽이 import
- **tools/rotate/panel.js** (신규) — viewer 가 import 할 panel 모듈. `viewMode: 'thumbnails'` → 좌측 격자 썸네일 자동 + 우측 패널에 옵션·적용 버튼

### 🔧 Changed

- **tools/viewer/viewer.js** — v1.0.0 단순 미리보기 + 새 탭 링크 → v2.0 panel mount 인프라
  - `PANEL_MODULES` lazy import 매핑 (현재 rotate 만 등록)
  - `viewMode` 자동 전환 (`preview` lazy 페이지 ↔ `thumbnails` 격자 ↔ `wysiwyg`)
  - `mountToolPanel(id)` / `unmountToolPanel()` — 우측 사이드바 swap + panel.cleanup 처리
  - api 객체: `onResult` (PDF 교체) · `onProgress` (공용 진행 바) · `requestPageSelect` (썸네일 콜백) · `requestPageOverlay` (Step 5c 예약) · `releaseOverlays` (Step 5c 예약)
- **tools/viewer/index.html** — `#thumbsContainer` (썸네일 모드 좌측) + `#sidebarPanel` (패널 mount 영역) 신규 마크업. 사이드바 도구 링크에 `data-tool` 속성. 안내 박스 = "🚧 v2.0 진행 중, 회전만 inline" 으로 갱신
- **tools/viewer/viewer.css** — `.tool-link-inline` 미세 강조, `.viewer-thumbs`, `.sidebar-panel`, `.panel-*` (옵션 격자 · 안내 박스 · 적용 row) 스타일 추가
- **tools/rotate/rotate.js** — 단독 페이지의 PDF 변환 본문 → `core.js` 의 `applyRotation()` 호출로 교체 (1줄 호출 + import 1줄). UI 결선만 유지

### 🚀 다음 PR (Step 5a)

페이지 작업 8개 (병합·분할·삭제·재정렬·추출·크기조정·자르기 — rotate 제외) panel 통합. 같은 패턴 (`core.js` 추출 + `panel.js` viewMode 선언 + `PANEL_MODULES` 등록).

### 라이브 검증 시나리오

1. https://sobjil-gdi-apps.mycafe24.ai/pdftools/tools/viewer/ → PDF 드롭
2. 좌측 = lazy 페이지 큰 미리보기, 우측 = 도구 리스트 (🔄 회전·반전만 inline 마크)
3. 🔄 회전·반전 클릭 → 좌측이 격자 썸네일 + 체크박스로 전환, 우측이 패널 (변환 종류 격자 + 적용 버튼)
4. 페이지 체크 + 90° 선택 → "변환 적용" → 다운로드 + viewer PDF 갱신
5. "← 도구 목록" → 사이드바 복귀 + 좌측 lazy 페이지 모드 복귀

---

## 🎉 v1.0.0 정식 출시 (2026-05-25)

### 라이브 v1.0.0 — 카드 **24/24 완성**

- **백엔드 인프라**: nodejs (Express) + Cloudflare Worker v3.4.0
  - `/api/unlock` (muhammara) — 일반 권한 제한 PDF
  - `/api/translate-deepl` — DeepL Free/Pro proxy (`:fx` 자동 감지)
  - `/api/health`
  - Replicate Worker `/marker` (PDF→MD · PDF→JSON+bbox)
- **23 + 1 도구** = 페이지 작업 8 + 콘텐츠 편집 6 + 추출·변환 4 + 기타 6 (PDF 뷰어 hub 포함)
- **PDF 뷰어 hub** (v1.0.0 신규): 좌측 미리보기 (pdf.js lazy 렌더 + 줌·너비맞춤) + 우측 23개 도구 사이드바
- 메인 랜딩 카드 v0.5.8 → **v1.0.0** + 미니앱 PDF 뷰어 (Soon → Live)

### 사용자 검증 완료

- PDF 번역 Marker 모드 (스테이블코인 영문 PDF 40쪽) = paragraph + 표 셀 + bullet list 박스 모두 번역
- 줄바꿈 보존 (bullet list / 표 행)
- DeepL 기본 / Marker AI 기본 (품질 우선)

### ⚠ v1.0.0 한계 — PDF 뷰어 hub 가 사용자 의도와 다름

사용자 보고 (2026-05-25, v1.0.0 출시 직후):
> "단순히 보기만 하는 뷰어 말고 일반적인 pdf뷰어들처럼 텍스트 선택, 복사, 이미지 복사 등 다양한 기능을 넣을 수 있을까? hub라는게 단순히 링크만 원한게 아니야.. 뷰어 안에서 작동하길 원했던거야."

현재 v1.0.0 뷰어 = 단순 미리보기 + 외부 링크 = MVP 수준. 뷰어 페이지 안내에 "v1.0.0 MVP · v2.0 확장 예정" 박제. 사용자 결정 = 세션 종료 + v2.0 다음 세션 풀 리팩토링.

### 🚀 v2.0 — PDF 뷰어 풀 hub 리팩토링 (다음 세션, 수일~수주)

> **★ 사용자 합의 (2026-05-25)**: **기존 23개 도구 페이지는 모두 그대로 유지 (지우지 X)**. 뷰어 안 inline 작동 = **추가** 기능. 사용자가 단독 도구 페이지 직접 접근하는 패턴도 보존. 도구 코드는 패널 component 와 단독 페이지 둘 다 지원 (공용 로직 모듈 + 양쪽에서 import).

#### 일반 PDF 뷰어 기능
- **TextLayer** (pdf.js 공식) → 텍스트 드래그 선택 + Ctrl+C 복사
- **Ctrl+F 검색** + 하이라이트 + 결과 점프
- **이미지 복사**:
  - 우클릭 메뉴: "이 페이지 이미지로 복사" (clipboard API)
  - 드래그로 영역 선택 → 그 부분만 캔버스 → clipboard
- 우클릭: 이 페이지만 PDF/PNG 다운로드

#### 23개 도구 inline 통합 = 핵심 리팩토링
- **각 도구 = 패널 component 화**:
  ```js
  // 예: tools/rotate/panel.js
  export default {
    name: '회전',
    icon: '🔄',
    mountPanel(core, container, onResult) {
      // core = 공유 PDF 인스턴스 (pdf-core)
      // container = 뷰어 우측 패널 DOM
      // onResult(newCore) = 뷰어가 자체 업데이트
    },
  }
  ```
- **공유 PDF 인스턴스** — 재로딩 X, 모든 도구가 같은 PDFCore 사용
- **결과 처리**: 다운로드 (현재 패턴) 또는 뷰어 자체 PDF 교체
- **공용 진행 UI** — 뷰어 자체 progress bar

#### UI 옵션 (v2.0 결정 필요)
- **A**: 좌측 PDF 미리보기 + 우측 패널 mount (현재 사이드바 자리 교체)
- **B**: 상단 탭바 — 도구별 다른 화면
- **C**: 좌측 PDF + 우측 도구 리스트 + 클릭 시 floating modal

#### 각 도구 리팩토링 작업량 (대략)
- **간단** (`tools/{rotate,remove-pages,extract-pages,extract-text,unlock,compress-images,to-image}/`): 자체 file input → core 사용, 1~2시간씩
- **중간** (`tools/{merge,split,reorder,resize,crop,from-image,to-markdown,compare}/`): 추가 입력 (다른 PDF·이미지) 처리 등, 2~4시간씩
- **WYSIWYG** (`tools/{watermark-text,watermark-image,page-number,header-footer,add-text,signature,translate,summary}/`): 미리보기 통합, 4~8시간씩
- **합계**: 23개 × 평균 ~3시간 = 70시간+ (분산 작업)

### v1.1 옵션 (선택지 — v2.0 부담 크면)
- v1.1 = 일반 뷰어 기능만 (TextLayer/검색/이미지 복사) 먼저 출시 후 v2.0 = inline 도구 점진 통합
- 또는 v1.0.x patch 로 가장 자주 쓰는 5~7개 도구만 우선 inline

### 기타 Future
- 표 격자 보존 = Marker 한계 (TableCell children 안 줌). 다른 모델/API 검토 가능
- 추가 언어 (베트남어·아랍어 등) — DeepL 지원 언어 확장 시
- 한컴 OCR 백엔드 흡수 (스캔 PDF 한국어 OCR)

### 작업 시작 명령 (다음 세션 — v2.0 풀 리팩토링)

```bash
cd D:/dev/GDI-Apps/.claude/worktrees/funny-kepler-1e1366
git fetch origin main && git checkout -b claude/v2.0-viewer-hub origin/main

# Step 1: viewer 구조 설계 — UI 결정 (A/B/C 옵션)
# Step 2: 도구 패널 인터페이스 정의 (mountPanel 시그니처)
# Step 3: shared/viewer-panel.js 신규 (공용 helper)
# Step 4: 첫 1개 도구 (예: 회전) inline 통합 검증
# Step 5: 나머지 22개 도구 점진 통합 (카테고리별)
# Step 6: TextLayer + 검색 + 이미지 복사 추가
# Step 7: v2.0.0 출시
```

---

## 🔖 v0.7.x 인수인계 (참고용, 2026-05-25 v0.7.8 시점)

### 진행 중 합의사항·결정 박제

- 사용자 PDF (fda Fidelity) = pdfcandy 외부 처리. unlock 도구는 일반 케이스만
- nodejs 전환 = 우회 (DeepL ✅ / Marker ✅ / 한컴 OCR) 흡수 발판
- 미니앱 = 메인 랜딩 우측 `<aside class="col-mini">` (단일 파일 utility)
- 메인 랜딩 = "도구" → "앱" 일괄 (pdftools 내부 "도구" 는 그대로)
- Worker 배포 = 별도 (CLAUDE.md §2.4 룰) — v3.4.0 배포 완료
- Marker output_format='json' 가설 폐기 → include_metadata: true 가 진짜 옵션
- 표 격자 보존 X (Marker 가 TableCell children 안 주는 한계) — 행 단위 줄바꿈만

### 진행 중 합의사항·결정 박제

- 사용자 PDF (fda Fidelity) = pdfcandy 외부 처리. unlock 도구는 일반 케이스만
- nodejs 전환 = 우회 (DeepL ✅ / Marker ✅ / 한컴 OCR) 흡수 발판
- 미니앱 = 메인 랜딩 우측 `<aside class="col-mini">` (단일 파일 utility)
- 메인 랜딩 = "도구" → "앱" 일괄 (pdftools 내부 "도구" 는 그대로)
- Worker 배포 = 별도 (CLAUDE.md §2.4 룰) — v3.4.0 배포 완료
- Marker output_format='json' 가설 폐기 → include_metadata: true 가 진짜 옵션
- 표 격자 보존 X (Marker 가 TableCell children 안 주는 한계) — 행 단위 줄바꿈만

### 작업 시작 명령

```bash
cd D:/dev/GDI-Apps/.claude/worktrees/funny-kepler-1e1366
git fetch origin main && git checkout -b claude/pdf-viewer-hub origin/main
# pdftools/tools/viewer/ 신규 폴더
# index.html + viewer.js + viewer.css
# pdftools/index.html 의 PDF 뷰어 카드 = "준비 중" → "Live"
# index.html (랜딩) 미니앱 "📄 PDF 뷰어 (Soon)" → 라이브 링크
# 메인 랜딩 PDF 딸깍 카드 v0.7.x → v1.0.0
```

---

## v1.0.0 — 2026-05-25 (🎉 정식 출시 + PDF 뷰어 hub)

### 🆕 PDF 뷰어 hub (남은 1 카드 = 24/24 완성)

`pdftools/tools/viewer/` 신규:
- `index.html` — 좌측 미리보기 영역 + 우측 도구 사이드바 (23개 도구 카테고리별 그룹)
- `viewer.css` — grid layout (1fr + 320px), 줌·페이지 번호 UI, 다크 테마
- `viewer.js` — pdf.js 로 lazy render (IntersectionObserver, rootMargin 300px), 줌 50~300% + 너비 맞춤 토글, devicePixelRatio 적용 (선명도)

### 🔧 메인 랜딩 (`index.html`)
- PDF 딸깍 카드 v0.5.8 → **v1.0.0**
- 카드 설명 "22개 앱" → "24개 앱" + "번역·뷰어 hub" 추가
- 미니앱 PDF 뷰어 (Soon → **Live**, `pdftools/tools/viewer/` 링크)

### 🔧 pdftools 카탈로그 (`pdftools/index.html`)
- intro phase-note → "🎉 v1.0.0 정식 출시 — 카드 24/24 완성"
- PDF 뷰어 카드 "준비 중" → "사용 가능" 라이브 (`tools/viewer/` 링크)
- PDF 번역 카드 설명 v0.7.8 갱신 (Marker AI + DeepL/Gemini)
- version v0.5.16 → v1.0.0

### 📚 박제 통합
- PATCHNOTE: 인수인계 섹션 = v1.0 출시 완료 + Future (v1.1+) 항목
- dev.md: 현재 상태 행 v1.0.0
- CLAUDE.md §2.4: v1.0.0 갱신

### 📚 회고
- Phase 1~5 누적 완료 (2026-05-19 ~ 2026-05-25, ~1주)
- 가장 큰 결정: v0.5.16 nodejs 백엔드 전환 (우회 흡수 인프라)
- 가장 큰 진단 빅: v0.7.0~0.7.2 Marker schema (output_format → include_metadata)
- 가장 큰 사용자 협력: v0.7.2 input schema 직접 제공 → 한 번에 fix

---

## v0.7.8 — 2026-05-25 (Marker 모드 줄바꿈 보존)

### 🔧 Changed
- `stripHtml` 줄바꿈 의미 태그 보존
  - `<tr>` → `\n` (표 행 분리)
  - `<td>/<th>` → `  ` (셀 구분 공백)
  - `<li>/<p>/<div>/<h*>/<br>` → `\n`
- `wrapTextToLines` 가 explicit `\n` 으로 split → 각 라인 별도 wrap (빈 줄 보존)
- Gemini prompt 에 "줄바꿈 보존" 지침 추가 (DeepL 은 text 그대로 보존하니 별도 X)

### 📚 회고
- 사용자 보고: 박스/표 번역 잘 되지만 줄바꿈이 한 줄로 합쳐짐 (v0.7.7)
- 표 격자 보존은 Marker 가 TableCell children 안 주는 한계 — 행 단위 줄바꿈만으로 가독성 충분
- 사용자 결정: "이정도로 끝내자" → PDF 뷰어 hub + v1.0.0 출시

---

## v0.7.7 — 2026-05-25 (ListGroup fallback)

### 🐛 Fixed
- `FALLBACK_BLOCK_TYPES` 에 `ListGroup` 추가 — 디자인된 bullet list 박스 (콜아웃)
- 진단 (v0.7.6): 사용자 영문 bullet list 박스 2개 = ListGroup 으로 분류, children X → 통째 fallback 필요
- 1줄 fix, Worker 재배포 X

---

## v0.7.6 — 2026-05-25 (Figure/Picture/ComplexRegion fallback)

### 🆕 Added
- `FALLBACK_BLOCK_TYPES` 신설 (Table 외에 Figure/FigureGroup/Picture/PictureGroup/ComplexRegion/Form)
- `hasDescendantOfTypes(node, TEXT_BLOCK_TYPES)` helper — 자손 텍스트 검사
- 자손 텍스트 있으면 그쪽 추출, 없으면 노드 통째 fallback (`blockType + '/fallback'`)

### 🔧 Changed
- 진단 출력 강화 (tableSamples → fallbackSamples + JSON.stringify 잘림 방지)

### 📚 회고
- 사용자 보고 (v0.7.5): 디자인된 텍스트 박스 (콜아웃) 번역 누락
- 진단: Marker 가 Picture/Figure 로 분류 → 우리 추출 함수가 무시

---

## v0.7.5 — 2026-05-25 (v0.7.4 hotfix + Table fallback)

### 🐛 Fixed
- v0.7.4 PR #226 Edit 매칭 실패로 코드 누락 — v0.7.5 로 실제 적용
- `extractMarkerBlocks` 진단 로그 (block_type 통계, tableSamples)
- Table/TableGroup fallback (TableCell 자손 없으면 Table 통째 추출)
- `hasDescendantOfType` helper

---

## v0.7.4 — 2026-05-25 (PR 코드 누락, v0.7.5 로 hotfix)

header/version 만 변경됨 (Edit 실패). 사용자 보고로 발견 → v0.7.5 로 즉시 hotfix.

---

## v0.7.3 — 2026-05-25 (표 셀 번역 + 기본값 변경)

### 🆕 Added
- `TEXT_BLOCK_TYPES` 에 `TableCell` 추가 → 표 안 셀 단위 번역 + bbox overlay

### 🔧 Changed
- 기본 추출 모드: pdf.js 라인 → **Marker AI**
- 기본 번역 엔진: Gemini → **DeepL**
- (사용자 요청 — 품질 우선)

### 📚 회고
- 사용자 보고 (v0.7.2): Marker 모드 잘 작동 — 단 표 안 텍스트 번역 X
- 단순 fix (TableCell 추가) 로 해결 시도 → 일부 PDF 에서 부족 → v0.7.5+ 로 확장

---

## v0.7.2 — 2026-05-24 (Marker include_metadata 옵션 — input schema 확정)

### 🎯 Schema 확정
- 사용자 input schema 분석으로 정답 발견: **`include_metadata: true`** → json_data + metadata
- v0.7.0/0.7.1 의 `output_format='json'` 가설 폐기 (input schema 에 없는 옵션이라 무시됨)

### 🔧 Changed
- Worker v3.4.0: marker buildInput 에 `output_format` 제거, `include_metadata` 추가
- translate.js: callMarkerForJson reqBody 수정

### 📚 회고
- v0.7.0/0.7.1 schema 추측 (output_format='json') → 사용자 보고로 json_data null 확인
- v0.7.1 schema 자동 감지 + Worker v3.3.1 _raw_output 노출로 진단
- 사용자 input schema 분석 → include_metadata 발견 → v0.7.2 한 번에 fix

---

## v0.7.1 — 2026-05-24 (Marker schema 자동 감지 + 진단)

### 🆕 Added
- callMarkerForJson 5 후보 schema 자동 탐색 (json_data / raw.json / raw.tree / ...)
- F12 콘솔 `[Marker] raw response schema 진단` 그룹 로그
- `polygon` (4 corner) → `bbox` 자동 변환 fallback
- Worker v3.3.1: `_raw_output` 디버그 필드 노출

---

## v0.7.0 — 2026-05-24 (PDF 번역 Marker 모드 — paragraph + bbox overlay)

### 🆕 추출 모드 옵션 신설

PDF 번역에 두 가지 모드 추가:

| 모드 | 속도 | 비용 | 자연스러움 | 적합 |
|---|---|---|---|---|
| pdf.js 라인 묶기 (기본, v0.6.0 유지) | 즉시 | 무료 | 보통 | 짧은 PDF · 간단한 레이아웃 |
| **Marker AI (신규)** | 30초~수 분 | ~6원/쪽 | 최고 (paragraph 의미) | 정형 문서 · 긴 문서 |

### 🆕 Marker 모드 흐름

1. `pdftools/tools/translate/translate.js` → Replicate Worker (`/marker`, output_format='json') 호출
2. Marker 응답의 `json_data` (BlockType 트리) 파싱 — `extractMarkerBlocks()`
3. 페이지별 텍스트 block 추출 (`Text`/`Title`/`SectionHeader`/`Caption`/`ListItem` 등)
4. block.text → Gemini/DeepL 번역 (paragraph 단위 = 자연스러움 ↑)
5. Marker page 픽셀 bbox ↔ pdf-lib pt 좌표 매핑 (`makeMarkerToPdfTransform`)
6. 각 block.bbox 자리에 흰 박스 + 번역 wrap text (`drawWrappedText` — 단어/문자 단위 wrap + fontSize 자동 추정)

### 🆕 Worker patch (cloudflare-worker/worker.js v3.3.0)

- `marker` buildInput 에 `output_format` 옵션 추가 ('markdown' 기본 / 'json' / 'html' / 'chunks')
- 응답 schema 변동 X (`json_data` 는 v3.2.0 부터 통과 중)
- 기존 PDF→MD 도구는 호출 변경 X (backwards compatible)
- ⚠ **Worker 배포 = Cloudflare 대시보드 / wrangler 수동** (CLAUDE.md §2.4 룰)
  - 헬스 체크: `curl https://gdi-replicate-proxy.sobjil.workers.dev` → version 확인
  - v3.2.0 → v3.3.0 으로 바뀌면 Marker 모드 작동

### 🔧 UI 변경

- 추출 모드 fieldset 추가 (라디오 2개)
- 결과 미리보기에 모드/예상 비용 노출 (Marker 모드 시 `selectedPages × 6원` 표시)
- 친절 에러: json_data 누락 시 "Worker v3.3.0 배포 필요" 안내

### 📚 회고

- 사용자 보고 (2026-05-24): v0.6.0 라인 단위 묶기도 자연스러움 부족 → "PDF→MD→번역→overlay" 흐름 제안
- 옵션 3가지 제시 (A: paragraph 휴리스틱 / B: Marker JSON+bbox / C: MD 다운로드)
- 사용자 결정: **"B 안 한번 해보고 맘에 안들면 C 안"**
- Worker 배포 의존성 = 위험. v3.3.0 미배포 시 친절 에러로 사용자가 즉시 인지

---

## v0.6.0 — 2026-05-24 (PDF 번역 정교화 — 라인 묶기 + DeepL 백엔드)

### 🆕 Added
- **`/api/translate-deepl` 백엔드 엔드포인트** — DeepL Free/Pro proxy. 키 끝 `:fx` 자동 감지 → `api-free.deepl.com` / `api.deepl.com` 분기. 키는 매 요청 클라이언트가 전송 (서버 저장 X), 로그에 마스킹 (끝 8자만). 400/403/429/456 친절 에러 매핑.
- **PDF 번역 DeepL 엔진 활성화** — 옵션 disabled 제거. 50개씩 chunk (DeepL 한도 50 안전망 45).
- **인접 item 묶기 휴리스틱** — `groupItemsIntoChunks()` 신규.
  - 라인 묶기: Y 차 ≤ `max(2, fontSize × 0.3)` + fontSize 차 ≤ 20%
  - chunk 분리: 같은 라인 안 X gap > `fontSize × 2` 면 별도 chunk
  - chunk str: pdf.js item.str join + 큰 gap 자리에 space 보강 (양쪽 끝 이미 space 면 skip)
  - 위치: 첫 item 의 (x, y), 너비 = lastEnd − firstX, fontSize = 평균

### 🔧 Changed
- **번역 단위 1:1 item → 라인 chunk** — pdf.js 가 단어별로 쪼개도 한 라인 통째로 번역. 자연스러움 ↑, 토큰 효율 ↑ (보통 50~80% 라인 축소).
- **Gemini 프롬프트** — "item" → "라인" 표기. "라인 안에서 어순 조정 가능" 명시.
- **결과 메시지** — `overlay X 라인 · skip Y · Z 쪽 (item N → 라인 M, P% 축소)` 형태로 효과 가시화.
- **api-keys.js → v1.5.1** — deeplApiKey note 에 "백엔드 proxy 경유 (CORS 우회 완성)" 추가. version 1.4.4 → 1.5.1 정정.

### 📚 회고
- **백엔드 인프라 가치 두 번째 입증**: v0.5.17 muhammara (unlock) 이어 DeepL CORS 까지 흡수. 앞으로 한컴 OCR · Marker proxy 등도 같은 패턴.
- **휴리스틱 한계**: 여러 줄 paragraph 묶기는 X (라인까지만). 줄바꿈이 의미 있는 PDF 가 흔해서 보수적 선택. paragraph 단위 묶기는 추후 옵션화 검토.
- **DeepL vs Gemini 사용처**: Gemini = 다국어·온톨로지 컨텍스트, DeepL = 정형 문서 (유럽어 ↔ 한국어/일본어) 의 자연스러움. 사용자가 PDF 성격 보고 선택.

---

## v0.5.17 — 2026-05-24 (백엔드 PDF unlock — muhammara only, qpdf/ped 옵션 회고)

### 시도·결과 정리

| 라이브러리 | 결과 | 원인 |
|---|---|---|
| @jspawn/qpdf-wasm (4년 stale) | npm 버전 X | ^0.2.0 존재 X (최신 0.0.2) |
| qpdf-wasm-esm-embedded | `__dirname is not defined` | ESM 환경 호환 X |
| @jspawn/qpdf-wasm 0.0.2 createRequire | `Failed to parse URL` | emscripten 가 브라우저 가정, nodejs URL fetch 충돌 |
| @jspawn/qpdf-wasm + wasmBinary + locateFile | 같은 URL parse 에러 | emscripten 내부 코드 patch 불가 |
| **muhammara** | ✅ 일반 PDF OK | 검증됨 (13KB 일반 PDF HTTP 200) |
| muhammara + fda PDF | invalid object refs 7개, 페이지 수 0 | strict parser, 비표준 구조 PDF 거절 |
| pdf-encrypt-decrypt (pdfcpu Go FFI) | `__vfprintf_chk symbol not found` | AISpace 컨테이너 glibc 호환 X |

### 결론

- ✅ **백엔드 인프라 가치 입증**: muhammara 가 일반 권한 제한 PDF 처리. /api/unlock 엔드포인트 작동.
- ❌ **극단 케이스 (fda Fidelity 등) = 라이브러리 옵션 사실상 소진**:
  - npm WASM 패키지들 = nodejs 환경 호환 X (emscripten 빌드 문제)
  - npm native FFI 패키지들 = PaaS glibc 호환 X
  - 진짜 qpdf CLI 만 처리 가능 (PaaS 에 없음, native install 불가)

### 사용자 대응 (현재)

- 일반 권한 제한 PDF → muhammara 가 처리
- 비표준 PDF (fda 같은) → 친절 에러 안내 (pdfcandy / Adobe Acrobat 일회 처리 권장)
- 클라이언트 fallback (pdf-lib + pdf.js saveDocument) 도 유지 → 백엔드 실패 시 자동 시도

### 백엔드 인프라 가치 (앞으로)

nodejs 전환 자체가 이 PDF 1개 위함이 아니라 **다음 흐름의 발판**:
- DeepL 백엔드 fetch (CORS 우회) — PDF 번역 v0.6+
- Marker (Replicate) 자체 흡수 검토
- 한컴 OCR 재검토 (v0.3.6 폐기 → 백엔드로 가능)
- 미래 도구의 백엔드 의존성 자유

### 교훈 박제

- **PaaS 환경 = native binary 의존 라이브러리 fragile** (glibc 버전·prebuilt 없는 architecture 등). WASM 도 nodejs 환경 미지원 흔함.
- **공식 native CLI (qpdf 등) = PaaS 컨테이너에 없으면 사용 불가** (apt-get / Dockerfile 둘 다 X).
- **백엔드 가능 가치는 단일 라이브러리 가치 보다 큼** — qpdf 1개 못 풀어도 다른 우회들 (DeepL/한컴 OCR 등) 흡수 가치.

---

## v0.5.16 — 2026-05-24 (🚀 백엔드 nodejs 전환 + qpdf-wasm 진짜 unlock)

### 🚀 백엔드 전환 — GDI-Apps static → nodejs (Express)

그동안 백엔드 X 로 우회·폐기한 사례 (OCR · Marker · DeepL · qpdf) 의 근본 해결. 신규 2개 파일 (`package.json` + `server.js`) + jszip CDN 교체. **5 앱 + 23 도구 코드 0줄 변경** (express.static 으로 그대로 serve).

전환 과정:
- 첫 deploy: AISpace Orchestrator 의 호환성 검사가 브라우저 jszip.min.js 의 `new Buffer()` 를 nodejs 호환성 오류로 잘못 잡음
- 3개 위치 (image-editor·md2hwpx·WeeklyBrief) 의 로컬 jszip.min.js → CDN 교체
- 사용자 웹콘솔에서 프로젝트 삭제 → 신규 deploy → nodejs 자동 감지 → npm install + npm start
- 헬스 체크 `GET /api/health` → `{"status":"ok","runtime":"nodejs","node_version":"v20.20.2"}`

### 🆕 `POST /api/unlock` — qpdf-wasm 진짜 권한 제한 제거

`@jspawn/qpdf-wasm` npm 패키지. qpdf CLI 가 WASM 으로 컴파일된 것. 백엔드에서 lazy 로드 (첫 호출 시에만 메모리 점유).

```
원본 PDF (강한 권한 제한)
   ↓
POST /api/unlock  Content-Type: application/pdf  body: bytes
   ↓
qpdf.FS.writeFile(input)
qpdf.callMain(['--decrypt', input, output])
qpdf.FS.readFile(output)
   ↓
응답 = unencrypted PDF bytes
```

#### unlock.js — cascade fallback

```
1단계: POST /api/unlock (qpdf-wasm)  ← 가장 강력
   ↓ 실패 시
2단계: 클라이언트 pdf-lib + saveDocument (loadPDFLibFresh)
   ↓ 실패 시
친절 에러 (열기 비밀번호 PDF — Adobe Acrobat 안내)
```

#### 안전망

- PDF 매직 넘버 (`%PDF-`) 검증
- qpdf exit code 분기 (0/2 정상, 3 = 열기 비밀번호 PDF)
- emscripten ExitStatus 예외 분기
- 가상 FS 파일 정리 (`unlink`) — 메모리 절약
- 100MB 입력 한도 (express.raw limit)

### 박제

- `CLAUDE.md §1` 배포 형태 갱신: static → nodejs (Express)
- `CLAUDE.md §3` 메모리 룰 #10 신설: nodejs 패턴
- 백엔드 전환 배경 + 점진 흡수 원칙

### 다음 단계 (v0.6+)

- DeepL 도 백엔드 흡수 (`POST /api/translate-deepl`) — CORS 우회 해결
- Marker (Replicate) 자체 흡수 검토 (Cloudflare Worker 의존 줄임)
- 한컴 OCR 재검토 (v0.3.6 폐기 → 백엔드 fetch 가능해짐)

### cache-bust

- `unlock.js?v=0.5.15` → `v=0.5.16`
- 메인 버전 배지 v0.5.16

### 교훈

**백엔드 = 미래 시점이 더 안전해지지 않음**. v1.0 진입 전 = 가장 좋은 timing. 도구 23개 < 미래 30+ 개 (재검증 부담 ↑). 사용자 통찰 정확.

---

## v0.5.15 — 2026-05-24 (잠금 풀기 — raster fallback 제거 + 솔직 에러)

### 🎯 대전제 재정립 (사용자 지적)

**잠금 풀기 도구의 본 목적 = 다른 도구 (번역·추출 등) 가 막혔을 때 우회 → 텍스트 처리 진행**.

v0.5.15 작업 중 임시로 넣었던 "pdf-lib 실패 시 pdf.js raster → 이미지 PDF" fallback 은 **텍스트 손실 → 사용자 의도 정반대**. 즉시 제거.

### 변경

- raster 화 fallback 제거
- pdf-lib 실패 시 솔직한 에러 + 옵션 안내:
  - ① Adobe Acrobat / qpdf 같은 외부 도구
  - ② 원본 발행처에 보호되지 않은 버전 요청
  - ③ 추후 qpdf-wasm 통합 예정 (v0.6+ 검토 중)
- 에러 메시지 분기: `Expected instance`·`encrypted`·`lookup`·`catalog` 키워드 매칭 시 "강한 보안 PDF — 처리 한계" 안내

### 다음 단계 (사용자 결정 대기)

강한 보안 PDF (한컴 ezPDF · 정부 발행물 등) 의 진짜 해결 = qpdf-wasm 도입. v0.6+ 진입 시 결정.

### cache-bust

- `unlock.js?v=0.5.14` → `v=0.5.15`

### 교훈 — **대전제 우선**

도구의 본 목적을 시야 안에 항상 둘 것. fix 가 본 목적과 충돌하면 fix 가 아니라 함정.

---

## v0.5.14 — 2026-05-24 (잠금 풀기 — 진짜 encryption 제거, saveDocument + pdf-lib save 조합)

### 🐛 Fixed — v0.5.13 의 잠금풀린 PDF 가 한컴/Adobe 에서 여전히 "암호 보안 / 문서 구성: 허용하지 않음" 표시 (사용자 보고)

#### 원인

pdf.js 의 `saveDocument()` 는 PDF **normalize (decrypt 포함)** 는 하지만 **encryption dictionary 자체 제거 X**. 결과 PDF 는 여전히 보안 메타데이터 보유 → 뷰어에서 같은 권한 제한 표시.

v0.5.13 흐름:
```
원본 → pdf.js saveDocument → 다운로드  ← encryption 메타 그대로 ❌
```

#### 진짜 unlock 흐름 (v0.5.14)

pdf-lib 의 `save()` 가 `ignoreEncryption: true` 로 load 한 doc 을 **자동으로 unencrypted PDF 로 export**. 이게 진짜 encryption 제거 (검증된 동작 — pdf-lib 1.17.1 패턴).

```
원본 PDF
   ↓
loadPDFLibFresh(core)
   ├ 1차: pdf-lib load(bytes, ignoreEncryption: true) + getPages sanity
   └ 실패 시: pdf.js saveDocument (catalog 정리) → pdf-lib load(normalized)
   ↓
doc.save()   ← ★ pdf-lib 가 encryption dictionary 제거하며 export
   ↓
다운로드 — "문서 보안: 보안 없음" 으로 표시 ✅
```

핵심 = **두 단계 조합**:
- pdf.js saveDocument: catalog reference 풀림 실패 회피 (강력 암호화 대응)
- pdf-lib save: encryption 메타데이터 제거 (진짜 unlock)

#### Before (v0.5.13)
```js
const core = await loadPDF(file);
const out = await core.getNormalizedBytes();  // pdf.js saveDocument 만
downloadPDF(out, ...);   // encryption 메타 그대로 ❌
```

#### After (v0.5.14)
```js
const core = await loadPDF(file);
const doc = await loadPDFLibFresh(core);  // catalog 실패 자동 fallback 포함
const out = await savePDFLib(doc);         // ★ pdf-lib save = encryption 제거
downloadPDF(out, ...);
```

### UI 메시지 갱신

> 인쇄·복사·편집 모두 가능한 새 PDF 다운로드됨. **한컴/Adobe 에서 "문서 보안: 보안 없음" 으로 표시되어야 합니다**. 본인 권리가 있는 파일에만 사용하세요.

→ 사용자가 결과 검증 방법 명시.

### cache-bust

- `unlock.js?v=0.5.13` → `v=0.5.14`
- 메인 버전 배지 v0.5.14

### 박제 — 잠금 풀기 = saveDocument + pdf-lib save 조합

`pdftools/doc/dev.md` §검증 패턴: 잠금 풀기는
1. catalog 정리 (pdf.js saveDocument fallback)
2. encryption 제거 (pdf-lib save)

두 단계 **모두** 필요. 둘 중 하나만으로는 불충분.

---

## v0.5.13 — 2026-05-24 (잠금 풀기 도구도 pdf.js saveDocument 직접 사용)

### 🐛 Fixed — 잠금 풀기에서도 같은 catalog 에러 (사용자 보고)

v0.5.12 의 `loadPDFLibFresh` 자동 fallback 은 **다른 도구들에만 적용** — 잠금 풀기 도구는 `shared/pdf-core.js` 안 쓰고 직접 `PDFLib.PDFDocument.load(bytes, { ignoreEncryption: true })` + `save()` 하는 단순 구조였음. 같은 PDF 에서 `getPageCount` → `getPages` → catalog lookup 실패.

```
unlock.js:70 Error: Expected instance of e, but got instance of undefined
  at e.getPages (PDFDocument.js:477:31)
  at e.getPageCount (PDFDocument.js:461:35)
```

### 핵심 변경: pdf-lib 우회

잠금 풀기 도구의 본 목적 = PDF 의 권한 제한·encryption 제거. **pdf.js 의 `saveDocument()` 자체가 PDF 를 완전 normalize (decrypt 포함) 한 bytes 를 반환** 하니 pdf-lib 안 거치고 그대로 다운로드.

#### Before (v0.5.12 까지)
```js
const doc = await PDFLib.PDFDocument.load(bytes, { ignoreEncryption: true });
const out = await doc.save();  // ← catalog 실패 시 던짐
downloadPDF(out, ...);
```

#### After (v0.5.13)
```js
const core = await loadPDF(file);
const out = await core.getNormalizedBytes();  // pdf.js saveDocument
downloadPDF(out, ...);
```

#### 장점

- **가장 강력**: pdf.js 가 load 가능한 모든 PDF 처리 (catalog 풀림 실패도 자동 normalize)
- **가장 단순**: pdf-lib 의존성 제거 (CDN 은 그대로 두지만 사용 X)
- **친절 에러**: 열기 비밀번호 PDF 만 fail → Adobe Acrobat / qpdf 안내

### 흐름

```
사용자 PDF
   ↓
pdf.js getDocument({ data: bytes }) → numPages 확인
   ↓
pdf.js saveDocument() → normalized bytes (decrypt 포함)
   ↓ 결과가 너무 작거나 비어있으면 → 손상 안내
   ↓
downloadPDF(out, '잠금풀림.pdf')
```

### 빈 PDF / 깨진 PDF 안전망

`out.byteLength < 100` 면 normalize 실패로 판단, "PDF 손상 가능" 에러.

### 열기 비밀번호 PDF

pdf.js 가 load 실패하면 (`encrypted` / `password` 키워드 매칭) → 친절 안내:
> 파일 열기 자체에 비밀번호가 필요한 PDF 는 비밀번호 없이 풀 수 없습니다. 원본 비밀번호를 알고 있다면 Adobe Acrobat 또는 qpdf 같은 별도 도구로 먼저 해제하세요.

### cache-bust

- `unlock.js?v=0.5.12` → `v=0.5.13`
- 메인 버전 배지 v0.5.13

### 박제 — 잠금 풀기 도구 = pdf.js 기반 (pdf-lib 우회)

`pdftools/doc/dev.md` §검증 패턴: 잠금 풀기는 다른 도구와 달리 **pdf-lib 안 거치고 pdf.js saveDocument 직접 사용**. v0.5.13 결정.

---

## v0.5.12 — 2026-05-24 (강력 암호화 PDF — pdf.js saveDocument fallback)

### 🐛 Fixed — `Expected instance of e, but got instance of undefined` (사용자 보고)

v0.5.11 의 `ignoreEncryption: true` 만으로는 일부 PDF (정부 발행 · ezPDF 생성 · 강한 권한 제한 등) 의 catalog reference 가 풀리지 않아 `PDFDocument.getPages()` 가 던짐.

```
PDFCatalog.js:12 e.Pages
↓
PDFDocument.computePages → t.lookup → undefined
↓
"Expected instance of e, but got instance of undefined"
```

**원인** — pdf-lib 의 `ignoreEncryption: true` 는 단순히 load 시 에러를 안 던질 뿐 **실제 stream decryption 안 함**. 권한 제한이 강한 PDF 는 catalog object 가 풀리지 않아 빈 결과.

**돌파구** — pdf.js 의 `PDFDocumentProxy.saveDocument()` 가 **PDF 를 완전 normalize (decrypt 포함)** 한 bytes 를 반환. 이걸 pdf-lib 에 넘기면 정상 처리.

### 핵심 변경: `shared/pdf-core.js`

#### 1. 자동 fallback 로직 (`loadPDFLibWithFallback`)

```
pdf-lib load(bytes, ignoreEncryption: true)
  ↓ sanity check (getPages 호출)
  ↓ 성공 → return doc
  ↓ 실패 + 에러 패턴 (Expected instance/encrypted/lookup) 매칭 시
pdf.js saveDocument()  →  normalized bytes
  ↓
pdf-lib load(normalized bytes)
  ↓ 또 실패 시 → 친절 에러 메시지 (잠금 해제 도구 권장)
```

#### 2. 신규 export `loadPDFLibFresh(core)`

매번 새 doc 가 필요한 도구 (워터마크·서명·번역 등) 가 안전 fallback 적용:

```js
// 기존
const src = await PDFLib.PDFDocument.load(currentCore.bytes, { ignoreEncryption: true });

// 신규 (자동 fallback)
const src = await loadPDFLibFresh(currentCore);
```

#### 3. 추가 helper `core.getNormalizedBytes()`

다른 도구가 normalize bytes 직접 필요한 경우 (캐시됨).

### 적용 범위

| 도구 | 변경 |
|---|---|
| `getPDFLib()` 캐시 사용 (extract-pages·split·rotate·remove-pages·reorder·resize·crop·to-image·to-markdown·extract-text·compare·compress-images·summary·from-image) | shared 의 자동 fallback 으로 처리 — 별도 변경 X |
| 직접 load 7개 (add-text·header-footer·page-number·signature·watermark-text·watermark-image·translate) | `PDFDocument.load(currentCore.bytes, { ignoreEncryption: true })` → `loadPDFLibFresh(currentCore)` |
| merge | 다중 파일이라 별개 (각 file 별 fresh load) — 일단 그대로 |
| unlock | 변경 X (잠금 해제 본 기능) |

### 친절한 에러 메시지

두 단계 fallback 모두 실패 시:
```
이 PDF 는 구조가 손상되었거나 강한 암호화로 처리할 수 없습니다.
[PDF 잠금 해제] 도구로 먼저 처리하거나, 원본 발행처에서 보호되지 않은 버전을 받아 다시 시도해보세요.
```

### cache-bust 일괄 sweep

- 모든 `.js` 의 `shared/pdf-core.js?v=0.5.11` → **v=0.5.12** (sed)
- 모든 `index.html` 의 자기 `.js?v=0.5.11` → **v=0.5.12** (sed)
- 메인 + 도구별 버전 배지 v0.5.12

### 박제 — 검증 패턴 누적 갱신

`pdftools/doc/dev.md` §6 — pdf-lib 처리 패턴에 추가:
- **`{ ignoreEncryption: true }` 만으로 부족 케이스** → `shared/pdf-core.js` 의 `loadPDFLibFresh()` / `getPDFLib()` 가 자동 처리
- 도구 작성 시 매번 PDFDocument.load 직접 호출 X — `loadPDFLibFresh(core)` 사용

### 교훈 강화

v0.5.11 = `ignoreEncryption` 옵션만으로 일관 처리한다고 박제했지만, 실제로 그 옵션도 한계 있었음 (강한 권한 제한 PDF). v0.5.12 = `loadPDFLibFresh` helper 가 한 곳에서 모든 케이스 처리 (옵션 + 자동 fallback + 친절 에러). **공통 helper 가 도구별 개별 처리보다 안전**.

---

## v0.5.11 — 2026-05-24 (전반 hotfix: 권한 제한 PDF 가 모든 도구에서 정상 처리)

### 🐛 Fixed — `ignoreEncryption: true` 일관 적용 (사용자 보고: 페이지 추출도 같은 에러)

v0.5.10 에서 PDF 번역 도구만 fix 했으나 페이지 추출 등 다른 도구에서도 같은 권한 제한 PDF 처리 실패. 전반 검토 후 일괄 fix.

#### 영향 범위

`shared/pdf-core.js` 의 캐시된 `getPDFLib()` 가 `PDFDocument.load(bytes)` 옵션 없이 호출 — 이 캐시를 사용하는 모든 도구 (extract-pages·split·remove-pages·rotate·reorder·resize·crop·to-image·to-markdown 등) 가 권한 제한 PDF 거절. 도구별 직접 load 도 일부 누락 (add-text·header-footer·page-number·signature·watermark-text·watermark-image).

#### Fix

1. **`shared/pdf-core.js` `getPDFLib()`** — `PDFDocument.load(bytes, { ignoreEncryption: true })` 디폴트 적용 → 모든 도구가 자동으로 권한 제한 PDF 처리
2. **직접 load 7개 도구**: 모두 `{ ignoreEncryption: true }` 옵션 추가:
   - `add-text.js`, `header-footer.js`, `page-number.js`, `signature.js`, `watermark-text.js`, `watermark-image.js`
   - `merge.js`: `{ ignoreEncryption: false }` → `true` (일관성 — 기존 false 는 의도된 게 아닌 안전망)
3. **`unlock.js`** = 변경 X (잠금 해제 기능 자체 = `ignoreEncryption: true` 명시 — 의도된 동작)

#### cache-bust 일괄 sweep

- 모든 도구의 `.js` 안 `shared/pdf-core.js?v=` 쿼리 → **v=0.5.11 통일** (sed 일괄)
- 모든 도구의 `index.html` 안 자기 `.js?v=` 쿼리 → **v=0.5.11 통일** (sed 일괄, 도구별 이름 정확 매칭)
- 메인 + 도구별 버전 배지 v0.5.11

### 📚 박제 — 검증 패턴 누적에서 누락된 항목 발견

`pdftools/doc/dev.md` 의 §6 검증 패턴 누적에 `pdf-lib (...·ignoreEncryption)` 명시되어 있지만 신규 도구 작성 시 누락 사례 반복 (v0.3.6+ OCR·v0.5.1 요약·v0.5.9 번역). 앞으로 신규 도구의 `PDFDocument.load(bytes)` 는 무조건 `{ ignoreEncryption: true }` 동반 — `shared/pdf-core.js` 디폴트 적용으로 강제화.

### 교훈

공통 모듈 (shared/pdf-core.js) 의 디폴트 안전 옵션 강제가 도구별 누락 위험을 근본 차단. 도구 작성 시 매번 옵션 챙기는 것 → 한 곳에서 옵션 박제.

---

## v0.5.10 — 2026-05-24 (PDF 번역 hotfix: 암호화 PDF load 대응)

### 🐛 Fixed — `PDFDocument.load` 가 암호화 PDF 에서 실패

사용자 보고: **"Input document to PDFDocument.load is encrypted. You can use PDFDocument.load(..., { ignoreEncryption: true }) if you wish to load the document anyways."**

원인: pdf-lib 가 권한 제한 (편집/인쇄 제한 — 비밀번호 X 형태) PDF 에 대해 디폴트로 에러. 다른 도구 (워터마크·잠금 해제 등) 는 이미 `ignoreEncryption: true` 사용 중인데 PDF 번역 도구에서 빠뜨림 (`pdftools/doc/dev.md` §검증 패턴 누적에 박제된 패턴 누락).

**Fix** — `translate.js` 의 `PDFLib.PDFDocument.load(currentCore.bytes)` → `PDFDocument.load(currentCore.bytes, { ignoreEncryption: true })`. 워터마크·잠금 해제 패턴과 동일.

### cache-bust

- `translate/index.html` + `translate.js?v=0.5.10`
- 메인 + translate 버전 배지 v0.5.10

---

## v0.5.9 — 2026-05-24 (Phase 5 #2 — PDF 번역 신규 MVP)

### 🆕 PDF 번역 도구 (`tools/translate/`)

본 앱의 ★4 난이도. 사용자 결정 (2026-05-24):
- **출력 형식 = 새 PDF (원본 레이아웃 위 번역 overlay)** — 가장 어려운 옵션
- **번역 엔진 = Gemini + DeepL 둘 다 (사용자 선택)** — UI 노출

#### MVP 흐름

1. pdf.js `getTextContent` 로 페이지별 텍스트 item 추출 (각 item: str, transform [a,b,c,d,e,f], width, height, fontSize)
2. 의미 있는 item 필터 (공백·1글자 기호·숫자만 제외)
3. 회전 텍스트 제외 (transform 의 b/c 가 0 아니면 skip — 원본 보존)
4. Gemini 배치 호출 (BATCH_SIZE=60, JSON 모드, id 매핑)
5. pdf-lib `PDFDocument.load(원본 bytes)` — 워터마크 패턴 reuse
6. 한글 폰트 임베드 (도착 언어 한국어/일본어/중국어면 나눔고딕, 영어면 Helvetica)
7. 각 item 마다:
   - 흰 박스 그려 원본 가리기 (`page.drawRectangle` — 흰색·opacity 1)
   - 번역 텍스트 그리기 (`page.drawText`) — 원본 너비 fit, 안 들어가면 폰트 자동 축소 (최소 4pt)
8. 새 PDF 다운로드

#### UI 옵션

- 번역 엔진: **Gemini** (디폴트) / DeepL (준비 중 — disabled)
- 출발 언어: 자동 / English / 한국어 / 日本語 / 中文
- 도착 언어: **한국어** (디폴트) / English / 日本語 / 中文
- 한글 폰트: 나눔고딕 / 나눔고딕 굵게 / 나눔명조 (도착 = 한국어 때만)
- 페이지 범위

#### Gemini 배치 prompt 핵심

- JSON 형식 강제 (`responseMimeType: 'application/json'`)
- 각 item 의 id 보존
- 짧은 어구도 자연스럽게
- 고유명사·숫자·약어는 그대로
- 이미 도착 언어인 텍스트는 그대로

#### DeepL — v0.6+ 예정

DeepL Free/Pro 모두 브라우저 직접 호출 시 **CORS 제한**. v0.6+ 에서 Cloudflare Worker proxy 추가 후 활성화 예정. 일단 UI 에 옵션 노출 + disabled + 안내.

### 🆕 shared/api-keys.js v1.5.0

- `deeplApiKey` 신규 추가 (DeepL Free/Pro, 발급 링크 + Free :fx 접미사 안내)
- pdftools 앱 매핑 (PDF 번역)

### 🆕 메인 카탈로그

- PDF 번역 카드 활성화 (사용 가능 badge + `tools/translate/`)
- 카드 22/24 → **23/24**. 남은 1: PDF 뷰어 hub (v1.0 직전 마지막)

### MVP 한계 (v0.5.9)

- **1:1 item 단위 번역** (문장 묶기 X — v0.6+ 정교화 예정). 짧은 어구는 어색할 수 있음
- 가로 텍스트만 (회전·세로 텍스트 보존)
- 표·이미지·수식 텍스트 추출 안 되면 원본 보존
- 줄 길이 변화 → 폰트 자동 축소 (4pt 까지). 그래도 안 맞으면 일부 잘림 가능
- 한글 폰트 임베드로 결과 PDF +4MB 정도 추가 (subset:false — 한글 글리프 누락 버그 회피)
- DeepL 미동작 (v0.6+ Worker proxy 추가 후)

### v0.6+ 정교화 후보

- 인접 item 묶어 문장 단위 번역 (Y 좌표 ±2 + X 연속 휴리스틱) — 자연스러움 ↑, 토큰 효율 ↑
- DeepL Worker proxy (cloudflare-worker/worker.js 에 핸들러 추가)
- 회전 텍스트 대응 (drawText 의 rotate 옵션 활용)
- 원본 폰트 크기 정확도 (font.heightAtSize 비교)
- 미리보기 (번역 전후 첫 페이지 사이드바이사이드)

### cache-bust

- `pdftools.css?v=0.5.9`, `translate/*.js?v=0.5.9`
- `shared/api-keys.js?v=1.5.0`
- 메인 + translate 버전 배지 v0.5.9

---

## v0.5.8 — 2026-05-24 (PDF 요약 — .html 다운로드 + 루트 view.html 범용 마크다운 뷰어 승격)

### 🆕 PDF 요약 결과 .html 다운로드 옵션 (다이어그램 self-contained)

사용자 질문: **"이거 md 는 뷰어 같은게 따로 있나?"**.

PDF 요약 결과의 mermaid 다이어그램은 **렌더된 SVG** 가 필요하지만, 일반 텍스트 에디터·메모장으로 .md 를 열면 raw 코드블록만 보임. 별도 뷰어 의존 X 하고 **그 자체로 어디서나 열리는 self-contained HTML** 출력 옵션 추가.

#### 출력 형식 옵션 3개 재편 (디폴트 = HTML)

```
☑ HTML · 추천   .html (브라우저로 바로 열림 · 다이어그램 + 다크 톤 스타일 포함)
☐ 마크다운     .md (mermaid 코드블록 포함 · 별도 뷰어 권장)
☐ 일반 텍스트  .txt (마크다운 기호 X · 복사·붙여넣기 편함)
```

#### .html 출력 구현

- `buildSelfContainedHtml()` — `sumResults.innerHTML` (이미 렌더된 도메인 chip + Mermaid SVG + 본문 HTML) 을 그대로 가져와서 다크 톤 스타일 + 헤더 (원본 파일명·생성일·모델명) + 푸터로 wrap
- **외부 의존성 X** — Mermaid CDN 도 불필요 (SVG 가 이미 인라인 박혀있음)
- 인터넷 끊겨도 그냥 열림
- 브라우저로 더블클릭 → 즉시 다이어그램 + 요약 보임

### 🆕 루트 view.html — 범용 마크다운 뷰어로 승격

기존 view.html 은 **저장소 안 .md 만** + **mermaid 미지원** (marked.js 만). 두 한계 한꺼번에 해결:

#### 1. 로컬 .md 드래그&드롭 지원

- URL 의 `?path=...` 없이 진입하면 **드롭존 UI** 표시
- 사용자가 로컬 .md 드래그하면 `FileReader.text()` 로 읽고 즉시 렌더
- 헤더에 **[📂 로컬 파일 열기]** 버튼 — path 있는 페이지에서도 다른 파일 열기 가능
- 파일 확장자/MIME 안 맞으면 confirm 후 진행 (관대)

#### 2. Mermaid 코드블록 자동 SVG 렌더

- marked 가 `<pre><code class="language-mermaid">` 로 만든 블록을 자동 swap → `<div class="mermaid">` → mermaid.run()
- Mermaid@10 ESM lazy load (mermaid 블록 있을 때만)
- 직전 헤더가 `📊` / `논리 흐름` / `flow` 등 포함이면 자동으로 같이 박스로 묶어서 인포그래픽 형태
- 다크 친화 테마 (PDF 요약 도구와 동일 색 톤)

#### 3. 도메인 chip 자동 분리

- 마크다운 첫 줄이 `🏷 감지된 도메인: ...` 형식이면 보라색 pill chip 으로 분리 렌더
- PDF 요약 결과 .md 가 자연스럽게 잘 보임

### 🔧 PDF 요약 도구 — 결과 다시 보기 안내

옵션 안내 박스 (`.cmp-scope-note`) 에 한 줄 추가:
```
💡 결과 다시 보기 — HTML 다운로드면 브라우저로 바로 열림.
   .md 는 📂 딸깍 마크다운 뷰어 또는 VS Code·Obsidian·Typora 같은
   mermaid 지원 뷰어에서 열기.
```

뷰어 링크가 `../../../view.html` 로 새 탭 열림.

### cache-bust

- `pdftools.css?v=0.5.8`, `summary.js?v=0.5.8`
- 메인 + summary 버전 배지 v0.5.8

### 한계 / 다음 단계

- view.html 의 mermaid 렌더는 인터넷 필요 (CDN). 오프라인은 .html 다운로드 권장
- .html 출력은 **이미 화면에 렌더된 결과** 를 가져오므로, 요약 후 옵션 바꾼 상태로 다운받으면 화면과 다를 수 있음 (lastSummary 의 format 기준)
- 코드블록 안에 mermaid 외 syntax highlighter X (필요하면 view.html 에 highlight.js 추가)

---

## v0.5.7 — 2026-05-24 (PDF 요약 — 🐛 hotfix: 파일 입력창 사라짐 fix)

### 🐛 Fixed — PDF 요약 도구 진입 시 파일 입력창 미렌더 (v0.5.6 회귀)

사용자 보고: **"파일 입력창이 사라졌어"**.

**원인** — v0.5.6 에서 추가한 Mermaid prompt 안 `summary.js:337` 의 backtick escape 쌍이 깨져, JS template literal 이 prompt 중간에 종료되고 그 아래 함수 본문이 garbled. `node --check` 는 lenient 해서 통과했지만 **ESM import 시 "Invalid or unexpected token" 으로 module load 자체가 실패** → `createFileInput(dropZone, ...)` 도 호출 안 됨 → 빈 dropZone.

**문제 라인**:
```
- 라벨 안 **특수문자 금지** (괄호 \`()\`, 따옴표 \`"\`, 꺾쇠 \`<>\`, 백틱 \`\``\`)
                                                                       ↑↑↑ 여기 \` \` 뒤 raw ` 가 template literal 을 닫아버림
```

**Fix** — backtick 을 prompt 안에서 코드 폰트로 표시할 필요 X. 단순 단어로 교체. 다른 inline code 백틱 도 안전한 단순 표현으로 정리:
```
- 라벨 안 **특수문자 금지** (괄호, 따옴표, 꺾쇠, 백틱 사용 X)
- 줄바꿈은 HTML br 태그 사용 (예: A[원인<br>화석연료])
- 엣지: A --> B (단순), A -->|조건 라벨| B (조건부)
- 그룹화 필요하면 subgraph 이름 ... end
```

검증: `node --input-type=module import('./summary.js')` 가 "document is not defined" 까지 도달 (browser 에서는 정상 동작).

### 교훈 — `node --check` 만으로는 ESM 호환성 검증 부족

`node --check` 는 syntax 만 lenient 하게 확인. 진짜 ESM import 시 parser 가 더 엄격. 앞으로 prompt 안에 backtick 들어가는 경우 **단순 단어** 로 표현하거나, 정말 필요하면 `String.raw` 또는 escape 짝수 갯수 확인 (`\`x\`` 형식 유지).

### cache-bust

- `summary.js?v=0.5.7`
- summary + 메인 버전 배지 v0.5.7

---

## v0.5.6 — 2026-05-24 (PDF 요약 — 온톨로지 상시 적용 + 📊 논리 흐름 Mermaid 다이어그램)

### 🔧 Changed — 온톨로지 = 기본 ON, 옵션 제거 (사용자 결정)

사용자 결정: **"온톨로지를 사용자 선택메뉴에서 없애고 기본적으로 무조건 적용되게"**.

v0.5.4 ~ v0.5.5 에서 온톨로지가 옵션 (체크박스) 으로 들어가 있었지만, 실제 결과 품질이 일관적으로 더 우수해서 **기본값으로 상시 적용** 으로 변경. 사용자가 켜고 끄지 않아도 자동.

- `index.html` — `<input id="optOntology">` 체크박스 제거
- `summary.js` — `optOntology` 참조 모두 삭제, `buildOntologyPrompt` 가 유일한 `buildPrompt` 로 통합
- 추가 옵션 fieldset 아래에 안내 박스 (`.cmp-scope-note`) 신설 — 온톨로지 + 논리 흐름도 자동 적용 안내

### 🆕 신규 — 논리 흐름 인포그래픽 (Mermaid flowchart)

사용자 아이디어: **"온톨로지를 활용해서 문서에서 논리의 흐름을 인포그래픽이나 순서도 형태로 가시화"**.

요약은 텍스트지만, 본문의 **논리 구조 자체** 는 그림이 훨씬 직관. AI 가 본문 분석해 다음 패턴 중 1~2개 선택 후 Mermaid `flowchart TD` 로 출력:

- **인과 사슬** (원인 → 결과 → 후속 영향)
- **전제-주장-근거** (전제 → 주장 → 근거 1·2·3 → 결론)
- **문제-해결** (문제 → 분석 → 대안 → 채택안 → 기대 효과)
- **시간·절차 순서** (단계 1 → 단계 2 → ... → 완료)
- **분류·계층** (상위 개념 → 하위 갈래)
- **비교·대조** (입장 A vs 입장 B → 결정)
- **목적-수단** (목표 → 전략 → 세부 실행)

#### 출력 구조 강제

Gemini 응답이 무조건 아래 순서:
```
🏷 감지된 도메인: ... (선택 근거: ...)

## 📊 논리 흐름
```mermaid
flowchart TD
    A[노드1] --> B[노드2]
    B --> C{분기}
    C -->|조건| D[결과]
```

## 핵심 요약
... (요약 본문)
```

클라이언트가:
1. 첫 줄 도메인 → 보라색 chip 분리
2. mermaid 코드블록 추출 → 별도 `.mermaid-wrap` 박스에 인포그래픽 SVG 렌더
3. 그 아래 요약 본문은 마크다운 lite 로 렌더

#### Mermaid 라이브러리 lazy load

- CDN: `https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs`
- 도구 진입 시점 X, **첫 요약 결과 렌더 시점에 처음 import** — 도구 로딩 부담 최소화
- 다크 친화 테마 자동 적용 (`themeVariables` 풀 커스텀: 노드 배경·텍스트·엣지 컬러 모두 다크 톤)
- 렌더 실패 시 mermaid 원본 코드를 pre 로 폴백 (사용자가 알아볼 수 있게)

#### prompt 핵심 규칙 (안정성)

- 노드 ID 영문 대문자, 라벨 한국어
- 노드 5~12개 (가독성)
- 노드 라벨 안 특수문자 (`()`, `"`, `<>`, 백틱) 금지 — 줄바꿈은 `<br>`
- 모양: `A[직사각형]` / `C{분기}` / `Z([둥근 종료])`
- 엣지: `A --> B` 또는 `A -->|조건| B`
- subgraph 그룹화 허용

### 🆕 CSS `.mermaid-wrap` + `.mermaid-title`

- 파란/보라 그라데이션 배경 (`linear-gradient(135deg, rgba(59,130,246,0.08), rgba(167,139,250,0.06))`)
- 파란 테두리 (`rgba(96, 165, 250, 0.28)`)
- 다이어그램 SVG `max-width: 100%; height: auto` → 반응형
- mermaid 가 직접 박는 `.nodeLabel` / `.edgeLabel` 색을 다크 친화로 보정 (`#f1f5f9` 텍스트, 엣지 라벨 배경 `rgba(30,41,59,0.85)`)
- 가로 스크롤 (`overflow-x: auto`) — 큰 다이어그램 대응

### UI 단순화

요약 도구 fieldset:

```
[Before v0.5.5]
☐ 🧬 온톨로지 기반 요약 (옵션)
☑ 핵심 키워드 추출
☑ 한국어로 요약

[After v0.5.6]
☑ 핵심 키워드 추출
☑ 한국어로 요약

(아래 안내 박스)
🧬 온톨로지 + 📊 논리 흐름도 자동 적용
- AI 가 도메인 자유 명명 → 핵심 개체 추출
- 본문 논리 구조 → Mermaid 순서도 생성
- 다이어그램 + 요약 본문 같이 출력
```

결과 미리보기 라인:
```
▶ 페이지 N쪽 · 길이: 보통 · 형식: 마크다운 · 🧬 온톨로지 + 📊 논리 흐름도 · 키워드 추출 · 한국어 요약 → Gemini gemini-3.5-flash 호출
```

### 메인 랜딩

- PDF 요약 카드 설명: `"Gemini AI 자동 요약 + 🧬 온톨로지 + 📊 논리 흐름 다이어그램 (Mermaid 인포그래픽)"`
- phase-note: `"카드 22/24 라이브. PDF 요약 🧬 온톨로지 + 📊 논리 흐름 다이어그램 (Mermaid 인포그래픽) 상시 적용"`

### cache-bust

- `pdftools.css?v=0.5.6`, summary 자원 모두 `?v=0.5.6`
- 메인 + summary 버전 배지 v0.5.6

### 한계 / 다음 단계

**v0.5.6 한계**:
- Mermaid syntax 에 모델이 가끔 특수문자 섞으면 렌더 실패 (그땐 코드를 pre 로 폴백)
- 다이어그램 1개만 (subgraph 로 그룹화는 가능). 다중 다이어그램 (논리 흐름 + 시간선 + 계층) 분기는 없음
- 매우 짧은 PDF (1쪽 정도) 도 무조건 다이어그램 — 가끔 과한 시각화

**v0.5.7+ 후보**:
- 다이어그램 다운로드 (SVG / PNG)
- 다이어그램 줌·팬 (큰 흐름 탐색용)
- 다이어그램 hover 시 본문 해당 섹션 하이라이트

---

## v0.5.5 — 2026-05-24 (온톨로지 — 6 고정 도메인 제거, AI 자유 결정)

### 🔧 Changed — 도메인 AI 가 자유롭게 결정 (사용자 피드백)

사용자 지적: **"도메인도 그냥 AI 한테 결정시키면 안됨?"**. 정확한 지적.

v0.5.4 는 6개 고정 도메인 (policy/research/legal/business/news/generic) 중 AI 가 1개 선택. 6개에 안 맞는 PDF (회의록·매뉴얼·진단서·시험문제 등) 는 어색하게 generic 으로 떨어짐.

**v0.5.5** = 6 도메인 고정 자체 제거. AI 가:
1. **도메인 이름 자유 명명** — "외교부 보도자료", "임상시험 결과 보고서", "도서관 운영 매뉴얼" 처럼 본문 맞춤
2. **핵심 개체 카테고리 자유 정의** — 도메인 특성에 맞게 4~6개
3. **섹션 구조 자유 결정** — 도메인 흐름에 맞게
4. **종결·문체 자유 선택** — 정책 명사형 / 학술 서술형 / 법률 단정형 등

### 핵심 변경

| 항목 | v0.5.4 | v0.5.5 |
|---|---|---|
| 도메인 | 6개 고정 (policy/research/legal/business/news/generic) | **AI 자유 명명** |
| prompt 크기 | ~5KB (6 도메인 명세 직렬화) | **~1KB** |
| 적합도 (회의록·매뉴얼·진단서 등) | generic 으로 떨어짐 | 본문 맞춤 |
| 결과 일관성 | 같은 종류 동일 분류 | 같은 종류라도 다른 명명 가능 (트레이드오프) |

### 코드 정리

- `const DOMAINS = { ... }` 상수 (40줄) **제거**
- `buildOntologyPrompt()` 본문 단순화 (도메인 명세 직렬화 → 자유 명명 예시 한 문단)
- prompt 안에 도메인 예시 (정책 보도자료 / 학술 논문 / 계약서 / 회의록 / 매뉴얼 / 진단서 / 강의 노트 등) 보여줘 모델이 자연스럽게 패턴 학습
- 핵심 개체 예시도 도메인 종류별 (계약서면 ... / 회의록이면 ... / 매뉴얼이면 ...) 으로 보여줘 가이드

### UI

옵션 라벨 갱신:

```
☐ 🧬 온톨로지 기반 요약
   AI 가 본문 보고 도메인 자유 명명 → 도메인별 핵심 개체 추출
   → 누락 없는 요약 (회의록·매뉴얼·진단서 등 어떤 문서든)
```

### cache-bust

`pdftools.css?v=0.5.5`, summary 자원 모두 `?v=0.5.5`. 버전 배지 v0.5.5

---

## v0.5.4 — 2026-05-24 (PDF 요약 — 🧬 온톨로지 기반 도메인 자동 감지 MVP)

### 🆕 신규 — 온톨로지 기반 요약 모드 (옵션 C 사용자 결정)

사용자 아이디어: **"온톨로지 기반 요약 기법 적용"**. 기존 자유 요약은 매번 다른 부분이 빠질 수 있음 (일관성 X). 온톨로지 = **개체-관계 구조** 를 먼저 추출 후 요약 → 누락 ↓ 사실 정확도 ↑.

#### 6 도메인 정의

| 도메인 | 핵심 개체 | 종결·문체 | 권장 섹션 |
|---|---|---|---|
| **policy** 정책·보도자료 | 정책명·부처·법령·대상·일정·예산 | 명사형 (~마련, ~수립) | 배경 → 내용 → 대상·일정·예산 → 향후 |
| **research** 학술·연구 | 주제·저자·가설·방법·결과·한계 | 서술형 (~한다, ~로 나타났다) | 배경·목적 → 방법 → 결과 → 논의 → 한계 |
| **legal** 법률·계약 | 당사자·대상·의무·권리·기간·금액·페널티 | 단정형 (~한다, ~지급한다) | 당사자·대상 → 의무·권리 → 기간·금액 → 조항 → 위반 시 |
| **business** 사업·보고서 | 사업명·조직·예산·일정·KPI·리스크 | 실용·실행 서술형 | 목적 → 추진 내용 → 예산·자원 → 일정 → 리스크 |
| **news** 뉴스·기사 | 사건·인물·시점·핵심 사실·발언·영향 | 객관 보도 (~했다) | 요지 → 배경 → 주요 사실 → 발언 → 영향 |
| **generic** 일반·기타 | 주제·개념·핵심 정보 | 균형 서술형 | 핵심 요지 → 주요 내용 → 관련 정보 |

#### 처리 방식 (단일 Gemini 호출 + 3단계 사고 유도)

```
[1단계] 본문 → 도메인 식별 (6개 중 1개)
[2단계] 도메인 핵심 개체 추출 (내부 사고만, 출력 X)
[3단계] 권장 섹션 + 종결 문체로 요약 작성
```

prompt 안에 6 도메인 명세 + 강제 출력 규칙:
- 첫 줄: `🏷 감지된 도메인: {이름} (선택 근거: ~~)` ← 클라이언트가 chip 분리 렌더
- 그 다음 줄부터: 마크다운 요약 본문

#### UI 변경

추가 옵션 fieldset 에 새 토글:

```
☐ 🧬 온톨로지 기반 요약
   도메인 자동 감지 (정책·연구·법률·사업·뉴스·일반) → 도메인별 핵심 개체 추출
   → 일관 누락 없는 요약
```

기본값 **OFF** (기존 자유 요약 동작 보존). 사용자가 켜야 적용. 결과 미리보기 라인에 "🧬 온톨로지 (도메인 자동)" 표시.

#### 결과 시각화

- 응답 첫 줄 `🏷 감지된 도메인: ...` → 보라색 chip 으로 분리 (`.ontology-chip`)
- 그 아래 본문은 기존 마크다운 lite 렌더 그대로
- plain 텍스트 모드여도 chip 은 별도 노출

### 🆕 CSS `.ontology-chip`

- 보라 톤 (`rgba(167, 139, 250, 0.18)` 배경 + `#c4b5fd` 텍스트) — 다크 친화
- pill 형태 (border-radius 999px)
- 요약 본문 위 14px 여백

### 한계 / 다음 단계

**v0.5.4 MVP 한계**:
- 단일 호출 — 도메인 감지가 잘못되면 전체 요약 잘못
- 도메인 명세가 prompt 안에 모두 노출 (토큰 ~5KB 추가). Gemini 1.5 Flash 입력 1M 토큰이라 부담 X
- 사용자 도메인 override X (다음 패치 후보)
- 개체 추출 결과 시각화 X (다음 패치 후보)

**v0.5.5+ 후보**:
- 2단계 호출 (1단계 = 도메인 + 개체 list, 2단계 = 본문 + 1단계 결과 → 요약)
- 도메인 사용자 override (자동 감지 외에 직접 선택)
- 개체 카드 시각화 (정책명·부처·날짜·금액 별도 박스)
- 도메인별 schema 시각화 (관계 다이어그램)

### cache-bust

- `pdftools.css?v=0.5.4`, summary 자원 모두 `?v=0.5.4`
- 버전 배지 v0.5.4

---

## v0.5.3 — 2026-05-24 (PDF→MD 에 Gemini 추가 + 카테고리 라벨 정리)

### 🔧 Changed — PDF→MD 도구의 [API 설정] 에 Gemini 도 노출

사용자 피드백. v0.5.2 에서 `filterKeys: ['replicateWorkerUrl']` 로 했었지만 Marker 의 `use_llm` 옵션 또는 후속 워크플로 (PDF→MD → 다른 AI 도구) 를 위해 Gemini 키도 같이 보이게:

```js
// v0.5.3
filterKeys: ['replicateWorkerUrl', 'geminiApiKey']
```

### 🔧 Changed — 카테고리 라벨에서 카운트·Phase 정보 제거

사용자 피드백: "각 카테고리마다 몇개 페이즈1 이런부분 이제 필요없을것 같아".

**Before**:
```
● 페이지 작업    8개 · Phase 1 ─────────
● 콘텐츠 편집   6개 · Phase 2 ─────────
● 추출·변환    4개 · Phase 1~3 ───────
● 기타 도구    6개 · Phase 1·4·5 ────
```

**After**:
```
● 페이지 작업 ───────────────────
● 콘텐츠 편집 ───────────────────
● 추출·변환  ───────────────────
● 기타 도구  ───────────────────
```

사용자는 카드를 보면 알 수 있는 정보 (개수) + 사용자에게 의미 X 한 내부 정보 (Phase 번호) 노출 X.

### 🔧 phase-note 단순화

"🚧 Phase 5 진행 (v0.5.2) — 요약 분량 2배 + ..." → "🚧 개발 중 (v0.5.3) — 카드 22/24 라이브. 남은 2: PDF 뷰어 hub · PDF 번역"

사용자 시점에 필요한 정보 (개발 진행 + 라이브 카드 수 + 남은 도구) 만.

### cache-bust

`pdftools.css?v=0.5.3`. CSS 자체는 변경 X 지만 메인 index.html 의 카드 그리드 변경 + 버전 일관성 위해 일괄.

---

## v0.5.2 — 2026-05-24 (PDF 요약 분량 2배 + API 설정 헤더 재정비)

### 🔧 Changed — PDF 요약 분량 2배 (사용자 피드백)

v0.5.1 의 요약 결과가 "요약은 요약인데 너무 요약됨". 사용자 요청: 모든 길이 2배 + UI 라벨에 줄 수 미리 표시.

| 옵션 | Before (v0.5.1) | After (v0.5.2) |
|---|---|---|
| 짧게 | 3~5 불릿 (~150자) | **6~10 불릿 (~300자, 약 10~15줄)** |
| 보통 (기본) | 섹션 1~3 + 3~5 항목 (~400자) | **섹션 2~4 + 6~10 항목 (~800자, 약 25~35줄)** |
| 상세 | 섹션 3~5 + 디테일 (~800자) | **섹션 4~6 + 디테일 (~1,600자, 약 50~70줄)** |

UI 라벨에 줄 수 미리 표시 (사용자가 선택 전에 분량 가늠 가능):
```
○ 짧게   약 10~15줄 · 6~10 불릿 (~300자)
● 보통 · 추천   약 25~35줄 · 섹션 2~4개 + 각 6~10 불릿 (~800자)
○ 상세   약 50~70줄 · 섹션 4~6개 + 디테일 (~1,600자)
```

### 🆕 shared/api-keys.js v1.4.4 — `filterKeys` 옵션

기존 `filterApp` 만으로는 한 앱이 여러 키를 쓸 때 도구별 분리가 안 됨 (예: pdftools 가 Replicate Worker + Gemini 둘 다). 새 옵션:

```js
// 메인 헤더 — 앱 전체 키
openModal({ filterApp: 'pdftools' });

// 서브 도구 — 자기 키만 (v1.4.4+)
openModal({ filterKeys: ['geminiApiKey'] });        // summary
openModal({ filterKeys: ['replicateWorkerUrl'] });  // to-markdown
```

- `filterKeys` 가 `filterApp` 보다 우선
- 헤더 부제: "(Gemini API Key — 이 도구 전용 · 다른 키는 PDF 딸깍 메인의 [API 설정] 에서)"

### 🆕 pdftools 메인 헤더 [API 설정] 버튼 (부활)

v0.3.10 에서 만들었다가 v0.3.13 OCR 폐기로 제거됐던 버튼 부활. 이번엔 pdftools 가 쓰는 모든 키 (`filterApp: 'pdftools'`) 표시:
- Replicate Worker URL (PDF→마크다운)
- Gemini API Key (PDF 요약, 곧 번역도)
- (향후 추가되는 키 자동 노출)

랜딩 페이지의 [API 키 통합 관리] 와 동일한 모달, 다만 pdftools 가 쓰는 키만 필터링.

### 🔧 서브 도구 [API 설정] 도 도구별 키 1개만

- `to-markdown`: `filterKeys: ['replicateWorkerUrl']`
- `summary`: `filterKeys: ['geminiApiKey']`

사용자가 한 도구를 쓰면서 다른 도구 키를 실수로 건드릴 가능성 ↓. "이 도구에 필요한 키는 이것" 이 명확.

### cache-bust

- `pdftools.css?v=0.5.2`, summary/index.html + summary.js `?v=0.5.2`
- `shared/api-keys.js?v=1.4.4` 일괄 (5개 페이지: 루트 / image-editor / WeeklyBrief / ai-debate / pdftools 메인 / pdftools/tools/to-markdown / pdftools/tools/summary)

---

## v0.5.1 — 2026-05-24 (Phase 5 #1 — PDF 요약 도구)

### 🆕 신규 도구 — PDF 요약 (`tools/summary/`)

pdf.js 로 본문 텍스트 추출 → Gemini API 호출 → 마크다운 요약. WeeklyBrief 의 Gemini 호출 패턴 reuse (model: `gemini-3.5-flash`, `GDIApiKeys.get('geminiApiKey')`).

**옵션**:
- **요약 길이**: 짧게 (3~5 불릿, ~150자) / 보통 (섹션 + 8~12 항목, ~400자) / 상세 (섹션 + 디테일, ~800자)
- **출력 형식**: 마크다운 / 일반 텍스트 (.md / .txt 다운로드)
- **핵심 키워드 추출** (5~8개, 끝줄)
- **한국어로 요약** (외국어 PDF → 한국어 변환, 해제 시 원문 언어 유지)
- **페이지 범위** (createPageRangeInput, 전체 또는 선택)

**처리 흐름**:
1. PDF 로드 + 페이지 범위 자동 인식
2. 사용자가 옵션 조정 → "요약 시작"
3. pdf.js getTextContent 로 선택 페이지 텍스트 추출 (페이지 사이 구분선 `==== N쪽 ====`)
4. Gemini `generateContent` 호출 (temperature: 0.4, responseSchema X 자유 마크다운)
5. 결과: 카드에 마크다운 lite 렌더 (헤더·불릿·번호리스트·강조·코드) 또는 plain text pre
6. 다운로드: `.md` 또는 `.txt`

**비용**:
- 사용자 본인 Gemini API 키 (`shared/api-keys.js` 의 `geminiApiKey`)
- **무료 티어 충분** — 분당 15 RPM, 일당 1500 RPD (gemini-3.5-flash 기준)
- 안내 박스에 명시 + [API 설정] 헤더 버튼

**안전망**:
- 추출 텍스트 < 50자 → "스캔 PDF 가능성 — PDF→마크다운 (Marker OCR) 으로 먼저 변환 권장" 안내
- 본문 > 250,000자 → 자동 잘림 (Gemini 1.5 Flash 입력 한도 안전 마진)
- HTTP 429 → "잠시 후 다시 시도 또는 페이지 범위 줄이기" 안내
- API_KEY_INVALID → 키 재확인 안내

### 🔧 shared/api-keys.js v1.4.3

`geminiApiKey` 의 `apps` 에 `{ id: 'pdftools', name: 'PDF 딸깍 (PDF 요약·번역)' }` 추가. 향후 v0.5.2 PDF 번역도 같은 키 공유.

### 🆕 새 마크다운 lite 렌더러 (`renderMarkdownLite`)

도구 내부용 단순 마크다운 → HTML 변환 (라이브러리 의존 X). 지원:
- `## 헤더` (h1~h6, 자동 레벨링)
- `- 불릿` / `* 불릿`
- `1. 번호 리스트`
- `**강조**` / `*이탤릭*` / `` `코드` ``
- 빈 줄 = 단락 구분

장점: 외부 마크다운 라이브러리 추가 없이도 결과 즉시 시각화. 단순 케이스만 다루므로 약 70줄.

### 🔧 변경

- `pdftools/index.html`: PDF 요약 카드 soon → live. 카드 **22/24** 라이브 (남은 2: 뷰어 + 번역)
- phase-note: "Phase 5 진행 (v0.5.1)"
- cache-bust: `pdftools.css?v=0.5.1`, summary 자원 `?v=0.5.1`
- cache-bust `?v=1.4.3` 일괄 갱신 (5개 페이지: 루트 / image-editor / WeeklyBrief / ai-debate / to-markdown)

### 📌 다음 작업 (Phase 5 남은 1개)

- **PDF 번역** (v0.5.2) — Gemini 또는 DeepL API. 페이지 범위 + 원문/번역문 나란히 표시 + 다운로드
- 그 후 **Phase 4 #3 PDF 뷰어 hub** (v0.6.0) → **v1.0.0 정식 출시 + 랜딩 카드 노출**

---

## v0.5.0 — 2026-05-24 (★ Phase 4 완료 + 카테고리 통합 → 기타 도구)

### 🎉 Phase 4 완료

Phase 4 결과 정리:
| 도구 | 패치 | 상태 |
|---|---|---|
| **PDF 비교** | v0.4.1 → v0.4.3 (다크 + 텍스트 전용 안내) | ✅ |
| **이미지 압축** | v0.4.4 | ✅ |
| PDF 뷰어 hub | Phase 4 #3 보류 — Phase 5 후 v1.0 직전에 마지막으로 (hub 가 가리킬 곳 모두 안정화된 시점) | ⏳ |

### 🔧 Changed — 카테고리 4개 통합 (사용자 결정)

기존 6개 카테고리 중 마지막 3개 (각각 2 카드씩만) 가 분량 작아 산만해 보임. **하나로 통합**:

| Before (6 카테고리) | After (4 카테고리) |
|---|---|
| ① 페이지 작업 (8) | ① 페이지 작업 (8) |
| ② 콘텐츠 편집 (6) | ② 콘텐츠 편집 (6) |
| ③ 추출·변환 (4) | ③ 추출·변환 (4) |
| ④ 최적화·보안 (2) | **④ 기타 도구 (6)** — 통합 |
| ⑤ 뷰어·비교 (2) | |
| ⑥ AI 도구 (2) | |

**기타 도구 카드 순서** (사용자 지정):
1. PDF 뷰어 (hub, 가장 마지막 구현)
2. PDF 비교
3. 이미지 압축
4. 잠금 풀기
5. PDF 요약 (Phase 5 진입 중)
6. PDF 번역

### 🎨 새 색 토큰

- `--cat-misc: #94a3b8` (slate-400, 중립 회색 톤) — 기타 도구 카테고리 dot
- 기존 `--cat-optimize`, `--cat-viewer`, `--cat-ai` 는 legacy 호환 위해 그대로 유지 (도구 페이지 내부 dot 가 직접 참조하는 경우)

### 🚀 Phase 5 진입

Phase 5 = **AI 도구 2개** (요약 + 번역) → 완성 시 **v1.0.0 정식 출시 + 랜딩 카드 노출**.

#### 첫 도구 (v0.5.1): PDF 요약
- pdf.js getTextContent 로 본문 추출
- Gemini 또는 Claude API 호출 (shared/api-keys.js 활용)
- WeeklyBrief 의 4섹션 요약 패턴 reuse 가능

### 변경 사항 요약

- 코드 변경: 카테고리 통합 (3 section → 1), pdftools.css 의 `--cat-misc` 토큰
- 카운트 변동 X: 카탈로그 24, 카드 21/24 (PDF 뷰어 + 요약 + 번역 = 3개 남음)
- cache-bust: `pdftools.css?v=0.5.0`, 버전 배지 v0.5.0
- phase-note 갱신

---

## v0.4.4 — 2026-05-24 (Phase 4 #2 — 이미지 압축 도구)

### 🆕 신규 도구 — 이미지 압축 (`tools/compress-images/`)

PDF 안 **JPEG image XObject** 를 다운샘플 + 품질 재인코딩으로 용량 절감. 텍스트·표·서식은 그대로 유지 (검색·복사 영향 X).

**핵심 기술** — pdf-lib indirect objects 트래버스 + PDFRawStream in-place 교체:

```js
// 1. 모든 indirect objects 트래버스 → image stream 찾기
ctx.indirectObjects.forEach((obj, ref) => {
  if (obj instanceof PDFRawStream && obj.dict.get(PDFName.of('Subtype')).encodedName === '/Image') {
    // Filter 가 /DCTDecode 면 JPEG
  }
});

// 2. JPEG bytes → canvas 다운샘플 → 새 JPEG bytes
// 3. 새 PDFRawStream 만들기 (메타 dict + 새 bytes)
// 4. context.assign(oldRef, newStream) — 페이지 ref 손댈 필요 X
```

**옵션 (사용자 검수)**:
- **JPEG 품질** 슬라이더 40~95 (기본 75)
- **최대 너비** 원본/2400/1920(기본)/1280/960 px
- **최소 크기** 0/20/50(기본)/100/200 KB — 작은 아이콘 보존

**처리 흐름**:
1. PDF 로드 후 자동 분석 → "이미지 N개 (JPEG M개, 기타 L개 건너뜀)" 표시
2. 사용자가 옵션 조정 (실시간 미리보기)
3. 압축 실행 → 페이지별 진행률 + 압축 후 더 커지면 원본 유지 (안전)
4. 결과: 원본 → 압축 후 MB + 절감 % + 처리 통계

**처리되는 것 / 안 되는 것** (안내 박스 명시):
- ✅ JPEG (DCTDecode) 다운샘플 + 재인코딩
- ⏭ PNG·CCITTFaxDecode·JBIG2·ImageMask — 안전 skip (원본 유지)
- ⏭ 텍스트·표·서식·메타데이터 — 그대로

**기술 한계**:
- DCTDecode JPEG 만 (가장 흔한 케이스 — 사진·스캔본은 거의 다 JPEG)
- ImageMask: true 인 이미지 skip
- 압축 후 크기가 더 커지면 원본 유지 (자동 안전장치)

### 🔧 변경

- `pdftools/index.html`: 이미지 압축 카드 soon → live. 최적화·보안 2/2 완성. 카드 **21/24** 라이브 (남은: PDF 뷰어 hub · PDF 요약·번역)
- phase-note + 버전 배지 v0.4.4
- cache-bust: pdftools.css `?v=0.4.4`, compress-images 자원 `?v=0.4.4`

### 📌 분모 정정 (21/22 → 21/24)

이전 PATCHNOTE 의 카탈로그 카운트가 일부 부정확. 실제 카테고리별 카드 수:
- 페이지 작업 8 + 콘텐츠 편집 6 + 추출·변환 4 (이미지 추출은 뷰어 통합) + 최적화·보안 2 + 뷰어·비교 2 + AI 도구 2 = **24**
- 라이브: 8+6+4+2+1+0 = **21**
- 남은 3: PDF 뷰어 hub (Phase 4 #3, hub 역할 — 다른 도구 안정화 후), PDF 요약·번역 (Phase 5)

### 📌 다음 작업 후보

- **PDF 뷰어 hub** (Phase 4 #3) — 가장 큰 작업. 다른 도구 호출 + 이미지 추출 내장. 모든 도구 안정화된 지금이 적기
- **Phase 4 회고 (v0.5.0)** → **Phase 5 (AI 도구)** — 요약·번역. 뷰어는 hub 라 v1.0 직전 마지막에 만들어도 됨

---

## v0.4.3 — 2026-05-24 (PDF 비교 — "텍스트만 비교" 안내 강조)

### 🔧 Changed — 비교 범위 한정 명시 (사용자 기대치 alignment)

사용자 피드백: "표·이미지 비교 못 하는 건 그냥 넘어가더라도, 설명에 텍스트만 비교한다는 점을 강조할 필요는 있겠다."

**카드 desc**: "두 PDF 텍스트 페이지별 diff (추가·삭제 시각 표시)" → "두 PDF 의 **텍스트만** 페이지별 diff (표·이미지·레이아웃은 비교 X)"

**tool-desc**: "텍스트 (텍스트 레이어)" 강조 + 별도 노란 안내 박스 추가:

```
⚠ 비교 범위 한정 — 이 도구는 텍스트만 비교합니다. 다음은 감지되지 않음:
  · 표 안 셀 내용 (텍스트로 추출되는 부분만 일반 텍스트와 함께 비교됨 — 표 구조 차이는 X)
  · 이미지·그래프·도형
  · 페이지 레이아웃·서식 (글자 크기·색·정렬·줄간 등)
텍스트 레이어가 없는 스캔 PDF 는 결과가 비어 나옵니다 (PDF 딸깍의 OCR 도구 또는
PDF→마크다운 변환 후 비교 권장).
```

### 🆕 새 CSS 클래스

- `.cmp-scope-note` — 노란 톤 (var(--warn) 좌측 막대 + 옅은 노랑 배경), 다크 테마 친화. 내부 `<ul>` 도 톤 일치

### cache-bust

- `pdftools.css?v=0.4.3` (단 새 클래스 1개만 추가, 기존 영향 X)
- `tools/compare/index.html` + `compare.js` 안 자원 모두 `?v=0.4.3`

---

## v0.4.2 — 2026-05-24 (PDF 비교 hotfix — 다크 테마 가시성)

### 🐛 Fix — 다크 테마에서 결과 영역 안 보임

v0.4.1 의 compare.js / index.html 에 **인라인 스타일로 라이트 톤 색상 (white 배경, #222 텍스트, 옅은 ins/del 색) 박힘** → pdftools 의 다크 테마 (`--bg: #1a1a1a`, `--text: #ebebee`) 에서 카드 배경·텍스트 거의 안 보이는 상태.

**Fix** — 모든 인라인 스타일 제거 + CSS 토큰 변수 기반 클래스:

| 변경 | Before | After |
|---|---|---|
| 요약 카드 | `bg:#eef4fb` | `bg:var(--panel)` + 좌측 4px `--accent` 강조 막대 |
| 페이지 details | `bg:white` | `bg:var(--panel)`, summary 는 `--panel-hover` |
| 페이지 본문 | `bg:white` | `bg:var(--bg)` (살짝 더 어두워서 details 안에서 구분) |
| 변동 없음 행 | `bg:#f5f7fa` | `bg:rgba(255,255,255,0.02)` (다크 위 살짝 떠 보임) |
| `<ins>` (추가) | 옅은 초록 `#d4edda` | `rgba(74,222,128,0.22)` + 색 `#b6f0c8` (다크 대비) |
| `<del>` (삭제) | 옅은 붉음 `#f8d7da` | `rgba(248,113,113,0.22)` + 색 `#fec5c5` + 취소선 |
| 동일 메시지 | `color:#28a745` | `var(--success)` + 점선 보더 박스 |

### 🆕 새 CSS 클래스 (`.cmp-*`)

- `.cmp-summary` (+ `.pill-ins`, `.pill-del`, `.warn-note`)
- `.cmp-page` (details) + `summary::before` 토글 화살표 자체 구현 (다크에서 기본 marker 보기 어려움)
- `.cmp-page .page-stat` / `.cmp-page .page-body`
- `.cmp-page-nochange` (변동 없는 페이지 한 줄)
- `.cmp-diff-ins` / `.cmp-diff-del` (인라인 diff)
- `.cmp-empty-msg` (모든 페이지 동일 시)

### 🔧 Changed

- `compare.js` 의 `renderResults()` + `renderDiffHTML()` 인라인 스타일 모두 제거 → 클래스만
- `index.html` 의 tool-desc 안내 색 샘플도 `.cmp-diff-ins`/`.cmp-diff-del` 클래스 사용
- HTML 다운로드 (별도 .html 파일) 는 **라이트 톤 유지** — 사용자가 인쇄·공유·이메일 첨부 용으로 쓸 거라 일반 white 배경이 자연스러움

### cache-bust

- `pdftools.css?v=0.4.2`
- `tools/compare/index.html` + `compare.js` 안 모든 자원 `?v=0.4.2`

---

## v0.4.1 — 2026-05-24 (Phase 4 #1 — PDF 비교 도구)

### 🆕 신규 도구 — PDF 비교 (`tools/compare/`)

두 PDF (A 원본 / B 변경본) 의 텍스트를 페이지별로 1:1 매칭하여 시각 diff. 보고서 개정판 검토, 계약서 변경 확인 등에 활용.

**핵심 기술**:
- `pdf.js getTextContent()` 로 각 페이지 텍스트 추출
- `diff-match-patch` (Google, Apache 2.0, CDN 로드) 로 텍스트 diff
- `dmp.diff_main()` → `[op, text]` 배열. op: -1=DELETE, 0=EQUAL, 1=INSERT
- `dmp.diff_cleanupSemantic(diffs)` 로 의미 단위 묶기 (단어·문장)

**UI**:
- 좌측 PDF A / 우측 PDF B (두 dropZone, `cmp-pair` grid 2 col)
- 두 PDF 모두 로드되면 옵션 + 비교 버튼 활성
- 페이지 수 다르면 공통 페이지만 비교 + 경고 안내
- 결과:
  - 요약 카드 (변경 페이지 수, +N자 / -N자 통계)
  - 페이지별 details (변경 페이지는 펼침, 변동 없음은 작은 줄 한 줄)
  - **시각 표시**: `<ins>` (초록 배경) / `<del>` (붉은 배경 + 취소선)

**옵션**:
- **변경된 페이지만 표시** (기본 ON) — 동일 페이지는 한 줄로 압축
- **의미 단위로 묶기** (기본 ON, `diff_cleanupSemantic`) — 글자 단위 noise 제거
- **공백·줄바꿈 차이 무시** (기본 OFF) — PDF 변환 노이즈 제거 용. 구조만 비교

**다운로드**:
- `📥 결과 HTML 다운로드` — 스타일 인라인 포함 단일 .html. 브라우저에서 바로 열림. 파일명: `A-vs-B-compare.html`

**한계** (사용자에게 명시):
- 1:1 페이지 매칭만 (페이지 수 다르면 짧은 쪽 끝까지)
- 텍스트 레이어 없는 스캔 PDF 는 빈 결과 (먼저 OCR 도구 권장 — 폐기됐으니 향후 Marker force_ocr 또는 외부 OCR 도구 사용 안내)
- 이미지·표 구조는 비교 X (텍스트만)

### 🔧 변경

- `pdftools/index.html`: PDF 비교 카드 soon → live. 카드 19/21 → **20/21**. 뷰어·비교 카테고리 0/2 → **1/2**
- `pdftools/pdftools.css`: `.cmp-pair` (2 col grid + 800px 미만 1 col) + `.cmp-side` / `.cmp-label` / `.cmp-info` 스타일 추가
- phase-note: "Phase 4 진행 (v0.4.1) — Phase 4 #1 PDF 비교 신규"
- 버전 배지 v0.4.0 → **v0.4.1**
- cache-bust: pdftools.css `?v=0.3.16` → `?v=0.4.1`. tools/compare/ 안 자원 모두 `?v=0.4.1`

### 📌 다음 작업 후보 (Phase 4 남은 2개)

- **이미지 압축** — pdf-lib + canvas 다운샘플. 텍스트 PDF 는 효과 미미 → "이미지 압축" 으로 정직하게
- **PDF 뷰어 hub** — 가장 큰 작업. 다른 도구 호출 + 이미지 추출 내장

---

## v0.4.0 — 2026-05-24 (★ Phase 3 회고 + Phase 4 진입)

### 🎉 Phase 3 완료 — 추출·변환 카테고리 4/4

Phase 1 (페이지 작업 8) + Phase 2 (콘텐츠 편집 6) + Phase 3 (추출·변환 4) = **18 도구 라이브** + Phase 1 의 잠금풀기 1 = **19/21**.

#### Phase 3 결과 요약

| 도구 | 패치 | 결과 |
|---|---|---|
| **PDF → 이미지** | v0.3.1 → v0.3.2 (UI fix) | ✅ JPG/PNG, DPI 선택, zip |
| **이미지 → PDF** | v0.3.3 → v0.3.4 (그리드·정렬·줌) → v0.3.5 (줌 캔버스 재렌더 fix) | ✅ WebP/GIF 변환 자동, 정렬·줌 |
| ~~OCR~~ | v0.3.6 ~ v0.3.13 (7회 시도) | 🗑 **완전 폐기** — 4종 엔진 (Tesseract / Gemini / 한컴 / Surya) 모두 한계 |
| **PDF → 마크다운** | v0.3.14 ~ v0.3.16 | ✅ Replicate Marker (Surya OCR 내장), balanced ≈ 6원/쪽 |
| ~~이미지 추출~~ | v0.3.17 | 📦 **뷰어 hub 안으로 통합** (Phase 4) |

#### Phase 3 인프라 신설

- `shared/thumbnail.js` 의 **줌 인프라** (v0.3.4) — setThumbWidth(w) API + CSS var `--thumb-w` + IO 재등록 debounce
- `shared/thumb-toolbar.js` (v0.3.4) — 그리드/리스트/정렬/줌 통일 컨트롤 (8개 도구 일괄 적용)
- `shared/file-input.js` 의 **middleEllipsis** (v0.3.5) — 긴 파일명을 "앞 + … + 뒤" 로
- 박스 풀 에디터 패턴 (Phase 2 에서 시작) → 텍스트 추가·서명 도구 안정화

#### ★ 박제할 교훈

1. **외부 API 의존 도구의 4가지 진입장벽** (v0.3.13 OCR 폐기에서 학습):
   - (1) 셋업 부담 (2) 키·결제 (3) 한도 (4) 일관 가용성
   - 한 가지라도 어긋나면 "딸깍" 원칙 위배

2. **돌파구 — Cloudflare Worker proxy** (v0.3.14 Marker 부활에서 검증):
   - Worker 가 비용·키·CORS 모두 흡수 → 사용자는 빈 키로 작동
   - image-editor 가 v3.1.x 부터 검증 — pdftools 가 v3.2.0 로 marker 모델 reuse
   - 검증된 패턴: `MODELS` 카탈로그 + `isOcr/isDocConv` 응답 분기

3. **카탈로그는 정적 X** (v0.3.17 이미지 추출 → 뷰어 통합):
   - 사용자 시나리오·기술 한계에 따라 도구를 합칠 수도, 뺄 수도 있음
   - 22 → 21 도 ★ — 도구 수가 많으면 hub 가 의미 있어짐

4. **로컬 도구 vs 클라우드 도구** (사용자 marker pip install 시도 → 단념):
   - Python 3.14 + Pillow 10.x prebuilt wheel 부재 → 컴파일 에러
   - Python 3.12 venv + 5GB 모델 + 30분 대기 = "딸깍" 정신 위배
   - **Worker proxy 가 정답** — 6원/쪽 비용을 Worker 가 흡수하니 사용자는 부담 X

5. **md2hwpx 와의 자연 연결** (v1.0.1):
   - PDF→MD 출력 `.zip` (md + images/) 을 md2hwpx 가 통째로 받음
   - `ImageEmbed.setLocalImages(dict)` 패턴 — 외부 fetch 없이 메모리 dict 우선
   - 이런 **앱 간 자연스러운 워크플로** 가 GDI-Apps 의 차별점

### 🚀 Phase 4 진입 — 최적화·보안 + 뷰어·비교

Phase 4 후보 (dev.md §3.4 + §3.5):

| 도구 | 우선순위 | 작업 부담 | 비고 |
|---|---|---|---|
| **이미지 압축** | ★3 | 중간 | pdf-lib + canvas 다운샘플. PDF 안 이미지만 손댐 — 텍스트 PDF 효과 미미. "이미지 압축" 으로 정직하게 표시 |
| **PDF 뷰어** | ★3 | 큼 | hub 역할 — 다른 도구 호출 + 이미지 추출 내장. **모든 도구가 안정화된 후가 합리적** (hub 가 가리킬 곳이 있어야 의미) |
| **PDF 비교** | ★4 | 중간 | 두 PDF 텍스트 diff (변경 부분 하이라이트). diff-match-patch 라이브러리 |

#### Phase 4 진입 시 첫 도구

다음 패치 (v0.4.1) 부터 본격 구현. 사용자가 첫 도구 선택 대기.

### 변경 사항 요약

- 코드 변경 X (회고·문서·버전 배지만)
- `pdftools/index.html`: 버전 배지 v0.3.17 → **v0.4.0**, phase-note "✅ Phase 3 완료 (v0.4.0) — Phase 4 진입"
- `pdftools/PATCHNOTE.md`: 본 회고 항목
- `pdftools/doc/dev.md`: 현재 상태 행 갱신
- `CLAUDE.md §2.4`: 상태 갱신, 박제 교훈 보강

---

## v0.3.17 — 2026-05-24 (카탈로그 정리 — 이미지 추출은 뷰어 안으로)

### 🗑 Removed (단독 카드) / 🆕 Added (뷰어 내장 기능)

사용자 결정: **"뷰어 내부에서 PDF 보는 와중에 이미지 추출 가능하게"**.

**카탈로그 정리**:
- 추출·변환 카테고리의 **"이미지 추출 (준비 중)" 단독 카드 제거** — 별도 도구로 만들 필요성 낮음
- 뷰어 (§3.5) 의 desc 에 명시: "PDF 뷰어 + 다른 도구 호출 hub **(이미지 추출 등 포함)**"
- Phase 4 뷰어 구현 시 `getOperatorList()` + `paintImageXObject` 패턴으로 PDF 안 image XObject 를 사이드바에 나열·클릭 추출

### 🔧 변경된 수치

| 항목 | Before (v0.3.16) | After (v0.3.17) |
|---|---|---|
| 카탈로그 전체 | 22 | **21** |
| 라이브 도구 | 19 | 19 (변동 X) |
| 추출·변환 카테고리 | 5개 (5/5 완성) | **4개 (4/4 완성)** |
| Phase 3 상태 | 진행 | **마무리** |

### 🔧 Changed

- `pdftools/index.html`: 이미지 추출 카드 HTML 블록 삭제, 카운트 라벨 갱신, 뷰어 카드 desc 보강, phase-note·meta description·버전 배지 0.3.17 로
- `pdftools/doc/dev.md`: 현재 상태 행, v1.0 범위, §3 카탈로그 헤더, §3.3 이미지 추출 행 (취소선 + 뷰어로 이동 메모), §3.5 뷰어 desc 보강

### 📌 다음 작업 후보

- **Phase 3 회고** — v0.3.x 완료 시점 정리 → v0.4.0 으로 minor bump
- **Phase 4 진입** — 이미지 압축 / **PDF 뷰어 (이미지 추출 hub 포함)** / PDF 비교

---

## v0.3.16 — 2026-05-24 (PDF→MD 옵션 디폴트 조정 — 페이지 구분선 off)

### 🔧 Changed — 페이지 구분선 삽입 디폴트 off

사용자 피드백: md2hwpx 워크플로에서 결과 .md 안 페이지 구분선 (`{N}` + 가로줄) 은 한컴 변환 시 노이즈가 된다. 디폴트를 끔으로 변경.

- `optPaginate` 체크박스의 `checked` 속성 제거 → **첫 로드 시 unchecked**
- "비우기"/reset 시에도 unchecked 유지
- 안내문 추가: "md2hwpx 변환 시 결과 깔끔하게 하려면 끄기 권장"

체크박스 자체는 그대로. 페이지 구분선이 필요하면 사용자가 직접 켜면 됨.

### cache-bust

pdftools.css/.js v0.3.15 → v0.3.16. pdftools/index.html 의 phase-note·버전 배지도 갱신.

---

## v0.3.15 — 2026-05-24 (PDF→MD hotfix — 변환 버튼 무반응 + 비용 안내 간소화)

### 🐛 Fix — 변환 버튼 무반응 (critical)

v0.3.14 의 to-markdown.js 에 NodeList flat 버그:

```js
// 🚫 버그
[modeRadios, [optPaginate, optFormatLines, optForceOcr, optNoImages]].flat()
  .forEach(el => el.addEventListener('change', updateResultPreview));
```

`modeRadios` 는 `querySelectorAll` 결과 → **NodeList** (Array X). `Array.prototype.flat()` 은 NodeList 를 펴지 않고 원소로 유지. forEach 첫 원소 = NodeList → `el.addEventListener` 호출 시 TypeError 발생 → **모듈 전체 실행 중단** → `btnConvert.addEventListener` 도 등록 X → "변환" 버튼 무반응.

**Fix**: spread 로 명시 펴기.

```js
// ✅ 수정
[...modeRadios, optPaginate, optFormatLines, optForceOcr, optNoImages]
  .forEach(el => el.addEventListener('change', updateResultPreview));
```

### 🔧 Changed — 비용 안내 간소화 (사용자 피드백)

**Before** (v0.3.14): 본문 중앙에 큰 노란 박스로 "💰 비용 안내 — Replicate marker 호출, balanced ≈ 6원/쪽, 기본 Worker 가 비용 부담 중..." 전체 안내문 노출.

**After** (v0.3.15): 박스 제거. 페이지 범위 입력란 아래 안내문 끝에 **동적 비용 계산** 추가:

```
큰 PDF (수십~수백 쪽) 는 비용·시간 절감을 위해 페이지 범위 제한 권장
                                              (약 12쪽 · 6원/쪽 ≈ 72원)
```

- 파일 로드 시 pdf.js 로 **전체 쪽수 빠르게 추정** (PDF 이면 자동, DOC·PPT·이미지 는 건너뜀)
- 페이지 범위 입력 (`"0,2-4"` 등 Marker 0-indexed) 파싱하여 선택 쪽수 계산
- 페이지 범위 비면 전체 쪽수 사용
- PDF 외 파일 (페이지 수 미상): `(쪽당 6원)` 으로만 표시
- 잘못된 페이지 범위 형식: `(페이지 범위 형식 확인)` 으로 사용자 안내

### 🔧 새 의존성 import (to-markdown.js)

비용 추정 위해 `shared/cdn.js` + `shared/pdf-core.js` import 추가 (페이지 수 분석만).

### 🆕 신규 헬퍼

`countSelectedPages(rangeStr, total)` — Marker `page_range` 문법 ("0,2-4") 파싱하여 선택 쪽수만 반환. 빈 입력 = 전체, 잘못된 입력 = 0.

### cache-bust

- `pdftools.css?v=0.3.15`
- `tools/to-markdown/to-markdown.js?v=0.3.15` (+ 도구 안 모든 shared import 도 `?v=0.3.15`)
- pdftools/index.html 의 버전 배지·phase-note 갱신

### 결과

✅ 변환 버튼 정상 작동. 사용자는 [API 설정] 클릭 없이도 (Marker 모델이 Worker 에 배포되어 있다면) 바로 변환 가능. 비용은 페이지 범위 입력에 따라 실시간 표시 → 사용자가 "이 PDF 는 얼마 들지" 즉시 판단 가능.

---

## v0.3.14 — 2026-05-24 (Phase 3 #4 PDF → 마크다운 — Replicate Marker 부활)

### 🆕 신규 도구 — PDF → 마크다운 (`tools/to-markdown/`)

v0.3.13 인수인계 사항 (옵션 A) 결정. 사용자가 [Replicate datalab-to/marker](https://replicate.com/datalab-to/marker) 단가 확인 — **balanced 기준 250쪽에 $4 (≈ 6원/쪽)** — 부담 적당하다고 판단, Marker 통합 진행.

**핵심 가치**:
- ✅ image-editor 와 동일한 **Cloudflare Worker proxy** (`gdi-replicate-proxy.sobjil.workers.dev`) reuse — 인프라 추가 0
- ✅ Marker 가 **Surya OCR 내장** → 텍스트 PDF + 스캔 PDF 둘 다 cover (force_ocr 토글로 강제 가능)
- ✅ 표·수식·강조 (볼드/이탤릭) 보존 (`format_lines: true`)
- ✅ 이미지도 별도 추출 → `.zip (md + images/)` 으로 묶음
- ✅ md2hwpx 워크플로 연결 (변환된 .md → 한글 HWPX 양식)

**옵션 (사용자 검수 가능)**:
- 품질: **fast / balanced (추천, 기본) / accurate** 3단
- 페이지 구분선 (`paginate`)
- 강조·수식 인식 (`format_lines`)
- OCR 강제 (`force_ocr`) — 스캔본 또는 텍스트 깨진 PDF 용
- 이미지 추출 생략 (`disable_image_extraction`) — 텍스트만 필요할 때
- 페이지 범위 (`page_range`, 0-indexed: `"0,2-4"`) — 큰 PDF 비용 절감용

**입력 형식**: PDF · DOC · DOCX · PPT · PPTX · PNG · JPG · WEBP

**비용 안내**: 도구 본문에 노란 카드로 노출 — 기본 Worker 는 WKYEO 가 비용 부담 중, 본인 Replicate 키 사용을 원하면 [API 설정] → Worker URL 변경.

### 🔧 Cloudflare Worker v3.2.0 (`cloudflare-worker/worker.js`)

- `MODELS` 카탈로그에 `'marker'` 추가 (owner: `datalab-to`, name: `marker`)
- 새 응답 플래그 `isDocConv: true` — Marker 만 `{ markdown, images[uri], page_count }` 직접 패스
- 이미지는 **URI 배열 그대로 클라이언트 전달** (Worker subrequest 한도 50 회피, 큰 PDF 안전성). 클라이언트가 직접 fetch + zip
- buildInput 안전장치:
  - `mode` 화이트리스트 (`fast`/`balanced`/`accurate` 외 무시, 기본 `balanced`)
  - `page_range` / `max_pages` 둘 다 들어오면 `page_range` 우선
  - `paginate`, `format_lines` 기본 ON, `force_ocr` 기본 OFF

### 🔧 `shared/api-keys.js` v1.4.2

`replicateWorkerUrl` 키 정의의 `apps` 에 `{ id: 'pdftools', name: 'PDF 딸깍 (PDF→MD)' }` 추가. PDF 딸깍 도구 안 [API 설정] 버튼이 이 키만 노출하도록 `openModal({ filterApp: 'pdftools' })` 패턴 적용.

### 🔧 `pdftools/index.html`

- **PDF → MD 카드 활성화** (soon → live). 카탈로그 22 도구 중 **19/22 라이브**
- intro 의 phase-note 갱신: "Phase 3 진행 (v0.3.14) — 추출·변환 카테고리 5/5 완성"
- meta description / intro 텍스트 에서 OCR 단어 제거 + "PDF→마크다운" 추가
- 버전 배지 v0.3.13 → v0.3.14
- 모든 자원 cache-bust `?v=0.3.14`

### 📚 신규 패턴 (다음 작업 참고)

| 패턴 | 위치 | 의미 |
|---|---|---|
| **Worker 모델 `isDocConv` 분기** | `cloudflare-worker/worker.js` | OCR (isOcr) 와 별도 분기. markdown + 이미지 URI 배열 응답. 향후 다른 문서 변환 모델 (예: zerox, olmOCR) 도 같은 플래그 패턴 |
| **이미지 URI 배열 클라이언트 fetch** | `tools/to-markdown/to-markdown.js` | Worker 가 base64 화 X, URI 만 전달. subrequest 한도 회피 + 큰 결과 안전성 |
| **가짜 progress tick** | `to-markdown.js` | Marker 실제 진행률 알 수 없음 → 3초마다 +1% 가짜 진행 (85% 상한). 사용자 안심용 |
| **단가 안내 카드** | `to-markdown/index.html` 본문 | 외부 API 도구 공통 패턴 — Marker 의존성·비용·기본 Worker 정책 본문 노출 (사용자 진입장벽 ↓) |

### ⚠ 사용자 액션 필요 — Cloudflare Worker 배포

`cloudflare-worker/worker.js` 변경분 (Marker 모델 추가) 은 **Cloudflare 대시보드 또는 wrangler 로 직접 배포** 해야 합니다 (저장소 push 만으로는 Worker 갱신 X). 배포 전까지는 PDF→MD 도구가 `Unknown model 'marker'` 응답을 받음.

배포 후 헬스 체크:
```
curl https://gdi-replicate-proxy.sobjil.workers.dev
→ { ok: true, version: "3.2.0", models: ["real-esrgan","sam2","lama","surya","marker"], ... }
```

### 📌 다음 작업 후보

- **OCR 도구 부활 검토** — Marker 가 OCR 도 cover (Surya 내장). "PDF → 텍스트 (OCR)" 별도 도구 만들지, 또는 PDF→MD 안에서 force_ocr 옵션으로 끝낼지 검토. 현재는 PDF→MD 만.
- **Marker 응답 미리보기 강화** — markdown 첫 1500자만 보임. 사용자 요청 시 전체 미리보기·복사 버튼 추가
- **Phase 4** — 이미지 압축, PDF 비교, 뷰어 등
- **Worker.js polling 안정성** (v3.2.1) — 5초 간격, transient 에러 재시도, 주석 history 정리

---

## v0.3.13 — 2026-05-24 (OCR 도구 완전 폐기 — v1.0 범위에서 제외)

### 🗑 사용자 결정 — OCR 단념

v0.3.6 ~ v0.3.12 까지 7개 패치에 걸쳐 OCR 엔진 4종 (Tesseract.js / Gemini / 한컴 / Google Vision / Surya) 시도. 각각의 결과:

| 엔진 | 검증 결과 |
|---|---|
| Tesseract.js | 한국어 인식 품질 부족 + 첫 실행 18MB |
| Gemini Vision | 무료 quota 너무 적음 (RPM·일일 한도 모두) |
| 한컴 OCR | self-hosted 모델 — 사용자가 서버 직접 띄워야 (무리) |
| Google Cloud Vision | 결제 계정 등록 필요 + API 키 발급 복잡 |
| Surya (Worker) | 페이로드 크면 502 (subrequest 한도) + 페이지마다 1~2분 |

**사용자 결정**: "구현 안 하는게 낫겠다" — **딸깍 원칙 (한 번에 결과)** 과 안 맞고 외부 의존성·비용·시간 모두 부담. v1.0 범위에서 제외.

### 🗑 Removed

- `pdftools/tools/ocr/` 폴더 (index.html · ocr.js)
- `pdftools/shared/ocr-client.js`
- `pdftools/shared/tesseract-lazy.js` (v0.3.6 부터 사용 X 였던 잔재)
- `pdftools/index.html` 의 OCR 카드
- `pdftools/index.html` 의 헤더 [API 설정] 버튼 (현재 API 쓰는 도구 0 → v0.3.10 룰 적용)
- `pdftools/index.html` 의 `<script src="../shared/api-keys.js"></script>` 로드
- `shared/api-keys.js` 의 `googleVisionKey` 슬롯
- `shared/api-keys.js` 의 `replicateWorkerUrl` apps 에서 `pdftools` 제거 (image-editor 만 유지)

### 🔄 카탈로그 22 → 23 → 22

- v0.0.0 기획: 22개
- v0.3.3 (이미지→PDF 부활): 22 → 23
- **v0.3.13 (OCR 폐기): 23 → 22**

### 🔄 카드 그리드 19/23 → 18/22

추출·변환 카테고리:
- 이전 (v0.3.12): 텍스트 추출 · 이미지 추출 (준비 중) · 페이지→이미지 · 이미지→PDF · **OCR** · PDF→MD (준비 중) = 6
- 신규 (v0.3.13): 텍스트 추출 · 이미지 추출 (준비 중) · 페이지→이미지 · 이미지→PDF · PDF→MD (준비 중) = 5

### 🔄 코어 모듈 13 → 11

제거된 `ocr-client.js` + `tesseract-lazy.js` → 11개 (`cdn` · `pdf-core` · `file-input` · `thumbnail` · `thumb-toolbar` · `page-range` · `download` · `progress-ui` · `coordinate` · `font-manager` · `jszip-lazy`)

### 🚫 향후 OCR 재시도 시 고려사항

만약 v1.x 이후 OCR 재도전 한다면 좋은 옵션:
- 사용자 가 자체 호스팅하는 **로컬 Tesseract 서버 (CLI)** — 정확도 비교적 양호 + 키 X + 네트워크 X (image-editor 의 ESRGAN 패턴 reuse)
- **PaddleOCR** local server
- **Google Document AI** (Vision API 보다 정확하지만 더 복잡)

### 🔄 Cache-bust

- `?v=0.3.12` → `?v=0.3.13` 일괄

### 🚧 다음

- v0.3.14 — PDF → MD (Phase 3 마지막)
  - 텍스트 PDF (텍스트 레이어 있음) 대상
  - 스캔 PDF 는 사용자가 다른 OCR 도구 거쳐 텍스트 PDF 로 변환 후 사용 안내
  - extractTextContent + (Gemini 정리 옵션 + 키 등록) 또는 단순 추출
- v0.4.0 — Phase 3 완료 회고

---

## v0.3.12 — 2026-05-24 (OCR 엔진 재교체 — 한컴/Gemini 제거 → Google Vision + Surya)

### 🔄 사용자 검증 결과 따른 엔진 재구성

사용자 직접 검증:
- **한컴 OCR**: `hancom-ocr.hancom.com:5000` DNS 실패 — **self-hosted 모델** (사용자가 SDK 받아 자체 서버 띄워야). "호스팅 서버는 무리" — 제거.
- **Gemini 3.5 Flash**: 무료 quota (RPM·일일 한도) 너무 부족. v0.3.11 의 retry 강화도 limit 자체 부족엔 무효. 제거.
- **Google Vision**: 월 1,000 unit 무료 (페이지 = 1 unit) — 1 PDF (보통 10~100쪽) 처리 충분
- **Surya**: image-editor 가 이미 사용 중인 Cloudflare Worker proxy → reuse 가능. 한국어 인식 우수

### 🗑 Removed
- `'gemini'` engine case
- `'hancom'` engine case + `recognizeHancomPdf` + `recognizeHancomImage` + `hancomFetch` 친절 에러
- `shared/api-keys.js` 의 `hancomOcrKey` · `hancomOcrUrl` 슬롯
- `shared/api-keys.js` 의 `geminiApiKey` 의 apps 에서 `pdftools` 제거 (다른 앱 등록은 유지)

### 🆕 Added
- `'google'` engine case 재추가 — DOCUMENT_TEXT_DETECTION + languageHints ko/en
  - 응답의 `textAnnotations[1..]` 에서 단어별 bbox 추출 (검색 가능 PDF 정확)
- `'surya'` engine case 신규 — `POST {workerUrl}/ocr` body `{image:dataURL, model:'surya'}`
  - image-editor `runReplicateOCR` 패턴 reuse
  - 응답 깊이 탐색으로 `text_lines` 추출 (`findOcrLines`)
  - Surya 의 `bbox [x1,y1,x2,y2]` 또는 `polygon` → 한컴 형식 `[x1,y1, x2,y1, x2,y2, x1,y2]` 8점으로 정규화
- `shared/api-keys.js` 의 `googleVisionKey` 슬롯 부활
- `shared/api-keys.js` 의 `replicateWorkerUrl` apps 에 `pdftools` 추가 (image-editor 와 worker 공유)

### 🆕 친절한 Worker 연결 실패 안내

Surya Worker fetch 실패 시 (`Failed to fetch`, `NetworkError`) → "Worker URL 확인, [API 설정] 에서 수정 가능" 안내.
Worker 가 502/500 응답 시 body 가 JSON 이 아닐 수 있어 → text 로 받아서 "subrequest 한도 초과 또는 Surya 일시 다운" 가능성 안내.

### 🔧 OCR 도구 UI

- 엔진 dropdown: **🌐 Google Cloud Vision (기본)** / **🤖 Surya (Worker)**
- DPI / 출력 형식 / 페이지 선택 — 그대로
- 한컴 PDF 통째 처리 안내 X (둘 다 페이지마다 호출)
- Surya 모드 시 페이지 간 300ms preventive delay (Worker subrequest 보호)
- Google 은 페이지 간 delay X (월 unit 단위라 RPM 보호 불필요)

### 🔍 검색 가능 PDF — 두 엔진 모두 정확

이전 v0.3.9: 한컴만 bbox 정확, Gemini 는 페이지 좌하단 invisible block.
신규 v0.3.12: **Google + Surya 둘 다 단어별 bbox 제공** → 위치 정확. fallback (bbox X) 만 페이지 좌하단.

### 🔄 Cache-bust

- `?v=0.3.11` → `?v=0.3.12` 일괄

### 🚧 다음

- v0.3.13 — PDF → MD (Phase 3 마지막)
- 검증 후: image-editor 의 Surya OCR → 같은 ocr-client.js reuse (코드 중복 제거)

---

## v0.3.11 — 2026-05-24 (Gemini 429 quota 보호 — 긴 backoff + preventive page delay)

### 🐛 Fixed — Gemini 429 (Too Many Requests) 무한 retry 실패

**증상**: OCR 도구에서 Gemini 엔진으로 7페이지 처리 → 429 "exceeded your current quota" 에러. v0.3.9 의 retry (1s→2s→4s) 가 429 quota window 회복 (분 단위) 에 비해 너무 짧아 retry 도 429 받고 실패.

**원인**:
- Gemini Flash 무료 티어 RPM (분당 요청) 약 10~15회
- 7페이지를 빠르게 연속 호출 + retry 3회 합쳐 짧은 시간에 10회+ → quota 초과
- 1s~4s backoff 는 quota window 가 1분이라 의미 X

**Fix** — 3가지 layer 보호:

#### 1. **Gemini 응답의 `retryDelay` 추출 (정확)**
Gemini 가 종종 응답 body 에 `"retryDelay": "60s"` 알려줌 — 그 값 정확히 사용 (+500ms 여유).

#### 2. **429 전용 보수적 backoff (fallback)**
retryDelay 없으면: **10s → 20s → 40s → 80s → 120s (cap)** · 최대 5회 시도. quota window 회복 시간 확보.
- 503/500/Deadline 등 transient 는 기존대로 1s→2s→4s→8s→16s

#### 3. **페이지 간 800ms preventive delay**
Gemini 모드 (한컴 X) 시 페이지마다 자동 sleep — quota 초과 사전 방지.

#### 4. **친절한 429 에러 메시지 (최종 실패 시)**
```
Gemini 무료 티어 RPM(분당 요청) 한도 초과.
잠시 후 다시 시도하거나, 🇰🇷 한컴 OCR 엔진 사용 권장
(페이지 다수 처리 시 한컴이 안정적).
또는 Google AI Studio 에서 유료 결제로 한도 ↑.
```

### 🔄 UI 안내

엔진 dropdown hint 메시지 갱신 — "Gemini 무료 티어 분당 10~15회 제한 — 페이지 다수 시 자동 sleep + 429 시 longer backoff. 10쪽↑ PDF 는 🇰🇷 한컴 권장."

### 🆕 헬퍼 — `extractRetryDelaySec(errMsg)`

응답 body 에 `"retryDelay": "60s"` 패턴 정규식 추출 → 정확한 quota 회복 대기.

### 🔄 Cache-bust

- `?v=0.3.10` → `?v=0.3.11` 일괄

### 🚧 다음

- v0.3.12 — PDF → MD (Phase 3 마지막) → v0.4.0 회고
- 향후: 사용자가 RPM·페이지 delay 직접 조절 가능한 옵션 (필요 시)

---

## v0.3.10 — 2026-05-24 (API 설정 헤더 통일 — 모든 앱 일관)

### 🔧 명명 통일 — "API 설정"

저장소 전체에서 사용자 표시 명을 **"API 설정"** 으로 통일 (이전 — image-editor 만 "API 설정", WeeklyBrief 는 "API 키"):

| 위치 | 이전 | 신규 |
|---|---|---|
| 루트 랜딩 (`/`) | "API 키 통합 관리" | 그대로 (전체 앱 키 다 보임) |
| image-editor 헤더 | API 설정 | 그대로 |
| WeeklyBrief 헤더 | "API 키" | **"API 설정"** |
| md2hwpx 헤더 | (없음, AI 미사용) | 그대로 X |
| pdftools 메인 랜딩 헤더 | (없음) | **신규 [API 설정]** |
| pdftools OCR 도구 헤더 | (사이드바에만) | **헤더 [API 설정] + 사이드바 중복 제거** |
| pdftools 나머지 도구 16개 | 그대로 X | (API 필요 X — 추가 X) |

### 🆕 pdftools — 메인 랜딩 + OCR 도구 헤더에 [API 설정] 버튼

```html
<button class="tbtn" type="button" id="btnApiKeys" title="API 설정 (이 앱에서 쓰는 키)"
        onclick="window.GDIApiKeys && window.GDIApiKeys.openModal({filterApp:'pdftools'})">
  <svg ...><circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/></svg>
  API 설정
</button>
```

- 아이콘: image-editor·WeeklyBrief 와 동일 (열쇠 SVG)
- 위치: `.tb-right` 의 `.version-badge` 좌측
- 동작: `GDIApiKeys.openModal({filterApp:'pdftools'})` — pdftools 가 쓰는 키만 모달에 표시 (Gemini · 한컴 OCR 키 + 서버 URL)
- shared/api-keys.js 자동 로드 (`<script src="../shared/api-keys.js"></script>`)

### 🗑 OCR 도구 사이드바 [🔑 API 키 관리] 버튼 제거

헤더에 [API 설정] 이 있으므로 중복. JS 핸들러도 정리.

### 🔄 결정 — 다른 16개 도구는 헤더 [API 설정] 추가 X

이유:
- 현재 API 가 필요한 도구는 OCR 만 (Phase 5 까지 가면 PDF→MD·번역·요약 등 추가 예정)
- API 가 필요 없는 도구 (병합·분할·회전·자르기·텍스트 추가 등) 에 버튼 두면 사용자 혼란
- **API 가 필요해지는 도구가 추가될 때마다 그 도구만 헤더에 추가**

### 🔄 한컴 OCR 키도 통합 관리 포함

v0.3.9 에서 추가한 `hancomOcrKey` + `hancomOcrUrl` 슬롯이 이미 통합 키 매니저 (`shared/api-keys.js`) 에 정의됨. **루트 랜딩 "API 키 통합 관리"** 모달 에서도 자동 노출 (`filterApp` 인자 없으면 모든 키).

### 🔄 Cache-bust

- `?v=0.3.9` → `?v=0.3.10` 일괄

### 🚧 다음

- v0.3.11 — PDF → MD (Phase 3 마지막) → v0.4.0 회고

---

## v0.3.9 — 2026-05-24 (OCR 대규모 개편 — 한컴 추가 + 검색 가능 PDF + Gemini retry)

사용자 피드백 일괄 반영:
1. Tesseract.js 제거 (품질 부족)
2. 한컴 OCR 추가 (사용자가 API 사양 PDF 3개 제공)
3. 출력 형식에 **검색 가능 PDF** (invisible 텍스트 레이어) 추가
4. Gemini 503 (DEADLINE_EXCEEDED) 에러 자동 재시도

### 🗑 Removed — Tesseract.js

이전: Gemini / Tesseract / Cloud Vision (v0.3.6~7) → Tesseract / Gemini (v0.3.8). v0.3.9 에서 **Tesseract 도 제거**.

근거: 사용자 검증 — "품질이 너무 안 좋음". 18MB 다운로드 부담 + 한국어 인식률 부족. `shared/tesseract-lazy.js` 파일은 남겨둠 (향후 다른 도구 또는 옵션 부활 가능).

### 🆕 한컴 OCR 엔진 추가

문서 (사용자 제공 PDF 3종) 기반 구현:

| 항목 | 값 |
|---|---|
| **API** | `POST {server}/argoocr` (예: `https://hancom-ocr.hancom.com:5000/argoocr`) |
| **Content-Type** | `multipart/form-data` |
| **필수** | `key` (32자 라이선스), `request_id`, `file_format` (pdf/image), `file_upload` |
| **응답** | `content.ocr_data[].page_text` · `.words[].text` + `.bbox` · `.image_width/height` |

**강점**:
- **PDF 통째 처리** — `file_format: 'pdf'` 한 번 호출로 모든 페이지 OCR (Gemini 는 페이지마다 호출)
- **단어별 bbox 제공** — 검색 가능 PDF 의 정확한 텍스트 위치 매핑
- 한국어 인식 최우수

**구현**:
- `recognizeBatch(pdfBytes, opts)` — PDF 통째
- `recognizeHancomImage(canvas, opts)` — 이미지 한 장 (fallback 용)
- 선택된 페이지 필터링은 결과 받은 후 클라이언트 측 (한 번 호출 후 필요 페이지만 사용)

### 🆕 검색 가능 PDF 출력 (`searchable_pdf`)

원본 PDF 위에 **invisible 텍스트 레이어** 추가 → PDF 뷰어에서 텍스트 검색 가능. 시각적으로는 원본 그대로.

**기술**:
- pdf-lib `drawText({ opacity: 0 })` — 안 보이지만 PDF text extraction 에 잡힘
- 한국어 폰트 `embedKoreanFont(src, 'nanumGothic')` — `shared/font-manager.js` reuse

**엔진별 정확도**:
- 🇰🇷 **한컴**: 단어별 bbox 활용 → 검색 시 원래 위치 정확 (마우스 끌어서 텍스트 복사도 정상)
- ✨ **Gemini**: bbox X → 페이지 좌하단에 invisible block (검색은 됨, 위치 표시 부정확)

**bbox 좌표 변환** (한컴 → PDF):
```js
const scaleX = pdfWidth / r.imageWidth;
const scaleY = pdfHeight / r.imageHeight;
// bbox 8 점 → 사각형 4 점으로 단순화 (회전 박스는 다음 라운드)
const pdfX = x1;
const pdfY = pdfHeight - y3;  // Y 뒤집힘
const fontSize = (y3 - y1) * 0.75;
```

### 🐛 Gemini 503/Deadline retry

**증상**: `Gemini API 503 — Deadline expired before operation could complete.`

**원인**: Gemini Flash 모델이 일시적 과부하 / 네트워크 deadline 초과. 일반적인 transient 에러.

**Fix**: `recognizeGeminiWithRetry()` — exponential backoff:
- 최대 3회 재시도 (1s → 2s → 4s 대기)
- 재시도 대상 에러: `503` / `UNAVAILABLE` / `Deadline` / `DEADLINE_EXCEEDED` / `429` / `RESOURCE_EXHAUSTED` / `500`
- console.warn 으로 진행 상황 알림

### 🔧 `shared/api-keys.js` 신규 슬롯 2개

```js
{
  id: 'hancomOcrKey',
  label: '한컴 OCR 라이선스 키',
  placeholder: '32자 라이선스 키',
  apps: [{ id: 'pdftools', name: 'PDF 딸깍 (OCR · 검색 가능 PDF)' }],
},
{
  id: 'hancomOcrUrl',
  label: '한컴 OCR 서버 URL',
  default: 'https://hancom-ocr.hancom.com:5000',
  // 자체 호스팅 시 변경 가능
}
```

### 🔄 UI 변경

- 엔진 dropdown: ✨ Gemini 3.5 Flash (기본) / 🇰🇷 한컴 OCR
- 출력 dropdown 옵션 3가지: 합본 .txt / 페이지별 zip / 🔍 검색 가능 PDF
- DPI row — 한컴 모드면 숨김 (PDF 통째 처리, DPI 의미 X)
- Tesseract 의 `lang` 옵션 row 완전 제거
- 한컴 모드 안내 메시지 — PDF 통째 처리 + 선택된 페이지만 결과 추출 안내

### 🔄 OCR 결과 데이터 형식 확장

```js
// 이전 (v0.3.8)
{ text: string, engine: string, pageNum?: number }

// 신규 (v0.3.9) — bbox 정보 추가 (한컴 만 제공)
{
  text: string,
  engine: 'gemini' | 'hancom',
  pageNum: number,
  words?: Array<{text, bbox: [x1,y1,x2,y2,x3,y3,x4,y4], score}>,  // 한컴 만
  imageWidth?: number,
  imageHeight?: number
}
```

### 🔄 Cache-bust

- `?v=0.3.8` → `?v=0.3.9` 일괄

### 🚧 다음

- v0.3.10 — PDF → MD (Phase 3 마지막) → v0.4.0 회고
- 향후: 회전 박스 처리 (한컴 bbox 8점 → affine 변환)
- 향후: image-editor Surya → Gemini 3.5 Flash 교체 (사용자 OCR 검증 후 결정)

---

## v0.3.8 — 2026-05-24 (OCR — Google Vision 제거 + Gemini 3.5 Flash 교체)

### 🔄 OCR 엔진 변경

사용자 결정: **Google Cloud Vision** 옵션 제거 → **Gemini 3.5 Flash** (multimodal LLM) 로 교체.

근거:
- Gemini 3.5 Flash 가 **multimodal** — 이미지 입력 받아 자연어 출력. OCR + 구조화 + 후처리 한 번에 가능
- 무료 티어 매우 관대 (Cloud Vision 월 1000 unit vs Gemini Flash 일 ~1500 요청)
- **이미 저장소의 다른 앱 (WeeklyBrief · AI 썰전) 에서 검증된 패턴**
- `geminiApiKey` 가 `shared/api-keys.js` 에 이미 정의 — 키 한 번 등록으로 여러 앱 공유
- 향후 PDF→MD · 번역 · 요약 등 Phase 5 도구도 동일 키 활용

### 변경 사양

| 항목 | 이전 (v0.3.7) | **신규 (v0.3.8)** |
|---|---|---|
| 엔진 dropdown | Tesseract / Google Vision | Tesseract / **Gemini 3.5 Flash** |
| API | `vision.googleapis.com/v1/images:annotate` | `generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent` |
| 인증 | `googleVisionKey` | **`geminiApiKey`** (이미 정의됨) |
| 호출 형식 | feature.type DOCUMENT_TEXT_DETECTION | `contents[].parts[]` (text + inline_data) |

### 🆕 Gemini OCR 호출 사양

```js
POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent?key={KEY}
body: {
  contents: [{
    parts: [
      { text: "이미지의 모든 텍스트를 정확하게 추출. 원본 줄바꿈·문단 구조 그대로 유지. 오타 교정 X, 해석·설명 X, 추출한 텍스트만 출력." },
      { inline_data: { mime_type: 'image/png', data: <base64> } }
    ]
  }],
  generationConfig: { temperature: 0, maxOutputTokens: 8192 }
}
→ json.candidates[0].content.parts[].text 합치기
```

- `temperature: 0` — OCR 은 결정론적 (창의성 X)
- `maxOutputTokens: 8192` — 한 페이지 분량
- `finishReason` 안전 체크 (안전 필터 등)

### 🔄 `shared/api-keys.js` 변경

- `googleVisionKey` 키 정의 **제거** (어떤 도구도 사용 X 였음 — v0.3.7 잠시 사용 후 v0.3.8 즉시 교체)
- `geminiApiKey` 의 `apps` 배열에 `pdftools` 등록 (note 도 갱신 — "OCR · 향후 PDF→MD·번역·요약")

### 🔄 `shared/ocr-client.js` 변경

- `'google'` → `'gemini'` engine case 교체
- `recognizeGoogle` → `recognizeGemini` (Gemini generateContent 호출)
- `GEMINI_MODEL`, `GEMINI_PROMPT_OCR` 상수화

### 🔄 OCR 도구 UI

- Dropdown 옵션: `✨ Gemini 3.5 Flash (정확도 ↑)`
- 키 미등록 confirm 메시지 — "Google AI Studio 에서 무료로 발급 가능" 안내
- 엔진 hint 동적 — Gemini 선택 시 "multimodal LLM, 한국어 우수, 빠름"
- 진행 메시지 — "Gemini 3.5 Flash OCR 준비 중..."

### 🚧 다음 라운드

- **v0.3.9 — 한컴 OCR 엔진 추가** (사용자가 정확한 endpoint·요청 형식 확인 후. https://developer.hancom.com/hancomocr/devguide/api 는 SPA 라 자동 추출 불가)
- v0.3.10 — PDF → MD (Phase 3 마지막)
- 향후 검토: image-editor 의 Surya OCR 도 Gemini 3.5 Flash 로 교체 (사용자 결정 — OCR 도구 검증 후)

### 🔄 Cache-bust

- `?v=0.3.7` → `?v=0.3.8` 일괄

---

## v0.3.7 — 2026-05-24 (OCR — Google Cloud Vision 엔진 추가)

### 🆕 Google Cloud Vision 옵션

기존 Tesseract.js 외에 **Google Cloud Vision API** 엔진 추가. 사이드바에서 선택:

| 엔진 | 키 | 정확도 | 속도 | 비용 |
|---|---|---|---|---|
| 🖥 **Tesseract.js** (기본) | X | 보통 | 5~20초/쪽 | 무료 |
| ☁️ **Google Cloud Vision** | 필요 (BYO) | ↑↑ | 1~3초/쪽 | 월 1000 unit 무료 |

Google Vision 의 강점:
- `DOCUMENT_TEXT_DETECTION` 모드 — 긴 문서·복잡한 레이아웃 우수
- 한국어 + 영어 동시 (`languageHints: ['ko', 'en']`)
- Tesseract 대비 정확도 + 속도 모두 우위 (특히 한자·표·복잡한 폰트)

### 🆕 공용 인프라 — `shared/ocr-client.js` (코어 모듈 13개)

엔진 추상화 — 도구 코드가 엔진별 호출 차이 X.

```js
import { createOcrSession } from '../../shared/ocr-client.js?v=0.3.7';

const sess = await createOcrSession({ engine, lang, apiKey });
try {
  for (const img of pages) {
    const { text } = await sess.recognize(img);  // engine 무관 동일 API
  }
} finally {
  await sess.terminate();
}
```

**recognize 입력**: `Blob` 또는 `HTMLCanvasElement` 둘 다 지원.
**terminate**: Tesseract 워커 정리 / Google 은 no-op.

### 🆕 `shared/api-keys.js` — `googleVisionKey` 슬롯 추가

루트 공용 키 매니저 (image-editor 와 공유) 에 신규 키 정의 1개 추가:
- `googleVisionKey` — Google Cloud Vision API Key
- apps: `[{ id: 'pdftools', name: 'PDF 딸깍 (OCR)' }]`
- links: 키 발급 · Vision API 활성화 · 무료 한도·요금

image-editor 등 기존 도구 영향 X (새 항목만 추가).

### 🔄 OCR 도구 UI 변화

- **엔진 dropdown** (Tesseract / Google Vision) — 변경 시 안내 메시지 동적
- **언어 row** — Tesseract 만 표시 (Google 은 imageContext.languageHints 자동)
- **🔑 API 키 관리 버튼** — `GDIApiKeys.openModal({ filterApp: 'pdftools' })` 호출
- 결과 미리보기 / 출력 형식 (합본/zip) / DPI 옵션은 v0.3.6 그대로
- Google 선택 후 키 미등록 시 confirm 후 모달 열기 안내

### 📐 Google Vision 호출 패턴

```js
POST https://vision.googleapis.com/v1/images:annotate?key={KEY}
body: {
  requests: [{
    image: { content: <base64> },
    features: [{ type: 'DOCUMENT_TEXT_DETECTION' }],
    imageContext: { languageHints: ['ko', 'en'] }
  }]
}
→ json.responses[0].fullTextAnnotation.text
```

큰 이미지 base64 인코딩 — `String.fromCharCode.apply` 의 인자 한도 회피 위해 0x8000 chunk 처리.

### 🚧 추가 예정 (다음 라운드)

- **한컴 OCR** — 한국어 정확도 최우수 (1쪽 ~2원). 정확한 endpoint·인증 방식 확인 후 v0.3.x 에 추가
- **검색 가능 PDF** — OCR 결과를 원본 PDF 위에 invisible 텍스트 레이어로 (v0.4.x)

### 🔄 Cache-bust

- `?v=0.3.6` → `?v=0.3.7` 일괄
- 코어 모듈 12 → **13** (+ `ocr-client`)

### 🚧 다음 (Phase 3 — 1개 남음)

- v0.3.8 — PDF → MD (Phase 3 마지막 → v0.4.0 회고)

---

## v0.3.6 — 2026-05-24 (Phase 3 #3 — OCR · Tesseract.js 브라우저 OCR)

### 🆕 OCR 도구

폴더: `tools/ocr/` · 카드 그리드 **19/23 활성**.

스캔 PDF (이미지로만 된 PDF) 의 글자를 **Tesseract.js** (브라우저 OCR) 로 텍스트 추출. **외부 API 키 X** — 사용자가 키 등록 없이 즉시 사용.

### 핵심 결정 — Tesseract.js vs 한컴/Google Vision

당초 계획은 한컴 OCR (1쪽 2원, 한국어 우수) + Google Vision (무료 한도) BYO 키 패턴. 그러나 OCR 키 슬롯이 아직 `shared/api-keys.js` 에 X + 사용자 진입장벽 (키 발급·결제) 고려.

**v0.3.6 결정**: Tesseract.js 5.x 우선 — 키 X, 즉시 사용, 한국어 + 영어 동시. 정확도는 상용보다 낮지만 적당함. 한컴/Google 은 다음 라운드 (v0.3.8+) 에 추가 옵션.

### 사용 옵션

- **언어**: 한국어 + 영어 (기본) / 한국어만 (빠름) / 영어만 (가장 빠름)
- **DPI**: 150 (빠름) / 200 (기본·균형) / 300 (정확도 ↑·느림)
- **출력 형식**: 합본 .txt (페이지 구분자) / 페이지별 .txt zip
- 페이지 선택 (썸네일 체크박스, 기본 전체)

### 🆕 공용 인프라 — `shared/tesseract-lazy.js`

- Tesseract.js 5.1.1 CDN lazy 로더 (`<script>` 동적 삽입)
- `await loadTesseract()` 한 번 → `window.Tesseract` 보장
- 첫 실행 시 traindata 약 18MB 다운로드 (브라우저 캐시), 이후 즉시

### 📐 핵심 코드 패턴

```js
const Tesseract = await loadTesseract();
const worker = await Tesseract.createWorker(['kor', 'eng'], 1);

for (const pNum of selectedPages) {
  // 페이지 → canvas (DPI 적용)
  const pageView = await pdf.getPage(pNum);
  const viewport = pageView.getViewport({ scale: dpi / 72 });
  const canvas = document.createElement('canvas');
  canvas.width = viewport.width; canvas.height = viewport.height;
  await pageView.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;

  // OCR — canvas 직접 전달 (Tesseract v5)
  const { data } = await worker.recognize(canvas);
  results.push({ pageNum: pNum, text: data.text });
}
await worker.terminate();
```

### 🆕 텍스트 미리보기

다운로드 후 사이드 영역에 첫 3쪽 OCR 결과 미리보기 (각 250자 까지). `.text-preview` CSS (이미 존재, 텍스트 추출 도구와 동일) 재사용.

### 단점·한계

- 첫 실행 시 18MB 다운로드 — 매우 느린 첫 인상
- 한 페이지당 약 5~20초 (DPI·이미지 복잡도에 따라)
- 정확도 — 한컴 OCR 보다 낮음 (특히 한자·표·복잡한 레이아웃)
- 검색 가능 PDF 출력 X — 다음 라운드 (v0.3.8)

### 🔄 Cache-bust

- `?v=0.3.5` → `?v=0.3.6` 일괄
- 코어 모듈 11 → **12** (+ `tesseract-lazy`)

### 🚧 다음 (Phase 3 — 1개 남음)

- v0.3.7 — PDF → MD (텍스트 추출 + OCR fallback + Gemini 정리)
- v0.3.8 (후보) — OCR 검색 가능 PDF + Google Vision / 한컴 OCR 옵션 추가

---

## v0.3.5 — 2026-05-24 (썸네일 줌 다시 render fix + 파일명 middle ellipsis)

### 🐛 Fixed — 줌 시 캔버스 실내용이 안 커지는 버그

**증상**: PDF 페이지 썸네일 도구 (to-image 등) 에서 줌 slider 를 늘리면 그리드 column 폭만 늘어남. canvas 안의 실제 PDF 페이지 이미지는 작은 상태 그대로. from-image 의 그리드 줌은 OK (img 태그는 max-width:100% 가 잘 작동).

**원인**: v0.3.4 의 `setThumbWidth(w)` 가 `--thumb-w` CSS var 만 갱신. canvas 의 native width/height 는 첫 render 때 작은 thumbWidth 기준으로 픽셀 고정. CSS `max-width:100%; height:auto` 가 inline 사이즈 변경에 부분 동작 (canvas 의 intrinsic size 때문에 column 너비가 커져도 따라가지 X).

**Fix**: `setThumbWidth(w)` 가 thumbWidth 변수 갱신 + 모든 `dataset.rendered` 제거 + IntersectionObserver 재등록. 가시 페이지부터 새 사이즈로 다시 render → 화질 유지 + 정확한 사이즈. 150ms debounce 로 slider 빠른 이동 시 호출 폭주 방지.

```js
setThumbWidth: (w) => {
  thumbWidth = w;
  container.style.setProperty('--thumb-w', `${w}px`);
  if (rerenderTimer) clearTimeout(rerenderTimer);
  rerenderTimer = setTimeout(() => {
    items.forEach(it => {
      delete it.dataset.rendered;
      io.observe(it);  // 가시 페이지 → renderItem 다시 호출 (closure 의 새 thumbWidth 사용)
    });
  }, 150);
}
```

### 🆕 파일명 middle ellipsis

**요청**: 긴 파일명 (예: `0522(23조간, 22일(금) 19시엠바고)다자통상협력과, APEC 통상장관회의...논의_p001.png`) 가 너무 길어 사이드바 침범 또는 뒷부분 짤림.

**구현**: 신규 `middleEllipsis(name, maxLen)` 헬퍼 (`shared/file-input.js` export). 앞 절반 + `…` + 뒤 절반 (확장자 보존 유지).

```
0522(23조간, 22일(금)…디지털·녹색산업 협력방안 논의_p001.png
                  ▲          ▲
                  앞 18자    뒤 18자 (확장자 포함)
```

적용:
- `shared/file-input.js` 의 `.sf-name` (createSortableList) — maxLen 50
- `tools/from-image/from-image.js` 의 `.fg-name` (그리드 모드) — `thumbW / 8` 동적 (줌 in 시 더 길게 보임)
- 원본 풀 파일명은 `title` 속성으로 hover 시 보임

### 🔄 Cache-bust

- `?v=0.3.4` → `?v=0.3.5` 일괄

### 🚧 다음 (Phase 3 — 2개 남음)

- v0.3.6 — OCR (한컴 + Google Vision)
- v0.3.7 — PDF → MD

---

## v0.3.4 — 2026-05-24 (썸네일 줌 인프라 + from-image 그리드/리스트/정렬)

### 🆕 공용 인프라 — `shared/thumb-toolbar.js`

신규 헬퍼 — 썸네일/그리드 위 toolbar (줌·정렬·뷰모드). **모든 도구가 동일 API** 로 줌 컨트롤 추가.

```js
createThumbToolbar(toolbarEl, {
  label: '페이지 미리보기',
  zoom: { initial: 160, min: 100, max: 320, step: 20,
          onChange: (w) => thumbCtrl.setThumbWidth(w) },
  sort?: { options: [{value, label}], value, onChange },     // 선택
  viewMode?: { current: 'grid', onChange }                    // 선택
});
```

UI: ▥/☰ 보기 토글 (선택) · 정렬 dropdown (선택) · ＋／slider／− 줌 컨트롤 (오른쪽 끝).

### 🆕 `shared/thumbnail.js` — `setThumbWidth(w)` API

기존 `renderThumbnails()` 컨트롤러에 줌 메소드 추가:
- `setThumbWidth(w)` — CSS variable `--thumb-w` 만 갱신 (캔버스 재렌더 X — 빠른 UX 우선, 약간 흐릿 가능)
- `getThumbWidth()` — 현재 값 조회

`.thumb-grid` CSS 의 `grid-template-columns` 가 `minmax(var(--thumb-w, 150px), 1fr)` 로 변경 — CSS var 변경만으로 즉시 columns 재계산.

### 🔧 모든 썸네일 도구에 줌 toolbar 일괄 적용 (8 도구)

| 도구 | 기본 폭 |
|---|---|
| reorder | 160 (기존 130) |
| remove-pages | 160 (기존 130) |
| extract-pages | 160 (기존 130) |
| rotate | 160 (기존 130) |
| crop | 140 (기존 110) |
| split | 140 (기존 120) |
| to-image | 160 (기존 140) |
| from-image (그리드 모드) | 180 |

모든 도구에서 100~320px 사이로 자유 조절. 8 도구 일관 UX.

### 🆕 from-image — 그리드/리스트 토글 + 정렬 + 줌

사용자 피드백 — 파일명 긴 리스트는 가독성 X. 풀 리팩토링:

**보기 모드 토글** (기본 그리드):
- **그리드**: 이미지 미리보기 (objectURL) + 파일명 + ✕ + Sortable.js (드래그 정렬)
- **리스트**: 기존 file-input.js 의 `createSortableList`

**정렬 dropdown**:
- 수동 (드래그) — 기본
- 파일명 ↑ / ↓ (ko locale)
- 크기 ↑ / ↓ (작은 순 / 큰 순)
- 자동 정렬 후 사용자가 드래그하면 자동으로 "수동" 으로 전환

**줌** (그리드 모드): 100~320px slider · `--thumb-w` CSS var

### 🆕 from-image 신규 CSS (`.fg-grid` / `.fg-item`)

자체 그리드 (썸네일 도구 패턴 reuse 안 함 — 이미지 파일 미리보기 전용):
- `.fg-grid` — CSS var minmax (썸네일과 같은 줌 변수)
- `.fg-item` — 카드 (썸네일 + 메타 + ✕)
- `.fg-thumb-wrap` — aspect-ratio 3/4, object-fit contain
- `.fg-dragging`, `.fg-drop-target` — Sortable 상태

### 🔄 모든 도구 version badge 통일

이전 — 도구별 cache-bust 버전 제각각 (v0.1.4 / v0.2.12 등 stale). 이제 **모든 도구 version-badge v0.3.4 통일** + `<script src="*.js?v=0.3.4">` 통일. sed 일괄 정리.

### 🔄 Cache-bust

- `?v=0.3.3` → `?v=0.3.4` 일괄
- 코어 모듈 카운트 10 → **11** (+ `thumb-toolbar`)

### 🚧 다음 (Phase 3 — 2개 남음)

- v0.3.5 — OCR (한컴 + Google Vision)
- v0.3.6 — PDF → MD

---

## v0.3.3 — 2026-05-24 (Phase 3 #2 — 이미지 → PDF · "이미지→PDF" 부활)

### 🆕 이미지 → PDF 도구

폴더: `tools/from-image/`. 카드 그리드 **18/23 활성** (카탈로그 22 → 23 — "이미지→PDF" 부활).

- 이미지 여러 장 (PNG / JPG / WebP / GIF) → PDF 1개 (페이지마다 한 장)
- 드래그&드롭 또는 클릭 + "+ 이미지 추가하기" 버튼 (계속 추가)
- 정렬 리스트 (Sortable.js) — ⠿ 핸들 드래그로 순서 변경, ✕ 로 개별 제거
- 옵션:
  - **페이지 크기**: 이미지 크기 그대로 (기본) / A4 / Letter / B5
  - **방향**: 자동 (이미지 비율) / 세로 / 가로
  - **여백**: 0 / 5 / 10 / 20mm
  - **이미지 맞춤**: Contain (비율 유지) / Fill (꽉 채움)

### 📐 핵심 변환 공식

```js
// 페이지 크기 결정
if (pageSize === 'auto') {
  pageW = img.width; pageH = img.height;   // 이미지 px = pt 매핑 (가장 단순)
} else {
  // A4·Letter·B5 preset, 방향 자동 시 이미지 비율에 맞춰 회전
}
// 이미지 맞춤 (Contain 기본 — 비율 유지, 페이지 중앙)
const scale = Math.min(availW / img.w, availH / img.h);
page.drawImage(img, { x: (pageW - img.w * scale) / 2, y: (pageH - img.h * scale) / 2, width: img.w * scale, height: img.h * scale });
```

### 🆕 WebP / GIF 처리

pdf-lib 가 직접 지원하지 않는 형식 (WebP/GIF 등) 은 **HTML Image + canvas → PNG 변환** 후 embedPng. 단 PNG/JPG 는 그대로 embedPng/embedJpg (불필요한 재인코딩 X — 원본 품질 유지).

### 🔧 인프라 — file-input.js `filterFiles` 와일드카드 + 콤마 지원

기존: `accept === f.type` 단일 매칭. WebP·GIF·여러 이미지 형식 한 번에 받기 어려웠음.

신규: `accept` 가 `image/*` (와일드카드) 또는 `image/png,image/jpeg` (콤마), `acceptExt` 가 `.png,.jpg,.webp` (콤마) 모두 지원.

```js
// 와일드카드 매칭
if (m.endsWith('/*')) return t.startsWith(m.slice(0, -1));
// 콤마 ext
exts.some(ext => fileName.toLowerCase().endsWith(ext))
```

다른 도구 (모두 PDF 단일) 는 영향 X.

### 🔄 도구 카탈로그 22 → 23 ("이미지→PDF" 부활)

dev.md §1.2 의 "만들기 카테고리 일괄 제외 (2026-05-23 결정)" 에서 **이미지→PDF 만 §9 결정 보류로 보존**해뒀음. 사용자 결정으로 이번에 부활. 추출·변환 카테고리에 PDF→이미지 짝 도구로 자연 배치.

### 🔄 Cache-bust

- `?v=0.3.2` → `?v=0.3.3` 일괄

### 🚧 다음 (Phase 3 — 2개 남음)

- v0.3.4 — OCR (한컴 + Google Vision)
- v0.3.5 — PDF → MD (텍스트 + 이미지 OCR + AI 정리)

---

## v0.3.2 — 2026-05-24 (PDF→이미지 UI fix + 기본값 변경)

### 🐛 Fixed — 썸네일이 1열로만 늘어진 문제

**증상**: PDF → 이미지 도구에서 썸네일이 좌측 컬럼에 한 줄로만 노출 (그리드 X).

**원인**: `.tool-main { display: flex; flex-direction: column; align-items: flex-start }`. v0.2.4 때 `.wm-preview` 등 inline-block 자녀가 stretch 되지 않게 박은 align-items 설정. 결과적으로 `.thumb-grid` (block 요소) 도 자기 자연 width 만 차지 → grid auto-fill 이 1열만 형성.

**Fix**: `.thumb-grid` 자체에 `width: 100%; align-self: stretch` 명시. 다른 도구의 썸네일 grid 도 자연스럽게 가득 채우게 됨 (`<details>` 안에 있던 도구들은 영향 X — details 안에서는 이미 stretch).

### 🔧 기본값 변경 — PNG / 200dpi

| 옵션 | 이전 | **신규** |
|---|---|---|
| 형식 | JPG | **PNG** (투명 유지) |
| DPI | 150 | **200** (고품질 인쇄) |
| JPG 품질 row | 표시 | **숨김** (PNG 기본이라) |

formatSel 변경 시 quality slider 토글은 그대로 유지.

### 🔄 Cache-bust

- `?v=0.3.1` → `?v=0.3.2` 일괄

---

## v0.3.1 — 2026-05-24 (Phase 3 #1 — PDF → 이미지)

### 🆕 PDF → 이미지 도구

폴더: `tools/to-image/` · 카드 그리드 **17/22 활성**.

- 페이지마다 JPG 또는 PNG 변환 → 다운로드
- 1쪽 → 단일 파일 / 여러 쪽 → **zip 묶음**
- 옵션:
  - **형식**: JPG (작음·사진용) / PNG (투명 유지)
  - **DPI**: 72 (화면) · 150 (기본) · 200 (인쇄) · 300 (최고)
  - **JPG 품질**: 40~100% slider (JPG 만 노출)
- 썸네일 그리드 + 체크박스 선택 (기본 전체 선택)
- "전체 선택" / "전체 해제" 빠른 토글
- 파일명: `{원본명}_p001.jpg` / zip 이면 `{원본명}-이미지150dpi.zip`

### 🆕 공용 인프라 — `shared/jszip-lazy.js`

- JSZip 3.10.1 CDN lazy 로더 (`<script>` 동적 삽입)
- `await loadJSZip()` 한 번 호출 → `window.JSZip` 보장
- Phase 3 이후 zip 묶음 다운로드 도구 (PDF→이미지, 이미지 추출, OCR 결과 묶음 등) 공통 사용
- 이미 `shared/download.js` 의 `downloadZip()` 가 `window.JSZip` 의존 — lazy 로더로 더 깔끔히 연결

### 📐 핵심 변환 공식

```js
const scale = dpi / 72;  // pdf.js viewport scale
const viewport = pageView.getViewport({ scale });
canvas.width = viewport.width;
canvas.height = viewport.height;
await pageView.render({ canvasContext, viewport }).promise;
canvas.toBlob(blob => ..., 'image/jpeg', quality);
```

- JPG: 흰 배경 fillRect 먼저 (PDF 의 투명 배경이 검정 되지 않게)
- PNG: 투명 배경 유지

### 🆕 도구별 IntersectionObserver lazy 썸네일 (기존 인프라 reuse)

`shared/thumbnail.js` 가 이미 IO 로 가시 영역만 렌더 — 큰 PDF (100쪽+) 도 즉시 노출 가능. 변환 자체는 클릭 시 페이지마다 풀 해상도 render.

### 🔄 Cache-bust

- `?v=0.3.0` → `?v=0.3.1` 일괄

### 🚧 다음 (Phase 3 진행 — 3개 남음)

- v0.3.3 — 이미지 → PDF (이미지 여러 장 → PDF 1개)
- v0.3.4 — OCR (한컴 + Google Vision, `shared/api-keys.js` reuse)
- v0.3.5 — PDF → MD (텍스트 + 이미지 OCR + AI 정리)

---

## v0.3.0 — 2026-05-24 (★ Phase 2 완료 회고 + Phase 3 진입 준비)

> Phase 1 의 v0.1.0 과 같은 회고 버전. 신규 도구·기능 X — 누적된 패턴·교훈을 박아두고 Phase 3 카탈로그를 정리한다.

### ✅ Phase 2 결과 (v0.2.0 → v0.2.17 — 약 18단계 패치, 단일 세션)

| # | 도구 | 폴더 | 완성 버전 |
|---|---|---|---|
| 1 | 워터마크 (텍스트) | `tools/watermark-text/` | v0.2.6 |
| 2 | 워터마크 (이미지) | `tools/watermark-image/` | v0.2.7 |
| 3 | 페이지 번호 | `tools/page-number/` | v0.2.8 |
| 4 | 머리글·바닥글 | `tools/header-footer/` | v0.2.9 |
| 5 | 텍스트 추가 (멀티페이지 풀 에디터) | `tools/add-text/` | v0.2.15 |
| 6 | 서명 (그리기 + 이미지 업로드) | `tools/signature/` | v0.2.17 |

**도구 누적: 16개** (Phase 1: 10 + Phase 2: 6) — 카드 그리드 16/22 활성.

### 🆕 Phase 2 누적 신규 공용 인프라

| 항목 | 위치 | 도입 시점 |
|---|---|---|
| `shared/font-manager.js` | shared | v0.2.0 |
| `.tool-sidebar.sidebar-sticky` (사이드바 sticky 패턴) | pdftools.css | v0.2.14 |
| `.ta-handle` / `.ta-rot-handle` 박스 풀 에디터 패턴 (8 핸들 + 회전) | pdftools.css | v0.2.13~17 |
| `.wm-sticky-group` (미리보기 sticky 패턴) | pdftools.css | v0.2.4 |
| 9-cell 위치/회전 grid 패턴 | pdftools.css | v0.2.3 |
| IntersectionObserver lazy 페이지 렌더 | add-text/signature | v0.2.15 |
| 모달 패턴 `.sig-modal` + 탭 `.sig-tab-*` | pdftools.css | v0.2.16 |

### 🔑 Phase 2 패턴 정리 (Phase 3+ 재사용 대상)

#### 1. 박스 풀 에디터 (.xxx-box 패턴)
- 8 핸들 리사이즈 (NW·N·NE·E·SE·S·SW·W) + 회전 핸들 (상단 ↻)
- 회전 행렬로 OPPOSITE 코너 anchored 리사이즈 (회전된 박스도 정상)
- 박스별 상태 (style·rotation·opacity·bgEnabled 등)
- 사이드바 selected 패널 양방향 sync
- DOM `.ta-handle` / `.ta-rot-handle` reuse (★ v0.2.17 fix 후 부모 종속 X)
- 적용: `.xxx-box.selected .ta-handle { display:block }` 한 줄만 등록

#### 2. 멀티페이지 뷰어 패턴
- 모든 페이지 `.ta-page-wrap` vertical stack
- IntersectionObserver `rootMargin:'400px 0px'` 로 lazy 렌더
- 첫 페이지만 즉시 (viewport 보장) + 나머지는 스크롤 시 자연
- placeholder 회색 + 페이지 번호 텍스트
- 박스 데이터에 `pageIndex` 필드 추가, 박스는 해당 페이지 `.ta-canvas-wrap` 의 자식

#### 3. 사이드바 sticky (멀티페이지 도구용)
- `.tool-sidebar.sidebar-sticky` — `position: sticky; max-height: calc(100vh - ...)`
- 액션 버튼 (적용/초기화) `.sidebar-action-group` 사이드바 최상단 — 페이지 스크롤 무관 항상 보임

#### 4. 한글 폰트 lazy 임베드
- `shared/font-manager.js` — `embedKoreanFont(pdfDoc, fontKey)`
- 외부 ttf CDN 한 번만 fetch + 캐시
- `subset: false` 필수 (한글 CID glyph 버그 회피 — v0.2.2 fix)
- 도구별 polyfont 같은 게 아니라 페이지마다 다른 폰트도 한 PDF 안에 여러 폰트 임베드 가능

#### 5. 좌표·회전 변환 정형
- `pdfRot = -cssRot` (CSS 시계 vs PDF 반시계)
- 박스 중심 회전 → bottom-left 회전 변환 (drawText·drawRectangle·drawImage 모두 동일 공식)
- canvas px ↔ PDF pt: `scaleX = pw / canvasW`
- Y 뒤집힘: `pdfY = ph - canvasY`

#### 6. 동일 자원 캐시 (성능)
- 폰트: `Map(fontKey → font)` — 한 PDF 에 한 번씩만 embedFont
- 이미지 (서명): `Map(dataUrl → embeddedImg)` — 동일 서명 여러 페이지 reuse 시 한 번만 embedPng/embedJpg
- 페이지: 각 페이지 `_pageView` 와 `_viewport` 캐시

#### 7. 매번 PDF 새 load (누적 방지)
- `await PDFLib.PDFDocument.load(currentCore.bytes)` 를 apply 마다 호출
- 이전 apply 의 결과 (워터마크·텍스트) 가 누적되지 않게 보장
- ★ v0.2.5 회고: `getPDFLib()` 캐시 함정 — 같은 doc 객체 재사용 시 watermark 가 매번 쌓임

### 🐛 Phase 2 주요 버그 + 교훈

| 버전 | 증상 | 원인 | 교훈 |
|---|---|---|---|
| v0.2.2 | 한글 워터마크 "대외비" → "비" 만 표시 | pdf-lib + fontkit 의 Korean CID subset 버그 | `embedFont(bytes, { subset: false })` 강제 |
| v0.2.4 | sticky 미작동 | `.tool-with-sidebar { align-items: start }` | `stretch` + sidebar `align-self: start` |
| v0.2.5 | 워터마크 위치/회전 적용 X (UI 콘솔만 정상) | `getPDFLib()` 캐시된 PDFDocument 객체에 누적 | 매 apply 마다 `PDFLib.PDFDocument.load()` 새로 |
| v0.2.6 | 회전 방향 반대 | CSS clockwise vs PDF counter-clockwise | `pdfRot = -cssRot` 명문화 |
| v0.2.14 | 박스 폰트 reset (color 만 유지) | `makeBoxElement` 에서 `applyBoxStyle(el,box)` 가 `innerHTML = '<input ...>'` 보다 먼저 호출 → `el.querySelector('.ta-text')` null | innerHTML 먼저 → applyBoxStyle 그 후 |
| v0.2.17 | 서명 박스 핸들 invisible | CSS `.ta-handle` 의 base 스타일이 `.ta-box .ta-handle` 부모 종속 → `.sg-box .ta-handle` 미적용 | 부모 종속 X 일반 스타일 + selected 토글만 `.ta-box.selected, .sg-box.selected` 양쪽 매칭 |

### 🔍 Phase 2 UX 결정사항

- **버튼 위치** (v0.2.15): 페이지가 길어지면 `<main>` 끝 `tool-actions` 가 안 보임 → 사이드바 최상단 `.sidebar-action-group` 으로 이동. 사이드바 sticky 라 항상 보임
- **미리보기 크기 적정값** (v0.2.10~11): 1100px 너무 큼 → 800px max 적정. `.tool-stage` max-width 1200px
- **9-cell 위치 grid + 임의 클릭 위치** (v0.2.1~3): 프리셋 + 자유 둘 다 제공
- **회전 9-cell + 자유 입력** (v0.2.3): 0/45/90 등 프리셋 8개 + 중앙 자유 입력 + 사이드바 slider
- **박스별 스타일 vs 전체 공통** (v0.2.13): 박스별 더 자연 → 풀 에디터로 전환
- **멀티페이지 + 사이드바 sticky** (v0.2.14): 첫 페이지 only → 모든 페이지 vertical stack
- **lazy 렌더링** (v0.2.15): 5페이지도 버벅임 → IntersectionObserver
- **투명도 별도 slider** (v0.2.15): 글자색 + 배경 동시 → 단일 slider
- **서명 corner 핸들 비율 자동 유지** (v0.2.16): 서명 비율 중요 → corner 만 비율 고정, edge 는 자유

### 📦 코어 모듈 9개 (변경 X)

Phase 1 에서 굳혀진 9개 (`cdn` · `pdf-core` · `file-input` · `thumbnail` · `page-range` · `download` · `progress-ui` · `coordinate` · `font-manager`). Phase 2 신규 추가는 `font-manager.js` 하나.

### 📊 Phase 2 통계

- 패치 횟수: 18 (v0.2.0 → v0.2.17)
- 신규 도구: 6
- 신규 JS 파일: 7 (6 도구 + font-manager)
- 신규 CSS 누적 (.ta-* / .sg-* / .sig-* / .wm-* / .hf-*): 약 400줄
- PR: 약 20개 (각 패치 + 회고)
- 단일 세션 (2026-05-23 ~ 2026-05-24)

---

### 🎯 Phase 3 진입 준비 — 추출·변환

#### Phase 3 대상 도구 (4개)

devplan + dev.md §3.3 의 추출·변환 카테고리:

| # | 도구 | 폴더 | 핵심 기술 |
|---|---|---|---|
| 1 | PDF → 이미지 | `tools/to-image/` | pdf.js render → canvas → toBlob (JPG/PNG, 페이지별 zip) |
| 2 | 이미지 → PDF | `tools/from-image/` | PDFLib.PDFDocument.create + embedJpg/embedPng (사용자 결정 #6 부활 — 단순 이미지 → PDF 1개) |
| 3 | PDF → MD (OCR) | `tools/to-markdown/` | pdf.js extractTextContent + OCR fallback + AI 정리 |
| 4 | OCR (한컴 / Google Vision) | `tools/ocr/` | 외부 API + `shared/api-keys.js` |

> 신규 도구 4개. **카드 그리드 20/22** 활성 예정.

#### Phase 3 기술 결정 (사용자 재확인 필요)

| 항목 | 후보 |
|---|---|
| **이미지→PDF** | sejda·ilovepdf 패턴: 이미지 여러 장 업로드 → 페이지마다 한 장 → PDF 다운로드. 페이지 크기 옵션 (Original/A4/Letter), 여백 옵션 |
| **PDF→이미지 zip** | 페이지별 JPG/PNG 선택 + 해상도 (DPI), 셀렉트 선 (페이지 범위), zip 묶음 |
| **PDF→MD AI** | Gemini 호출 (이미지 에디터 / md2hwpx 패턴 reuse) |
| **OCR 키 모델** | `shared/api-keys.js` 의 `hancomOcr` + `googleVision` 슬롯 (image-editor 와 공유) |

#### Phase 3 가 도입할 신규 공용 인프라

- `shared/jszip-lazy.js` — JSZip CDN lazy load (PDF→이미지 zip)
- `shared/image-canvas.js` — 이미지 파일 → canvas → 리사이즈/JPEG 인코딩 (이미지→PDF, OCR 전처리)
- `shared/ocr-client.js` — 한컴 / Google Vision API 추상화 (이후 Phase 5 의 진짜 검색에서도 reuse)
- `shared/gemini-client.js` — Gemini API 호출 wrapper (image-editor / WeeklyBrief 패턴)

#### Phase 4·5 미리보기

- **Phase 4 (최적화·보안)**: 압축 (이미지 다운샘플) · 메타데이터 편집 · PDF/A 변환 (재고)
- **Phase 5 (뷰어·비교 + AI)**: 뷰어 hub (다른 도구 호출) · PDF 비교 · 진짜 검색 (OCR 결합) · PDF 번역 · 요약

### 🔄 Cache-bust

- `?v=0.2.17` → `?v=0.3.0` 일괄

### 🚧 다음 작업

- v0.3.1 (Phase 3 첫 도구) — PDF → 이미지 추출 (사용자 결정 후 진행)

---

## v0.2.17 — 2026-05-24 (서명 박스 핸들 표시 fix)

### 🐛 Fixed — 서명 박스의 모서리 핸들·회전 핸들 미표시

**증상**: 서명 박스 (`.sg-box`) 가 선택되어도 8 모서리 핸들·회전 핸들이 보이지 않음. 드래그 이동은 가능하지만 크기·회전 변경 X.

**원인**: v0.2.16 에서 핸들 DOM 자체는 `.ta-handle` / `.ta-rot-handle` 을 reuse 했으나, CSS 선택자가 `.ta-box .ta-handle`, `.ta-box.selected .ta-handle` 등 **`.ta-box` 부모 종속** 으로 작성됨. `.sg-box .ta-handle` 에는 매칭 X → 모든 핸들 스타일 (width/height/background/display) 미적용 → 화면에 보이지 않음. 박스 drag 는 mousedown 이벤트가 박스 외곽에 잘 작동했지만 핸들 자체는 invisible.

**Fix**: `.ta-handle` / `.ta-h-*` / `.ta-rot-handle` 의 스타일을 부모 종속 없이 일반화. selected 상태 토글만 `.ta-box.selected, .sg-box.selected` 양쪽 매칭:

```css
.ta-handle { /* 기본 스타일 — 부모 종속 X */ }
.ta-box.selected .ta-handle,
.sg-box.selected .ta-handle { display: block; }
.ta-rot-handle { /* 기본 스타일 */ }
.ta-box.selected .ta-rot-handle,
.sg-box.selected .ta-rot-handle { display: flex; }
```

→ 텍스트 추가·서명 두 도구 모두 정상 표시. 추후 추가 박스 도구도 `.xxx-box.selected .ta-handle` 만 selector 등록하면 reuse 가능.

### 🔄 Cache-bust

- `?v=0.2.16` → `?v=0.2.17` 일괄

---

## v0.2.16 — 2026-05-23 (Phase 2 #6 — 서명 도구 ★ Phase 2 완료)

### 🆕 서명 도구

폴더: `tools/signature/` · 카드 그리드 **16/22 활성**.

두 가지 서명 입력 방식:
1. **그리기** — 모달 캔버스 (700×280) 마우스/터치 직접 서명
2. **이미지 업로드** — PNG/JPG/WebP 파일 (투명 PNG 권장)

박스 패턴은 add-text 의 v0.2.15 풀 에디터 그대로 reuse:
- 드래그 이동
- 8 핸들 리사이즈 (**corner 핸들은 비율 자동 유지** — 서명은 비율 중요. edge 핸들은 자유, Shift 누르면 비율 강제)
- 회전 핸들 (Shift 15° 스냅)
- 박스별 투명도
- 멀티페이지 + lazy 페이지 렌더

### 🆕 서명 모달

- 탭 [✏️ 그리기] [📁 이미지 업로드]
- ESC 또는 ✕ 또는 배경 클릭 → 닫기
- "박스 추가" 버튼 — 그리기 비어있거나 업로드 없으면 disabled

**그리기 탭**:
- 색 3종 (검정·남색·빨강)
- 굵기 3단계 (얇음·보통·굵음)
- ↶ 되돌리기 (최대 30단계 history — `ImageData` stack)
- 지우기 (전체 clear)
- 마우스 + 터치 (touch-action: none)
- 캔버스 자체는 transparent → toDataURL 결과 투명 PNG

**업로드 탭**:
- 파일 선택 → preview + 파일명·크기 표시
- 즉시 추가 가능

### 🆕 사이드바 — 서명 전용

- "+ 서명 만들기 (그리기/업로드)" 큰 버튼
- 선택된 서명 panel: 미리보기 + 투명도 slider + 회전
- 서명 목록: 미니 이미지 미리보기 + 페이지 배지 + ✕

### 🆕 PDF 적용

- dataUrl → bytes 변환 (data: prefix 제거 + atob)
- JPG/PNG 자동 판별 → `embedJpg` or `embedPng`
- 동일 dataUrl 캐시 (한 서명을 여러 페이지에 사용 시 한 번만 임베드)
- `drawImage()` + 회전 + opacity 적용 (박스 중심 회전 → bottom-left 변환)

### 신규 CSS

- `.sg-box` — 서명 박스 (`.ta-handle` reuse)
- `.sg-img` — 서명 이미지 (object-fit: fill, pointer-events: none)
- `.sig-modal` — 모달 + .sig-modal-panel/head/foot
- `.sig-tab-bar` + `.sig-tab-btn` + `.sig-tab-panel`
- `.sig-draw-canvas` — 그리기 캔버스 (touch-action: none)
- `.sig-color-btn` (원형 색) + `.sig-width-btn` (텍스트 버튼)
- `.sig-upload-pick` + `.sig-upload-preview-box`
- `.ta-list-sig` — 서명 목록 미니 미리보기

### ✅ Phase 2 완료 정리 (v0.2.0 → v0.2.16)

| # | 도구 | 버전 |
|---|---|---|
| 1 | 워터마크 (텍스트) | v0.2.0~v0.2.6 |
| 2 | 워터마크 (이미지) | v0.2.7 |
| 3 | 페이지 번호 | v0.2.8 |
| 4 | 머리글·바닥글 | v0.2.9 |
| 5 | 텍스트 추가 (풀 에디터, 멀티페이지) | v0.2.12~v0.2.15 |
| 6 | 서명 | v0.2.16 |

공통 인프라:
- `shared/font-manager.js` — 한글 폰트 lazy 임베드 (3종)
- `shared/coordinate.js` — canvas↔PDF 좌표 변환
- `.tool-sidebar.sidebar-sticky` — 멀티페이지 뷰어 + sticky 도구 패턴
- IntersectionObserver lazy 페이지 렌더

### 🔄 Cache-bust

- `?v=0.2.15` → `?v=0.2.16` 일괄

### 🚧 다음 (Phase 3 진입 준비)

- v0.3.0 — Phase 2 완료 회고 + Phase 3 아이디어 정리 (추출·변환)
- Phase 3 후보: PDF→이미지, PDF→텍스트 (개선), PDF→MD (OCR), 이미지→PDF, OCR (한컴/Google)

---

## v0.2.15 — 2026-05-23 (텍스트 추가 — 투명도 + 적용 버튼 사이드바 이동 + lazy 페이지 렌더)

### 🆕 박스 투명도

- 사이드바 "선택된 박스" 패널에 **투명도 slider** (0~100%)
- DOM: 글자색 + 배경색 모두 rgba 로 변환해 opacity 반영 (CSS `inherit` 으로 input 도 따라옴)
- 핸들·회전 핸들·박스 외곽 border 는 영향 X (사용자 조작 가시성 유지)
- PDF apply: `drawText({ opacity })`, `drawRectangle({ opacity })` 동시 적용
- `box.opacity = 1` default

### 🔧 적용·초기화 버튼을 사이드바 상단으로

이전: `<div class="tool-actions">` 가 `<main>` 끝 → 모든 페이지 스크롤 후 노출.

신규:
- 사이드바 최상단에 `.sidebar-action-group` 그룹 추가
- "텍스트 적용 (다운로드)" `btn-primary` + "초기화" `btn-secondary` 묶음
- 사이드바가 sticky 라 화면 상단에 항상 보임
- 강조: accent 배경 (rgba 0.06) + accent border + bold 폰트
- 기존 `<div class="tool-actions">` 제거

### 🆕 페이지 lazy 렌더링 (IntersectionObserver)

이전: 모든 페이지를 직렬로 await render — 5페이지에 버벅임, 큰 PDF 면 더 심함.

신규 3단계:
1. **Stage 1**: 전 페이지의 placeholder 빠르게 생성 (회색 + "페이지 N" 텍스트 + "스크롤 시 자동 렌더링")
2. **Stage 2**: 첫 페이지 즉시 render (시야 보장)
3. **Stage 3**: 나머지 페이지는 `IntersectionObserver` 로 시야 들어오는 순간 render
   - `rootMargin: '400px 0px'` — 약간 미리 렌더 (스크롤 시 끊김 방지)
   - 한 번 렌더 후 `unobserve` (중복 방지)
   - `pi.rendered`, `pi._renderTask` 동시 호출 방지

### 효과 (체감)

| 페이지 수 | 이전 (v0.2.14) | **v0.2.15** |
|---|---|---|
| 5 | 살짝 버벅임 | 즉시 |
| 20 | 1~2초 멈춤 | 즉시 (첫 페이지만 렌더 후 노출) |
| 100 | 사실상 hang | 즉시 + 스크롤 시 자연스러움 |

### 신규 CSS

- `.tool-sidebar .sidebar-action-group` — accent 강조 배경 + border
- `.tool-sidebar .sidebar-action-group .btn-primary/btn-secondary` — 폰트 굵게 + 라운드

### 신규 JS 함수

- `renderPage(pi)` — 페이지 lazy render (중복 방지)
- `drawPlaceholder(canvasEl, pageNum)` — 회색 placeholder + 텍스트
- `hexToRgba(hex, a)` — opacity 반영 색

### 🔄 Cache-bust

- `?v=0.2.14` → `?v=0.2.15` 일괄

### 🚧 다음

- v0.2.16 — 서명 (canvas 그리기 + 이미지 업로드, Phase 2 마지막)
- v0.3.0 — Phase 2 완료 회고

---

## v0.2.14 — 2026-05-23 (텍스트 추가 — 멀티페이지 + 배경색 + 폰트 reset 버그 fix)

### 🐛 Fixed — 폰트 reset 버그

**증상**: 박스 1 만들고 폰트·크기 변경 → 새 박스 추가 → 박스 1 화면상 폰트가 기본값(나눔고딕)·기본 크기로 reset (색깔은 유지). 단 export PDF 는 정상.

**원인**: `makeBoxElement()` 에서 `applyBoxStyle(el, box)` 를 호출한 **후** `el.innerHTML = '<input ...>'` 으로 input 을 추가. applyBoxStyle 가 `el.querySelector('.ta-text')` 를 호출하는데 아직 input 이 mount 안 되어 있어 null → fontSize/fontFamily/textAlign 적용 안 됨. CSS 의 `.ta-box .ta-text { font-family: 'Nanum Gothic' ... }` 기본값으로 fallback. color 는 부모 .ta-box 의 inline color (= box.color) 를 `color: inherit` 으로 상속받으므로 유지됨.

**Fix**: `makeBoxElement` 에서 innerHTML 먼저 → 그 후 `applyBoxStyle(el, box)` 호출.

### 🆕 박스 배경색 (단색)

- 사이드바 "선택된 박스" 패널에 **배경색 체크박스 + color picker**
- 체크 시 박스에 단색 배경 (반투명 흰 배경 → bgColor)
- color picker 만 변경해도 자동으로 enable
- PDF 적용: `drawRectangle()` (회전 인식 — 박스 local bottom-left 회전 변환, `rotate: PDFLib.degrees(pdfRotDeg)`)
- 텍스트 보다 먼저 그려서 배경이 뒤로

### 🆕 멀티페이지 — 모든 페이지 vertical stack (뷰어 패턴)

이전 v0.2.13: 첫 페이지만. 사용자 요청: "모든 페이지를 뷰어처럼 밑으로 쭉 늘어트리고 도구를 sticky 형태로".

- **모든 페이지** 를 세로로 렌더링 (`.ta-pages-container`)
- 각 페이지 wrapper `.ta-page-wrap` — 페이지 번호 라벨 + 캔버스
- 페이지마다 박스 추가 가능 — 캔버스 클릭 → 그 페이지에 박스
- 박스 데이터에 `pageIndex` 추가 (0-based)
- 박스 element 는 해당 페이지의 `.ta-canvas-wrap` 안에 위치 (페이지 wrapper 가 부모 좌표계)
- 드래그·리사이즈·회전 좌표 변환 시 해당 페이지의 `canvasEl.getBoundingClientRect()` 사용

### 🆕 사이드바 sticky (멀티페이지 전용)

- `.tool-sidebar.sidebar-sticky` 클래스 추가 (CSS)
- `position: sticky; top: calc(var(--tph) + 16px); max-height: calc(100vh - var(--tph) - 32px); overflow-y: auto`
- 페이지 스크롤 시 사이드바는 화면에 고정, 옵션 길어도 자체 스크롤
- 워터마크 등 기존 도구는 미리보기 sticky (`.wm-sticky-group`) 유지 — 패턴 분리

### 🔧 사이드바 구조 변경

- "선택된 박스" panel 에 **배경색 form-row** 추가 (체크박스 + color + hex)
- 정렬 라벨 "글자색" 으로 명확화 (vs 배경색)
- 색 picker 크기 36×28 (기존 40×30) — 컴팩트
- 박스 목록 항목에 **p1 / p2 …** 페이지 배지 (멀티페이지 구분)
- 박스 목록 클릭 시 해당 박스로 자동 스크롤 (`scrollIntoView`)
- 박스 목록 max-height 280px + 자체 스크롤 (박스 많을 때)

### 🔧 적용 (apply) 변경

- 박스를 페이지별로 처리 (`src.getPage(box.pageIndex)`)
- 페이지마다 canvas → PDF scale 따로 계산 (페이지 크기 다를 수 있음)
- `b.text.trim() || b.bgEnabled` 필터 — 배경만 채워진 박스도 적용
- 성공 메시지: `{box}개 박스 / {page}개 페이지`

### 신규 CSS

- `.ta-pages-container` — 페이지 stack
- `.ta-page-wrap` — 페이지 panel (패딩 + border)
- `.ta-page-num` — 페이지 번호 라벨
- `.ta-page-canvas` — 캔버스 (흰 배경 + shadow)
- `.tool-sidebar.sidebar-sticky` — sticky 사이드바
- `.ta-list-page` — 박스 목록 페이지 배지

### 🔄 Cache-bust

- `?v=0.2.13` → `?v=0.2.14` 일괄

### 🚧 다음 (Phase 2 마무리)

- v0.2.15 — 서명 (canvas 그리기 + 이미지 업로드)
- v0.3.0 — Phase 2 완료 회고 + Phase 3 진입 준비

---

## v0.2.13 — 2026-05-23 (텍스트 추가 — 풀 에디터로 업그레이드)

### 🔧 Changed — image-editor 텍스트 교체 영역 패턴 이식

v0.2.12 의 텍스트 추가 도구가 너무 단순함 — 박스 이동만 가능, 폰트·크기·색이 **전체 박스 공통**, 정렬·회전·크기 변형 X. 사용자 요청으로 **image-editor 의 텍스트 교체 영역과 동일 UX** 로 풀 리팩토링.

### 🆕 박스별 인터랙션

- **빈 곳 클릭** → 새 박스 (이전: 한 점 클릭 → 자동 크기 input)
- **박스 클릭** → 선택 (핸들·회전 핸들 노출)
- **빈 곳 클릭 (선택 상태)** → 해제
- **ESC** → 선택 해제
- **Delete/Backspace** → 선택 박스 삭제 (텍스트 input 외부에서)

### 🆕 핸들 8개 (리사이즈)

- NW · N · NE · E · SE · S · SW · W (image-editor 와 동일)
- 회전 인식 — 회전된 박스도 OPPOSITE 코너 anchored, 회전 좌표계로 변환 후 리사이즈
- 최소 크기 30px
- 코드: `oppositeCornerLocal()` + 행렬 변환 (회전 행렬 + 역행렬)

### 🆕 회전 핸들

- 박스 상단 ↻ 동그라미 핸들
- 드래그로 회전 (마우스 각도 추적, 박스 중심 기준)
- **Shift** 누르면 15° 스냅
- 사이드바 회전 입력·슬라이더 와 양방향 sync

### 🆕 박스별 스타일 (per-box)

각 박스가 자기 스타일을 가짐 (이전: 전체 공통):
- 폰트 (3종 — 나눔고딕·굵게·명조)
- 크기 (6~200pt, wheel 비활성)
- 색 (color picker + hex 표시)
- **정렬** ⇤ ≡ ⇥ (좌·중·우, 가로) — 신규
- 회전 (0~360°, input + slider) — 신규

선택된 박스 panel 이 사이드바에 노출, 박스 선택에 따라 값 sync.

### 🆕 사이드바 재구성

| 그룹 | 내용 |
|---|---|
| 사용법 | 5단계 안내 (클릭·드래그·핸들·회전·키보드) |
| 선택된 박스 | 미선택 시 안내 / 선택 시 폰트·크기·색·정렬·회전 + 삭제 버튼 |
| 박스 목록 | 항목 클릭으로 선택, ✕ 로 개별 삭제, 선택 시 강조 |

### 🆕 정렬·회전 PDF 렌더 (apply)

- 박스 local 좌표 → 박스 중심 기준 offset → 회전 행렬 적용 → PDF world 좌표
- `pdfRot = -box.rotation` (CSS clockwise → PDF counter-clockwise, v0.2.6 패턴 재사용)
- 박스별 폰트 한 번씩만 임베드 (Map 캐시) — 동일 폰트 여러 박스 효율
- `font.widthOfTextAtSize()` 로 정렬 anchor 계산 (left/center/right)
- 세로 baseline 근사: `bh/2 - sizePt * 0.3` (중앙)

### 신규 CSS

- `.ta-box` — selected/dragging/resizing/rotating 상태
- `.ta-box .ta-text` — `position:absolute; inset:0` (박스 채움)
- `.ta-handle` (8개) + `.ta-rot-handle` (회전)
- `.ta-align-grid` + `.ta-align-btn` (좌/중/우)
- `.ta-list-item` (선택 가능, hover/selected/del)

### 🔄 Cache-bust

- `?v=0.2.12` → `?v=0.2.13` 일괄 (21개 파일)
- `pdftools.css?v=0.2.13`

### 🚧 다음 (Phase 2 마무리)

- v0.2.14 — 서명 (canvas 그리기 + 이미지 업로드)
- v0.3.0 — Phase 2 완료 회고 + Phase 3 진입 준비

---

## v0.2.12 — 2026-05-23 (Phase 2 #5 — 텍스트 추가 WYSIWYG)

### 🆕 텍스트 추가 도구

빈 양식 PDF (양식 필드 X) 에 직접 텍스트 박기. 신청서 양식 채우기 등.

- 폴더: `tools/add-text/`
- 카드 그리드 15/22 활성

### WYSIWYG 인터랙션
1. **canvas 클릭** → 그 위치에 텍스트 박스 (input) 추가 + 자동 포커스
2. **박스 안 input** 에 텍스트 입력 (실시간 너비 자동 조정)
3. **박스 드래그** (input 외 영역) 로 위치 이동
4. **✕ 버튼** 으로 박스 삭제
5. **"모두 삭제"** 로 전체 reset

### 옵션 (사이드바)
- 박스 사용법 안내
- 폰트 / 크기 / 색 (전체 박스 공통)
- 박스 목록 (번호 + 텍스트 미리보기)
- 모두 삭제 버튼

### 적용
- 첫 페이지에 모든 박스 텍스트 그리기
- 빈 박스는 무시 (텍스트 채워진 박스만)
- baseline 보정 — input 좌상단 → drawText 좌하단 (Y 뒤집힘 + sizePt 보정)

### 신규 CSS
- `.ta-canvas-wrap` — canvas cursor crosshair (클릭 가능 표시)
- `.ta-box` — floating 텍스트 박스 (accent 점선 + 흰 배경)
- `.ta-box.dragging` — accent2 실선
- `.ta-box .ta-text` — input (배경 투명, 폰트 inherit)
- `.ta-box .ta-remove` — 빨간 원형 ✕ 버튼

### 🔄 Cache-bust
- `pdftools.css?v=0.2.11` → `v=0.2.12`
- shared import URL `?v=0.2.12` 일괄

### 🚧 다음 (Phase 2 마무리)
- v0.2.13 — 서명 (canvas 그리기 + 이미지 업로드)
- v0.3.0 — Phase 2 완료 회고 + Phase 3 진입 준비

---

## v0.2.11 — 2026-05-23 (미리보기 크기 적정값 조정 — v0.2.10 의 1100 너무 컸음)

### 🔧 Changed — 사용자 피드백 즉시 반영

v0.2.10 (1100px) → "너무 커"

**조정**:
- `.tool-stage` max-width: 1400 → **1200px**
- canvas `targetW` max: 1100 → **800px**

### 효과 (이전 v0.2.10 vs 신규 v0.2.11)
| 화면 너비 | v0.2.9 (이전) | v0.2.10 (너무 큼) | **v0.2.11 (적정)** |
|---|---|---|---|
| 1920×1080 | ~600px | 1100px | **~800px** |
| 1366×768 | ~600px | 1000px | **~800px** |

옵션 사이드바와 미리보기 동시 가시.

### 🔄 Cache-bust
- `pdftools.css?v=0.2.10` → `v=0.2.11`
- 5개 도구 .js `?v=0.2.11` + shared import URL 모든 버전 통일

---

## v0.2.10 — 2026-05-23 (미리보기 canvas 크기 확장 — 사이드바 도구 5개)

### 🔧 Changed — 사용자 요청 fix

사용자 의견: "미리보기와 패널 사이 여백이 남는다, 미리보기가 더 커도 좋겠다".

이전: `.tool-stage { max-width: 900px }` → 사이드바 280px 빼면 미리보기 영역 ~600px. 화면 좌우 빈 공간 큼.

**수정**:

#### 1. tool-stage max-width 확장
```css
.tool-stage {
  max-width: 1400px;  /* 900 → 1400 */
}
```

#### 2. canvas targetW 를 `.tool-main` 폭 기반 동적 계산
이전:
```js
const targetW = Math.min(680, previewWrap.clientWidth || 680);  // 항상 680 cap
```

신규:
```js
const mainEl = canvas.closest('.tool-main');
const availW = mainEl ? mainEl.clientWidth - 4 : 680;
const targetW = Math.max(400, Math.min(1100, availW));
```

`.tool-main` 의 실제 가용 폭 기반 (사이드바 빼고 남는 공간). max 1100px, min 400px.

### 영향 도구 (사이드바 + canvas 미리보기 있는 5개)
- 워터마크 (텍스트) — `watermark-text.js`
- 워터마크 (이미지) — `watermark-image.js`
- 페이지 번호 — `page-number.js`
- 머리글·바닥글 — `header-footer.js`
- 자르기 — `crop.js`

### 효과
- 1920×1080 화면: 미리보기 ~1100px (이전 ~600px) — **약 1.8배**
- 1366×768 화면: 미리보기 ~1000px
- 모바일 (≤768px): grid 가 단일 컬럼으로 자동 전환 → 영향 X

### 🔄 Cache-bust
- `pdftools.css?v=0.2.9` → `v=0.2.10`
- 5개 도구 .js `?v=0.2.10`
- 다른 도구의 shared import URL 도 `?v=0.2.10` 일괄

---

## v0.2.9 — 2026-05-23 (Phase 2 #4 — 머리글·바닥글 사용 가능)

### 🆕 머리글·바닥글 도구

PDF 페이지마다 상단·하단 텍스트 추가. 좌·중·우 6칸 (HL/HC/HR + FL/FC/FR).

- 폴더: `tools/header-footer/`
- 라이브: `https://sobjil-gdi-apps.mycafe24.ai/pdftools/tools/header-footer/`
- 카드 그리드 14/22 활성

### 6칸 구조
```
┌───── 머리글 (상단) ─────┐
│ 좌측  │  중앙  │  우측  │
├──────────────────────┤
│                      │
│      페이지 내용      │
│                      │
├──────────────────────┤
│ 좌측  │  중앙  │  우측  │
└───── 바닥글 (하단) ─────┘
```

각 칸 자유 텍스트 입력. 빈 칸은 그리지 X.

### 페이지 번호 변수
- `{n}` = 현재 쪽 (사용자 시각, fromPage 부터 1로 시작)
- `{total}` = 총 쪽

예: 우측 머리글에 `{n} / {total}` → 페이지마다 "1 / 10", "2 / 10" ...

### 옵션
- **6칸 텍스트**: 모두 자유 입력 (60자, 변수 사용 가능)
- **폰트**: 3종 (공통)
- **크기**: 6~36pt (default 10)
- **색**: color picker (default 진한 회색)
- **여백**: 페이지 가장자리에서 거리 (default 30pt)
- **적용 페이지**: 모든 페이지 / 특정 페이지부터

### pdf-lib 정렬 패턴
- 좌측 정렬: `x = marginPt`
- 중앙 정렬: `x = pw/2 - textW/2`
- 우측 정렬: `x = pw - marginPt - textW`

### 텍스트 워터마크 패턴 재사용
- sticky group, font-manager, 매번 새 src load, UI active 우선

### 신규 CSS
- `.hf-overlay` — 머리글·바닥글 미리보기 (6개 absolute, top/bottom/left/right + translateX)

### 🔄 Cache-bust
- `pdftools.css?v=0.2.8` → `v=0.2.9`
- shared import URL `?v=0.2.9` 일괄

### 🚧 다음 (Phase 2 마무리)
- v0.2.10 — 텍스트 추가 (WYSIWYG, 자유 위치)
- v0.2.11 — 서명 (canvas 그리기 + 이미지)
- v0.2.x 또는 v0.2.0 으로 Phase 2 회고 묶음 (Phase 1 처럼)

---

## v0.2.8 — 2026-05-23 (Phase 2 #3 — 페이지 번호 사용 가능)

### 🆕 페이지 번호 도구

PDF 각 페이지에 페이지 번호 추가. 한국 행정 문서 관례 프리셋 4가지 + 사용자 정의 포맷.

- 폴더: `tools/page-number/`
- 라이브: `https://sobjil-gdi-apps.mycafe24.ai/pdftools/tools/page-number/`
- 카드 그리드 13/22 활성

### 포맷 프리셋
| 프리셋 | 결과 |
|---|---|
| **`{n}쪽`** (default) | 1쪽, 2쪽 |
| `{n} / {total}쪽` | 1 / 10쪽 |
| `- {n} -` | - 1 - |
| `{n}` | 1 |
| 사용자 정의 | `p. {n}`, `{n}/{total}`, `{n}ページ` 등 |

`{n}` = 현재 쪽, `{total}` = 총 쪽. 사용자 정의에 둘 다 사용 가능.

### 옵션
- **시작 번호**: 첫 번호 (default 1, 예: 표지 제외 시 0 또는 i)
- **위치**: 9분할 grid (default = **하단 중앙**, 가장 흔한 위치) + 드래그
- **폰트**: 나눔고딕 / 나눔고딕 굵게 / 나눔명조 (한글 임베드)
- **크기**: 6~48pt (default 11)
- **색**: color picker (default 검정)
- **적용 페이지**: 모든 페이지 / 특정 페이지부터 (예: 3쪽부터 시작 → 표지·차례 제외)

### 텍스트 워터마크 패턴 재사용
- sticky group (`.wm-sticky-group`)
- 9분할 위치 + 드래그
- 매번 새 `PDFDocument.load` (누적 방지)
- UI active 우선 읽기
- `font-manager.embedKoreanFont` (subset: false)

### pdf-lib 패턴
```js
const src = await PDFLib.PDFDocument.load(currentCore.bytes);
const font = await embedKoreanFont(src, fontKey);

allPages.forEach((page, i) => {
  if (i + 1 < fromPage) return;
  const n = startN + (i + 1 - fromPage);
  const text = template.replace('{n}', n).replace('{total}', total);
  // 위치 + baseline 보정 (회전 X)
  page.drawText(text, { x, y, size, font, color });
});
```

### 🔄 Cache-bust
- `pdftools.css?v=0.2.7` → `v=0.2.8`
- 모든 도구 .js 의 shared import URL `?v=0.2.8` 일괄
- version-badge 일괄

### 🚧 다음 (Phase 2 진행)
- v0.2.9 — 머리글·바닥글 (좌·중·우 6칸 + 페이지 번호 변수)
- v0.2.10 — 텍스트 추가 (WYSIWYG)
- v0.2.11 — 서명 (canvas 그리기 + 이미지)

---

## v0.2.7 — 2026-05-23 (Phase 2 #2 — 워터마크 (이미지) 사용 가능)

### 🆕 워터마크 (이미지) 도구

로고·도장 이미지 (PNG/JPG) 를 PDF 페이지에 워터마크로 추가.

- 폴더: `tools/watermark-image/`
- 라이브: `https://sobjil-gdi-apps.mycafe24.ai/pdftools/tools/watermark-image/`
- 카드 그리드 12/22 활성

### 사용 흐름
1. PDF 1개 드롭
2. 사이드바 "이미지 선택" → PNG/JPG 파일 선택
3. 크기 (5~100% 페이지 너비), 투명도, 위치, 회전 옵션 조정
4. "워터마크 적용" → 결과 PDF 다운로드

### pdf-lib 패턴
```js
const src = await PDFLib.PDFDocument.load(currentCore.bytes);  // 매번 새 doc (누적 방지)
const embeddedImg = type === 'png'
  ? await src.embedPng(imgBytes)
  : await src.embedJpg(imgBytes);
page.drawImage(embeddedImg, {
  x, y, width, height, opacity,
  rotate: PDFLib.degrees(pdfRot)  // CSS 시계 → PDF 반시계 보정
});
```

### 텍스트 워터마크 패턴 모두 재사용
v0.2.0~v0.2.6 에서 잡힌 fix 패턴 모두 적용:
- 사이드바 + 미리보기 sticky group (`.wm-sticky-group`)
- 9분할 위치 grid + 드래그 (`POS_MAP`, `wmPos`)
- 9방향 회전 grid + 자유 입력 (`rotBtns`, `rotInput`)
- 매번 새 PDFDocument.load (누적 방지)
- CSS 시계 ↔ PDF 반시계 보정 (`pdfRot = -rot`)
- `rotInput` wheel 비활성화
- UI active 우선 읽기 (강건화)
- 적용 직전 console.log

### 신규 CSS
- `.wm-img-input`, `.wm-img-pick`, `.wm-img-info`, `.wm-img-thumb` — 이미지 선택 영역
- `.wm-img-overlay` — canvas 위 `<img>` 미리보기 (드래그 가능)

### 외부 라이브러리 (HTML 정적 로드)
- pdf-lib 1.17.1 (텍스트 워터마크의 fontkit 제외 — 이미지는 폰트 불필요)

### 🔄 Cache-bust
- `pdftools.css?v=0.2.6` → `v=0.2.7`
- `watermark-text.js` 의 shared import URL 도 `?v=0.2.7` (일관성)
- version-badge 일괄 v0.2.7

### 🚧 다음 (Phase 2 진행)
- v0.2.8 — 페이지 번호 + 머리글·바닥글 묶음
- v0.2.9 — 텍스트 추가 (WYSIWYG)
- v0.2.10 — 서명 (canvas 그리기 + 이미지)

---

## v0.2.6 — 2026-05-23 (정보 카드 sticky + 회전 방향 fix)

### 🆕 정보 카드도 미리보기와 함께 sticky

미리보기 (`.wm-preview`) 와 결과 정보 (`.result-preview`) 를 **`.wm-sticky-group` wrapper** 로 묶고 wrapper 를 sticky.

```html
<div class="wm-sticky-group">  <!-- 신규 wrapper -->
  <div class="wm-preview">...</div>
  <div class="result-preview">...</div>
</div>
```

이제 페이지 스크롤 시 미리보기 + 결과 정보 카드 둘 다 화면 상단에 따라옴.

### 🐛 Fixed — 회전 방향 (CSS 시계 vs PDF 반시계)

**증상**: 사용자가 225° 선택 → 미리보기는 좌하 방향, 결과 PDF 는 우상 방향. 회전 방향이 반대.

**원인**: 두 시스템의 회전 방향 기본값이 다름:
- **CSS `transform: rotate(N deg)`**: 시계방향 (시각 일반 직관)
- **pdf-lib `rotate: degrees(N)`**: **반시계방향** (수학 표준)

미리보기 = CSS 시계, 결과 = PDF 반시계 → 사용자가 본 방향과 결과가 반대.

**수정**: PDF 에 `-rot` 적용해서 CSS 와 일치시킴:
```js
const pdfRot = -rot;
const rad = pdfRot * Math.PI / 180;  // 좌표 보정도 같은 부호
// ...
page.drawText(text, {
  ...,
  rotate: PDFLib.degrees(pdfRot)  // 시계방향 (CSS 와 일치)
});
```

좌표 보정의 `rad` 도 `pdfRot` 로 계산. 회전 행렬 일관성 유지.

이제 사용자가 9방향 grid 클릭 (45° = 우상 대각선) → 미리보기 우상 대각선 → 결과 PDF 도 우상 대각선.

### 🔄 Cache-bust
- `pdftools.css?v=0.2.5` → `v=0.2.6`
- `watermark-text.js?v=0.2.5` → `v=0.2.6`
- shared import URL `?v=0.2.6` 일괄

---

## v0.2.5 — 2026-05-23 (★ 워터마크 누적 fix — src 매번 새 load + sticky 정정)

### 🐛 진짜 원인 — `pdf-core.js` 의 `getPDFLib()` 캐시

v0.2.4 의 콘솔 로그가 정확한 값을 보여줘서 추적 가능:
```
[워터마크 적용] { rot: 225, pos: {x: 0.1, y: 0.1}, ... }
```

데이터 흐름 자체는 OK. 그런데 결과 PDF 는 매번 같은 "중앙 + 270°".

**진짜 원인**: `currentCore.getPDFLib()` 는 **PDFDocument 인스턴스를 캐시**. 매번 같은 doc 을 in-place 수정. 사용자가 워터마크 적용을 두 번 하면 같은 doc 에 **워터마크 누적**.

- 1차 적용 (default 값): 중앙 + 45° → 다운로드. 결과 PDF 에 워터마크 1개.
- 2차 적용 (사용자가 옵션 변경, 225° + 좌상단): 같은 doc 에 **또** 워터마크 추가. 다운로드한 결과 PDF 에 **2개** 워터마크. 단, 225° 회전 + 좌상단 위치는 회전 후 텍스트가 페이지 밖으로 나가 안 보임 → 사용자는 첫 번째 (중앙 45°) 만 봄.
- 사용자 시각: "위치 변경 안 됨, 회전 안 됨"

### 수정

`btnApply` 안에서 **`currentCore.bytes` 에서 매번 새 PDFDocument 로 load**. 캐시 우회.

```js
// 이전 (캐시된 src, 누적됨)
const src = await currentCore.getPDFLib();

// 신규 (매번 새 doc, 깨끗한 원본)
const src = await PDFLib.PDFDocument.load(currentCore.bytes);
```

매번 깨끗한 원본 + 워터마크 1개 → 옵션 정확히 반영.

### 🔧 Sticky 미리보기 정정 (v0.2.4 에서도 작동 X 였던 이유)

**원인**: `.tool-with-sidebar { align-items: start; }` 였음 → `.tool-main` 의 height = 자기 자녀 (= 미리보기) height. sticky scroll 가능 범위 = 0.

**수정**:
```css
.tool-with-sidebar {
  align-items: stretch;  /* .tool-main 이 사이드바 만큼 stretch */
}
.tool-main {
  align-items: flex-start;  /* 자녀 stretch 방지 */
}
.tool-sidebar {
  /* position: sticky 제거 — 사이드바는 자연스럽게 길어짐 */
  align-self: start;
}
```

이제 사이드바 길어 페이지 스크롤 시 .tool-main 도 같은 height → 그 안 .wm-preview 가 viewport 안에서 sticky 로 따라옴.

### 🔄 Cache-bust
- `pdftools.css?v=0.2.4` → `v=0.2.5`
- `watermark-text.js?v=0.2.4` → `v=0.2.5`
- shared import URL `?v=0.2.5` 일괄

### 🐛 알려진 이슈 (다음 fix)
회전 225° + 좌상단 (0.1, 0.1) 위치에선 텍스트가 회전 후 페이지 밖으로 나가 안 보일 수 있음 (좌표 보정 한계). 사용자가 미리보기에서 본 위치와 결과 PDF 가 일부 어긋날 수 있음. 위치 자체는 정확히 적용되지만 회전된 텍스트가 페이지 경계 넘어가는 케이스. v0.2.x patch 에서 좌표 보정 + 페이지 경계 안 clamp 옵션 검토.

---

## v0.2.4 — 2026-05-23 (워터마크 미리보기 sticky + 회전/위치 적용 안 되는 버그 fix)

### 🆕 미리보기 sticky (사용자 UX 요청)

옵션 조정 시 사이드바가 길어 페이지를 스크롤하면 미리보기가 화면 밖으로 사라지는 문제. CSS `position: sticky` 로 미리보기가 화면 상단에 고정되도록.

```css
.wm-preview {
  position: sticky;
  top: calc(var(--tph) + 16px);  /* topbar 아래 */
  align-self: flex-start;
  z-index: 5;
}
```

### 🐛 Fixed — 회전/위치 적용 안 되는 버그

증상:
- 어떤 회전 각도 클릭해도 결과 PDF 는 315° 로 적용됨
- 위치를 옮겨도 (9방향 / 드래그) 결과 PDF 는 중앙 고정

원인 추정: **number input 의 wheel 동작** — 페이지 스크롤 시 마우스 wheel 이 `<input type="number">` 위에 있으면 자동으로 값이 변경됨 (브라우저 기본 동작). 사용자가 옵션 조정 → 페이지 스크롤 → 의도 X 한 wheel 이 input 의 wmRotation 값을 임의 변경.

### 수정

#### 1. wheel 이벤트 비활성화
```js
rotInput.addEventListener('wheel', e => e.preventDefault(), { passive: false });
```

#### 2. UI active 우선 읽기 (강건화)
btnApply 시점에 wmRotation 변수 대신 **`.wm-rot-btn.active` 의 dataset.rot 우선** 읽기:
```js
const rotActiveBtn = document.querySelector('.wm-rot-btn.active');
const rot = rotActiveBtn
  ? parseInt(rotActiveBtn.dataset.rot)
  : (parseFloat(rotInput.value) || wmRotation);
```

active 버튼이 있으면 그게 사용자 의도 — 변수 동기화 문제 회피.

#### 3. 디버그 로그
적용 직전 콘솔에 값 출력 (다음 fix 시 추적 도움):
```js
console.log('[워터마크 적용]', { text, rot, pos, sizePt, opacity });
```

### 🔄 Cache-bust
- `pdftools.css?v=0.2.3` → `v=0.2.4`
- `watermark-text.js?v=0.2.3` → `v=0.2.4`
- shared import URL `?v=0.2.4` 일괄

---

## v0.2.3 — 2026-05-23 (워터마크 회전 각도 9방향 grid + 자유 입력)

### 🔧 Changed — 회전 각도 UI 전면 개편

이전 v0.2.0 의 3가지 라디오 (0° / 45° / 90°) → 9방향 grid 패턴 (위치 grid 와 동일 디자인).

#### 9방향 grid 레이아웃
| ↖ 315° | ↑ 0° | ↗ 45° |
|---|---|---|
| ← 270° | **[입력]** | → 90° |
| ↙ 225° | ↓ 180° | ↘ 135° |

- **8방향 프리셋 버튼**: 45° 간격 (0, 45, 90, 135, 180, 225, 270, 315)
- **중앙 텍스트 input**: 사용자가 임의 각도 직접 입력 (-360 ~ 360, step 1)
  - 입력 시 다른 8방향 active 해제
  - 입력 값이 프리셋과 일치하면 해당 버튼 자동 active 표시
- 프리셋 버튼 클릭 시 input 값도 동기화

#### 사용 시나리오
- 흔한 워터마크 각도 — 프리셋 클릭 한 번
- 임의 각도 (예: 30°, 60°, 7° 등) — 중앙 input 직접 입력
- 음수 각도 — 입력 가능 (예: -30°)

### 🆕 Added — CSS 컴포넌트
- `.wm-rot-grid` — 3×3 grid (위치 grid 와 같은 디자인)
- `.wm-rot-btn` — 프리셋 버튼 (active 강조)
- `.wm-rot-input` — 중앙 input (focus 시 accent ring)

### 🔄 Cache-bust
- `pdftools.css?v=0.2.2` → `v=0.2.3`
- `watermark-text.js?v=0.2.2` → `v=0.2.3` + shared import URL 일괄 v0.2.3

### 🐛 알려진 이슈 (다음 fix 예정)
- 회전 시 워터마크 위치가 미리보기와 PDF 결과에서 약간 어긋남 (좌표 보정 baseline 계산 미세 오차). 사용자가 드래그로 미세 조정 가능하나 v0.2.x patch 에서 baseline 보정값 fine-tune 예정.

---

## v0.2.2 — 2026-05-23 (한글 워터마크 글자 깨짐 fix — subset 끄기)

### 🐛 Fixed — pdf-lib + fontkit 의 한글 글리프 subset 매핑 버그

**증상**: 워터마크 "대외비" 입력 → 결과 PDF 에는 **"비" 만** 표시. 앞 글자 ("대", "외") 가 빈 글리프로 매핑됨.

**원인**: `pdf-lib 1.17.1 + @pdf-lib/fontkit 1.1.1` 의 한글 CID 폰트 subset 처리 버그 — `embedFont(bytes, { subset: true })` 시 일부 한글 글리프가 누락되거나 매핑 깨짐. 라틴 폰트는 정상 동작하지만 한글 같은 CJK 의 큰 글리프 셋에서 자주 발생.

**수정**: `font-manager.js` 의 `embedKoreanFont` 기본 옵션 `subset` 을 **`true` → `false`** 로 변경:
```js
// 이전
const font = await pdfDoc.embedFont(bytes, { subset: true });
// 신규 (default)
const font = await pdfDoc.embedFont(bytes, { subset: false });
```

옵션 매개변수도 추가 — 미래 사용자가 명시적으로 `subset: true` 시도 가능:
```js
export async function embedKoreanFont(pdfDoc, fontKey, options = {}) {
  const subset = options.subset ?? false;
  ...
}
```

### 트레이드오프

- **장점**: 한글 글자 깨짐 X — 모든 한글 정상 표시
- **단점**: 결과 PDF 크기 **+4MB** (나눔고딕 전체 임베드). 원본 2MB PDF + 워터마크 → 결과 6MB

### 🔄 Cache-bust
- `pdftools.css?v=0.2.1` → `v=0.2.2`
- 모든 도구 .js 의 shared import URL `?v=0.2.2` (font-manager.js 변경)
- version-badge 일괄

---

## v0.2.1 — 2026-05-23 (워터마크 텍스트 fix + 위치 임의 지정)

### 🐛 Fixed — font-manager.js 의 cdn.js import 캐시

**증상**: 워터마크 적용 시 "지원하지 않는 폰트 키: nanumGothicBold (사용 가능: )" 에러.

**원인**: `shared/font-manager.js` 의 `import { CDN } from './cdn.js'` 에 `?v=` 없음 → 브라우저가 v0.2.0 의 새 `font-manager.js` 는 로드했지만, 그 안 import 한 `cdn.js` 는 **이전 버전 (CDN.fonts 없음)** 의 캐시 사용. `CDN.fonts` 가 `undefined` 라 모든 폰트 키가 lookup 실패.

**수정**: shared 모듈끼리도 import URL 에 `?v=` 박는 표준 패턴 도입:
```js
// 이전
import { CDN } from './cdn.js';
// 신규
import { CDN } from './cdn.js?v=0.2.1';
```

매 PR 마다 shared 모듈 변경 시 cross-import URL 도 같이 bump.

### 🆕 Added — 워터마크 위치 임의 지정 (사용자 요청)

이전 v0.2.0 은 페이지 중앙 고정. v0.2.1 은 자유 위치.

#### 9분할 quick buttons
사이드바 "위치" 그룹에 3×3 버튼 그리드. 좌상/상중/우상 / 좌중/**중앙(기본)**/우중 / 좌하/하중/우하.
- 각 위치 = canvas 비율 (예: 좌상 = 10%, 10% / 중앙 = 50%, 50%)
- 클릭 시 active 표시 + 미리보기 즉시 이동

#### 미리보기 드래그 (미세 조정)
- `wm-overlay` 가 `pointer-events: auto` + `cursor: move`
- mousedown → mousemove → 위치 비율 갱신 (0~1, clamp)
- mouseup 시 종료
- 드래그 중엔 9분할 active 해제 (사용자 정의 위치 = 어느 quick 위치와도 불일치)

#### 좌표 모델
- `wmPos = { x: 0~1, y: 0~1 }` — canvas 좌상단 기준 비율
- 페이지 크기가 다양해도 같은 상대 위치 유지 (각 페이지마다 `cx = wmPos.x * pageW`)
- 적용 시 Y 뒤집힘 보정 (`cy = ph - wmPos.y * ph`) + 회전 행렬 보정

#### 결과 미리보기 라벨
- 중앙 (50%, 50%) — "중앙"
- 그 외 — "(70%, 30%)" 같은 위치 좌표

### 🔄 Cache-bust
- `pdftools.css?v=0.2.0` → `v=0.2.1`
- `watermark-text.js?v=0.2.0` → `v=0.2.1`
- `shared/font-manager.js` 의 `cdn.js` import → `?v=0.2.1`
- version-badge 일괄

phase-note 는 마일스톤 (v0.2.0) 표시 유지 — patch 버전은 별도.

---

## v0.2.0 — 2026-05-23 (★ Phase 2 진입 — font-manager.js + 워터마크 텍스트)

### 🎉 Phase 2 시작 — 콘텐츠 편집 카테고리

`coordinate.js` 위에 한글 폰트 인프라 (`font-manager.js`) 추가. 콘텐츠 편집 6개 도구의 첫 도구 (워터마크 텍스트) 진입.

### 🆕 Added — `shared/font-manager.js`

한글 폰트 lazy 임베드 모듈. Phase 2 콘텐츠 편집 카테고리 (워터마크·페이지번호·머리바닥글·텍스트 추가) 의 핵심 인프라.

- **`loadFontBytes(url)`** — CDN 에서 `.ttf` 다운로드 + ArrayBuffer 캐시 (URL 단위)
- **`embedKoreanFont(pdfDoc, fontKey)`** — fontkit 1회 등록 + `pdfDoc.embedFont(bytes, { subset: true })`. subset 활성으로 사용된 글리프만 임베드 (파일 크기 절감)
- **`getAvailableFonts()`** — UI 드롭다운용 폰트 목록
- **`clearFontCache()`** — 메모리 절약용 (보통 호출 X)

3종 한글 폰트 (CDN, Google Fonts 의 .ttf 직접 — OFL 라이선스):
- 나눔고딕 (`nanumGothic`)
- 나눔고딕 굵게 (`nanumGothicBold`)
- 나눔명조 (`nanumMyeongjo`)

`cdn.js` 에 `CDN.fonts` 객체 추가 — 폰트 URL 일괄 관리.

### 🆕 Added — 워터마크 (텍스트) 도구

폴더: `tools/watermark-text/`. 한글 텍스트 워터마크를 페이지 중앙에 추가.

#### UX
- 좌측: 첫 페이지 미리보기 (canvas) + 워터마크 overlay 텍스트 (실시간 시각화)
- 우측 사이드바: 텍스트 / 폰트 / 크기 / 색 / 투명도 / 회전 / 적용 페이지
- 옵션 변경 시 미리보기 즉시 갱신 (input 이벤트)
- 미리보기는 화면 비율에 맞춰 pt → px 환산 (canvasW / pdfPageW 스케일)

#### 옵션
- **텍스트**: 한글 OK, 40자 제한
- **폰트**: 3종 드롭다운
- **크기**: 8~400pt (default 80)
- **색**: HTML5 color picker (default 빨강 `#dc2626`)
- **투명도**: 5~100% slider (default 30)
- **회전**: 0° / 45° / 90° 라디오 (default 45°)
- **적용 페이지**: 모든 페이지 / 첫 페이지만

#### pdf-lib 패턴
- `pdfDoc.registerFontkit(window.fontkit)` 1회
- `pdfDoc.embedFont(bytes, { subset: true })`
- `page.drawText(text, { font, size, color: PDFLib.rgb(r,g,b), opacity, rotate: degrees(N), x, y })`
- 회전 중심을 페이지 중앙으로 설정하기 위해 텍스트 폭/높이 계산 후 회전 행렬로 위치 보정

### 🆕 Added — CSS 컴포넌트
- `.wm-preview` — canvas 컨테이너 (position: relative)
- `.wm-overlay` — 중앙 + transform translate + rotate + opacity transition

### 🔧 Changed — index.html
- 워터마크 (텍스트) 카드 활성 (11/22)
- phase-note: "✅ Phase 1 완료" → "🚧 Phase 2 진행"

### 🔄 Cache-bust
- `pdftools.css?v=0.1.5` → `v=0.2.0`
- 모든 도구 .js 의 shared import URL `?v=0.2.0` (cdn.js 변경, font-manager.js 신규)
- version-badge 일괄 v0.2.0

### 🚧 다음 (Phase 2 진행)
- v0.2.1 — 워터마크 (이미지): `drawImage` 패턴
- v0.2.2 — 페이지 번호 + 머리글·바닥글 묶음
- v0.2.3 — 텍스트 추가 (WYSIWYG, coordinate.js 활용)
- v0.2.4 — 서명 (canvas 그리기 + 이미지)

---

## v0.1.5 — 2026-05-23 (썸네일 hover 시 selected 테두리 사라지는 버그 fix)

### 🐛 Fixed — CSS specificity 충돌

선택된 썸네일에 마우스 hover 시 accent 보더가 사라지고 회색 보더로 바뀌는 버그.

**원인**: CSS specificity 비교
- `.thumb-grid.thumb-selectable .thumb-item:hover` → **0,4,0** (3 클래스 + :hover)
- `.thumb-item.thumb-selected` → 0,2,0 (2 클래스)

hover 셀렉터가 더 specific 해서 `border-color: var(--border-strong)` 이 `border-color: var(--accent)` 를 덮어쓰기.

**수정**: selected + hover 조합에 명시적 룰 추가
```css
.thumb-grid.thumb-selectable .thumb-item.thumb-selected,
.thumb-grid.thumb-selectable .thumb-item.thumb-selected:hover {
  border-color: var(--accent);  /* 항상 accent 유지 */
}
.thumb-grid.thumb-selectable .thumb-item.thumb-selected:hover {
  background: rgba(74, 158, 255, 0.15);  /* hover 시 살짝 더 진한 accent 배경 */
}
```

영향: 회전·페이지 삭제·페이지 추출·자르기 (selectable 사용 도구 전부)

### 🔄 Cache-bust
- `pdftools.css?v=0.1.4` → `v=0.1.5` (모든 페이지)
- version-badge 일괄

도구 .js 변경 X — import URL `?v=` 그대로 유지.

---

## v0.1.4 — 2026-05-23 (회전 도구 버그 fix 2건)

### 🐛 Fixed

#### 1. 상하/좌우 반전 시 페이지 백지 (rotate.js)

원인: pdf-lib `drawPage` 옵션에서 `width`/`height` 와 `xScale`/`yScale` 을 **동시 지정**하면 width/height 가 우선 적용되어 xScale 음수 (flip) 가 무시됨 → 빈 페이지 그려짐.

수정: flip 분기에서 `width`/`height` 옵션 제거. `xScale`/`yScale` 만 사용:
```js
// 좌우 flip — width/height 빼고 xScale: -1 + x: w
newPage.drawPage(ep, { x: w, y: 0, xScale: -1, yScale: 1 });
// 상하 flip
newPage.drawPage(ep, { x: 0, y: h, xScale: 1, yScale: -1 });
```

정상 페이지 (변환 대상 X) 도 `width`/`height` 빼고 native 크기로 그리도록 단순화.

#### 2. 썸네일 이미지 클릭 작동 X (전체 도구 영향)

원인: v0.1.1 에서 `shared/thumbnail.js` 에 이미지 클릭 토글 코드 추가했지만, 도구 .js 의 import URL 에 `?v=` 가 없어서 브라우저가 **구 thumbnail.js 캐시** 사용. AISpace 정적 호스팅의 ETag 만으론 캐시 무효화 불충분 케이스.

수정: 모든 도구 .js 의 shared import URL 에 `?v=0.1.4` 일괄 박기 (sed 일괄). 미래 shared 모듈 변경 시도 같은 패턴으로 bump.

```js
// 이전
import { setupPdfJs } from '../../shared/cdn.js';
// 신규
import { setupPdfJs } from '../../shared/cdn.js?v=0.1.4';
```

이 패턴이 표준 — 매 PR 마다 도구 .js bump 시 import URL ?v= 도 함께 갱신.

### 🔄 Cache-bust

- `pdftools.css?v=0.1.3` → `v=0.1.4` (변경 X 이지만 동기화)
- 모든 도구 .js: `?v=*` → `v=0.1.4`
- 모든 도구의 shared import URL: 신규 `?v=0.1.4`
- version-badge 일괄 v0.1.4

---

## v0.1.3 — 2026-05-23 (사용자 fix 3차 — 자르기 개편)

### 🔧 Changed — 자르기 도구 전면 개편

사용자 fix 2건 + 사이드바 레이아웃 통일.

#### 1. 페이지 선택 (이전: 첫 페이지만 / 모든 페이지만)
- 사이드바에 "전체 선택" 토글 체크박스 + N/M 카운터 (회전 도구와 동일 패턴)
- 메인 영역 하단 썸네일 그리드 — 체크박스 또는 이미지 클릭으로 페이지별 선택
- 기본값: 전체 선택 (로딩 직후)
- 기존 "모든 페이지 / 첫 페이지만" 라디오 제거 — 통합됨

#### 2. Corner resize handle ★ (이미지 에디터 패턴)
사각형 4개 꼭짓점에 작은 핸들 (#fff 보더 + accent 배경). 다음 3가지 인터랙션 자동 분기:

| 위치 | 동작 | cursor |
|---|---|---|
| 빈 영역 mousedown | 새 사각형 그리기 | `crosshair` |
| 사각형 본체 mousedown | 사각형 전체 이동 | `move` |
| 꼭짓점 핸들 mousedown | 해당 꼭짓점 드래그 (반대 꼭짓점 고정) | `nwse-resize` / `nesw-resize` |

내부 상태 머신: `mode: 'idle' | 'drawing' | 'moving' | 'resizing'`. mousemove 마다 위치별 cursor 자동 변경.

### 🆕 Added — corner handle 스타일 (pdftools.css)
- `.crop-handle` — 12×12 accent 배경 + #fff 보더 + glow shadow
- `.crop-handle.nw/.ne/.sw/.se` — 4 모서리 위치
- `pointer-events: none` (canvas 가 hit test 처리, overlay 는 시각만)

### 🔄 Cache-bust
- `pdftools.css?v=0.1.2` → `v=0.1.3`
- `crop.js?v=0.0.9` → `v=0.1.3` (전면 개편)
- 모든 도구 페이지 version-badge 일괄

### 🎉 사용자 fix 1차·2차·3차 모두 완료

| 버전 | 변경 |
|---|---|
| v0.1.1 | 공통 (썸네일 이미지 클릭 토글) + 분할 (open + zero-padding) + 페이지 삭제 (리프레시) + 재정렬 (drop indicator) |
| v0.1.2 | 회전 → "회전·반전" (사이드바 + 전체선택 + 5각도 아이콘화 + 리프레시) |
| v0.1.3 | 자르기 (페이지 선택 + corner handle) |

### 🚧 다음 — Phase 2 콘텐츠 편집 (v0.2.0)
- `shared/font-manager.js` 신설 (한글 폰트 lazy 임베드)
- 6개 도구: 워터마크 (텍스트·이미지) · 페이지 번호 · 머리글·바닥글 · 텍스트 추가 · 서명
- `coordinate.js` 가 핵심 인프라로 재사용

---

## v0.1.2 — 2026-05-23 (사용자 fix 2차 — 회전·반전 대규모 개편)

### 🔧 Changed — 회전 도구 전면 개편

기능명 **"회전" → "회전·반전"**. 사용자 fix 5건 한 번에 통합.

#### 1. 사이드바 레이아웃
페이지 미리보기 (좌측) + 옵션 사이드바 (우측). 새 `.tool-with-sidebar` 패턴 — 향후 자르기 등도 재사용.
- 우측 사이드바는 `position: sticky` — 스크롤해도 옵션이 항상 보임
- 모바일 (≤768px) 에선 단일 컬럼으로 자동 전환

#### 2. 전체 선택 토글 체크박스
- 사이드바 상단에 "전체 선택" 체크박스 + `N/M` 카운터 (선택/총)
- 한 번 클릭 = 모든 페이지 선택 / 다시 클릭 = 모두 해제
- 일부만 선택 시 `indeterminate` 상태 (네이티브 체크박스 시각)
- 기존 "적용 대상 (선택/전체)" 라디오 제거 — 통합됨

#### 3. 변환 종류 5종 (★ 신규 좌우/상하 반전)
모두 큰 아이콘 버튼으로 시각화 (2×3 그리드, 마지막 칸 빈자리):

| 변환 | 아이콘 | 구현 |
|---|---|---|
| 90° | ↻ Feather rotate-cw | `setRotation(degrees((cur + 90) % 360))` 누적 |
| 180° | ⇅ Feather refresh-ccw | `setRotation(... + 180)` 누적 |
| 270° | ↺ Feather rotate-ccw | `setRotation(... + 270)` 누적 |
| **좌우 반전** | ← → 점선 축 | 새 doc + `embedPages` + `drawPage({ x: w, xScale: -1 })` |
| **상하 반전** | ↑ ↓ 점선 축 | 새 doc + `embedPages` + `drawPage({ y: h, yScale: -1 })` |

회전은 setRotation 누적 패턴 (가벼움). 반전은 pdf-lib `setRotation` 만으론 표현 불가 → `embedPages` + `drawPage` xScale/yScale 음수 패턴.

#### 4. 적용 후 자동 리프레시 ★
사용자 fix 핵심: "현재 원본(A)을 회전시켜 한번 저장후(B) 다시 회전을 선택하면 적용하면 A가 회전하는게 아니라 B가 회전됨"

→ 변환 적용 후 결과 PDF 를 **새 `currentCore` 로 박고 썸네일 재렌더**. 다음 변환은 **결과 기준**. 연속 작업 자연스러움.

원본 파일명은 첫 로드 시 `originalFileName` 으로 보존 — 매 다운로드마다 같은 base name + 작업 suffix.

### 🆕 Added — 사이드바 레이아웃 컴포넌트 (pdftools.css)
- `.tool-with-sidebar` — grid 1fr 280px (모바일 단일 컬럼)
- `.tool-main` / `.tool-sidebar` — 좌·우 영역
- `.tool-sidebar .sidebar-group` — 옵션 그룹 (구분선 자동)
- `.select-all-row` — 전체 선택 체크박스 행 (카운터 포함)
- `.rotate-options` + `.rotate-btn` — 2열 아이콘 버튼 그리드 (active 강조)

### 🔄 Cache-bust
- `pdftools.css?v=0.1.1` → `v=0.1.2`
- `rotate.js?v=0.0.7` → `v=0.1.2` (대규모 개편)
- 모든 도구 페이지 version-badge 일괄

### 🚧 다음 (fix 3차)
**v0.1.3** — 자르기 개편: 페이지 선택 (현재 첫/전체만) + corner resize handle (이미지 에디터 패턴)

---

## v0.1.1 — 2026-05-23 (사용자 fix 1차 — 공통 인프라 + 4건)

### 🔧 Changed — 공통 인프라 (`shared/thumbnail.js`)

전체 도구에 적용되는 썸네일 동작 변경.

- **★ 페이지 이미지 클릭으로 체크 토글** — 이전엔 체크박스를 정확히 클릭해야만 선택됐는데, 이제 이미지 어디든 클릭하면 체크 on/off (체크박스 native 클릭도 그대로 동작). 사용자 fix 요청 "체크박스 클릭해야만 선택되는데 그냥 그 페이지 이미지를 선택해도 체크 on off 되도록"
- `selectable: true` 시 컨테이너에 `.thumb-selectable` 클래스 자동 추가 → CSS 에서 `cursor: pointer` + hover 효과
- 영향 도구: 회전·페이지 삭제·페이지 추출 (selectable 사용 도구 전부) + 향후 자르기

### 🔧 Changed — 페이지 재정렬 drop indicator 강조

`Sortable.js` 옵션에 `ghostClass: 'thumb-drop-target'` + `dragClass: 'thumb-dragging'` 추가:
- `.thumb-drop-target` — accent2 점선 보더 + glow shadow + accent 배경 (드롭 위치 시각화)
- `.thumb-dragging` — opacity 0.4 (드래그 중인 항목)

사용자 fix: "이 페이지가 어디로 이동되는지 마커 표시"

### 🔧 Changed — 분할 (Split)

사용자 fix 2건:
- **페이지 미리보기 디폴트 펼침** — `<details class="thumb-collapsible">` 에 `open` 속성 추가 (이전엔 클릭해야 열림)
- **★ Zero-padding 파일 넘버링** — 총 페이지 수의 자릿수에 맞춰 패딩
  - 예: 총 12쪽 → `분할-01.pdf` ~ `분할-12.pdf` (이전 `분할-1.pdf` ~ `분할-12.pdf` 정렬 깨짐)
  - 예: 총 100쪽 → `분할-001.pdf` ~ `분할-100.pdf`
  - "각 페이지 1장씩" + "N페이지씩 묶어서" 두 모드 모두 적용

### 🔧 Changed — 페이지 삭제 리프레시

사용자 fix: "삭제 실행시 미리보기가 리프레시 되도록"
- 삭제 후 결과 PDF 를 **다운로드 + 새 currentCore 로 박고 썸네일 재렌더**
- 다음 삭제는 **결과 PDF 기준** (이전엔 in-place 수정된 src 가 그대로 남아 누적 효과)
- 로딩 정보 바에 "(삭제 결과 기준)" 표시
- 회전 도구도 같은 패턴 적용 예정 (v0.1.2 회전 개편 시)

### 🔄 Cache-bust

- `pdftools.css?v=0.1.0` → `v=0.1.1` (모든 도구 페이지 일괄)
- `split.js?v=0.0.6` → `v=0.1.1`
- `remove-pages.js?v=0.0.7` → `v=0.1.1`
- version-badge 일괄 v0.1.1

### 🚧 다음 (fix 2차·3차)

- **v0.1.2** — 회전 대규모 개편 (사이드바 + 전체선택 토글 + 상하/좌우반전 추가 + 5각도 아이콘화 + 리프레시 패턴)
- **v0.1.3** — 자르기 개편 (페이지 선택 + corner resize handle)

---

## v0.1.0 — 2026-05-23 (★ Phase 1 완료 — 잠금 풀기 + 회고)

### 🎉 Phase 1 완료

코어 인프라 + 페이지 작업 카테고리 전부 + 추출 기본 = **누적 10개 도구**. 첫 minor 버전. Phase 2 (콘텐츠 편집) 진입 준비 완료.

마일스톤 정책 (dev.md §7) 대로 **랜딩 카드는 아직 비공개** — Phase 5 누적 완료 후 v1.0.0 에서 활성.

### 🆕 Added — 잠금 풀기 (Unlock)

가장 단순한 UI 의 마지막 Phase 1 도구. 옵션 X, "딸깍" 한 번.

- 폴더: `tools/unlock/`
- 패턴: `pdf-lib.load({ ignoreEncryption: true })` → `save()` — 권한 제한 PDF 의 인쇄·복사·편집 금지 플래그 자동 제거
- 시나리오 자동 판별:
  - **권한 제한 PDF** (파일 열림, 인쇄·복사·편집만 막힘) → 자동 해제 ✅
  - **열기 암호 PDF** → 친절한 안내 (v0.1.x 에서 qpdf-wasm 도입 예정)
  - **잠금 X PDF** → 그대로 통과 (no-op, 결과 동일)
- 외부 라이브러리 추가 X (pdf-lib 만, qpdf-wasm 보류) — 가장 가볍게 시작
- 사용 모듈: file-input + download + progress-ui

### 📜 Phase 1 회고

#### 코어 모듈 (`shared/`) — 8개

| 모듈 | 도입 | 핵심 |
|---|---|---|
| `cdn.js` | v0.0.3 + v0.0.6 (`setupPdfJs()`) | 외부 라이브러리 CDN URL 일괄 관리. pdf.js ESM 동적 import 통합 헬퍼 |
| `pdf-core.js` | v0.0.3 | pdf-lib + pdf.js lazy 인스턴스 동시 관리 |
| `file-input.js` | v0.0.3 | 드롭 zone + Sortable 다중 파일 정렬 |
| `thumbnail.js` | v0.0.3 | IntersectionObserver 가상 스크롤 (큰 PDF 메모리 절약) + 선택/드래그 옵션 |
| `page-range.js` | v0.0.3 | "1-3, 5, 7-9" 파싱/역변환 + UI |
| `download.js` | v0.0.3 | 단일 PDF + JSZip 묶음 + autoFileName |
| `progress-ui.js` | v0.0.3 | 진행률 모달 + 4종 토스트 (에러·성공·정보·경고) |
| **`coordinate.js`** | v0.0.9 | PDF ↔ canvas 좌표 변환 + Y축 뒤집힘 + 회전 보정. **Phase 2 핵심 인프라** |

#### 도구 (`tools/`) — 10개 ✅

| 카테고리 | 도구 | 핵심 |
|---|---|---|
| 페이지 작업 | 병합 (v0.0.5) | file-input 정렬 + pdf-lib copyPages |
| 페이지 작업 | 분할 (v0.0.6) | 3 모드 (each/range/group) + JSZip 묶음 |
| 페이지 작업 | 회전 (v0.0.7) | pdf-lib setRotation 누적 |
| 페이지 작업 | 페이지 삭제 (v0.0.7) | 뒤에서부터 removePage |
| 페이지 작업 | 페이지 재정렬 (v0.0.7) | thumbnail draggable + onReorder |
| 페이지 작업 | 페이지 추출 (v0.0.7) | 범위 ↔ 썸네일 양방향 동기화 |
| 페이지 작업 | 페이지 크기 조정 (v0.0.8) | pdf-lib embedPages + drawPage |
| 페이지 작업 | 자르기 (v0.0.9) | coordinate.js + drag UI + cropBox/mediaBox |
| 추출 | 텍스트 추출 (v0.0.8) | pdf.js getTextContent |
| 최적화·보안 | 잠금 풀기 (v0.1.0) | pdf-lib ignoreEncryption + save |

#### 검증된 pdf-lib 사용 패턴
- `copyPages(src, indices)` — 새 doc 으로 페이지 복사 (병합·분할·재정렬·추출)
- `removePage(idx)` 뒤에서부터 — 인덱스 보존 (삭제)
- `setRotation(degrees(N))` — 누적 회전
- `embedPages([page])` + `drawPage(ep, options)` — 새 크기 페이지에 원본 embed (크기 조정 · **Phase 2 콘텐츠 편집 핵심 패턴**)
- `setMediaBox` + `setCropBox` — 페이지 경계 설정 (자르기)
- `load({ ignoreEncryption: true })` — 권한 제한 자동 해제

#### 검증된 pdf.js 사용 패턴
- `getDocument({ data }).promise` — 메모리 PDF 로드
- `getPage(n).getViewport({ scale })` + canvas render — 썸네일 (가상 스크롤로 메모리 절약)
- `getTextContent().items` — 텍스트 레이어 추출

#### 신규 UI 컴포넌트 (pdftools.css)
- `.tool-stage` (도구 페이지 레이아웃) / `.tool-header` / `.tool-back` / `.tool-cat-dot`
- `.btn-primary` / `.btn-secondary`
- `.fi-zone` / `.sf-list` (file-input)
- `.thumb-grid` / `.thumb-item` (썸네일)
- `.pr-input` / `.pr-btn` (페이지 범위)
- `.pg-overlay` / `.pg-modal` / `.pg-toast` (진행률·토스트)
- `.split-mode` (라디오 fieldset)
- `.form-row` (label + input·select)
- `.loaded-info` / `.result-preview` (정보 박스)
- `.thumb-collapsible` (details/summary)
- `.text-preview` (monospace pre)
- `.crop-canvas-wrap` / `.crop-overlay` (자르기 mask)

### 🚧 다음 (Phase 2 — 콘텐츠 편집)

`shared/font-manager.js` 신설 (한글 폰트 lazy 임베드) + 6개 도구:
- 워터마크 (텍스트) — fontkit + 나눔고딕 임베드
- 워터마크 (이미지) — pdf-lib drawImage
- 페이지 번호 — 한국식 포맷 (1쪽, -1-, 1/10)
- 머리글·바닥글
- 텍스트 추가 — WYSIWYG 좌표 지정 (coordinate.js 활용)
- 서명 — canvas 그리기 + 이미지 업로드

목표 v0.2.0. Phase 1 의 인프라가 거의 다 재사용됨 → 빠른 진행 가능.

### 🔧 Changed

- 인덱스 카드: `.tool-card.soon` → `.live` (10/22 활성, 페이지 작업 카테고리 + 추출 + 보안 첫 도구 완성)
- 인덱스 phase-note: "Phase 1 진행" → "✅ Phase 1 완료 (v0.1.0)"
- 모든 도구 페이지 css `?v=0.0.9` → `v=0.1.0` 일괄 bump

### 🔄 Cache-bust

- `pdftools.css?v=0.0.9` → `v=0.1.0` (변경 X 이지만 minor 버전 통일)

---

## v0.0.9 — 2026-05-23 (Phase 1 도구 #9 — 자르기 + coordinate.js 신설)

### 🆕 Added — 자르기 (Crop)

페이지 여백 잘라내기. 첫 페이지를 canvas 에 큰 미리보기로 렌더 → **드래그로 영역 지정** 또는 **빠른 버튼 (여백 5% / 10% / 초기화)**.

- 폴더: `tools/crop/`
- 적용 대상: 모든 페이지 (같은 영역, 페이지 크기 비율 자동 조정) / 첫 페이지만
- 회전된 페이지는 `unrotateRect` 로 자동 보정 + 경고 토스트
- `setMediaBox` + `setCropBox` 둘 다 설정 → 결과 PDF 의 페이지 크기 자체가 자르기 영역

### ★ 신규 모듈: `shared/coordinate.js`

Phase 2 콘텐츠 편집 카테고리 (워터마크·텍스트 추가·이미지 추가·서명) 의 **핵심 인프라**. 자르기가 첫 사용자.

| 함수 | 역할 |
|---|---|
| `canvasRectToPdf` | canvas 좌표 (좌상단·px) → PDF 좌표 (좌하단·pt) — **Y축 뒤집힘 처리** |
| `pdfRectToCanvas` | 역변환 |
| `canvasPointToPdf` | 점 좌표 변환 (클릭 위치용) |
| `unrotateRect` | 회전된 페이지 (90/180/270°) 의 화면 좌표 → 회전 전 PDF 좌표 보정. cropBox/mediaBox 가 항상 회전 전 좌표라서 필수 |
| `ptToMm` / `mmToPt` | 단위 변환 (1pt = 25.4/72 mm) |
| `normalizeRect` | 음수 너비·높이 정규화 (드래그 방향 무관) |
| `clampRect` | 사각형을 다른 사각형 안으로 clip (캔버스 영역 벗어남 방지) |

### 🆕 Added — 자르기 UI (pdftools.css)

- `.crop-canvas-wrap` — canvas 컨테이너 (relative)
- `.crop-canvas-wrap canvas` — `cursor: crosshair`, user-select none
- `.crop-overlay` — accent 보더 + `box-shadow: 0 0 0 9999px rgba(0,0,0,0.4)` 로 자르기 외부 영역 어둡게 (mask 효과)
- `.crop-controls` — 빠른 버튼 + 안내 한 줄

### 🔧 Changed

- 인덱스 카드: `.tool-card.soon` → `.live` (9/22 활성)
- 모든 도구 페이지 (9개) css `?v=0.0.8` → `v=0.0.9` 일괄 bump

### 🔄 Cache-bust

- `pdftools.css?v=0.0.8` → `v=0.0.9` (`.crop-*` 클래스 4개 추가)

### 🚧 다음 (Phase 1 마무리 = v0.1.0)

마지막 도구 **잠금 풀기** + **Phase 1 회고 + v0.1.0 정식 출시**:
- qpdf-wasm 도입 (~5MB, lazy 로드) — 권한 제한 자동 해제 + 열기 암호 입력 해제 시나리오 자동 판별
- 단독 PR. Phase 1 의 코어 모듈·도구·인프라 회고 박음
- v0.1.0 = 10개 도구 + 코어 모듈 8개 (cdn·pdf-core·file-input·thumbnail·page-range·download·progress-ui·**coordinate**) 완성

---

## v0.0.8 — 2026-05-23 (Phase 1 도구 #7~8 — 페이지 크기 조정 + 텍스트 추출)

### 🆕 Added — 2개 도구 묶음

| 도구 | 폴더 | 핵심 |
|---|---|---|
| **페이지 크기 조정** | `tools/resize/` | A4/A3/A5/B5/Letter/Legal × 세로/가로. 콘텐츠 3 모드 (fit/stretch/keep) |
| **텍스트 추출** | `tools/extract-text/` | pdf.js getTextContent → .txt 단일 또는 페이지별 zip. 로딩 직후 첫 페이지 자동 미리보기 |

### ★ 인프라 검증

- **pdf-lib `embedPages` + `drawPage`** 첫 실전 (resize) — 새 doc 의 새 크기 페이지에 원본을 embed 객체로 그리는 패턴. 콘텐츠 편집 카테고리 (Phase 2 워터마크·텍스트 추가 등) 의 핵심 패턴
- **pdf.js `getTextContent`** 첫 실전 (extract-text) — `items[].str + (hasEOL ? '\n' : ' ')` 패턴으로 줄바꿈 복원
- **Blob 직접 다운로드** (`downloadBlob`) — `.txt` MIME 타입 첫 사용
- **`TextEncoder`** zip 묶음 안 텍스트 파일 변환

### 🆕 Added — 폼 컴포넌트 (pdftools.css)

- `.form-row select` — 표준 크기·방향 선택용. focus 시 accent ring
- `.text-preview` — `<pre>` 기반 텍스트 미리보기 박스 (monospace, 최대 240px, 자동 줄바꿈)

### 🔧 Changed

- 인덱스 카드 2개: `.tool-card.soon` → `.live` (8/22 활성)
- 모든 도구 페이지 (8개) css `?v=0.0.7` → `v=0.0.8` 일괄 bump
- version-badge 일괄 v0.0.8

### 🔄 Cache-bust

- `pdftools.css?v=0.0.7` → `v=0.0.8` (`.form-row select` + `.text-preview` 신규 클래스 추가)

### 🚧 다음 (Phase 1 마무리)

- **v0.0.9** — 자르기 (drag 영역 지정 UI). `coordinate.js` 신설 (PDF ↔ 화면 좌표 변환, Phase 2 핵심 인프라)
- **v0.1.0** — 잠금 풀기 (qpdf-wasm 도입, 첫 wasm 의존성) + **Phase 1 완료 정식 출시**

---

## v0.0.7 — 2026-05-23 (Phase 1 도구 #3~6 — 페이지 작업 4종 묶음)

### 🆕 Added — 4개 도구 한 번에

Phase 1 페이지 작업 카테고리의 4개 도구. 인터페이스 비슷해서 묶음 PR. 카드 그리드 6개 활성 (병합·분할·회전·삭제·재정렬·추출).

| 도구 | 폴더 | 핵심 |
|---|---|---|
| **회전** | `tools/rotate/` | 90/180/270° (선택/전체). 기존 회전과 누적 |
| **페이지 삭제** | `tools/remove-pages/` | 썸네일 체크 → 선택 페이지 제거 (1쪽 이상 남기기 보호) |
| **페이지 재정렬** | `tools/reorder/` | 썸네일 드래그로 순서 변경 → 새 PDF |
| **페이지 추출** | `tools/extract-pages/` | 범위 입력 + 썸네일 체크 양방향 동기화 |

### ★ 인프라 검증

- **`thumbnail.js` 의 `draggable: true`** 옵션 첫 실전 (재정렬 도구) — Sortable.js 자동 활성, `onReorder` 콜백으로 1-based 순서 받음
- **범위 ↔ 썸네일 양방향 동기화** (추출 도구) — `formatPageRange()` + `setValue()` + `thumbCtrl.selectAll`/`clear` 조합 패턴 정립

### pdf-lib 사용 패턴 (다른 도구도 참고)

- **회전**: `page.setRotation(degrees((current + N) % 360))` — in-place, 누적
- **삭제**: `removePage(idx)` — in-place. 다중 삭제 시 **뒤에서부터** 인덱스 보존
- **재정렬**: 새 doc + `copyPages(src, newIndices)` — src 보존
- **추출**: 새 doc + `copyPages(src, selectedIndices)` — 분할 'range' 모드와 동일

### 🔧 Changed

- 인덱스 카드 4개: `.tool-card.soon` → `.live`
- 모든 도구 페이지 (merge·split·rotate·remove-pages·reorder·extract-pages) version-badge 와 `?v=` 일괄 0.0.7

### 🔄 Cache-bust

- `pdftools.css?v=0.0.6` → `v=0.0.7`

### 🚧 다음 (Phase 1 마무리)

페이지 크기 조정 → 자르기 → 잠금 풀기 → 텍스트 추출 = 누적 10개 + v0.1.0 (Phase 1 완료, 첫 minor 버전). 자르기는 `coordinate.js` 신규 (Phase 2 핵심 인프라). 잠금 풀기는 qpdf-wasm 도입. 텍스트 추출은 pdf.js `getTextContent` 활용.

---

## v0.0.6 — 2026-05-23 (Phase 1 도구 #2 — 분할 사용 가능)

### 🆕 Added — 분할 (Split)

1개 PDF 를 여러 개로 나누는 도구. **3가지 모드**:

| 모드 | 동작 |
|---|---|
| **각 페이지 1장씩** | N쪽 PDF → N개 파일 (낱장 분리) |
| **지정한 범위만 새 PDF 로** | 예: 1-3, 5 → 4쪽짜리 새 PDF 1개 |
| **N페이지씩 묶어서** | 매 N쪽마다 새 파일 (10쪽 + 3씩 = 4개 파일) |

결과가 2개 이상이면 **zip 으로 묶어** 자동 다운로드, 1개면 PDF 단독 다운로드.

- 폴더: `pdftools/tools/split/`
- 라이브: `https://sobjil-gdi-apps.mycafe24.ai/pdftools/tools/split/`
- 카드 그리드 분할 카드 활성

### ★ 모듈 첫 사용 — 6개 모듈 검증 통과

분할 도구는 Phase 1 코어 모듈을 가장 폭넓게 사용. 한 도구로 6개 모듈 검증:

- ✅ **`cdn.js → setupPdfJs()`** (★ 신규 함수) — pdf.js (ESM) 동적 import + window 글로벌 + worker 경로 설정 통합. 한 줄로 setup
- ✅ **`pdf-core.js → loadPDF`** — File → pdf.js + pdf-lib lazy 인스턴스 (★ 첫 실전)
- ✅ **`thumbnail.js → renderThumbnails`** — IntersectionObserver 가상 스크롤 (★ 첫 실전, 큰 PDF 메모리 절약)
- ✅ **`page-range.js → createPageRangeInput`** — "1-3, 5, 7-9" 파싱 + 전체/홀수/짝수 빠른 버튼 (★ 첫 실전)
- ✅ **`download.js → downloadZip`** — JSZip 묶음 다운로드 (★ 첫 실전)
- ✅ **`progress-ui.js`** — 진행률 모달 + 토스트 (병합과 동일)

### 🆕 Added — 도구 공통 폼 요소 (pdftools.css)

분할에서 처음 사용. 다른 도구도 그대로 재사용 가능한 클래스:

- `.split-mode` — 라디오 그룹 fieldset (모드 선택)
- `.form-row` — 라벨 + input (숫자·텍스트 입력)
- `.loaded-info` — 로딩된 파일 정보 바 (accent 톤)
- `.result-preview` — 결과 미리보기 (accent 좌측 보더)
- `.thumb-collapsible` — details/summary 접힘 패턴 (썸네일 영역 등)

### 🔧 Changed — cdn.js `setupPdfJs()` 신설

기존 `setupPDFJSWorker()` (worker 만 설정) 는 deprecated. 신규 `setupPdfJs()` 가:
- pdf.js ESM 동적 import
- worker 경로 설정
- `window.pdfjsLib` 박기
- 1회 캐시 (여러 번 호출해도 1번만 로드)

모든 도구가 한 줄로 setup: `await setupPdfJs();`

### 🔄 Cache-bust

- `pdftools.css?v=0.0.5` → `v=0.0.6`
- 모든 도구 페이지 (merge·split) 의 version-badge / css ?v= 일괄 갱신

### 🚧 다음 (Phase 1 누적 진행)

회전 → 페이지 삭제 → 페이지 재정렬 → 페이지 추출 → 페이지 크기 조정 → 자르기 → 잠금 풀기 → 텍스트 추출 = 누적 10개 + v0.1.0. 페이지 작업 6개 (회전·삭제·재정렬·추출 등) 는 인터페이스 비슷해서 묶음 PR 가능.

---

## v0.0.5 — 2026-05-23 (Phase 1 도구 #1 — 병합 사용 가능)

### 🆕 Added — 첫 도구: 병합 (Merge)

여러 PDF 를 하나로 묶는 도구. Phase 1 코어 모듈들의 진짜 검증.

- 폴더: `pdftools/tools/merge/`
- 라이브 URL: `https://sobjil-gdi-apps.mycafe24.ai/pdftools/tools/merge/`
- 카드 그리드의 병합 카드 — `.tool-card.soon` → `.tool-card.live` (href 활성)

**사용 흐름**:
1. 드롭 zone 에 PDF 여러 개 드래그 (또는 클릭 업로드, 다중 선택)
2. 정렬 리스트에서 드래그 핸들 (⠿) 로 순서 변경 가능
3. ✕ 버튼으로 개별 파일 제거
4. "병합 시작" 클릭 → 결과 PDF 자동 다운로드 + 성공 토스트
5. 손상·암호 PDF 는 자동 건너뜀 + 건너뛴 파일 토스트 안내

**사용 모듈**: `file-input.js` (드롭 zone + 정렬 리스트) · `download.js` (자동 파일명 + 다운로드) · `progress-ui.js` (진행률 모달 + 토스트). 코어 모듈 인터페이스 첫 실전 검증.

**외부 라이브러리** (HTML 에서 로드): pdf-lib 1.17.1 · Sortable.js 1.15.2

### 🆕 Added — 도구 페이지 공통 레이아웃

`pdftools.css` 에 모든 도구 페이지가 공유할 클래스 추가:
- `.tool-stage` (max-width 900px 중앙 배치 + 상단 topbar 패딩 보정)
- `.tool-header`, `.tool-back`, `.tool-title-row`, `.tool-cat-dot` (카테고리 색 dot), `.tool-desc`
- `.tool-body`, `.file-summary`, `.tool-actions`
- `.btn-primary` (accent 색 큰 버튼), `.btn-secondary` (border 만)
- 모바일 반응형 (액션 버튼 전체 너비)

다른 도구도 이 레이아웃 그대로 재사용 — 도구별 HTML 구조 일관성.

### 🔧 Changed

- 인덱스 phase-note: "Phase 0 — 인프라 세팅" → "Phase 1 진행 — 첫 도구 (병합) 사용 가능"
- 인덱스 병합 카드: `.tool-card.soon` → `.tool-card.live` + `href="tools/merge/"`

### 🔄 Cache-bust

- `pdftools.css?v=0.0.4` → `v=0.0.5`
- 도구 페이지 모두 `?v=0.0.5` (신규)
- version badge v0.0.4 → v0.0.5

### 🚧 다음 (Phase 1 누적 진행)

분할 → 회전 → 페이지 삭제 → 페이지 재정렬 → 페이지 추출 → 페이지 크기 조정 → 자르기 → 잠금 풀기 → 텍스트 추출 = 누적 10개 도구 + v0.1.0.

---

## v0.0.4 — 2026-05-23 (배경 톤 통일 — md2hwpx · image-editor 와 일치)

### 🔧 Changed — 디자인 토큰 전환 (GitHub 톤 → 회색 톤)

이전 pdftools 는 랜딩 페이지 (`styles/landing.css`) 의 GitHub-ish 톤 (#0d1117 / #1c2128) 을 따랐는데, 실제 다른 도구 앱 (md2hwpx · image-editor) 은 회색 톤 (#1a1a1a / #232323 / #2a2a2a) 으로 통일돼 있었음. 사용자 피드백 반영해 도구 앱 톤으로 전환.

| 토큰 | 이전 (GitHub 톤) | 신규 (회색 톤, 다른 앱 일치) |
|---|---|---|
| `--bg` | #0d1117 | **#1a1a1a** |
| `--panel` | #1c2128 | **#232323** |
| `--panel-hover` | #22272e | **#2a2a2a** |
| `--toolbar` | X (rgba) | **#2a2a2a** ★ |
| `--hover` | X | **#333** (신규, .tbtn hover) |
| `--border` | #30363d | **#383838** |
| `--border-strong` | #444c56 | **#484848** |
| `--text` | #e6edf3 | **#ebebee** |
| `--text2` | #b1bac4 | **#b0b0b7** |
| `--text3` | #8b949e | **#70707a** |
| `--accent2` | #6cc4ff | **#6db1ff** |

### 🔧 Changed — body 배경 단순화

- 이전: `radial-gradient × 2 + linear-gradient` 3 레이어 (랜딩 페이지 스타일)
- 신규: `background: var(--bg);` 단색 (md2hwpx · image-editor 와 동일)

### 🔧 Changed — topbar 배경 단순화

- 이전: `rgba(13, 17, 23, 0.92) + backdrop-filter: blur(8px)` (반투명·블러)
- 신규: `background: var(--toolbar);` 단색 (다른 앱 동일)
- `.tbtn:hover` 배경: `var(--panel-hover)` → `var(--hover)` (#333)

### 🔄 Cache-bust

- `pdftools.css?v=0.0.3` → `v=0.0.4`
- version badge v0.0.3 → v0.0.4

### 시각 효과

이제 pdftools 진입 페이지가 md2hwpx · image-editor 와 같은 회색 톤. 도구 사이를 오가도 일관된 시각.

---

## v0.0.3 — 2026-05-23 (Phase 1 진입 — 코어 모듈 7개 세팅)

### 🆕 Added — 공통 모듈 (`pdftools/shared/`)

dev.md §4 의 도구↔모듈 의존 매트릭스에 따라 거의 모든 도구가 공유할 인프라 모듈 세팅. 도구 자체는 X (다음 PR 부터 추가).

| 파일 | 역할 |
|---|---|
| **`pdf-core.js`** | PDF 로딩 wrapper. pdf-lib (편집) + pdf.js (렌더링·텍스트) lazy 인스턴스 관리. File / ArrayBuffer / URL 입력 지원 |
| **`file-input.js`** | 드래그&드롭 + 클릭 업로드. 다중 파일 정렬 리스트 (Sortable.js 활용, 없으면 정렬만 비활성) |
| **`thumbnail.js`** | 페이지 썸네일 렌더러. **IntersectionObserver 가상 스크롤** (큰 PDF 메모리 절약). 체크박스 + 드래그 옵션 |
| **`page-range.js`** | "1-3, 5, 7-9" ↔ [1,2,3,5,7,8,9] 파싱·역변환. 'all'/'전체'/'*' 지원. 입력 UI 컴포넌트 (전체/홀수/짝수 빠른 버튼) |
| **`download.js`** | 단일 PDF / 임의 Blob / **JSZip 묶음** 다운로드. 파일명 자동 생성 (`autoFileName`, `appendTimestamp`) |
| **`progress-ui.js`** | 진행률 모달 (취소 가능) + 에러·성공·정보·경고 토스트 (자동 사라짐) |
| **`cdn.js`** | 외부 라이브러리 CDN URL 일괄 관리 — pdf-lib · pdf.js (+worker) · fontkit · JSZip · Sortable · diff-match-patch |

### 🆕 Added — 컴포넌트 스타일

`pdftools.css` 에 모듈 컴포넌트 클래스 추가:
- `.fi-zone` (드롭 zone), `.sf-list` (정렬 리스트)
- `.thumb-grid` (썸네일 그리드)
- `.pr-input` (페이지 범위 입력)
- `.pg-overlay` / `.pg-modal` (진행률), `.pg-toast` (토스트 4종)

모든 도구 페이지가 `pdftools.css` 만 로드하면 모듈 컴포넌트 즉시 사용 가능.

### 🔧 Changed

- `pdftools.css?v=0.0.2` → `v=0.0.3` (CLAUDE.md 메모리 룰 #8)
- version badge v0.0.2 → v0.0.3

### 📚 Documentation

- 코어 모듈 사용법은 각 파일 상단 JSDoc 참고
- 도구 페이지 패턴: `<script src="CDN.pdfLib">` 정적 로드 → `<script type="module" src="merge.js">` 가 `import` 로 모듈 활용

### 🚧 다음 (Phase 1 도구)

코어 모듈 위에 첫 도구들 진입 — 가장 단순한 **병합 (Merge)** 부터. file-input + sortable list + pdf-core + download 조합으로 즉시 구현 가능. Phase 1 완료 시 누적 10개 도구 + v0.1.0.

---

## v0.0.2 — 2026-05-23 (topbar 디자인 통일 + PDF 아이콘 변경)

### 🔧 Changed

- **topbar 디자인 통일** — 다른 앱 (md2hwpx · image-editor) 패턴과 일치하도록 조정:
  - `.topbar`: `position: sticky` → `fixed`, height `var(--tph)` (44px) 고정, padding `10px 20px` → `0 10px`
  - `.tb-left/.tb-right`: `flex: 1; min-width: 0;` 추가로 양쪽 균형
  - `.home-link`: 평소에도 hint 박스 (`background: rgba(74,158,255,0.06); border: 1px solid rgba(74,158,255,0.18);`). 폰트 12px → 11px
  - `.logo`: 색 `var(--text)` → `var(--accent)` (★ 가장 큰 시각 변화). 폰트 14px → 12px
  - `.sep`: margin `0 4px` → `0 2px`
  - `.tbtn`: padding `5px 10px` → `4px 7px`, 폰트 12px → 11.5px
  - `.stage`: fixed topbar 분 padding-top 보정 (`calc(var(--tph) + 32px)`)
- **새 디자인 토큰** — `--tph: 44px;`, `--r: 6px;` (다른 앱과 일관성 위해)

### 🎨 PDF 딸깍 로고 SVG 변경

- 기존: 파일 모양 + 가로 줄 2개 (텍스트 representation)
- 신규: 파일 모양 + 하단 PDF 텍스트 박스 (`<rect>` + `<text>`). 사용자 참조 아이콘 (PDF 텍스트 들어간 파일 모양) 반영
- viewBox 24x24, stroke-width 2 (다른 앱 통일)

### 🔄 Cache-bust

- `pdftools.css?v=0.0.1` → `v=0.0.2` (CLAUDE.md 메모리 룰 #8)

---

## v0.0.1 — 2026-05-23 (Phase 0 — 인프라 세팅)

### 🎉 첫 진입

PDF 편집·조작 전용 도구 모음 앱의 인프라 골격을 세팅. 라이브 동작하는 도구 X (모두 "준비 중" 상태). 라이브 URL 은 노출되지만 랜딩 카드는 아직 활성 X — 직접 URL (`https://sobjil-gdi-apps.mycafe24.ai/pdftools/`) 로만 접근.

### 🆕 Added

- **앱 진입 페이지** — `pdftools/index.html` 카드 그리드. 22개 도구를 6 카테고리 (페이지 작업 8 · 콘텐츠 편집 6 · 추출·변환 5 · 최적화·보안 2 · 뷰어·비교 2 · AI 2) 로 구분해서 노출. 모두 "준비 중" 상태로 표시
- **공통 스타일** — `pdftools/pdftools.css`. 디자인 토큰은 루트 `styles/landing.css` 와 일치 (GitHub-ish 다크 테마, accent #4a9eff). 6 카테고리별 dot 색상 변별, 반응형 (640px 이하 컴팩트)
- **topbar** — 다른 앱(md2hwpx · image-editor) 과 동일 패턴. 좌측 "딸깍 개발실" 홈 링크 + PDF 딸깍 로고, 우측 패치 노트 링크 + 버전 뱃지
- **footer** — 개발 문서 / 패치 노트 / GitHub 링크
- **PWA 자원** — 아이콘 + manifest 는 루트 자원 상위 경로 참조 (다른 앱 컨벤션)
- **`PATCHNOTE.md`** — 본 파일 신설

### 📚 Documentation

- 진실 출처 [pdftools/doc/dev.md](doc/dev.md) 는 직전 PR ([#127](https://github.com/sobjil/GDI-Apps/pull/127)) 에서 신설됨. 도구 22개 카탈로그 + 공통 모듈 11개 + 도구↔모듈 의존 매트릭스 + Phase 1~5 누적 개발 계획 + PDF 기술 특유 함정 9개

### 🚧 다음 (Phase 1)

코어 인프라 모듈 (`pdf-core` · `file-input` · `thumbnail` · `page-range` · `download` · `progress-ui`) 세팅 + 페이지 작업 카테고리 8개 도구 + 잠금 풀기 + 텍스트 추출 = 누적 10개 도구. 목표 v0.1.0.
