123 lines
9.3 KiB
Markdown
123 lines
9.3 KiB
Markdown
|
|
# Jude's Note — PDF Annotation สำหรับ e-Court
|
|||
|
|
|
|||
|
|
ระบบ annotate ไฟล์ PDF คดีบนเว็บ แบบ **Non-Destructive** — ไฟล์ PDF ต้นฉบับไม่ถูกแก้ไขเด็ดขาด annotation ทั้งหมด (highlight / เส้นวาด / comment / bookmark) เก็บใน PostgreSQL เป็นพิกัด % ของขนาดหน้า แล้วซิงก์ระหว่างทุก browser แบบ realtime ผ่าน **SSE (Server-Sent Events)**
|
|||
|
|
|
|||
|
|
สร้างด้วย Next.js 14 (App Router) + TypeScript strict + pdfjs-dist v4 + Drizzle ORM + PostgreSQL (Docker container เดียว)
|
|||
|
|
|
|||
|
|
## ฟีเจอร์
|
|||
|
|
|
|||
|
|
- **Highlight** — คลุมดำข้อความ เลือกสีจาก floating toolbar (5 สี) วาดซ้อนหลังข้อความด้วย `mix-blend-mode: multiply`
|
|||
|
|
- **Freehand draw** — วาดเส้นอิสระบน SVG layer ความหนาสัมพัทธ์กับขนาดหน้า ไม่เบี้ยวเวลา zoom
|
|||
|
|
- **Sticky note comment** — คลิกวาง pin บนหน้า พร้อม popover ข้อความ/ผู้เขียน/เวลา
|
|||
|
|
- **Bookmark** — จดตำแหน่ง (หน้า + % ความสูง) กดแล้ว smooth-scroll กลับตำแหน่งเดิม
|
|||
|
|
- **Realtime sync** — เปิด 2 แท็บ วาดในแท็บ A แท็บ B เห็นทันทีผ่าน SSE (`EventSource` ต่อใหม่อัตโนมัติเมื่อขาด)
|
|||
|
|
- **Page Virtualization** — render เฉพาะหน้าใน viewport ± 1 หน้า ไฟล์หลายสิบหน้าเลื่อนลื่น
|
|||
|
|
- ไม่มีระบบ login — anonymous identity เก็บใน `localStorage` (ปุ่มลบแสดงเฉพาะรายการของตัวเอง)
|
|||
|
|
|
|||
|
|
## สิ่งที่ต้องมี (Prerequisites)
|
|||
|
|
|
|||
|
|
| เครื่องมือ | เวอร์ชันที่ใช้พัฒนา/ทดสอบ | หมายเหตุ |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Node.js | v24.3.0 | ≥ 20 น่าจะใช้ได้ |
|
|||
|
|
| Docker | 29.1.3 | ใช้รัน PostgreSQL container ตัวเดียว |
|
|||
|
|
|
|||
|
|
## ขั้นตอน Setup จากศูนย์
|
|||
|
|
|
|||
|
|
### 1. ติดตั้ง dependencies
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm install
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> postinstall จะคัดลอก `pdf.worker.min.mjs` จาก `node_modules/pdfjs-dist/build/` มาไว้ที่ `public/` ให้อัตโนมัติ
|
|||
|
|
|
|||
|
|
### 2. รัน PostgreSQL container
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
docker run -d --name judes-note-db \
|
|||
|
|
-e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres \
|
|||
|
|
-p 54322:5432 \
|
|||
|
|
-v judes-note-db-data:/var/lib/postgresql/data \
|
|||
|
|
postgres:17-alpine
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
ครั้งต่อไปแค่ `docker start judes-note-db` — ข้อมูลอยู่ใน volume `judes-note-db-data` ไม่หาย
|
|||
|
|
|
|||
|
|
### 3. สร้างไฟล์ `.env`
|
|||
|
|
|
|||
|
|
```env
|
|||
|
|
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/ecourt-note
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
(เทียบกับ `.env.example` — มีแค่ตัวเดียว)
|
|||
|
|
|
|||
|
|
### 4. สร้างตารางในฐานข้อมูล
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npx drizzle-kit push
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5. Seed ไฟล์ตัวอย่าง
|
|||
|
|
|
|||
|
|
ไฟล์ตัวอย่างอยู่ที่ `public/samples/` แล้ว — ลงทะเบียนในตาราง `files` (รันเฉพาะเมื่อตารางว่าง ไม่งั้นข้อมูลจะซ้ำ):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
docker exec judes-note-db psql -U postgres -c "insert into files (title, pdf_url) values ('01012-672427-4f-690609-0000840227.pdf','/samples/01012-672427-4f-690609-0000840227.pdf'), ('01012-672427-1f-690609-0000840223.pdf','/samples/01012-672427-1f-690609-0000840223.pdf'), ('dashboard-report-2026-09-03.pdf','/samples/dashboard-report-2026-09-03.pdf');"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6. รัน dev server
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run dev
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
เปิด http://localhost:3000 — คลิกไฟล์เพื่อเข้าหน้า annotate
|
|||
|
|
|
|||
|
|
## การใช้งานบนหน้า annotate
|
|||
|
|
|
|||
|
|
| ปุ่ม | พฤติกรรม |
|
|||
|
|
|---|---|
|
|||
|
|
| ลูกศร (select) | คลุมดำข้อความ → เด้ง toolbar เลือกสี → ติด highlight บนหน้า |
|
|||
|
|
| ดินสอ (draw) | ลากเมาส์วาดเส้นอิสระ (ปล่อย = บันทึก) |
|
|||
|
|
| ลูกศร+กล่องแชท (comment) | คลิกจุดบนหน้า → พิมพ์ข้อความ → บันทึก pin |
|
|||
|
|
| bookmark+ | เพิ่ม bookmark ที่ตำแหน่งกึ่งกลางจอปัจจุบัน |
|
|||
|
|
| zoom − / + | สเกล 50%–300% (annotation ทั้งหมด scale ตามเพราะเก็บเป็น %) |
|
|||
|
|
|
|||
|
|
Sidebar ซ้าย: แท็บ Bookmarks / Annotations — คลิกรายการเพื่อเลื่อนกลับไปตำแหน่งนั้น และมีปุ่มลบเฉพาะรายการของตัวเอง (anonymous identity จาก `localStorage`)
|
|||
|
|
|
|||
|
|
ทดสอบ realtime: เปิดหน้า annotate ไฟล์เดียวกันใน 2 แท็บ แล้ววาด/comment จากแท็บหนึ่ง — อีกแท็บเห็นทันทีโดยไม่ต้อง refresh
|
|||
|
|
|
|||
|
|
## Troubleshooting
|
|||
|
|
|
|||
|
|
| อาการ | สาเหตุ/ทางแก้ |
|
|||
|
|
|---|---|
|
|||
|
|
| Realtime ไม่ซิงก์ | sync ใช้ pub/sub **in-memory ของ process เดียว** — ทุกแท็บต้องเข้าผ่าน dev server เดียวกัน (ถ้าวันหนึ่งรันหลาย instance ต้องเพิ่ม Redis pub/sub หรือ Postgres LISTEN/NOTIFY ก่อน) |
|
|||
|
|
| หน้าเว็บ 500 หลังรัน `next build` ระหว่างที่ dev server กำลังรัน | build เขียนทับ cache ของ dev — ปิด dev server ก่อน build หรือ restart dev server หลัง build |
|
|||
|
|
| ต่อ DB ไม่ได้ | ตรวจว่า container รันอยู่ (`docker start judes-note-db`) และพอร์ต 54322 ไม่ถูกใช้โดย container อื่น |
|
|||
|
|
| Highlight ใช้ไม่ได้กับไฟล์คดีตัวอย่าง | ไฟล์ `01012-*.pdf` ทั้งสองเป็น**ภาพสแกนล้วน** (ไม่มี text layer) — ใช้ `dashboard-report-2026-09-03.pdf` ซึ่งเป็น text-based PDF แทน จึงจะคลุมดำได้ (DRAW/COMMENT/BOOKMARK ใช้ได้ปกติกับทุกไฟล์) |
|
|||
|
|
|
|||
|
|
## หมายเหตุทางเทคนิค
|
|||
|
|
|
|||
|
|
- **Realtime = SSE + pub/sub in-process:** Server Actions (`app/actions/annotations.ts`) publish event หลังเขียน DB สำเร็จทุกครั้ง → `lib/annotate-events.ts` → SSE endpoint `app/api/annotations/stream` → hook เปิด `EventSource` รับ event (payload เป็น DTO จาก server ตรง ๆ)
|
|||
|
|
- UI ใช้ `useOptimistic` ที่มาจาก React canary build ซึ่ง Next.js 14 vendored ไว้ให้ — หากหยิบ component ไปรันนอก Next bundling (เช่น unit test ด้วย `react` เปล่า) จะหา export นี้ไม่เจอ
|
|||
|
|
- พิกัด annotation ทุกชนิดเก็บเป็น **% ของขนาดหน้า** → render ถูกต้องทุก zoom/ขนาดจอ และ `mergeRectsByLine` รวบ rect ให้เหลือ 1 อันต่อบรรทัดก่อนบันทึก highlight
|
|||
|
|
- สถาปัตยกรรม/สเปกฉบับเต็ม: `docs/PLAN.md` · กติกาที่ต้องรักษาตอนแก้โค้ด: `CLAUDE.md` · ฟีเจอร์ต่อยอด: `docs/PLAN-FEAT.md`
|
|||
|
|
|
|||
|
|
## โครงสร้างหลัก
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
db/schema.ts ตาราง + JSONB strict union + DTO + toDTO
|
|||
|
|
db/index.ts postgres.js + drizzle (prepare: false)
|
|||
|
|
lib/annotate-events.ts pub/sub in-process (actions → SSE route)
|
|||
|
|
utils/coordinates.ts px ⇄ % + clamp + mergeRectsByLine
|
|||
|
|
utils/user.ts anonymous identity (localStorage)
|
|||
|
|
hooks/useAnnotationRealtime.ts EventSource รับ SSE event + dedupe ด้วย id
|
|||
|
|
app/actions/annotations.ts Server Actions CRUD + publish event (ทางเขียนทางเดียว)
|
|||
|
|
app/api/annotations/stream/route.ts SSE endpoint (GET ?fileId=)
|
|||
|
|
app/page.tsx รายการไฟล์
|
|||
|
|
app/annotate/[id]/page.tsx โหลด file + initial data → PDFViewer
|
|||
|
|
components/pdf/ PDFViewer (root + virtualization), InkLayer, HighlightToolbar
|
|||
|
|
components/sidebar/ AnnotationSidebar
|
|||
|
|
public/samples/ ไฟล์ PDF ตัวอย่าง
|
|||
|
|
```
|