Skip to main content

บอร์ดประกาศ - รายละเอียดและคอมเมนต์

ภาพรวม

หน้ารายละเอียดแสดงประกาศหนึ่งรายการแบบเต็ม พร้อมส่วนสนทนาด้านล่างที่ผู้ใช้ เข้ามาแสดงความคิดเห็นได้ ความคิดเห็นรองรับการอ้างอิงความคิดเห็นอื่น (quote) การแนบรูป การกดรีแอ็กชัน และการลบความคิดเห็นของตัวเอง

ข้อแตกต่างจากฟีดที่ควรทราบคือ ความคิดเห็นเรียงจาก เก่าไปใหม่ ตามธรรมชาติของ การสนทนา ในขณะที่ฟีดประกาศเรียงจากใหม่ไปเก่า แต่ทั้งสองใช้รูปแบบ cursor เหมือนกัน ดังนั้นการกด "โหลดเพิ่ม" จึงต่อท้ายรายการเดิมเหมือนกันทั้งคู่

Business Flow

  1. ผู้ใช้เปิด /{hash}/bulletin/{postId} — ระบบตรวจก่อนว่ารหัสประกาศเป็นจำนวนเต็มบวก ถ้าไม่ใช่จะตอบเป็นหน้าไม่พบข้อมูลทันที
  2. ยืนยันตัวตนด้วยกลไกเดียวกับหน้าฟีด แล้วโหลดข้อมูลสามชุดพร้อมกัน: การตั้งค่าบอร์ดและหมวดหมู่ ตัวประกาศ และรายการความคิดเห็น (cursor pagination ครั้งละ 20 รายการ)
  3. เรนเดอร์ตัวประกาศแบบเต็ม: หัวข้อเป็นหัวเรื่อง ต่อด้วยบรรทัดผู้เขียน เนื้อหาเต็ม รูปภาพที่แนบมา และแถบรีแอ็กชัน
  4. รายการความคิดเห็นแต่ละรายการแสดง:
    • ป้ายผู้เขียนแบบไม่ระบุตัวตน
    • หากเป็นการอ้างอิงความคิดเห็นอื่น จะแสดงข้อความย่อ (ไม่เกิน 120 ตัวอักษร) ของความคิดเห็นต้นทาง หากต้นทางถูกลบหรือถูกซ่อนไปแล้ว จะแสดงสถานะว่าถูกลบ เหมือนกันทุกกรณี เพื่อไม่ให้เนื้อหาที่ถูกลบรั่วผ่านการ quote
    • แตะที่ข้อความอ้างอิงเพื่อเลื่อนไปยังความคิดเห็นต้นทาง หากยังอยู่ในหน้า
    • ปุ่มอ้างอิง และปุ่มลบซึ่งจะแสดงเฉพาะความคิดเห็นของตัวเอง
  5. ช่องเขียนความคิดเห็นจะแสดงเมื่อประกาศนั้นเปิดรับความคิดเห็น และผู้ใช้ไม่ได้ถูกบล็อก
    • จำกัดความยาวตามค่าที่ตั้งไว้ในบอร์ด
    • จำนวนรูปที่แนบได้ในความคิดเห็นเป็นค่าคนละตัวกับของประกาศ (ค่าเริ่มต้นคือ 2 รูป)
    • เมื่อส่งสำเร็จ ระบบจะโหลดทั้งรายการความคิดเห็นและตัวประกาศใหม่ เพื่อให้ตัวเลขจำนวนความคิดเห็นตรง
  6. หากประกาศปิดรับความคิดเห็น จะแสดงข้อความแจ้งแทนช่องเขียน
  7. การลบความคิดเห็นเรียก endpoint ที่อ้างอิงด้วยรหัสความคิดเห็นโดยตรง ไม่ได้อยู่ใต้เส้นทางของประกาศ
  8. การจัดการ error: สถานะ 404 ครอบคลุมทุกกรณีที่ผู้ใช้ "มองไม่เห็น" ประกาศนั้น ไม่ว่าจะถูกลบ ถูกซ่อน ไม่ใช่ของตน หรืออยู่คนละหมวดที่ไม่มีสิทธิ์ ส่วนสถานะ 401 หมายถึงเซสชันหมดอายุ
  9. มีปุ่มย้อนกลับไปที่หน้าบอร์ดอยู่ในหน้า เพื่อไม่ต้องพึ่งปุ่ม back ของเบราว์เซอร์

หน้าจอและองค์ประกอบหลัก

  • หน้ารายละเอียด (src/app/[hash]/bulletin/[postId]/page.tsx) — จุดเข้าและ การตรวจความถูกต้องของรหัสประกาศ
  • ตัวควบคุมหน้า (post-detail.container.tsx) — ประสานการโหลดข้อมูลสามชุด จัดการสถานะการอ้างอิง และการลบความคิดเห็น
  • การ์ดประกาศ (PostSheet) — ใช้ตัวเดียวกับฟีด แต่สลับเป็นโหมดรายละเอียด ซึ่งแสดงเนื้อหาเต็มและใช้หัวข้อเป็นหัวเรื่องของหน้า
  • รายการความคิดเห็น (CommentList) — รวมการแสดงข้อความอ้างอิงและปุ่มการกระทำ
  • ช่องเขียนความคิดเห็น (CommentComposer) — พร้อมตัวนับความยาวและการแนบรูป
  • องค์ประกอบร่วม — ตะแกรงรูป ป้ายผู้เขียน แถบรีแอ็กชัน และหน้าจอสถานะ ใช้ร่วมกับหน้าฟีดทั้งหมด

ฝั่งข้อมูลใช้ hook useBulletinPost และ useBulletinComments โดยเรียกผ่าน service ตัวเดียวกับหน้าฟีด

Endpoint ที่ใช้ในหน้านี้

MethodPath
GET/bulletin/{hash}/posts/{id}
GET/bulletin/{hash}/posts/{id}/comments?cursor=&limit=
POST/bulletin/{hash}/posts/{id}/comments
DELETE/bulletin/{hash}/comments/{commentId}
note

เส้นทางลบความคิดเห็นอยู่ใต้ /comments โดยตรง ไม่ได้อยู่ใต้ /posts เพราะรหัสที่ส่งไปคือรหัสของความคิดเห็น ไม่ใช่รหัสประกาศ

จุดเชื่อมต่อกับฟีเจอร์อื่น

  • ใช้โครงการยืนยันตัวตน ธีม และการแปลง error ชุดเดียวกับ bulletin-board
  • ปุ่มรายงานเนื้อหาและเมนูของเจ้าของโพสต์อยู่ในชั้นสิทธิ์และการจัดการเนื้อหา (ดู bulletin-moderation)
  • การอัปโหลดไฟล์ — ใช้กับการแนบรูปในความคิดเห็น (ดู file-upload)
  • มีชุดทดสอบครอบคลุมรายการความคิดเห็น ช่องเขียน และการอ้างอิงความคิดเห็น

รายละเอียดฝั่ง Backend (Client API)

การอ่านเธรดคอมเมนต์

GET /api/bulletin/:hash/posts/:id/comments

  1. ผ่าน resolveAccessCanView() → แล้ว ตรวจการมองเห็นตัวโพสต์ก่อนเสมอ — โพสต์ที่ผู้เรียกมองไม่เห็นตอบ 404 ไม่ใช่รายการคอมเมนต์ว่าง (รายการว่างจะบอกใบ้ว่า โพสต์นั้นมีอยู่จริง)
  2. cursor และ limit ทำงานเหมือนฟีด (default 20) และ cursor ที่พังรูปถูกยอมรับเป็น "เริ่มจากต้น" ไม่ใช่ 400
  3. SQL คืน คอมเมนต์ที่ published รวมกับคอมเมนต์ pending ของผู้เรียกเอง (เป็น UNION ALL) ส่วน hidden/deleted ไม่มีใครเห็น — ผู้ใช้ที่โพสต์ในบอร์ดที่ต้องอนุมัติจึงยังเห็นคอมเมนต์ ของตัวเองที่รออนุมัติอยู่
  4. คอมเมนต์เรียง ASC ดังนั้น cursor คือแถวสุดท้ายของหน้า และ response เป็น object ที่มี nextCursor ไม่ใช่ array เปล่า เพราะ cursor เป็น opaque ถ้าไม่ส่งมา client ไปหน้า 2 ไม่ได้

การ resolve quote — ทำฝั่ง server แบบ batch

นี่คือจุดออกแบบเด่นของ endpoint นี้: ข้อความย่อของคอมเมนต์ที่ถูกอ้างอิงถูก resolve ที่ server ด้วย query เดียวต่อหนึ่งหน้า ไม่ให้ client จับคู่เอง

  • รวบรวม id ของ quote ทั้งหน้าแล้ว dedupe (คอมเมนต์ยอดนิยมมักถูก quote หลายครั้งในหน้าเดียว) แล้วดึงทีเดียว — กัน N+1 ที่จะเป็น 20 round trip ในเธรดที่คุยกันสนุก
  • เหตุที่ต้องทำฝั่ง server: client เดิมจับคู่กับคอมเมนต์ที่ตัวเองมีอยู่ในหน้า ทำให้ quote ที่เป้าอยู่หน้าก่อนไม่ render และแย่กว่านั้นคือ render สำเนาเก่าของคอมเมนต์ที่ถูกลบไปแล้ว
  • เป้าที่ถูก soft-delete / ถูกซ่อนโดย moderator / อยู่ tenant อื่น / หายไปเลย ถูกยุบเป็น สถานะเดียวกัน คือ "ถูกลบ" พร้อมข้อความย่อว่าง เพื่อให้ client พูดว่า "คอมเมนต์นี้ถูกลบ" ได้โดยไม่ต้องเดา และไม่รั่วข้อมูลว่ากรณีไหนเป็นกรณีไหน
  • quote ใช้ predicate การมองเห็น ตัวเดียวกับ listing จึงไม่เคยเปิดเผยคอมเมนต์ที่ผู้เรียก อ่านในเธรดไม่ได้
  • โครงข้อมูล quote ที่ส่งออกมีเพียง id, ชนิดผู้เขียน, ธง "เป็นของฉัน" และข้อความย่อ ไม่มี field ใดที่พา author_line_user_id ออกไปได้ และตัดข้อความที่ 120 rune

การเขียนคอมเมนต์

POST /api/bulletin/:hash/posts/:id/comments — rate limit 10 ครั้ง/60 วินาที

  1. ใช้ CanComment() ซึ่ง ต่างจาก CanWrite(): block ทั้งสอง scope ห้ามคอมเมนต์ (คนที่ถูก mute เฉพาะคอมเมนต์ยังโพสต์และกดรีแอ็กชันได้ แต่คอมเมนต์ไม่ได้)
  2. ตรวจการมองเห็นโพสต์ → 404 ถ้ามองไม่เห็น ต้องเป็น 404 ไม่ใช่การเปิดเผยว่าโพสต์ปิดคอมเมนต์
  3. โพสต์ที่ปิดคอมเมนต์ → 403 BULLETIN_COMMENTS_CLOSED
  4. เนื้อหาที่ trim แล้วว่าง → 400 BULLETIN_EMPTY_BODY (ขอบล่างที่ระบบเดิมไม่มี — คอมเมนต์ว่างคือแถวเปล่าในเธรดของทุกคน); ยาวเกินค่า max_comment_lengthBULLETIN_BODY_TOO_LONG
  5. รูปเกิน max_images_per_comment (default 2 เป็น setting แยกจากฝั่งโพสต์ที่ default 4 โดยเจตนา — บอร์ดที่ขยายด้านหนึ่งต้องไม่ขยายอีกด้านโดยไม่รู้ตัว) → BULLETIN_IMAGE_LIMIT
  6. คอมเมนต์ที่ถูก quote ต้อง มีอยู่, อยู่บนโพสต์นี้, และ published ไม่งั้น 400 "invalid quote comment" และการค้นเป็น tenant-scoped
  7. สถานะเริ่มต้นขึ้นกับ require_comment_approval: เปิดไว้ → pending ไม่งั้น published
  8. ทำงานใน transaction เดียว: insert คอมเมนต์ → commit รูปจาก temp/ ไป path ถาวร (ต้องหลัง insert เพราะ key ถาวรมี comment id อยู่ในนั้น) → อัปเดตรายการรูป → เขียน audit log
  9. คืนคอมเมนต์ที่ resolve quote มาให้แล้ว สถานะ 201 — client จึงได้รูปข้อมูลเดียวกับที่ได้ จาก listing ไม่ต้องมี special case

การลบคอมเมนต์

DELETE /api/bulletin/:hash/comments/:id

  • gate ด้วย CanComment() ไม่ใช่ CanWrite() — คนที่ถูก block scope คอมเมนต์ควรถูกล็อก ออกจาก action ของคอมเมนต์ทั้งหมด รวมถึงการลบของตัวเอง
  • ความล้มเหลวทุกแบบ (ไม่มีคอมเมนต์นั้น / อยู่คนละ OA / ไม่ใช่ของตัวเอง) ตอบ 404 เหมือนกันหมด
  • เป็น soft delete พร้อม audit log ไม่ใช่ SQL DELETE แล้วตอบ 204