แก้เฉพาะชื่อที่แสดงต่อผู้ใช้และเอกสาร: หัวเว็บหน้าแรก (app/page.tsx), <title> ทุกหน้า (app/layout.tsx), README, CLAUDE.md, docs/PLAN.md, docs/PLAN-FEAT.md, .claude/agents/* — โลโก้ JN ใช้ initials เดิมได้ คง identifier ทางเทคนิคไว้: container judes-note-db + volume, package.json name, localStorage key "judes-note:user" (เปลี่ยนแล้ว identity/ข้อมูลเดิมหาย) Co-Authored-By: Claude Code <noreply@anthropic.com>
12 KiB
CLAUDE.md
ภาพรวมโปรเจกต์
Judge's Note — ระบบ PDF Annotation สำหรับ e-Court (โปรเจกต์ Next.js แยกต่างหาก ไม่เกี่ยวกับ dms-main ที่เป็น Nuxt)
- ฟีเจอร์: highlight ข้อความ, วาดเส้น freehand, sticky note comment, bookmark
- อ่านแผนฉบับเต็มก่อนเริ่มงานใด ๆ:
docs/PLAN.md(สถาปัตยกรรม, schema, พฤติกรรม feature, ลำดับ implement, edge cases) — ฟีเจอร์ต่อยอด (F1–F19 + N1/N2) อ่านเพิ่ม:docs/PLAN-FEAT.md
Tech Stack
- Next.js 14 (App Router) + TypeScript strict
- pdfjs-dist v4 (
^4.10.38) — built-in TextLayer + worker จากpublic/pdf.worker.min.mjs(postinstall คัดลอกจาก node_modules) - Drizzle ORM + PostgreSQL (Docker container เดียว:
judes-note-db, imagepostgres:17-alpine, พอร์ต 54322) — ไม่ใช้ Supabase + realtime sync ด้วย SSE (Server-Sent Events) ผ่านapp/api/annotations/stream+ pub/sub in-process (lib/annotate-events.ts) - Tailwind CSS
- ไม่มีระบบ auth — anonymous identity จาก localStorage (
utils/user.ts)
Invariants ที่ต้องรักษา (ห้ามละเมิด)
- Non-Destructive: ไฟล์ PDF ต้นฉบับไม่ถูกแก้ไขเด็ดขาด — annotation เก็บใน DB เป็นพิกัดเท่านั้น (export PDF ของ F8 สร้างไฟล์ใหม่จากสำเนาในหน่วยความจำ — อ่าน
public/อย่างเดียว ห้ามเขียนทับ) - พิกัดทุกอย่างใน DB เป็น % ของขนาดหน้า —
rects,paths,position,strokeWidth(% ของ width),yOffset(% ของ height) — แปลง px⇄% ผ่านutils/coordinates.tsเท่านั้น และก่อนบันทึก HIGHLIGHT ต้องรวบ rects ด้วยmergeRectsByLineเหลือ 1 rect ต่อบรรทัด - ลำดับ layer ตาม PLAN.md §1: canvas (
pointer-events: noneเสมอ) → highlight-layer (noneเสมอ,mix-blend-mode: multiply) → textLayer → InkLayer (SVG) → comment pins — สลับ pointer-events ตาม tool ปัจจุบัน - เขียนผ่าน Server Actions เท่านั้น (
app/actions/annotations.ts) และหลังเขียน DB สำเร็จทุกครั้ง ต้อง publish event ผ่านlib/annotate-events.ts+revalidatePathหน้า viewer และ summary ของ fileId นั้น (helperrevalidateFilePages— สั่ง browser ทิ้ง Router Cache ฝั่ง client,force-dynamicปิดแค่ฝั่ง server ไม่พอ: ไม่งั้นกด back แล้วคลิกเข้าไฟล์ใหม่ยังได้ initial data เก่าจนต้อง hard refresh) / อ่าน + sync ผ่าน SSE hook (hooks/useAnnotationRealtime.tsเปิดEventSourceไป/api/annotations/stream?fileId=) — event เป็น DTO (camelCase + ISO string) จาก server ตรง ๆ - Timestamp ข้าม server→client→SSE เป็น ISO string เสมอ (DTO types) — ห้ามส่ง Date object ตรง ๆ
- pdfjs-dist ต้อง
dynamic import()ภายใน useEffect เท่านั้น — ห้าม import top-level ใน module scope ที่ server evaluate และต้องตั้งGlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs"(postinstall คัดลอกไฟล์ไปpublic/) — ห้ามใช้new URL(..., import.meta.url) - InkLayer ใช้ SVG แบบไม่มี viewBox — render ด้วย px จริง แปลง % → px จากขนาดที่ ResizeObserver วัด (viewBox จะทำให้ความหนาเส้นบิดเบี้ยว)
- JSONB
dataเป็น strict discriminated union (AnnotationDataแยกตาม enumtype) — ห้าม loosen type เป็นany/generic object - Optimistic update ใช้
useOptimistic(React 18) ภายในstartTransition— React rollback อัตโนมัติเมื่อ action ล้มเหลว + dedupe ด้วย id ในhooks/useAnnotationRealtime.ts(duplicate ชั่วขณะกับ realtime event หายเอง) - Page Virtualization: render เฉพาะหน้าใน viewport ± 1 หน้าด้วย IntersectionObserver — หน้าพ้นจอเก็บ placeholder ขนาดจริง (scroll ไม่ jump, bookmark ยังคำนวณตำแหน่งได้) และ unmount canvas/TextLayer/InkLayer คืน memory — ห้าม render ทุกหน้าพร้อมกัน
- DB connection:
DATABASE_URLชี้ที่ plain PostgreSQL containerjudes-note-db(postgresql://postgres:postgres@127.0.0.1:54322/ecourt-note) +prepare: falseใน postgres.js เสมอ — pub/sub ของ SSE เป็น in-memory (ใช้ได้เมื่อรัน process เดียว; ถ้ารันหลาย instance ต้องเพิ่ม Redis pub/sub หรือ Postgres LISTEN/NOTIFY ก่อน)
โครงสร้างไฟล์ (ตามแผน)
db/schema.ts ← ตาราง + JSONB types + DTO + mapper toDTO
db/index.ts ← postgres.js + drizzle (DATABASE_URL → container judes-note-db, prepare:false)
lib/annotate-events.ts ← pub/sub in-process (actions publish → SSE route subscribe)
lib/server-pdf.ts ← path guard อ่าน PDF จาก public/ (export ใช้)
utils/coordinates.ts ← pixel⇄% + getPointerPercent + clamp + mergeRectsByLine
utils/geometry.ts ← bbox/translate/scale/hitTestPath ของ annotation (pure, หน่วย %)
utils/annotation-display.ts ← label/ข้อความย่อต่อชนิด + ค่า default TEXTBOX/STAMP (sidebar/สรุป/CSV ใช้ร่วม)
utils/pdf-export.ts ← แปลงพิกัดจอ⇄PDF (/Rotate) + wrapText + hexToRgb (export route)
utils/user.ts ← anonymous identity (localStorage)
hooks/useAnnotationRealtime.ts ← EventSource ต่อ SSE stream + dedupe ด้วย id
hooks/useHistory.ts ← undo/redo stack ฝั่ง client (F2)
app/actions/annotations.ts ← Server Actions CRUD + validation + publish event
app/api/annotations/stream/route.ts ← SSE endpoint (GET ?fileId=)
app/api/annotations/export/route.ts ← export PDF ฝัง annotation (pdf-lib, F8)
app/api/annotations/export-csv/route.ts ← export สรุป CSV พร้อม BOM (F16)
app/page.tsx ← รายการไฟล์ (server component)
app/annotate/[id]/page.tsx ← โหลด file + initial data → PDFViewer
app/annotate/[id]/summary/page.tsx ← หน้าสรุปหมายเหตุจัดกลุ่มตามหน้า (F15)
components/pdf/PDFViewer.tsx ← client root (tool state, zoom, selection, lazy render หน้า)
components/pdf/InkLayer.tsx ← SVG freehand + รูปทรง + redaction
components/pdf/TextboxStampLayer.tsx ← overlay TEXTBOX + STAMP (F17/F18)
components/pdf/AnnotationSelectionLayer.tsx ← กรอบ selection + handles ย้าย/ปรับขนาด (F1) + แถบสี/ความหนา + ปุ่มลบ
components/sidebar/AnnotationSidebar.tsx ← + โน้ตคำอธิบาย (note) inline ต่อรายการ (PLAN-FEAT.md §7 N1)
ขอบเขต — ห้ามทำเกิน (Out of Scope)
ขอบเขตด้านล่างเป็นของเฟสแรก — ฟีเจอร์ต่อยอด (edit/ย้าย, undo/redo, export, reply, resolve ฯลฯ) อนุญาตเฉพาะตาม task F1–F19 ใน
docs/PLAN-FEAT.mdและทำผ่าน/implement-featเท่านั้น ห้ามทำเกินสเปกของแผนนั้น — ปัจจุบันทำครบตามแผนแล้ว ยกเว้น F11 (export/import JSON) ที่ถูกถอดออกจากโค้ดแล้ว: แก้ไข/ย้าย/ปรับขนาด (F1), undo/redo (F2), คีย์ลัด (F3), กรองรายคน (F4), รูปทรง (F5), eraser (F6), สไตลัส/pressure (F7), redaction + export PDF (F8), reply เธรด (F9), resolve (F10), deep link (F12), แก้ไข comment (F13), ค้นหา (F14), หน้าสรุป (F15), export CSV (F16), text box (F17), stamp (F18), version history + กู้คืน (F19) — ฟีเจอร์เสริมหลังแผน (อนุญาตตาม §7 ของdocs/PLAN-FEAT.md): โน้ตคำอธิบายnote?ฝังใน annotation ทุกชนิดยกเว้น COMMENT แก้ไขใน sidebar + hover tooltip (N1), bookmark โครงสร้าง 3 ระดับparent_id+ โน้ต + popup สร้าง/แก้ไข/ย้ายหัวข้อ + ปุ่ม bookmark ในแผง selection + ชื่อ default จากข้อความที่คลุม (N2)
- ไม่มี login/roles, ไม่มี presence — undo/redo เป็น history ฝั่ง client ต่อ session ต่อผู้ใช้ ไม่เก็บใน DB (D10) ส่วน version history/กู้คืน (F19) เก็บในตาราง
annotation_versions - Annotation: สร้าง + ลบ + แก้ไข/ย้าย/ปรับขนาด/เปลี่ยนสี–ความหนา/แก้ไขข้อความ ของตัวเอง ในหน้าเดียวกัน — resolve เป็นสิ่งเดียวที่ใครก็ toggle ได้ (D6)
- ปุ่มลบ/แก้ไข/ประวัติแสดงเฉพาะรายการของตัวเอง (
userIdตรงกัน) เท่านั้น - หน้า
/อ่านรายการจาก DB เท่านั้น — ไม่มีหน้า upload ไฟล์ (sample data seed ด้วย SQL ใน README) - ลายเซ็นจากรูป/upload ยังเป็น out of scope (D7 — stamp เป็น preset ข้อความเท่านั้น, ลายเซ็นใช้ tool DRAW วาด)
Claude Tooling (ใน .claude/)
agents/plan-reviewer— ตรวจโค้ดกับ invariants ด้านบน + verify criteria ของ task (report-only ไม่แก้โค้ด) — เรียกหลัง implement แต่ละ taskskills/implement-task—/implement-task <N>implement task N จาก PLAN.md §7 พร้อม verify ตามเกณฑ์skills/implement-feat—/implement-feat <F-number หรือ feature id>implement task F1–F19 จาก PLAN-FEAT.md พร้อม verify + sync-db + sync invariant/rules ตามตาราง §6 ของแผนนั้นskills/sync-db—/sync-dbdrizzle push + seed ไฟล์ตัวอย่างrules/*.md— กติกาเชิงปฏิบัติแยกตามพื้นที่ (pdf-viewer,coordinates,data-layer,realtime) โหลดอัตโนมัติเมื่อแตะไฟล์ที่ match — แก้ invariant ต้องแก้ที่ CLAUDE.md/PLAN.md ก่อน แล้วค่อย sync มา rules