export+sidebar: กรองตามรายคนที่เลือกดูครบทั้งจอและ export + REDACTION คงไว้เสมอ (คำขอผู้ใช้ 2026-09-22)

export PDF (F8):
- route รับ ?user=<userId> เพิ่ม: กรอง annotation ตาม userId นั้น ยกเว้น REDACTION
  ติดไปเสมอ (D4 — กล่องดำหาย = เนื้อหาที่ปิดกั้นกลับมาอ่านได้) และ bookmarks → PDF
  outline กรองตามผู้บันทึกด้วย (หัวข้อแม่โดนกรองแต่ลูกยังอยู่ ลูกขึ้น root ตามตรรกะ
  buildOutline เดิม) ไม่ส่ง param = ทั้งหมด เหมือนเดิม
- ชื่อไฟล์เติมชื่อผู้ใช้ท้าย: <title>-annotated-<ชื่อ>.pdf — ชื่อจาก user_name ที่
  mirror ในตาราง (ไม่รับ text จาก client) ผ่าน encodeRFC5987 รองรับชื่อไทย
- PDFViewer handleExportPdf: resolve filter ปัจจุบันก่อนยิง route (mine →
  user.userId) ส่ง &user= เมื่อไม่ใช่ ทุกคน

จอ (F4):
- visibleAnnotations คง REDACTION ของทุกคนไว้เสมอแม้กรองดูของคนอื่น — สูตรเดียวกับ
  export: กันเนื้อหาที่ปิดกั้นกลับมาอ่านได้บนจอ (เดิมหายไปตาม filter) และจอตรงกับ
  ไฟล์ export / ผลค้นหา F14 (visibleSearchResults) ใช้สูตรเดียวกัน
- sidebar แท็บ Bookmark กรองตามเจ้าของหมายเหตุเหมือน annotation (เดิม F4 คุมแค่
  annotation — bug report 2026-09-22): ต้นไม้ ตามหัวข้อ มุมมอง ตามหน้า ตัวเลข
  แท็บ และ badge จำนวนลูก ใช้ visibleBookmarks ชุดเดียวกัน — ลูกที่แม่โดนกรองขึ้น
  root เหมือน export; empty state แยก ไม่มีที่คั่นหน้าของเจ้าของหมายเหตุที่เลือก
  ออกจากไฟล์ว่างจริง
- popup ย้ายหัวข้อ (movableTo) และ bookmarkById ยังเห็นต้นไม้เต็ม — ย้ายไปใต้หัวข้อ
  ของคนอื่นได้ตามเดิม

verify: tsc + Playwright e2e (สร้าง redaction จริงเป็น user1 → สลับ user3 กรองของ
ตัวเอง: rect ดำของ user1 ยัง render ครบ, tab Bookmark (10)→(2) และแถวต้นไม้ 2 ตรง
DB เป๊ะ, ลบ redaction ทดสอบทิ้งจน DB เหลือ 0) + export จริง 4 filter นับ PDF outline
ตรงจำนวน bookmarks ของผู้ใช้นั้นใน DB ทุกแบบ — sync docs/PLAN-FEAT.md §F8

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-22 15:34:37 +07:00
parent 9ba8f0a8ec
commit 0aef3ff028
4 changed files with 73 additions and 16 deletions

View file

@ -14,7 +14,7 @@ import {
type PDFPage,
type PDFRef,
} from "pdf-lib";
import { asc, eq, inArray } from "drizzle-orm";
import { and, asc, eq, inArray } from "drizzle-orm";
import { db } from "@/db";
import { PdfPathError, readPublicPdf } from "@/lib/server-pdf";
@ -274,9 +274,21 @@ export async function GET(req: Request) {
.from(annotations)
.where(eq(annotations.fileId, fileId))
.orderBy(asc(annotations.createdAt));
// F4: export ตาม filter รายคนที่กำลังดู (?user=<userId> จาก sidebar — ไม่ส่ง = ทั้งหมด)
// REDACTION ติดไปเสมอแม้กรองคนอื่น (D4 — กล่องดำหายไป = เนื้อหาที่ปิดกั้นกลับมาอ่านได้)
const userParam = url.searchParams.get("user");
const filteredRows = userParam
? rows.filter((r) => r.userId === userParam || r.type === "REDACTION")
: rows;
// ชื่อผู้ใช้เติมท้ายชื่อไฟล์ (mirror ในตาราง — ไม่รับ text จาก client) — กรองแล้ว
// ผู้นั้นยังไม่มีรายการเลย = ใช้ userId ตรง ๆ
const filterName = userParam
? (rows.find((r) => r.userId === userParam && r.userName)?.userName ??
userParam)
: null;
// sort แยกตามชนิดก่อนวาด (stable ภายในชนิดด้วย createdAt) — กัน REDACTION เก่าโดน
// HIGHLIGHT/DRAW ที่สร้างทีหลังทับจนข้อความง้ำกลับมาอ่านได้ (D4)
const dtos = rows
const dtos = filteredRows
.map(toAnnotationDTO)
.sort((a, b) => TYPE_ORDER[a.type] - TYPE_ORDER[b.type]);
@ -634,10 +646,16 @@ export async function GET(req: Request) {
// bookmarks → PDF outline (แผง Bookmarks ของ reader กดกระโดดไปหน้า + yOffset)
// เรียงตามลำดับหน้าเหมือน sidebar (byPageOrder ของ N5: หน้า → ตำแหน่งบนหน้า → เวลาสร้าง)
// กรองตามรายคนเหมือน annotation (คำขอผู้ใช้ 2026-09-22) — หัวข้อแม่โดนกรองแต่ลูกยังอยู่
// ลูกขึ้น root (buildOutline จัดการ parent หายเคสนี้อยู่แล้ว)
const bookmarkRows = await db
.select()
.from(bookmarks)
.where(eq(bookmarks.fileId, fileId))
.where(
userParam
? and(eq(bookmarks.fileId, fileId), eq(bookmarks.userId, userParam))
: eq(bookmarks.fileId, fileId),
)
.orderBy(
asc(bookmarks.pageIndex),
asc(bookmarks.yOffset),
@ -646,7 +664,9 @@ export async function GET(req: Request) {
buildOutline(doc, bookmarkRows);
const out = await doc.save();
const filename = encodeRFC5987(`${file.title}-annotated.pdf`);
const filename = encodeRFC5987(
`${file.title}-annotated${filterName ? `-${filterName}` : ""}.pdf`,
);
return new Response(new Uint8Array(out), {
headers: {
"content-type": "application/pdf",