Skip to main content

ตัวสร้างเมนู

ภาพรวม​

ตัวสร้างเมนู (Menu Builder) ใช้ออกแบบเมนูแบบหน้าเว็บหรือ LIFF ที่มีโครงสร้างเป็นต้นไม้หลายชั้น แล้วเผยแพร่ผ่านลิงก์ที่มี token สาธารณะ จุดต่างสำคัญคือโครงเมนูทั้งก้อนถูกเก็บเป็น JSON ชุดเดียวในหนึ่งระเบียน ไม่ได้แยกเป็นตารางต่อรายการเมนู ทำให้การแก้ไขทั้งโครงสร้างและการบันทึกเป็นการทำงานครั้งเดียว

รายการเมนูหนึ่งตัวเลือกได้ 4 ชนิด ได้แก่ โฟลเดอร์ที่มีรายการลูก (node), ลิงก์ออกไปเว็บภายนอก (linkout), หน้าเนื้อหา (content_page) และลิงก์รวมเนื้อหา (content_link) โดยชนิดสุดท้ายเลือกได้อีกว่าจะพาไปหน้าใหม่หรือแสดงเนื้อหาในตัวเมนูเลย พร้อมเลือกรูปแบบการแสดงผลเป็นการ์ดหรือปุ่ม

นอกจากนี้ยังกำหนดกลุ่มผู้ชมเป็นรายรายการเมนูได้ และปรับแต่งหน้าตาได้ละเอียดทั้งระดับธีมของทั้งเมนู (สี, ความมน, ระยะห่าง, ขนาดตัวอักษร) และระดับรายการเดี่ยว (สี, ขอบ, รูปประกอบ, ตำแหน่งข้อความ)

Business Flow​

  1. เปิดหน้ารายการเมนู ระบบต้องรู้ก่อนว่าผู้ใช้กำลังทำงานกับ LINE OA ใด หากยังไม่ได้เลือก OA จะขึ้นคำเตือนแทนรายการ
  2. แถบตัวกรองใช้รูปแบบ "กรอกแล้วกดค้นหา" คือค่าที่พิมพ์หรือเลือกจะพักไว้ก่อน แล้วจึงนำไปใช้เมื่อกดปุ่มค้นหา เพื่อไม่ให้ยิงคำขอทุกครั้งที่พิมพ์
  3. ตารางแสดงชื่อเมนูพร้อมชื่อเทมเพลตที่ใช้, จำนวนรายการเมนูทั้งหมด (นับรวมทุกชั้น), สถานะ, token สาธารณะที่คัดลอกและออกใหม่ได้จากในคอลัมน์, วันที่สร้าง และปุ่มคำสั่ง (แก้ไข / คัดลอกลิงก์ / ทำสำเนา / ลบ)
  4. การคัดลอกลิงก์ ตรวจก่อนว่ามีรายการเมนูใดตั้งกลุ่มผู้ชมไว้หรือไม่ ถ้ามีและ OA มี LIFF ID จะได้ลิงก์แบบ LIFF เพื่อระบุตัวตนผู้เปิด ถ้าไม่มีจะได้ URL แบบเว็บปกติ
  5. การทำสำเนา สร้างเมนูใหม่จากโครงเดิมทั้งชุด ส่วนการลบเป็นการลบแบบ soft delete ที่ฝั่งหลังบ้าน
  6. หน้าสร้างและแก้ไข แบ่งเป็น 3 คอลัมน์ทำงานคู่กัน โดยตั้งค่าเริ่มต้นให้ทั้งธีมและเทมเพลตแบบรายการ (list) เมื่อเปิดจากเมนูเดิมจะโหลดโครงเมนูและ token มาเติมให้
  7. ชื่อเมนูอยู่ในฟอร์มแยก แต่ถูกซิงก์กลับเข้าโครงเมนูทันทีที่พิมพ์ เพราะทั้งตัวอย่างและหน้าสาธารณะเรนเดอร์ชื่อจากโครงเมนู
  8. คอลัมน์ซ้าย (โครงเมนู) ใช้เพิ่ม/แก้/ลบรายการ มีปุ่มกางทั้งหมด ยุบทั้งหมด และล้างการปรับแต่งสไตล์ทั้ง tree การเพิ่มรายการเปิดหน้าต่างที่ให้เลือกชนิดก่อน แล้วฟิลด์ที่เหลือจะเปลี่ยนตามชนิดที่เลือก การลากวางจัดลำดับอนุญาตเฉพาะกรณีที่รายการยังอยู่ใต้แม่เดิม เพื่อกันการย้ายข้ามเมนูย่อยโดยไม่ตั้งใจ
  9. คอลัมน์กลาง (ตัวอย่างสด) แสดงเมนูในกรอบมือถือ เดินเข้าเมนูย่อยได้พร้อม breadcrumb และปุ่มย้อนกลับ มีช่องค้นหา และเมื่อกดรายการชนิดลิงก์รวมเนื้อหาที่ตั้งให้แสดงในตัว ระบบจะไปดึงเนื้อหาจริงมาแสดงเป็นการ์ดหรือปุ่ม
  10. คอลัมน์ขวา (ตัวปรับแต่ง) แบ่งเป็นการปรับรายการเดี่ยว (สไตล์ / รูปภาพ / กลุ่มผู้ชม) และการปรับธีมทั้งเมนู ซึ่งมีชุดธีมสำเร็จรูปให้เลือก 5 แบบ ส่วนตัวเลือกเทมเพลตกำหนดว่าจะจัดเรียงแบบรายการหรือแบบตาราง (พร้อมกำหนดจำนวนคอลัมน์)
  11. การบันทึก จะตรวจชื่อก่อน ถ้าเป็นเมนูใหม่ระบบจะสร้างแล้วพาเข้าสู่โหมดแก้ไขทันทีเพื่อให้ได้ token มาใช้งานต่อ ปุ่มพรีวิวจะเปิดใช้ได้เมื่อมี token แล้ว

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

หน้ารายการ (src/app/menu-builder/page.tsx) — ตาราง, ตัวกรองแบบ staged, การทำสำเนา, การออก token ใหม่, การคัดลอกลิงก์ และการลบ

หน้าตัวสร้าง (src/app/menu-builder/form/page.tsx) — โครง 3 คอลัมน์ที่ประสานงานระหว่างต้นไม้เมนู ตัวอย่างสด และตัวปรับแต่ง พร้อมปุ่มบันทึก พรีวิว และคัดลอกลิงก์สาธารณะ

คอมโพเนนต์หลักในโฟลเดอร์ src/components/menu-builder/

  • MenuTree — แก้โครงเมนู, หน้าต่างเพิ่ม/แก้รายการ, ตัวเลือกชนิดรายการ และการลากวางจัดลำดับ
  • MenuPreviewEnhanced — ตัวอย่างสดที่เดินเมนูได้จริงและ preview เนื้อหาแบบในตัว
  • ThemeCustomizer และ TemplateSelector — ปรับธีมระดับเมนูและเลือกเลย์เอาต์
  • ButtonCustomizer — ปรับสไตล์ รูป และกลุ่มผู้ชมของรายการเดี่ยว
  • ImageUploader — อัปโหลดรูปเข้าที่เก็บไฟล์ผ่าน POST /menu-builder/upload-image แล้วเก็บเฉพาะ URL ลงในโครงเมนู
  • ContentPagePicker และ ContentLinkPicker — เลือกหน้าเนื้อหาที่เผยแพร่แล้วหรือลิงก์รวมเนื้อหาที่เปิดใช้งานอยู่ โดยส่งกลับเป็น token
  • MenuPublicView — ตัวเรนเดอร์เมนูฝั่งสาธารณะ ซึ่งใช้ร่วมกับหน้า public
  • utils/menu.util.ts — ฟังก์ชันจัดการต้นไม้ในหน่วยความจำ เช่น การค้นหา เพิ่ม ลบ ย้าย และคำนวณความลึก

เซอร์วิสกลาง (src/services/menu-builder.service.ts) — ครอบคลุมการสร้าง อ่าน แก้ไข ลบ ทำสำเนา ออก token ใหม่ อ่านด้วย token สาธารณะ และอัปโหลดรูป

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

  • สิทธิ์การเข้าถึง — ใช้ subject menu-builder ซึ่งเมนูด้านข้างตรวจสิทธิ์ก่อนแสดง ปลดล็อกจากโมดูล line-oa ฝั่งหลังบ้าน
  • LINE OA Management — ต้องมี OA ที่เลือกอยู่ก่อนจึงจะโหลดหรือสร้างเมนูได้ และต้องอ่าน hash กับ LIFF ID จากที่นี่เพื่อประกอบลิงก์
  • จัดการเนื้อหา (Content Management) — รายการเมนูชนิดหน้าเนื้อหาอ้างอิง token สาธารณะของหน้านั้น
  • ลิงก์รวมเนื้อหา (Content Links) — รายการเมนูชนิดลิงก์รวมเนื้อหาอ้างอิง token ของลิงก์ และใช้ endpoint preview เพื่อแสดงรายการเนื้อหาในตัวอย่าง
  • กลุ่มผู้ชม (Audience) — ใช้จำกัดการมองเห็นเป็นรายรายการเมนู ซึ่งส่งผลให้ลิงก์ต้องเปิดผ่าน LIFF
  • หน้า Public — เมนูที่เผยแพร่แล้วถูกเปิดผ่านหน้า public ซึ่งใช้ตัวเรนเดอร์ตัวเดียวกับที่ CMS ใช้
  • การตั้งค่าและที่เก็บไฟล์ — URL สาธารณะประกอบจาก NEXT_PUBLIC_BASE_APP_URL และรูปที่อัปโหลดถูกเก็บไว้ที่ object storage โดยโครงเมนูเก็บเพียง URL

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

โมดูลนี้อยู่ที่ internal/modules/menubuilder/ และแบ่งเป็น 2 โซนชัดเจน คือ route ของผู้ดูแลใต้ /api/menu-builder ที่ต้องล็อกอิน และ route สาธารณะเส้นเดียวที่ไม่มี guard เลย

สิทธิ์ที่ต้องมี (และข้อสังเกตสำคัญ)​

  • route ผู้ดูแลทุกเส้นต้องผ่าน JwtAuth ระดับ global
  • policy metadata ของโมดูลนี้ประกาศไว้เป็นโมดูล system_module ไม่ใช่โมดูลของตัวเอง ซึ่งเป็นการยกมาตามระบบเดิมตรง ๆ ผลคือถ้าวันใดเปิดบังคับใช้ policy จริง สิทธิ์ที่ตัดสินการเข้าถึงตัวสร้างเมนูจะไปผูกกับ system_module แทนที่จะเป็น menu-builder — จุดนี้ต่างจาก subject menu-builder ที่เมนูด้านข้างใช้ตรวจ
  • ปัจจุบัน policy metadata ยังไม่ถูกบังคับใช้ และไม่มี ModuleGate คุม ผู้ใช้ที่ล็อกอินแล้วจึงยิง endpoint เหล่านี้ได้

สิ่งที่ backend ทำตอนสร้าง คัดลอก และออก token ใหม่​

  • POST /api/menu-builder — backend เป็นฝ่ายสร้าง public token ให้เอง ไม่ได้รับจากฟอร์ม จึงเป็นเหตุผลที่หน้าจอต้องบันทึกก่อนแล้วเข้าโหมดแก้ไขจึงจะมี token ไปทำลิงก์และพรีวิว
  • POST /api/menu-builder/:id/clone — คัดลอกโครงเมนูทั้งชุดเป็นระเบียนใหม่ และ ออก token ใหม่ให้สำเนา ลิงก์ของต้นฉบับจึงไม่ถูกกระทบ เหมาะกับการทำเวอร์ชันตามฤดูกาลหรือทดลอง A/B
  • POST /api/menu-builder/:id/regenerate-token — เขียน token ใหม่ทับของเดิม ลิงก์เดิมตายทันทีที่เรียกสำเร็จ ไม่มีช่วงผ่อนผัน ถ้าลิงก์เดิมถูกฝังไว้ในริชเมนูหรือข้อความที่ส่งออกไปแล้ว จะต้องไปแก้ปลายทางเอง
  • PUT /api/menu-builder/:id — เป็นการเขียนทับโครงเมนูทั้งก้อน (ไม่ใช่ PATCH บางส่วน) ซึ่งตรงกับที่หน้าจอเก็บโครงทั้งต้นไม้ไว้ในหน่วยความจำแล้วส่งทีเดียว

การอัปโหลดรูป​

POST /api/menu-builder/upload-image ตรวจว่าไฟล์ที่ส่งมาเป็นรูปจริงก่อนรับ แล้วเก็บลง object storage ผ่านบริการจัดเก็บไฟล์กลาง สิ่งที่คืนกลับมาให้หน้าจอคือ URL ซึ่งถูกฝังไว้ในโครงเมนู ไม่ได้เก็บไฟล์ไว้ในระเบียนเมนู

เส้นทางสาธารณะ​

  • GET /api/menu-builder/public/:token เปิดได้โดยไม่มี guard ใด ๆ — ใครถือ token ก็เปิดดูโครงเมนูได้
  • มีเส้นทางสาธารณะคู่ขนานอีกชุดในโมดูล public คือ GET /api/public/menu/:token ซึ่งทำงานคล้ายกัน ทั้งสองเส้นใช้ token เดียวกัน จึงควรรู้ว่าการปิดกั้นเส้นใดเส้นเดียวไม่ได้ปิดอีกเส้น
  • การจำกัดกลุ่มผู้ชมเป็นรายรายการเมนูไม่ได้ทำให้ตัวโครงเมนูเป็นความลับ ต้องพึ่งการระบุตัวตนผ่าน LIFF ตอนเรนเดอร์

การลบและข้อควรระวังเรื่อง soft delete​

  • DELETE /api/menu-builder/:id เป็น soft delete โดยประทับเวลาลงคอลัมน์ deleted_at
  • คอลัมน์นี้ไม่ได้ผูกกับกลไก soft delete ของ ORM จึงมีผลตามมา 2 ข้อ:
    • ทุกคำสั่งอ่านต้องใส่เงื่อนไข deleted_at IS NULL ด้วยตัวเอง ถ้าลืม เมนูที่ลบแล้วจะกลับมาโผล่ รวมถึงโผล่ผ่านเส้นทางสาธารณะได้
    • ตัวคำสั่งลบต้องเขียนแบบข้ามตัวกรองอัตโนมัติ (unscoped) เพื่อประทับเวลาได้ถูกระเบียน
  • ตารางที่เกี่ยวข้อง: menu_builder และ line_oa