Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 모든 PR에 상대방 리뷰를 자동 요청한다 (비차단 — 머지 조건 아님).
# PR 작성자 본인은 자동으로 제외된다.
* @enu3379 @onetwothr1
43 changes: 43 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: 버그 리포트
description: 재현 가능한 버그 신고
title: "[bug] "
labels: ["bug"]
body:
- type: textarea
id: repro
attributes:
label: 재현 절차
description: 처음부터 순서대로. 사용한 PDF 파일 특성도 함께 (스캔본/텍스트/용량 등).
placeholder: |
1. ...
2. ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: 기대 동작
validations:
required: true
- type: textarea
id: actual
attributes:
label: 실제 동작
description: 에러 메시지가 있으면 그대로 붙여넣기.
validations:
required: true
- type: input
id: env
attributes:
label: 환경
description: OS / Chrome 버전 / 확장 버전(커밋)
placeholder: "macOS 15 / Chrome 138 / dev@a55695c"
validations:
required: true
- type: textarea
id: logs
attributes:
label: 스크린샷 · 콘솔 로그
description: chrome://extensions → 서비스 워커 콘솔, 뷰어 페이지 DevTools 콘솔 (선택)
validations:
required: false
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
blank_issues_enabled: true
37 changes: 37 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: 기능 제안
description: 새 기능 또는 개선 작업 지시서 — 사람이든 에이전트든 이 이슈만 보고 착수할 수 있게 작성
title: "[feat] "
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: 배경 · 해결하려는 문제
description: 왜 필요한가? 지금 무엇이 불편한가?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: 제안 내용
description: 어떻게 동작해야 하는가? UI가 있다면 어디에 어떻게 보이는가?
validations:
required: true
- type: textarea
id: acceptance
attributes:
label: 수용 기준 (Acceptance Criteria)
description: 이 체크리스트가 모두 충족되면 이슈 완료. 에이전트에게 그대로 맡길 수 있을 만큼 구체적으로.
placeholder: |
- [ ] ...
- [ ] ...
- [ ] typecheck·테스트 통과
validations:
required: true
- type: textarea
id: references
attributes:
label: 참고 자료
description: 관련 스펙 문서 절, 데모, 스크린샷, 링크 (선택)
validations:
required: false
19 changes: 19 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Summary

<!-- 무엇을, 왜 바꿨는지 1-3문장 -->

Closes #

## Changes

-

## Test plan

<!-- 어떻게 확인했는지: 단위 테스트, 수동 QA 절차, 스크린샷 등 -->

## Checklist

- [ ] PR 제목이 Conventional Commits 형식 (`feat: …`, `fix: …` — squash 커밋 메시지가 됨)
- [ ] `npm run typecheck` · `npm test` 통과
- [ ] AI-assisted (에이전트가 작성/보조한 PR이면 체크)
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
push:
branches:
- main
- dev
workflow_dispatch:

concurrency:
Expand Down
37 changes: 37 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Agent Guide — PDFViewer / Margin

Chrome MV3 PDF reader extension ("Margin"): a pdf.js-based viewer with highlights, memos, figure/table detection, and a note hub. Vite + TypeScript, tested with Vitest.

The spec is [docs/implementation-plan.md](docs/implementation-plan.md) (Korean) — it is the source of truth for behavior and UI rules. Full collaboration rules: [CONTRIBUTING.md](CONTRIBUTING.md) (Korean).

## Commands

```sh
npm ci # install from lockfile
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run build # vite build → dist/
```

On Windows, if npm scripts fail with `"node" is not recognized`, use the `:win` variants (`npm run typecheck:win`, `test:win`, `build:win`).

For manual checks, load `dist/` as an unpacked extension at `chrome://extensions`.

## Layout

- `src/viewer/` — PDF viewer UI (pdf.js), entry `viewer.html`
- `src/hub/` — note hub, entry `hub.html`
- `src/core/` — shared logic (anchors, formatting)
- `src/sw.ts` — MV3 service worker
- `test/` — Vitest unit tests
- `docs/` — spec, progress log, QA guides

## Workflow rules (operational minimum)

1. Never commit directly to `main` or `dev` — rulesets reject direct pushes.
2. Branch from `dev`: `feature/<issue#>-<slug>`, `fix/<issue#>-<slug>`, `chore/<slug>`. Only `hotfix/<slug>` branches from `main` (and must merge into both `main` and `dev`).
3. Open PRs against `dev`. It is squash-merged: **the PR title becomes the commit message**, so PR titles must follow Conventional Commits (`feat: …`, `fix: …`, `chore: …`).
4. Reference the issue in the PR body (`Closes #N`).
5. Run `npm run typecheck` and `npm test` before opening a PR. CI (macOS + Windows) must pass to merge.
6. Check the **AI-assisted** box in the PR template.
7. Never commit secrets or `.env` files.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
60 changes: 60 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Contributing — PDFViewer / Margin

협업 규칙의 단일 출처(single source of truth). 사람과 AI 에이전트 모두 이 문서를 따른다.
(코딩 에이전트용 운영 요약은 [AGENTS.md](AGENTS.md), 구현 스펙은 [docs/implementation-plan.md](docs/implementation-plan.md))

## 브랜치 전략

| 브랜치 | 역할 | 규칙 |
|---|---|---|
| `main` | 무결점 릴리스 | `dev`와 `hotfix/*`의 PR만 받음 (merge commit). 머지 시 버전 태그 |
| `dev` | 통합 (기본 브랜치) | 모든 작업 브랜치의 PR 대상. squash 머지만 |
| `feature/<이슈#>-<슬러그>` | 기능 | `dev`에서 분기 |
| `fix/<이슈#>-<슬러그>` | 버그 수정 | `dev`에서 분기 |
| `chore/<슬러그>` | 문서·리팩토링·설정 | `dev`에서 분기, 이슈 없어도 됨 |
| `hotfix/<슬러그>` | 긴급 수정 | **`main`에서 분기**, main과 dev **양쪽에** 머지 |

- 작업 브랜치는 머지되면 자동 삭제된다. 짧게 유지할 것.
- `main`·`dev`는 룰셋이 보호한다: 직접 push·force-push·삭제 불가, CI 통과 필수, 머지 방식도 강제됨(dev는 squash만, main은 merge commit만 버튼이 뜬다).

## 작업 흐름

1. **이슈에서 시작** — 배경과 수용 기준을 이슈에 적는다. 이슈가 곧 작업 지시서다: 사람이든 에이전트든 이슈 본문만 보고 착수할 수 있어야 한다.
2. `dev`에서 브랜치를 딴다.
3. PR을 `dev`로 연다. 제목은 Conventional Commits 형식, 본문에 `Closes #이슈번호`.
4. CI(macOS·Windows: typecheck → test → build) 통과 후 squash 머지한다. 리뷰 승인은 머지 조건이 아니지만, CODEOWNERS가 상대에게 리뷰 요청을 자동으로 보낸다.

## PR 제목 = 커밋 컨벤션

squash 머지 시 **PR 제목이 dev의 커밋 메시지가 된다.** 개별 커밋은 자유롭게 하되 PR 제목만 지키면 된다:

| 타입 | 용도 |
|---|---|
| `feat:` | 기능 추가·변경 |
| `fix:` | 버그 수정 |
| `refactor:` | 동작 변화 없는 구조 개선 |
| `docs:` | 문서 |
| `test:` | 테스트 |
| `chore:` | 빌드·설정·기타 |

예: `feat: add manual crop mode to figure panel`

PR은 작게 — 하나의 PR은 하나의 이슈/주제만 다룬다.

## 릴리스

1. `dev` → `main` PR을 연다 (merge commit — 릴리스 경계가 히스토리에 남는다). 미루지 말고 릴리스 단위로 자주 승격할 것.
2. 머지 후 태그: `git tag vX.Y.Z && git push origin vX.Y.Z`
3. `release.yml`이 확장 zip을 빌드해 GitHub Release에 첨부한다.

## 복구 원칙

- 잘못 머지됐으면 **revert**: `git revert -m 1 <머지커밋>`. 히스토리는 지우지 않고 앞으로만 쌓는다.
- 특정 시점을 남기고 싶으면 브랜치가 아니라 **태그** (`v0.4.0-rc.1` 같은 프리릴리스 태그 포함).
- force-push 차단이 곧 복구 가능성의 보장이다. 한번 머지된 상태는 언제든 되돌아갈 수 있다.

## AI 협업

- 에이전트(Claude Code, Codex 등)도 이 문서의 규칙을 그대로 따른다.
- AI가 작성·보조한 PR은 템플릿의 **AI-assisted** 체크박스를 켠다 — 리뷰어가 리뷰 강도를 판단하는 신호.
- 시크릿·`.env`는 절대 커밋하지 않는다. CI에 필요한 값은 GitHub Secrets에 넣는다.
31 changes: 25 additions & 6 deletions docs/fig-extract-integration.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# fig-extract 엔진 통합 규약

figure 감지 엔진(`src/core/fig-extract.js`)의 반입·사용 규약. 엔진 알고리즘은 별도
저장소 **figure-preview-test**에서 개발·검증되며, 이 repo에는 빌드 산출물처럼 vendoring한다.
저장소 **PDFViewer-Figure-Extract**에서 개발·검증되며, 이 repo에는 빌드 산출물처럼 vendoring한다.

엔진 repo 문서 (로컬 `C:\Users\kimde\Desktop\figure-preview-test`, 원격 https://github.com/onetwothr1/PDFViewer-Figure-Extract):
엔진 repo 문서 (원격 https://github.com/onetwothr1/PDFViewer-Figure-Extract):
- `docs/DEV.md` — 엔진 개발 진입점 (통합 계약, 릴리스 절차, 로드맵)
- `docs/ALGORITHM.md` — 감지 알고리즘 상세

## 작업 경계

- **엔진(figure-preview-test) 담당**: 문서에 어떤 figure가 존재하는가(번호·페이지), region bbox(그림 영역만),
- **엔진(PDFViewer-Figure-Extract) 담당**: 문서에 어떤 figure가 존재하는가(번호·페이지), region bbox(그림 영역만),
캡션 전체 텍스트, 캡션 블록 bbox. → **문서 내 figure 목록의 단일 진실 공급원은 엔진이다.**
- **Margin 담당**: `fig-engine.ts`(타입 래퍼, `toPdfRect`/`toFigureEntries` 변환),
captionAnchor(엔진이 준 captionText를 S_p에서 검색해 오프셋 계산), 본문 mentions 스캔·링크 주입(§5.4, `mentions.ts`),
Expand All @@ -23,7 +23,7 @@ figure 감지 엔진(`src/core/fig-extract.js`)의 반입·사용 규약. 엔진
| `src/core/fig-extract.js` | (vendored) 엔진 본체 (전역 `FigExtract` 등록) |
| `src/core/fig-extract.d.ts` | strict TS에서 위 .js를 side-effect import하기 위한 스텁 |
| `src/core/fig-engine.ts` | 타입 정의 + `toPdfRect`/`toFigureEntries` + 전역 `pdfjsLib` 주입 — 통합 접점은 이 파일 하나 |
| `src/viewer/panel/tab-figures.ts` | 그림·표 탭 UI — 엔진 스캔(lazy)·프리뷰 카드·페이지 점프 |
| `src/viewer/panel/tab-figures.ts` | 그림·표 탭 UI — PDF 문서 준비 직후 엔진 스캔 시작·프리뷰 카드·페이지 점프 |

## 사용법

Expand All @@ -41,7 +41,7 @@ const seeds = toFigureEntries(res, (p) => pageHeights[p]);

## 현재 통합 상태

- `tab-figures.ts`가 구현됨: 그림·표 탭 최초 오픈 시 엔진 스캔 → 프리뷰 카드(크롭 이미지·캡션 텍스트)
- `tab-figures.ts`가 구현됨: PDF.js 문서 객체가 준비되면 엔진 스캔을 즉시 시작 → 프리뷰 카드(크롭 이미지·캡션 텍스트)
렌더, 카드 클릭 시 해당 페이지 점프. 결과는 **세션 메모리만** (storage 저장 안 함).
- 미구현 (M3 잔여, Margin 측): `toFigureEntries()`로 FigureEntry 생성 후 storage 저장,
captionAnchor 계산, 본문 mentions 스캔·참조 링크 주입(§5.4), 수동 크롭 연동(§6).
Expand All @@ -55,10 +55,29 @@ const seeds = toFigureEntries(res, (p) => pageHeights[p]);

- **좌표계**: 엔진은 pt 단위·좌상단 원점. Margin 저장 규약(PDF user space, 좌하단 원점)으로는
`toPdfRect()`가 변환한다 (`y' = pageHeight − y`).
- **figure 식별 키 = (num, page)** (v2.5.0+): 같은 `num`이 다른 페이지에 복수 등장할 수 있다
(합본 논문·부록 번호 재시작 — #14). num 단독을 키로 쓰지 말 것 — `toFigureEntries`의
`fig{num}-p{page}` ID가 올바른 키다. 결과 정렬은 page 오름차순 → num 자연순 (결정적).
- **suspectedMissing** (v2.4.0+): 감지된 정수 번호 1..최대 중 빠진 번호 목록 (미탐지 의심).
소비자가 무시해도 되는 보고 필드 — "이 논문에 Fig N이 있을 텐데 못 잡았다" UI에 활용 가능.
- **취소** (v2.5.0+): `opts.signal`(AbortSignal) 전달 시 페이지 단위로 체크해 AbortError로 reject.
문서 교체 시 이전 스캔 중단에 사용 (#12). v2.5.1+: abort 시 진행 중 페이지 렌더도 `RenderTask.cancel()`로
즉시 중단 — 페이지 경계까지 기다리지 않는다. **호스트는 문서 교체 시 반드시 signal을 abort해야 한다**
(엔진은 메커니즘만 제공 — signal 미전달 시 스캔이 끝까지 진행됨).
- **크롭 캔버스 수명/메모리** (#12, v2.5.1+): `figure.cropCanvas`는 그림 영역만의 크롭 렌더(scale 2.2)다.
엔진은 페이지 전체 캔버스를 보관하지 않는다(스캔 중 동시 상주 최대 1장) — figure마다 페이지 전체
캔버스를 물던 구조(~9.4MB/페이지 상주)가 사라졌다. 프리뷰 생성 후 `cropCanvas` 참조를 버리면 GC 회수.
페이지 렌더 LRU·object URL revoke는 여전히 Margin 몫.
- **pdf.js 버전**: 엔진은 pdfjs-dist 4.10.38(프로젝트 고정 버전) 기준으로 테스트 샘플 검증됨.
- **confidence**: 현재 1.0 고정 (플레이스홀더). 추후 감지 경로별 실측 값으로 교체 예정.
- **Table 미지원**: 엔진은 figure만 감지한다. Table region은 v1에서 수동 크롭으로 처리.
- **텍스트 레이어 없는 PDF(스캔본)**: 캡션을 찾지 못해 figures가 빈 배열 — 정상 동작.
- **캡션 앵커·다방향 한계**: "Figure N" 표기가 아예 없는 문서는 구조적 미탐지다. v2.8.0부터 캡션 위·아래·좌·우 figure 후보를 지원하지만, side caption의 세로 정렬 증거가 약하거나 기존 상향 후보가 강하면 보수적으로 미탐지/기존 영역을 유지할 수 있다. 캡션과 figure가 서로 다른 페이지인 레이아웃도 미지원이다 (엔진 repo ALGORITHM.md §알려진 한계).
- **캡션 표기 확대 (v2.9.x)**: 번호 뒤 구분자가 없는 표기(RSC·Springer `Fig. 1 본문…`, Wiley 자간 분리 `F I G U R E 1 본문…`)를 **문서 수준 게이트를 통과한 문서에서만** 앵커로 승격한다 — 한 문서가 캡션 관습을 하나만 쓴다는 전제라, hard 앵커가 이미 잡히는 문서에는 적용되지 않는다(표기가 섞인 문서는 미적용). 나란한 figure의 캡션이 8pt 미만 간격으로 한 줄에 붙은 경우도 분해해 각각 앵커한다.
- **번호 글리프에 ToUnicode 매핑이 없는 PDF는 원리상 미탐지**: 번호가 화면에는 정상으로 보이는데 텍스트 레이어에 문자가 없는 문서가 있다(Wiley 일부). 엔진이 아니라 PDF 쪽 문제라 사용자 눈에는 "번호가 멀쩡히 보이는데 안 잡힌다"로 보인다 — 문의가 오면 수동 크롭 안내가 맞다.
- **영역 경계 정밀화 (v2.10.x)**: figure/table·나란한 컬럼 경계 판정을 개선했다 — table 캡션을 **경계로만** 인식해 인접 figure 크롭에서 table을 제외(v2.10.0, table 자체 방출은 없음), 좌우로 나란한 두 figure가 서로를 통째로 크롭하던 것을 각자 캡션 컬럼으로 분리(v2.10.1 같은 baseline, v2.10.2 baseline 어긋난 offset). 출력 필드·좌표계·(num,page) 식별자 불변 — bbox가 더 타이트해질 뿐이라 소비자 코드 변경은 불요.
- **캡션 문법 확대 (v2.11.0)**: 보충·부록 캡션의 inline 표기를 새로 잡는다 — `Fig. S1.`·`Figure S1:`·`Figure A1.`(문자접두 번호), `Supplemental`/`Supporting Figure N`(접두), `FIG. 3 (color online).`·`Figure 1 (저자명).`(괄호 한정구). 전부 **점형 canonical**(`S.N`·`A.N`)으로 방출하므로 `num` 필드에 `"S.1"`·`"A.1"` 형태가 더 자주 등장한다(v2.6.0의 `ED.N`·prefix `S.N`과 동일한 표기 규약 — 새 값 형태 아님). 출력 필드·좌표계·(num,page) 식별자·manifest 스키마 불변, 소비자 코드 변경 불요. 주의: 한 물리 figure의 캡션에 다른 계열 라벨이 중첩된 오제출 문서(예: Extended Data 캡션 본문에 `Figure S1.`)는 같은 그림을 `ED.N`+`S.N` 두 번 방출할 수 있다(candidate suppression 미구현 — 엔진 repo 백로그, n=1 코너).
- **전면 figure 크롭 개선 (v2.12.0)**: Nature Extended Data류 **전면(full-page) figure**가 과대 패널티에 눌려 페이지 일부만 크롭되던 것을 해소했다 — 전면 figure의 크롭 영역이 더 정확(전체)해진다. 출력 필드·좌표계·(num,page)·manifest 스키마 불변, 소비자 코드 변경 불요(bbox가 truth에 더 가까워질 뿐).
- 엔진은 백그라운드 탭에서 크롬 타이머 스로틀링의 영향을 받는다(분석이 수십 배 느려짐).
전체 문서 스캔은 사용자가 뷰어를 보고 있는 동안 idle로 돌리는 것을 권장.

Expand All @@ -71,4 +90,4 @@ const seeds = toFigureEntries(res, (p) => pageHeights[p]);
`fig-engine.ts`·`fig-extract.d.ts` 타입과 이 문서의 계약 서술을 함께 갱신
4. 이 문서 §주의사항이 새 버전과 어긋나지 않는지 확인 (예: confidence 실측화 시 해당 항목 갱신)
5. `npm run typecheck && npm run build` 확인 후 그림·표 탭에서 샘플 PDF 1개 스모크 테스트
6. 커밋 메시지에 엔진 버전 명시 (예: `chore: bump fig-extract to v2.3.0`)
6. 커밋 메시지에 엔진 버전 명시 (예: `chore: bump fig-extract to v2.3.0`)
Loading