eCourt-note/docs/PLAN-DOCSET.md

111 lines
22 KiB
Markdown
Raw Normal View 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-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-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) — กลุ่มเกิด/หายตามข้อมูลจริงใน `files` ไม่มีสถานะกลุ่มค้างในตารางอื่น
3. **หน้ารายการ `/` จัดกลุ่มตาม `typeId`** (section ต่อกลุ่ม + ส่วน "ไม่มีกลุ่ม" ท้ายสุด) + **หน้าใหม่ `/groups/[typeId]`** แสดงเอกสารทั้งกลุ่มที่รู้จัก พร้อมหมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" และการ์ดแต่ละไฟล์หุบ/กางดู bookmark+annotation ของไฟล์นั้นได้ (2026-09-23)
## Design
### D1 — Schema (`db/schema.ts` + `drizzle/`)
```ts
// 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 ใต้หัวกลุ่ม — ย่อจากข้อความเดิม "… — อาจไม่ครบทุกฉบับของกลุ่ม" ตามคำขอผู้ใช้ 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/อัปโหลด)
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 ยังทำงานเหมือนเดิม