eCourt-note/app/api/bookmarks/export/route.ts
Your Name 55d09ce5c3 N7 ส่งออกรายการ bookmark เป็น JSON แทนรายงาน PDF (เปลี่ยน deliverable ตามคำขอผู้ใช้ 2026-09-17) — route /api/bookmarks/export เขียนใหม่จาก PDFDocument.create ทั้งชุด (ฟอนต์ Sarabun/layout A4/แบ่งหน้า/ต้นไม้ indent 3 ระดับ รวม ~190 บรรทัด) เป็น JSON ข้อมูลล้วนจาก DB: validate fileId (UUID → 400) + ไม่พบไฟล์ = 404 → query bookmarks ทั้งไฟล์ orderBy pageIndex → yOffset → createdAt (ลำดับ N5 เดียวกับ sidebar) → envelope { fileId, title, exportedAt (ISO), bookmarks: BookmarkDTO[] } ผ่าน toBookmarkDTO ตรง ๆ (invariant #5) เก็บ parentId ครบ lossless (reconstruct ต้นไม้จากข้อมูลเองได้) จึงถอด build tree/cycle guard/ฟอนต์/layout ทิ้งหมด; pretty-print 2 space + content-type application/json; charset=utf-8 + attachment filename* จาก encodeRFC5987("{title}-bookmarks.json") (pattern F8/F16); 0 bookmark = "bookmarks": [] ไม่ 404 (ปุ่มทำงานเสมอ); ไม่โหลดไฟล์เอกสารต้นฉบับเลย (invariant #1) + read-only ไม่ publish event ไม่ revalidatePath + ปุ่มท้ายแถว toggle เงื่อนไขค้นหา N6 ในแท็บ "ที่คั่นหน้า" เปลี่ยนไอคอน Download → Braces ({ }) พร้อม title/aria-label "เป็น JSON" (ยังเป็นลิงก์ธรรมดา pattern Export CSV F16 — ส่งออกทุก bookmark ของไฟล์เสมอ ไม่ผูกเงื่อนไขค้นหา N6) + PLAN-FEAT.md §7 N7 เขียนสเปกใหม่เป็น JSON + PLAN-BOOKMARK-PDF.md ติด note หัวไฟล์ว่าโค้ด PDF ในแผนเป็น historical record (สเปกปัจจุบันอยู่ PLAN-FEAT) + gap-analysis ปิด gap กลุ่ม B 5.3.11 ✅ (N5) 5.3.12 ✅ (N6) 5.3.14 ✅ (API นี้) 5.3.15 ✅ (export PDF ของ F8 ฝัง bookmark เป็น outline ติดไปกับไฟล์) พร้อม header อัปเดตล่าสุด 2026-09-17 — ไฟล์เดียวกันมี 5.3.19/5.3.22 ✅ mock login/5.3.23 ของงาน login ตามไปด้วย
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 13:43:00 +07:00

52 lines
2.2 KiB
TypeScript

import { asc, eq } from "drizzle-orm";
import { db } from "@/db";
import { bookmarks, files, toBookmarkDTO } from "@/db/schema";
import { encodeRFC5987 } from "@/utils/pdf-export";
// ส่งออกรายการ bookmark เป็น JSON (N7 — เดิมเป็นรายงาน PDF ตาม requirement 5.3.15
// เปลี่ยนตามคำขอผู้ใช้ 2026-09-17)
// - ข้อมูลล้วนจาก DB ไม่โหลดไฟล์เอกสารต้นฉบับเลย (invariant #1)
// - array flat เก็บ parentId ครบ (lossless) เรียงตามลำดับ N5 เหมือน sidebar
// ต้นไม้ reconstruct จาก parentId ได้เอง จึงไม่ต้อง build tree/cycle guard
// - read-only: ไม่ publish event ไม่ revalidatePath (ไม่เขียน DB)
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export async function GET(req: Request) {
const fileId = new URL(req.url).searchParams.get("fileId") ?? "";
if (!UUID_RE.test(fileId)) {
return new Response("fileId ไม่ถูกต้อง", { status: 400 });
}
const [file] = await db.select().from(files).where(eq(files.id, fileId));
if (!file) return new Response("ไม่พบไฟล์นี้", { status: 404 });
const rows = await db
.select()
.from(bookmarks)
.where(eq(bookmarks.fileId, fileId))
.orderBy(
asc(bookmarks.pageIndex),
asc(bookmarks.yOffset),
asc(bookmarks.createdAt),
);
// item เป็น BookmarkDTO ตรง ๆ (camelCase + ISO string — invariant #5)
const payload = {
fileId: file.id,
title: file.title,
exportedAt: new Date().toISOString(),
bookmarks: rows.map(toBookmarkDTO),
};
const filename = encodeRFC5987(`${file.title}-bookmarks.json`);
return new Response(JSON.stringify(payload, null, 2) + "\n", {
headers: {
"content-type": "application/json; charset=utf-8",
// filename เป็น ASCII fallback + filename* UTF-8 สำหรับชื่อไทย (pattern F8/F16)
"content-disposition": `attachment; filename="bookmarks.json"; filename*=UTF-8''${filename}`,
},
});
}