Skip to main content

หน้าขอบคุณหลังส่งฟอร์ม

ภาพรวม​

หน้าขอบคุณคือหน้าจอที่ผู้ใช้เห็นหลังส่งฟอร์มสำเร็จ และยังใช้เป็นปลายทางของฟอร์มแบบส่งได้ครั้งเดียวเมื่อผู้ใช้ที่เคยส่งแล้วเปิดฟอร์มซ้ำ

แอดมินสามารถเลือกได้ว่าจะใช้ข้อความมาตรฐานของระบบ หรือปรับแต่งเองทั้งไอคอน รูปภาพ หัวข้อ ข้อความแบบ rich text ปุ่มปลายทาง และการนับถอยหลังปิดหน้าต่างอัตโนมัติ

จุดออกแบบที่สำคัญคือการตีความค่าที่ยังไม่ได้ตั้งค่า ระบบถือว่าค่าที่เป็น null, ออบเจ็กต์ว่าง หรือออบเจ็กต์ที่ระบุโหมดเป็น default ล้วนหมายถึงการใช้ค่ามาตรฐาน ผลลัพธ์คือฟอร์มเก่าทุกฟอร์มได้หน้าขอบคุณภาษาไทยพร้อมปุ่มปิดทันที โดยไม่ต้อง migrate ข้อมูลใด ๆ

Business Flow​

  1. หลังส่งฟอร์มสำเร็จ container จะพาผู้ใช้ไปยัง path /:hash/form/:id/thank-you
  2. หน้านี้โหลดข้อมูลฟอร์มอีกครั้งเพื่อดึงการตั้งค่าหน้าขอบคุณและธีม พร้อมโหลดข้อมูล OA เพื่อดึง botBasicId มาใช้สร้างลิงก์ไปยังห้องแชท
  3. หน้านี้เรียก useLiffInit() ซึ่ง ไม่ใช่ useLiffAuth() เพราะฟอร์มที่ไม่บังคับล็อกอินหรือถูกเปิดจากเบราว์เซอร์ทั่วไปไม่ควรถูกเด้งไปหน้าล็อกอิน แต่ยังต้อง init LIFF ก่อนเพื่อให้เรียก liff.isInClient() และ liff.closeWindow() ได้
  4. ระบบ resolve การตั้งค่าหน้าขอบคุณให้เป็นค่าที่ใช้เรนเดอร์จริง ครอบคลุมไอคอน รูปภาพ หัวข้อ เนื้อหา ปุ่ม และการปิดอัตโนมัติ
  5. เรนเดอร์หน้าจอตามกฎต่อไปนี้
    • หากตั้งทั้งรูปภาพและไอคอน รูปภาพจะมีลำดับความสำคัญสูงกว่า
    • เนื้อหาแบบปรับแต่งเองซึ่งเป็น HTML จะถูกเรนเดอร์ผ่าน Tiptap viewer เท่านั้น ห้ามใช้ dangerouslySetInnerHTML โดยเด็ดขาด
    • เนื้อหามาตรฐานเป็นข้อความธรรมดา
  6. ปุ่มปลายทางทำงานตามชนิดที่แอดมินเลือกไว้ ดังตารางด้านล่าง
  7. หากมีการตั้งค่าปิดอัตโนมัติ (รับค่าระหว่าง 3 ถึง 60 วินาที และใช้ได้เฉพาะเมื่อปุ่มไม่ใช่ชนิดเปิด URL หรือไม่มีปุ่ม) หน้าจอจะแสดงวงแหวนนับถอยหลังแล้วกดปุ่มให้อัตโนมัติเมื่อครบเวลา
  8. ข้อความบอกผู้ใช้ว่าปิดหน้าต่างนี้ได้เลย จะแสดงเฉพาะกรณีที่ไม่มีปุ่มหรือปุ่มเป็นชนิดเปิด URL เนื่องจากกรณีอื่นตัวปุ่มทำหน้าที่เป็นทางออกอยู่แล้ว

ชนิดของปุ่มปลายทาง​

ชนิดพฤติกรรม
ปิดหน้าต่างเรียก closeLiff() เพื่อปิดหน้าต่าง LIFF
เปิดแชท OAหากอยู่ในแอป LINE จะปิดหน้าต่างเพื่อให้ผู้ใช้กลับเข้าห้องแชทเอง หากอยู่นอกแอปจะเปิดลิงก์เพิ่มเพื่อน/แชทของ OA นั้น
สลับ rich menuทำงานเหมือนการปิดหน้าต่าง เนื่องจากการสลับ rich menu เกิดขึ้นฝั่งเซิร์ฟเวอร์ตอนบันทึกคำตอบแล้ว
เปิด URLนำผู้ใช้ไปยัง URL ที่แอดมินกำหนด
ไม่มีปุ่มไม่แสดงปุ่มใด ๆ

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

หน้าเพจ

  • หน้าขอบคุณ (src/app/[hash]/form/[id]/thank-you/page.tsx) รับผิดชอบการโหลดข้อมูลและ init LIFF

Container และคอมโพเนนต์ (อยู่ภายใต้ src/components/form-builder/thank-you/)

  • Container (thank-you.container.tsx) รวมตรรกะการ resolve การตั้งค่าให้เป็นค่าที่ใช้เรนเดอร์จริง และประกอบหน้าจอทั้งหมด
  • คอมโพเนนต์ปุ่ม (thank-you-action.tsx) จัดการพฤติกรรมของปุ่มแต่ละชนิด รวมถึงการ normalize เครื่องหมาย @ ในรหัสบอทก่อนประกอบเป็น URL แชท
  • โมดูลข้อความมาตรฐาน (default-copy.ts) เก็บชุดข้อความที่ใช้เมื่อแอดมินไม่ได้ปรับแต่ง

ตัวช่วยที่ใช้ร่วม

  • useLiffInit() (src/hooks/use-liff-init.ts) init LIFF โดยไม่บังคับล็อกอิน
  • closeLiff() (src/lib/liff-close.ts) ปิดหน้าต่าง LIFF พร้อมทางสำรอง
  • ชนิดข้อมูลของการตั้งค่าหน้าขอบคุณและชนิดปุ่ม อยู่ในไฟล์ type ของ form builder

ส่วนนี้มี unit test ครอบคลุมทั้งการเรนเดอร์หน้าจอและการประกอบลิงก์แชท OA อยู่ใน src/components/form-builder/thank-you/__tests__/

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

  • ใช้ Tiptap viewer ร่วมกับ หน้าอ่านบทความ โดยทั้งโปรเจกต์มี guard test ที่ห้ามใช้ raw HTML ในการเรนเดอร์เนื้อหาจากผู้ใช้
  • ต้องได้ค่า botBasicId จาก เส้นทาง hash และการระบุ LINE OA จึงจะสร้างปุ่มเปิดแชท OA ได้
  • ใช้ CSS variable ธีมชุดเดียวกับ กรอกฟอร์ม เพื่อความต่อเนื่องของหน้าตา
  • รองรับการตั้งค่า prefers-reduced-motion ของผู้ใช้ โดยวงแหวนนับถอยหลังจะเคลื่อนไหวเฉพาะเมื่อผู้ใช้ไม่ได้ปิดแอนิเมชัน

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

หน้าขอบคุณ ไม่มี endpoint ของตัวเอง ทั้งการตั้งค่าและ side effect ที่แท้จริงเกิดขึ้นที่อื่น หัวข้อนี้อธิบายว่า backend ทำอะไรจริงเบื้องหลังหน้านี้

เหตุผลที่ปุ่ม "สลับ rich menu" ไม่ได้ยิง API อะไรเลย​

จุดออกแบบสำคัญคือ การสลับ rich menu ถูกสั่งจากฝั่งเซิร์ฟเวอร์ตอนบันทึกคำตอบสำเร็จ ไม่ใช่จากการกดปุ่มบนหน้าขอบคุณ เหตุผลด้าน security ตรงไปตรงมา: ถ้าให้หน้าเว็บยิงเอง ใครที่รู้ URL ของฟอร์มก็ POST เปลี่ยน rich menu ของตัวเองได้โดยไม่ต้องกรอกฟอร์มจริง — ปุ่มชนิดนี้บนหน้าเว็บจึงทำงานเหมือนปุ่มปิดหน้าต่างเท่านั้น เพราะงานจริงเสร็จไปตั้งแต่ตอน submit แล้ว

กลไกสลับเมนู 2 ตัวที่เป็นอิสระต่อกัน​

backend มีกลไก 2 ตัวที่ยิงได้พร้อมกันในการส่งฟอร์มครั้งเดียว:

1. ธง "แปลงเป็นสมาชิก" (ตั้งที่ระดับฟอร์ม)

  • ส่งงานเข้าคิวเพื่อให้ worker สลับ rich menu ไปเป็นเมนูของสมาชิก
  • ถ้าส่งงานเข้าคิวล้มเหลว ทั้ง request จะล้มตามไปด้วย — ต่างจากกลไกที่ 2 ซึ่ง best-effort ล้วน กรณีนี้ผู้ใช้จะเห็น error ทั้งที่คำตอบอาจถูกบันทึกไปแล้ว
  • จากนั้นอัปเดตประเภทผู้ใช้เป็น member แบบ best-effort — การอัปเดตนี้จำเป็นเพราะ payload ที่ส่งเข้าคิวมี field ประเภทผู้ใช้อยู่ก็จริง แต่ไม่มี worker ตัวใดอ่านมันเลย (ทั้งระบบเดิมและระบบใหม่) ถ้าไม่มีขั้นตอนนี้ rich menu จะเปลี่ยนแต่ผู้ใช้ยังเป็น guest อยู่ ซึ่งเป็น defect ที่แก้ไว้ตรงจุดนี้

2. ปุ่มชนิด "สลับ rich menu" ในการตั้งค่าหน้าขอบคุณ

  • backend อ่านการตั้งค่าหน้าขอบคุณอย่างระวังตัวมาก: ต้องเป็นโหมดปรับแต่งเอง ชนิด action ต้องเป็น rich menu และรหัสเมนูต้องเป็นจำนวนเต็มที่ถูกต้อง — ไม่ครบข้อใดข้อหนึ่งจะ ข้ามไปเงียบๆ พร้อม log เตือน ไม่ทำให้ request ล้ม (เพราะคอลัมน์นี้เป็น JSON ดิบที่อาจถูกเขียนไว้ก่อนที่ CMS จะมี validator)
  • ตรวจความเป็นเจ้าของเมนูก่อนส่งงาน — เช็คว่า rich menu นั้นเป็นของ OA ของฟอร์มจริง โดยใช้ OA จาก ตัวฟอร์มเอง ไม่ใช่จากค่าใน request จุดนี้เป็นด่านเดียวบนเส้นทางนี้ และจำเป็นจริง เพราะฝั่ง worker ค้น rich menu โดยไม่กรอง OA — ถ้าไม่ตรวจตรงนี้ ฟอร์มของ OA หนึ่งจะสั่งใช้เมนูของ OA อื่นได้
  • ความล้มเหลวทุกกรณีเป็นเพียงคำเตือนใน log เพราะคำตอบถูกบันทึกไปแล้ว ผู้ใช้ไม่ได้ทำอะไรผิด และการสลับเมนูถือเป็นของแถม
  • กลไกนี้ ไม่แตะประเภทผู้ใช้ และต้องไม่แตะ เพราะหน้าที่ของมันคือเปลี่ยนเมนู ไม่ใช่เปลี่ยนสถานะสมาชิก

ข้อจำกัดที่ควรรู้: ลำดับการสลับเมนูไม่การันตี​

ถ้าฟอร์มเปิดทั้งธง "แปลงเป็นสมาชิก" และตั้งปุ่มชนิดสลับ rich menu ไว้พร้อมกัน จะมี 2 งานเข้าคิวเดียวกัน และ งานที่ถูกประมวลผลหลังสุดเป็นผู้ชนะ ลำดับนี้ไม่มีการรับประกัน — CMS ระบุข้อจำกัดนี้ไว้แล้ว ถ้าต้องการผลลัพธ์ที่แน่นอน ควรเลือกใช้อย่างใดอย่างหนึ่ง

ข้อมูลที่หน้านี้ใช้มาจากไหน​

  • การตั้งค่าหน้าขอบคุณทั้งก้อนถูกส่งกลับมาแล้วทั้งใน response ของการส่งฟอร์ม และใน GET /form-builder/:hash — หน้าเว็บจึง render ปุ่มและ countdown ได้เองโดยไม่ต้องเรียก endpoint เพิ่ม
  • botBasicId ที่ใช้สร้างลิงก์ห้องแชท OA มาจาก endpoint แปลง hash (ดู เส้นทาง hash และการระบุ LINE OA)
  • backend ไม่ได้ resolve ค่า default ของหน้าขอบคุณให้ — ส่งค่าดิบตามที่เก็บไว้ (ซึ่งอาจเป็น null หรือ object ว่าง) การตีความว่า "ค่าว่างหมายถึงใช้ค่ามาตรฐาน" ทั้งหมดเป็นหน้าที่ของฝั่งเว็บ

Edge case ที่ควรรู้​

  • ฟอร์มที่ตั้งปุ่มสลับ rich menu ไว้แต่ไม่บังคับล็อกอิน LINE จะข้ามการส่งงานไปเงียบๆ เพราะไม่รู้ว่าจะสลับเมนูให้ใคร (CMS บังคับให้เปิดการล็อกอินตอนบันทึกอยู่แล้ว จุดนี้เป็นเข็มขัดนิรภัยตอน runtime)
  • ผู้ใช้ที่เปิดหน้าขอบคุณซ้ำหรือ refresh จะไม่ทำให้เมนูสลับซ้ำ เพราะไม่มีการยิง API ใดๆ จากหน้านี้
  • การสลับเมนูเป็นงาน asynchronous ผ่านคิว — ผู้ใช้อาจเห็นเมนูเปลี่ยนช้ากว่าที่หน้าขอบคุณแสดงขึ้น หรือไม่เปลี่ยนเลยถ้า worker มีปัญหา โดยที่หน้าเว็บไม่มีทางรู้