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