diff --git a/.claude/rules/data-layer.md b/.claude/rules/data-layer.md index ed1ae2f..efb2fd2 100644 --- a/.claude/rules/data-layer.md +++ b/.claude/rules/data-layer.md @@ -6,7 +6,7 @@ paths: # Data Layer Rules (รายละเอียด: docs/PLAN.md §3, §4) -- โครงสร้างตาราง/enum/JSONB union อ้าง §3 — หลังแก้ `db/schema.ts` ต้องรัน `/sync-db` **และ `npx drizzle-kit generate` ทุกครั้ง**: dev sync ด้วย push แต่ **image Docker รัน `scripts/migrate.mjs` จากไฟล์ SQL ใน `drizzle/` เท่านั้น** — ลืม generate = schema ฝั่ง server ตกรุ่นจน query พัง (เคยหลุด N6 `bookmarks.user_name` → หน้า annotate 500 ทั้งหน้าบน Docker) ; ไฟล์ที่ generated ควร idempotent (`IF NOT EXISTS`) เพราะ migrate.mjs ถือ error already-exists ทั้งไฟล์ว่า applied หมด +- โครงสร้างตาราง/enum/JSONB union อ้าง §3 — หลังแก้ `db/schema.ts` ต้องรัน `/sync-db` - Enum/union ขยายตาม PLAN-FEAT.md §1 ทีละ task — ตอนนี้เพิ่มแล้ว: `SHAPE` + `ShapeData` (rect/ellipse เก็บ `rect` bbox, line/arrow เก็บ `from`/`to` ตาม D3), `REDACTION` + `RedactionData` (`{ rect }` — ดำทึบ ไม่มีสีให้เปลี่ยน), `TEXTBOX` + `TextboxData` (F17 — `{ position, text, color, fontSize, bold?, opacity?, align?, segments?, width? }` position เป็น**มุมซ้ายบนของกล่องเสมอ** (align จัดข้อความในกล่องอย่างเดียวแบบ Smallpdf — กล่องไม่ขยับ), fontSize = % ของ width; bold/opacity(0–1)/align('left'|'center'|'right')/width (กล่องกว้างคงที่ % ของ width — สไตล์ Smallpdf: มี = กล่องกว้างคงที่ลากขอบปรับได้, ไม่มี = shrink-to-fit แถวเดิม) เป็น optional ไม่มี = ตัวปกติ/ทึบเต็ม/ชิดซ้าย/หุ้มข้อความ — bold ตอน export ต้องมี `public/fonts/Sarabun-Bold.ttf` ไม่งั้น fallback เป็น Regular; `segments?` = ตัวหนาเฉพาะส่วน (rich text — editor เป็น contentEditable + Ctrl/Cmd+B, เก็บเฉพาะเมื่อมี bold run) โดย field `text` เก็บข้อความรวม (concat) เสมอเพื่อค้นหา F14/preview sidebar และ export ใช้ `wrapStyledText` วาดแบบ run ผสม), `STAMP` + `StampData` (F18 — `{ label, position, scale, color? }` scale = ความกว้างกรอบเป็น % ของ width, color optional ไม่มี = `STAMP_COLOR`), `DrawData.widths?` (F7 pressure, optional ยาวเท่า paths; `Point` define ที่ `db/schema.ts` แล้ว re-export จาก `utils/coordinates.ts`), `DrawData.pen?` ('pencil' | 'highlighter' — optional ไม่มี = pencil; ค่า preset ต่อ pen อยู่ที่ `DRAW_PENS` ใน `utils/annotation-display.ts`), `DrawData.opacity?` (0–1 optional — เส้นใหม่เก็บชัดเสมอ แถวเดิม fallback ตาม preset ของ pen ผ่าน `effectiveDrawOpacity`), `HighlightData.style?` ('highlight' | 'underline' | 'strikethrough' | 'squiggle' — optional ไม่มี = ไฮไลต์ทึบตามเดิม; เลือกจาก popup หลังคลุมดำ), `HighlightData.opacity?` (0–1 optional ไม่มี = ทึบเต็ม — ตั้งจาก dropdown ของ tool highlight ตอนสร้าง + แผง selection แก้ได้หลังสร้าง; export: ไฮไลต์ทึบคูณเข้าค่าเดิม 0.35 ส่วนเส้นทุกแบบใช้ตรง ๆ) — เพิ่ม type ใหม่ทุกครั้งต้องเติม case ใน `validateAnnotationData` ครบถ้วน (invariant #8) - column `data` ใช้ `$type()` (discriminated union ตาม enum `type`) — ห้าม `any` หรือ object type กว้าง — `note?: string` (โน้ตคำอธิบายของรายการ เช่น เหตุผลที่วาด — PLAN-FEAT.md §7 N1) มีในทุกชนิด**ยกเว้น COMMENT** (มี `message` ทำหน้าที่นี้อยู่แล้ว) — validator `vNote` trim แล้ว ค่าว่าง = ไม่มีโน้ต; อ่านค่าผ่าน `annotationNote()` ใน `utils/annotation-display.ts` เท่านั้น (sidebar/สรุป/CSV ใช้ร่วม) - DTO (`AnnotationDTO`, `BookmarkDTO`): timestamp เป็น **ISO string** เสมอ ผ่าน mapper `toDTO` — ห้ามส่ง `Date` object ข้าม server→client diff --git a/.dockerignore b/.dockerignore index e7a1b4c..cb088f3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,5 +8,3 @@ npm-debug.log* .env .env.* !.env.example -# ไฟล์ที่อัปโหลดระหว่าง dev ห้ามติดไปใน image (production เขียน runtime + เก็บใน volume ไม่ใช่ bake ตอน build) -public/uploads diff --git a/.gitignore b/.gitignore index d557901..e52b340 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,3 @@ next-env.d.ts .claude/settings.local.json # N4: ไฟล์ที่ผู้ใช้อัปโหลดผ่านหน้ารายการ (รันไทม์ — ไม่เข้า repo) public/uploads/ - -# compose ทดสอบบนเครื่อง dev (build จาก repo + DB แยก รันที่ 6704) — ไม่ใช่ของ production -docker-compose.test.yml diff --git a/CLAUDE.md b/CLAUDE.md index b970931..d1e317f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,6 @@ app/actions/files.ts ← Server Action uploadFile — validate + เขีย app/api/annotations/stream/route.ts ← SSE endpoint (GET ?fileId=) app/api/annotations/export/route.ts ← export PDF ฝัง annotation (pdf-lib, F8) app/api/annotations/export-csv/route.ts ← export สรุป CSV พร้อม BOM (F16) -app/uploads/[name]/route.ts ← เสิร์ฟไฟล์อัปโหลดจาก public/uploads/ (N4 — Next production เห็นไฟล์ static เฉพาะที่มีตอน startup ไฟล์ที่เขียน runtime ต้องเสิร์ฟผ่าน route) app/page.tsx ← รายการไฟล์ (server component) app/annotate/[id]/page.tsx ← โหลด file + initial data → PDFViewer app/annotate/[id]/summary/page.tsx ← หน้าสรุปหมายเหตุจัดกลุ่มตามหน้า (F15) diff --git a/app/globals.css b/app/globals.css index 9862123..0673981 100644 --- a/app/globals.css +++ b/app/globals.css @@ -108,11 +108,10 @@ /* NumberCombo (ความหนา/ความทึบ/ขนาด): ▲▼ ซ้อนแนวตั้ง 2 ปุ่ม — ถ้าโดน min 36px คอลัมน์จะสูง 72px และ input (items-stretch) ถูกดึงบวมตามทั้งแถว — ปุ่ม ▲▼ - จึงหลุดจาก rule ด้านบน (combo-step) แล้วตั้งความสูงแถว input เองที่ 36px แทน - (:first-child — เว้น dropdown ของ preset ที่เป็นลูก div ตัวที่สอง ไม่ให้ถูก - บังคับความสูงจนตัดรายการ): แตะ input = เปิด preset dropdown (เปิดขึ้นบน — L2: - แถวท้ายเมนูเปิดลงจะล้นเขต scroll ของเมนู) คือ touch path หลัก ส่วน ▲▼ - (ครึ่งแถว) เป็น fine-tune สำรอง — ขนาดใกล้ desktop ไม่บวม */ + จึงหลุดจาก rule ด้านบน (combo-step) แล้วตั้งความสูงแถว input เองที่ 36px + แทน (:first-child — แถว chips preset ของ L2 โดน rule ปุ่ม 36px ของเมนู + ครอบอยู่แล้ว ห้ามบังคับสูงซ้ำ): แตะ chips preset = touch path หลัก + ส่วน ▲▼ (ครึ่งแถว) เป็น fine-tune สำรอง — ขนาดใกล้ desktop ไม่บวม */ [data-tool-menu] .number-combo > div:first-child { height: 2.25rem; } @@ -245,15 +244,6 @@ html { touch-action: auto; } -/* เส้นคั่นบางระหว่างแถวตั้งค่า (PanelRow) ที่ต่อกันในเมนู tool/แผง selection — - ระยะ/เส้นเท่ากับแทรก
ไว้ระหว่างแถว - ทุกจุด (แถวแรกของกลุ่มไม่มีเส้น — ไม่ซ้ำกับ divider ที่เมนูใส่ไว้เองแล้ว) */ -.panel-row + .panel-row { - margin-top: 0.375rem; - padding-top: 0.625rem; - border-top: 1px solid #f1f5f9; -} - /* M6: safe-area — defensive (ค่าเป็น 0 จนกว่าจะใส่ viewport-fit=cover ใน meta ของ layout — ยังไม่ใส่ตามขอบเขตงานนี้ ใส่ไว้ก่อนเพื่อไม่ต้องแก้ CSS ซ้ำ) */ header { diff --git a/app/page.tsx b/app/page.tsx index 64f2e83..72f099c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { asc } from "drizzle-orm"; -import { FileImage, FileText } from "lucide-react"; +import { ChevronRight, FileImage, FileText } from "lucide-react"; import { db } from "@/db"; import { files } from "@/db/schema"; @@ -45,17 +45,13 @@ export default async function HomePage() { -
-

เพิ่มเอกสาร

- -
-

เอกสาร

{rows.length > 0 && ( {rows.length} ไฟล์ )}
+ {rows.length === 0 ? (
@@ -72,7 +68,7 @@ export default async function HomePage() { {rows.map((f) => (
  • {imageFormatFromUrl(f.pdfUrl) ? ( @@ -92,13 +88,14 @@ export default async function HomePage() { {new Date(f.createdAt).toLocaleString("th-TH")} - {/* ปุ่มเข้า annotate แทนลิงก์กางเต็มการ์ด (M4) ตาม feedback — เป้าจิ้ม 36px (H4) */} + + {/* M4 (UX audit 2026-09-16): ลิงก์กางเต็มการ์ด — จอสัมผัสจิ้มตรงไหนก็เข้าได้ + (เดิมปุ่ม "Annotate" ~30px อย่างเดียว และเป็นภาษาอังกฤษใน UI ไทย) */} - Annotate - + className="absolute inset-0 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1" + aria-label={`เปิด ${f.title}`} + />
  • ))} diff --git a/app/uploads/[name]/route.ts b/app/uploads/[name]/route.ts deleted file mode 100644 index 6802811..0000000 --- a/app/uploads/[name]/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { readFile } from "node:fs/promises"; - -import { - fileKindFromUrl, - imageContentTypeFromUrl, - SAMPLE_FILE_EXTENSIONS, -} from "@/utils/file-kind"; -import { PdfPathError, resolvePublicPdfPath } from "@/lib/server-pdf"; - -// N4 — เสิร์ฟไฟล์ที่อัปโหลด (`public/uploads/.`) ผ่าน route handler -// ไม่ใช่ static file ของ Next: production server (next start / standalone) จะไล่ -// รายชื่อไฟล์ใน public/ เก็บไว้ใน Set **ครั้งเดียวตอน startup** เท่านั้น -// (next/dist/server/lib/router-utils/filesystem.js — setupFsCheck) ไฟล์ที่ action -// เขียนลง public/uploads/ หลัง server รันแล้วจึง GET ตรง ๆ ไม่ได้ (404) -// ฝั่ง next dev ไม่ผ่าน branch นั้น เลยทำงานได้ — อาการคลาสสิก "local ได้ production พัง" -// อ่านอย่างเดียว (invariant #1) + path guard ของ lib/server-pdf เหมือน /api/pdf (§5) - -// ชื่อไฟล์ต้องเป็น uuid + extension ใน whitelist เท่านั้น (ตรงรูปที่ uploadFile เขียน) -// — ชื่ออื่น 404 กลับไป ไม่รู้จักก็ไม่เปิด (กัน path traversal โดยแบบชื่อเอง: อักขระ -// นอก hex/ขีด/จุด ผ่าน regex นี้ไม่ได้) -const UUID_RE = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - -export const dynamic = "force-dynamic"; - -export async function GET( - _req: Request, - { params }: { params: { name: string } } -) { - const dot = params.name.lastIndexOf("."); - if (dot <= 0) return new Response(null, { status: 404 }); - const [id, ext] = [ - params.name.slice(0, dot), - params.name.slice(dot).toLowerCase(), - ]; - if (!UUID_RE.test(id)) return new Response(null, { status: 404 }); - // ext ต้องอยู่ใน whitelist เดียวกับ uploadFile — ชื่ออื่น 404 ไม่เสิร์ฟ - if (!SAMPLE_FILE_EXTENSIONS.includes(ext)) { - return new Response(null, { status: 404 }); - } - - // helper เหล่านี้คาดชื่อไฟล์เต็ม (จุดต้องอยู่หลังอักขระตัวแรก) — ส่งชื่อเต็ม ไม่ใช่ bare ext - const contentType = - fileKindFromUrl(params.name) === "image" - ? imageContentTypeFromUrl(params.name) - : "application/pdf"; - - let bytes: Buffer; - try { - // guard: resolve แล้วต้องอยู่ใน public/ เท่านั้น (รูปแบบชื่อผ่าน regex อยู่แล้ว - // — ชั้นนี้กันไว้เผื่อมีคนเรียก helper นี้ซ้ำในอนาคตด้วย input อื่น) - const diskPath = resolvePublicPdfPath(`/uploads/${id}${ext}`); - bytes = await readFile(diskPath); - } catch (err) { - if (err instanceof PdfPathError) return new Response(null, { status: 404 }); - if ((err as NodeJS.ErrnoException).code === "ENOENT") { - return new Response(null, { status: 404 }); - } - throw err; - } - - return new Response(new Uint8Array(bytes), { - headers: { - "Content-Type": contentType, - // ชื่อไฟล์เป็น uuid ต่อการอัปโหลดหนึ่งครั้ง ไม่มีการเขียนทับ — ค่าเดียวกันตลอดชีวิตไฟล์ - "Cache-Control": "public, max-age=31536000, immutable", - }, - }); -} diff --git a/components/UploadFileForm.tsx b/components/UploadFileForm.tsx index e9daa62..ed5efab 100644 --- a/components/UploadFileForm.tsx +++ b/components/UploadFileForm.tsx @@ -62,9 +62,6 @@ export default function UploadFileForm() { > {pending ? "กำลังอัปโหลด…" : "อัปโหลด"} -

    - รองรับ PDF, JPG, PNG, TIFF — ไม่เกิน 100MB ต่อไฟล์ -

    {error &&

    {error}

    } ); diff --git a/components/pdf/AnnotationSelectionLayer.tsx b/components/pdf/AnnotationSelectionLayer.tsx index 3ae33c1..1035ab7 100644 --- a/components/pdf/AnnotationSelectionLayer.tsx +++ b/components/pdf/AnnotationSelectionLayer.tsx @@ -512,7 +512,7 @@ export default function AnnotationSelectionLayer({ > {highlightData && ( <> - + @@ -550,7 +550,7 @@ export default function AnnotationSelectionLayer({ )} {strokeData && ( - + )} @@ -568,7 +568,7 @@ export default function AnnotationSelectionLayer({ )} {stampData && ( <> - + - + commitTextboxPatch({ color: c })} diff --git a/components/pdf/PDFViewer.tsx b/components/pdf/PDFViewer.tsx index aacf4ae..447d431 100644 --- a/components/pdf/PDFViewer.tsx +++ b/components/pdf/PDFViewer.tsx @@ -3039,7 +3039,7 @@ export default function PDFViewer({ ))}
    - + @@ -3127,7 +3127,7 @@ export default function PDFViewer({ ); })}
    - + @@ -3220,7 +3220,7 @@ export default function PDFViewer({ ))}
    - + @@ -3298,7 +3298,7 @@ export default function PDFViewer({ ))}
    - + @@ -3360,7 +3360,7 @@ export default function PDFViewer({ overflowY: "auto", }} > - + diff --git a/components/pdf/ToolControls.tsx b/components/pdf/ToolControls.tsx index 95ba3d2..b052441 100644 --- a/components/pdf/ToolControls.tsx +++ b/components/pdf/ToolControls.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useRef, useState, type ComponentType, type ReactNode } from "react"; -import { Check, ChevronDown, ChevronUp, Highlighter, Strikethrough, Underline } from "lucide-react"; +import { useEffect, useState, type ComponentType, type ReactNode } from "react"; +import { ChevronDown, ChevronUp, Highlighter, Strikethrough, Underline } from "lucide-react"; import type { HighlightStyle } from "@/db/schema"; @@ -25,37 +25,31 @@ export const INK_COLORS = [ "#3b82f6", // น้ำเงิน "#8b5cf6", // ม่วง "#ec4899", // ชมพู - "#b45309", // น้ำตาล "#111827", // ดำ (default textbox) "#6b7280", // เทา ]; -/** แถวตั้งค่าในเมนู/แผงเครื่องมือ — label ซ้าย (optional: แถวสีไม่ใส่ ให้ตารางกินเต็มแถว) - * + ตัวควบคุมขวา */ -export function PanelRow({ label, children }: { label?: string; children: ReactNode }) { +/** แถวตั้งค่าในเมนู/แผงเครื่องมือ — label ซ้าย + ตัวควบคุมขวา */ +export function PanelRow({ label, children }: { label: string; children: ReactNode }) { return ( -
    - {label ? ( - {label} - ) : null} +
    + {label} {children}
    ); } -/** แถวปุ่มสีวงกลม preset + ปุ่มสีรุ้งวงกลม (color picker เลือกสีอื่นได้ไม่จำกัด) - * grid 4 ต่อแถว — 11 สี preset + สีรุ้ง = 12 วง เรียงจบพอดี 3 แถว (4+4+4) - * ช่อง ~37px พ้นมาตรฐาน touch 36px เลยไม่ต้องปลด min ใน CSS จอสัมผัส */ +/** แถวปุ่มสีวงกลม preset + ปุ่มสีรุ้ง (color picker เลือกสีอื่นได้ไม่จำกัด) */ export function ColorSwatches({ value, onChange }: { value: string; onChange: (c: string) => void }) { return ( -
    +
    {INK_COLORS.map((c) => (
    - {open && ( -
    - {options.map((opt) => ( + {/* L2 (UX audit 2026-09-16): preset เดิมเป็น dropdown เปิดลงจากช่อง — แถวท้าย + ของเมนู tool (ความทึบ/ความหนา) ล้นเขต scroll ของเมนู ต้องเลื่อนเมนูจึงเห็น + ครบ เปลี่ยนเป็น chips แถวเดียวใต้ช่อง: เห็นค่าที่เลือกได้ทันที ไม่มี popup + ซ้อนในเมนูอีก (จอสัมผัสได้ min 36px จาก rule [data-tool-menu] โดยอัตโนมัติ) */} +
    + {options.map((opt) => { + const active = value === opt; + return ( - ))} -
    - )} + ); + })} +
    ); } diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile index bdd4a8e..cea7533 100644 --- a/docker/web.Dockerfile +++ b/docker/web.Dockerfile @@ -37,10 +37,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts # postgres.js โดน Next บีบ bundle เข้า server chunks — standalone node_modules ไม่มี package นี้ # แต่ migrate.mjs import มันตรง ๆ → คัดลอกจาก stage deps (v3 ไม่มี dependency ย่อย โฟลเดอร์เดียวพอ) COPY --from=deps --chown=nextjs:nodejs /app/node_modules/postgres ./node_modules/postgres -# สร้างโฟลเดอร์ uploads ให้ user nextjs เป็นเจ้าของ — named volume ที่ compose mount ทับ -# (เก็บไฟล์อัปโหลดข้าม redeploy) จะได้ ownership จาก image ตอน init: -# ไม่มี dir นี้ใน image = volume ถูกสร้างโดย root แล้ว action เขียนไฟล์ไม่ได้ (EACCES) -RUN mkdir -p public/uploads && chown nextjs:nodejs public/uploads USER nextjs EXPOSE 3000 # DATABASE_URL ฉีดตอนรันผ่าน env ของ container (อย่าฝังใน image) diff --git a/docs/PLAN-FEAT.md b/docs/PLAN-FEAT.md index 0e86424..c7ee7c3 100644 --- a/docs/PLAN-FEAT.md +++ b/docs/PLAN-FEAT.md @@ -306,7 +306,6 @@ Dependencies เพิ่มเติม: F3 ← F2 · F12/F13/F19 ← F1 · F15 - **UI:** ฟอร์มเดียวบนหน้า `/` (`components/UploadFileForm.tsx`) — `` + ปุ่มอัปโหลด (`useTransition`, disabled ระหว่างส่ง) รับทั้ง PDF และรูป - **Server Action `uploadFile`** (`app/actions/files.ts`): รับ `FormData` — validate extension (whitelist เดียวกับ `SAMPLE_FILE_EXTENSIONS`) + ขนาด ≤ 100MB + ไฟล์ไม่ว่าง → เขียน `public/uploads/.` (**ชื่อไฟล์บนดิสก์จาก randomUUID เท่านั้น** — กันชนกันและกัน path traversal; `title` = ชื่อไฟล์เดิมที่ sanitize) → insert `files (title, pdf_url)` → `revalidatePath("/")` — ไม่มี SSE event (รายการไฟล์ไม่ได้ sync ผ่าน SSE) - **next.config:** `experimental.serverActions.bodySizeLimit = "100mb"` (default 1MB ไม่พอสำหรับ PDF/สแกนศาล) -- **เสิร์ฟไฟล์อัปโหลดผ่าน `app/uploads/[name]/route.ts`** (GET — อ่าน `public/uploads/.` + path guard ของ `lib/server-pdf.ts` + content-type จาก `utils/file-kind.ts`): Next.js production (`next start`/standalone) ไล่รายชื่อไฟล์ใน `public/` เก็บใน Set **ครั้งเดียวตอน startup** (router-utils/filesystem.js `setupFsCheck`) — ไฟล์ที่ action เขียนตอน runtime GET ตรง ๆ แล้ว 404 (dev ไม่เป็น → อาการ "local ผ่าน Docker พัง"); ไฟล์ที่ bake ตอน build (pdf.worker/ฟอนต์/samples) ถูกเสิร์ฟโดย static path ก่อน route นี้อยู่แล้วตามลำดับ routing; PDF ใน viewer โหลดผ่าน `/api/pdf` อยู่แล้ว ตัวที่ต้องใช้ route นี้จริง = รูปภาพ (``) - **ความสัมพันธ์กับ seed:** seed-samples จัดการเฉพาะ `/samples/%` — ไฟล์อัปโหลดใน `/uploads/` ไม่ถูก seed ลบ - **Out of scope ของ N4:** หน้าจัดการ/ลบ/แก้ชื่อไฟล์ใน UI, drag-drop, progress bar, multi-file พร้อมกัน, auth ผูกเจ้าของไฟล์, สิทธิ์ต่อไฟล์ diff --git a/drizzle/0001_bright_celestials.sql b/drizzle/0001_bright_celestials.sql deleted file mode 100644 index a502cea..0000000 --- a/drizzle/0001_bright_celestials.sql +++ /dev/null @@ -1,11 +0,0 @@ -ALTER TABLE "bookmarks" ADD COLUMN IF NOT EXISTS "user_name" text DEFAULT '' NOT NULL;--> statement-breakpoint --- N6 backfill ชื่อผู้บันทึกของ bookmark เดิมจาก annotations ตามแผน PLAN-BOOKMARK.md D2 --- (เดิมรันมือครั้งเดียวบน dev — ฝั่ง deployment ใช้ migrate.mjs จึงต้องมาในไฟล์นี้; --- WHERE user_name = '' ทำให้รันซ้ำปลอดภัย แถวที่หาชื่อไม่ได้คง '' → UI แสดง "ไม่ทราบชื่อ") -UPDATE "bookmarks" b -SET user_name = COALESCE(( - SELECT a.user_name FROM annotations a - WHERE a.user_id = b.user_id AND a.user_name <> '' - ORDER BY a.created_at DESC LIMIT 1 -), '') -WHERE b.user_name = ''; diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json deleted file mode 100644 index aac0fed..0000000 --- a/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,475 +0,0 @@ -{ - "id": "8124204c-9872-4e64-9486-693d5c639ff6", - "prevId": "010b9cd2-5118-42d5-94c2-fdbfc3227018", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.annotation_replies": { - "name": "annotation_replies", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "annotation_id": { - "name": "annotation_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_name": { - "name": "user_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "message": { - "name": "message", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "annotation_replies_annotation_idx": { - "name": "annotation_replies_annotation_idx", - "columns": [ - { - "expression": "annotation_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "annotation_replies_annotation_id_annotations_id_fk": { - "name": "annotation_replies_annotation_id_annotations_id_fk", - "tableFrom": "annotation_replies", - "tableTo": "annotations", - "columnsFrom": [ - "annotation_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.annotation_versions": { - "name": "annotation_versions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "annotation_id": { - "name": "annotation_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "file_id": { - "name": "file_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "page_index": { - "name": "page_index", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_name": { - "name": "user_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "annotation_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "data": { - "name": "data", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "annotation_versions_file_idx": { - "name": "annotation_versions_file_idx", - "columns": [ - { - "expression": "file_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "annotation_versions_file_id_files_id_fk": { - "name": "annotation_versions_file_id_files_id_fk", - "tableFrom": "annotation_versions", - "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.annotations": { - "name": "annotations", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "file_id": { - "name": "file_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "page_index": { - "name": "page_index", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_name": { - "name": "user_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "annotation_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "data": { - "name": "data", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "resolved": { - "name": "resolved", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "annotations_file_page_idx": { - "name": "annotations_file_page_idx", - "columns": [ - { - "expression": "file_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "page_index", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "annotations_file_id_files_id_fk": { - "name": "annotations_file_id_files_id_fk", - "tableFrom": "annotations", - "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.bookmarks": { - "name": "bookmarks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "file_id": { - "name": "file_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "page_index": { - "name": "page_index", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_name": { - "name": "user_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "''" - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "parent_id": { - "name": "parent_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "y_offset": { - "name": "y_offset", - "type": "double precision", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "bookmarks_file_id_files_id_fk": { - "name": "bookmarks_file_id_files_id_fk", - "tableFrom": "bookmarks", - "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.files": { - "name": "files", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "pdf_url": { - "name": "pdf_url", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.annotation_type": { - "name": "annotation_type", - "schema": "public", - "values": [ - "HIGHLIGHT", - "DRAW", - "COMMENT", - "SHAPE", - "REDACTION", - "TEXTBOX", - "STAMP" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index cba85a9..548fbe1 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,13 +8,6 @@ "when": 1789031603082, "tag": "0000_narrow_quentin_quire", "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1789544914887, - "tag": "0001_bright_celestials", - "breakpoints": true } ] } \ No newline at end of file