eCourt-note/CLAUDE.md
Your Name fa880b314e
All checks were successful
Deploy eCourt Note Web to UAT Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to UAT Server / Build Web Docker Image (push) Successful in 48s
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to UAT Server / Deploy eCourt Note Web to UAT Server (push) Successful in 2s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 48s
Deploy eCourt Note Web to UAT Server / Notify Deployment Status (push) Successful in 1s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 13s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
BASE_PATH: serve ใต้ /docedit/note — next.config.mjs ตั้ง basePath + env NEXT_PUBLIC_BASE_PATH (ประกาศค่าเดียวที่ไฟล์นี้) ให้ utils/base-path.ts (ใหม่) withBasePath() เติม prefix ให้ URL ฝั่ง browser ที่ Next ไม่เติมให้เอง (fetch/EventSource/plain href/workerSrc/img src): PDFViewer (workerSrc, /api/pdf ซ่อม ToUnicode, fallback file.pdfUrl, image branch N3 รวบเป็นตัวแปร fileUrl, export PDF F8), SSE stream ใน useAnnotationRealtime, ปุ่มส่งออก bookmark JSON (N7) + คัดลอกลิงก์ F12 ใน AnnotationSidebar (window.location.origin + basePath กัน paste แล้ว 404), Export CSV (F16) หน้า summary — <Link>/next/navigation/middleware.ts ไม่ต้องแก้ (Next 14.2 auto-prefix ให้ + matcher จับ path ที่ตัด basePath แล้ว + NextURL redirect ใส่กลับเอง ยืนยันจาก source ใน node_modules) + pdf_url ใน DB เก็บ path ไม่รวม basePath เติมตอนใช้เท่านั้น + sync เอกสารตาม governance: CLAUDE.md invariant #6 (workerSrc ผ่าน withBasePath) + #12 ใหม่ (URL ฝั่ง browser ที่สร้างเองต้องผ่าน withBasePath), PLAN.md A3/§4, rules pdf-viewer/realtime, plan-reviewer #8, README URL localhost:3000/docedit/note — verify: tsc strict ผ่าน + curl (root / 404, ไม่มี cookie 307→/docedit/note/login, worker 200, SSE reachable, มี cookie 200) + Playwright login จริงเปิดไฟล์ 44 หน้า render ครบ (SSE /api/pdf /worker 200 ผ่าน basePath) + code-reviewer ผ่าน (vitest 126 tests, ไม่พบ bug, blob URL ไม่โดน prefix ซ้ำ, env inline ครบทั้ง client/server ตาม define-env-plugin) — deploy: basePath เป็น build-time ต้อง build image ใหม่ถึงจะเสิร์ฟที่ /docedit/note + reverse proxy ต้อง forward path เต็มแบบไม่ strip prefix
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 16:06:20 +07:00

91 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (sidebar/สรุป/CSV ใช้ร่วม)
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 ← หน้าสรุปหมายเหตุจัดกลุ่มตามหน้า (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)
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