ตัวสร้างเมนู
ภาพรวม
ตัวสร้างเมนู (Menu Builder) ใช้ออกแบบเมนูแบบหน้าเว็บหรือ LIFF ที่มีโครงสร้างเป็นต้นไม้หลายชั้น แล้วเผยแพร่ผ่านลิงก์ที่มี token สาธารณะ จุดต่างสำคัญคือโครงเมนูทั้งก้อนถูกเก็บเป็น JSON ชุดเดียวในหนึ่งระเบียน ไม่ได้แยกเป็นตารางต่อรายการเมนู ทำให้การแก้ไขทั้งโครงสร้างและการบันทึกเป็นการทำงานครั้งเดียว
รายการเมนูหนึ่งตัวเลือกได้ 4 ชนิด ได้แก่ โฟลเดอร์ที่มีรายการลูก (node), ลิงก์ออกไปเว็บภายนอก (linkout), หน้าเนื้อหา (content_page) และลิงก์รวมเนื้อหา (content_link) โดยชนิดสุดท้ายเลือกได้อีกว่าจะพาไปหน้าใหม่หรือแสดงเนื้อหาในตัวเมนูเลย พร้อมเลือกรูปแบบการแสดงผลเป็นการ์ดหรือปุ่ม
นอกจากนี้ยังกำหนดกลุ่มผู้ชมเป็นรายรายการเมนูได้ และปรับแต่งหน้าตาได้ละเอียดทั้งระดับธีมของทั้งเมนู (สี, ความมน, ระยะห่าง, ขนาดตัวอักษร) และระดับรายการเดี่ยว (สี, ขอบ, รูปประกอบ, ตำแหน่งข้อความ)
Business Flow
- เปิดหน้ารายการเมนู ระบบต้องรู้ก่อนว่าผู้ใช้กำลังทำงานกับ LINE OA ใด หากยังไม่ได้เลือก OA จะขึ้นคำเตือนแทนรายการ
- แถบตัวกรองใช้รูปแบบ "กรอกแล้วกดค้นหา" คือค่าที่พิมพ์หรือเลือกจะพักไว้ก่อน แล้วจึงนำไปใช้เมื่อกดปุ่มค้นหา เพื่อไม่ให้ยิงคำขอทุกครั้งที่พิมพ์
- ตารางแสดงชื่อเมนูพร้อมชื่อเทมเพลตที่ใช้, จำนวนรายการเมนูทั้งหมด (นับรวมทุกชั้น), สถานะ, token สาธารณะที่คัดลอกและออกใหม่ได้จากในคอลัมน์, วันที่สร้าง และปุ่มคำสั่ง (แก้ไข / คัดลอกลิงก์ / ทำสำเนา / ลบ)
- การคัดลอกลิงก์ ตรวจก่อนว่ามีรายการเมนูใดตั้งกลุ่มผู้ชมไว้หรือไม่ ถ้ามีและ OA มี LIFF ID จะได้ลิงก์แบบ LIFF เพื่อระบุตัวตนผู้เปิด ถ้าไม่มีจะได้ URL แบบเว็บปกติ
- การทำสำเนา สร้างเมนูใหม่จากโครงเดิมทั้งชุด ส่วนการลบเป็นการลบแบบ soft delete ที่ฝั่งหลังบ้าน
- หน้าสร้างและแก้ไข แบ่งเป็น 3 คอลัมน์ทำงานคู่กัน โดยตั้งค่าเริ่มต้นให้ทั้งธีมและเทมเพลตแบบรายการ (list) เมื่อเปิดจากเมนูเดิมจะโหลดโครงเมนูและ token มาเติมให้
- ชื่อเมนูอยู่ในฟอร์มแยก แต่ถูกซิงก์กลับเข้าโครงเมนูทันทีที่พิมพ์ เพราะทั้งตัวอย่างและหน้าสาธารณะเรนเดอร์ชื่อจากโครงเมนู
- คอลัมน์ซ้าย (โครงเมนู) ใช้เพิ่ม/แก้/ลบรายการ มีปุ่มกางทั้งหมด ยุบทั้งหมด และล้างการปรับแต่งสไตล์ทั้ง tree การเพิ่มรายการเปิดหน้าต่างที่ให้เลือกชนิดก่อน แล้วฟิลด์ที่เหลือจะเปลี่ยนตามชนิดที่เลือก การลากวางจัดลำดับอนุญาตเฉพาะกรณีที่รายการยังอยู่ใต้แม่เดิม เพื่อกันการย้ายข้ามเมนูย่อยโดยไม่ตั้งใจ
- คอลัมน์กลาง (ตัวอย่างสด) แสดงเมนูในกรอบมือถือ เดินเข้าเมนูย่อยได้พร้อม breadcrumb และปุ่มย้อนกลับ มีช่องค้นหา และเมื่อกดรายการชนิดลิงก์รวมเนื้อหาที่ตั้งให้แสดงในตัว ระบบจะไปดึงเนื้อหาจริงมาแสดงเป็นการ์ดหรือปุ่ม
- คอลัมน์ขวา (ตัวปรับแต่ง) แบ่งเป็นการปรับรายการเดี่ยว (สไตล์ / รูปภาพ / กลุ่มผู้ชม) และการปรับธีมทั้งเมนู ซึ่งมีชุดธีมสำเร็จรูปให้เลือก 5 แบบ ส่วนตัวเลือกเทมเพลตกำหนดว่าจะจัดเรียงแบบรายการหรือแบบตาราง (พร้อมกำหนดจำนวนคอลัมน์)
- การบันทึก จะตรวจชื่อก่อน ถ้าเป็นเมนูใหม่ระบบจะสร้างแล้วพาเข้าสู่โหมดแก้ไขทันทีเพื่อให้ได้ 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— เลือกหน้าเนื้อหาที่เผยแพร่แล้วหรือลิงก์รวมเนื้อหาที่เปิดใช้งานอยู่ โดยส่งกลับเป็น tokenMenuPublicView— ตัวเรนเดอร์เมนูฝั่งสาธารณะ ซึ่งใช้ร่วมกับหน้า publicutils/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— จุดนี้ต่างจาก subjectmenu-builderที่เมนูด้านข้างใช้ตรวจ - ปัจจุบัน policy metadata ยังไม่ถูกบังคับใช้ และไม่มี
ModuleGateคุม ผู้ใช้ที่ล็อกอินแล้วจึงยิง endpoint เหล่านี้ได้
สิ่งที่ backend ทำตอนสร้าง คัดลอก และออก token ใหม่
POST /api/menu-builder— backend เป็นฝ่ายสร้าง public token ให้เอง ไม่ได้รับจากฟอร์ม จึงเป็นเหตุผลที่หน้าจอต้องบันทึกก่อนแล้วเข้าโหมดแก้ไขจึงจะมี token ไปทำลิงก์และพรีวิวPOST /api/menu-builder/:id/clone— คัดลอกโครงเมนูทั้งชุดเป็นระเบียนใหม่ และ ออก token ใหม่ให้สำเนา ลิงก์ของต้นฉบับจึงไม่ถูกกระทบ เหมาะกับการทำเวอร์ชันตามฤดูกาลหรือทดลอง A/BPOST /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