แอปกระดานข่าว (Bulletin)
ภาพรวม
แอปกระดานข่าว (Bulletin) เป็นหนึ่งใน "Apps" ซึ่งเป็นโมดูลเสริมที่ผู้ดูแลระดับแพลตฟอร์มเปิดหรือปิดให้แต่ละองค์กรได้ ไม่ได้ควบคุมด้วยระบบสิทธิ์ตามบทบาทเหมือนโมดูลอื่น
หน้าที่ของแอปคือทำ บอร์ดปิดประกาศแบบฟอรัม ให้สมาชิกของ LINE OA เข้าอ่าน โพสต์ และแสดงความคิดเห็นผ่านเว็บฝั่งลูกค้า โดยฝั่ง CMS ทำหน้าที่เป็น เครื่องมือกำกับดูแลเนื้อหา (moderation) ครบชุด
ผู้ใช้ฝั่ง CMS คือแอดมินหรือผู้กำกับดูแล ซึ่งทำงาน 7 อย่างผ่าน 7 แท็บ ได้แก่ ตรวจประกาศที่รออนุมัติ, ตรวจความคิดเห็น, ตัดสินรายงานจากผู้ใช้, จัดหมวดหมู่, บล็อกสมาชิก, ตั้งค่าบอร์ดพร้อมธีม และดูบันทึกการตรวจสอบย้อนหลัง
จุดที่ทำให้โมดูลนี้ต่างจากที่อื่นอย่างชัดเจน
- มี ระบบสถานะตราประทับ ที่เป็นสัญญาร่วมข้ามโปรเจกต์ ไฟล์เดียวกันนี้ถูกทำสำเนาไปอยู่ที่เว็บฝั่งลูกค้าด้วย และมีชุดทดสอบในทั้งสองฝั่งที่รันกับข้อมูลทดสอบชุดเดียวกัน
- มี ตัวแก้ธีมพร้อมด่านตรวจค่าความต่างของสี ตามมาตรฐาน WCAG ซึ่งเป็นภาพสะท้อนของตรรกะเดียวกันที่ฝั่งเซิร์ฟเวอร์
- ตัวแสดงประกาศหนึ่งใบมี สองชุดโดยเจตนา เพราะ CMS กับเว็บฝั่งลูกค้าใช้เครื่องมือจัดสไตล์ต่างกัน
Business Flow
การเข้าถึงและการปรากฏของแอป
- เมนูด้านข้างเรียกทะเบียนแอปตอนโหลดหน้า แล้วเก็บเฉพาะแอปที่เปิดใช้งาน หากพบกระดานข่าวจะเพิ่มรายการเมนูย่อยพร้อม ป้ายตัวเลขจำนวนรายการที่รอตรวจ ซึ่งเป็นผลรวมของประกาศ ความคิดเห็น และรายงานที่ค้างอยู่
- ป้ายตัวเลขจะถูกดึงก็ต่อเมื่อทะเบียนแอปยืนยันว่าแอปเปิดอยู่เท่านั้น เพราะ endpoint นี้จะปฏิเสธคำขอเมื่อแอปถูกปิด และการยิงคำขอที่ถูกปฏิเสธทุกครั้งที่โหลดหน้าจะทำให้ log เต็มไปด้วยข้อมูลที่ไม่จริง
- ทุกหน้าใต้กลุ่ม
/appsใช้ตัวป้องกันเส้นทางร่วมกัน ซึ่งอ่านรหัสแอปจาก URL ตรวจกับทะเบียน แล้วพากลับหน้าแรกหากแอปถูกปิด และ อนุญาตให้ผ่านหากเรียกทะเบียนไม่สำเร็จ เพราะฝั่งเซิร์ฟเวอร์ยังคุมข้อมูลอยู่ - layout ของแอปวาดแท็บทั้ง 7 ตัว โดย 3 แท็บแรกมีป้ายตัวเลขของตัวเอง ส่วน breadcrumb และเมนูที่ active แต่ละหน้าตั้งเอง
ประกาศและคิวตรวจ
- หน้าประกาศโหลดการตั้งค่าบอร์ดครั้งเดียวเพื่อเก็บธีมที่ผ่านการประมวลผลแล้ว (ให้พรีวิววาดด้วยธีมจริงของผู้เช่า) และค่าจำกัดต่าง ๆ สำหรับส่งให้ตัวเขียนประกาศ พร้อมโหลดรายการหมวดหมู่
- ตัวเลือกสถานะแบ่งเป็น รอตรวจ / เผยแพร่แล้ว / ซ่อน / ลบแล้ว ประกอบกับตัวเลือกหมวดและช่องค้นหา
- เมื่อเลือกสถานะรอตรวจ ระบบจะบังคับดึงทั้งคิวมาในหน้าเดียว เพราะคิวตรวจต้องเห็นทั้งหมด ไม่ใช่ทีละ 20 รายการ และหน้าจอจะเปลี่ยนเป็นมุมมองคิวตรวจแทนมุมมองตาราง
มุมมองคิวตรวจ
- ฝั่งซ้ายเป็นรายการคิวที่แสดงเวลา หัวเรื่อง ผู้เขียน และบรรทัดแรกของเนื้อหา พร้อมป้ายสีแดงเมื่อมีรายงานเข้ามา
- ฝั่งขวาแสดงประกาศเต็มใบบนพื้นผิวที่ใช้ธีมจริงของบอร์ด โดยบังคับให้วาดใหม่ทุกครั้งที่เปลี่ยนรายการเพื่อให้ภาพเคลื่อนไหวของตราประทับเล่นซ้ำได้
- การเลือกรายการอิงตำแหน่ง ไม่ใช่รหัส หลังจากจัดการรายการหนึ่งเสร็จ รายการนั้นจะหายไปจากคิว และตำแหน่งเดิมจะกลายเป็นรายการถัดไปเองโดยอัตโนมัติ ทำให้คิวเดินหน้าต่อโดยไม่กระโดดกลับไปที่หัวรายการ
- มีปุ่มลัดคีย์บอร์ด สำหรับเลื่อนขึ้นลง อนุมัติ ซ่อน และลบ พร้อมเงื่อนไขป้องกันไม่ให้ทำงานขณะที่ผู้ใช้กำลังพิมพ์ในช่องกรอก มีหน้าต่างเปิดอยู่ หรือคิวว่าง
- การอนุมัติและการซ่อนไม่มีกล่องยืนยัน เพราะย้อนกลับได้ แต่ การลบมีกล่องยืนยัน เพราะระบบบันทึกการตรวจสอบระบุไว้ว่าสถานะที่ถูกลบแล้วกู้กลับไม่ได้
- คำสั่งอื่นที่ทำได้จากคิว ได้แก่ ปักหมุด, เปิดปิดการแสดงความคิดเห็น, แก้ไขประกาศ และบล็อกผู้เขียน โดยปุ่มบล็อกจะใช้งานไม่ได้เมื่อประกาศนั้นเขียนโดยแอดมินเอง
มุมมองตาราง
- คอลัมน์ประกอบด้วยลำดับ, ประกาศ (หัวเรื่องคลิกเพื่อแก้ไขได้ พร้อมเนื้อหาย่อ), ผู้เขียนพร้อมป้ายบอกประเภท, หมวด, สถานะเป็นป้ายสี, ตัวเลขการมีส่วนร่วม, วันที่สร้าง และปุ่มคำสั่ง
- ปุ่มคำสั่งมีเงื่อนไขชัดเจน คือปุ่มแก้ไขใช้ไม่ได้กับประกาศที่ลบแล้ว, ปุ่มปักหมุดใช้ได้เฉพาะประกาศที่เผยแพร่แล้ว และประกาศที่ซ่อนอยู่จะมีปุ่มนำกลับมาเผยแพร่แทนปุ่มซ่อน
- หน้านี้แบ่งหน้าที่ฝั่งเซิร์ฟเวอร์
หน้าต่างเขียนและแก้ประกาศทางการ
- หน้าต่างนี้ไม่มีช่องเลือกสถานะโดยเจตนา เพราะฝั่งเซิร์ฟเวอร์บังคับให้ประกาศที่แอดมินสร้างเป็นสถานะเผยแพร่เสมอ แม้บอร์ดจะเปิดโหมดต้องอนุมัติก่อนไว้ก็ตาม (เพราะแอดมินคือผู้อนุมัติเอง) และการแก้ของแอดมินจะไม่ทำให้ประกาศที่เผยแพร่แล้วถอยกลับไปรออนุมัติ ระบบจึงใช้ข้อความอธิบายแทนการใส่ control ที่ API ไม่สนใจ
- ค่าจำกัดมาจากการตั้งค่าของบอร์ด ไม่ใช่ค่าตายตัว ทั้งจำนวนรูปต่อประกาศและความยาวเนื้อหา ส่วนความยาวหัวเรื่องเป็นค่าคงที่ในโค้ดเพราะเป็นความกว้างของคอลัมน์ในฐานข้อมูล ไม่ควรให้ตั้งต่อบอร์ดได้เกินกว่าที่เก็บได้จริง
- รูปอัปโหลดผ่านบริการของโมดูลจัดการเนื้อหา เก็บเป็นรายการ URL และลบทีละใบได้
- เมื่อบันทึก ระบบจะส่งค่าหมวดเป็นค่าว่างอย่างชัดเจนหากผู้ใช้ล้างหมวดออก เพราะการละเว้นคีย์จะไม่ทำให้ค่าถูกเปลี่ยน
ความคิดเห็น
- หน้านี้เป็นคิวความคิดเห็นระดับบอร์ด แบ่งตามสถานะเดียวกับประกาศ
- คอลัมน์แสดงเนื้อความแบบย่อที่กางอ่านต่อได้, ผู้เขียน, ประกาศต้นทาง, จำนวนรายงาน, วันที่ และปุ่มคำสั่ง
- ชื่อประกาศต้นทางถูกโหลดแบบชะลอ โดยดึงเพียงคำขอเดียวต่อประกาศหนึ่งรายการที่ไม่ซ้ำกัน และเริ่มดึงเมื่อผู้ใช้เลื่อนเมาส์ไปหรือโฟกัสที่รายการนั้น พร้อมกลไกกันการยิงซ้ำ
- ปุ่มคำสั่งปรากฏตามสถานะปัจจุบัน โดยข้อความของปุ่มซ่อนจะเปลี่ยนเป็น "ปฏิเสธ" เมื่อความคิดเห็นนั้นยังรอตรวจอยู่
- ทุกคำสั่งที่สำเร็จจะโหลดรายการใหม่และรีเฟรชป้ายตัวเลขบนเมนู
รายงานจากผู้ใช้
- หน้านี้แบ่งตามสถานะ 3 แบบ คือเปิดอยู่ / ตัดสินแล้ว / ยกคำร้อง
- คอลัมน์แสดงเป้าหมายของรายงาน, เหตุผล, รายละเอียด, ผู้รายงานเป็น LINE user ID ดิบ, วันที่ และปุ่มคำสั่ง
- ผู้รายงานแสดงเป็นรหัสดิบเพราะ API ไม่ได้ผูกข้อมูลโปรไฟล์มาด้วย และการไปเดาชื่อจาก endpoint อื่นคือการแสดงข้อมูลของคนอื่นแบบคาดเดา
- รายงานที่เปิดอยู่มีปุ่มตัดสินและปุ่มยกคำร้อง ส่วนสถานะอื่นแสดงเป็นป้ายเท่านั้น เพราะการตัดสินย้อนกลับไม่ได้
- แถวขยายได้เพื่อดูเป้าหมายจริง โดยหากเป้าหมายเป็นประกาศจะแสดงประกาศเต็มใบพร้อมปุ่มซ่อนและลบ และหากเป็นความคิดเห็นจะแสดงรายละเอียดพร้อมปุ่มจัดการเช่นกัน หากโหลดเป้าหมายไม่ได้จะแจ้งว่าเป้าหมายหายไป
- การค้นหาความคิดเห็นตามรหัสเป็นวิธีอ้อมเพื่อชดเชยข้อจำกัดของ API เนื่องจากไม่มี endpoint สำหรับอ่านความคิดเห็นรายตัว ระบบจึงต้องไล่อ่านรายการความคิดเห็นทั้งบอร์ด โดยจำกัดไว้ไม่เกิน 5 หน้า เพราะการไล่อ่านแบบไม่จำกัดจะกลายเป็นการถล่ม API ของตัวเอง
หมวดหมู่
- หน้านี้โหลดรายการหมวดพร้อมการตั้งค่าบอร์ด เพราะต้องรู้สิทธิ์การดูระดับบอร์ดเพื่ออธิบายความหมายของการสืบทอดค่า
- หมวดมี 2 แกนที่แยกกันอย่างชัดเจน และหน้าจอออกแบบมาเพื่อไม่ให้ผู้ใช้อ่านรวมเป็นค่าเดียว
- แกนการดู กำหนดว่าใครเห็นหมวดนี้ โดยเลือกได้ระหว่างสืบทอดจากบอร์ด, เปิดให้สมาชิกทุกคน หรือจำกัดตามกลุ่มผู้ชม
- แกนการเขียน กำหนดว่าใครโพสต์เข้าหมวดนี้ได้ ระหว่างสมาชิกหรือเฉพาะแอดมิน โดยหมวดที่จำกัดให้แอดมินเขียนยังคงให้สมาชิกอ่านได้อยู่
- มีด่านตรวจสำคัญที่ฝั่งหน้าเว็บ คือถ้าผู้ใช้เลือกจำกัดตามกลุ่มผู้ชมแต่ไม่เลือกกลุ่มใดเลย ระบบจะไม่ยอมให้บันทึก เพราะการส่งค่าจำกัดพร้อมรายการกลุ่มที่ว่างเปล่ามีความหมายว่า "สมาชิกทุกคน" ไม่ใช่ "ไม่มีใคร"
- ฟอร์มหมวดประกอบด้วยชื่อ, สีประจำหมวด, ตัวเลือกสิทธิ์การดู, ตัวเลือกกลุ่มผู้ชม (แสดงเมื่อจำเป็น), ตัวเลือกสิทธิ์การเขียน, ลำดับ และสถานะ
สมาชิกที่ถูกบล็อก
- หน้านี้แสดงรายการการบล็อกทั้งหมด โดย API รับเพียงพารามิเตอร์แบ่งหน้า ไม่มีตัวกรองตามสถานะหรือขอบเขต
- คอลัมน์แสดง LINE user ID แบบดิบ (ด้วยเหตุผลเดียวกับหน้ารายงาน), ขอบเขตการบล็อก (ทั้งหมด หรือเฉพาะการแสดงความคิดเห็น), สถานะ, วันหมดอายุ, เหตุผล และวันที่บล็อก
- สถานะคำนวณจากข้อมูล 3 ส่วน ได้แก่ ถูกยกเลิกแล้ว, หมดอายุแล้ว หรือยังมีผลอยู่
- การยกเลิกบล็อกเป็นการเพิกถอนแบบไม่ลบข้อมูลจริง แม้ API จะตอบกลับด้วยข้อความว่าลบสำเร็จก็ตาม
- หน้าต่างบล็อกผู้ใช้ (ซึ่งใช้ร่วมกับคิวตรวจ) รับ LINE user ID, ขอบเขต, เหตุผล และวันหมดอายุ โดยมีหลักการสำคัญ 2 ข้อ คือ ไม่ต้องให้แอดมินยกเลิกการบล็อกเดิมก่อน เพราะฝั่งเซิร์ฟเวอร์จัดการให้ในธุรกรรมเดียวกัน การขยายขอบเขตจึงเป็นแค่การสร้างใหม่อีกครั้ง และ ไม่ส่งวันหมดอายุที่ว่าง เพราะการละเว้นค่าหมายถึงการบล็อกถาวรซึ่งเป็นค่าเริ่มต้นอยู่แล้ว
ตั้งค่าบอร์ด
- หน้านี้แบ่งเป็น 6 การ์ด ได้แก่ การแชร์ลิงก์, ข้อมูลบอร์ด, สิทธิ์การเข้าถึง, การกำกับดูแล, ค่าจำกัด และธีม
- การ์ดแชร์ลิงก์แสดง URL ของบอร์ดทั้งแบบเว็บและแบบ LIFF พร้อมปุ่มคัดลอก และแยกกรณีที่ยังไม่มี hash ออกจากกรณีที่มี hash แต่ไม่มี LIFF ID
- การ์ดสิทธิ์การเข้าถึงกำหนดได้แยกกันระหว่างสิทธิ์การดูและสิทธิ์การเขียน โดยแต่ละอันเลือกได้ว่าเปิดให้สมาชิกหรือผู้เยี่ยมชม
- การ์ดการกำกับดูแลมีสวิตช์ 3 ตัว คือต้องอนุมัติประกาศก่อน, ต้องอนุมัติความคิดเห็นก่อน และอนุญาตให้ผู้ใช้รายงานเนื้อหา
- การ์ดค่าจำกัดกำหนดจำนวนรูปต่อประกาศ, ความยาวเนื้อหา, ความยาวความคิดเห็น, ค่าเริ่มต้นของการเปิดความคิดเห็น และชุดอิโมจิที่ใช้แสดงปฏิกิริยา (จำกัดไม่เกิน 8 ตัว)
- สถานะธีมที่เก็บไว้คือชุดค่าที่แอดมินตั้งทับเท่านั้น ไม่ใช่ชุดค่าที่ผ่านการประมวลผลแล้ว เพราะบอร์ดที่สร้างใหม่ยังไม่มีค่าธีมใดเลย หากไปเริ่มต้นด้วยค่าที่ประมวลผลแล้ว การบันทึกครั้งแรกจะเขียนค่าทับทั้งชุดทันทีและทำให้บอร์ดนั้นหลุดออกจากลำดับชั้นของธีมสำเร็จรูปตลอดไป
- ก่อนบันทึก ระบบตรวจค่าความต่างของสีที่ฝั่งหน้าเว็บก่อน หากพบปัญหาจะแจ้งทันทีโดยไม่เสียเวลาเดินทางไปกลับเซิร์ฟเวอร์ ทั้งนี้ฝั่งเซิร์ฟเวอร์ตรวจเงื่อนไขชุดเดียวกันและยังเป็นผู้ตัดสินสุดท้าย การตรวจฝั่งหน้าเว็บเป็นเพียงทางลัดเพื่อความรวดเร็ว
ตัวแก้ธีมและด่านตรวจค่าความต่างของสี
- ค่าที่เก็บคือชุดค่าที่ตั้งทับ (patch) ไม่ใช่ชุดค่าทั้งหมด การล้างค่าขั้นสูงจะถูกตั้งเป็นค่าว่างซึ่งฝั่งเซิร์ฟเวอร์อ่านว่า "ทิ้งค่าที่ตั้งทับนี้" แล้วตกกลับไปใช้สีเน้น ไม่ใช่ค่าว่างเปล่า
- ลำดับการทับค่ามี 3 ชั้น จากชุดสำเร็จรูป ทับด้วยสีเน้นที่ตั้งเอง แล้วทับด้วยค่าเฉพาะแต่ละโทเคน โดยมีชุดสำเร็จรูป 4 แบบ แต่ละแบบกำหนดโทเคนสี 6 ค่าไว้ นอกจากนี้ยังปรับความมนของมุมและความหนาแน่นของการจัดวางได้
- ด่านตรวจค่าความต่างของสี ตรวจคู่สีที่เป็นข้อความบนพื้น 3 คู่ ให้ผ่านเกณฑ์อัตราส่วนอย่างน้อย 4.5 ต่อ 1 ตามมาตรฐาน WCAG
- มีคู่สีหนึ่งที่ไม่ถูกใส่ในรายการตรวจโดยเจตนา เพราะหากใส่ ชุดสีเริ่มต้นของตัวผลิตภัณฑ์เองจะไม่ผ่าน และด่านตรวจที่ปฏิเสธค่าเริ่มต้นของตัวเองคือด่านที่ผู้ใช้จะเรียนรู้ที่จะหลีกเลี่ยง
- ด่านตรวจรับค่าที่ผ่านการประมวลผลแล้ว ไม่ใช่ค่าดิบที่ตั้งทับ เพราะถ้าตัดสินจากค่าที่ตั้งทับล้วน ๆ ด่านจะทำงานเฉพาะเมื่อผู้ใช้ตั้งครบทั้งสองฝั่งของคู่สี ทำให้การตั้งเพียงฝั่งเดียวหลุดผ่านไปแสดงผลที่ค่าความต่างต่ำกว่าเกณฑ์
- พื้นผิวของบอร์ดเป็นจุดเดียวที่สีของผู้เช่าไปสัมผัส DOM โดยเขียนเป็นตัวแปร CSS บนกล่องครอบ และยังตรวจรูปแบบของค่าสีซ้ำอีกครั้งแม้เซิร์ฟเวอร์จะตรวจมาแล้ว
- พรีวิวในกรอบมือถือแสดงประกาศตัวอย่าง 3 ใบที่เลือกมาให้เห็นทุกส่วนของชุดสีพร้อมกัน ทั้งประกาศทางการที่ปักหมุด ประกาศทั่วไป และประกาศที่รอตรวจ
บันทึกการตรวจสอบย้อนหลัง
- หน้านี้มีตัวกรอง 3 ตัว คือประเภทเป้าหมาย (เลือกจากชุดปิด 6 ค่า เพราะค่านอกชุดจะถูกปฏิเสธ ไม่ใช่ได้หน้าว่าง), รหัสเป้าหมาย และประเภทการกระทำ (เลือกจากรายการ 22 รายการ)
- รายการการกระทำครอบคลุมทุกอย่างที่ทำได้กับประกาศ ความคิดเห็น รายงาน การบล็อก หมวด และการตั้งค่า โดย ไม่มีรายการแก้ไขความคิดเห็นโดยเจตนา
- ป้ายสีของการกระทำถูกกำหนดจากส่วนท้ายของชื่อการกระทำ เช่น การลบและการปฏิเสธเป็นสีแดง การอนุมัติและการสร้างเป็นสีเขียว การซ่อนและการเพิกถอนเป็นสีส้ม
- คอลัมน์ผู้ทำใช้ชื่อที่บันทึกไว้ ณ ขณะทำ ไม่ใช่การผูกข้อมูลสด เพราะการเปลี่ยนชื่อในภายหลังต้องไม่ไปเขียนทับประวัติ
- แถวขยายได้เพื่อดูค่าก่อนและหลังการเปลี่ยนแปลง โดย ค่าทั้งสองฝั่งถูกแสดงแยกกันเสมอและไม่เคยจับคู่ตามคีย์ เพราะค่าก่อนหน้าใช้รูปแบบชื่อฟิลด์ของ entity แต่ค่าหลังในกรณีที่แก้บางส่วนถูกสร้างจากรายการอัปเดตของ ORM จึงใช้รูปแบบชื่อคอลัมน์ หากจับคู่ตามคีย์จะแสดงผลราวกับว่าฟิลด์ถูกเปลี่ยนเป็นค่าว่าง ทั้งที่เป็นเพียงการสะกดที่ต่างกัน
- การจัดวางแบบแนวตั้งของบล็อกนี้เป็นสิ่งจำเป็นเชิงการใช้งาน ไม่ใช่การตกแต่ง เพราะการจัดแบบแนวนอนทำให้คอลัมน์ค่าแคบจนข้อความภาษาไทยและ URL ตัดบรรทัดทีละหนึ่งถึงสองตัวอักษร ซึ่งเป็นต้นเหตุจริงของปัญหาการแสดงผลซ้อนทับที่เคยถูกรายงานมา
ระบบสถานะตราประทับ
ระบบนี้ตัดสินว่าประกาศหนึ่งใบควรแสดงตราประทับแบบใดจากสถานะของประกาศ มุมมองของผู้ดู และประเภทของผู้เขียน
- ประกาศที่รอตรวจจะไม่ปรากฏต่อผู้อื่นเลย แต่ผู้เขียนเห็นของตัวเองว่ารอตรวจอยู่ และแอดมินเห็นในคิว
- ประกาศที่ถูกซ่อนหรือถูกลบมีเพียงแอดมินเท่านั้นที่เห็นสถานะนั้น
- ประกาศที่เผยแพร่แล้วและเขียนโดยแอดมินจะได้ตราประทับว่าเป็นประกาศทางการ
- สถานะการกำกับดูแลมีน้ำหนักเหนือประเภทของผู้เขียน ประกาศทางการที่ถูกซ่อนจะแสดงตราว่าถูกซ่อน ไม่ใช่ตราประกาศทางการ เพราะคำถามว่า "ประกาศนี้ยังแสดงอยู่หรือไม่" เร่งด่วนกว่า และการสลับลำดับจะทำให้ประกาศที่ถูกระงับยังคงแบกอำนาจขององค์กรอยู่
- ตัวตราประทับมีคำอธิบายสำหรับโปรแกรมอ่านหน้าจอ เพราะการหมุนและสีเป็นตัวสื่อความหมายเพียงอย่างเดียวและทั้งคู่ไม่รอดถึงผู้ใช้ที่ใช้โปรแกรมอ่านหน้าจอ ทั้งนี้ ตราประทับนี้ไม่ใช่เครื่องหมายรับรองตัวตน
หน้าจอและองค์ประกอบหลัก
ตัวป้องกันเส้นทางร่วม (src/app/apps/layout.tsx) และ layout ของแอป (src/app/apps/bulletin/layout.tsx) — ตรวจการเปิดใช้งานแอปและวาดแท็บทั้ง 7 พร้อมป้ายตัวเลข
หน้าตามแท็บ (src/app/apps/bulletin/*/page.tsx) — ประกาศ, ความคิดเห็น, รายงาน, หมวด, สมาชิกที่ถูกบล็อก, ตั้งค่า และบันทึกการตรวจสอบ โดยหน้าประกาศแยกเป็นมุมมองคิวตรวจกับมุมมองตาราง และมีหน้าต่างเขียนประกาศของตัวเอง
คอมโพเนนต์ร่วมของแอป — ตัวแสดงประกาศหนึ่งใบ, พื้นผิวบอร์ดที่ถือค่าสีของผู้เช่า, ตราประทับ และหน้าต่างบล็อกผู้ใช้
ไลบรารีภายในของแอป — ระบบสถานะตราประทับ (lib/stamp.ts ซึ่งห้ามพึ่งพาสิ่งใดเลยเพราะเป็นสำเนาตรงตัวกับเว็บฝั่งลูกค้า), ตัวประมวลผลธีมกับด่านตรวจค่าความต่างของสี (lib/theme.ts), ตัวนับรายการรอตรวจ, ตัวประกอบลิงก์บอร์ด, ตัวค้นหาความคิดเห็นตามรหัส, ตัวจัดการคลิปบอร์ด, ตัวจัดรูปแบบข้อความ และชุดคำภาษาไทยที่ใช้ในพรีวิว
เซอร์วิส — src/services/bulletin.service.ts ครอบคลุมทุกคำสั่งของแอปพร้อมตัวแปลข้อผิดพลาดกลาง ซึ่งรองรับรูปแบบข้อผิดพลาด 3 แบบ ได้แก่ ข้อผิดพลาดการตรวจสอบที่ส่งข้อความมาเป็นรายการ, ข้อผิดพลาดเพดานการปักหมุดที่ต้องแปลงรหัสเป็นประโยคบอกวิธีแก้ และข้อผิดพลาดทั่วไปที่อาจไม่มีข้อความมาด้วย
จุดเชื่อมต่อกับฟีเจอร์อื่น
- ทะเบียนแอปและผู้ดูแลระดับแพลตฟอร์ม — เป็นตัวกำหนดว่าแอปนี้จะปรากฏหรือไม่ CMS อ่านได้อย่างเดียว
- สิทธิ์การเข้าถึง — มีการประกาศ subject ไว้แต่ไม่มีการแม็ปจากฝั่งหลังบ้าน จึงถูกใช้เป็นเพียงคีย์ของเมนูและ breadcrumb การควบคุมจริงอยู่ที่ทะเบียนแอปและตัวป้องกันฝั่ง API
- เว็บฝั่งลูกค้า — เป็นที่ตั้งของบอร์ดสำหรับสมาชิก และ แชร์ไฟล์เชิงสัญญา 2 ไฟล์ กับ CMS ได้แก่ ระบบสถานะตราประทับ (ทำสำเนาตรงตัวและใช้ข้อมูลทดสอบชุดเดียวกัน) และชุดคำที่แสดงในพรีวิว (ต้องเทียบกันได้ทั้งคีย์และคำ)
- API ฝั่ง Go — เป็นแหล่งอ้างอิงหลักของโครงสร้างข้อมูลและตรรกะการประมวลผลธีม โดยไฟล์ธีมฝั่งหน้าเว็บเป็นภาพสะท้อนที่ถูกตรึงไว้ด้วยชุดทดสอบ
- กลุ่มผู้ชม (Audience) — ใช้จำกัดการมองเห็นของหมวดหมู่
- จัดการเนื้อหา (Content Management) — แอปยืมบริการอัปโหลดรูปของโมดูลนี้มาใช้
- LIFF และคอนโซลผู้พัฒนา LINE — URL ปลายทางของ LIFF ถูกตรึงไว้ที่รากของเว็บฝั่งลูกค้า ลิงก์บอร์ดจึงต้องต่อ path เพิ่มท้าย มิฉะนั้นจะเปิดไปที่หน้าแรกแทนที่จะเป็นบอร์ด
- ข้อจำกัดของ API ที่ต้องแก้ทางที่ฝั่งหน้าเว็บ — ไม่มี endpoint อ่านความคิดเห็นรายตัว, รายการการบล็อกไม่มีตัวกรอง และทั้งรายการการบล็อกกับรายการรายงานไม่ได้ผูกข้อมูลโปรไฟล์ LINE มาด้วย
รายละเอียดฝั่ง Backend (CMS API)
โมดูลนี้อยู่ที่ internal/modules/bulletin/ ทุก endpoint อยู่ใต้ /api/apps/bulletin/ ต่างจากโมดูลอื่นตรงที่ ไม่ได้ยกมาจากระบบเดิม แต่เขียนใหม่ตาม design spec จึงมีกติกาด้านความปลอดภัยของข้อมูลที่ชัดเจนและถูกบังคับทั่วทั้งโมดูล
กติกาความปลอดภัยของข้อมูล 6 ข้อ
กติกาชุดนี้ระบุไว้ในโค้ดทุกไฟล์ของโมดูล และอธิบายพฤติกรรมหลายอย่างที่หน้าจอเห็น
- ทุกคำสั่งฐานข้อมูลผ่านตัวช่วยที่บังคับขอบเขตให้อัตโนมัติ โดยบังคับทั้งรหัสองค์กรและรหัส LINE OA พร้อมกัน ไม่ใช่แค่อย่างใดอย่างหนึ่ง ทำให้ไม่มีทางเผลอเขียน query ที่ข้ามขอบเขตองค์กรได้
- รหัสที่ผู้เรียกส่งมาถูกตรวจ "ความเป็นเจ้าของ" ไม่ใช่แค่ว่ามีอยู่จริง และเมื่อไม่พบจะตอบ 404 ไม่ใช่ 403 โดยตั้งใจ เพราะการตอบ 403 เท่ากับยืนยันว่าแถวนั้นมีอยู่จริงในองค์กรอื่น ซึ่งเป็นการรั่วข้อมูล
- ทุกการเปลี่ยนแปลงข้อมูลกับการเขียน audit log อยู่ในธุรกรรมเดียวกัน จึงไม่มีกรณีที่การกระทำสำเร็จแต่ประวัติหาย หรือกลับกัน
- โค้ดห้ามเขียนคอลัมน์ตัวนับ (จำนวนความคิดเห็น จำนวนรีแอ็กชัน จำนวนรายงาน) เพราะ trigger ของฐานข้อมูลเป็นเจ้าของตัวเลขเหล่านี้ ตัวเลขที่หน้าจอเห็นจึงมาจากฐานข้อมูลโดยตรงและไม่มีทางเพี้ยนจากการเขียนซ้ำซ้อน
- ห้ามแก้โพสต์กับความคิดเห็นในธุรกรรมเดียวกัน เพราะ trigger จับ advisory lock แยกตามชนิดเป้าหมาย การสลับลำดับการล็อกจะทำให้เกิด deadlock
- การลบคือการเปลี่ยนสถานะเป็น "ลบแล้ว" พร้อมประทับเวลาเสมอ ไม่มีการลบจริง ข้อมูลจึงยังตรวจสอบย้อนหลังได้
สิทธิ์ที่ต้องมี
- ทุก route ถูกครอบด้วย
AppEnabledGuard— องค์กรต้องถูกเปิดแอปนี้ให้ก่อน มิฉะนั้นได้ 403 และการเปิดแอปเป็นอำนาจของผู้ดูแลระดับแพลตฟอร์มเท่านั้น - ลำดับการตรวจคือ ตรวจว่าแอปเปิดอยู่ไหม แล้วจึงตรวจ policy แล้วจึงเข้า handler
- policy metadata ประกาศไว้เป็นโมดูล
line-oaแยกตามการกระทำ แต่ตัวที่มีผลจริงในทางปฏิบัติคือการตรวจว่าแอปถูกเปิดหรือไม่ ซึ่งสอดคล้องกับที่ฝั่งหน้าเว็บใช้ subject เป็นเพียงคีย์ของเมนู
พฤติกรรมของคิวงานดูแลเนื้อหา
GET /api/apps/bulletin/pending-countมีไว้ให้เมนูแสดงตัวเลขค้างอนุมัติโดยเฉพาะ จึงเรียกได้ถี่กว่าการโหลดคิวจริง- แถวรายงานเนื้อหาจะไม่ถูกลบ แม้จะปัดตกไปแล้ว เพราะมีดัชนีที่ใช้กันไม่ให้คนเดิมรายงานเนื้อหาชิ้นเดิมซ้ำ ถ้าลบแถวทิ้ง คนเดิมจะรายงานซ้ำได้เรื่อย ๆ การ "จัดการแล้ว" จึงเป็นการเปลี่ยนสถานะของรายงาน
- การยกเลิกการบล็อก (
DELETE /api/apps/bulletin/blocks/:id) เป็นการเพิกถอน ไม่ใช่การลบแถว ประวัติว่าเคยบล็อกใครไว้ยังคงอยู่ GET /api/apps/bulletin/audit-logเป็น อ่านอย่างเดียว ไม่มี endpoint ใดให้แก้หรือลบประวัติการกระทำของทีมงาน
ตารางและกลไกฐานข้อมูลที่เกี่ยวข้อง
ตารางในกลุ่ม bulletin (กระดาน, โพสต์, ความคิดเห็น, การมีส่วนร่วม, รายงาน, การบล็อก และ audit log) โดยมี trigger ของฐานข้อมูลเป็นเจ้าของคอลัมน์ตัวนับและเป็นผู้จับ advisory lock ซึ่งเป็นเหตุผลว่าทำไมกติกาข้อ 4 และ 5 จึงเข้มงวด