# Judge'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/` แล้ว — sync ลงตาราง `files` (idempotent รันซ้ำได้): ```bash npm run seed-samples ``` > บน production ไม่ต้องรันมือ — script เดียวกัน (`scripts/seed-samples.mjs`) รันอัตโนมัติตอน container start ต่อจาก migration ไฟล์ที่หายจาก `public/samples/` จะถูกลบ row พร้อม annotation ที่ผูกอยู่ (CASCADE) ### 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 ตัวอย่าง ```