eCourt-note/docs/API-DOCUMENTS.md
Your Name 8cc15ea75d
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 52s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 13s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
refactor(routes): move routes under /note, APIs under /api/note
เตรียม basePath /docedit เป็นพื้นที่กลางให้แอปพี่น้อง (wordaddin ใน
อนาคต) — เดิม basePath ครอบ /docedit/note ทั้งหมดจนแชร์ prefix ไม่ได้
(Next มี basePath ได้ทางเดียว) และจัด API แยกต้นไม้ตามที่ผู้ใช้ขอ

(1) โครงสร้างใหม่: basePath = /docedit + route หน้าทั้งหมดย้ายเข้า
app/note/** + API 4 เส้นย้ายเข้า app/api/note/** + statics ย้ายเข้า
public/note/** (samples, uploads, pdf.worker) — หน้า/statics URL ปลายทาง
คงเดิมเป๊ะ (/docedit/note/…) ส่วน URL API เปลี่ยนเป็น /docedit/api/note/…
ตามคำขอผู้ใช้ (N10 endpoint เปลี่ยน — ต้องแจ้ง backend e-Court ก่อนใช้)

(2) กลไก: withBasePath (utils/base-path.ts) เติม base + app path /note
ยกเว้น path ที่ขึ้นต้น /api/ เติมแค่ base (caller เขียน /api/note/… เต็ม);
resolvePublicPdfPath root ที่ public/note (traversal guard เดิม);
middleware matcher ยกเว้นใหม่ (note/login, api/note, note/uploads,
note/samples, note/pdf.worker, _next, fonts, favicon) + redirect
/note/login; next.config เพิ่ม redirects /docedit -> /docedit/note;
navigation ภายใน 19 จุด + revalidatePath 3 จุด ใส่ prefix /note;
pdf_url ใน DB เป็น logical path ไม่แก้แม้แต่ตัวเดียว (seed/upload
เขียนค่าเดิม, browser เติมผ่าน withBasePath, disk เติมผ่าน guard)

(3) ตามแก้: patch-pdf-worker (DEST public/note + mkdirSync ปลายทาง),
seed-samples (SAMPLES_DIR), uploadFile (UPLOAD_DIR), Dockerfile/
.dockerignore/.gitignore/docker-compose.test.yml, tests 2 ไฟล์ที่อ้าง
disk path จริง, sync เอกสารทั้งหมด (CLAUDE.md invariant #6/#12 + tree,
README, docs/API-DOCUMENTS.md, .claude/rules/*, sync-db skill)

(4) !! ตอน deploy ต้องย้าย volume ตาม: mount /app/public/uploads ->
/app/public/note/uploads + ย้ายไฟล์ใน volume ให้ตรงตำแหน่ง ไม่งั้นไฟล์
ที่เคยอัปโหลด (พร้อม annotation ที่ผูกอยู่) 404 ทันทีที่สลับ image

(5) verify: build ผ่าน (route table /api/note/** + /note/** ครบ), tsc,
vitest 215/215, curl ครบทุก endpoint (login/redirect chain, worker,
samples, uploads, SSE, api/pdf, export, N8 register, N10 bundle + deep
link), Playwright browser จริง (canvas + textLayer ไทย render, browser
ยิง URL ใหม่ทั้งหมดไม่มี request ไป URL เก่า), plan-reviewer +
code-reviewer 3 รายงาน 0 blocking

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 14:55:07 +07:00

291 lines
23 KiB
Markdown
Raw Permalink 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.

# คู่มือการใช้ Documents API (N10)
> สำหรับ: ทีม backend ของ e-Court ที่จะเรียก API นี้แบบ **server-to-server** (ไม่มี browser/หน้า UI)
> ใช้กับโค้ดเวอร์ชัน 2026-09-25 · สเปกฉบับเต็ม/เหตุผลการออกแบบ: [`docs/PLAN-API.md`](./PLAN-API.md)
## 1. API นี้ทำอะไรให้
ดึงข้อมูลที่เกิดในระบบ Judge's Note กลับไปใช้ต่อ โดยถามได้ 2 แบบจาก **endpoint เดียว**:
| ต้องการ | ส่งอะไร | ได้อะไร |
|---|---|---|
| เอกสารเดียว | `fileId` (id ของ e-Court) | `documents` ยาว 1 ชิ้น พร้อม annotation + bookmark ของไฟล์นั้น |
| ทุกเอกสารในกลุ่ม | `typeId` | `documents` ทุกไฟล์ในกลุ่ม เรียงตามวันที่เปิดเข้าระบบ |
| เอกสารเดียว (ยืนยันกลุ่มด้วย) | `fileId` + `typeId` พร้อมกัน | เหลือเฉพาะเอกสารเดียวที่ตรง**ทั้งสอง**เงื่อนไข (AND) — ไม่ตรงคู่ = 404 |
เป็น **read-only** ทั้งหมด — ข้อมูลที่ได้เป็น snapshot ณ ตอนเรียก ไม่มี webhook/event push แจ้งเมื่อมีรายการใหม่ (ต้องยิงซ้ำเองตามความจำเป็น)
## 2. สิ่งที่ต้องมีก่อนเรียก
| สิ่งที่ต้องมี | ค่า |
|---|---|
| URL | `https://<host>/docedit/api/note/external/documents` (API อยู่ใต้ `app/api/note/**` ของ basePath `/docedit` — ต่างจากหน้า UI ที่อยู่ใต้ `/docedit/note/…`) |
| Method | `POST` (อ่านอย่างเดียว แต่ใช้ POST เพราะถามด้วย JSON body — GET ที่แนบ body ไม่เป็นมาตรฐาน) |
| API Key | ขอจากผู้ดูแลระบบ Judge's Note — เก็บเป็น secret ฝั่ง backend e-Court อย่าใส่ใน log/URL |
| Content-Type | `application/json` |
## 3. Authentication
ส่ง API Key ผ่าน header **`x-api-key`** ทุก request:
```http
POST /docedit/api/note/external/documents HTTP/1.1
Host: <host>
x-api-key: <API Key ที่ได้รับ>
content-type: application/json
```
- ไม่ใส่ header หรือ key ผิด → **401** พร้อมข้อความเดียวกันทั้งสองกรณี (`{"error":"API key ไม่ถูกต้อง"}`)
- Key นี้เชื่อถือได้ระดับ **backend e-Court เท่านั้น** — ห้ามส่งต่อให้ browser/แอปฝั่งผู้ใช้ (API ไม่ได้ใส่ CORS headers เพราะออกแบบให้เรียกจาก server เท่านั้น)
- ผลลัพธ์รวมตำแหน่งกล่องปิดพราง (REDACTION) ด้วย — กล่าวคือผู้ถือ key เห็นว่ามีการปิดพรางตรงไหนของหน้า (เห็นเฉพาะพิกัด เนื้อหาใต้กล่องไม่ได้)
## 4. Request body
```jsonc
{
"fileId": "<fileId ของ e-Court>", // ถามเอกสารเดียว
"typeId": "<typeId ของกลุ่ม>", // ถามทุกเอกสารในกลุ่ม
"filter": { // optional ทั้งก้อน
"type": "bookmarks", // "bookmarks" | "annotations" — ไม่ส่ง = ทั้งสอง
"userId": "<userId ของระบบนี้>", // หรือ array — กรองตามเจ้าของรายการ
"annotationTypes": ["COMMENT", "HIGHLIGHT"] // หรือค่าเดียว — กรองชนิดของ annotation
}
}
```
### 4.1 field ระดับบนสุด
| field | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `fileId` | string | — | id ของเอกสาร **ตามที่ e-Court ส่งมาในลิงก์** (`?fileId=`) ไม่ใช่ UUID ของระบบนี้ |
| `typeId` | string | — | รหัสกลุ่มเอกสาร ตามที่ส่งมาในลิงก์ (`?typeId=`) |
| `filter` | object | ไม่กรอง | ดู §4.2 |
กติกา:
- ต้องมี `fileId` **หรือ** `typeId` อย่างน้อยหนึ่งอย่าง — ขาดทั้งคู่ = 400
- ส่ง**ทั้งคู่**ได้ = AND กรองเฉพาะเอกสารเดียวที่ตรงทั้งสองเงื่อนไข (เช่น กันเข้าผิดกลุ่ม) — ไม่ตรงคู่ = 404
- ค่า string จะถูก trim — ส่งมาเป็นค่าว่าง/whitespace ล้วน = ถือว่าไม่ได้ส่ง field นั้น
- รับเฉพาะ 3 field นี้ — field อื่น (พิมพ์ผิด เช่น `fileID`, `filters`) = 400 บอกชื่อ field ที่ไม่รู้จัก
### 4.2 `filter`
| field | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| `type` | `"bookmarks"` \| `"annotations"` | ทั้งสอง | เลือกเอาเฉพาะส่วน — ส่วนที่ไม่เลือกคืนเป็น array ว่าง ไม่ใช่หายไปจาก response |
| `userId` | string หรือ array ของ string | ทุกคน | กรองตาม**เจ้าของรายการ**ทั้ง annotations และ bookmarks (`userId` ระบบนี้ = `u_userid` ที่เห็นใน DTO) — ไม่กรองตามผู้ตอบ reply (reply ตามมาครบกับ annotation ที่ผ่านตัวกรอง) |
| `annotationTypes` | string หรือ array ของ enum | ทุกชนิด | กรองชนิดของ annotation — มีผลเฉพาะเมื่อผลลัพธ์รวม annotations (เลือก `type:"bookmarks"` อย่างเดียว = เพิกเฉยเงียบ ๆ ไม่ใช่ 400) |
ค่า enum ของ `annotationTypes`: `HIGHLIGHT` `DRAW` `COMMENT` `SHAPE` `REDACTION` `TEXTBOX` `STAMP` — ค่าอื่น = 400
`filter` รับเฉพาะ 3 field นี้เช่นกัน (`type`/`userId`/`annotationTypes`) — field อื่น = 400
## 5. Response 200
รูปเดียวเสมอ (ไม่ว่าถามแบบไหน): `{ "documents": [ … ] }` — `documents` เป็น array ยาว 1 ตอนถามด้วย `fileId`, หลายชิ้นตอนถามด้วย `typeId` (กลุ่มว่าง = array ว่าง ไม่ใช่ error)
### 5.1 field ของแต่ละเอกสาร
| field | ชนิด | คำอธิบาย |
|---|---|---|
| `id` | string | UUID ของไฟล์ในระบบนี้ (ใช้ประกอบ `url` — ไม่ต้องจำ) |
| `externalFileId` | string \| null | `fileId` ของ e-Court ตามที่ส่งมาในลิงก์ — **null** ได้: ไฟล์ในกลุ่มที่มาโดยไม่ผ่าน fileId (เข้าจากลิงก์ไม่มี fileId หรืออัปโหลดในระบบนี้) |
| `title` | string | ชื่อเอกสาร |
| `typeId` | string \| null | กลุ่ม — null = ไม่มีกลุ่ม |
| `createdAt` | string | ISO 8601 (วันที่เปิดเข้าระบบครั้งแรก) |
| `annotations` | array | ดู §5.2 — เรียง `(pageIndex, createdAt)` |
| `bookmarks` | array | ดู §5.3 — เรียง `pageIndex → yOffset → createdAt` |
ลำดับ `documents` เมื่อถามด้วย `typeId`: ตาม `createdAt` ของไฟล์ (เก่า → ใหม่)
### 5.2 annotation แต่ละรายการ
| field | ชนิด | คำอธิบาย |
|---|---|---|
| `id` | string | UUID ของรายการ |
| `fileId` | string | UUID ของไฟล์ |
| `pageIndex` | number | **0-based** — หน้า 1 ของเอกสาร = `0` |
| `userId` / `userName` | string | ผู้สร้างรายการ (`userName` = `u_userlogin` ของ e-Court) |
| `type` | string | หนึ่งใน 7 enum ด้านบน |
| `data` | object | รายละเอียดตามชนิด — ดูตาราง §5.4 |
| `createdAt` / `updatedAt` | string | ISO 8601 |
| `resolved` | boolean | สถานะ "จัดการแล้ว" (F10) |
| `resolvedAt` | string \| null | ISO 8601 / null |
| `url` | string | deep link กลับมาหน้าอ่านเอกสารของระบบนี้ ไปตรงรายการนี้ (`…/annotate/<id>?ann=<annotationId>`) — เป็น **URL เต็ม** ใช้ได้ทันที |
| `replies` | array | คำตอบในเธรดของ comment — ดู §5.5 — เรียง `createdAt` |
รวม annotation **ทุกสถานะ** (ดู `resolved`) และ**ทุกผู้ใช้** (เว้นแต่กรองด้วย `filter.userId`)
### 5.3 bookmark แต่ละรายการ
| field | ชนิด | คำอธิบาย |
|---|---|---|
| `id` | string | UUID ของรายการ |
| `fileId` | string | UUID ของไฟล์ |
| `pageIndex` | number | **0-based** เช่นเดียวกับ annotation |
| `userId` / `userName` | string | ผู้สร้าง (`userName` อาจเป็น `""` สำหรับรายการเก่า = ไม่ทราบชื่อ) |
| `title` | string | ชื่อหัวข้อ bookmark |
| `parentId` | string \| null | โครงสร้าง 3 ระดับ — null = ระดับบนสุด, มีค่า = เป็นหัวข้อย่อยของ bookmark นั้น |
| `note` | string \| null | โน้ตย่อยของ bookmark |
| `yOffset` | number | ตำแหน่งบนหน้า เป็น % ของความสูงหน้า (0–100) |
| `createdAt` | string | ISO 8601 |
| `url` | string | deep link กลับมาหน้าอ่าน (`…/annotate/<id>?page=<หน้า>` เป็น **1-based** — `?page=1` = `pageIndex` 0) |
### 5.4 field `data` ของ annotation ตามชนิด
พิกัดทุกค่าเป็น **% ของขนาดหน้า** (ไม่ใช่ pixel — วาดทับตำแหน่งเดิมได้ทุกขนาดจอ): `Rect` = `{x, y, width, height}`, `Point` = `{x, y}`
| `type` | field ใน `data` | หมายเหตุ |
|---|---|---|
| `HIGHLIGHT` | `text`, `color`, `rects[]`, `style?`, `opacity?`, `note?` | `text` = ข้อความที่คลุม, `rects` = 1 rect ต่อบรรทัด, `style` = `highlight`\|`underline`\|`strikethrough`\|`squiggle` (ไม่มี = ไฮไลต์ทึบ) |
| `DRAW` | `strokeColor`, `strokeWidth`, `paths[]`, `pen?`, `widths?`, `opacity?`, `note?` | เส้นวาดมือ — `strokeWidth` เป็น % ของความกว้างหน้า, `pen` = `pencil`\|`highlighter`, `widths` = ความหนาต่อจุดจากสไตลัส (ยาวเท่า `paths`) |
| `COMMENT` | `position`, `message` | sticky note — `position` = ตำแหน่ง pin |
| `SHAPE` | `shape`, `strokeColor`, `strokeWidth`, `note?` + `rect` (rect/ellipse) **หรือ** `from`/`to` (line/arrow) | `shape` = `rect`\|`ellipse`\|`line`\|`arrow` |
| `REDACTION` | `rect`, `note?` | กล่องปิดพราง — เห็นเฉพาะตำแหน่ง ไม่มีเนื้อหาใต้กล่องใน DB |
| `TEXTBOX` | `position`, `text`, `color`, `fontSize`, `bold?`, `opacity?`, `align?`, `segments?`, `width?`, `note?` | `position` = มุมซ้ายบนของกล่อง, `fontSize` = % ของความกว้างหน้า, `text` เก็บข้อความรวมเสมอ (แม้มี `segments` ตัวหนาเฉพาะส่วน) |
| `STAMP` | `label`, `position`, `scale`, `color?`, `note?` | ตรายางข้อความ preset — `scale` = ความกว้างกรอบ % ของความกว้างหน้า |
`note` (optional ทุกชนิด) = โน้ตคำอธิบายของรายการ เช่น เหตุผลที่ไฮไลต์
### 5.5 reply แต่ละรายการ (ใน `replies` ของ annotation)
| field | ชนิด | คำอธิบาย |
|---|---|---|
| `id` | string | UUID ของคำตอบ |
| `annotationId` | string | UUID ของ annotation แม่ |
| `userId` / `userName` | string | ผู้ตอบ |
| `message` | string | ข้อความ |
| `createdAt` | string | ISO 8601 |
## 6. Error
error ทุกสถานะเป็น JSON รูปเดียวกัน: `{ "error": "<ข้อความไทย>" }`
| สถานะ | เมื่อไร | ข้อความ |
|---|---|---|
| 400 | body ไม่ใช่ JSON ที่ถูกต้อง | `body ต้องเป็น JSON ที่ถูกต้อง` |
| 400 | body ไม่ใช่ object | `body ต้องเป็น JSON object` |
| 400 | มี field ที่ไม่รู้จัก | `ไม่รู้จัก field '<field>' (รับเฉพาะ fileId, typeId, filter)` |
| 400 | `fileId`/`typeId` ไม่ใช่ string | `fileId ต้องเป็น string` / `typeId ต้องเป็น string` |
| 400 | ไม่มีทั้ง `fileId` และ `typeId` | `ต้องระบุ fileId หรือ typeId อย่างน้อยหนึ่งอย่าง` |
| 400 | `filter` ไม่ใช่ object / มี field อื่น | `filter ต้องเป็น object เช่น { "type": "bookmarks" }` / `filter รับเฉพาะ field 'type', 'userId', 'annotationTypes'` |
| 400 | `filter.type` ค่าไม่รู้จัก (รวมเอกพจน์ `annotation`/`bookmark`) | `filter.type รับค่า bookmarks หรือ annotations เท่านั้น` |
| 400 | `filter.userId` ไม่ใช่ string/array ของ string | `filter.userId ต้องเป็น string หรือ array ของ string` |
| 400 | `filter.annotationTypes` ค่านอก enum | `filter.annotationTypes รับค่า HIGHLIGHT, DRAW, COMMENT, SHAPE, REDACTION, TEXTBOX, STAMP เท่านั้น` |
| 401 | ไม่ใส่ `x-api-key` หรือ key ผิด | `API key ไม่ถูกต้อง` (ข้อความเดียวกันกัน enumeration) |
| 401 | ฝั่ง server ยังไม่ตั้ง key | `API ยังไม่ได้ตั้งค่าคีย์` |
| 404 | ถาม `fileId` ที่ไม่เคยเปิดในระบบ | `ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยเปิด)` |
| 404 | ส่งคู่ `fileId`+`typeId` แต่ไม่ตรงคู่ | `ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยเปิด หรือไม่ได้อยู่ในกลุ่ม typeId ที่ระบุ)` |
| 500 | ระบบนี้มีปัญหาฐานข้อมูล ณ ตอนนั้น | `เกิดข้อผิดพลาดของระบบ โปรดลองใหม่อีกครั้ง` (รายละเอียดอยู่ใน log ฝั่ง server อย่างเดียว) |
การแยก "ไม่มีข้อมูล" ออกจาก "ผิดพลาด":
- เอกสารเคยเปิดแต่ไม่มีรายการ → **200** พร้อม `annotations: []` / `bookmarks: []`
- กลุ่มไม่มีเอกสารเลย → **200** พร้อม `documents: []`
- `filter.userId` ไม่ตรงกับผู้ใช้ใดในระบบ → **200** รายการว่าง (ไม่ใช่ 400)
## 7. ตัวอย่างการเรียก
### 7.1 เอกสารเดียว เอาทุกอย่าง
```bash
curl -X POST -H "x-api-key: $EXTERNAL_API_KEY" -H "content-type: application/json" \
-d '{"fileId":"1234"}' \
"http://localhost:3000/docedit/api/note/external/documents"
```
Response (ตัดย่อ):
```jsonc
{
"documents": [
{
"id": "3f2a9c1e-8b7d-4e2a-9f10-11a2b3c4d5e6",
"externalFileId": "1234",
"title": "คำพิพากษา ผบ.123/2569",
"typeId": "CASE-2569-0001",
"createdAt": "2026-09-24T09:15:33.120Z",
"annotations": [
{
"id": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"fileId": "3f2a9c1e-8b7d-4e2a-9f10-11a2b3c4d5e6",
"pageIndex": 0,
"userId": "u_1234",
"userName": "somchai.j",
"type": "COMMENT",
"data": { "position": { "x": 41.2, "y": 37.8 }, "message": "ข้อเท็จจริงตรงนี้ขัดกับคำให้การ ร.2" },
"createdAt": "2026-09-24T10:02:11.530Z",
"updatedAt": "2026-09-24T10:02:11.530Z",
"resolved": false,
"resolvedAt": null,
"url": "http://localhost:3000/docedit/note/annotate/3f2a9c1e-…?ann=7c1d2e3f-…",
"replies": [
{
"id": "9e8f0a1b-2c3d-4e5f-6a7b-8c9d0e1f2a3b",
"annotationId": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"userId": "u_2200",
"userName": "somsri.k",
"message": "บันทึกไว้ก่อน ค่อยถามผู้เบิกความวันพรุ่งนี้",
"createdAt": "2026-09-25T08:44:02.310Z"
}
]
}
],
"bookmarks": [
{
"id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"fileId": "3f2a9c1e-8b7d-4e2a-9f10-11a2b3c4d5e6",
"pageIndex": 2,
"userId": "u_1234",
"userName": "somchai.j",
"title": "บทวินิจฉัย",
"parentId": null,
"note": null,
"yOffset": 12.4,
"createdAt": "2026-09-24T10:05:47.900Z",
"url": "http://localhost:3000/docedit/note/annotate/3f2a9c1e-…?page=3"
}
]
}
]
}
```
### 7.2 เอกสารเดียว เฉพาะ bookmark ของผู้ใช้หนึ่ง
```bash
curl -X POST -H "x-api-key: $EXTERNAL_API_KEY" -H "content-type: application/json" \
-d '{"fileId":"1234","filter":{"type":"bookmarks","userId":"u_1234"}}' \
"http://localhost:3000/docedit/api/note/external/documents"
```
(`userId` เป็นค่าของระบบนี้ — เก็บ collect จาก response ก่อนหน้า หรือดึงมาทั้งหมดแล้วกรองฝั่งผู้เรียกเองก็ได้)
### 7.3 ทุกเอกสารในกลุ่ม เฉพาะ COMMENT และ HIGHLIGHT
```bash
curl -X POST -H "x-api-key: $EXTERNAL_API_KEY" -H "content-type: application/json" \
-d '{"typeId":"CASE-2569-0001","filter":{"annotationTypes":["COMMENT","HIGHLIGHT"]}}' \
"http://localhost:3000/docedit/api/note/external/documents"
```
ได้ `documents` หลายชิ้นเรียงตามวันที่เปิดเข้าระบบ — ไฟล์ที่มาโดยไม่ผ่าน fileId (อัปโหลดในระบบนี้) ก็รวมอยู่ในกลุ่ม โดย `externalFileId: null`
### 7.4 fileId + typeId พร้อมกัน (AND)
```bash
curl -X POST -H "x-api-key: $EXTERNAL_API_KEY" -H "content-type: application/json" \
-d '{"fileId":"1234","typeId":"CASE-2569-0001"}' \
"http://localhost:3000/docedit/api/note/external/documents"
```
ได้เฉพาะเอกสารเดียวที่ตรงทั้งคู่ — ไฟล์นี้ไม่เคยเปิดหรืออยู่คนละกลุ่มกับ typeId ที่ระบุ = 404
## 8. ข้อควรรู้/ข้อจำกัด
1. **เอกสารต้องเคยเปิดผ่านลิงก์ e-Court ก่อนอย่างน้อยหนึ่งครั้ง** — ระบบนี้ลงทะเบียนเอกสาร (metadata-only) เมื่อมีผู้เปิดจากลิงก์ `?fileId=&fileUrl=&typeId=` ครั้งแรกเท่านั้น ยังไม่เคยเปิด = 404 (ไม่ใช่ error ฝั่ง e-Court) · ลิงก์ที่ไม่มี `fileId` (ส่งแต่ `fileUrl`) จะค้นด้วยวิธีนี้ไม่ได้
2. **`pageIndex` เป็น 0-based ส่วน `?page=` ใน url เป็น 1-based** — `pageIndex: 0` กับ `?page=1` คือหน้าเดียวกัน
3. **`url` อ่าน origin จาก request ของผู้เรียก** — ถ้าระบบนี้ deploy หลัง reverse proxy ต้องตั้งให้ proxy ส่งต่อ `x-forwarded-proto`/`x-forwarded-host` ให้ถูกต้อง ไม่งั้นลิงก์จะชี้ origin ของ proxy แทน public URL
4. **timestamp เป็น ISO 8601 (UTC) ทุกจุด**
5. ไม่มี pagination/rate limiting/caching — กลุ่มใหญ่ได้ response ยาวตามจำนวนรายการจริง
6. ไม่มี API ฝั่งเขียน (สร้าง/แก้ annotation ผ่าน API) และไม่มีการดึงไฟล์ PDF/bytes ผ่าน endpoint นี้ — ข้อมูลที่ได้มีเฉพาะ metadata + พิกัด + ข้อความของ annotation/bookmark