ecourt+groups: N8 เปิดเอกสารจากลิงก์ ?fileUrl=&fileId=&typeId= (metadata-only + allowlist + ดึงไฟล์สด + login วนกลับ + การ์ดจำลองลิงก์ + path dev /samples) + N9 กลุ่มเอกสารตาม typeId — หน้ารายการจัด section ต่อกลุ่ม + "ไม่มีกลุ่ม" ท้ายสุด + หน้า /groups/[typeId] พร้อมหมายเหตุ "อาจไม่ครบทุกฉบับ" + docs sync ครบ (2026-09-23)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-23 10:44:14 +07:00
parent 0aef3ff028
commit 329ae8e956
31 changed files with 2766 additions and 155 deletions

View file

@ -336,3 +336,28 @@ Dependencies เพิ่มเติม: F3 ← F2 · F12/F13/F19 ← F1 · F15
- **UI:** ปุ่มไอคอน `Braces` ({ }) ท้ายแถว toggle เงื่อนไขค้นหา N6 ในแท็บ "ที่คั่นหน้า" — `<a href="/api/bookmarks/export?fileId=">` ลิงก์ธรรมดา (pattern Export CSV F16 — ไม่มี JS/state) + `coarse-36` — ส่งออก**ทุก bookmark ของไฟล์เสมอ** (ไม่ผูกเงื่อนไขค้นหา N6 ที่ตั้งไว้)
- **Read-only:** ไม่ publish event ไม่ `revalidatePath` (ไม่เขียน DB — เหมือน `searchBookmarks`/route export เดิม)
- **Out of scope ของ N7:** export ตามเงื่อนไขค้นหา N6, รวม annotation/เพิ่ม bookmark ลงหน้าสรุป F15 หรือ CSV, กลับไปเป็นรายงาน PDF (ถอดแล้ว — จะเอาคืนดูแผนเดิม `docs/PLAN-BOOKMARK-PDF.md`), export ข้ามหลายไฟล์ (gap 5.3.10)
### N8 · เปิดเอกสารจากลิงก์ e-Court `?fileUrl=&fileId=&typeId=` (2026-09-22 · implement แล้ว)
> แผนงานฉบับเต็ม: `docs/PLAN-ECOURT.md`
- **สัญญาลิงก์:** `fileUrl` จำเป็น + `fileId` แนะนำ (dedupe key `external_id = "ecourt:<fileId>"`, ไม่มี fileId fallback `"url:<href>"`) + `typeId` แนะนำ (รหัสกลุ่มเอกสารที่เกี่ยวเนื่องกัน เช่น คดีเดียวกัน — ส่งต่อให้ **N9** `docs/PLAN-DOCSET.md` จัดกลุ่มตอนแสดงผล)
- **ไม่เก็บสำเนาไฟล์ (metadata-only):** ลง DB แค่ `title` (จาก basename ของ URL) + `external_id` + URL เต็มใน `pdf_url` + `type_id` — ตอนเปิดดู/export server ดึงไฟล์สดจาก e-Court ผ่าน route ของเรา (กัน CORS + คุม SSRF ที่จุดเดียว) — invariant #1 ครบ
- **หลักเวอร์ชัน — id เปลี่ยน = ไฟล์ใหม่:** fileId เดิม + URL เปลี่ยน (Temp link อายุใหม่) = row เดิม annotation ครบ + refresh `pdf_url`; fileId ใหม่ = row ใหม่เริ่ม blank ไม่สืบทอด; ลิงก์ส่ง `typeId` มา = ย้ายกลุ่มตามค่าล่าสุด (ไม่ส่ง = คงเดิม ห้ามเคลียร์)
- **Entry point 2 จุด:** หน้า `/` และ `/annotate` อ่าน query แล้ว `registerExternalFile` (`lib/external-import.ts` — dedupe + catch `23505` พร้อมกัน 2 แท็บ) → `redirect("/annotate/<uuid>")` — เรียกระหว่าง render ของ server component **ไม่ใช่ Server Action** (carve-out invariant #4: หน้า force-dynamic + redirect ออกทันที ไม่ต้อง publish event/`revalidatePath`) / fail = กล่อง error ไทย `role="alert"` เหนือ section เพิ่มเอกสาร หน้ารายการยังแสดง
- **Validation (pure `utils/external-file.ts`):** allowlist โฮสต์ `admincourt.go.th` (suffix match กัน `admincourt.go.th.evil.com`/`xadmincourt.go.th`) + env `EXTERNAL_FILE_HOSTS` เพิ่มได้, `https:` เท่านั้น, ห้าม `user:pass@` — และรับ **path ในเครื่อง** (`/samples/…` `/uploads/…` — decode + ห้าม `..` + ตรวจไฟล์มีจริง) เพื่อทดสอบ grouping ใน dev + เติม `https://` ให้ URL ที่ไม่มี scheme
- **ดึงไฟล์สด (`lib/remote-file.ts`):** `fetchSourceBytes` — validate ซ้ำชุดเต็มหลัง redirect (`res.url`), `Content-Length` pre-check + นับ byte ระหว่าง stream จำกัด **100MB**, timeout **60s**, guard body null/0 ไบต์ — ใช้ 2 จุด: `/api/pdf` และ `/api/annotations/export` (F8) ก่อนเข้า pipeline เดิม
- **Viewer:** `withBasePath` ปล่อย absolute `http(s)://` ผ่านเฉย ๆ (invariant #12), PDF branch โหลดผ่าน `/api/pdf?fileId=` อยู่แล้ว, **รูปรีโมต (N3)** เปลี่ยนไปโหลดผ่าน `/api/pdf?fileId=` ด้วย (แยกทางด้วย `isHttpUrl`)
- **Login วนกลับ (D5):** `middleware.ts` เดิมล้าง query ทิ้ง — แก้เป็นเก็บ `next = pathname + search` แนบไป `/login` (เฉพาะ `next !== "/"`) → หน้า login `sanitizeNextPath` (เฉพาะขึ้นต้น `/` และไม่ใช่ `//`/`/\` — กัน open-redirect) → มี session แล้ว `redirect(next ?? "/")` / login ผ่าน `LoginForm` `router.replace(next ?? "/")` — deep link ของคนยังไม่ login ไม่หาย
- **การ์ดจำลองลิงก์:** `components/EcourtLinkForm.tsx` บนหน้ารายการ `/` (กรอก fileUrl/fileId/typeId กด "เปิดเอกสาร" → `router.push("/annotate?fileUrl=&fileId=&typeId=")`) + แสดงลิงก์เต็มสด ๆ — ทดสอบก่อนมีลิงก์จริงจาก e-Court
- **Out of scope ของ N8:** auth/credential ต่อ e-Court (ส่งต่อ token/cookie ถ้าปลายทางต้องมี session จริง), ระบบหลายเวอร์ชัน + สืบทอด annotation ข้ามเวอร์ชัน, หน้าจัดการ/ลบ/แก้ `typeId` ด้วยมือใน UI (กลุ่มมาจากลิงก์ e-Court อย่างเดียว — N9), แคชสำเนาไฟล์รีโมตลงดิสก์, โฮสต์นอก allowlist, API ดึงข้อมูลกลับให้ e-Court (**N10** `docs/PLAN-API.md`) — known risk ที่รับไว้: `/api/pdf` + export เป็น proxy ไม่ต้อง login (รู้ fileId = ดึง bytes ได้)
### N9 · กลุ่มเอกสารตาม `typeId` จาก e-Court + หน้า `/groups/[typeId]` (2026-09-23)
> แผนงานฉบับเต็ม: `docs/PLAN-DOCSET.md` — ต่อยอด `type_id` ที่ N8 เขียนตอน `registerExternalFile`
- **กลุ่มระดับเดียว กำหนดด้วย `files.type_id`** (จากลิงก์ e-Court `?typeId=` — เขียนที่ `registerExternalFile` ของ N8 เท่านั้น: insert ครั้งแรก + ย้ายกลุ่มเมื่อลิงก์รอบใหม่ส่ง `typeId` คนละค่า, ไม่ส่ง = คงเดิม ห้ามเคลียร์) — ไม่มีตารางกลุ่ม/many-to-many/หลายระดับ: กลุ่ม = ค่า distinct ของ `type_id` ณ ตอน query, แสดงผลด้วยรหัสกลุ่มโดยตรง (ไม่มีคอลัมน์ชื่อกลุ่ม — ถอด `type_name` ตามคำขอผู้ใช้) — ไฟล์ seed/อัปโหลด N4/ลิงก์ไม่มี typeId = NULL เสมอ
- **หน้ารายการ `/` จัดกลุ่ม:** section ต่อกลุ่ม (`type_id` ไม่ NULL) เรียงตาม `createdAt` ของไฟล์ล่าสุดในกลุ่ม (กลุ่มที่มี activity ใหม่อยู่บน) — การ์ดไฟล์ในกลุ่มเรียง `createdAt` asc เหมือนเดิม + ปุ่ม Annotate เดิม; หัวกลุ่ม = รหัสกลุ่ม + จำนวนไฟล์ + ลิงก์ "เอกสารในกลุ่ม" → `/groups/{encodeURIComponent(typeId)}`; ไฟล์ `type_id` NULL → section **"ไม่มีกลุ่ม"** ท้ายสุด (ใกล้ฟอร์มเพิ่มเอกสาร — แสดงเสมอถ้ามีไฟล์) — จัดกลุ่มเป็นการแสดงผลล้วนบน server component เดิม ไม่มี client component/action ใหม่
- **หน้า `/groups/[typeId]`** (server component `force-dynamic` — login ถูกบังคับโดย `middleware.ts` อยู่แล้ว): query `files where type_id` `orderBy createdAt` — **ไม่มี = 404** (กลุ่มที่ระบบไม่รู้จัก/ไฟล์ย้ายออกหมดแล้ว — ไม่มีตารางกลุ่มค้าง); หัวกลุ่ม (รหัส + "N เอกสาร") + ลิงก์กลับ `/` + กล่อง info **"แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้ — อาจไม่ครบทุกฉบับของกลุ่ม"** แสดงทุกครั้ง; รายการ = ชื่อไฟล์ + วันที่นำเข้า (`formatThaiDate`) + จำนวน annotation/bookmark (query นับแยกด้วย `groupBy fileId` — 2 query เสริม ไม่ join หนัก) + ปุ่ม Annotate → `/annotate/{fileId}`; `generateMetadata` ตั้งชื่อแท็บ browser ตามรหัสกลุ่ม
- **ไม่มี Server Action/SSE/migration ใหม่** (schema `type_id` + index มากับ N8 ใน migration เดียวกัน) — การเปลี่ยนกลุ่มเกิดที่ N8 ตอน register แล้ว redirect ออกจาก `/` ทันที ไม่มี Router Cache ค้าง
- **Out of scope ของ N9:** จัดกลุ่ม/แก้ชื่อ/ลบกลุ่มด้วยมือใน UI, ไฟล์อยู่หลายกลุ่ม (many-to-many) / กลุ่มซ้อนหลายระดับ, bookmark รวมทั้งกลุ่ม (ครึ่งหลังของ gap 5.3.10 — ยังไม่ปิด), export JSON ตามกลุ่ม (N7 ยังรับ `fileId` เดียว), รายการเอกสารครบทุกฉบับของคดีรวมฉบับที่ยังไม่เคยเปิด (ข้อมูลอยู่ที่ e-Court), API ให้ระบบภายนอก (**N10** `docs/PLAN-API.md`)