"use client"; import { useState, type ChangeEvent, type FormEvent } from "react"; import { withBasePath } from "@/utils/base-path"; /** * ฟอร์ม "จำลอง e-Court — ส่งเนื้อไฟล์เข้า staging" บนหน้า /note/dev (N11 — * PLAN-STAGED.md · คำขอผู้ใช้ 2026-11-04): กดทดสอบ contract ทั้ง 3 ขั้นตามที่ * backend e-Court ต้องทำจริงในหน้าเดียว — (1) POST /api/note/files/stage * (base64 ของไฟล์ที่เลือก) → (2) POST /api/auth/token แบบ purpose:"login" * ด้วย fid เดิม (ทางเข้ารวม 2026-11-09) → * (3) ลิงก์ /note?t=… (fid-only ไม่มี fileUrl — D5) * · ต้องใส่ DOCVER_API_KEY จริงทุกครั้งจึงไม่เป็นประตูหลัง (เหมือน MockEcourtForm) * · หน้า /note/dev เปิดด้วย env NOTE_SSO_DEV_MINT=1 อย่างเดียว * · ไม่ใช่ dropzone ของแอป (การตัดสินใจข้อ 3 ของแผนยังอยู่ครบ — เครื่องมือ dev * สำหรับผู้ถือ key เท่านั้น) */ export default function MockStageForm() { const [apiKey, setApiKey] = useState(""); const [fid, setFid] = useState("STAGE-1"); const [userid, setUserid] = useState("oom"); const [username, setUsername] = useState("สรวิชญ์"); const [title, setTitle] = useState(""); const [typeId, setTypeId] = useState(""); const [file, setFile] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState<{ fileId: string; link: string } | null>(null); const onFileChange = (e: ChangeEvent) => { setFile(e.target.files?.[0] ?? null); setResult(null); }; const onSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); setResult(null); if (file === null) { setError("เลือกไฟล์เอกสารก่อน (.pdf / .jpg / .png / .tif)"); return; } if (file.size > 100 * 1024 * 1024) { setError(`ไฟล์ ${file.name} ใหญ่เกิน 100MB (เนื้อไฟล์) — endpoint จะตอบ 413`); return; } setBusy(true); try { // base64 มาตรฐานต่อเนื่อง — readAsDataURL ได้ base64 ต่อเนื่องพร้อม padding // อยู่แล้ว (ตัด prefix data:…;base64,) และไฟล์ใหญ่ก็ไม่ชน call stack แบบ btoa const b64 = await readFileAsBase64(file); // (1) stage เนื้อไฟล์ — ขั้นที่ e-Court ต้องทำก่อนแสดงปุ่มเปิดเอกสาร const stageRes = await fetch(withBasePath("/api/note/files/stage"), { method: "POST", headers: { "x-api-key": apiKey, "content-type": "application/json" }, body: JSON.stringify({ fid: fid.trim(), data: b64, ...(title.trim() !== "" ? { title: title.trim() } : {}), ...(typeId.trim() !== "" ? { typeId: typeId.trim() } : {}), }), }); const stageData = (await stageRes.json().catch(() => null)) as | { fileId?: string; error?: string } | null; if (!stageRes.ok || stageData?.fileId === undefined) { throw new Error( `stage ไม่สำเร็จ (HTTP ${stageRes.status}) — ${stageData?.error ?? "ไม่ทราบสาเหตุ"}`, ); } // (2) mint login token ด้วย fid **เดิม** — dedupe key ecourt: ผูกกัน // (purpose:"login" = ทางเข้า mint login-token ของ /api/auth/token) const tokenRes = await fetch(withBasePath("/api/auth/token"), { method: "POST", headers: { "x-api-key": apiKey, "content-type": "application/json" }, body: JSON.stringify({ userid: userid.trim(), username: username.trim(), fid: fid.trim(), ttl_seconds: 600, purpose: "login", }), }); const tokenData = (await tokenRes.json().catch(() => null)) as | { access_token?: string; error?: string } | null; if (!tokenRes.ok || tokenData?.access_token === undefined) { throw new Error( `stage สำเร็จ แต่ขอ token ไม่สำเร็จ (HTTP ${tokenRes.status}) — ${tokenData?.error ?? "ไม่ทราบสาเหตุ"}`, ); } // (3) ลิงก์ fid-only (ไม่มี fileUrl — เอกสาร staging) · withBasePath("") = // /docedit/note (D11 — ห้ามส่ง "/note" เข้า withBasePath) setResult({ fileId: stageData.fileId, link: `${withBasePath("")}?t=${encodeURIComponent(tokenData.access_token)}`, }); } catch (err) { setError(err instanceof Error ? err.message : "ทดสอบ staging ไม่สำเร็จ"); } finally { setBusy(false); } }; const inputCls = "genesis-input h-9 px-3 py-1.5 disabled:bg-canvas disabled:text-ink-faint"; const labelCls = "text-xs font-medium text-ink-soft"; return (
setApiKey(e.target.value)} className={inputCls} autoComplete="off" required />
setFid(e.target.value)} className={inputCls} required />
setUserid(e.target.value)} className={inputCls} required />
setUsername(e.target.value)} className={inputCls} required />
setTitle(e.target.value)} className={inputCls} placeholder={file?.name ?? "เอกสารจาก e-Court"} />
setTypeId(e.target.value)} className={inputCls} />
{file !== null && ( {file.name} · {(file.size / 1024 / 1024).toFixed(2)} MB (base64 ~ {((file.size * 1.34) / 1024 / 1024).toFixed(0)} MB) )}
{error && (

{error}

)} {result && (

stage สำเร็จ — fileId {result.fileId}{" "} (ลิงก์อายุ 10 นาที · เอกสารอยู่ใน RAM ตาม TTL หลังแลก session)

เปิดลิงก์ {result.link.slice(0, 80)}…
)}
); } /** base64 ต่อเนื่องของไฟล์ทั้งก้อน */ function readFileAsBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const result = String(reader.result ?? ""); const comma = result.indexOf(","); if (!result.startsWith("data:") || comma < 0) { reject(new Error("อ่านไฟล์ไม่สำเร็จ")); return; } resolve(result.slice(comma + 1)); }; reader.onerror = () => reject(new Error("อ่านไฟล์ไม่สำเร็จ")); reader.readAsDataURL(file); }); }