bookmark: ดักความยาวชื่อ/โน้ตที่ช่องกรอก + F19 สรุปสิ่งที่เปลี่ยนใน dialog ประวัติ + formatThaiDate — (1) bookmark limits: utils/bookmark-limits.ts ใหม่เป็น source of truth เดียว (ชื่อ 120/โน้ต 200 ตัวอักษร) server action import ร่วมแทน const ท้องถิ่น + ใส่ maxLength ที่ popup บันทึก bookmark (PDFViewer) และฟอร์มแก้ไข ✏ ใน sidebar ดักทั้งพิมพ์/paste ตั้งแต่ในช่อง ไม่รอ error ตอนกดบันทึก + แก้ off-by-one ของ normalizeBookmarkTitle (เดิม slice 120 แล้วต่อ … = 121 ตัว ทำให้คลุมข้อความยาวแล้วกด B บันทึกเลยโดน limit ของ server ตีกลับทั้งที่ไม่ได้พิมพ์เอง — ตอนนี้ slice 119 + … = 120 พอดีตามสเปก §7 N2 ที่เขียนไว้ ≤ 120 อยู่แล้ว) + PLAN-FEAT.md §7 N2 บันทึกเพิ่ม — (2) งานคู่ขนาดจากอีก session (แชร์ไฟล์ PDFViewer/sidebar เลยรวม commit): dialog ประวัติ F19 แสดงสรุปสิ่งที่เปลี่ยนต่อรอบผ่าน describeAnnotationChange ใหม่ใน utils/annotation-display.ts (เทียบ snapshot ก่อนแก้ v.data กับ state ถัดไป — snapshot รอบใหม่กว่า หรือค่าปัจจุบันจาก realtime lookup สด historyCurrent — สรุปเป็นหน่วยเดียวกับตัวปรับ UI: ความหนา ×10 ความทึบ % fontSize px ชื่อสี preset ย้าย/ปรับขนาดจาก bbox ผลต่างเกิน GEOM_EPS_PCT 0.5% ข้อความ/โน้ต/label) + dialog ขยาย w-96 max-h-[60vh] แถวเปลี่ยน items-start + utils/date-format.ts ใหม่ formatThaiDate วันที่ไทยเต็ม "18 กันยายน พ.ศ. 2569" (+เวลา) แทน toLocaleString("th-TH") ทุกจุดแสดงวันที่ใน UI (หน้ารายการไฟล์ sidebar ประวัติ/รายการ timestamps ใน PDFViewer) กัน server/client locale ไม่ตรงกัน + tests ใหม่/ขยาย (date-format + describeAnnotationChange) — CLAUDE.md sync โครงสร้าง utils — verify: tsc strict ผ่าน + vitest 139/139 ผ่าน
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 43s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 3s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-17 17:02:31 +07:00
parent 8ba911a119
commit e0c78d68fe
11 changed files with 469 additions and 38 deletions

View file

@ -1,5 +1,14 @@
import { describe, expect, it } from "vitest";
import type { AnnotationDTO, AnnotationData, AnnotationType } from "@/db/schema";
import type {
AnnotationDTO,
AnnotationData,
AnnotationType,
CommentData,
DrawData,
HighlightData,
StampData,
TextboxData,
} from "@/db/schema";
import {
DRAW_PENS,
STAMP_FONT_RATIO,
@ -8,6 +17,7 @@ import {
annotationNote,
annotationPreviewText,
annotationTypeLabel,
describeAnnotationChange,
effectiveDrawOpacity,
sanitizePdfText,
stampFitFontSize,
@ -138,6 +148,94 @@ describe("stampFitFontSize — ย่อฟอนต์ตรายางให
});
describe("describeAnnotationChange — สรุปสิ่งที่เปลี่ยนของ version history (F19)", () => {
it("HIGHLIGHT เปลี่ยนสี/ความทึบ → ชื่อสีไทยจาก palette + % ตามหน่วย UI", () => {
const prev: HighlightData = { text: "ข้อ 3", color: "#ef4444", rects: [], opacity: 0.5 };
const next: HighlightData = { text: "ข้อ 3", color: "#3b82f6", rects: [], opacity: 1 };
expect(describeAnnotationChange("HIGHLIGHT", prev, next)).toEqual([
"สี: แดง → น้ำเงิน",
"ความทึบ: 50% → 100%",
]);
});
it("สีนอก palette (ปุ่มสีรุ้ง) → แสดง hex ตรง ๆ", () => {
const prev: HighlightData = { text: "a", color: "#123456", rects: [] };
const next: HighlightData = { text: "a", color: "#abcdef", rects: [] };
expect(describeAnnotationChange("HIGHLIGHT", prev, next)).toEqual(["สี: #123456 → #abcdef"]);
});
it("COMMENT แก้ข้อความ + ย้าย pin", () => {
const prev: CommentData = { position: { x: 10, y: 20 }, message: "ดูจุดนี้" };
const next: CommentData = { position: { x: 40, y: 20 }, message: "ดูจุดนี้ (แก้)" };
expect(describeAnnotationChange("COMMENT", prev, next)).toEqual([
'แก้ข้อความ: "ดูจุดนี้" → "ดูจุดนี้ (แก้)"',
"ย้ายตำแหน่ง",
]);
});
it("DRAW ย้ายทั้งเส้น → ย้ายตำแหน่ง / eraser ตัดจุดโดย bbox เดิม → แก้เส้น", () => {
const prev: DrawData = {
strokeColor: "#ef4444",
strokeWidth: 0.4,
paths: [
{ x: 10, y: 10 },
{ x: 20, y: 10 },
{ x: 30, y: 10 },
{ x: 40, y: 10 },
],
};
const moved: DrawData = { ...prev, paths: prev.paths.map((p) => ({ ...p, y: p.y + 5 })) };
expect(describeAnnotationChange("DRAW", prev, moved)).toEqual(["ย้ายตำแหน่ง"]);
const erased: DrawData = {
...prev,
paths: [prev.paths[0], prev.paths[1], prev.paths[3]], // ตัดจุดกลางทิ้ง bbox คงเดิม
};
expect(describeAnnotationChange("DRAW", prev, erased)).toEqual(["แก้เส้น"]);
});
it("DRAW/SHAPE เปลี่ยนความหนา → แสดง ×10 ตามหน่วยของตัวปรับใน UI", () => {
const prev: DrawData = { strokeColor: "#ef4444", strokeWidth: 0.4, paths: [{ x: 1, y: 1 }] };
const next: DrawData = { ...prev, strokeWidth: 0.8 };
expect(describeAnnotationChange("DRAW", prev, next)).toEqual(["ความหนา: 4 → 8"]);
});
it("โน้ต N1: เพิ่ม / ลบ / แก้", () => {
const base: DrawData = { strokeColor: "#ef4444", strokeWidth: 0.4, paths: [] };
expect(describeAnnotationChange("DRAW", base, { ...base, note: "เหตุผล" })).toEqual([
"เพิ่มโน้ต: เหตุผล",
]);
expect(describeAnnotationChange("DRAW", { ...base, note: "เหตุผล" }, base)).toEqual(["ลบโน้ต"]);
expect(
describeAnnotationChange("DRAW", { ...base, note: "เหตุผล" }, { ...base, note: "เหตุผลใหม่" }),
).toEqual(["แก้โน้ต: เหตุผล → เหตุผลใหม่"]);
});
it("ไม่มีอะไรเปลี่ยน → ว่าง", () => {
const same: CommentData = { position: { x: 5, y: 5 }, message: "m" };
expect(describeAnnotationChange("COMMENT", same, { ...same })).toEqual([]);
});
it("TEXTBOX แก้ข้อความแล้วกล่อง wrap ใหม่ → ไม่แจ้ง ปรับขนาด ที่เป็นผลพลอง", () => {
const prev: TextboxData = {
position: { x: 10, y: 10 },
text: "สั้น",
color: "#111827",
fontSize: 2.5,
};
const next: TextboxData = { ...prev, text: "ข้อความที่ยาวขึ้นมาก" };
const changes = describeAnnotationChange("TEXTBOX", prev, next);
expect(changes).toContain('แก้ข้อความ: "สั้น" → "ข้อความที่ยาวขึ้นมาก"');
expect(changes).not.toContain("ปรับขนาด");
});
it("STAMP เปลี่ยนสีไม่ระบุ (undefined) ต้องเทียบกับ default STAMP_COLOR ก่อน", () => {
const prev: StampData = { label: "สำคัญ", position: { x: 10, y: 10 }, scale: 18 };
const next: StampData = { ...prev, color: "#b91c1c" }; // = default ไม่ถือว่าเปลี่ยน
expect(describeAnnotationChange("STAMP", prev, next)).toEqual([]);
});
});
describe("sanitizePdfText — ล้างข้อความเพี้ยนจาก textLayer ของ PDF ไทย (glyph ต่อ glyph)", () => {
// control char ขยะที่พบจริงใน textLayer ของคำสั่งศาลปกครอง เช่น 04/05/0e/0f/03/12/1b
const ctrl = (n: number) => String.fromCharCode(n);

View file

@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { formatThaiDate } from "@/utils/date-format";
// สร้าง Date จาก local components แล้ววนผ่าน ISO กลับ — ผล test ไม่ขึ้นกับ
// timezone ของเครื่องที่รัน (เว้นช่วง DST transition ทับพอดี ซึ่งไทยไม่มี)
describe("formatThaiDate — วันที่ไทยแบบเต็ม พ.ศ. ตามธรรมเนียมเอกสารราชการ", () => {
it("ไม่มีเวลา → 18 กันยายน พ.ศ. 2569", () => {
const d = new Date(2026, 8, 18, 16, 33, 0);
expect(formatThaiDate(d.toISOString())).toBe("18 กันยายน พ.ศ. 2569");
});
it("มีเวลา → 18 กันยายน พ.ศ. 2569 16:33:00 น.", () => {
const d = new Date(2026, 8, 18, 16, 33, 0);
expect(formatThaiDate(d.toISOString(), true)).toBe("18 กันยายน พ.ศ. 2569 16:33:00 น.");
});
it("วัน/ชั่วโมง/นาที/วินาทีหลักเดียวไม่มีศูนย์นำหน้าที่วัน + ศูนย์นำหน้าที่เวลา", () => {
const d = new Date(2026, 10, 5, 9, 5, 3);
expect(formatThaiDate(d.toISOString(), true)).toBe("5 พฤศจิกายน พ.ศ. 2569 09:05:03 น.");
});
it("29 กุมภาพันธ์ปีอธิกสุรทิน (2028 → พ.ศ. 2571)", () => {
const d = new Date(2028, 1, 29, 23, 59, 59);
expect(formatThaiDate(d.toISOString(), true)).toBe("29 กุมภาพันธ์ พ.ศ. 2571 23:59:59 น.");
});
});