23 KiB
แผน 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, commit6940b38) ตามคำขอผู้ใช้: ให้ 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 — การรับ/refreshtypeIdของเอกสารจากลิงก์เกิดที่registerExternalFileของ N8 ทั้งหมด (เดิมแผนนี้ไม่มี code path ที่เขียนtype_idเอง — ต่อมาเพิ่มช่องกลุ่มของฟอร์มอัปโหลด N4 ตามคำขอผู้ใช้ 2026-09-23) — N8 implement ครบแล้ว (2026-09-22 รวม D5): schematype_id+ index มากับ migrationdrizzle/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)
- กลุ่มระดับเดียว กำหนดด้วย
typeIdจากลิงก์ e-Court — เพิ่ม column บนตารางfilesค่าเดียว (type_id— ถอดtype_nameตามคำขอผู้ใช้ 2026-09-22, แสดงผลด้วยรหัสกลุ่มโดยตรง) — ไม่มีตารางกลุ่มแยก ไม่มี many-to-many ไม่มีหลายระดับ (เอกสารหนึ่งอยู่กลุ่มเดียวตามธรรมชาติของงาน) — ความหมายของ id เป็นของ e-Court ฝั่งเราถือว่า "ส่งมาเหมือนกัน = กลุ่มเดียวกัน" - เข้ามาอัตโนมัติ ไม่มี UI จัดการกลุ่ม — ไฟล์ได้กลุ่มเมื่อเปิดผ่านลิงก์ที่มี
typeId(เขียนที่ N8) หรือกรอกช่อง "กลุ่ม typeId" ตอนอัปโหลด N4 (เพิ่ม 2026-09-23) — กลุ่มเกิด/หายตามข้อมูลจริงในfilesไม่มีสถานะกลุ่มค้างในตารางอื่น ไม่มีหน้าทางย้ายไฟล์ที่มีอยู่เดิมเข้า/ออกกลุ่ม - หน้ารายการ
/จัดกลุ่มตาม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สองที่: N8registerExternalFile(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_idNULL (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.typeIdorderBy 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 + วันเวลาที่สร้าง (formatThaiDatewithTime — คำขอผู้ใช้ 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.md5.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 (ออกแบบไว้ล่วงหน้า)
- ลิงก์รอบใหม่ส่ง
typeIdคนละค่า → ย้ายกลุ่มตามค่าล่าสุด (เกิดที่ N8) — annotation/bookmark ผูกกับfile_idไม่เกี่ยวกับกลุ่ม จึงตามไฟล์ไปเอง - ลิงก์รอบใหม่ไม่ส่ง
typeId→ ไฟล์คงกลุ่มเดิม (N8: ห้ามเคลียร์ค่าเดิม) - ไฟล์ไม่เคยมี
typeId(seed / อัปโหลด N4 ที่ไม่กรอกกลุ่ม / ลิงก์เก่า) → section "ไม่มีกลุ่ม" บนหน้า/, ไม่มีหน้ากลุ่ม - กลุ่มแสดงผลด้วยรหัสล้วน — ไม่มีคอลัมน์ชื่อกลุ่ม (ถอด
type_nameตามคำขอผู้ใช้ 2026-09-22) — ถ้าภายหลังต้องการชื่อกลุ่ม กลับมาเพิ่ม column ใหม่ตาม governance typeIdมีอักขระพิเศษ/ยาว → ผ่าน URL ด้วยencodeURIComponent; normalize ที่ N8 (trim → ว่าง = NULL, cap 200); Next decode ให้อัตโนมัติตอนอ่านparams- กลุ่มว่างลง (ไฟล์ย้ายออกหมด) → กลุ่มหายจากหน้า
/เอง และ/groups/[typeId]กลายเป็น 404 (query จากfilesจริง ไม่มีตารางกลุ่มค้าง) - Dev DB แชร์กับการทดสอบของผู้ใช้ → เทสหน้า UI ด้วยการตั้ง
type_idบน id ที่จับเป๊ะ ไม่ reset ข้อมูลทั้งกลุ่ม - N8 ยังไม่ implement ตอนทำ N9 →
verify หน้า UI ด้วยการเขียน DB ตรง (task 3–4) แล้วรอ e2e จริงตอนปิดงาน N8(ล้าสมัย — N8 implement ครบแล้ว 2026-09-22: verify ได้ด้วยลิงก์จำลอง/การ์ดEcourtLinkForm+ path/samples/…ทันที) 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 เสร็จ)
npm run dev→ login → เปิดลิงก์ e-Court 2 ไฟล์ที่typeIdเดียวกัน + 1 ไฟล์typeIdต่างกลุ่ม → หน้า/จัด 2 section ตามกลุ่ม + section "ไม่มีกลุ่ม" (ไฟล์ seed/อัปโหลดที่ไม่กรอกกลุ่ม); อัปโหลดไฟล์พร้อมกรอกกลุ่ม → การ์ดไปอยู่ section ของกลุ่มนั้น + หน้า/groups/[typeId]เห็นไฟล์อัปโหลดด้วย- หัวกลุ่ม → "เอกสารในกลุ่ม" →
/groups/[typeId]: เห็นไฟล์ครบตามลำดับ + หมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" + จำนวน annotation/bookmark ต่อไฟล์ - กาง "Annotations & Bookmarks (N)" ในการ์ดไฟล์ → เห็น bookmark+annotation ทั้งไฟล์เรียงตามหน้า + กดแถวแต่ละรายการเด้งไปตำแหน่งนั้นใน viewer (
?ann=/?page=) - กด Annotate → เข้า viewer ของไฟล์นั้นได้ตามปกติ
- เปิดลิงก์รอบใหม่ของไฟล์เดิมด้วย
typeIdคนละค่า → ไฟล์ย้ายกลุ่ม กลุ่มเดิมลดจำนวน/หายไปเมื่อว่าง - Regression: อัปโหลด N4 / annotate / ค้นหา N6 / export JSON N7 ยังทำงานเหมือนเดิม