All checks were successful
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) Successful in 52s
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 1s
เตรียม basePath /docedit เป็นพื้นที่กลางให้แอปพี่น้อง (wordaddin ใน อนาคต) — เดิม basePath ครอบ /docedit/note ทั้งหมดจนแชร์ prefix ไม่ได้ (Next มี basePath ได้ทางเดียว) และจัด API แยกต้นไม้ตามที่ผู้ใช้ขอ (1) โครงสร้างใหม่: basePath = /docedit + route หน้าทั้งหมดย้ายเข้า app/note/** + API 4 เส้นย้ายเข้า app/api/note/** + statics ย้ายเข้า public/note/** (samples, uploads, pdf.worker) — หน้า/statics URL ปลายทาง คงเดิมเป๊ะ (/docedit/note/…) ส่วน URL API เปลี่ยนเป็น /docedit/api/note/… ตามคำขอผู้ใช้ (N10 endpoint เปลี่ยน — ต้องแจ้ง backend e-Court ก่อนใช้) (2) กลไก: withBasePath (utils/base-path.ts) เติม base + app path /note ยกเว้น path ที่ขึ้นต้น /api/ เติมแค่ base (caller เขียน /api/note/… เต็ม); resolvePublicPdfPath root ที่ public/note (traversal guard เดิม); middleware matcher ยกเว้นใหม่ (note/login, api/note, note/uploads, note/samples, note/pdf.worker, _next, fonts, favicon) + redirect /note/login; next.config เพิ่ม redirects /docedit -> /docedit/note; navigation ภายใน 19 จุด + revalidatePath 3 จุด ใส่ prefix /note; pdf_url ใน DB เป็น logical path ไม่แก้แม้แต่ตัวเดียว (seed/upload เขียนค่าเดิม, browser เติมผ่าน withBasePath, disk เติมผ่าน guard) (3) ตามแก้: patch-pdf-worker (DEST public/note + mkdirSync ปลายทาง), seed-samples (SAMPLES_DIR), uploadFile (UPLOAD_DIR), Dockerfile/ .dockerignore/.gitignore/docker-compose.test.yml, tests 2 ไฟล์ที่อ้าง disk path จริง, sync เอกสารทั้งหมด (CLAUDE.md invariant #6/#12 + tree, README, docs/API-DOCUMENTS.md, .claude/rules/*, sync-db skill) (4) !! ตอน deploy ต้องย้าย volume ตาม: mount /app/public/uploads -> /app/public/note/uploads + ย้ายไฟล์ใน volume ให้ตรงตำแหน่ง ไม่งั้นไฟล์ ที่เคยอัปโหลด (พร้อม annotation ที่ผูกอยู่) 404 ทันทีที่สลับ image (5) verify: build ผ่าน (route table /api/note/** + /note/** ครบ), tsc, vitest 215/215, curl ครบทุก endpoint (login/redirect chain, worker, samples, uploads, SSE, api/pdf, export, N8 register, N10 bundle + deep link), Playwright browser จริง (canvas + textLayer ไทย render, browser ยิง URL ใหม่ทั้งหมดไม่มี request ไป URL เก่า), plan-reviewer + code-reviewer 3 รายงาน 0 blocking Co-Authored-By: Claude Code <noreply@anthropic.com>
118 lines
5.9 KiB
TypeScript
118 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
// N8 — การ์ดจำลองลิงก์ e-Court บนหน้ารายการ: กรอก fileUrl (+ fileId + typeId) แล้วกด
|
|
// "เปิดเอกสาร" จะพาไป /annotate?fileUrl=&fileId=&typeId= ซึ่งลงทะเบียนเอกสาร
|
|
// (metadata-only) แล้ว redirect เข้าหน้า viewer ให้เอง — ใช้ทดสอบในเครื่องก่อนมี
|
|
// ลิงก์จริงจาก e-Court (รับทั้ง path ในระบบเช่น /samples/… และ URL ของโฮสต์ที่อนุญาต
|
|
// — ไม่มี scheme นำหน้าระบบเติม https:// ให้)
|
|
// แถบลิงก์เต็มข้างล่างแสดง URL จริงที่ปุ่มจะพาไป (absolute — copy ไปเปิด/ส่งต่อได้) ตามที่พิมพ์สด ๆ
|
|
// Genesis: การ์ด surface 12px radius ขอบ line ใน section "เปิดเอกสารจากลิงก์ e-Court"
|
|
// (แยกจากส่วนเอกสาร) / ปุ่มเป็น secondary ขอบฟ้า / input ผ่าน .genesis-input
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
|
|
import { withBasePath } from "@/utils/base-path";
|
|
|
|
const inputClass =
|
|
"genesis-input mt-1 w-full text-base transition aria-invalid:border-error";
|
|
|
|
export default function EcourtLinkForm() {
|
|
const router = useRouter();
|
|
const [fileUrl, setFileUrl] = useState("");
|
|
const [fileId, setFileId] = useState("");
|
|
const [typeId, setTypeId] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
// origin เอาตอน client mount เท่านั้น — SSR ไม่มี window (กัน hydration mismatch)
|
|
const [origin, setOrigin] = useState("");
|
|
|
|
useEffect(() => {
|
|
setOrigin(window.location.origin);
|
|
}, []);
|
|
|
|
// query ชุดเดียวกันทั้ง preview และตอนกดปุ่ม — สิ่งที่เห็นคือสิ่งที่ถูกเปิดเป๊ะ
|
|
const params = new URLSearchParams();
|
|
if (fileUrl.trim()) params.set("fileUrl", fileUrl.trim());
|
|
if (fileId.trim()) params.set("fileId", fileId.trim());
|
|
if (typeId.trim()) params.set("typeId", typeId.trim());
|
|
const queryString = params.toString();
|
|
const fullLink = origin
|
|
? `${origin}${withBasePath("/annotate")}${queryString ? `?${queryString}` : ""}`
|
|
: "";
|
|
|
|
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
if (!fileUrl.trim()) {
|
|
setError("กรอกลิงก์ไฟล์ก่อน");
|
|
return;
|
|
}
|
|
setError(null);
|
|
// router.push เติม basePath ให้เอง (invariant #12 — ต่างจาก fetch/plain href)
|
|
router.push(`/note/annotate?${queryString}`);
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={onSubmit} className="flex flex-col rounded-xl border border-line bg-white p-5">
|
|
{/* ไม่มีหัวการ์ดซ้ำ — h2 "เปิดเอกสารจากลิงก์ e-Court" ของ section ในหน้ารายการ
|
|
เป็นหัวเดียวพอแล้ว (responsive audit 2026-09-24: เดิมซ้ำสองชั้นติดกัน) */}
|
|
<div className="grid gap-x-4 gap-y-3 sm:grid-cols-2">
|
|
<div className="sm:col-span-2">
|
|
<label htmlFor="ecourt-file-url" className="block text-sm font-medium text-ink">
|
|
ลิงก์ไฟล์
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="ecourt-file-url"
|
|
name="fileUrl"
|
|
required
|
|
value={fileUrl}
|
|
onChange={(e) => setFileUrl(e.target.value)}
|
|
placeholder="เช่น /samples/ชื่อไฟล์.pdf หรือ https://…"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="ecourt-file-id" className="block text-sm font-medium text-ink">
|
|
รหัสเอกสาร fileId
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="ecourt-file-id"
|
|
name="fileId"
|
|
value={fileId}
|
|
onChange={(e) => setFileId(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="ecourt-type-id" className="block text-sm font-medium text-ink">
|
|
กลุ่ม typeId
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="ecourt-type-id"
|
|
name="typeId"
|
|
value={typeId}
|
|
onChange={(e) => setTypeId(e.target.value)}
|
|
placeholder="เช่น 1"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
className="focus-ring mt-4 w-full rounded-md border border-primary/30 px-4 py-2 text-sm font-medium text-primary transition hover:-translate-y-px hover:border-primary hover:bg-primary/5"
|
|
>
|
|
เปิดเอกสาร
|
|
</button>
|
|
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
|
|
กดแล้วระบบลงทะเบียนเอกสาร (ถ้ายังไม่มี) แล้วพาเข้าหน้าอ่าน/มาร์กเอกสารทันที —
|
|
ไฟล์รีโมตรับเฉพาะ https ของโฮสต์ที่อนุญาต
|
|
</p>
|
|
<p className="mt-3 break-all rounded-lg bg-ink/[0.04] px-3 py-2.5 text-xs text-ink-soft">
|
|
<span className="font-medium text-ink">ลิงก์เต็ม: </span>
|
|
{fullLink || "…"}
|
|
</p>
|
|
{error && <p className="mt-2 text-xs text-error-ink">{error}</p>}
|
|
</form>
|
|
);
|
|
}
|