Skip to main content

Settings — User Attribute Setup

ภาพรวม​

User Attribute Setup (ฝั่ง backend เรียก attribute-master) คือหน้าประกาศ schema กลางของ custom attribute ที่ผูกกับผู้ใช้ LINE ของแต่ละ OA ล่วงหน้า — กำหนดว่าจะเก็บฟิลด์อะไร ชนิดข้อมูลใด และฟิลด์ไหนนำไปกรอง audience หรือแสดงในรายงานได้

โครงสร้างมีสองระดับที่ควรแยกให้ชัด: ชุด attribute (attribute master) หนึ่งรายการมีชื่อ คำอธิบาย และสถานะ ภายในนั้นบรรจุ ฟิลด์ (attribute) เป็น tree ได้ ระบบอ้างถึงฟิลด์ด้วย key (สร้างอัตโนมัติ ไม่แสดงและแก้ไม่ได้) และให้คนอ่านด้วย label

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

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

1. ดูรายการชุด attribute​

เปิดเมนู Settings > User Attribute Setup ตัวกรองมี 2 ช่อง — ค้นหาจากชื่อ และเลือกสถานะ (ทั้งหมด / active / inactive) ค่าที่กรองถูกจดจำใน sessionStorage ตารางแสดงชื่อ, คำอธิบาย, จำนวนฟิลด์ (นับเฉพาะระดับบนสุด ไม่นับฟิลด์ลูก), สถานะ, และวันที่สร้าง กดชื่อเพื่อเปิดดูแบบอ่านอย่างเดียว หรือกดไอคอนดินสอเพื่อแก้ไข

2. สร้างด้วยมือ​

  1. กดปุ่ม Create
  2. กรอกชื่อชุด attribute (บังคับ ไม่เกิน 255 ตัวอักษร), คำอธิบาย (ไม่บังคับ) และสถานะ (ค่าเริ่มต้น active)
  3. เพิ่มฟิลด์ในตัวแก้ไข schema แบบ tree — ตั้ง label, เลือกชนิดข้อมูล (string / number / date / boolean / array / object) และเปิดสวิตช์ filterable และ displayInReport ตามต้องการ
    • ฟิลด์ชนิด array/object เปิดสองสวิตช์นี้ไม่ได้ (สวิตช์ถูก disable)
    • ฟิลด์ใหม่ที่กดเพิ่มมาได้ค่าเริ่มต้นเป็น filterable = เปิด, displayInReport = เปิด
    • ปุ่มเพิ่มฟิลด์เพิ่มได้เฉพาะระดับบนสุด — ตัวแก้ไขในหน้านี้ไม่มีทางสร้างฟิลด์ลูก ฟิลด์ที่เป็น tree มาได้จากการนำเข้า JSON หรือจากข้อมูลเดิมเท่านั้น
  4. บันทึก ระบบตรวจสองอย่างก่อนส่ง — ทุกฟิลด์ต้องมี label ที่ไม่ว่าง และ label ต้องไม่ซ้ำกันทั้ง tree (เทียบแบบไม่สนตัวพิมพ์เล็ก/ใหญ่)

3. นำเข้าจาก JSON​

ปัจจุบันเส้นทางนี้ถูกปิดไว้ด้วย feature flag (ATTRIBUTE_JSON_IMPORT_ENABLED = false) ปุ่ม Import from JSON จะไม่ปรากฏ และถ้าเข้า URL ตรง ระบบจะ redirect กลับหน้ารายการ โค้ดยังอยู่ครบ เมื่อเปิดใช้ขั้นตอนจะเป็น

  1. ขั้นที่ 1 — กรอกชื่อ คำอธิบาย และ root path (ไม่บังคับ ใช้เมื่อ attribute ที่ต้องการอยู่ใน object ย่อย เช่น RetrieveAgentDetailsResponse.RCMSAgent) แล้ววาง JSON ตัวอย่าง
  2. กด Parse & Continue — หน้าเว็บ JSON.parse ตรวจรูปแบบเบื้องต้นแล้วส่งไปให้ backend เป็นผู้แปลงเป็น schema ทั้งหมด หน้าเว็บไม่ได้เดาชนิดข้อมูลเอง
  3. ขั้นที่ 2 — ตรวจสอบและปรับ label / ชนิดข้อมูล / สวิตช์ที่ระบบแปลงมาให้ แล้วกด Save

4. ใช้งานต่อในเมนูอื่น​

  1. เปิดเมนู Segment audience > Multi-source filter เลือก source ชนิด custom attribute — ระบบเรียก GET /attribute-master/filterable ซึ่งคืน attribute พื้นฐานของระบบ 10 ค่าขึ้นก่อน (Display Name, First Name, Last Name, Email, Mobile No, Language, Follow Status, User Type, Registered Date, Last Activity Date) แล้วต่อด้วย custom attribute ที่เปิด filterable โดยตั้ง label เป็น [ชื่อชุด] label และ path เป็น ชื่อชุด.path
  2. เปิดเมนู All friends — attribute ที่เปิด displayInReport จะกลายเป็นคอลัมน์เพิ่มในตารางโดยอัตโนมัติ (เรียก GET /attribute-master/reportable ซึ่งคืนเฉพาะ custom attribute ไม่มี attribute พื้นฐาน) ส่วนหน้าต่างรายละเอียดเพื่อน (detail modal) อ่านรายการเต็มแทน และยังคำนวณ "attribute กำพร้า" ให้ดู — คีย์ที่มีข้อมูลอยู่ในตัวผู้ใช้แต่ไม่มีอยู่ใน schema แล้ว
  3. เปิดเมนู Workflow — แผงตั้งค่า node ดึงรายการเต็มโดยกรองเฉพาะชุดที่สถานะ active (เป็นผู้บริโภครายเดียวที่กรองสถานะ) และสร้าง/แก้ attribute จากในแผงได้เลยโดยไม่ต้องกลับมาหน้านี้
  4. เปิดเมนู Friends Track — ดึงรายการเต็ม (จำกัด 100 ชุดแรก) แล้วสร้างตัวเลือกจากฟิลด์ระดับบนสุดเท่านั้น เป็น ชื่อชุด.key

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

ยืนยันจาก internal/modules/attributemaster/{controller,service,dto,parser}.go และ internal/modules/planlimits/service.go ของ line-management-cms-api-go:

  • key ของ attribute เป็นค่าที่แก้ไม่ได้ตลอดอายุ และ backend บังคับไว้จริง — คอมเมนต์ในตัวแก้ไขฝั่งหน้าเว็บระบุตรงว่า key เป็น id ภายในที่ "ตั้งใจไม่ให้แสดงและแก้ไข" ส่วนฝั่ง backend มีการตรวจซ้ำโดยเทียบ schema ใหม่กับ schema ที่เก็บไว้: ถ้าพบฟิลด์ที่ label เดิมยังอยู่แต่ key เปลี่ยนไป จะตอบ 400 พร้อมข้อความ Attribute keys cannot be changed. "<label>" already exists — edit its label or delete it instead of changing its key.
  • ความไม่ซ้ำของ label และ key ถูกตรวจ "ข้ามชุด attribute" ทั้ง OA ไม่ใช่แค่ในชุดเดียว — นี่คือส่วนที่หน้าเว็บตรวจไม่ครบ: หน้าเว็บตรวจแค่ภายในชุดที่กำลังแก้อยู่ แต่ backend วนดูทุกชุดที่สถานะ active ใน OA เดียวกัน แล้วปฏิเสธด้วย Attribute key "<key>" already exists in attribute master "<ชื่อชุดอื่น>" หรือ Label "<label>" already exists in attribute master "<ชื่อชุดอื่น>" — จึงเป็นไปได้ที่ฟอร์มผ่านการตรวจฝั่งหน้าเว็บแล้วมาโดน 400 ตอนกดบันทึกจริง
  • ชื่อชุด attribute ต้องไม่ซ้ำ — ตรวจทั้งตอนสร้างและตอนเปลี่ยนชื่อ ตอบ Attribute master with this name already exists
  • โควตาแพ็กเกจมีจริงและนับ "ทุกฟิลด์ในทุกระดับ" — AssertAttributeQuota ทำงานทั้งตอนสร้างและตอนแก้ schema โดยนับจำนวนฟิลด์แบบ flatten (นั่นคือฟิลด์ลูกทุกชั้นถูกนับด้วย ไม่ใช่แค่ระดับบนสุดเหมือนตัวเลขในตารางรายการ) แล้วบวกกับที่มีอยู่แล้ว ถ้าเกินจะตอบ 400 ว่า Plan limit reached: this would use N of M attributes. Please upgrade your plan. โควตานี้คิด ต่อ LINE OA เมื่อ token มี lineOaId (ซึ่งเป็นกรณีปกติ) และคิดต่อองค์กรเมื่อไม่มี ค่า -1 หมายถึงไม่จำกัด และหน้าเว็บไม่ได้แสดงตัวเลขโควตาไว้ที่ใดเลย จึงเห็นข้อความนี้ครั้งแรกตอนกดบันทึกไม่ผ่าน
  • ชนิด object/array ถูกตัดออกจากผลลัพธ์ filterable/reportable ที่ฝั่งเซิร์ฟเวอร์อีกชั้น — ไม่ได้อาศัยแค่การ disable สวิตช์ในหน้าเว็บ ดังนั้นฟิลด์ที่เคยเปิดสวิตช์ไว้ตอนเป็น string แล้วภายหลังเปลี่ยนชนิดเป็น object จะยังมีค่า filterable: true ค้างอยู่ใน schema แต่จะไม่ถูกส่งออกไปให้เมนูอื่นใช้
  • ค่าเริ่มต้นของสวิตช์ไม่ตรงกันใน 3 ที่ที่สร้าง attribute ได้ — ตัวแก้ไขในหน้านี้ตั้ง filterable: true, displayInReport: true, ตัวแปลง JSON ที่ backend ตั้ง filterable: true, displayInReport: false, และแผงตั้งค่า node ใน Workflow ตั้งทั้งสองค่าเป็น false — ควรตรวจสวิตช์ทุกครั้งหลังสร้าง attribute จากช่องทางอื่นที่ไม่ใช่หน้านี้
  • ตัวแปลง JSON เดาชนิดข้อมูลจากค่าตัวอย่าง และเดา label จากชื่อคีย์ — สตริงที่ขึ้นต้นด้วยรูปแบบ YYYY-MM-DD (มีหรือไม่มีเวลาต่อท้ายก็ได้) ถูกตัดสินเป็นชนิด date ตัวเลขเป็น number บูลีนเป็น boolean array/object เดินเข้าไปสร้างฟิลด์ลูกให้ (array จะสร้างลูกจากสมาชิกตัวแรกและต่อ path ด้วย []) ส่วน label มาจากการแปลงชื่อคีย์เป็น Title Case (แยกคำจาก camelCase และ _) — agentName จึงกลายเป็น "Agent Name"
  • การตรวจผลกระทบก่อนลบใช้การค้นข้อความในทั้ง JSON จึงเกิดผลบวกเท็จได้ง่าย — GET /attribute-master/:id/check-attribute-usage นับ 4 อย่าง: จำนวนผู้ใช้ที่มีคีย์นี้ใน custom_attribute (ตรวจแบบ jsonb existence จึงแม่นยำ), trigger rule (เทียบ source_config->>'key' กับ custom.<key> แบบตรงตัว จึงแม่นยำ) แต่ audience และ rich message ใช้ LIKE '%<key>%' บนข้อความ JSON ทั้งก้อน — คีย์ที่สั้นหรือทั่วไป (เช่น id, name, type) จะไปตรงกับ audience/rich message แทบทุกรายการ ทำให้กล่องคำเตือนโชว์รายการที่ไม่เกี่ยวข้องจำนวนมาก
  • กล่องคำเตือนก่อนลบไม่บล็อกการลบเลย — ปุ่มยืนยันเขียนว่า "Delete Anyway" และไม่ถูก disable ในทุกกรณี แถมถ้าการเรียกตรวจผลกระทบล้มเหลว หน้าเว็บจะ กลืน error แล้วสร้างผลลัพธ์ปลอมที่เป็นศูนย์ทั้งหมด — "ตรวจไม่สำเร็จ" จึงหน้าตาเหมือน "ไม่มีการใช้งาน" ทุกประการ นอกจากนี้ ถ้าชุด attribute ยังไม่ถูกบันทึก (หรืออยู่ในเส้นทางนำเข้า JSON) ระบบจะลบฟิลด์ทันทีโดยไม่เรียกตรวจและไม่แสดงกล่องเลย
  • การลบชุด attribute เป็น soft delete และไม่มีการตรวจผลกระทบใด ๆ — DELETE /attribute-master/:id ตรวจแค่ว่ามีรายการนี้อยู่จริง แล้ว soft delete ทันที ไม่ได้เรียก checkAttributeUsage ก่อน (การตรวจผลกระทบมีเฉพาะกับการลบฟิลด์รายตัวในตัวแก้ไข)
  • การส่ง orderBy เป็นชื่อคอลัมน์ที่ไม่รู้จักได้ 500 โดยเจตนา — โค้ดจำลองพฤติกรรม EntityPropertyNotFoundError ของ TypeORM แทนที่จะต่อสตริงลง ORDER BY ตรง ๆ (คอมเมนต์ระบุเหตุผลเรื่องความปลอดภัยไว้ชัด)
  • การบันทึกรักษาลำดับคีย์ของ JSON ที่ส่งเข้ามาจริง — controller ถอดรหัส body แบบ order-preserving เพื่อให้ลำดับฟิลด์ใน schema ที่เก็บลง jsonb ตรงกับที่ลูกค้าส่งมา (ตรงกับพฤติกรรม JSON.stringify ของต้นทาง TS) ลำดับที่เห็นในตัวแก้ไขจึงเป็นลำดับเดียวกับที่จะไปโผล่เป็นคอลัมน์ในรายงาน
  • POST /attribute-master/import-json ไม่มีผู้เรียกในหน้าเว็บเลย — เส้นทางนำเข้าจริงในหน้าเว็บใช้ POST /attribute-master/parse-json เพื่อดูตัวอย่าง แล้วส่ง schema ที่แก้แล้วผ่าน POST /attribute-master ปกติ ผลข้างเคียงที่ตามมาคือ เส้นทางนำเข้าไม่ผ่านการตรวจ label ซ้ำ/ว่างของหน้าเว็บ (การตรวจนั้นอยู่ใน onFinish ของฟอร์มสร้างด้วยมือเท่านั้น) — ต้องพึ่งการตรวจฝั่ง backend อย่างเดียว

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

  • Segment audience — อ่าน GET /attribute-master/filterable เมื่อเลือก source ชนิด custom attribute และเลือกตัวดำเนินการตามชนิดข้อมูลของ attribute ที่เลือก
  • All friends (รายงาน) — อ่าน GET /attribute-master/reportable มาสร้างคอลัมน์เพิ่มอัตโนมัติ และหน้าต่างรายละเอียดเพื่อนอ่านรายการเต็มเพื่อแก้ค่าได้ (ยกเว้นชนิด object/array)
  • Workflow — อ่านรายการเต็มโดยกรองสถานะ active และสร้าง/แก้ attribute ได้จากในแผง node
  • Friends Track — อ่านรายการเต็ม (100 ชุดแรก) แล้วสร้างตัวเลือกจากฟิลด์ระดับบนสุด
  • Trigger rule — ตรวจแล้ว ไม่ได้อ่าน attribute-master โดยตรง มันหาชนิดข้อมูลของเงื่อนไขจาก merge tag (GET /template-message/merge-tags) แต่การตรวจผลกระทบก่อนลบของหน้านี้จะไปนับ trigger rule ที่มี source_type = attribute_change และ key ตรงกับ custom.<key> ให้
  • Form builder (Field Mapping) และ BigQuery Sync (การแม็ปฟิลด์) — ทั้งคู่ใช้ namespace custom. เดียวกันแต่ ไม่ได้อ่านนิยามจากเมนูนี้: Form builder อ่านจาก merge tag ส่วน BigQuery Sync เป็นช่องพิมพ์อิสระที่มีคำนำหน้า custom. ติดไว้ให้ ไม่มีตัวเลือกและไม่มีการตรวจว่าคีย์นั้นมีอยู่จริง
  • Import Data (import-mapping) — ตรวจแล้ว ไม่ได้อ่าน attribute-master มันอ่านรายการฟิลด์ปลายทางจาก GET /import-mapping/fields แยกของตัวเอง

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

  • ลบฟิลด์ของรายการที่บันทึกไว้แล้ว ระบบจะแจ้งผลกระทบ (จำนวนผู้ใช้ที่มีค่าอยู่, trigger rule, audience, rich message ที่อ้างถึง) แต่เป็นข้อมูลประกอบการตัดสินใจเท่านั้น ไม่ได้บล็อกการลบจริง และตัวเลขฝั่ง audience/rich message มีโอกาสเป็นผลบวกเท็จสูงเมื่อคีย์สั้นหรือเป็นคำทั่วไป
  • ตัวเลข "จำนวนฟิลด์" ในตารางรายการนับแค่ระดับบนสุด แต่โควตาแพ็กเกจนับทุกระดับ — ชุดที่นำเข้าจาก JSON ซ้อนหลายชั้นจะกินโควตามากกว่าที่ตารางบอกเสมอ
  • label ต้องไม่ซ้ำกันข้ามชุด attribute ทั้ง OA ไม่ใช่แค่ในชุดเดียว — ถ้าเจอ 400 ตอนกดบันทึกที่หน้าเว็บไม่ได้เตือนไว้ล่วงหน้า ให้ตรวจว่ามีชุดอื่นใช้ label เดียวกันอยู่หรือไม่
  • key แก้ไม่ได้ตลอดอายุ วิธีเดียวคือลบฟิลด์แล้วสร้างใหม่ ซึ่งจะทำให้ข้อมูลของผู้ใช้ที่ผูกกับคีย์เดิมกลายเป็นข้อมูลกำพร้า (กล่องคำเตือนใช้คำว่า orphaned ตรง ๆ)
  • ตัวแก้ไขในหน้านี้ไม่มีทางสร้างฟิลด์ลูกเลย ถ้าต้องการโครงสร้าง tree ต้องใช้เส้นทางนำเข้า JSON — ซึ่งปัจจุบันถูกปิดด้วย feature flag
  • เปลี่ยนชนิดฟิลด์เป็น object/array ไม่ได้ล้างค่าสวิตช์ filterable/displayInReport เดิม แต่ backend จะไม่ส่งฟิลด์นั้นออกไปให้เมนูอื่นใช้ — ถ้า attribute หายไปจากตัวเลือกกรอง audience ให้ตรวจชนิดข้อมูลก่อน
  • หน้านี้และฟอร์มของหน้านี้ ไม่มี PermissionGuard ที่ฝั่งหน้าเว็บเลย และรายการในเมนูข้างก็ไม่ถูกกรองสิทธิ์ (ต่างจาก OTP Verification ที่กรอง) การป้องกันจริงจึงมาจาก ModuleGate ฝั่งเซิร์ฟเวอร์เท่านั้น

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

  • ฝั่งหน้าเว็บ — ไม่มี PermissionGuard ครอบทั้ง /attribute-setup และ /attribute-setup/form ตรวจยืนยันแล้วว่าไฟล์ทั้งสอง export component ตรง ๆ และรายการในเมนูข้างเป็นสมาชิกของ settingsChildren แบบไม่มีเงื่อนไข ทั้งที่มี mapping 'attribute-master' → [MODULE_URL.ATTRIBUTE_SETUP] เตรียมไว้แล้วก็ไม่มีอะไรเรียกใช้กับเส้นทางนี้
  • ฝั่งเซิร์ฟเวอร์ — ทุกเส้นทางถูกครอบด้วย ModuleGate("attribute-master") ที่บังคับใช้จริง โดยใช้ g.Use(...) ที่ระดับ group จึงครอบทั้ง 10 เส้นทางแน่นอน ถ้าผู้ดูแลระดับแพลตฟอร์มปิดโมดูลนี้ให้องค์กรใด ผู้ใช้ประเภท customer จะได้ 403 ทันทีแม้เรียก API ตรง (ผู้ใช้ประเภท onemoby ข้ามได้ และด่านนี้ fail open เมื่ออ่านค่าไม่สำเร็จ)
  • policy annotation ระดับการกระทำมีประกาศไว้ครบทั้ง 10 เส้นทาง (readAll, read, create, update, delete ของ subject AttributeMaster) — มากกว่าโมดูล Form builder ที่ประกาศเพียง 2 เส้นทาง — แต่ทั้งหมดวิ่งผ่าน middleware ที่เป็น no-op ตามที่ internal/modules/auth/policy.go ระบุไว้ การบังคับจริงจึงอยู่ที่ ModuleGate ระดับโมดูล ไม่ใช่ที่ policy ระดับการกระทำ
  • การจำกัดขอบเขตข้อมูลจริงเกิดที่ชั้น repository — ทุก query กรองด้วย organization_id และ line_oa_id ของผู้เรียก และ soft delete ถูกกรองอัตโนมัติ ส่วน checkAttributeUsage กรองด้วย line_oa_id เพียงค่าเดียว
  • ไม่มีเส้นทางใดเป็น @Public() ทุกเส้นทางอยู่หลัง JwtAuthGuard ระดับ global และไม่มีเส้นทางใดต้องเป็น Super Admin

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

Frontend (cms-web-2026)

  • src/app/attribute-setup/page.tsx, src/app/attribute-setup/form/page.tsx — จุดเข้า (ไม่มี PermissionGuard) และตัวกัน route ของเส้นทางนำเข้า JSON
  • src/components/attribute-setup/attribute-setup.flags.ts — feature flag ATTRIBUTE_JSON_IMPORT_ENABLED
  • src/components/attribute-setup/list/attribute-setup.container.tsx — ตาราง/ตัวกรอง/การนับฟิลด์ระดับบนสุด
  • src/components/attribute-setup/form/attribute-setup-form.container.tsx — การตรวจ label, เส้นทางนำเข้า JSON 2 ขั้น
  • src/components/attribute-setup/form/components/attribute-editor.tsx — ตัวแก้ไข tree, กฎ disable สวิตช์, ค่าเริ่มต้นของฟิลด์ใหม่
  • src/components/attribute-setup/form/components/attribute-delete-warning-modal.tsx — กล่องคำเตือนผลกระทบก่อนลบฟิลด์
  • src/services/attribute-master.service.ts — endpoint ทั้งหมดที่หน้าเว็บเรียกใช้

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

  • internal/modules/attributemaster/controller.go — 10 เส้นทาง, ModuleGate("attribute-master"), การถอดรหัส body แบบรักษาลำดับคีย์
  • internal/modules/attributemaster/service.go — การตรวจ key/label ข้ามชุด, การเรียกโควตา, checkAttributeUsage, /filterable และ /reportable
  • internal/modules/attributemaster/parser.go — การเดาชนิดข้อมูล, การแปลงชื่อคีย์เป็น label, การเดินฟิลด์ลูก
  • internal/modules/attributemaster/dto.go — กฎตรวจข้อมูลขาเข้า
  • internal/modules/planlimits/service.go — AssertAttributeQuota และการนับฟิลด์แบบ flatten

ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ ตั้งค่า Attribute (Attribute Setup)