eCourt-note/lib/openapi.ts

413 lines
17 KiB
TypeScript
Raw Normal View History

2026-10-07 10:16:12 +07:00
// OpenAPI 3.0 spec ของ API ทดสอบทั้งหมดของ judes-note — เส้นเดียวกับที่ Swagger UI
// แสดงผล: กลุ่ม template (DOCX Content Control) + N10 external API
// route handler GET /api/swagger (app/api/swagger/route.ts) เป็นคนเดียวที่ใช้
// — inline เป็น JSON ลงใน HTML ตอน respond (ไม่มี endpoint spec แยก)
//
// URL ใน paths เป็น path เต็มรวม /api/… ตาม invariant #12 (URL จริง
// /docedit/api/… — servers เติมแค่ basePath ไม่มี /note ซ้อน)
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const ANNOTATION_TYPE_ENUM = [
"HIGHLIGHT",
"DRAW",
"COMMENT",
"SHAPE",
"REDACTION",
"TEXTBOX",
"STAMP",
] as const;
export const openApiSpec = {
openapi: "3.0.3",
info: {
title: "Document Template & External API ของระบบ Judge's Note",
version: "1.0.0",
description:
"API ทดสอบของระบบ Judge's Note — กลุ่ม Template (DOCX Content Control) " +
"และ API ภายนอกสำหรับ backend e-Court (N10)",
},
servers: [{ url: BASE_PATH, description: "origin เดียวกับหน้านี้ (basePath /docedit)" }],
tags: [
{
name: "template",
description:
"Template เอกสาร DOCX ด้วย Content Control — ส่ง template (.docx) + data (JSON) " +
"แล้วได้เอกสารที่กรอกค่าแล้วกลับมา",
},
{
name: "Judge's Note",
description:
"API ภายนอก (N10) — backend e-Court เรียกแบบ server-to-server ของ (Judge's Note)" +
"ดึง annotations/bookmarks ของเอกสารที่เคยเปิดในระบบนี้",
},
],
paths: {
"/api/template/test": {
get: {
tags: ["template"],
summary: "ตรวจสอบว่า service มีชีวิต",
responses: {
200: {
description: "service พร้อมใช้งาน",
content: {
"application/json": {
schema: {
type: "object",
properties: {
ok: { type: "boolean" },
service: { type: "string" },
},
},
},
},
},
},
},
},
"/api/template/extract": {
post: {
tags: ["template"],
summary: "ดึงค่า Content Control ทั้งหมดออกมาจากไฟล์ .docx",
description:
"อ่าน w:sdt (Content Control) ทุกตัวใน word/document.xml — คืน map " +
"ชื่อแท็ก (w:tag) → ข้อความปัจจุบันในนั้น (ใช้สำรวจว่า template มีแท็กอะไรบ้างก่อนเรียก render)",
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
required: ["file"],
properties: {
file: {
type: "string",
format: "binary",
description: "ไฟล์ .docx ที่มี Content Control",
},
},
},
},
},
},
responses: {
200: {
description: "รายการแท็กพร้อมข้อความ",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ExtractResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/template/render": {
post: {
tags: ["template"],
summary: "สร้างเอกสารจาก template (.docx) + data (JSON)",
description:
"แทนค่า JSON ลงใน Content Control ของ template แล้วคืนไฟล์ .docx ใหม่ " +
'(filename ตอน download = generated-document.docx) — ส่ง data ได้ 2 ทาง: ' +
'field "data" เป็นไฟล์ JSON หรือ field "dataJson" เป็น string ตรง ๆ (เลือกอย่างใดอย่างหนึ่ง)',
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
required: ["template"],
properties: {
template: {
type: "string",
format: "binary",
description: "ไฟล์ .docx ที่เป็น template (มี Content Control)",
},
data: {
type: "string",
format: "binary",
description: 'ไฟล์ JSON เช่น { "blackno": "คดีหมายเลขดำที่", … }',
},
dataJson: {
type: "string",
description:
"JSON เป็น string ตรง ๆ (ทางเลือกแทนการแนบไฟล์ data)",
example:
'{"notname":"คำสั่งเรียก ให้ทำคำให้การ","blackno":"คดีหมายเลขดำที่","blackyear":"๑๐/๒๕๖๙"}',
},
},
},
},
},
},
responses: {
200: {
description: "ไฟล์ .docx ที่กรอกค่าแล้ว (binary)",
content: {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
schema: { type: "string", format: "binary" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/note/external/documents": {
post: {
tags: ["external"],
summary: "ดึง annotations/bookmarks ของเอกสาร (N10)",
description:
"ถามด้วย fileId (เอกสารเดียว — id ของ e-Court ที่ลิงก์มา) และ/หรือ typeId " +
"(ทุกเอกสารในกลุ่ม) — ส่งทั้งคู่ = AND เหลือเฉพาะเอกสารเดียวที่ตรงทั้งสองเงื่อนไข " +
"· response รูปเดียวเสมอ { documents: […] } (กลุ่มว่าง = array ว่าง) " +
"· read-only: snapshot ข้อมูล ณ ตอนเรียก · แต่ละรายการมี url deep link กลับหน้าอ่านของระบบนี้",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ExternalQuery" },
examples: {
"ถามด้วย fileId (เอกสารเดียว)": {
value: { fileId: "DOC-2569-0001" },
},
"ทุกเอกสารในกลุ่ม": {
value: { typeId: "GROUP-01" },
},
"กลุ่ม + เฉพาะ bookmark ของคนใดคนหนึ่ง": {
value: {
typeId: "GROUP-01",
filter: { type: "bookmarks", userId: "u0001" },
},
},
},
},
},
},
responses: {
200: {
description: "bundle ของเอกสารที่ตรงเงื่อนไข",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/DocumentsResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: {
description: "ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
404: {
description: "ไม่พบเอกสาร (ยังไม่เคยเปิดในระบบ หรือไม่อยู่ในกลุ่ม typeId ที่ระบุ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
},
components: {
securitySchemes: {
ApiKeyAuth: {
type: "apiKey",
in: "header",
name: "x-api-key",
description:
"ค่าเดียวกับ env EXTERNAL_API_KEY ของ server — กดปุ่ม Authorize ด้านบนเพื่อใส่ครั้งเดียวแล้วใช้ทุก request",
},
},
responses: {
BadRequest: {
description:
"request ไม่ถูกต้อง (field หาย / ชนิดไฟล์ผิด / ค่า enum ไม่รู้จัก / " +
"JSON ผิด format / ไฟล์แนบใหญ่เกิน 100MB)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
ServerError: {
description: "เกิดข้อผิดพลาดของระบบ (เช่นไฟล์ .docx ไม่ถูกต้อง / DB ล้ม)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
},
schemas: {
Error: {
type: "object",
properties: {
success: { type: "boolean", example: false },
error: { type: "string" },
},
},
ExtractResponse: {
type: "object",
properties: {
success: { type: "boolean", example: true },
data: {
type: "object",
description: "ชื่อแท็ก Content Control → ข้อความในนั้น (หลายบรรทัดคั่นด้วย \\n)",
additionalProperties: { type: "string" },
example: {
blackno: "คดีหมายเลขดำที่",
court: "ศาลปกครองสุพรรณบุรี",
},
},
},
},
ExternalQuery: {
type: "object",
description:
"ต้องมี fileId หรือ typeId อย่างน้อยหนึ่งอย่าง (ส่งทั้งคู่ได้ = AND เหลือเอกสารเดียว)",
properties: {
fileId: {
type: "string",
description: "id เอกสารของ e-Court — ตรงกับที่ส่งมากับลิงก์ ?fileId= ตอนเปิดเอกสาร",
},
typeId: {
type: "string",
description: "รหัสกลุ่มเอกสาร — คืนทุกไฟล์ในกลุ่มนี้ (เรียงตามวันที่สร้าง)",
},
filter: {
type: "object",
properties: {
type: {
type: "string",
enum: ["bookmarks", "annotations"],
description: "เลือกเฉพาะชนิดรายการ (ไม่ส่ง = ทั้งสอง ส่วนที่ไม่เลือกคืนเป็น array ว่าง)",
},
userId: {
description: "กรองตามเจ้าของรายการ (string เดี่ยวหรือ array)",
oneOf: [
{ type: "string" },
{ type: "array", items: { type: "string" } },
],
},
annotationTypes: {
description: "กรองชนิดของ annotation — มีผลเฉพาะเมื่อรวม annotations",
oneOf: [
{ type: "string", enum: [...ANNOTATION_TYPE_ENUM] },
{
type: "array",
items: { type: "string", enum: [...ANNOTATION_TYPE_ENUM] },
},
],
},
},
},
},
},
DocumentsResponse: {
type: "object",
properties: {
documents: {
type: "array",
items: { $ref: "#/components/schemas/DocumentBundle" },
},
},
},
DocumentBundle: {
type: "object",
description: "field ของเอกสารอยู่ระดับเดียวกับ annotations/bookmarks (bundle ยุบชั้น)",
properties: {
id: { type: "string", format: "uuid" },
externalFileId: {
type: "string",
nullable: true,
description: "id ของ e-Court (null = ไฟล์ url:-fallback หรืออัปโหลดในกลุ่ม)",
},
title: { type: "string" },
typeId: { type: "string", nullable: true },
createdAt: { type: "string", format: "date-time" },
annotations: {
type: "array",
items: { $ref: "#/components/schemas/ExternalAnnotation" },
},
bookmarks: {
type: "array",
items: { $ref: "#/components/schemas/ExternalBookmark" },
},
},
},
ExternalAnnotation: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
fileId: { type: "string", format: "uuid" },
pageIndex: { type: "integer", description: "0-based" },
userId: { type: "string" },
userName: { type: "string" },
type: { type: "string", enum: [...ANNOTATION_TYPE_ENUM] },
data: {
type: "object",
description:
"ข้อมูลเฉพาะชนิด (พิกัดเป็น % ของขนาดหน้า) เช่น HIGHLIGHT { text, color, rects }, COMMENT { message, position }",
},
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
resolved: { type: "boolean" },
resolvedAt: { type: "string", format: "date-time", nullable: true },
url: {
type: "string",
description: "deep link กลับหน้าอ่าน พร้อม ?ann= ของรายการนี้",
},
replies: {
type: "array",
items: { $ref: "#/components/schemas/ExternalReply" },
},
},
},
ExternalBookmark: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
fileId: { type: "string", format: "uuid" },
pageIndex: { type: "integer", description: "0-based" },
userId: { type: "string" },
userName: { type: "string" },
title: { type: "string" },
parentId: {
type: "string",
nullable: true,
description: "null = ระดับบนสุด (มีค่า = sub ของ bookmark นั้น)",
},
note: { type: "string", nullable: true },
yOffset: { type: "number", description: "ตำแหน่งบนหน้า (% ของความสูง)" },
createdAt: { type: "string", format: "date-time" },
url: {
type: "string",
description: "deep link กลับหน้าอ่าน พร้อม ?page= 1-based ของหน้านี้",
},
},
},
ExternalReply: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
annotationId: { type: "string", format: "uuid" },
userId: { type: "string" },
userName: { type: "string" },
message: { type: "string" },
createdAt: { type: "string", format: "date-time" },
},
},
},
},
} as const;