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

111 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# แผน 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/`)
```ts
// 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 ยังทำงานเหมือนเดิม