eCourt-note/app/api/template/test/note.md
Your Name b41816e2b6
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 53s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
feat(template): add restrict-edit protection script
สคริปต์ทดลอง make-protected.mjs ตามแนวทาง Token-Based ใน note.md:
- เปิด documentProtection readOnly + enforcement ใน settings.xml
- ครอบ Content Control ทุกช่องด้วย permStart/permEnd (edGrp=everyone)
  ให้แก้ได้เฉพาะในช่อง + เติม w:lock=sdtLocked กันลบช่อง
- ฝัง custom properties (token/primary-key/owner) ที่ docProps/custom.xml
  พร้อมแก้ [Content_Types].xml และ _rels/.rels ให้ครบ
- verify อ่านไฟล์กลับทุกครั้งหลังเขียน (exit 1 ถ้าไม่ผ่าน)
- บันทึกบทเรียน/ผลทดสอบ Word จริง รอบ 1-2 ใน note.md
  พร้อม T3.docx/output-T3.docx อัปเดตตามผลรัน

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-07 16:55:30 +07:00

14 KiB

Docx Template

lib-tmpl.ts เป็นแชร์ไลบารีสำหรับการทำ Template ใช้ Content Control ใน MS Word

วิธีการใช้ API

## Swagger UI — เปิดในเบราว์เซอร์ (ลองยิงจริงผ่านปุ่ม Try it out ได้เลย ไม่ต้อง login)
http://localhost:3000/docedit/api/swagger

## template/test ไว้ทดสอบเท่านั้น
curl.exe http://localhost:3000/docedit/api/template/test

## extract ค่าออกมาจาก content control
curl.exe -X POST http://localhost:3000/docedit/api/template/extract -F "file=@./output-T3.docx"

## render เอกสาร (docx) = template(docx)+data(json)  
### อาจจะเพิ่มการเอา template จาก url และ file(local) ยังไม่รู้ว่าทางนั้นจะส่งไฟล์แบบไหนมาให้ เอาแค่นี้ไว้ทดสอบก่อน  
curl.exe -X POST http://localhost:3000/docedit/api/template/render -F "template=@./T3.docx" -F "data=@./T2.json" -o output.docx
### data เอาจาก string
curl -X POST http://localhost:3000/docedit/api/template/render \
  -F "template=@template.docx" \
  -F 'dataJson={"member_name":"นายสมชาย","member_id":"123456"}' \
  -o generated-document.docx

data(json) ตอนนี้อยู่ในรูปแบบ

{
  "notname": "คำสั่งเรียก ให้ทำคำให้การ",
  "nottype": "(ต. ๑)",
  "blackno": "คดีหมายเลขดำที่",
  "blackyear": "๑๐/๒๕๖๙",
  ...
}

data(json)น่าจะปรับเป็นแบบนี้เพื่อรองรับการทำ Token-Based Seamless Integration หลักผ่าน API

  • ส่วนของ cc จะไปกรอกใน Content Control
  • uid,uname,fid,ott,expire สร้างเป็น token ฝังใน custom property ของเอกสาร
{
  "uid":"id ผู้มีสิทธิ์แก้ไขเอกสาร",
  "uname": "ชื่อผู้มีสิทธิ์แก้ไขเอกสาร สำหรับแสดงผล",
  "fid":"file id จะได้เซฟข้อมูลกลับไปที่ Oracle ถูกตารางและหน่วยงาน",
  "ott":"อายุของ one time token แบบระยะสั้น",
  "expire": "อายุเอกสารแบบระยะยาวหลังเอา ott ไปแลก (optional)",
  cc:{
    "notname": "คำสั่งเรียก ให้ทำคำให้การ",
    "nottype": "(ต. ๑)",
    "blackno": "คดีหมายเลขดำที่",
    "blackyear": "๑๐/๒๕๖๙",
    ...
  }
}

ที่ต้องทำเพิ่ม

  • render ปัจจุบันมีไว้ให้ทางศาลทดสอบ อันที่ใช้จริงหลังกดสร้าง Template เพื่อ Download จะสร้างเอกสารเวอร์ชั่นแรกใน object storage หรือถ้ามีอยู่แล้ว จะเป็นการอัปเดต
  • Backend จัดการ version
  • อัปเดตข้อมูลและเอกสารกลับระบบเดิม ส่วนนี้ยังไม่ชัดเจนอาจจะทำ mock api เตรียมไว้ก่อน

ทดลอง Restrict Edit + Custom Properties (2026-10-07)

สคริปต์ make-protected.mjs — อ่าน docx → เปิด protection + ฝัง custom properties → เขียนไฟล์ → อ่านกลับ verify ทุกครั้ง

node app/api/template/test/make-protected.mjs [in.docx] [out.docx]
# ไม่ส่ง argument = อ่านและเขียนทับ output-T3.docx (ค่าตัวอย่างฝังอยู่ในตัวสคริปต์ CUSTOM_PROPS)

Restrict Edit (เปิดผ่านโค้ด)

  • อยู่ที่ word/settings.xml → <w:documentProtection w:edit="readOnly" w:enforcement="1"/>
  • T3.docx มี element นี้อยู่เดิมแต่เป็น w:enforcement="0" (ตั้งค่าไว้ใน Word แต่ไม่ได้กดบังคับใช้) — สคริปต์แค่พลิกเป็น 1 ไม่ต้องแทรก element ใหม่
  • บทเรียนจากทดสอบใน Word จริง (2026-10-07): readOnly + enforcement ล็อคทั้งเอกสารรวมถึง Content Control ด้วย — CC ไม่ได้ถูกยกเว้นให้แก้อัตโนมัติ ต้องคู่กับข้อถัดไป
  • กลไกที่ถูกต้อง: ครอบ w:sdt ทุกตัวที่มี w:tag ด้วย <w:permStart w:id="N" w:edGrp="everyone"/> … <w:permEnd w:id="N"/> — คือกลไก "Exceptions (optional): Everyone" ที่แผง Restrict Editing ของ Word ใช้เวลาเราเลือกบริเวณแล้วติ๊ก Everyone (บริเวณใน permission range แก้ได้ นอกนั้นล็อค) — แทรกที่ระดับเดียวกับ sdt (inline ใน w:p หรือ block ใน w:body ก็ valid ตาม grammar)
  • สคริปต์เติม w:lock="sdtLocked" ให้ CC ที่ยังไม่มีด้วย (T3 ขาด acc1, nottitle) — พอช่องถูกเปิดให้แก้ผ่าน permission แล้ว ช่องที่ไม่มี lock จะถูกลบได้ตามไปด้วย
  • ไม่ใส่ password → ผู้ใช้ปลดเองได้จาก Review → Restrict Editing → Stop Protection — ตั้งใจไว้แบบนี้เพราะ concept คือ token ใน custom property เป็นตัวกำกับสิทธิ์จริง ส่วน protection เป็นแค่เกราะกันแก้พลาด (ถ้าจะใส่ password ต้องคำนวณ legacy hash: SHA-1 ของ password+salt วนตาม w:cryptSpinCount ตาม ECMA-376 — ยังไม่ทำ)

ฝังข้อมูลใน Property เอกสาร (token / primary-key / owner)

ทางเลือกที่ศึกษา:

  1. docProps/custom.xml (custom properties) — ที่เลือกใช้ เห็นได้ใน Word: File → Info → Properties → Advanced Properties → แท็บ Custom; add-in ฝั่ง Word (Office.js) อ่าน/เขียนได้ผ่าน Document.properties.customProperties (WordApi 1.3+)
    • ไฟล์ที่ไม่มี part นี้อยู่ (กรณีปกติของ docx) ต้องทำครบ 3 อย่างพร้อมกัน ไม่งั้น Word ถือว่าไฟล์เสีย:
      1. สร้าง docProps/custom.xml — property แต่ละตัว: fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" คงที่ + pid ≥ 2 ไม่ซ้ำ + name + ค่า string ใน vt:lpwstr
      2. [Content_Types].xml เพิ่ม <Override PartName="/docProps/custom.xml" ContentType="application/vnd.openxmlformats-officedocument.custom-properties+xml"/>
      3. _rels/.rels เพิ่ม Relationship ชนิด .../relationships/custom-properties Target docProps/custom.xml
  2. docProps/core.xml (core properties: title/subject/creator/description) — แก้ง่าย เห็นในหน้า Properties ปกติ เหมาะกับข้อมูลที่แสดงต่อผู้ใช้ ไม่ใช่ token
  3. w:docVar ใน word/settings.xml (document variables) — ไม่โผล่ใน UI เลย อ่านได้ผ่าน VBA/VSTO (Document.Variables) แต่ Word JS API ไม่มีทางตรง — ไม่เลือก

gotcha ที่เจอ: XMLSerializer ของ @xmldom/xmldom คง <?xml ...?> declaration ไว้ในผลลัพธ์อยู่แล้ว — ห้าม prepend เพิ่มเอง จะได้ declaration ซ้อนเป็น XML invalid (Word/node อ่านไม่ได้)

extract บอก alignment รายช่อง — key <tag>@align (2026-10-07) — ถอดออกแล้วตามคำขอ (ยังไม่เอา)

  • เคย implement ใน lib-tmpl.ts + spec lib/openapi.ts แล้วถอดกลับสถานะเดิม — extract ปัจจุบันคืนแค่ { tag: text } เหมือนเดิม
  • แนวทางที่พิสูจน์แล้วว่าใช้ได้ (เผื่อกลับมาทำภายหลัง): คืน key <tag>@align (ค่า w:jc เช่น thaiDistribute) เฉพาะช่องที่ต่างจาก default ของเอกสาร โดย resolve: w:pPr/w:jc ตรงของย่อหน้า → style ที่ใช้ (ไล่ w:pStyle → w:basedOn) → docDefaults ("left"/"start" นับเป็นไม่ตั้ง) — บริบทย่อหน้าของช่อง: CC block = w:p แรกใน w:sdtContent / CC inline = w:p ที่ครอบ sdt
  • ผลทดสอบตอน implement: direct pPr ชนะ style ✓ inline/block จับได้ทั้งคู่ ✓ และ T3 จัดช่อง 8 ตัวผ่าน style อยู่แล้ว (nottype/blackyear/redyear/acc1/acc2/groupname/detail1/pname — ชิดขวา/กลาง) — ช่องเหล่านี้จะโผล่ @align ตามจริง

จัด paragraph แบบ Thai Distributed — ตัดสินใจ: จัดเองใน Word ตอนทำ template (2026-10-07)

  • ค่า OOXML คือ <w:jc w:val="thaiDistribute"/> ใน w:pPr (ใน Word UI: Paragraph → Alignment → Thai Distributed, ทางลัด Ctrl+Shift+J) — ต่างจาก distribute ธรรมดาตรงที่จัดกระจายช่องว่างระหว่างคำเป็นหลัก ไม่บีบยืดตัวอักษร/สระ-วรรณยุกต์
  • ไม่ตั้งผ่านโค้ด — ผู้ออกแบบ template จัดเองใน Word ตอนทำ template ทางเดียวกับที่ตั้งฟอนต์/ย่อหน้าอื่น ๆ (เคยทดลองตั้ง default ทั้งเอกสารผ่าน styles.xml → w:docDefaults/w:pPrDefault/w:pPr แล้วถอดออกตามคำขอ)
  • วิธีจัดเองใน Word:
    • รายย่อหน้า/ราย CC: คลิกในย่อหน้า → Ctrl+Shift+J หรือ Home → Paragraph launcher → Alignment เลือก Thai Distributed
    • ทั้งเอกสาร: คลิกขวา style Normal → Modify → Format → Paragraph → Thai Distributed
    • จัด "เฉพาะช่อง CC" ได้เฉพาะ CC แบบ block (มี w:p ใน w:sdtContent ของตัวเอง เช่น detail1) — CC แบบ inline อยู่ในบรรทัดเดียวกับข้อความอื่น การจัดมีผลทั้งบรรทัด
  • จุดสำคัญต่อ render pipeline: insertBlockContent ของ lib-tmpl clone w:pPr ของย่อหน้าแรกให้ทุกบรรทัดที่แทนค่า — จัดไว้ใน CC ตั้งแต่ต้นค่าจะสืบทอดไปทุกบรรทัดที่ render ใส่ข้อมูลอัตโนมัติ ไม่ต้องทำอะไรฝั่ง render
  • (อ้างอิงเชิงเทคนิคกรณีต้องตั้งผ่านโค้ดภายหลัง: w:jc ใน w:pPr เป็น fixed sequence — ต้องอยู่หลัง w:autoSpaceDN/w:spacing/w:ind และก่อน w:textDirection/w:rPr/w:sectPr)

ผล verify (อัตโนมัติทุกครั้งที่รันสคริปต์)

  • documentProtection เปิดใช้ (readOnly + enforcement=1) ✓
  • docProps/custom.xml มี + relationship + content-type override ครบ ✓
  • Content Control 27 ตัวครบเดิม (plain 26 = มี w:text, rich 1 = detail1) ✓
  • permission ranges: permStart/permEnd ครบตามจำนวน CC ที่มี tag (27/27) ✓
  • ทุก CC มี w:lock="sdtLocked" (กันลบช่อง) ✓
  • extractContentControls (lib-tmpl) อ่านคืนได้ทุก tag ตรงกับ output.docx ก่อน protect 100% ✓

ผลทดสอบใน Word จริง (2026-10-07 รอบ 1)

  • ✅ แถบเหลือง protection ขึ้น / แก้นอก CC ไม่ได้ / ลบ CC ไม่ได้
  • ❌ B5: CC แก้ไม่ได้ เพราะ readOnly ล็อคทั้งเอกสาร → แก้ด้วย permStart/permEnd ตามด้านบนแล้ว (รอบ 2)
  • ✅ custom properties ครบ / ปลด protection ได้แบบไม่มี password

เหลือตรวจใน Word จริง (รอบ 2)

  • เปิด output-T3.docx → พิมพ์/แก้ในช่อง CC ต้องได้แล้ว (B5)
  • ลบ CC ต้องไม่ได้ทุกตัว (รวม acc1, nottitle ที่เติม lock ให้)
  • ขั้น E เดิม: F12 เซฟสำเนา → เปิดใหม่ → properties ยังอยู่ครบ
  • (ผ่านแล้วทั้งหมด → ย้าย logic ไปเป็นฟังก์ชันใน lib-tmpl.ts แล้วต่อเข้า render pipeline)