genesis+worker: design system Genesis ทั้งชุด + patch pdf.worker แก้ fake-space ไทย + ปิด review design/responsive/quality รอบ 2026-09-24 — (1) Genesis: ฟอนต์ General Sans self-host + DM Sans + Sarabun, AppNav 56px backdrop-blur แทน SessionBar, DotGrid หน้า login, palette/รัศมี/เงา/focus ตาม docs/DESIGN.md ครบทุกหน้า (2) postinstall scripts/patch-pdf-worker.mjs ถอด if (scaledDim) รอบ prevTransform ให้อัปเดตทุก glyph — กัน addFakeSpaces แทรก space กลางคำไทยใน PDF ที่ justify ด้วย letter-spacing + test กัน regression (tests/scripts/ + fixture thai_letterspacing.pdf, invariant #6) (3) ฟอร์มอัปโหลด + ช่องกลุ่ม typeId ทางเลือก — normDocumentGroup ที่ utils/external-file.ts เป็น single source ร่วมกับ N8 (4) token fixes: focus ring ปุ่ม pin + แถว bookmark ตามหน้า, ปุ่ม action ใน popup/composer/dialog เป็น rounded-md ทั้งคู่, selection preview เป็น tint ของ primary แทน blue-500 ดิบ, default สี shape/highlight รวมที่ utils/annotation-display, loading animate-pulse + จดข้อยกเว้น (เงา pin, font weights ของ viewer) ลง DESIGN.md (5) responsive: Export PDF max-lg:order-first ให้เห็นบน iPad, แถวบน header แถวเดียวเลื่อนได้บนจอแคบ, re-fit ความกว้างตอน toggle sidebar เฉพาะเมื่อยังไม่ซูมเอง, pin popover flip ขึ้นบนเมื่อใกล้ท้ายหน้า, toast hint ตอนสลับ tool กลางเอกสาร, ป้าย "ชนิด · ผู้ทำ" ของรายการผู้อื่นสำหรับจอสัมผัส (6) คุณภาพ: @layer ครอบ @media กันกฎ touch รั่ยไป desktop (Tailwind v3 ยก @layer ออกจาก media ทิ้ง wrapper — ยืนยันด้วย build จริง + วัดบนจอ), กฎ M5 ครอบ contenteditable กัน Safari zoom ตอนแก้ไข TEXTBOX บน iPad, COMMENT ใช้ box มาตราฐานเดียวตาม pin px จริง (render/scroll target/move-clamp/bookmark), ตัดหัวการ์ดซ้ำกับ h2 หน้ารายการ — พร้อมไฟล์ untracked ที่ postinstall/โค้ดอ้างถึงครบชุด (app/fonts/, AppNav, DotGrid, tests/) กัน fresh clone fail ที่ npm install (คำขอผู้ใช้ 2026-09-24)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
e254b935ce
commit
c5c6b65bd2
42 changed files with 1429 additions and 745 deletions
78
docs/DESIGN.md
Normal file
78
docs/DESIGN.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Genesis
|
||||
|
||||
## Overview
|
||||
An editorial precision interface for **Judge's Note** — a Thai court PDF annotation app (judes-note) used daily by judges on desktops and iPads. The aesthetic is quietly confident — bold display typography, generous spacing, and gallery-frame card surfaces. The mood is professional and modern without being sterile. High information density balanced by breathing room. (Text is primarily Thai; Latin display type carries the brand voice in headings and numbers.)
|
||||
|
||||
## Colors
|
||||
- **Primary** (rgb(0 51 161 / <alpha-value>) / #0033A1): CTAs, active states, links, focus indicators, interactive highlights — royal blue
|
||||
- **Primary Hover** (#002780): Darker blue for hover states on primary elements
|
||||
- **Neutral** (#9C9C9C): Placeholders, disabled states — decorative metadata only (see `ink` families for readable text)
|
||||
- **Background** (#FAFAFA): Page background, light warm gray
|
||||
- **Surface** (#FFFFFF): Cards, panels, modals, nav backdrop
|
||||
- **Text Primary** (#0A0A0A): Headings, body text, primary labels — near-black
|
||||
- **Text Secondary** (#6B6B6B): Descriptions, metadata, secondary labels — passes 4.5:1 on white; use for meaningful small text (dates, counts, authors)
|
||||
- **Border** (#E8E8EC): Card borders, dividers, input borders — subtle and recessive
|
||||
- **Success** (#10B981): Positive status — borders and tinted backgrounds (`success/10`–`/15`)
|
||||
- **Warning** (#F59E0B): Pending status, caution — borders and tinted backgrounds (`warning/10`–`/15`)
|
||||
- **Error** (#EF4444): Destructive actions, validation errors — borders and tinted backgrounds (`error/10`–`/15`)
|
||||
- **Success Ink** (#047857), **Warning Ink** (#B45309), **Error Ink** (#B91C1C): Text- and icon-safe variants of the status colors (≥5:1 on white). Any status *text or icon* on a light surface uses the `-ink` token; the base 500 tokens are for fills, borders, and tints only. Never reach for raw Tailwind `amber-*`/`emerald-*`/`red-*` shades.
|
||||
|
||||
Note: the template's Secondary green (#20970B) has no role in this app and is not registered as a token. Code font (JetBrains Mono) is likewise unused — no code UI in this app.
|
||||
|
||||
## Typography
|
||||
- **Display Font**: General Sans — loaded from Fontshare
|
||||
- **Body Font**: DM Sans — loaded from Google Fonts
|
||||
- **Code Font**: JetBrains Mono — loaded from Google Fonts
|
||||
|
||||
Display and heading text uses General Sans at bold weight with tight letter spacing (-0.03em to -0.04em). Body and UI text uses DM Sans at regular and medium weights. The contrast between the geometric display font and the humanist body font creates a refined editorial feel. Code blocks, API keys, and CLI commands use JetBrains Mono at regular weight.
|
||||
|
||||
Type scale: Display 72px, Headline 60px, Section heading 32px, Subhead 24px, Body 15px, Small 13px, Caption 12px, Overline 11px uppercase.
|
||||
|
||||
## Elevation
|
||||
This design uses minimal shadows. Cards rest flat with a 1px border and gain a subtle shadow on hover (0 8px 30px rgba(0,0,0,0.08)) combined with a -2px vertical lift. Primary buttons gain a tinted glow shadow on hover (0 4px 12px rgba(0,51,161,0.35)). The nav uses backdrop-blur rather than a shadow to convey elevation. Dropdowns and popovers use shadow-lg. Focus indicators use a 2px solid royal-blue outline with a 2px offset (≥3:1 against adjacent surfaces, WCAG 1.4.11/2.4.13) — a translucent halo alone is too faint to qualify.
|
||||
|
||||
## Components
|
||||
- **Buttons**: Primary uses blue fill with white text, 6px radius, medium weight. Secondary uses transparent bg with 1px border, same radius. Ghost has no border or bg, just text color change on hover. Destructive uses red text with red border. All buttons shift up 1px on hover. Sizes: small (32px), medium (38px), large (44px).
|
||||
- **Cards**: White surface, 1px subtle border, 12px radius, overflow hidden. Content cards (file cards, form cards) stack a content area with title, metadata, and actions. Hover lifts the card 2px and increases shadow. Transition duration 200ms.
|
||||
- **Inputs**: 1px subtle border, surface background, 6px radius, 8px vertical and 14px horizontal padding, 14px font size (16px on touch devices — iOS zooms on focus below 16px). Focus: border turns blue with the 2px outlined focus indicator. Error: border turns red. Placeholder text uses muted color.
|
||||
- **Chips**: Tag chips use rounded-full (pill shape), gray-100 background, gray-600 text, 4px vertical and 12px horizontal padding, 12px font size. Active state: blue background with white text. Status chips follow the same shape but use semantic colors (green for published, yellow for pending, red for rejected).
|
||||
- **Lists**: Stacked rows with 1px dividers between items. Each row is flex with space-between, 12px vertical and 16px horizontal padding. Hover: subtle background change.
|
||||
- **Checkboxes**: 20px size, rounded-full, gray-200 unchecked, blue checked with white checkmark. Used as toggle switches for preferences.
|
||||
- **Tooltips**: Native browser tooltips via title attributes. No custom tooltip component currently.
|
||||
- **Navigation**: Sticky top nav with backdrop-blur, 56px height, 1px bottom border. Logo left, links center (desktop) or hamburger drawer (mobile), user avatar dropdown right. Nav links: 14px medium weight, hover shows bg-alt background.
|
||||
- **Search**: Global search triggered by ⌘K. Rendered as a rounded-xl bar with magnifying glass icon and keyboard shortcut badge.
|
||||
|
||||
## Spacing
|
||||
- Base unit: 4px
|
||||
- Scale: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96px
|
||||
- Component padding: small 8×12, medium 10×16, large 12×24
|
||||
- Section spacing: 32px mobile, 48px tablet, 64px desktop
|
||||
- Container max width: 1280px with 24px horizontal padding
|
||||
- Card grid gap: 20-24px
|
||||
|
||||
## Border Radius
|
||||
- 4px: Tags, chips, badges, inline code — and menu/list rows inside dropdowns, panels, and popovers (a row belongs to its container, not a standalone button)
|
||||
- 6px: Buttons, inputs, selects — including small action buttons inside popups, composers, and dialogs; a cancel/save pair must always share the same radius
|
||||
- 8px: Metadata cards, dropdowns, panels
|
||||
- 12px: Kit preview cards, search bar, featured sections
|
||||
- 9999px: Avatars, status dots, pill badges
|
||||
|
||||
## Do's and Don'ts
|
||||
- Do use blue (rgb(0 51 161 / <alpha-value>)) only for interactive elements — never for decoration or static text. Exceptions (brand marks, agreed 2026-09-23): the JN logo tile, and the blue header band of the summary page. Static numbers (e.g. folio numbers) use `ink`.
|
||||
- Do maintain the 4px spacing grid for all padding, margins, and gaps
|
||||
- Do use General Sans for headings and DM Sans for body — never swap them
|
||||
- Do keep kit cards at 12px radius and buttons/inputs at 6px — don't mix these values
|
||||
- Do provide sufficient contrast in both light and dark modes — test both
|
||||
- Do use the `-ink` status tokens for status text/icons and the 500 tokens for fills, borders, and tints
|
||||
- Don't use raw Tailwind palette shades (slate, amber, emerald, red…) — only the tokens registered in tailwind.config.ts
|
||||
- Don't use pure black (#000000) or pure white (#FFFFFF) for text — use the defined palette values
|
||||
- Don't add decorative gradients or illustrations — the interactive dot grid is the only decorative element
|
||||
- Don't use shadows on static elements — reserve shadow elevation for hover and focus states (floating layers and document pins excepted — see App-specific exceptions)
|
||||
- Don't use more than two font weights on a single screen (dense working-surface screens such as the document viewer are excepted — see App-specific exceptions)
|
||||
- Don't place more than one primary (filled blue) button in the same view section
|
||||
|
||||
## App-specific exceptions
|
||||
- The document viewer header is a solid white bar (not the nav's backdrop-blur): it hosts a dense tool row where blur would smear the scrolled document beneath. Only the site nav (AppNav) blurs.
|
||||
- Comment pins on the document page carry a small static shadow: they are floating markers that must lift off the paper layer, in the same spirit as dropdown/popover elevation (agreed 2026-09-24).
|
||||
- The document viewer (working surface) uses three font weights on one screen — 400 body, 500 labels/buttons, 600 panel/file titles. The two-weight rule still holds for content pages (agreed 2026-09-24).
|
||||
- The text-selection preview in the viewer (drag-preview tint of the select/comment/textbox/stamp tools) is a translucent tint of primary (`rgb(0 51 161 / 0.3)`), keeping every blue on the screen in the brand hue (agreed 2026-09-24).
|
||||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
> สถานะ: **implement แล้ว (2026-09-23)** — `tsc` + vitest 142/142 + Playwright ผ่านทุกข้อ verify ของ task 3–4 + plan-reviewer/code-reviewer ไม่พบ violation (คงเหลือ e2e ลิงก์ e-Court จริงตามแผน ตอนปิดงาน N8) — เขียนใหม่ 2026-09-22 แทนร่างเดิม "Document Set จัดชุดมือใน UI" (2026-09-17, commit 6940b38) ตามคำขอผู้ใช้: ให้ e-Court บอกกลุ่มผ่านลิงก์แทนการจัดชุดด้วยมือ
|
||||
> เพิ่มเติม (2026-09-23 ตามคำขอผู้ใช้): การ์ดไฟล์หน้า `/groups/[typeId]` หุบ/กางดู bookmark+annotation ทั้งไฟล์ (ปิด gap 5.3.10 ครึ่งหลังที่เคยเป็น out of scope) + ย่อข้อความหมายเหตุเหลือ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้"
|
||||
> เพิ่มเติม (2026-09-23 ตามคำขอผู้ใช้): ฟอร์มอัปโหลด N4 เพิ่มช่อง **"กลุ่ม typeId"** ทางเลือก (ไม่กรอก = ไม่มีกลุ่ม) — `uploadFile` กลายเป็นจุดเขียน `type_id` ที่สองนอกจาก N8 (insert ตอนสร้าง row ใหม่เท่านั้น normalize เหมือน N8: trim → ว่าง = NULL, cap 200) — ยังไม่มี UI แก้กลุ่มของไฟล์ที่มีอยู่เดิม
|
||||
> เลขฟีเจอร์: ร่างเดิม (2026-09-17) เคยใช้เลข N8 — ยกให้แผนลิงก์ e-Court (`docs/PLAN-ECOURT.md`) ซึ่งเป็น dependency ต้นทาง แผนนี้จึงเป็น **N9**
|
||||
> Dependency: **N8** — การรับ/refresh `typeId` เกิดที่ `registerExternalFile` ของ N8 ทั้งหมด (แผนนี้ไม่มี code path ที่เขียน `type_id` เอง) — **N8 implement ครบแล้ว (2026-09-22 รวม D5)**: schema `type_id` + index มากับ migration `drizzle/0002` ด้วย
|
||||
> Dependency: **N8** — การรับ/refresh `typeId` ของเอกสารจากลิงก์เกิดที่ `registerExternalFile` ของ N8 ทั้งหมด (เดิมแผนนี้ไม่มี code path ที่เขียน `type_id` เอง — ต่อมาเพิ่มช่องกลุ่มของฟอร์มอัปโหลด N4 ตามคำขอผู้ใช้ 2026-09-23) — **N8 implement ครบแล้ว (2026-09-22 รวม D5)**: schema `type_id` + index มากับ migration `drizzle/0002` ด้วย
|
||||
> ตาม governance ของโปรเจกต์: implement ต้องเขียนสเปกลง `docs/PLAN-FEAT.md` §7 (**N9**) + ขยาย whitelist ใน `CLAUDE.md` + อัปเดต `docs/PLAN.md` §3 (schema) ก่อน — เป็น task แรกของแผน แล้วรันผ่าน `/implement-feat N9`
|
||||
|
||||
## Context
|
||||
|
|
@ -15,7 +16,7 @@
|
|||
## การตัดสินใจ (ยืนยันกับผู้ใช้แล้ว 2026-09-22)
|
||||
|
||||
1. **กลุ่มระดับเดียว กำหนดด้วย `typeId` จากลิงก์ e-Court** — เพิ่ม column บนตาราง `files` ค่าเดียว (`type_id` — ถอด `type_name` ตามคำขอผู้ใช้ 2026-09-22, แสดงผลด้วยรหัสกลุ่มโดยตรง) — ไม่มีตารางกลุ่มแยก ไม่มี many-to-many ไม่มีหลายระดับ (เอกสารหนึ่งอยู่กลุ่มเดียวตามธรรมชาติของงาน) — ความหมายของ id เป็นของ e-Court ฝั่งเราถือว่า "ส่งมาเหมือนกัน = กลุ่มเดียวกัน"
|
||||
2. **เข้ามาอัตโนมัติทั้งหมด ไม่มี UI จัดการกลุ่ม** — ไฟล์ได้กลุ่มเมื่อเปิดผ่านลิงก์ที่มี `typeId` (เขียนที่ N8) — กลุ่มเกิด/หายตามข้อมูลจริงใน `files` ไม่มีสถานะกลุ่มค้างในตารางอื่น
|
||||
2. **เข้ามาอัตโนมัติ ไม่มี UI จัดการกลุ่ม** — ไฟล์ได้กลุ่มเมื่อเปิดผ่านลิงก์ที่มี `typeId` (เขียนที่ N8) หรือกรอกช่อง "กลุ่ม typeId" ตอนอัปโหลด N4 (เพิ่ม 2026-09-23) — กลุ่มเกิด/หายตามข้อมูลจริงใน `files` ไม่มีสถานะกลุ่มค้างในตารางอื่น ไม่มีหน้าทางย้ายไฟล์ที่มีอยู่เดิมเข้า/ออกกลุ่ม
|
||||
3. **หน้ารายการ `/` จัดกลุ่มตาม `typeId`** (section ต่อกลุ่ม + ส่วน "ไม่มีกลุ่ม" ท้ายสุด) + **หน้าใหม่ `/groups/[typeId]`** แสดงเอกสารทั้งกลุ่มที่รู้จัก พร้อมหมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" และการ์ดแต่ละไฟล์หุบ/กางดู bookmark+annotation ของไฟล์นั้นได้ (2026-09-23)
|
||||
|
||||
## Design
|
||||
|
|
@ -23,21 +24,21 @@
|
|||
### D1 — Schema (`db/schema.ts` + `drizzle/`)
|
||||
|
||||
```ts
|
||||
// files เพิ่ม (เก็บเฉพาะเอกสารนำเข้าจากลิงก์ — ไฟล์ seed/อัปโหลด = NULL เสมอ):
|
||||
// files เพิ่ม (เอกสารจากลิงก์ e-Court / อัปโหลด N4 ที่กรอกกลุ่ม — ไฟล์ seed และอัปโหลดที่ไม่กรอก = NULL เสมอ):
|
||||
typeId: text("type_id"),
|
||||
// + index ("files_type_id_idx") บน type_id
|
||||
```
|
||||
|
||||
- ไม่มี FK/ตาราง `document_sets` (ต่างจากร่างเดิม) — กลุ่ม = ค่า distinct ของ `type_id` ณ ตอน query
|
||||
- migration: `/sync-db` (push) + **`npx drizzle-kit generate`** — rule data-layer: Docker migrate จาก SQL ใน `drizzle/` เท่านั้น; ไฟล์ generated idempotent (`IF NOT EXISTS`)
|
||||
- การเขียน `type_id` ทั้งหมดเป็นของ N8 `registerExternalFile` (insert ครั้งแรก + update เมื่อลิงก์รอบใหม่ส่ง `typeId` มา — ดู PLAN-ECOURT.md D1) — แผนนี้**อ่านอย่างเดียว**
|
||||
- การเขียน `type_id` สองที่: N8 `registerExternalFile` (insert ครั้งแรก + update เมื่อลิงก์รอบใหม่ส่ง `typeId` มา — ดู PLAN-ECOURT.md D1) และ `uploadFile` ของ N4 ช่องกลุ่มทางเลือก (insert ตอนสร้าง row ใหม่เท่านั้น — เพิ่ม 2026-09-23) — แผนนี้ (N9) **อ่านอย่างเดียว**
|
||||
- ไม่มี DTO/mapper ใหม่ — หน้าที่ใช้เป็น server component ทั้งหมด ข้อมูลข้ามไป client เป็น plain object `{id, title, typeId}` (ถ้าต้องส่ง `createdAt` ข้าม boundary = ISO string ตาม invariant #5)
|
||||
|
||||
### D2 — หน้ารายการ `/` จัดกลุ่ม (`app/page.tsx`)
|
||||
|
||||
- query `files` เหมือนเดิม → แบ่งเป็น section ต่อกลุ่ม (`type_id` ไม่ NULL) เรียงตาม `createdAt` ของไฟล์ล่าสุดในกลุ่ม (กลุ่มที่มี activity ใหม่อยู่บน) — การ์ดไฟล์ในกลุ่มเรียง `createdAt` เหมือนเดิม + ปุ่ม Annotate เดิม
|
||||
- หัวกลุ่ม = `type_id` + จำนวนไฟล์ + **ลิงก์ "เอกสารในกลุ่ม" → `/groups/{encodeURIComponent(typeId)}`** (icon Folder)
|
||||
- ไฟล์ที่ `type_id` NULL (seed / อัปโหลด N4 / ลิงก์แบบไม่มี typeId) → section **"ไม่มีกลุ่ม"** ไว้ท้ายสุด (ใกล้ฟอร์มเพิ่มเอกสาร) — แสดงเสมอถ้ามีไฟล์
|
||||
- ไฟล์ที่ `type_id` NULL (seed / อัปโหลด N4 ที่ไม่กรอกกลุ่ม / ลิงก์แบบไม่มี typeId) → section **"ไม่มีกลุ่ม"** ไว้ท้ายสุด (ใกล้ฟอร์มเพิ่มเอกสาร) — แสดงเสมอถ้ามีไฟล์
|
||||
- ไม่มี client component ใหม่ ไม่มี action ใหม่ — จัดกลุ่มเป็นการแสดงผลล้วนบน server component เดิม
|
||||
|
||||
### D3 — หน้า `/groups/[typeId]` แสดงเอกสารในกลุ่ม (`app/groups/[typeId]/page.tsx`)
|
||||
|
|
@ -83,7 +84,7 @@ Server component `force-dynamic` (login ถูกบังคับโดย `mi
|
|||
|
||||
1. **ลิงก์รอบใหม่ส่ง `typeId` คนละค่า** → ย้ายกลุ่มตามค่าล่าสุด (เกิดที่ N8) — annotation/bookmark ผูกกับ `file_id` ไม่เกี่ยวกับกลุ่ม จึงตามไฟล์ไปเอง
|
||||
2. **ลิงก์รอบใหม่ไม่ส่ง `typeId`** → ไฟล์คงกลุ่มเดิม (N8: ห้ามเคลียร์ค่าเดิม)
|
||||
3. **ไฟล์ไม่เคยมี `typeId`** (seed / อัปโหลด N4 / ลิงก์เก่า) → section "ไม่มีกลุ่ม" บนหน้า `/`, ไม่มีหน้ากลุ่ม
|
||||
3. **ไฟล์ไม่เคยมี `typeId`** (seed / อัปโหลด N4 ที่ไม่กรอกกลุ่ม / ลิงก์เก่า) → section "ไม่มีกลุ่ม" บนหน้า `/`, ไม่มีหน้ากลุ่ม
|
||||
4. **กลุ่มแสดงผลด้วยรหัสล้วน** — ไม่มีคอลัมน์ชื่อกลุ่ม (ถอด `type_name` ตามคำขอผู้ใช้ 2026-09-22) — ถ้าภายหลังต้องการชื่อกลุ่ม กลับมาเพิ่ม column ใหม่ตาม governance
|
||||
5. **`typeId` มีอักขระพิเศษ/ยาว** → ผ่าน URL ด้วย `encodeURIComponent`; normalize ที่ N8 (trim → ว่าง = NULL, cap 200); Next decode ให้อัตโนมัติตอนอ่าน `params`
|
||||
6. **กลุ่มว่างลง (ไฟล์ย้ายออกหมด)** → กลุ่มหายจากหน้า `/` เอง และ `/groups/[typeId]` กลายเป็น 404 (query จาก `files` จริง ไม่มีตารางกลุ่มค้าง)
|
||||
|
|
@ -102,7 +103,7 @@ Server component `force-dynamic` (login ถูกบังคับโดย `mi
|
|||
|
||||
## Verification (ภาพรวม end-to-end — หลัง N8 เสร็จ)
|
||||
|
||||
1. `npm run dev` → login → เปิดลิงก์ e-Court 2 ไฟล์ที่ `typeId` เดียวกัน + 1 ไฟล์ `typeId` ต่างกลุ่ม → หน้า `/` จัด 2 section ตามกลุ่ม + section "ไม่มีกลุ่ม" (ไฟล์ seed/อัปโหลด)
|
||||
1. `npm run dev` → login → เปิดลิงก์ e-Court 2 ไฟล์ที่ `typeId` เดียวกัน + 1 ไฟล์ `typeId` ต่างกลุ่ม → หน้า `/` จัด 2 section ตามกลุ่ม + section "ไม่มีกลุ่ม" (ไฟล์ seed/อัปโหลดที่ไม่กรอกกลุ่ม); อัปโหลดไฟล์พร้อมกรอกกลุ่ม → การ์ดไปอยู่ section ของกลุ่มนั้น + หน้า `/groups/[typeId]` เห็นไฟล์อัปโหลดด้วย
|
||||
2. หัวกลุ่ม → "เอกสารในกลุ่ม" → `/groups/[typeId]`: เห็นไฟล์ครบตามลำดับ + หมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" + จำนวน annotation/bookmark ต่อไฟล์
|
||||
3. กาง "Annotations & Bookmarks (N)" ในการ์ดไฟล์ → เห็น bookmark+annotation ทั้งไฟล์เรียงตามหน้า + กดแถวแต่ละรายการเด้งไปตำแหน่งนั้นใน viewer (`?ann=` / `?page=`)
|
||||
4. กด Annotate → เข้า viewer ของไฟล์นั้นได้ตามปกติ
|
||||
|
|
|
|||
|
|
@ -303,8 +303,10 @@ Dependencies เพิ่มเติม: F3 ← F2 · F12/F13/F19 ← F1 · F15
|
|||
|
||||
### N4 · อัปโหลดไฟล์แบบง่าย (2026-09-15)
|
||||
|
||||
- **UI:** ฟอร์มเดียวบนหน้า `/` (`components/UploadFileForm.tsx`) — `<input type="file" accept=".pdf,.jpg,.jpeg,.png,.tif,.tiff">` + ปุ่มอัปโหลด (`useTransition`, disabled ระหว่างส่ง) รับทั้ง PDF และรูป
|
||||
- **Server Action `uploadFile`** (`app/actions/files.ts`): รับ `FormData` — validate extension (whitelist เดียวกับ `SAMPLE_FILE_EXTENSIONS`) + ขนาด ≤ 100MB + ไฟล์ไม่ว่าง → เขียน `public/uploads/<uuid>.<ext>` (**ชื่อไฟล์บนดิสก์จาก randomUUID เท่านั้น** — กันชนกันและกัน path traversal; `title` = ชื่อไฟล์เดิมที่ sanitize) → insert `files (title, pdf_url)` → `revalidatePath("/")` — ไม่มี SSE event (รายการไฟล์ไม่ได้ sync ผ่าน SSE)
|
||||
> เพิ่มเติม (2026-09-23 ตามคำขอผู้ใช้): ช่อง **"กลุ่ม typeId"** ทางเลือกในฟอร์ม (ไม่กรอก = ไม่มีกลุ่ม) — ไฟล์อัปโหลดเข้ากลุ่ม N9 ได้โดยไม่ต้องผ่านลิงก์ e-Court; normalize เหมือน N8 (`trim` → ว่าง = NULL, cap 200 — `normTypeId` mirroring `normGroup`)
|
||||
|
||||
- **UI:** ฟอร์มเดียวบนหน้า `/` (`components/UploadFileForm.tsx`) — `<input type="file" accept=".pdf,.jpg,.jpeg,.png,.tif,.tiff">` + ช่อง "กลุ่ม typeId" ทางเลือก (uncontrolled ส่งไปกับ FormData) + ปุ่มอัปโหลด (`useTransition`, disabled ระหว่างส่ง) รับทั้ง PDF และรูป
|
||||
- **Server Action `uploadFile`** (`app/actions/files.ts`): รับ `FormData` — validate extension (whitelist เดียวกับ `SAMPLE_FILE_EXTENSIONS`) + ขนาด ≤ 100MB + ไฟล์ไม่ว่าง → เขียน `public/uploads/<uuid>.<ext>` (**ชื่อไฟล์บนดิสก์จาก randomUUID เท่านั้น** — กันชนกันและกัน path traversal; `title` = ชื่อไฟล์เดิมที่ sanitize) → insert `files (title, pdf_url, type_id)` (typeId จากช่องกลุ่ม — ว่าง = NULL, insert ตอนสร้าง row ใหม่เท่านั้น ไม่มีทางแก้กลุ่มไฟล์เดิม) → `revalidatePath("/")` — ไม่มี SSE event (รายการไฟล์ไม่ได้ sync ผ่าน SSE)
|
||||
- **next.config:** `experimental.serverActions.bodySizeLimit = "100mb"` (default 1MB ไม่พอสำหรับ PDF/สแกนศาล)
|
||||
- **เสิร์ฟไฟล์อัปโหลดผ่าน `app/uploads/[name]/route.ts`** (GET — อ่าน `public/uploads/<uuid>.<ext>` + 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 นี้จริง = รูปภาพ (`<img src={pdfUrl}>`)
|
||||
- **ความสัมพันธ์กับ seed:** seed-samples จัดการเฉพาะ `/samples/%` — ไฟล์อัปโหลดใน `/uploads/` ไม่ถูก seed ลบ
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue