// 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;