BASE_PATH: serve ใต้ /docedit/note — next.config.mjs ตั้ง basePath + env NEXT_PUBLIC_BASE_PATH (ประกาศค่าเดียวที่ไฟล์นี้) ให้ utils/base-path.ts (ใหม่) withBasePath() เติม prefix ให้ URL ฝั่ง browser ที่ Next ไม่เติมให้เอง (fetch/EventSource/plain href/workerSrc/img src): PDFViewer (workerSrc, /api/pdf ซ่อม ToUnicode, fallback file.pdfUrl, image branch N3 รวบเป็นตัวแปร fileUrl, export PDF F8), SSE stream ใน useAnnotationRealtime, ปุ่มส่งออก bookmark JSON (N7) + คัดลอกลิงก์ F12 ใน AnnotationSidebar (window.location.origin + basePath กัน paste แล้ว 404), Export CSV (F16) หน้า summary — <Link>/next/navigation/middleware.ts ไม่ต้องแก้ (Next 14.2 auto-prefix ให้ + matcher จับ path ที่ตัด basePath แล้ว + NextURL redirect ใส่กลับเอง ยืนยันจาก source ใน node_modules) + pdf_url ใน DB เก็บ path ไม่รวม basePath เติมตอนใช้เท่านั้น + sync เอกสารตาม governance: CLAUDE.md invariant #6 (workerSrc ผ่าน withBasePath) + #12 ใหม่ (URL ฝั่ง browser ที่สร้างเองต้องผ่าน withBasePath), PLAN.md A3/§4, rules pdf-viewer/realtime, plan-reviewer #8, README URL localhost:3000/docedit/note — verify: tsc strict ผ่าน + curl (root / 404, ไม่มี cookie 307→/docedit/note/login, worker 200, SSE reachable, มี cookie 200) + Playwright login จริงเปิดไฟล์ 44 หน้า render ครบ (SSE /api/pdf /worker 200 ผ่าน basePath) + code-reviewer ผ่าน (vitest 126 tests, ไม่พบ bug, blob URL ไม่โดน prefix ซ้ำ, env inline ครบทั้ง client/server ตาม define-env-plugin) — deploy: basePath เป็น build-time ต้อง build image ใหม่ถึงจะเสิร์ฟที่ /docedit/note + reverse proxy ต้อง forward path เต็มแบบไม่ strip prefix
All checks were successful
Deploy eCourt Note Web to UAT Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to UAT Server / Build Web Docker Image (push) Successful in 48s
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to UAT Server / Deploy eCourt Note Web to UAT Server (push) Successful in 2s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 48s
Deploy eCourt Note Web to UAT Server / Notify Deployment Status (push) Successful in 1s
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 0s

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-17 16:06:20 +07:00
parent 34c3cf2a8f
commit fa880b314e
12 changed files with 43 additions and 21 deletions

View file

@ -115,6 +115,7 @@ import {
} from "@/utils/coordinates";
import type { OacUser } from "@/utils/user";
import { fileKindFromUrl, imageFormatFromUrl } from "@/utils/file-kind";
import { withBasePath } from "@/utils/base-path";
import { decodeTiffPages } from "@/utils/tiff-client";
import {
TEXTBOX_DEFAULT_WIDTH_PCT,
@ -1401,7 +1402,7 @@ export default function PDFViewer({
setExporting(true);
setExportError(null);
try {
const res = await fetch(`/api/annotations/export?fileId=${file.id}`);
const res = await fetch(withBasePath(`/api/annotations/export?fileId=${file.id}`));
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(text || `HTTP ${res.status}`);
@ -1449,21 +1450,23 @@ export default function PDFViewer({
setDoc(null);
setPdfjs(null);
const objectUrls: string[] = [];
// URL จริงที่ browser ใช้โหลดไฟล์ (fetch/img.src ตรง — ต้องเติม basePath เอง)
const fileUrl = withBasePath(file.pdfUrl);
(async () => {
let pages: { src: string | null; width: number; height: number }[];
if (imageFormatFromUrl(file.pdfUrl) === "tiff") {
// N3: TIFF หลายเฟรม = หลายหน้า — decode ครั้งเดียวต่อการเปิดไฟล์
// (utif2 dynamic import ภายใน decodeTiffPages) แล้วเก็บ blob URL ไว้ใช้ต่อ
const buffer = await (await fetch(file.pdfUrl)).arrayBuffer();
const buffer = await (await fetch(fileUrl)).arrayBuffer();
pages = await decodeTiffPages(buffer, IMAGE_PAGE_WIDTH);
for (const p of pages) if (p.src) objectUrls.push(p.src);
} else {
const img = new Image();
img.src = file.pdfUrl;
img.src = fileUrl;
await img.decode();
const factor = IMAGE_PAGE_WIDTH / img.naturalWidth;
pages = [
{ src: file.pdfUrl, width: IMAGE_PAGE_WIDTH, height: img.naturalHeight * factor },
{ src: fileUrl, width: IMAGE_PAGE_WIDTH, height: img.naturalHeight * factor },
];
}
if (cancelled) {
@ -1488,16 +1491,16 @@ export default function PDFViewer({
(async () => {
const pdfjs = await import("pdfjs-dist");
pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";
pdfjs.GlobalWorkerOptions.workerSrc = withBasePath("/pdf.worker.min.mjs");
// โหลดผ่าน /api/pdf ที่ซ่อม ToUnicode จาก ActualText ของ PDF ไทยที่ cmap เพี้ยน
// (ข้อความถูกทั้ง select/copy/ค้นหา) — endpoint fail ก็ถอยไปไฟล์ดิบตามเดิม
let data: ArrayBuffer;
try {
const res = await fetch(`/api/pdf?fileId=${file.id}`);
const res = await fetch(withBasePath(`/api/pdf?fileId=${file.id}`));
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data = await res.arrayBuffer();
} catch {
data = await (await fetch(file.pdfUrl)).arrayBuffer();
data = await (await fetch(withBasePath(file.pdfUrl))).arrayBuffer();
}
const doc = await pdfjs.getDocument({ data }).promise;
loadedDoc = doc;