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 มีปัญหา โดยที่หน้าเว็บไม่มีทางรู้