eCourt-note/docs/PLAN-DOCSET.md
Your Name c5c6b65bd2 genesis+worker: design system Genesis ทั้งชุด + patch pdf.worker แก้ fake-space ไทย + ปิด review design/responsive/quality รอบ 2026-09-24 — (1) Genesis: ฟอนต์ General Sans self-host + DM Sans + Sarabun, AppNav 56px backdrop-blur แทน SessionBar, DotGrid หน้า login, palette/รัศมี/เงา/focus ตาม docs/DESIGN.md ครบทุกหน้า (2) postinstall scripts/patch-pdf-worker.mjs ถอด if (scaledDim) รอบ prevTransform ให้อัปเดตทุก glyph — กัน addFakeSpaces แทรก space กลางคำไทยใน PDF ที่ justify ด้วย letter-spacing + test กัน regression (tests/scripts/ + fixture thai_letterspacing.pdf, invariant #6) (3) ฟอร์มอัปโหลด + ช่องกลุ่ม typeId ทางเลือก — normDocumentGroup ที่ utils/external-file.ts เป็น single source ร่วมกับ N8 (4) token fixes: focus ring ปุ่ม pin + แถว bookmark ตามหน้า, ปุ่ม action ใน popup/composer/dialog เป็น rounded-md ทั้งคู่, selection preview เป็น tint ของ primary แทน blue-500 ดิบ, default สี shape/highlight รวมที่ utils/annotation-display, loading animate-pulse + จดข้อยกเว้น (เงา pin, font weights ของ viewer) ลง DESIGN.md (5) responsive: Export PDF max-lg:order-first ให้เห็นบน iPad, แถวบน header แถวเดียวเลื่อนได้บนจอแคบ, re-fit ความกว้างตอน toggle sidebar เฉพาะเมื่อยังไม่ซูมเอง, pin popover flip ขึ้นบนเมื่อใกล้ท้ายหน้า, toast hint ตอนสลับ tool กลางเอกสาร, ป้าย "ชนิด · ผู้ทำ" ของรายการผู้อื่นสำหรับจอสัมผัส (6) คุณภาพ: @layer ครอบ @media กันกฎ touch รั่ยไป desktop (Tailwind v3 ยก @layer ออกจาก media ทิ้ง wrapper — ยืนยันด้วย build จริง + วัดบนจอ), กฎ M5 ครอบ contenteditable กัน Safari zoom ตอนแก้ไข TEXTBOX บน iPad, COMMENT ใช้ box มาตราฐานเดียวตาม pin px จริง (render/scroll target/move-clamp/bookmark), ตัดหัวการ์ดซ้ำกับ h2 หน้ารายการ — พร้อมไฟล์ untracked ที่ postinstall/โค้ดอ้างถึงครบชุด (app/fonts/, AppNav, DotGrid, tests/) กัน fresh clone fail ที่ npm install (คำขอผู้ใช้ 2026-09-24)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 11:46:02 +07:00

23 KiB
Raw Blame History

แผน N9 — กลุ่มเอกสารตาม typeId จาก e-Court + หน้ารายการเอกสารในกลุ่ม

สถานะ: implement แล้ว (2026-09-23) — tsc + vitest 142/142 + Playwright ผ่านทุกข้อ verify ของ task 3–4 + plan-reviewer/code-reviewer ไม่พบ violation (คงเหลือ e2e ลิงก์ e-Court จริงตามแผน ตอนปิดงาน N8) — เขียนใหม่ 2026-09-22 แทนร่างเดิม "Document Set จัดชุดมือใน UI" (2026-09-17, commit 6940b38) ตามคำขอผู้ใช้: ให้ e-Court บอกกลุ่มผ่านลิงก์แทนการจัดชุดด้วยมือ เพิ่มเติม (2026-09-23 ตามคำขอผู้ใช้): การ์ดไฟล์หน้า /groups/[typeId] หุบ/กางดู bookmark+annotation ทั้งไฟล์ (ปิด gap 5.3.10 ครึ่งหลังที่เคยเป็น out of scope) + ย่อข้อความหมายเหตุเหลือ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" เพิ่มเติม (2026-09-23 ตามคำขอผู้ใช้): ฟอร์มอัปโหลด N4 เพิ่มช่อง "กลุ่ม typeId" ทางเลือก (ไม่กรอก = ไม่มีกลุ่ม) — uploadFile กลายเป็นจุดเขียน type_id ที่สองนอกจาก N8 (insert ตอนสร้าง row ใหม่เท่านั้น normalize เหมือน N8: trim → ว่าง = NULL, cap 200) — ยังไม่มี UI แก้กลุ่มของไฟล์ที่มีอยู่เดิม เลขฟีเจอร์: ร่างเดิม (2026-09-17) เคยใช้เลข N8 — ยกให้แผนลิงก์ e-Court (docs/PLAN-ECOURT.md) ซึ่งเป็น dependency ต้นทาง แผนนี้จึงเป็น N9 Dependency: N8 — การรับ/refresh typeId ของเอกสารจากลิงก์เกิดที่ registerExternalFile ของ N8 ทั้งหมด (เดิมแผนนี้ไม่มี code path ที่เขียน type_id เอง — ต่อมาเพิ่มช่องกลุ่มของฟอร์มอัปโหลด N4 ตามคำขอผู้ใช้ 2026-09-23) — N8 implement ครบแล้ว (2026-09-22 รวม D5): schema type_id + index มากับ migration drizzle/0002 ด้วย ตาม governance ของโปรเจกต์: implement ต้องเขียนสเปกลง docs/PLAN-FEAT.md §7 (N9) + ขยาย whitelist ใน CLAUDE.md + อัปเดต docs/PLAN.md §3 (schema) ก่อน — เป็น task แรกของแผน แล้วรันผ่าน /implement-feat N9

Context

  • กรณีจริงของศาล: คดีหนึ่งงวด = หลายเอกสาร (คำฟ้อง / คำพิพากษา / คำสั่งศาล) — ผู้ใช้อยากเห็นเอกสารที่เกี่ยวเนื่องกันจัดกลุ่มมาแสดง และมีหน้ารวมทุกเอกสารของกลุ่ม
  • ร่างเดิม (2026-09-17) ออกแบบเป็นตาราง document_sets + ย้ายไฟล์เข้า/ออกด้วยมือใน UI — ผู้ใช้ตัดสินใจใหม่ (2026-09-22): ให้ e-Court ส่งรหัสกลุ่มมากับลิงก์ (?typeId= — เปลี่ยนชื่อจาก caseId ตามคำขอ) จึงไม่ต้องมีตารางแยก / Server Actions / UI จัดการกลุ่มเลย เหลือแค่ column + การแสดงผล
  • ข้อจำกัดที่ยอมรับ (ผู้ใช้ยืนยัน 2026-09-22): ระบบเรารู้จักเฉพาะเอกสารที่เคยเปิดผ่านลิงก์ e-Court (row เกิดตอน registerExternalFile) — หน้ากลุ่มจึงแสดงเฉพาะเอกสารที่รู้จักแล้ว พร้อมหมายเหตุกำกับว่า "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" (รายการครบทั้งคดีอยู่ใน e-Court ไม่ใช่ระบบเรา — ข้อความเดิมยาวกว่า ย่อตามคำขอผู้ใช้ 2026-09-23)

การตัดสินใจ (ยืนยันกับผู้ใช้แล้ว 2026-09-22)

  1. กลุ่มระดับเดียว กำหนดด้วย typeId จากลิงก์ e-Court — เพิ่ม column บนตาราง files ค่าเดียว (type_id — ถอด type_name ตามคำขอผู้ใช้ 2026-09-22, แสดงผลด้วยรหัสกลุ่มโดยตรง) — ไม่มีตารางกลุ่มแยก ไม่มี many-to-many ไม่มีหลายระดับ (เอกสารหนึ่งอยู่กลุ่มเดียวตามธรรมชาติของงาน) — ความหมายของ id เป็นของ e-Court ฝั่งเราถือว่า "ส่งมาเหมือนกัน = กลุ่มเดียวกัน"
  2. เข้ามาอัตโนมัติ ไม่มี UI จัดการกลุ่ม — ไฟล์ได้กลุ่มเมื่อเปิดผ่านลิงก์ที่มี typeId (เขียนที่ N8) หรือกรอกช่อง "กลุ่ม typeId" ตอนอัปโหลด N4 (เพิ่ม 2026-09-23) — กลุ่มเกิด/หายตามข้อมูลจริงใน files ไม่มีสถานะกลุ่มค้างในตารางอื่น ไม่มีหน้าทางย้ายไฟล์ที่มีอยู่เดิมเข้า/ออกกลุ่ม
  3. หน้ารายการ / จัดกลุ่มตาม typeId (section ต่อกลุ่ม + ส่วน "ไม่มีกลุ่ม" ท้ายสุด) + หน้าใหม่ /groups/[typeId] แสดงเอกสารทั้งกลุ่มที่รู้จัก พร้อมหมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" และการ์ดแต่ละไฟล์หุบ/กางดู bookmark+annotation ของไฟล์นั้นได้ (2026-09-23)

Design

D1 — Schema (db/schema.ts + drizzle/)

// files เพิ่ม (เอกสารจากลิงก์ e-Court / อัปโหลด N4 ที่กรอกกลุ่ม — ไฟล์ seed และอัปโหลดที่ไม่กรอก = NULL เสมอ):
typeId: text("type_id"),
// + index ("files_type_id_idx") บน type_id
  • ไม่มี FK/ตาราง document_sets (ต่างจากร่างเดิม) — กลุ่ม = ค่า distinct ของ type_id ณ ตอน query
  • migration: /sync-db (push) + npx drizzle-kit generate — rule data-layer: Docker migrate จาก SQL ใน drizzle/ เท่านั้น; ไฟล์ generated idempotent (IF NOT EXISTS)
  • การเขียน type_id สองที่: N8 registerExternalFile (insert ครั้งแรก + update เมื่อลิงก์รอบใหม่ส่ง typeId มา — ดู PLAN-ECOURT.md D1) และ uploadFile ของ N4 ช่องกลุ่มทางเลือก (insert ตอนสร้าง row ใหม่เท่านั้น — เพิ่ม 2026-09-23) — แผนนี้ (N9) อ่านอย่างเดียว
  • ไม่มี DTO/mapper ใหม่ — หน้าที่ใช้เป็น server component ทั้งหมด ข้อมูลข้ามไป client เป็น plain object {id, title, typeId} (ถ้าต้องส่ง createdAt ข้าม boundary = ISO string ตาม invariant #5)

D2 — หน้ารายการ / จัดกลุ่ม (app/page.tsx)

  • query files เหมือนเดิม → แบ่งเป็น section ต่อกลุ่ม (type_id ไม่ NULL) เรียงตาม createdAt ของไฟล์ล่าสุดในกลุ่ม (กลุ่มที่มี activity ใหม่อยู่บน) — การ์ดไฟล์ในกลุ่มเรียง createdAt เหมือนเดิม + ปุ่ม Annotate เดิม
  • หัวกลุ่ม = type_id + จำนวนไฟล์ + ลิงก์ "เอกสารในกลุ่ม" → /groups/{encodeURIComponent(typeId)} (icon Folder)
  • ไฟล์ที่ type_id NULL (seed / อัปโหลด N4 ที่ไม่กรอกกลุ่ม / ลิงก์แบบไม่มี typeId) → section "ไม่มีกลุ่ม" ไว้ท้ายสุด (ใกล้ฟอร์มเพิ่มเอกสาร) — แสดงเสมอถ้ามีไฟล์
  • ไม่มี client component ใหม่ ไม่มี action ใหม่ — จัดกลุ่มเป็นการแสดงผลล้วนบน server component เดิม

D3 — หน้า /groups/[typeId] แสดงเอกสารในกลุ่ม (app/groups/[typeId]/page.tsx)

Server component force-dynamic (login ถูกบังคับโดย middleware.ts อยู่แล้ว — path ใหม่เข้า net อัตโนมัติ):

  • โหลด: files where type_id = params.typeId orderBy createdAt (Next decode dynamic param ให้อัตโนมัติ — อย่าเรียก decodeURIComponent ซ้ำ ตาม edge case #5) — ไม่มี = 404 (กลุ่มที่ระบบไม่รู้จัก/ย้ายไฟล์ออกหมดแล้ว)
  • แสดง: หัวกลุ่ม (type_id + "N เอกสาร") + ลิงก์กลับ /
  • หมายเหตุกำกับทุกครั้ง (ผู้ใช้ยืนยัน): "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" (กล่อง info ใต้หัวกลุ่ม — ย่อจากข้อความเดิม "… — อาจไม่ครบทุกฉบับของกลุ่ม" ตามคำขอผู้ใช้ 2026-09-23)
  • รายการเอกสาร: ชื่อไฟล์ + วันที่นำเข้า (formatThaiDate) + จำนวน annotation/bookmark + ปุ่ม/ลิงก์ Annotate → /annotate/{fileId} (ไม่ต้อง deep link หน้า — เข้าไปเหมือนปกติ)
  • การ์ดแต่ละไฟล์หุบ/กางดู bookmark+annotation ทั้งไฟล์ (คำขอผู้ใช้ 2026-09-23 — แถบ summary เป็น en: "Annotations & Bookmarks (N)"): select รายการเต็ม 2 query (inArray fileId + orderBy หน้า — เดิมนับอย่างเดียวด้วย groupBy) แล้วจัดตามไฟล์ใน JS; รวมเป็นรายการเดียวเรียงตามหน้า (หน้าเดียวกันให้ที่คั่นหน้าก่อนหมายเหตุ — ไล่ตามตำแหน่งในเอกสาร); แต่ละแถว deep link เดียวกับหน้าสรุป F12 (?ann=<id> / ?page=N) + รูปแบบแถว/ไอคอน/โน้ต/ป้าย "จัดการแล้ว"/"ใน: …" ของ sub bookmark ตามหน้าสรุป F15 + วันเวลาที่สร้าง (formatThaiDate withTime — คำขอผู้ใช้ 2026-09-23) ในบรรทัด meta ก่อน "โดย"; ใช้ <details> ล้วน ไม่มี client component; ไฟล์ที่ไม่มีรายการเลยไม่มีแถบหุบ/กาง
  • ไม่มี SSE (snapshot ณ ตอนโหลด เหมือนหน้าสรุป F15) ไม่มีปุ่มแก้ไข/ย้ายไฟล์ (จัดการที่ e-Court ฝั่งเดียว)
  • generateMetadata ตั้งชื่อแท็บ browser ตามชื่อกลุ่ม (pattern เดียวกับหน้าสรุป F15)

D4 — สิทธิ์/การเข้าถึง

  • ใครก็ที่ login เห็นได้ทุกกลุ่ม (mock login — roles ยัง out of scope ตามแผนหลัก)
  • ไม่มี Server Action ใหม่ → ไม่มี publish SSE / revalidatePath ของแผนนี้ (การเปลี่ยนกลุ่มเกิดที่ N8 ตอน register แล้ว redirect ออกจาก / ทันที — ไม่มี Router Cache ค้าง)

Docs/rules sync (plan-first — ทำเป็น task แรก ก่อนแตะโค้ด)

ไฟล์ สิ่งที่แก้
docs/PLAN.md §3 เพิ่ม column files.type_id (text NULL + index — ไม่มีตารางกลุ่ม/ไม่มีคอลัมน์ชื่อกลุ่ม) + files.external_id (ค้างจาก N8 — ทำพร้อมกันตอน sync 2026-09-23)
docs/PLAN-FEAT.md §7 หัวข้อใหม่ N9 · กลุ่มเอกสารตาม typeId + หน้า /groups/[typeId] (format ตาม N5–N7 + อ้างแผนฉบับเต็ม docs/PLAN-DOCSET.md)
CLAUDE.md whitelist ฟีเจอร์เสริมหลังแผน เพิ่ม N9 + โครงสร้างไฟล์เพิ่ม app/groups/[typeId]/page.tsx
.claude/rules/data-layer.md เพิ่ม: files.type_id เขียนที่ N8 registerExternalFile เท่านั้น (ลิงก์ไม่ส่ง = คงเดิม) — ไม่มี action/SSE ใหม่ของ N9
  • docs/requirement/gap-analysis.md 5.3.10 ครึ่งหลัง ("หรือชุดของเอกสาร") — เดิมแผนนี้ไม่ปิด (แสดงเฉพาะ เอกสาร รวมกลุ่ม) แต่ปิดแล้ว 2026-09-23 ด้วยการหุบ/กางดู bookmark+annotation ต่อไฟล์บนหน้ากลุ่ม (D3)
  • .claude/rules/realtime.md, pdf-viewer.md, coordinates.md ไม่แตะ

ลำดับ task + verify (ทุก task: npx tsc --noEmit ผ่าน)

# งาน ไฟล์หลัก Verify เพิ่มเติม
1 Docs sync N9 ตารางด้านบน สเปก §7 ครบตาม format N5–N7; PLAN.md §3 มี column ใหม่
2 Schema + migrate — ทำแล้วกับ N8 (type_id + files_type_id_idx อยู่ใน db/schema.ts + migration drizzle/0002 idempotent + dev DB ยืนยันด้วย psql 2026-09-23) db/schema.ts, drizzle/ ตรวจแล้วผ่าน: psql \d files เห็น type_id + index (ไม่มี type_name)
3 หน้า / จัดกลุ่ม app/page.tsx ทดสอบด้วยลิงก์จำลอง (การ์ด EcourtLinkForm / path /samples/… + typeId — N8 พร้อมแล้ว) หรือ update files set type_id ตรง ๆ ก็ได้: จัด section ถูก / "ไม่มีกลุ่ม" ท้ายสุด; regression: อัปโหลด N4 + Annotate ยังทำงาน
4 หน้า /groups/[typeId] app/groups/[typeId]/page.tsx รายการครบถูกลำดับ + หมายเหตุแสดง + นับ annotation/bookmark ตรง + กด Annotate เปิดไฟล์ได้; typeId ไม่รู้จัก = 404; ชื่อแท็บ browser ตามกลุ่ม
5 ปิดงาน — plan-reviewer + code-reviewer + vitest + tsc; e2e เต็ม (ลิงก์ e-Court จริง) ทำตอนปิดงาน N8

Edge cases (ออกแบบไว้ล่วงหน้า)

  1. ลิงก์รอบใหม่ส่ง typeId คนละค่า → ย้ายกลุ่มตามค่าล่าสุด (เกิดที่ N8) — annotation/bookmark ผูกกับ file_id ไม่เกี่ยวกับกลุ่ม จึงตามไฟล์ไปเอง
  2. ลิงก์รอบใหม่ไม่ส่ง typeId → ไฟล์คงกลุ่มเดิม (N8: ห้ามเคลียร์ค่าเดิม)
  3. ไฟล์ไม่เคยมี typeId (seed / อัปโหลด N4 ที่ไม่กรอกกลุ่ม / ลิงก์เก่า) → section "ไม่มีกลุ่ม" บนหน้า /, ไม่มีหน้ากลุ่ม
  4. กลุ่มแสดงผลด้วยรหัสล้วน — ไม่มีคอลัมน์ชื่อกลุ่ม (ถอด type_name ตามคำขอผู้ใช้ 2026-09-22) — ถ้าภายหลังต้องการชื่อกลุ่ม กลับมาเพิ่ม column ใหม่ตาม governance
  5. typeId มีอักขระพิเศษ/ยาว → ผ่าน URL ด้วย encodeURIComponent; normalize ที่ N8 (trim → ว่าง = NULL, cap 200); Next decode ให้อัตโนมัติตอนอ่าน params
  6. กลุ่มว่างลง (ไฟล์ย้ายออกหมด) → กลุ่มหายจากหน้า / เอง และ /groups/[typeId] กลายเป็น 404 (query จาก files จริง ไม่มีตารางกลุ่มค้าง)
  7. Dev DB แชร์กับการทดสอบของผู้ใช้ → เทสหน้า UI ด้วยการตั้ง type_id บน id ที่จับเป๊ะ ไม่ reset ข้อมูลทั้งกลุ่ม
  8. N8 ยังไม่ implement ตอนทำ N9 → verify หน้า UI ด้วยการเขียน DB ตรง (task 3–4) แล้วรอ e2e จริงตอนปิดงาน N8 (ล้าสมัย — N8 implement ครบแล้ว 2026-09-22: verify ได้ด้วยลิงก์จำลอง/การ์ด EcourtLinkForm + path /samples/… ทันที)
  9. typeId มี / (เช่น เลขคดีไทย ด2024/2567) → ลิงก์ /groups/…%2F… — Next 14 route ได้ (match บน raw path ก่อน decode) แต่ reverse proxy บางตัว (Tomcat/JBoss ปฏิเสธ %2F โดย default) อาจ 404 ทั้งที่กลุ่มมีจริง — known risk ที่รับไว้ (code-reviewer 2026-09-23): ทดสอบ e2e กับ deployment จริงของ e-Court ตอนปิดงาน N8 ถ้าเจอปัญหาแก้ที่จุดสร้างลิงก์จุดเดียว (replace / ด้วยตัวอักขระปลอดภัยตอน encode) หรือเปลี่ยนเป็น query param

Out of scope (ไม่ทำในแผนนี้)

  • จัดกลุ่ม/แก้ชื่อ/ลบกลุ่มด้วยมือใน UI ของเรา (ทิ้งจากร่างเดิม — กลุ่มมาจาก e-Court อย่างเดียว)
  • ไฟล์อยู่หลายกลุ่ม (many-to-many) / กลุ่มซ้อนหลายระดับ
  • bookmark รวมทั้งกลุ่ม — ทำแล้ว (2026-09-23): การ์ดไฟล์หน้ากลุ่มหุบ/กางดู bookmark+annotation ต่อไฟล์ (D3)
  • export JSON ตามกลุ่ม (N7 ยังรับ fileId เดียว)
  • API ดึงข้อมูลให้ระบบภายนอก = N10 docs/PLAN-API.md (แผนแยก)
  • รายการเอกสารครบทุกฉบับของคดีรวมฉบับที่ยังไม่เคยเปิด (ข้อมูลอยู่ที่ e-Court)

Verification (ภาพรวม end-to-end — หลัง N8 เสร็จ)

  1. npm run dev → login → เปิดลิงก์ e-Court 2 ไฟล์ที่ typeId เดียวกัน + 1 ไฟล์ typeId ต่างกลุ่ม → หน้า / จัด 2 section ตามกลุ่ม + section "ไม่มีกลุ่ม" (ไฟล์ seed/อัปโหลดที่ไม่กรอกกลุ่ม); อัปโหลดไฟล์พร้อมกรอกกลุ่ม → การ์ดไปอยู่ section ของกลุ่มนั้น + หน้า /groups/[typeId] เห็นไฟล์อัปโหลดด้วย
  2. หัวกลุ่ม → "เอกสารในกลุ่ม" → /groups/[typeId]: เห็นไฟล์ครบตามลำดับ + หมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" + จำนวน annotation/bookmark ต่อไฟล์
  3. กาง "Annotations & Bookmarks (N)" ในการ์ดไฟล์ → เห็น bookmark+annotation ทั้งไฟล์เรียงตามหน้า + กดแถวแต่ละรายการเด้งไปตำแหน่งนั้นใน viewer (?ann= / ?page=)
  4. กด Annotate → เข้า viewer ของไฟล์นั้นได้ตามปกติ
  5. เปิดลิงก์รอบใหม่ของไฟล์เดิมด้วย typeId คนละค่า → ไฟล์ย้ายกลุ่ม กลุ่มเดิมลดจำนวน/หายไปเมื่อว่าง
  6. Regression: อัปโหลด N4 / annotate / ค้นหา N6 / export JSON N7 ยังทำงานเหมือนเดิม