docs: คู่มือการใช้ N10 Documents API สำหรับทีม backend e-Court (docs/API-DOCUMENTS.md) — เอกสารส่งต่อผู้เรียกใหม่ แยกจาก docs/PLAN-API.md ที่เป็นสเปก/เหตุผลการออกแบบภายใน: เนื้อหาเขียนจากโค้ดจริงทุก field (route + lib/external-api.ts + lib/external-api-auth.ts + db/schema.ts DTO) — (1) ภาพรวม 3 แบบการถาม (fileId เดียว / typeId ทั้งกลุ่ม / ทั้งคู่ = AND) + URL เต็มใต้ basePath /docedit/note + เหตุผลใช้ POST (ถามด้วย JSON body — GET ที่แนบ body ไม่มี defined semantics ตาม RFC 9110 proxy/gateway อาจตัดทิ้ง) (2) auth ผ่าน header x-api-key: ข้อความ 401 ซ้ำกันกัน enumerate + key เชื่อถือระดับ backend เท่านั้น (เห็นตำแหน่ง REDACTION) (3) request body ตาราง field ระดับบนสุด + filter ทั้ง 3 มิติ (type/userId/annotationTypes) พร้อมกติกา trim ค่าว่าง = ไม่ได้ส่ง + strict validation 400 กัน typo (4) response 200: ตาราง field เอกสาร/annotation/bookmark/reply + data ตามชนิด annotation ทั้ง 7 (พิกัด % ของหน้า) + ลำดับรายการ (annotations pageIndex+createdAt / bookmarks N5 / replies createdAt / documents files.createdAt) + ความหมาย url deep link เต็ม (?ann= / ?page= 1-based) (5) ตาราง error ทุก status พร้อมข้อความไทยตรงตามที่โค้ดตอบจริง + การแยก 'ไม่มีข้อมูล' (200 + array ว่าง) จาก 'ผิดพลาด' (6) ตัวอย่าง curl 4 แบบ พร้อม response ตัวอย่างเต็มรูป annotation+reply+bookmark (7) ข้อควรรู้: เอกสารต้องเคยเปิดผ่านลิงก์ e-Court ก่อน, pageIndex 0-based vs ?page= 1-based, reverse proxy ต้องส่ง x-forwarded-proto/host ให้ถูกไม่งั้น url ชี้ origin ผิด, ไม่มี pagination/write API — README แก้จุดเดียว: หัวข้อ N10 ชี้มาที่คู่มือ โดยคงลิงก์ PLAN-API.md เป็นสเปก (คำขอผู้ใช้ 2026-09-25)
Some checks failed
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) Failing after 13s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Has been skipped
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Failing after 0s

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-25 13:19:43 +07:00
parent f4d46c7720
commit 882325b58a
2 changed files with 292 additions and 1 deletions

View file

@ -94,7 +94,7 @@ Sidebar ซ้าย: แท็บ Bookmarks / Annotations — คลิกร
## API สำหรับระบบภายนอก (N10)
Read-only API ให้ **backend ของ e-Court** เรียกแบบ server-to-server (ไม่มีหน้า UI) — endpoint เดียว `POST /api/external/documents` (JSON body) ตรวจสิทธิ์ด้วย API Key เดียวใน env `EXTERNAL_API_KEY` ส่งผ่าน header `x-api-key` (env ไม่ตั้ง/ว่าง = ปฏิเสธทุก request — fail closed) — รายละเอียดเต็ม: `docs/PLAN-API.md`
Read-only API ให้ **backend ของ e-Court** เรียกแบบ server-to-server (ไม่มีหน้า UI) — endpoint เดียว `POST /api/external/documents` (JSON body) ตรวจสิทธิ์ด้วย API Key เดียวใน env `EXTERNAL_API_KEY` ส่งผ่าน header `x-api-key` (env ไม่ตั้ง/ว่าง = ปฏิเสธทุก request — fail closed) — คู่มือการใช้สำหรับผู้เรียก: `docs/API-DOCUMENTS.md` · สเปก/เหตุผลการออกแบบ: `docs/PLAN-API.md`
```jsonc
// POST /docedit/note/api/external/documents (URL เต็มฝั่งผู้เรียกอยู่ใต้ basePath)

291
docs/API-DOCUMENTS.md Normal file
View file

@ -0,0 +1,291 @@
# คู่มือการใช้ 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/note/api/external/documents` (แอป serve ใต้ basePath `/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/note/api/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/note/api/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/note/api/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/note/api/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/note/api/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