eCourt-note/README.md

126 lines
9.9 KiB
Markdown
Raw Normal View History

# Judge's Note — PDF Annotation สำหรับ e-Court
2026-09-10 14:21:21 +07:00
ระบบ 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 หน้า ไฟล์หลายสิบหน้าเลื่อนลื่น
mock login e-Court หน้า /login + identity จาก cookie oacCase แทน localStorage — dropdown ผู้ใช้ MOCK_USERS (user1–user5, utils/user.ts pure module ให้ client+server ใช้ร่วม) รหัสผ่านกลาง Demo1234 ผ่าน server action login (เช็ครายชื่อฝั่ง server เป็น defense กัน hand-craft + delay 500ms ให้เห็น "กำลังตรวจสอบ…") ตั้ง cookie oacCase ตาม format จริงของระบบศาล 16 field — u_userid/u_userlogin ต่างกันต่อคน (= ชื่อผู้ใช้) เพื่อให้สิทธิ์ "แก้ไขได้เฉพาะของตัวเอง" (guard ด้วย userId) แยกกันจริงทั้ง 5 คน httpOnly/sameSite lax/8 ชม. + middleware.ts บังคับ login ทุกหน้า (parse จริงด้วย parseOacCase ไม่ใช่แค่มี cookie — ขยะ/ว่าง/ขาด field = redirect /login; matcher ยกเว้น /login, /api/*, /uploads/*, _next และ static ใน public/ — SSE/export จึงไม่โดน 302) + lib/session.ts getSessionUser อ่าน identity ฝั่ง server ใช้ในทุกหน้า (defense ซ้อน middleware) + PDFViewer รับ user เป็น prop จาก annotate page ตั้งแต่ first paint (หมดช่วง null user ที่เคยสร้าง annotation ด้วย userId="" ได้ + ไม่มี hydration mismatch; Server Actions ไม่ต้องแก้ — vString รับ userId ใหม่อยู่แล้ว) + SessionBar แสดง "เข้าสู่ระบบเป็น <user>" + ปุ่มออกจากระบบ (ลบ cookie ด้วย path "/" ตรงตอน set + redirect ใน action เอง — client router.replace เองชนกับ re-render ของหน้าที่เพิ่งเสีย session จน navigation ไม่เกิด) แทน UserPicker mock 10 คน (ลบไฟล์พร้อม MOCK_USERS/localStorage/getLocalUser ทั้งชุด) + LoginForm การ์ด max-w-sm กลางจอ reuse brand lockup และปุ่ม primary เดิม, input text-base 16px กัน iOS zoom บน iPad, dropdown placeholder "เลือกผู้ใช้" บังคับเลือก (ไม่ preselect กัน login ผิดคนโดยไม่ตั้งใจ), error แถบเดียว role=alert "รหัสผ่านไม่ถูกต้อง" (dropdown เลือกผิดไม่ได้แล้ว), ตอนผิดล้าง password คงผู้ใช้ที่เลือกไว้ + focus+select + tests/utils/user.test.ts ล็อก format จริงของ e-Court (parse ตัวอย่าง cookie จริง 235821/tu_2, garbage/empty/ขาด field → null, round-trip, buildOacCaseCookie ครบ 16 field) + sync CLAUDE.md/PLAN.md §0 A4/README — annotation สมัย mock-user-01/UUID ไม่ใช่ของตัวเองอีก (auth จริงเทียบระบบศาล + roles ยัง out of scope; /api/* และ /uploads/* ยังไม่ต้อง login ตามขอบเขต mock) Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 13:41:02 +07:00
- หน้า login (mock e-Court — dropdown `user1`–`user5` รหัสผ่านกลาง `Demo1234`) — identity จาก cookie `oacCase` บังคับทุกหน้า (ปุ่มลบ/แก้ไขแสดงเฉพาะรายการของตัวเอง)
2026-09-10 14:21:21 +07:00
## สิ่งที่ต้องมี (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 ไฟล์ตัวอย่าง
popup ซ้อนกัน/ไม่ปิดตอนกดปุ่ม tool + แท็บที่คั่นหน้ากลับไม่ได้ + งานค้างใน working tree: ไฟล์รูปภาพ annotate ได้ (N3) + อัปโหลดไฟล์ (N4): ส่วนที่ 1 — แก้ popup ตาม bug report ("กดแก้ไขแล้วไปกดที่ tool popup ของแก้ไขไม่ปิด/แสดงซ้อนกัน"): (1) ปุ่ม tool แบบ dropdown ทั้ง 5 (highlight/draw/shape/stamp/textbox) เรียก switchTool เสมอแม้เป็น tool เดิม — เดิมกดปุ่ม tool ที่ active อยู่จะ toggle เมนูอย่างเดียวไม่เรียก switchTool (ซึ่งเป็นที่เดียวที่เคลียร์ popup บนหน้า: editor/แผง selection/popover) ผลคือ popup แก้ไขค้างเปิด + dropdown เปิดซ้อนขึ้นมาอีกชั้น — ตอนนี้จำ intent เปิด/พับเมนูไว้ก่อนเรียก (switchTool ปิดเมนูทุกตัว) แล้วเปิดคืนเฉพาะที่ตั้งใจ; toggle พับเมนูและกลไกกลืนคลิก M4 เท่าเดิม (mousedown บนปุ่ม mark อยู่แล้ว) (2) switchTool ปิด popup bookmark ด้วย + openBookmarkComposer ปิดเมนูทั้ง 5 ก่อนเปิด popup (เดิมคีย์ลัด B ระหว่างเมนูเปิด = สอง popup ซ้อนกัน) (3) คลิกหน้าว่าง = ปิด popover ของ comment pin + เลือกรายการอื่น/goToAnnotation ปิด popover เดิมทิ้ง (เดิมปิด popover ได้แค่กด pin เดิม/Escape/สลับ tool) (4) COMMENT ที่เปิด popover อยู่ไม่ render กรอบ selection/แผงเครื่องมือ — bbox ของ COMMENT ครอบพื้นที่ pin พอดี แผงจะทับ popover และแถบ handle บังการคลิก (sidebar ยังตาม selectedId ไฮไลต์เหมือนเดิม) (5) แก้แท็บ sidebar ตาม bug report ("เลือก Annotation แล้วจะกลับไป tabs bookmarks ไม่ได้"): effect เดิม dep รวม tab และบังคับ setTab ไปแท็บ annotations ทุกครั้งที่รันตราบ selectedId ไม่ null ทำให้กดแท็บที่คั่นหน้าแล้วถูกดึงกลับทันที — ใช้ ref จำ id ที่ประกาศแล้ว บังคับสลับเฉพาะตอน selectedId เพิ่งเปลี่ยน เคลียร์เป็น null ตอน deselect ให้เลือกรายการเดิมซ้ำภายหลังยังสลับแท็บให้ ส่วน scroll แถวตอนเปิดแท็บหมายเหตุคงเดิม - sync rules/pdf-viewer.md: กติกาใหม่ popup บนหน้าเปิดได้ทีละชุดห้ามค้างเปิดซ้อนกัน (ปุ่ม dropdown เรียก switchTool เสมอ + วงจรปิด popover/แผง) และยกเว้น markMenuClosed ของคีย์ B ในกติกา M4 - verify: tsc ผ่าน, vitest 120/120, Playwright 18 เคสผ่าน (repro ตรงตามรายงาน: สร้าง textbox → แก้ไขข้อความ → กดปุ่ม tool เดิม → editor+แผงปิด+เมนูใช้ได้; popover pin ไม่ซ้อนแผง/ปิดด้วยคลิกหน้าว่าง; popup bookmark ปิดตอนสลับ tool; แท็บที่คั่นหน้ากลับได้) ส่วนที่ 2 — งานที่ค้างใน working tree ก่อนหน้า (ตามแผน docs/PLAN-IMAGE.md): - N3 รองรับไฟล์รูปภาพ jpg/jpeg/png/tif เป็นเอกสาร annotate ได้ครบทุกชนิด (viewer render img แทน canvas ไม่มี textLayer, scale contract เดียวกับ export) + TIFF หลายเฟรม = หลายหน้า (decode ผ่าน utif2 dynamic import → PNG blob URL ต่อเฟรม ฝั่ง client) + export PDF ฝังรูป (utils/image-to-pdf.ts: embedJpg/embedPng + EXIF rotate) + utils/file-kind.ts derive ชนิดไฟล์ + tests ของทั้งสอง utils - N4 อัปโหลดไฟล์ PDF/รูป ≤100MB ผ่านหน้ารายการ (components/UploadFileForm.tsx + app/actions/files.ts: validate + เขียนไฟล์ใหม่ที่ public/uploads/ เท่านั้น — invariant #1) - เอกสาร: docs/PLAN-IMAGE.md เพิ่มใหม่ + sync PLAN.md/PLAN-FEAT.md/CLAUDE.md/README/gap-analysis/data-layer rule Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 14:12:51 +07:00
ไฟล์ตัวอย่างอยู่ที่ `public/samples/` แล้ว — sync ลงตาราง `files` (idempotent รันซ้ำได้) — รองรับทั้ง `.pdf` และรูปภาพ `.jpg/.jpeg/.png/.tif/.tiff` (N3):
2026-09-10 14:21:21 +07:00
```bash
npm run seed-samples
2026-09-10 14:21:21 +07:00
```
> บน production ไม่ต้องรันมือ — script เดียวกัน (`scripts/seed-samples.mjs`) รันอัตโนมัติตอน container start ต่อจาก migration ไฟล์ที่หายจาก `public/samples/` จะถูกลบ row พร้อม annotation ที่ผูกอยู่ (CASCADE)
2026-09-10 14:21:21 +07:00
### 6. รัน dev server
```bash
npm run dev
```
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
เปิด http://localhost:3000/docedit/note — คลิกไฟล์เพื่อเข้าหน้า annotate หรืออัปโหลดไฟล์ใหม่ (PDF/รูปภาพ ≤ 100MB) ผ่านฟอร์มบนหน้ารายการ (N4 — ไฟล์เก็บที่ `public/uploads/`)
2026-09-10 14:21:21 +07:00
## การใช้งานบนหน้า annotate
| ปุ่ม | พฤติกรรม |
|---|---|
| ลูกศร (select) | คลุมดำข้อความ → เด้ง toolbar เลือกสี → ติด highlight บนหน้า |
| ดินสอ (draw) | ลากเมาส์วาดเส้นอิสระ (ปล่อย = บันทึก) |
| ลูกศร+กล่องแชท (comment) | คลิกจุดบนหน้า → พิมพ์ข้อความ → บันทึก pin |
| bookmark+ | เพิ่ม bookmark ที่ตำแหน่งกึ่งกลางจอปัจจุบัน |
| zoom − / + | สเกล 50%–300% (annotation ทั้งหมด scale ตามเพราะเก็บเป็น %) |
mock login e-Court หน้า /login + identity จาก cookie oacCase แทน localStorage — dropdown ผู้ใช้ MOCK_USERS (user1–user5, utils/user.ts pure module ให้ client+server ใช้ร่วม) รหัสผ่านกลาง Demo1234 ผ่าน server action login (เช็ครายชื่อฝั่ง server เป็น defense กัน hand-craft + delay 500ms ให้เห็น "กำลังตรวจสอบ…") ตั้ง cookie oacCase ตาม format จริงของระบบศาล 16 field — u_userid/u_userlogin ต่างกันต่อคน (= ชื่อผู้ใช้) เพื่อให้สิทธิ์ "แก้ไขได้เฉพาะของตัวเอง" (guard ด้วย userId) แยกกันจริงทั้ง 5 คน httpOnly/sameSite lax/8 ชม. + middleware.ts บังคับ login ทุกหน้า (parse จริงด้วย parseOacCase ไม่ใช่แค่มี cookie — ขยะ/ว่าง/ขาด field = redirect /login; matcher ยกเว้น /login, /api/*, /uploads/*, _next และ static ใน public/ — SSE/export จึงไม่โดน 302) + lib/session.ts getSessionUser อ่าน identity ฝั่ง server ใช้ในทุกหน้า (defense ซ้อน middleware) + PDFViewer รับ user เป็น prop จาก annotate page ตั้งแต่ first paint (หมดช่วง null user ที่เคยสร้าง annotation ด้วย userId="" ได้ + ไม่มี hydration mismatch; Server Actions ไม่ต้องแก้ — vString รับ userId ใหม่อยู่แล้ว) + SessionBar แสดง "เข้าสู่ระบบเป็น <user>" + ปุ่มออกจากระบบ (ลบ cookie ด้วย path "/" ตรงตอน set + redirect ใน action เอง — client router.replace เองชนกับ re-render ของหน้าที่เพิ่งเสีย session จน navigation ไม่เกิด) แทน UserPicker mock 10 คน (ลบไฟล์พร้อม MOCK_USERS/localStorage/getLocalUser ทั้งชุด) + LoginForm การ์ด max-w-sm กลางจอ reuse brand lockup และปุ่ม primary เดิม, input text-base 16px กัน iOS zoom บน iPad, dropdown placeholder "เลือกผู้ใช้" บังคับเลือก (ไม่ preselect กัน login ผิดคนโดยไม่ตั้งใจ), error แถบเดียว role=alert "รหัสผ่านไม่ถูกต้อง" (dropdown เลือกผิดไม่ได้แล้ว), ตอนผิดล้าง password คงผู้ใช้ที่เลือกไว้ + focus+select + tests/utils/user.test.ts ล็อก format จริงของ e-Court (parse ตัวอย่าง cookie จริง 235821/tu_2, garbage/empty/ขาด field → null, round-trip, buildOacCaseCookie ครบ 16 field) + sync CLAUDE.md/PLAN.md §0 A4/README — annotation สมัย mock-user-01/UUID ไม่ใช่ของตัวเองอีก (auth จริงเทียบระบบศาล + roles ยัง out of scope; /api/* และ /uploads/* ยังไม่ต้อง login ตามขอบเขต mock) Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 13:41:02 +07:00
Sidebar ซ้าย: แท็บ Bookmarks / Annotations — คลิกรายการเพื่อเลื่อนกลับไปตำแหน่งนั้น และมีปุ่มลบเฉพาะรายการของตัวเอง (identity จากหน้า login)
2026-09-10 14:21:21 +07:00
ทดสอบ 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
mock login e-Court หน้า /login + identity จาก cookie oacCase แทน localStorage — dropdown ผู้ใช้ MOCK_USERS (user1–user5, utils/user.ts pure module ให้ client+server ใช้ร่วม) รหัสผ่านกลาง Demo1234 ผ่าน server action login (เช็ครายชื่อฝั่ง server เป็น defense กัน hand-craft + delay 500ms ให้เห็น "กำลังตรวจสอบ…") ตั้ง cookie oacCase ตาม format จริงของระบบศาล 16 field — u_userid/u_userlogin ต่างกันต่อคน (= ชื่อผู้ใช้) เพื่อให้สิทธิ์ "แก้ไขได้เฉพาะของตัวเอง" (guard ด้วย userId) แยกกันจริงทั้ง 5 คน httpOnly/sameSite lax/8 ชม. + middleware.ts บังคับ login ทุกหน้า (parse จริงด้วย parseOacCase ไม่ใช่แค่มี cookie — ขยะ/ว่าง/ขาด field = redirect /login; matcher ยกเว้น /login, /api/*, /uploads/*, _next และ static ใน public/ — SSE/export จึงไม่โดน 302) + lib/session.ts getSessionUser อ่าน identity ฝั่ง server ใช้ในทุกหน้า (defense ซ้อน middleware) + PDFViewer รับ user เป็น prop จาก annotate page ตั้งแต่ first paint (หมดช่วง null user ที่เคยสร้าง annotation ด้วย userId="" ได้ + ไม่มี hydration mismatch; Server Actions ไม่ต้องแก้ — vString รับ userId ใหม่อยู่แล้ว) + SessionBar แสดง "เข้าสู่ระบบเป็น <user>" + ปุ่มออกจากระบบ (ลบ cookie ด้วย path "/" ตรงตอน set + redirect ใน action เอง — client router.replace เองชนกับ re-render ของหน้าที่เพิ่งเสีย session จน navigation ไม่เกิด) แทน UserPicker mock 10 คน (ลบไฟล์พร้อม MOCK_USERS/localStorage/getLocalUser ทั้งชุด) + LoginForm การ์ด max-w-sm กลางจอ reuse brand lockup และปุ่ม primary เดิม, input text-base 16px กัน iOS zoom บน iPad, dropdown placeholder "เลือกผู้ใช้" บังคับเลือก (ไม่ preselect กัน login ผิดคนโดยไม่ตั้งใจ), error แถบเดียว role=alert "รหัสผ่านไม่ถูกต้อง" (dropdown เลือกผิดไม่ได้แล้ว), ตอนผิดล้าง password คงผู้ใช้ที่เลือกไว้ + focus+select + tests/utils/user.test.ts ล็อก format จริงของ e-Court (parse ตัวอย่าง cookie จริง 235821/tu_2, garbage/empty/ขาด field → null, round-trip, buildOacCaseCookie ครบ 16 field) + sync CLAUDE.md/PLAN.md §0 A4/README — annotation สมัย mock-user-01/UUID ไม่ใช่ของตัวเองอีก (auth จริงเทียบระบบศาล + roles ยัง out of scope; /api/* และ /uploads/* ยังไม่ต้อง login ตามขอบเขต mock) Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 13:41:02 +07:00
utils/user.ts parse/build cookie oacCase (mock login e-Court)
2026-09-10 14:21:21 +07:00
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=)
popup ซ้อนกัน/ไม่ปิดตอนกดปุ่ม tool + แท็บที่คั่นหน้ากลับไม่ได้ + งานค้างใน working tree: ไฟล์รูปภาพ annotate ได้ (N3) + อัปโหลดไฟล์ (N4): ส่วนที่ 1 — แก้ popup ตาม bug report ("กดแก้ไขแล้วไปกดที่ tool popup ของแก้ไขไม่ปิด/แสดงซ้อนกัน"): (1) ปุ่ม tool แบบ dropdown ทั้ง 5 (highlight/draw/shape/stamp/textbox) เรียก switchTool เสมอแม้เป็น tool เดิม — เดิมกดปุ่ม tool ที่ active อยู่จะ toggle เมนูอย่างเดียวไม่เรียก switchTool (ซึ่งเป็นที่เดียวที่เคลียร์ popup บนหน้า: editor/แผง selection/popover) ผลคือ popup แก้ไขค้างเปิด + dropdown เปิดซ้อนขึ้นมาอีกชั้น — ตอนนี้จำ intent เปิด/พับเมนูไว้ก่อนเรียก (switchTool ปิดเมนูทุกตัว) แล้วเปิดคืนเฉพาะที่ตั้งใจ; toggle พับเมนูและกลไกกลืนคลิก M4 เท่าเดิม (mousedown บนปุ่ม mark อยู่แล้ว) (2) switchTool ปิด popup bookmark ด้วย + openBookmarkComposer ปิดเมนูทั้ง 5 ก่อนเปิด popup (เดิมคีย์ลัด B ระหว่างเมนูเปิด = สอง popup ซ้อนกัน) (3) คลิกหน้าว่าง = ปิด popover ของ comment pin + เลือกรายการอื่น/goToAnnotation ปิด popover เดิมทิ้ง (เดิมปิด popover ได้แค่กด pin เดิม/Escape/สลับ tool) (4) COMMENT ที่เปิด popover อยู่ไม่ render กรอบ selection/แผงเครื่องมือ — bbox ของ COMMENT ครอบพื้นที่ pin พอดี แผงจะทับ popover และแถบ handle บังการคลิก (sidebar ยังตาม selectedId ไฮไลต์เหมือนเดิม) (5) แก้แท็บ sidebar ตาม bug report ("เลือก Annotation แล้วจะกลับไป tabs bookmarks ไม่ได้"): effect เดิม dep รวม tab และบังคับ setTab ไปแท็บ annotations ทุกครั้งที่รันตราบ selectedId ไม่ null ทำให้กดแท็บที่คั่นหน้าแล้วถูกดึงกลับทันที — ใช้ ref จำ id ที่ประกาศแล้ว บังคับสลับเฉพาะตอน selectedId เพิ่งเปลี่ยน เคลียร์เป็น null ตอน deselect ให้เลือกรายการเดิมซ้ำภายหลังยังสลับแท็บให้ ส่วน scroll แถวตอนเปิดแท็บหมายเหตุคงเดิม - sync rules/pdf-viewer.md: กติกาใหม่ popup บนหน้าเปิดได้ทีละชุดห้ามค้างเปิดซ้อนกัน (ปุ่ม dropdown เรียก switchTool เสมอ + วงจรปิด popover/แผง) และยกเว้น markMenuClosed ของคีย์ B ในกติกา M4 - verify: tsc ผ่าน, vitest 120/120, Playwright 18 เคสผ่าน (repro ตรงตามรายงาน: สร้าง textbox → แก้ไขข้อความ → กดปุ่ม tool เดิม → editor+แผงปิด+เมนูใช้ได้; popover pin ไม่ซ้อนแผง/ปิดด้วยคลิกหน้าว่าง; popup bookmark ปิดตอนสลับ tool; แท็บที่คั่นหน้ากลับได้) ส่วนที่ 2 — งานที่ค้างใน working tree ก่อนหน้า (ตามแผน docs/PLAN-IMAGE.md): - N3 รองรับไฟล์รูปภาพ jpg/jpeg/png/tif เป็นเอกสาร annotate ได้ครบทุกชนิด (viewer render img แทน canvas ไม่มี textLayer, scale contract เดียวกับ export) + TIFF หลายเฟรม = หลายหน้า (decode ผ่าน utif2 dynamic import → PNG blob URL ต่อเฟรม ฝั่ง client) + export PDF ฝังรูป (utils/image-to-pdf.ts: embedJpg/embedPng + EXIF rotate) + utils/file-kind.ts derive ชนิดไฟล์ + tests ของทั้งสอง utils - N4 อัปโหลดไฟล์ PDF/รูป ≤100MB ผ่านหน้ารายการ (components/UploadFileForm.tsx + app/actions/files.ts: validate + เขียนไฟล์ใหม่ที่ public/uploads/ เท่านั้น — invariant #1) - เอกสาร: docs/PLAN-IMAGE.md เพิ่มใหม่ + sync PLAN.md/PLAN-FEAT.md/CLAUDE.md/README/gap-analysis/data-layer rule Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 14:12:51 +07:00
app/page.tsx รายการไฟล์ + ฟอร์มอัปโหลด (N4)
2026-09-10 14:21:21 +07:00
app/annotate/[id]/page.tsx โหลด file + initial data → PDFViewer
components/pdf/ PDFViewer (root + virtualization), InkLayer, HighlightToolbar
components/sidebar/ AnnotationSidebar
popup ซ้อนกัน/ไม่ปิดตอนกดปุ่ม tool + แท็บที่คั่นหน้ากลับไม่ได้ + งานค้างใน working tree: ไฟล์รูปภาพ annotate ได้ (N3) + อัปโหลดไฟล์ (N4): ส่วนที่ 1 — แก้ popup ตาม bug report ("กดแก้ไขแล้วไปกดที่ tool popup ของแก้ไขไม่ปิด/แสดงซ้อนกัน"): (1) ปุ่ม tool แบบ dropdown ทั้ง 5 (highlight/draw/shape/stamp/textbox) เรียก switchTool เสมอแม้เป็น tool เดิม — เดิมกดปุ่ม tool ที่ active อยู่จะ toggle เมนูอย่างเดียวไม่เรียก switchTool (ซึ่งเป็นที่เดียวที่เคลียร์ popup บนหน้า: editor/แผง selection/popover) ผลคือ popup แก้ไขค้างเปิด + dropdown เปิดซ้อนขึ้นมาอีกชั้น — ตอนนี้จำ intent เปิด/พับเมนูไว้ก่อนเรียก (switchTool ปิดเมนูทุกตัว) แล้วเปิดคืนเฉพาะที่ตั้งใจ; toggle พับเมนูและกลไกกลืนคลิก M4 เท่าเดิม (mousedown บนปุ่ม mark อยู่แล้ว) (2) switchTool ปิด popup bookmark ด้วย + openBookmarkComposer ปิดเมนูทั้ง 5 ก่อนเปิด popup (เดิมคีย์ลัด B ระหว่างเมนูเปิด = สอง popup ซ้อนกัน) (3) คลิกหน้าว่าง = ปิด popover ของ comment pin + เลือกรายการอื่น/goToAnnotation ปิด popover เดิมทิ้ง (เดิมปิด popover ได้แค่กด pin เดิม/Escape/สลับ tool) (4) COMMENT ที่เปิด popover อยู่ไม่ render กรอบ selection/แผงเครื่องมือ — bbox ของ COMMENT ครอบพื้นที่ pin พอดี แผงจะทับ popover และแถบ handle บังการคลิก (sidebar ยังตาม selectedId ไฮไลต์เหมือนเดิม) (5) แก้แท็บ sidebar ตาม bug report ("เลือก Annotation แล้วจะกลับไป tabs bookmarks ไม่ได้"): effect เดิม dep รวม tab และบังคับ setTab ไปแท็บ annotations ทุกครั้งที่รันตราบ selectedId ไม่ null ทำให้กดแท็บที่คั่นหน้าแล้วถูกดึงกลับทันที — ใช้ ref จำ id ที่ประกาศแล้ว บังคับสลับเฉพาะตอน selectedId เพิ่งเปลี่ยน เคลียร์เป็น null ตอน deselect ให้เลือกรายการเดิมซ้ำภายหลังยังสลับแท็บให้ ส่วน scroll แถวตอนเปิดแท็บหมายเหตุคงเดิม - sync rules/pdf-viewer.md: กติกาใหม่ popup บนหน้าเปิดได้ทีละชุดห้ามค้างเปิดซ้อนกัน (ปุ่ม dropdown เรียก switchTool เสมอ + วงจรปิด popover/แผง) และยกเว้น markMenuClosed ของคีย์ B ในกติกา M4 - verify: tsc ผ่าน, vitest 120/120, Playwright 18 เคสผ่าน (repro ตรงตามรายงาน: สร้าง textbox → แก้ไขข้อความ → กดปุ่ม tool เดิม → editor+แผงปิด+เมนูใช้ได้; popover pin ไม่ซ้อนแผง/ปิดด้วยคลิกหน้าว่าง; popup bookmark ปิดตอนสลับ tool; แท็บที่คั่นหน้ากลับได้) ส่วนที่ 2 — งานที่ค้างใน working tree ก่อนหน้า (ตามแผน docs/PLAN-IMAGE.md): - N3 รองรับไฟล์รูปภาพ jpg/jpeg/png/tif เป็นเอกสาร annotate ได้ครบทุกชนิด (viewer render img แทน canvas ไม่มี textLayer, scale contract เดียวกับ export) + TIFF หลายเฟรม = หลายหน้า (decode ผ่าน utif2 dynamic import → PNG blob URL ต่อเฟรม ฝั่ง client) + export PDF ฝังรูป (utils/image-to-pdf.ts: embedJpg/embedPng + EXIF rotate) + utils/file-kind.ts derive ชนิดไฟล์ + tests ของทั้งสอง utils - N4 อัปโหลดไฟล์ PDF/รูป ≤100MB ผ่านหน้ารายการ (components/UploadFileForm.tsx + app/actions/files.ts: validate + เขียนไฟล์ใหม่ที่ public/uploads/ เท่านั้น — invariant #1) - เอกสาร: docs/PLAN-IMAGE.md เพิ่มใหม่ + sync PLAN.md/PLAN-FEAT.md/CLAUDE.md/README/gap-analysis/data-layer rule Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 14:12:51 +07:00
public/samples/ ไฟล์ตัวอย่าง (PDF + รูปภาพ) — seed ด้วย npm run seed-samples
public/uploads/ ไฟล์ที่อัปโหลดผ่านหน้ารายการ (N4)
2026-09-10 14:21:21 +07:00
```