브라우저에서 동작하는 PDF 온라인 편집기. PDF 파서/라이터를 외부 라이브러리 없이 PDF 객체 레벨에서 직접 구현하는 것을 핵심 원칙으로 한다.
상태: 앱 버전 0.7.4 (
/api/health가 반환하는VERSION상수 기준;package.json은0.1.0으로 별도 관리되지 않음). 변경 내역은docs/14~16변경 요약(v0.2~v0.4)과 git 이력을 본다. 보류/대안 결정:docs/adr/.
PDF 를 서버에 올려 페이지 삭제·재배치·병합, 텍스트 편집·추가를 브라우저에서 하고 결과 PDF 를 내려받는다. 외부 PDF 라이브러리 없이 파서/라이터/렌더러를 직접 구현했다.
POST /api/documents/{docId}/ops 가 받는 연산(frontend/src/pdf/ops/types.ts, 한 번에 최대 200개):
| op | 설명 |
|---|---|
delete-pages |
페이지 삭제 |
reorder-pages |
페이지 순서 변경 (permutation) |
rotate-pages |
페이지 회전 (90 / -90 / 180) |
add-text |
텍스트 추가 (Core 14 폰트 또는 업로드한 TTF) |
edit-text |
추출된 텍스트 조각 편집 |
edit-text-group |
인접한 텍스트 조각을 묶어 편집 |
그 밖에 undo/redo, 다른 PDF 삽입(insert-pdf), 여러 문서에서 페이지를 골라 병합(POST /api/merge, 화면 /m), TTF 업로드(/fonts), 페이지별 SVG/썸네일 렌더, 한/영 UI 전환을 제공한다. 암호화된 PDF 는 거부(415 unsupported-encrypted)하고, 업로드는 %PDF- 헤더가 필요하다.
- PDF 처리에 어떤 외부 라이브러리도 사용하지 않는다 —
pdf.js,pdf-lib,jsPDF,mupdf,pdfkit,poppler등 전부 금지. - PDF 명세(ISO 32000-1)에 기반해 헤더/객체/스트림/xref/트레일러를 직접 파싱하고 직렬화한다.
- 압축 필터(
FlateDecode등)는 Node 표준 라이브러리(zlib)와 WebCompressionStream만 사용 — PDF 전용 라이브러리는 금지. - 자세한 의사결정 근거는
docs/adr/0001-no-pdf-libraries.md참고.
- 텍스트 편집 — 페이지에서 추출된 텍스트 조각을 인라인 편집 (
docs/06-features.md#1) - 텍스트 추가 — 임의 위치에 새 텍스트 박스 삽입 (
#2) - 페이지 순서 변경 — 드래그로 페이지 재배치 (
#3) - 페이지 삭제 — 단일/다중 페이지 삭제 (
#4) - PDF 병합 — 여러 PDF에서 페이지를 골라 새 PDF로 합성 (
#5)
┌────────────────────────── Edit2me (별도 repo: github.com/CocoRoF/Edit2me) ──┐
│ │
│ Next.js 15 (App Router, basePath=/edit2me) │
│ ├─ app/ ─ UI (React 19 + Tailwind 4) │
│ ├─ app/api/ ─ 서버 사이드 PDF 파싱/직렬화 │
│ └─ src/pdf/ ─ 자체 PDF 엔진 (parser, writer, renderer, ops) │
│ │
└──────────────────────────────────┬─────────────────────────────────────────┘
│ 호스트 빌드 시 git clone
▼
┌───────────────────────────────────────────────────────┐
│ hr_blog2.0 (별도 repo, 호스트) │
│ edit2me/ │
│ ├─ Dockerfile ← git clone Edit2me 후 빌드 │
│ └─ Dockerfile.dev ← 동일 │
│ │
│ docker-compose: edit2me-frontend service │
│ nginx ─ /edit2me/* → edit2me-frontend:3000 │
│ └ /uploads/* → minio:9000 │
│ minio ─ 버킷 `pdf-edit` (Edit2me 전용) │
└───────────────────────────────────────────────────────┘
소스는 분리. 호스트(hr_blog2.0)는 자체 Dockerfile로 빌드 시점에 Edit2me를 git clone해서 가져온다 — 호스트 파일시스템에 Edit2me가 같이 있을 필요 없음. 자세한 통합 방식은 docs/09-integration-hr-blog.md.
| # | 문서 | 내용 |
|---|---|---|
| 00 | vision.md | 목표, 비목표, 성공 기준 |
| 01 | architecture.md | 컴포넌트, 데이터 흐름, 디렉토리 구조 |
| 02 | pdf-format.md | PDF 바이너리 포맷 핵심 정리 (구현 레퍼런스) |
| 03 | parser.md | 토크나이저 → 객체 → 문서 트리 |
| 04 | writer.md | 객체 직렬화, xref, incremental update |
| 05 | renderer.md | 페이지를 Canvas에 그리는 자체 렌더러 |
| 06 | features.md | MVP 기능의 사양과 알고리즘 |
| 07 | ui-ux.md | 화면 구성, 상호작용, 단축키 |
| 08 | api-contract.md | Next.js Route Handler 계약 |
| 09 | integration-hr-blog.md | nginx/docker-compose/MinIO 통합 |
| 10 | roadmap.md | 단계별 마일스톤 |
| 11 | testing.md | 테스트 전략과 코퍼스 |
| 12 | risks.md | 리스크와 미해결 질문 |
| 13 | quality-review.md | v0 audit + 진행 체크리스트 |
| 14 | v0.2-changelog.md | PR #1–9 변경 요약 |
| 15 | v0.3-changelog.md | PR #10–18 변경 요약 |
| 16 | v0.4-changelog.md | PR #19–23 vector renderer |
ADR: docs/adr/ (0001 no-libs · 0002 nextjs-monolith · 0003 mount-under-blog · 0004 raster-deferred)
- Next.js 15 (App Router) — UI + API Route Handlers
- React 19 + TypeScript + Tailwind CSS 4 (hr_blog2.0과 동일 스택)
- Node.js 런타임 (API 측 PDF 파싱). 저장소의 Dockerfile 은
node:22-slim,build:cmaps는 Node 22+ 필요(내장fetch). - S3 호환 객체 스토리지 (MinIO 등) — 호스트가 제공. 클라이언트는
@aws-sdk/client-s3(PDF 라이브러리가 아니므로 ADR-0001 허용) - 페이지 DnD:
@dnd-kit, 아이콘:lucide-react
추가 의존성은 ADR을 통해서만 들어온다.
Edit2me는 자체 .env 파일을 가지지 않는다. 모든 설정은 호스트(예: hr_blog2.0의 docker-compose)가 컨테이너에 환경변수로 주입한다. 라이브러리는 자기를 호스팅하는 환경을 알지 않는다.
| 변수 | 필수 | 기본값 | 설명 |
|---|---|---|---|
NEXT_PUBLIC_BASE_PATH |
권장 | /edit2me |
URL prefix. 빌드/런타임 양쪽에 필요. |
MINIO_ENDPOINT |
✓ | localhost:9000 |
host:port |
MINIO_ACCESS_KEY |
✓ | MINIO_ROOT_USER, 없으면 빈 값 |
S3 자격증명 |
MINIO_SECRET_KEY |
✓ | MINIO_ROOT_PASSWORD, 없으면 빈 값 |
S3 자격증명 |
MINIO_BUCKET |
— | edit2me |
업로드/결과를 둘 버킷 |
MINIO_SECURE |
— | false |
true면 https |
EDIT2ME_MAX_UPLOAD_MB |
— | 200 |
업로드 한도 |
EDIT2ME_GIT_SHA, EDIT2ME_BUILT_AT |
— | unknown |
/api/health 응답에 노출되는 빌드 정보 |
EDIT2ME_CMAP_OUT_DIR |
— | 자동 탐지 | npm run build:cmaps 출력 경로 |
오브젝트 만료(24h 등)는 코드가 아니라 운영자가 버킷 라이프사이클로 적용한다. 서버 메모리 내 문서 캐시 TTL 은 5분(lib/doc-cache.ts)이다.
호스트 측 통합 예시: hr_blog2.0의 docker-compose.dev.yml 의 edit2me-frontend 서비스가 environment: 블록으로 위 변수를 직접 주입한다. Edit2me repo 안에는 어떤 .env* 파일도 두지 않는다.
MinIO(또는 S3 호환 스토리지)가 localhost:9000 에서 돌고 있어야 업로드가 동작한다.
cd frontend/src
npm install
npm run build:cmaps # (선택) CJK CMap 생성 — 한글/CJK 텍스트 편집에 필요
export MINIO_ACCESS_KEY=... MINIO_SECRET_KEY=...
npm run dev # 3000 포트, basePath /edit2me
# http://localhost:3000/edit2me (헬스체크: /edit2me/api/health)컨테이너: frontend/Dockerfile(production, npm run build 후 npm run start)과 frontend/Dockerfile.dev(hot reload) 모두 frontend/src/ 를 /app 에 복사하고 3000 포트를 연다.
frontend/src 에서 실행한다.
npm run typecheck # tsc --noEmit
npm run test # vitest run (테스트 8개 파일, it/test 48개)
npm run build # next build (production)
npm run build:cmaps # Adobe-Korea1/Japan1/GB1/CNS1 → pdf/fonts/cid-mappings/data/*.json (gitignore 대상)