eCourt-note/CLAUDE.md
Your Name e713bcc5e3
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 52s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 6s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
summary: เปลี่ยนคำว่า "สารบัญ" เป็น "รายการตามหน้า" ทั้งหน้าสรุป — ตามคำขอผู้ใช้ 2026-09-18 (คำเดิมสื่อว่าเป็นสารบัญครบทุกหน้าของเอกสาร แต่บล็อกนี้แสดงเฉพาะหน้าที่มี annotation/bookmark คำใหม่ตรงเนื้อหาจริงกว่า) แก้ให้เป็นชื่อเดียวกันทุกจุด: หัวข้อ desktop (h2), ป้ายมือถือ <details> ("รายการตามหน้าและผู้ร่วมทำ"), aria-label ของ <nav> ทั้ง 2 จุด (ตัดส่วน "หน้าที่มีรายการ" ทิ้ง — คำใหม่สื่อความหมายนั้นอยู่แล้วและ link แต่ละรายการมี aria-label "หน้า N, X รายการ" ของตัวเอง) + sync comment ที่เรียกบล็อกนี้ (page.tsx 6 จุด, globals.css 1 จุด) และคำอธิบายหน้าใน CLAUDE.md (3 จุด) — เหลือคำ "สารบัญ" จุดเดียวโดยเจตนาใน comment page.tsx ("จุดไข่ปลาแบบสารบัญของเอกสารราชการ") เพราะอ้างถึงรูปแบบภาพ dot leader ไม่ใช่ชื่อบล็อก — เป็นการแก้ string/comment ล้วน ไม่แต่ตรรกะ (grep ยืนยันไม่มี test/e2e ผูกข้อความนี้)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 12:09:43 +07:00

20 KiB
Raw Permalink Blame History

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–N7) อ่านเพิ่ม: docs/PLAN-FEAT.md

Tech Stack

  • Next.js 14 (App Router) + TypeScript strict — serve ใต้ basePath /docedit/note (next.config.mjs)
  • 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, image postgres:17-alpine, พอร์ต 54322) — ไม่ใช้ Supabase + realtime sync ด้วย SSE (Server-Sent Events) ผ่าน app/api/annotations/stream + pub/sub in-process (lib/annotate-events.ts)
  • Tailwind CSS
  • mock login e-Court — หน้า /login (dropdown MOCK_USERS user1–user5 + รหัสผ่านกลาง Demo1234) ตั้ง cookie oacCase ตาม format จริงของระบบศาล → identity (userId = u_userid, userName = u_userlogin — ต่างกันต่อคน เพื่อสิทธิ์เจ้าของ) อ่านจาก cookie ผ่าน lib/session.ts + บังคับทุกหน้าด้วย middleware.ts (auth จริงเทียบระบบ e-Court + roles ยัง out of scope)

Invariants ที่ต้องรักษา (ห้ามละเมิด)

  1. Non-Destructive: ไฟล์ต้นฉบับ (PDF/รูปภาพ) ไม่ถูกแก้ไขเด็ดขาด — annotation เก็บใน DB เป็นพิกัดเท่านั้น (export PDF ของ F8/N3 สร้างไฟล์ใหม่จากสำเนาในหน่วยความจำ — อ่าน public/ อย่างเดียว ห้ามเขียนทับ; อัปโหลด N4 เขียนไฟล์ใหม่ที่ public/uploads/ เท่านั้น ห้ามแตะไฟล์เดิม)
  2. พิกัดทุกอย่างใน DB เป็น % ของขนาดหน้า — rects, paths, position, strokeWidth (% ของ width), yOffset (% ของ height) — แปลง px⇄% ผ่าน utils/coordinates.ts เท่านั้น และก่อนบันทึก HIGHLIGHT ต้องรวบ rects ด้วย mergeRectsByLine เหลือ 1 rect ต่อบรรทัด
  3. ลำดับ layer ตาม PLAN.md §1: canvas (pointer-events: none เสมอ) → highlight-layer (none เสมอ, mix-blend-mode: multiply) → textLayer → InkLayer (SVG) → comment pins — สลับ pointer-events ตาม tool ปัจจุบัน
  4. เขียนผ่าน Server Actions เท่านั้น (app/actions/annotations.ts) และหลังเขียน DB สำเร็จทุกครั้ง ต้อง publish event ผ่าน lib/annotate-events.ts + revalidatePath หน้า viewer และ summary ของ fileId นั้น (helper revalidateFilePages — สั่ง 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 ตรง ๆ
  5. Timestamp ข้าม server→client→SSE เป็น ISO string เสมอ (DTO types) — ห้ามส่ง Date object ตรง ๆ
  6. pdfjs-dist ต้อง dynamic import() ภายใน useEffect เท่านั้น — ห้าม import top-level ใน module scope ที่ server evaluate และต้องตั้ง GlobalWorkerOptions.workerSrc = withBasePath("/pdf.worker.min.mjs") (postinstall คัดลอกไฟล์ไป public/) — ห้ามใช้ new URL(..., import.meta.url)
  7. InkLayer ใช้ SVG แบบไม่มี viewBox — render ด้วย px จริง แปลง % → px จากขนาดที่ ResizeObserver วัด (viewBox จะทำให้ความหนาเส้นบิดเบี้ยว)
  8. JSONB data เป็น strict discriminated union (AnnotationData แยกตาม enum type) — ห้าม loosen type เป็น any/generic object
  9. Optimistic update ใช้ useOptimistic (React 18) ภายใน startTransition — React rollback อัตโนมัติเมื่อ action ล้มเหลว + dedupe ด้วย id ใน hooks/useAnnotationRealtime.ts (duplicate ชั่วขณะกับ realtime event หายเอง)
  10. Page Virtualization: render เฉพาะหน้าใน viewport ± 1 หน้าด้วย IntersectionObserver — หน้าพ้นจอเก็บ placeholder ขนาดจริง (scroll ไม่ jump, bookmark ยังคำนวณตำแหน่งได้) และ unmount canvas/TextLayer/InkLayer คืน memory — ห้าม render ทุกหน้าพร้อมกัน
  11. DB connection: DATABASE_URL ชี้ที่ plain PostgreSQL container judes-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 ก่อน)
  12. URL ฝั่ง browser ที่สร้างเองต้องผ่าน withBasePath() (utils/base-path.ts) — app serve ใต้ basePath /docedit/note ครอบคลุม: fetch()/EventSource/plain <a href> (export CSV/JSON)/workerSrc/img.src/file.pdfUrl ตอนโหลดไฟล์/ลิงก์คัดลอก F12 — ส่วน <Link>/next/navigation (router.push/replace/redirect) เติม basePath ให้เอง และ middleware.ts จับ path ที่ตัด basePath แล้ว (เขียน matcher/url.pathname เหมือนเดิม — NextURL ใส่ basePath กลับตอน redirect เอง); pdf_url ใน DB เก็บ path ไม่รวม basePath เติมตอนใช้เท่านั้น

โครงสร้างไฟล์ (ตามแผน)

db/schema.ts          ← ตาราง + JSONB types + DTO + mapper toDTO
db/index.ts           ← postgres.js + drizzle (DATABASE_URL → container judes-note-db, prepare:false)
middleware.ts         ← บังคับ login ทุกหน้า — ไม่มี cookie oacCase ที่ parse ได้ = redirect /login (ยกเว้น /login, /api/*, /uploads/*, static ใน public/)
lib/annotate-events.ts ← pub/sub in-process (actions publish → SSE route subscribe)
lib/server-pdf.ts     ← path guard อ่าน PDF จาก public/ (export ใช้)
lib/session.ts        ← getSessionUser — identity จาก cookie oacCase (mock login e-Court)
utils/coordinates.ts  ← pixel⇄% + getPointerPercent + clamp + mergeRectsByLine
utils/geometry.ts     ← bbox/translate/scale/hitTestPath ของ annotation (pure, หน่วย %)
utils/annotation-display.ts ← label/ข้อความย่อต่อชนิด + ค่า default TEXTBOX/STAMP + describeAnnotationChange สรุปสิ่งที่เปลี่ยนของ version history F19 (sidebar/สรุป/CSV ใช้ร่วม)
utils/date-format.ts  ← formatThaiDate วันที่ไทยแบบเต็ม "18 กันยายน พ.ศ. 2569" (+เวลา "… 16:33:00 น.") — ทุกจุดที่แสดงวันที่ใน UI ใช้ร่วม (เว้น export CSV ที่ lock Asia/Bangkok)
utils/pdf-export.ts   ← แปลงพิกัดจอ⇄PDF (/Rotate) + wrapText + hexToRgb (export route)
utils/user.ts         ← parse/build ค่า cookie oacCase → OacUser {userId: u_userid, userName: u_userlogin} (pure module — middleware ใช้ร่วมได้)
utils/base-path.ts    ← withBasePath() เติม basePath /docedit/note ให้ URL ฝั่ง browser ที่ Next ไม่เติมให้เอง (ค่าจาก next.config.mjs ผ่าน env — invariant #12)
utils/file-kind.ts    ← derive ชนิดไฟล์ pdf/image จาก extension ของ path (N3 — ใช้ร่วม viewer/export/หน้ารายการ/seed)
utils/tiff-client.ts  ← decode TIFF ทุกเฟรม → PNG blob URL ฝั่ง client (N3)
utils/image-to-pdf.ts ← สร้าง PDF ใหม่จากไฟล์รูป embedJpg/embedPng + EXIF rotate + TIFF→PNG (N3, export route)
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/actions/auth.ts   ← Server Action login (mock user_x/Demo1234 → ตั้ง cookie oacCase) / logout (ลบ cookie)
app/actions/files.ts  ← Server Action uploadFile — validate + เขียนไฟล์ใหม่ที่ public/uploads/ (N4)
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/api/bookmarks/export/route.ts        ← ส่งออกรายการ bookmark เป็น JSON (N7 — ข้อมูลล้วนจาก DB ไม่โหลดไฟล์ต้นฉบับ; เดิมเป็นรายงาน PDF เปลี่ยนตามคำขอผู้ใช้ 2026-09-17)
app/uploads/[name]/route.ts          ← เสิร์ฟไฟล์อัปโหลดจาก public/uploads/ (N4 — Next production เห็นไฟล์ static เฉพาะที่มีตอน startup ไฟล์ที่เขียน runtime ต้องเสิร์ฟผ่าน route)
app/page.tsx                        ← รายการไฟล์ (server component)
app/login/page.tsx                  ← หน้า login (มี session แล้ว redirect /)
app/annotate/[id]/page.tsx          ← โหลด file + initial data → PDFViewer
app/annotate/[id]/summary/page.tsx  ← หน้าสรุป Annotation+Bookmark จัดกลุ่มตามหน้า (design "แฟ้มสำนวน" 2026-09-17: แถบหัวน้ำเงิน + รายการตามหน้าจุดไข่ปลา + การ์ดรายหน้า 1 หน้า 1 การ์ด ขอบ folio — คำขอผู้ใช้ 2026-09-17 แทนแผ่นกระดาษแผ่นเดียวคั่นเส้น; ต่อหน้า = บรรทัดสรุปหน้า (ไม่มีหัวข้อ) + Bookmark/Annotation หุบ-กางได้ผ่าน CollapsibleBlock; รายการตามหน้า desktop = การ์ด sticky มี max-h, มือถือ < lg = <details> การ์ด ไม่มี JS; generateMetadata ตั้งชื่อแท็บ browser ตามคดี; ตอนพิมพ์ header เป็นกรอบเส้นน้ำเงิน + ซ่อนแถบ action/รายการตามหน้ามือถือ — กฎ @media print อยู่ใน globals.css) — อย่าถอด class `summary-page` ของ <main> (กฎ coarse 44px ใน globals.css ผูกอยู่กับ selector นี้ ครอบทั้ง `a` และ `button`)
components/summary/CollapsibleBlock.tsx ← client component เดียวของหน้าสรุป (state หุบ/กางกลุ่ม Bookmark/Annotation — children ส่งจาก server ได้ตรง ๆ)
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)
components/AnnotationTypeIcon.tsx ← ไอคอนตามชนิด annotation (ย้ายจาก sidebar) — ไม่มี "use client" ให้ server component ใช้ร่วมได้ (sidebar + หน้าสรุป)
components/UploadFileForm.tsx ← ฟอร์มอัปโหลดไฟล์บนหน้ารายการ (N4)
components/LoginForm.tsx ← ฟอร์ม login (client — เรียก action login แล้ว router.replace("/"))
components/SessionBar.tsx ← แถบแสดงผู้ใช้ที่ login + ปุ่มออกจากระบบบนหน้ารายการ (แทน UserPicker เดิม)

ขอบเขต — ห้ามทำเกิน (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), รองรับไฟล์รูปภาพ jpg/jpeg/png/tif เป็นเอกสาร annotate ได้ครบทุกชนิด + TIFF หลายเฟรม = หลายหน้า + export PDF ฝังรูป (N3 — แผน docs/PLAN-IMAGE.md), อัปโหลดไฟล์ PDF/รูปผ่านหน้ารายการแบบง่าย (N4), bookmark เรียงตามลำดับหน้า (N5), ค้นหา bookmark พร้อมเงื่อนไข คำค้น/ช่วงหน้า/ผู้บันทึก/ช่วงวันที่ + bookmarks.user_name (N6 — แผน docs/PLAN-BOOKMARK.md), รายงานรายการ bookmark เป็น PDF ผ่าน route แยก (สร้างเอกสารใหม่ล้วน ไม่โหลดไฟล์ต้นฉบับ — N7 แผน docs/PLAN-BOOKMARK-PDF.md — 2026-09-17 เปลี่ยน deliverable เป็นส่งออก JSON แทนรายงาน PDF ตามคำขอผู้ใช้: flat array BookmarkDTO เรียง N5 + envelope fileId/title/exportedAt ผ่าน route เดิม)

  • ไม่มี auth จริง/roles (login เป็น mock หน้า /login ตั้ง cookie oacCase — ไม่ได้ตรวจกับระบบ e-Court จริง), ไม่มี presence — undo/redo เป็น history ฝั่ง client ต่อ session ต่อผู้ใช้ ไม่เก็บใน DB (D10) ส่วน version history/กู้คืน (F19) เก็บในตาราง annotation_versions
  • Annotation: สร้าง + ลบ + แก้ไข/ย้าย/ปรับขนาด/เปลี่ยนสี–ความหนา/แก้ไขข้อความ ของตัวเอง ในหน้าเดียวกัน — resolve เป็นสิ่งเดียวที่ใครก็ toggle ได้ (D6)
  • ปุ่มลบ/แก้ไข/ประวัติแสดงเฉพาะรายการของตัวเอง (userId ตรงกัน) เท่านั้น
  • หน้า / อ่านรายการจาก DB + ฟอร์มอัปโหลดไฟล์เดียวแบบง่าย (N4 — PDF/jpg/jpeg/png/tif/tiff ≤ 100MB เขียนไฟล์ใหม่ที่ public/uploads/ เท่านั้น; ไม่มีหน้าจัดการ/ลบไฟล์ใน UI; sample data sync ด้วย npm run seed-samples)
  • ลายเซ็นจากรูป/upload ยังเป็น out of scope (D7 — stamp เป็น preset ข้อความเท่านั้น, ลายเซ็นใช้ tool DRAW วาด)

Claude Tooling (ใน .claude/)

  • agents/plan-reviewer — ตรวจโค้ดกับ invariants ด้านบน + verify criteria ของ task (report-only ไม่แก้โค้ด) — เรียกหลัง implement แต่ละ task
  • skills/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-db drizzle push + seed ไฟล์ตัวอย่าง
  • rules/*.md — กติกาเชิงปฏิบัติแยกตามพื้นที่ (pdf-viewer, coordinates, data-layer, realtime) โหลดอัตโนมัติเมื่อแตะไฟล์ที่ match — แก้ invariant ต้องแก้ที่ CLAUDE.md/PLAN.md ก่อน แล้วค่อย sync มา rules