"use client"; // หน้าแรก /note — ข้อมูลเกี่ยวกับระบบ (2026-10-30 ตามคำขอผู้ใช้: ระบบทำอะไรได้ / // วิธีเข้าใช้) + bootstrap ของลิงก์ e-Court (contract ปลายทางลิงก์ยังชี้ที่นี่): // มี credential ใน URL (t หรือ ticket_data+HmacSha256 — จากลิงก์ที่ e-Court ชี้ตรงมา // ที่ /note) = แลกเป็น access_token ที่ POST /api/note/auth/session แล้วเก็บ // localStorage "note-auth" (แบบ add-in) → ตัด credential ออกจาก URL ทันที → มี fileId // = พาเข้า viewer / ไม่มี = แสดงหน้าข้อมูล · ไม่มี credential = หน้า public ธรรมดา // (แบนเนอร์ ?sso= ของลิงก์เก่า + fileError ยังรองรับ) import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { Bookmark, ExternalLink, FileText, Highlighter, MessagesSquare, } from "lucide-react"; import AppNav from "@/components/AppNav"; import { parseSsoLinkParams, ssoBanner, type SsoBanner } from "@/lib/note-auth"; import { readNoteSession, saveNoteSession, type NoteSession } from "@/lib/note-session"; import { withBasePath } from "@/utils/base-path"; type Phase = "boot" | "exchanging" | "ready"; type SessionResponse = { access_token?: string; token_type?: string; expires_in?: number; userid?: string; username?: string; fileId?: string; fileError?: string; reason?: string; }; export default function NoteHome() { const router = useRouter(); const [phase, setPhase] = useState("boot"); const [banner, setBanner] = useState(null); const [fileError, setFileError] = useState(null); const [session, setSession] = useState(null); // StrictMode dev รัน effect สองครั้ง — bootstrap ต้องเกิดครั้งเดียวจริง ๆ const bootedRef = useRef(false); useEffect(() => { if (bootedRef.current) return; bootedRef.current = true; const url = new URL(window.location.href); const stripParams = () => { url.search = ""; window.history.replaceState(null, "", url.toString()); }; const fail = (reason: "expired" | "invalid" | "error") => { setBanner(ssoBanner(reason)); setSession(readNoteSession()); setPhase("ready"); }; const parsed = parseSsoLinkParams(url); const hasCredentialKey = url.searchParams.get("t") !== null || url.searchParams.get("ticket_data") !== null || url.searchParams.get("HmacSha256") !== null; if (parsed.ok) { setPhase("exchanging"); const body: Record = {}; if (parsed.credential.kind === "jwt") { body.t = parsed.credential.token; } else { body.ticket_data = parsed.credential.ticketData; body.HmacSha256 = parsed.credential.hmacHex; } if (parsed.fileUrl !== undefined) body.fileUrl = parsed.fileUrl; if (parsed.typeId !== undefined) body.typeId = parsed.typeId; // credential ต้องออกจาก URL ทันทีที่รู้ผล — แต่ strip ก่อนยิงไม่ได้ (reload // กลางคัน = ลิงก์ตาย) จึงกันค้างด้วย timeout: เครือข่ายนิ่งเกิน 45 วิ = ตัดจบ const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), 45_000); void (async () => { try { const res = await fetch(withBasePath("/api/note/auth/session"), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: controller.signal, }); const data = (await res.json().catch(() => null)) as SessionResponse | null; stripParams(); if ( !res.ok || data === null || data.access_token === undefined || data.userid === undefined || data.username === undefined || data.expires_in === undefined ) { fail( res.status === 401 && data?.reason === "expired" ? "expired" : res.status === 401 ? "invalid" : "error", ); return; } const { access_token, userid, username, expires_in, fileId, fileError } = data; saveNoteSession({ token: access_token, userid, username, expMs: Date.now() + expires_in * 1000, }); if (fileError !== undefined) setFileError(fileError); if (fileId !== undefined) { // เข้า viewer เลย — คงสถานะ exchanging ให้ spinner อยู่จนหน้า viewer มาแทน router.replace(`/note/annotate/${fileId}`); return; } setSession(readNoteSession()); setPhase("ready"); } catch { stripParams(); fail("error"); } finally { window.clearTimeout(timeoutId); } })(); return; } if (hasCredentialKey) { // มี credential ใน URL แต่รูปไม่ผ่าน (ticket ครึ่งเดียว/ยาวเกิน cap/สองชุดพร้อม // กัน) — ตัดทิ้ง + แจ้งผู้ใช้ ไม่ปล่อยค่าเน่าค้างอยู่ใน history (D11) stripParams(); fail("invalid"); return; } // ไม่มี credential — หน้าข้อมูลระบบปกติ (แบนเนอร์ ?sso= ของลิงก์เก่า · // fileError จาก GET /api/note/files/open ตอนเปิดไฟล์ตัวอย่างไม่สำเร็จ) setBanner(ssoBanner(url.searchParams.get("sso") ?? undefined)); const openFileError = url.searchParams.get("fileError"); if (openFileError !== null && openFileError !== "") setFileError(openFileError); setSession(readNoteSession()); setPhase("ready"); }, [router]); return ( <>
{phase !== "ready" ? (

{phase === "exchanging" ? "กำลังเข้าสู่ระบบผ่านลิงก์ของ e-Court…" : "กำลังโหลด…"}

) : ( <> {/* ชื่อระบบ Latin ล้วน → General Sans tracking แน่นตาม Genesis display type */}

Judge's Note

ระบบอ่านและทำเครื่องหมายเอกสารคดีของ e-Court — มาร์กข้อความ วาดเส้น ใส่ความเห็น คั่นหน้า และสรุปหมายเหตุทั้งคดีได้ในที่เดียว โดยไฟล์ต้นฉบับไม่ถูกแก้ไข

{banner && (
{banner.text}
)} {fileError && (
เปิดเอกสารจากลิงก์ไม่สำเร็จ: {fileError}
)} {/* ── ระบบนี้ทำอะไรได้ ────────────────────────────────────────── */}

ระบบนี้ทำอะไรได้

{[ { icon: Highlighter, title: "มาร์กและวาดบนเอกสาร", detail: "มาร์กข้อความไฮไลต์ได้หลายแบบ วาดเส้นด้วยปากกา วาดรูปทรง " + "ใส่กล่องข้อความและตรายาง รวมถึงปิดบังข้อมูลที่ละเอียดอ่อน " + "— ย้าย แก้ไข ย่อ-ขยายได้หลังวาง พร้อมปุ่มเรียกคืน (undo/redo)", }, { icon: MessagesSquare, title: "ความเห็นและทำงานร่วมกัน", detail: "หมุดความเห็นพร้อมตอบกลับเป็นเธรด ทำเครื่องหมาย “จัดการแล้ว” " + "เมื่อเสร็จ และเห็นการเปลี่ยนแปลงของทุกคนแบบสด ๆ " + "ในไฟล์เดียวกันได้ทันที", }, { icon: Bookmark, title: "คั่นหน้าและสรุปคดี", detail: "คั่นหน้าเป็นหัวข้อซ้อนกันได้ 3 ระดับพร้อมโน้ตย่อย ค้นหาทั้งไฟล์ " + "แล้วดูรวมทั้งหมดที่หน้า “สรุปหมายเหตุ” — พิมพ์ออกมาเป็น " + "เอกสารสรุปของคดีได้", }, { icon: FileText, title: "เอกสารจาก e-Court", detail: "เปิด PDF และรูปภาพ (รวมไฟล์ TIFF หลายหน้า) จากลิงก์ของ e-Court " + "จัดกลุ่มตามคดี และส่งออกเป็น PDF ที่ฝังมาร์กทุกอย่างไว้ในตัว " + "— ไฟล์ต้นฉบับต้นทางไม่ถูกแตะต้อง", }, ].map(({ icon: Icon, title, detail }) => (

{title}

{detail}

))}
{/* ── วิธีเข้าใช้งาน ─────────────────────────────────────────── */}

วิธีเข้าใช้งาน

เข้าใช้งานผ่านลิงก์ของ e-Court

ระบบนี้ไม่มีหน้าล็อกอิน — เปิดเอกสารจากระบบ e-Court แล้วระบบพาเข้าหน้าอ่านเอกสารโดยอัตโนมัติ ใช้งานต่อได้ 8 ชม. ต่อการเข้าหนึ่งครั้ง

ผู้พัฒนา/ผู้ทดสอบ:{" "} เครื่องมือทดสอบทางเข้าและเอกสารตัวอย่าง →

)}
); }