eCourt-note/docs/PLAN-DOCSET.md

19 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-17) เคยใช้เลข N8 — ยกให้แผนลิงก์ e-Court (docs/PLAN-ECOURT.md) ซึ่งเป็น dependency ต้นทาง แผนนี้จึงเป็น N9 Dependency: N8 — การรับ/refresh typeId เกิดที่ registerExternalFile ของ N8 ทั้งหมด (แผนนี้ไม่มี code path ที่เขียน type_id เอง) — 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-22)

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

Design

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

// files เพิ่ม (เก็บเฉพาะเอกสารนำเข้าจากลิงก์ — ไฟล์ 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) — แผนนี้อ่านอย่างเดียว
  • ไม่มี 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 ใต้หัวกลุ่ม)
  • รายการเอกสาร: ชื่อไฟล์ + วันที่นำเข้า (formatThaiDate) + จำนวน annotation/bookmark (query นับแยกด้วย groupBy fileId — 2 query เสริม ไม่ join หนัก) + ปุ่ม/ลิงก์ Annotate → /annotate/{fileId} (ไม่ต้อง deep link หน้า — เข้าไปเหมือนปกติ)
  • ไม่มี 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 ครึ่งหลัง ("หรือชุดของเอกสาร") — ยังไม่ปิด ด้วยแผนนี้ (แผนนี้แสดง เอกสาร รวมกลุ่ม ไม่ใช่ bookmark รวมกลุ่มตาม requirement) — อย่าเปลี่ยนสถานะ
  • .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 รวมทั้งกลุ่ม (ครึ่งหลังของ requirement 5.3.10 แบบร่างเดิม — น่าดึงกลับมาภายหลังได้เมื่อผู้ใช้ขอ)
  • 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/อัปโหลด)
  2. หัวกลุ่ม → "เอกสารในกลุ่ม" → /groups/[typeId]: เห็นไฟล์ครบตามลำดับ + หมายเหตุ "อาจไม่ครบทุกฉบับ" + จำนวน annotation/bookmark ต่อไฟล์
  3. กด Annotate → เข้า viewer ของไฟล์นั้นได้ตามปกติ
  4. เปิดลิงก์รอบใหม่ของไฟล์เดิมด้วย typeId คนละค่า → ไฟล์ย้ายกลุ่ม กลุ่มเดิมลดจำนวน/หายไปเมื่อว่าง
  5. Regression: อัปโหลด N4 / annotate / ค้นหา N6 / export JSON N7 ยังทำงานเหมือนเดิม