Skip to main content

Form builder

ภาพรวม​

Form builder คือเครื่องมือสร้างฟอร์มออนไลน์ (คล้าย Google Form) แล้วเผยแพร่เป็นลิงก์ให้ผู้ใช้ LINE กรอก จากนั้นดูคำตอบ สถิติ และส่งออกเป็น CSV ได้ในระบบ CMS เหมาะกับฟอร์มลงทะเบียนกิจกรรม แบบสำรวจ หรือฟอร์มยืนยันตัวตนก่อนรับสิทธิ์

จุดที่ทำให้เมนูนี้ต่างจากฟอร์มทั่วไปคือ Profile Mapping — เชื่อมฟอร์มเข้ากับฐานข้อมูลสมาชิกจากเมนู Member Database เพื่อให้ฟอร์มค้นหาระเบียนสมาชิกจากคำตอบที่กรอกเข้ามา แล้วตัดสินใจต่อว่าจะรับหรือปฏิเสธการส่ง และ (ถ้าเปิด) จะส่ง OTP ไปยืนยันตัวตนที่เบอร์/อีเมลใน ระเบียนที่ค้นหาเจอ ไม่ใช่ค่าที่ผู้กรอกพิมพ์เข้ามา

บทบาทที่ใช้งาน — ทีมการตลาดหรือทีมดูแลลูกค้าที่ต้องเก็บข้อมูลจากผู้ใช้ LINE (โมดูลนี้มี ModuleGate บังคับจริงที่ backend และเป็นโมดูลที่มีการตรวจสอบภายในเข้มที่สุดชุดหนึ่งในระบบ — ดูหัวข้อ สิทธิ์การเข้าถึง)

เส้นทางผู้ใช้งาน​

1. ดูรายการฟอร์ม​

เปิดเมนู Form builder หน้ารายการไม่มีแท็บ มีเพียงตัวกรอง 2 ช่อง — ค้นหาจากชื่อ/คำสำคัญ และเลือกสถานะ (ตัวเลือกที่หน้าเว็บเสนอมีแค่ active / inactive / draft ทั้งที่ backend รู้จักสถานะ delete ด้วย เพราะการลบเป็น soft delete ที่เปลี่ยนสถานะเป็น delete) เรียงตามวันที่สร้างใหม่ไปเก่า และค่าตัวกรองถูกจดจำใน sessionStorage

ตารางมี 7 คอลัมน์ — ชื่อ, คำอธิบาย, ลิงก์ (กดคัดลอกได้), วันที่สร้าง (เรียงได้), สถานะ, จำนวนคำตอบ, และปุ่มแก้ไข / ดูคำตอบ / ลบ

2. สร้างฟอร์ม​

  1. กดสร้างใหม่ กรอกชื่อฟอร์ม (ไม่เกิน 200 ตัวอักษร) และคำอธิบาย (ไม่เกิน 250 ตัวอักษร) — ชื่อต้องไม่ซ้ำกับฟอร์มอื่นใน OA เดียวกัน
  2. เพิ่มคำถามได้สูงสุด 10 ข้อ — ไม่นับหัวข้อ (Header 1/Header 2) และเส้นคั่น (Divider) ซึ่งเพิ่มได้ไม่จำกัด ชนิดคำถามมีทั้งหมด 22 แบบ แบ่งเป็น
    • ช่องกรอกทั่วไป — ข้อความสั้น, ย่อหน้า, ตัวเลข, วันที่, URL, อัปโหลดไฟล์, ข้อความกำหนดเงื่อนไขเอง (custom input)
    • ข้อมูลส่วนบุคคล — ชื่อ, นามสกุล, อีเมล, เบอร์โทร, เลขบัตรประชาชน, วันเกิด — 6 ชนิดนี้ใส่ได้ชนิดละ 1 ข้อต่อฟอร์มเท่านั้น
    • ตัวเลือก — เลือกข้อเดียว, เลือกหลายข้อ, ดรอปดาวน์, ดรอปดาวน์เลือกหลายข้อ, ยอมรับเงื่อนไข (terms)
    • ตรวจสอบฐานข้อมูล (DB Validation) — ปุ่มนี้จะถูกล็อกไว้จนกว่าจะเปิด Profile Mapping และเลือกฐานข้อมูลแล้ว เมื่อปลดล็อกจะถูกเลื่อนขึ้นมาเป็นตัวเลือกแรก
  3. ตั้งเงื่อนไขการแสดงคำถาม (เช่น แสดงคำถาม B เมื่อคำถาม A ตอบว่า…) — ต้องกรอกครบทั้งคำถามแม่ ตัวดำเนินการ และค่า (ยกเว้นตัวดำเนินการ "ไม่ว่าง" ที่ไม่ต้องมีค่า)
  4. ปรับแต่งรูปแบบ — รูปหน้าปก, ชุดสี 7 ค่า และข้อความบนปุ่ม ดูตัวอย่างสดที่คอลัมน์ขวา
  5. ตั้งหน้าขอบคุณ — โหมด default (ค่าคงที่ ทุกช่องถูกล็อก) หรือ custom เลือก action ได้ 5 แบบ (ปิดหน้าต่าง / เปิดลิงก์ / ไปที่แชท OA / เปลี่ยนริชเมนูจากเมนู Rich menu / ไม่มีปุ่ม)
  6. กดบันทึกเป็น draft หรือเผยแพร่เป็น active — เผยแพร่แล้วย้อนกลับเป็น draft ไม่ได้ (ตัวเลือก "ฉบับร่าง" จะหายไปจากฟอร์มเมื่อสถานะปัจจุบันไม่ใช่ draft) แต่สลับ active ↔ inactive ได้อิสระ

3. เชื่อมกับ Member Database (Profile Mapping)​

แผงตั้งค่าแบ่งเป็น 3 ขั้นชัดเจน

  1. แหล่งข้อมูล — เลือกฐานข้อมูลจากเมนู Member Database (บังคับเมื่อเปิดสวิตช์แล้ว) รายการดึงมาแค่ 100 รายการแรก
  2. จับคู่ฟิลด์ — ระบบแสดงคอลัมน์ของฐานข้อมูลที่เลือก แต่ละคอลัมน์กด "เพิ่มช่องกรอก" ได้ ระบบจะสร้างคำถามชนิด DB Validation ให้อัตโนมัติ หากไม่จับคู่คอลัมน์ใดเลยจะขึ้นคำเตือนว่า "ฟอร์มจะรับใครก็ได้"
  3. ผลลัพธ์ — กำหนดสองกรณี
    • พบระเบียน — เลือก custom attribute ที่จะประทับ (ไม่บังคับ) พร้อมค่าที่จะเขียน (ค่าเริ่มต้น true) และเลือกเปิด "หนึ่งบัญชี LINE ต่อหนึ่งระเบียน" ได้ พร้อมข้อความแจ้งเมื่อระเบียนถูกผูกไว้แล้ว
    • ไม่พบระเบียน — เลือกได้ 2 ทาง คือ ปฏิเสธการส่ง (แสดงข้อความที่กำหนด ไม่บันทึกอะไรเลย) หรือ สมัครเป็นสมาชิกใหม่ (รับข้อมูล แต่ไม่ประทับ attribute ยืนยัน)

4. เปิดการยืนยันตัวตนด้วย OTP (ไม่บังคับ)​

  1. แท็บ การยืนยันตัวตน จะเปิดสวิตช์ได้เฉพาะเมื่อ Profile Mapping เปิดอยู่ + เลือกฐานข้อมูลแล้ว + ฐานข้อมูลนั้นมีคอลัมน์ ถ้าเงื่อนไขหลุดไประหว่างแก้ไข ระบบจะปิดสวิตช์ให้อัตโนมัติ
  2. แท็บนี้อ่านสถานะความพร้อมจากเมนู Settings > OTP Verification — ช่องทางที่ยัง "ยังไม่ได้กำหนดค่า" จะถูก disable และมีคำเตือนพร้อมลิงก์ไปตั้งค่า ถ้าไม่มีช่องทางใดพร้อมเลย ปุ่มเพิ่มช่องยืนยันจะกดไม่ได้
  3. เลือกช่องยืนยันได้เพียง 1 ช่องเท่านั้น — เลือกคอลัมน์ของระเบียนสมาชิก (ไม่ใช่คำถามในฟอร์ม) และช่องทางส่ง (SMS หรืออีเมล)
  4. ปรับข้อความบนหน้ากรอก OTP ได้ 4 ค่า — หัวข้อ (80 ตัวอักษร), คำอธิบาย (ใส่ {destination} เพื่อแทรกปลายทางที่ถูกปิดบัง), ข้อความบนปุ่มยืนยัน (40), ข้อความปุ่มส่งซ้ำ (40)
  5. แท็บนี้จะหายไปทั้งแท็บ ถ้าผู้ใช้ไม่มีสิทธิ์ดู member-database

5. ดูคำตอบและส่งออก​

  1. กดปุ่มดูคำตอบจากหน้ารายการฟอร์ม
  2. ดูการ์ดสถิติ 3 ใบ — ทั้งหมด / ส่งครบถ้วน / ยังไม่ครบถ้วน กดที่การ์ดเพื่อกรองตาราง
  3. ตารางมี 3 คอลัมน์ตรึงซ้าย (เวลาส่ง, ส่งครบหรือไม่, LINE User ID แบบคัดลอกได้) แล้วต่อด้วยคอลัมน์ที่สร้างอัตโนมัติตามคำถามในฟอร์ม (ไม่นับหัวข้อ/เส้นคั่น) คำตอบชนิดไฟล์แสดงลิงก์ "ดูไฟล์" เปิดดู PDF/รูปในหน้าต่างซ้อนได้
  4. กดส่งออก CSV — ไฟล์มี UTF-8 BOM นำหน้าเพื่อให้ Excel อ่านภาษาไทยได้ถูกต้อง ชื่อไฟล์สร้างจากฝั่งหน้าเว็บเป็น form-responses-<formHash>-<timestamp>.csv

รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง​

ยืนยันจาก internal/modules/formbuilder/{controller,service,dto,otp,thankyou,contrast}.go ของ line-management-cms-api-go:

  • form_hash คำนวณจากชื่อฟอร์มด้วย SHA-256 และมี unique index ระดับ "ทั้งฐานข้อมูล" — ค่านี้คือ hex(sha256(title))[:12] และคอลัมน์ form_builder.form_hash มี UNIQUE INDEX ที่ไม่ได้ผูกกับ line_oa_id เลย ผลที่ตามมามีสองข้อที่ควรรู้
    • ต่าง OA กันก็ตั้งชื่อฟอร์มซ้ำกันไม่ได้ — การตรวจชื่อซ้ำ (existsByTitle) กรองด้วย line_oa_id จึงผ่าน แต่ INSERT จะไปชนกับ unique index แล้วกลายเป็น error 500 ไม่ใช่ข้อความ "Title is already exist" ที่อ่านรู้เรื่อง
    • ฟอร์มที่ลบไปแล้วยังจองชื่อไว้ — soft delete เปลี่ยนสถานะเป็น delete แต่ไม่ล้าง form_hash ส่วน existsByTitle ตัดสถานะ delete ออกจากการตรวจ จึงสร้างฟอร์มชื่อเดิมซ้ำได้ในสายตาหน้าเว็บ แต่จะพังตอน INSERT
    • form_hash ไม่ถูกคำนวณใหม่เมื่อเปลี่ยนชื่อฟอร์ม — ลิงก์ที่เผยแพร่ไปแล้วจึงยังใช้ได้ต่อ แต่ค่า hash จะไม่ตรงกับชื่อใหม่อีก
  • PUT /form-builder/:id ที่ไม่ส่ง theme มาด้วยจะได้ 500 ทุกครั้ง — เป็นพฤติกรรมที่พอร์ตมาโดยเจตนา โค้ด TS เดิมอ่าน theme.cover แบบไม่มี guard เมื่อ theme เป็น undefined/null จะโยน TypeError ที่ไม่ใช่ HttpException → Nest ตอบ 500 ฝั่ง Go จึงจำลอง 500 นั้นไว้ (errThemeCoverTypeError) หน้าเว็บจริงส่ง theme มาทุกครั้งจึงไม่เจอ แต่การเรียก API ตรงต้องส่ง theme มาด้วยเสมอ (เส้นทางสร้างฟอร์มก็เจอปัญหาเดียวกันเมื่อแนบ coverImage มาพร้อมกับ theme ที่ว่าง)
  • thankYou ถูกบังคับรูปทรงแบบปิด (closed shape) ต่างจาก theme ที่รับทุกอย่าง — คีย์ที่ไม่รู้จักในทุกระดับคือ 400 เพราะบล็อกนี้คุมการนำทางและการสลับริชเมนู กฎที่ตรวจจริงมีตัวอย่างเช่น
    • action.url ต้องเป็นสกีมา https หรือ line เท่านั้น — http ถูกปฏิเสธ (ไม่ upgrade ให้) มี allowlist กัน javascript:/data: แม้จะแทรกอักขระควบคุมเพื่ออำพราง และปฏิเสธ URL ที่มี userinfo (https://[email protected]/)
    • action.url จำเป็นเมื่อ type เป็น url และต้องเป็น null/ไม่มีเลยเมื่อ type เป็นอย่างอื่น (กันกรณี rich_menu_id ค้างอยู่แล้วยิงตอนกดปุ่มปิด) กฎเดียวกันใช้กับ rich_menu_id
    • auto_close_seconds ต้องอยู่ระหว่าง 3–60 วินาที และเป็น null เสมอเมื่อ action เป็น url หรือ none
    • message ผ่าน deny-list ของแท็ก/สกีมาที่เป็นอันตราย (<script, <iframe, javascript:, srcdoc=, event handler แบบ on…= ฯลฯ) — คอมเมนต์ในซอร์สระบุชัดว่านี่ไม่ใช่ sanitizer ความปลอดภัยจริงมาจากฝั่ง client-web ที่เรนเดอร์ผ่าน TiptapViewer
  • การเลือกริชเมนูในหน้าขอบคุณถูกตรวจกรรมสิทธิ์จริงที่ฐานข้อมูล — ระบบนับ rich_menu ที่ id ตรง และ line_oa_id ตรงกับ OA ของฟอร์มเอง (บนเส้นทาง update ใช้ค่าที่เก็บไว้ในฐานข้อมูล ไม่ใช่ OA ของผู้เรียก) และต้องมีสถานะ active — พลาดแล้วได้ 404 ที่แยกไม่ออกจาก "ไม่มีเมนูนี้" โดยเจตนา นอกจากนี้ถ้าฟอร์มไม่ได้บังคับ LINE login การเลือก action rich_menu จะถูกปฏิเสธ 400 เพราะไม่มี lineUserId ให้สลับเมนู
  • การเปิด OTP มีด่านกันการเปิดนอกช่องทาง (out-of-band) — validateOtpConfigGuard อ่านตาราง otp_config ของ OA ผู้เรียกโดยตรงทุกครั้งที่สร้าง/แก้ฟอร์ม แล้วปฏิเสธ 400 พร้อมข้อความชี้ไปที่หน้าตั้งค่า OTP ถ้าช่องทางที่ฟอร์มขอเปิดยังไม่พร้อม การเรียก API ตรงเพื่อข้ามหน้าเว็บจึงเปิด OTP ไม่ได้
  • profileMapping.otp ถูกจำกัดที่ 1 ช่องยืนยันในระดับ backend ไม่ใช่แค่ UI — ตัวตรวจสอบมีกฎ fields may contain only one verification field ตรง ๆ พร้อมกฎว่า "เปิด OTP แต่ไม่มี field" คือ 400 ("เป็นบั๊กของ editor ไม่ใช่สถานะที่ถูกต้อง") ส่วนคีย์อื่นของ profileMapping ยังรับทุกอย่างเหมือนเดิม — มีแต่คีย์ย่อย otp ที่ถูกล็อกรูปทรง
  • การตรวจสีตามมาตรฐาน WCAG ถูกเขียนไว้ครบแต่ยัง "ไม่เปิดใช้" ในการบันทึก — ฝั่ง backend ตรวจแค่ว่าสองคีย์ใหม่ (primaryColor, secondaryColor) เป็นรหัสสี hex จริง (#RGB/#RRGGBB/#RRGGBBAA) ส่วนตาราง 5 คู่ที่ต้องได้อัตราคอนทราสต์ขั้นต่ำ (ข้อความ/พื้นหน้า 4.5:1, ป้ายปุ่ม/ปุ่ม 3:1 ฯลฯ) มีสูตรคำนวณครบแต่ยังไม่บล็อกการบันทึก เพราะคีย์สีเดิม 5 ค่าไม่ได้ถูกบังคับรูปแบบไว้ ถ้าเปิดกะทันหันจะทำให้ฟอร์มเก่าบันทึกไม่ได้ทั้งหมด — คำเตือนจึงไปโผล่เป็น alert ในหน้าเว็บแทน (ที่คนเลือกสีแก้ได้ทันที)
  • การอัปโหลดรูปหน้าปกตรวจชนิดไฟล์จาก magic bytes ไม่ใช่นามสกุล — backend รับ image/jpeg, image/png, image/gif, image/webp และอ่านหัวไฟล์เพื่อกำหนดนามสกุลจริง ขณะที่หน้าเว็บรับแค่ JPG/PNG และบังคับเพิ่ม (ขนาดต่ำกว่า 10 MB, กว้างอย่างน้อย 1024px, สัดส่วน 4:1 พร้อมเครื่องมือครอป) — GIF/WebP จึงอัปโหลดผ่าน API ได้แต่อัปผ่านหน้าเว็บไม่ได้ และถ้าอัปโหลดล้มเหลว ระบบจะบันทึกฟอร์มต่อโดยตั้ง theme.cover เป็น null ไม่ใช่ตอบ error
  • หน้ารายการ/คำตอบถูกจำกัดขอบเขต OA จริงแต่ด้วยวิธีต่างกัน — GET /form-builder/:id กรองด้วย id AND line_oa_id, ส่วน DELETE และ PUT ตรวจกรรมสิทธิ์แยกก่อนแล้วตอบ 404/400 เมื่อไม่ใช่ของตัวเอง แต่ทั้งสองเส้นทางที่ id ไม่มีอยู่จริง จะได้ HTTP 200 พร้อม body เป็น null ไม่ใช่ 404 (พฤติกรรมเดิมจาก TS findOne ที่คงไว้โดยเจตนา) ส่วนคำตอบ/สถิติ/CSV ที่อ้างด้วย form_hash ถูกจำกัดขอบเขตด้วยการ JOIN กลับไปที่ form_builder แล้วกรอง line_oa_id เพราะตาราง form_submission ไม่มีคอลัมน์เจ้าของ

เมนูที่เกี่ยวข้องกันในเส้นทางนี้​

  • Member Database — เป็นเงื่อนไขบังคับของ Profile Mapping และเป็นเงื่อนไขทางอ้อมของ OTP ด้วย (ปิด Profile Mapping = แท็บยืนยันตัวตนเปิดไม่ได้) และเป็นแหล่งของ "คอลัมน์ระเบียน" ที่ OTP จะส่งรหัสไปให้
  • Settings > OTP Verification — เป็นชั้นล่างที่บอกว่า OA นี้ส่ง OTP ได้ทางช่องทางใด แท็บยืนยันตัวตนอ่าน smsConfigured/emailConfigured จากที่นี่มาตัดสินว่าจะเปิดให้เลือกได้หรือไม่ และ backend ยังตรวจซ้ำอีกชั้นตอนบันทึกฟอร์ม
  • Rich menu — action rich_menu ของหน้าขอบคุณต้องอ้างริชเมนูที่เป็นของ OA ของฟอร์มและมีสถานะ active (หน้าเว็บดึงมาแค่ 100 รายการแรก)
  • Segment audience — เลือกฟอร์มนี้เป็นเงื่อนไขสร้างกลุ่มเป้าหมายจากคำตอบได้ (ตรวจแล้วในเมนูนั้นมี section ของ form-builder โดยเฉพาะ)
  • Attribute Setup — แหล่ง custom attribute ที่ใช้เป็นปลายทางใน Field Mapping และ attribute "ยืนยันแล้ว" ของ Profile Mapping — แต่มีข้อควรรู้: หน้าจับคู่ฟิลด์ของ Form builder ไม่ได้อ่านจาก /attribute-master โดยตรง มันอ่านจาก GET /template-message/merge-tags แล้วคัดเอาคีย์ที่ขึ้นต้นด้วย custom. มาใช้

จุดที่ควรระวัง​

  • ชื่อฟอร์มไม่สามารถซ้ำกับ OA อื่นได้ (เพราะ form_hash unique ระดับฐานข้อมูล) และฟอร์มที่ลบแล้วยังจองชื่อไว้ — เจอ error 500 ที่อ่านไม่รู้เรื่องได้จากสองกรณีนี้ ให้เปลี่ยนชื่อเป็นแบบที่จำเพาะกับ OA ของตัวเองไว้ก่อน
  • ข้อจำกัดที่มีแค่ฝั่งหน้าเว็บ ไม่มีการตรวจซ้ำที่ backend เลย ได้แก่ จำนวนคำถามสูงสุด 10 ข้อ, ความยาวชื่อฟอร์ม 200 ตัวอักษร / คำอธิบาย 250 / ชื่อคำถาม 200, กฎ "ข้อมูลส่วนบุคคลชนิดละ 1 ข้อ", กฎ "ต้องมีคำถามอย่างน้อย 1 ข้อและต้องมีข้อที่บังคับกรอก" และกฎขนาด/สัดส่วนรูปหน้าปก — เรียก API ตรงข้ามข้อจำกัดเหล่านี้ได้ทั้งหมด
  • ข้อความ "คุณสามารถเพิ่มตัวเลือกได้สูงสุด 10 ตัวเลือก" ในหน้าตั้งคำถามเป็นข้อความอธิบายเท่านั้น ปุ่มเพิ่มตัวเลือกไม่ได้ถูกปิดที่ตัวเลือกที่ 10 จึงใส่เกินได้จริง
  • เผยแพร่แล้วกลับเป็น draft ไม่ได้ — ตรวจสอบให้ครบก่อนกดเผยแพร่ ถ้าต้องการหยุดรับคำตอบชั่วคราวให้เปลี่ยนเป็น inactive แทน
  • การกด "ทั้งหมด" บนการ์ดสถิติไม่ได้ล้างตัวกรอง แต่ส่ง submitStatus=total ไปที่ API ตรง ๆ (ไม่ใช่ค่า complete/incomplete ที่ backend รู้จัก) — ผลลัพธ์ที่ได้จึงเท่ากับไม่กรองอะไร เพราะ backend ตกลงมาที่กรณี default
  • หน้าแก้ไขฟอร์ม (/form-builder/form) ตรวจสิทธิ์ด้วย subject line-oa-management ไม่ใช่ form-builder เหมือนหน้ารายการและหน้าคำตอบ — เป็นความไม่สอดคล้องที่ควรทราบเมื่อจัดสิทธิ์ให้ผู้ใช้
  • การลบฟอร์มในหน้ารายการเป็น soft delete และ error ที่เกิดขึ้นระหว่างลบถูกกลืนไปเงียบ ๆ (catch เปล่า) — ถ้าฟอร์มไม่หายจากตารางให้รีเฟรชและตรวจดูอีกครั้ง

สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)​

  • ฝั่งหน้าเว็บ — หน้ารายการและหน้าคำตอบถูกครอบด้วย PermissionGuard ตรวจ action VIEW ของ subject form-builder แต่ หน้าฟอร์มแก้ไขตรวจ subject line-oa-management (ดูจุดที่ควรระวัง) ระบบสิทธิ์ฝั่งหน้าเว็บมีเพียง action VIEW และ EXPORT จึงไม่มีการแยกสิทธิ์สร้าง/แก้/ลบในระดับ UI
  • ฝั่งเซิร์ฟเวอร์ — ทั้ง 9 เส้นทางของโมดูลนี้ถูกครอบด้วย ModuleGate("form-builder") ที่บังคับใช้จริง ถ้าผู้ดูแลระดับแพลตฟอร์มปิดโมดูลนี้ให้องค์กรใด ผู้ใช้ประเภท customer ในองค์กรนั้นจะได้ 403 ทันทีแม้เรียก API ตรง (ผู้ใช้ประเภท onemoby ข้ามด่านนี้ได้เสมอ และด่านนี้ออกแบบให้ fail open — ถ้าอ่านค่าไม่สำเร็จจะปล่อยผ่าน ไม่ใช่ปฏิเสธ)
  • policy annotation ระดับการกระทำมีประกาศไว้เพียง 2 เส้นทาง (create บน POST /form-builder และ update บน PUT /form-builder/:id) และวิ่งผ่าน middleware ที่เป็น no-op ทั้งหมด — internal/modules/auth/policy.go ระบุตรงว่าการบังคับ policy ระดับ API ถูกปิดไว้โดยเจตนา (PoliciesGuard.canActivate คืนค่า true เสมอในต้นทาง TS) permission มีผลแค่กับการซ่อน/แสดงเมนูในหน้าเว็บ
  • ข้อสังเกตที่แก้ไขจากเอกสารเชิงเทคนิคชุดเดิม — เอกสารเดิมบรรยาย Form builder ว่าเป็นโมดูลที่ "บังคับสิทธิ์เข้มที่สุดในระบบ" ซึ่งไม่ตรงตามตัวอักษร: ปัจจุบันมี 15 โมดูล ที่มี ModuleGate เหมือนกัน (campaign, rich-message, rich-menu, audiences, workflow, attribute-master, customer-database, import-mapping, auto-response, friend-track, template-message, api-key, trigger-rule, report-all-friends และ form-builder) และโมดูล attribute-master ยังประกาศ policy ระดับการกระทำครบทั้ง 10 เส้นทางมากกว่า form-builder ด้วย สิ่งที่ทำให้ Form builder เข้มจริงคือการตรวจสอบภายในบริการ ไม่ใช่ middleware — ตรวจกรรมสิทธิ์ข้าม OA เอง (ตอบ 404 ไม่ใช่ 403), ตรวจกรรมสิทธิ์ริชเมนูด้วย query แยก, ด่านกันการเปิด OTP นอกช่องทาง, allowlist สกีมา URL, deny-list เนื้อหา และตัวตรวจ closed-shape ของ thankYou/profileMapping.otp ซึ่งไม่มีโมดูลอื่นในระบบทำครบชุดนี้
  • ไม่มีเส้นทางใดในโมดูลนี้เป็น @Public() ทุกเส้นทางอยู่หลัง JwtAuthGuard ระดับ global

ไฟล์อ้างอิงจากซอร์สโค้ด​

Frontend (cms-web-2026)

  • src/app/form-builder/page.tsx, src/app/form-builder/form/page.tsx, src/app/form-builder/response/page.tsx — จุดเข้าและ PermissionGuard ของแต่ละหน้า
  • src/components/form-builder/list/ — form-builder.container.tsx, form-builder-filter.tsx, form-builder-table.tsx
  • src/components/form-builder/form/ — form-builder-form.tsx (การตรวจสอบก่อนบันทึกทั้งชุด), hooks/useFormBuilder.ts, hooks/useQuestionOperations.ts (กฎจำนวนคำถาม), components/ProfileMappingSettings.tsx, verification/verification-form.tsx (แท็บ OTP), thank-you-form/, theme-form/form-colors-section.tsx (คำเตือนคอนทราสต์)
  • src/components/form-builder/form/constants/form-builder.constants.ts — MAX_QUESTIONS, กลุ่มชนิดคำถาม
  • src/components/form-builder/constants/theme-form.constants.ts — ค่าสีเริ่มต้น 7 ค่าและกฎรูปหน้าปก
  • src/components/form-builder/response/ — การ์ดสถิติ, ตารางคำตอบ, การส่งออก CSV

Backend (line-management-cms-api-go)

  • internal/modules/formbuilder/controller.go — 9 เส้นทาง, ModuleGate("form-builder"), การอ่านฟอร์มแบบ multipart
  • internal/modules/formbuilder/service.go — form_hash, การตรวจชื่อซ้ำ, กรรมสิทธิ์ข้าม OA, ตรรกะ 500 ของ theme, CSV export
  • internal/modules/formbuilder/dto.go — กฎตรวจข้อมูลขาเข้าและกฎ mime type ของรูปหน้าปก
  • internal/modules/formbuilder/otp.go — closed shape ของ profileMapping.otp และด่านตรวจ otp_config
  • internal/modules/formbuilder/thankyou.go — closed shape ของ thankYou, allowlist URL, deny-list เนื้อหา
  • internal/modules/formbuilder/contrast.go — สูตร WCAG, ค่าสีเริ่มต้น, เหตุผลที่ยังไม่บังคับคอนทราสต์

ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ สร้างฟอร์มและคำตอบ (Form Builder)