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>
88 lines
4.3 KiB
TypeScript
88 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
// N4 — ฟอร์มอัปโหลดไฟล์เอกสารแบบง่ายบนหน้ารายการ (PDF/รูป รับพร้อมกัน)
|
|
// เสร็จแล้วรายการโผล่เองจาก revalidatePath("/note") ของ action
|
|
// Genesis: หน้าตาเป็นการ์ด surface 12px radius ขอบ line (ใต้หัวข้อของ section
|
|
// "เอกสาร" — ไฟล์ที่อัปโหลดเข้ารายการนั้น) + ปุ่ม primary เดียวของ section
|
|
import { useState, useTransition } from "react";
|
|
import { Upload } from "lucide-react";
|
|
|
|
import { uploadFile } from "@/app/actions/files";
|
|
|
|
const ACCEPT = ".pdf,.jpg,.jpeg,.png,.tif,.tiff";
|
|
|
|
/** ขนาดสูงสุดตาม validator ของ uploadFile (app/actions/files.ts) — เช็คฝั่ง client
|
|
* ก่อนอัปโหลด (M5, UX audit 2026-09-16): ไฟล์สแกนคดีที่เกินไม่ต้องรอส่งจนจบ
|
|
* บนเน็ตช้าก่อนเจอ error ของ server */
|
|
const MAX_BYTES = 100 * 1024 * 1024;
|
|
|
|
export default function UploadFileForm() {
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [pending, startTransition] = useTransition();
|
|
|
|
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
const form = e.currentTarget;
|
|
setError(null);
|
|
const input = form.elements.namedItem("file") as HTMLInputElement | null;
|
|
const file = input?.files?.[0];
|
|
if (file && file.size > MAX_BYTES) {
|
|
setError("ไฟล์ใหญ่เกิน 100MB — เลือกไฟล์เล็กกว่า");
|
|
return;
|
|
}
|
|
startTransition(async () => {
|
|
try {
|
|
const result = await uploadFile(new FormData(form));
|
|
if (result.ok) {
|
|
form.reset();
|
|
} else {
|
|
setError(result.error);
|
|
}
|
|
} catch {
|
|
// action fail ที่ระดับ transport (เช่น body ใหญ่เกิน bodySizeLimit ของ next.config)
|
|
setError("อัปโหลดไม่สำเร็จ — ลองใหม่อีกครั้ง");
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={onSubmit} className="flex flex-col rounded-xl border border-line bg-white p-5">
|
|
<div className="flex items-center gap-2">
|
|
<Upload aria-hidden className="h-4 w-4 shrink-0 text-ink-faint" />
|
|
<p className="text-sm font-semibold text-ink">อัปโหลดไฟล์</p>
|
|
</div>
|
|
<input
|
|
type="file"
|
|
name="file"
|
|
accept={ACCEPT}
|
|
required
|
|
disabled={pending}
|
|
className="mt-4 block w-full text-sm text-ink-soft file:mr-3 file:cursor-pointer file:rounded-md file:border-0 file:bg-ink/[0.05] file:px-4 file:py-2 file:text-sm file:font-medium file:text-ink"
|
|
/>
|
|
{/* กลุ่ม typeId ทางเลือก (N9) — ช่องเดียวกับลิงก์ e-Court, ไม่กรอก = "ไม่มีกลุ่ม";
|
|
uncontrolled ส่งไปกับ FormData เหมือนช่องไฟล์ (normalize ที่ action) */}
|
|
<label htmlFor="upload-type-id" className="mt-3 block text-sm font-medium text-ink">
|
|
กลุ่ม typeId
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="upload-type-id"
|
|
name="typeId"
|
|
disabled={pending}
|
|
placeholder="ไม่กรอก = ไม่มีกลุ่ม"
|
|
className="genesis-input mt-1 w-full text-base"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={pending}
|
|
className="focus-ring mt-4 w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-white transition hover:-translate-y-px hover:bg-primary-hover hover:shadow-btn-glow disabled:pointer-events-none disabled:opacity-50"
|
|
>
|
|
{pending ? "กำลังอัปโหลด…" : "อัปโหลด"}
|
|
</button>
|
|
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
|
|
รองรับ PDF, JPG, PNG, TIFF — ไม่เกิน 100MB ต่อไฟล์
|
|
</p>
|
|
{error && <p className="mt-2 text-xs text-error-ink">{error}</p>}
|
|
</form>
|
|
);
|
|
}
|