# คู่มือผู้ใช้งาน (USER MANUAL)

## แผนภาพระบบ (Service Diagram)

```mermaid
flowchart LR
  subgraph fi["เครือข่ายของสถาบันการเงิน (on-prem)"]
    direction LR
    user["ผู้ใช้ฝ่ายธุรกิจ<br/>เบราว์เซอร์"]
    wf["ระบบอื่น เช่น<br/>Thinker Workflow"]

    subgraph td["Thinker Docs"]
      api["/v1/generate<br/>/v1/templates<br/>/v1/documents"]
      eng["เครื่องมือเรนเดอร์<br/>fontkit"]
      qpdf["qpdf<br/>ใส่รหัสผ่าน AES-256"]
    end

    db[("PostgreSQL<br/>เทมเพลต · เวอร์ชัน · audit")]
    obj[("Object storage<br/>PDF ต้นฉบับ + เอกสารที่สร้าง")]
  end

  user -- "เซสชัน (คุกกี้)" --> api
  wf -- "X-Api-Key" --> api
  api --> eng --> qpdf
  api <--> db
  api <--> obj

  note["ไม่มีข้อมูลใดออกนอกเครือข่ายสถาบันการเงิน<br/>PII อยู่ในคำขอและในไฟล์ที่เก็บเท่านั้น — ไม่เคยอยู่ใน log"]
  td -.- note
```


## วงจรชีวิตของเทมเพลต (Template Lifecycle)

```mermaid
flowchart TD
  up["อัปโหลด PDF ต้นฉบับของสถาบันการเงิน"] --> design["ลากฟิลด์ไปวางบนเอกสาร"]
  design --> draft["ฉบับร่าง"]
  draft --> lint{"ตรวจผ่านไหม?"}
  lint -- "ไม่ผ่าน" --> fix["แก้ตามที่ระบบบอก<br/>ข้อความล้นกรอบ · ฟอนต์ไม่มีสระ<br/>ฟิลด์ทับกัน · นิพจน์หาค่าไม่เจอ"]
  fix --> lint
  lint -- "ผ่าน" --> pub["เผยแพร่ → เวอร์ชัน v(n)"]
  pub --> frozen["เวอร์ชันถูกแช่แข็งตลอดกาล<br/>สัญญาฟิลด์เปลี่ยนไม่ได้อีก"]
  frozen --> gen["ระบบอื่นเรียก POST /v1/generate"]
  frozen --> fill["ผู้ใช้ฝ่ายธุรกิจกรอกเองที่หน้า /fill"]
  pub -.-> newdraft["แก้ต่อ → แตกเป็นฉบับร่างใหม่<br/>เวอร์ชันเดิมไม่ถูกแตะ"]
  newdraft --> draft

  style frozen fill:#ddf4ff,stroke:#0969da
  style lint fill:#fff8c5,stroke:#9a6700
```

> **เผยแพร่คือประตูทางเดียว** ฉบับร่างที่ตรวจไม่ผ่านจะเผยแพร่ไม่ได้ — และคีย์ API เรนเดอร์ฉบับร่างไม่ได้เลย เพราะฉบับร่างคือเทมเพลตที่ระบบ *เชื่ออยู่แล้ว* ว่าจะผลิตเอกสารผิด

---

## หน้าออกแบบเทมเพลต — มีอะไรอยู่ตรงไหน

![หน้าออกแบบเทมเพลต: แถบเครื่องมือ 21 ชนิดฟิลด์อยู่ด้านซ้าย, PDF ที่อัปโหลดอยู่ตรงกลางพร้อมฟิลด์ case_id / fullName / loanAmount ที่ลากมาวางทับบนเส้นของเอกสารจริง, หัวท้ายกระดาษและลายน้ำเป็นฟิลด์อ่านอย่างเดียว, พาเนลแก้ไขฟิลด์และผลการตรวจอยู่ด้านขวา และปุ่มเผยแพร่อยู่ล่างขวา](/docs/img/designer.svg)

| # | คืออะไร |
| --- | --- |
| **1** | **แถบเครื่องมือ** — ฟิลด์ 21 ชนิด (ข้อความ ตัวเลข วันที่ พ.ศ. รูป QR บาร์โค้ด ตาราง ช่องติ๊ก … และท้ายสุดคือ **หัว/ท้ายกระดาษ · ลายน้ำ · นิพจน์**) |
| **2** | **ลากมาวางบน PDF จริงของสถาบันการเงิน** — ไม่ได้วาดเอกสารใหม่ เราวางทับของเดิม กรอบประคือฟิลด์ข้อมูลที่ระบบอื่นจะส่งค่ามาให้ |
| **3** | **พาเนลแก้ไขฟิลด์** — ชนิด ชื่อ ฟอนต์ ขนาด และ "ย่อขนาดอัตโนมัติเมื่อยาวเกิน" |
| **4** | **หัว/ท้ายกระดาษ** — ใส่ `{currentPage}` / `{totalPages}` ได้ ปรากฏทุกหน้าเอง |
| **5** | **ลายน้ำ** — พิมพ์ข้อความเอง หมุนได้ ปรับความจางได้ เลือกได้ว่าจะแสดงทุกหน้าไหม |
| **6** | **ผลการตรวจ** — ต้องผ่านก่อนถึงจะเผยแพร่ได้ |
| **7** | **เผยแพร่** — แช่แข็งเวอร์ชันนั้นตลอดกาล |

> **สีของกรอบบอกความหมาย** กรอบ**น้ำเงินประ** = ฟิลด์ข้อมูล ระบบอื่นต้องส่งค่ามาให้ (อยู่ในสัญญาฟิลด์) · กรอบ**เทาประ** = อ่านอย่างเดียว เช่นเลขหน้าและลายน้ำ **ไม่อยู่ในสัญญา** ไม่มีใครต้องส่งค่ามาให้

---

## 1. ข้อมูลทั่วไปเกี่ยวกับผลิตภัณฑ์
**Thinker Docs** เป็นระบบสร้างเอกสาร PDF ภายในองค์กร (On-Premises) ในรูปแบบของเทมเพลตสำหรับฝ่ายสนับสนุนและธุรการของสถาบันการเงิน บริการนี้ช่วยให้ผู้ใช้งานสายธุรกิจสามารถออกแบบและจัดวางตำแหน่งข้อมูลในสัญญาหรือเอกสารทางการเงินได้ด้วยตนเองผ่านหน้าจอเว็บเบราว์เซอร์อย่างแม่นยำ และยังเปิดให้ระบบภายนอกสามารถเรียกสร้างเอกสารที่กรอกข้อมูลอัตโนมัติได้อย่างรวดเร็วและปลอดภัยภายใต้มาตรฐานการประมวลผลข้อมูลส่วนบุคคล (PDPA) โดยข้อมูลทั้งหมดจะถูกประมวลผลอยู่ภายใต้เครือข่ายภายในของสถาบันการเงินเพื่อความปลอดภัยสูงสุด

---

## 2. การออกแบบเทมเพลต (Designing a Template)
การทำงานเริ่มจากการสร้างเทมเพลตในหน้าจอระบบ โดยมีขั้นตอนดังนี้:
1. **การอัปโหลดเอกสารฐาน (Base PDF)**: อัปโหลดเอกสาร PDF ต้นฉบับที่ต้องการนำมาใช้เป็นฐานในการกรอกข้อมูล (จำกัดขนาดไฟล์ไม่เกิน 25 MB และหนาไม่เกิน 20 หน้า) ระบบจะตรวจเช็กและปฏิเสธเอกสารฐานที่เปิดใช้งานการเข้ารหัสผ่าน มีสคริปต์ JavaScript หรือมีลายมือชื่อดิจิทัล (Digital Signature) อยู่ก่อน เนื่องจากระบบจะแปลงหน้าเอกสารฐานเป็นรูปแบบกราฟิก ซึ่งทำให้ระบบโต้ตอบเดิมหรือลายเซ็นดิจิทัลถูกลบออกโดยสิ้นเชิง การปฏิเสธตั้งแต่ขั้นตอนอัปโหลดจึงเป็นไปเพื่อความปลอดภัย
2. **การจัดวางฟิลด์ข้อมูล**: ลากฟิลด์ข้อมูลจากแถบเครื่องมือด้านซ้ายและวางลงบนตำแหน่งที่ต้องการในเอกสาร โดยตำแหน่ง (X, Y) และขนาดของฟิลด์ทั้งหมดจะถูกวัดและแสดงผลเป็นหน่วย **มิลลิเมตร (มม.)** เพื่อให้ตรงตามสัดส่วนกระดาษจริงของสถาบันการเงินเมื่อพิมพ์ออกมา โดยห้ามแปลงหน่วยเป็นพิกเซล (px) เพื่อรักษาความแม่นยำ
3. **การเปิดใช้งานตัวเลือกบีบขนาดฟอนต์อัตโนมัติ (dynamicFontSize)**: ระบบจะตั้งค่าเปิดใช้ `dynamicFontSize` เป็นค่าเริ่มต้นสำหรับฟิลด์ข้อความ เพื่อป้องกันการตัดคำหรือข้อความล้นกรอบเมื่อข้อมูลชื่อหรือที่อยู่ภาษาไทยของผู้ลงนามมีความยาวแตกต่างกัน

### ประเภทฟิลด์ข้อมูลที่ใช้งานได้ (Available Field Types)
ระบบมีฟิลด์ข้อมูลให้เลือกใช้งานทั้งหมด 21 ประเภท ดังนี้:
1. **กล่องข้อความ (text)**: ข้อความทั่วไปที่สามารถย่อขนาดตัวอักษรอัตโนมัติได้
2. **ตัวเลข (number)**: ข้อความตัวเลขสำหรับมูลค่าเงิน รองรับการกำหนดทศนิยม (decimals), ตัวคั่นหลักพัน (thousandsSeparator), สัญลักษณ์นำหน้า (เช่น ฿), สัญลักษณ์ต่อท้าย (เช่น บาท) และการแปลงเป็นเลขไทย (๑๒๓)
3. **วันที่และเวลาไทย (dateTimeTh)**: แสดงวันที่ปีพุทธศักราช (พ.ศ.) ของไทย (คำนวณปี BE ด้วยการบวก 543 ปีเข้ากับปี ค.ศ. สากลโดยอัตโนมัติ) กำหนดรูปแบบการแสดงผล (pattern) ได้ เช่น แสดงเฉพาะวันที่ (`d MMMM yyyy`) หรือแสดงพร้อมเวลา (`d MMMM yyyy HH:mm`)
4. **ข้อความหลายตัวแปร (multiVariableText)**: กล่องข้อความที่นำคำคงที่และตัวแปรข้อความต่างๆ มารวมกัน
5. **วันที่ (dateTime)**: วันที่และเวลารูปแบบเกรกอเรียนสากล (ค.ศ.) เหมาะสำหรับบันทึกเวลาของระบบ (Timestamp)
6. **เวลา (time)**: เวลาในรูปแบบเกรกอเรียนสากล
7. **รูปภาพ (image)**: วางไฟล์รูปภาพประกอบเอกสาร
8. **ภาพเวกเตอร์ (svg)**: วางไฟล์รูปภาพรูปแบบเวกเตอร์ SVG
9. **คิวอาร์โค้ด (qrcode)**: รหัสคิวอาร์สำหรับการสแกนลิงก์หรือข้อมูล
10. **บาร์โค้ด Code 128 (code128)**: บาร์โค้ดมาตรฐาน 1D
11. **บาร์โค้ด EAN-13 (ean13)**: บาร์โค้ดมาตรฐานสินค้า
12. **ตาราง (table)**: แสดงรายการข้อมูลรูปแบบตาราง เช่น ตารางค่างวด
13. **กล่องเครื่องหมาย (checkbox)**: ช่องเครื่องหมายติ๊กถูก สามารถปรับความหนาเส้นขอบ (borderWidth) เป็น 0 เพื่อให้เหลือเฉพาะเครื่องหมายติ๊กถูก ในกรณีที่เอกสารฐานมีกรอบสี่เหลี่ยมพิมพ์มาอยู่แล้ว และปรับความมนของมุม (borderRadius) ได้
14. **กลุ่มปุ่มตัวเลือก (radioGroup)**: ปุ่มตัวเลือกที่ผู้ใช้สามารถเลือกได้เพียงข้อเดียว
15. **ตัวเลือกดรอปดาวน์ (select)**: เมนูตัวเลือกสำหรับการใช้งานบนเว็บเบราว์เซอร์
16. **เส้นตรง (line)**: สำหรับวาดเส้นแบ่งส่วนบนกระดาษ
17. **กล่องสี่เหลี่ยม (rectangle)**: กล่องตกแต่งเลย์เอาต์เอกสาร
18. **วงรี (ellipse)**: วงกลมหรือวงรีสำหรับตกแต่งเอกสาร
19. **หัว/ท้ายกระดาษ (headerFooter)**: ข้อความพิเศษแสดงในตำแหน่งเดียวกันทุกหน้าโดยอัตโนมัติ เช่น `หน้า {currentPage} จาก {totalPages}` ระบบจะประมวลผลตัวเลขหน้าอย่างถูกต้องแม้อนาคตจะมีการแนบไฟล์เพิ่มเติม
20. **ลายน้ำ (watermark)**: ข้อความจางๆ แสดงตรงกลางทุกหน้า (เช่น "ฉบับร่าง" เอียง 45 องศา ระดับความโปร่งใส 15% เป็นตัวอักษรสีเทาเพื่อความเรียบร้อย)
21. **นิพจน์ (expression)**: ข้อความที่ระบบ **คำนวณเอง** จากค่าของฟิลด์อื่น เช่น `เรียน คุณ{fullName}` หรือยอดรวมท้ายตาราง ระบบภายนอกไม่ต้องส่งค่านี้มา เพราะมันคิดจากค่าที่ส่งมาแล้ว — ดูวิธีใช้ฉบับเต็มที่ **ข้อ 7 นิพจน์**

---

## 3. การตรวจก่อนเผยแพร่ — กฎที่บล็อกเทมเพลตที่จะผลิตเอกสารผิด
เมื่อผู้ใช้ออกแบบเสร็จและสั่ง "เผยแพร่" (Publish) ระบบจะนำเทมเพลตเข้าสู่ขั้นตอนการตรวจลินต์อย่างละเอียด หากตรวจพบข้อผิดพลาดระดับ `error` ระบบจะระงับและบล็อกการเผยแพร่ทันทีเพื่อป้องกันเอกสารที่ใช้งานจริงเกิดการชำรุด โดยกฎการตรวจสอบมีดังนี้:

*   **`field.unnamed`**: พบฟิลด์ที่ไม่มีการระบุชื่อ
*   **`field.duplicate_name`**: พบฟิลด์ที่มีชื่อซ้ำกันในหน้าเดียวกัน
*   **`field.bad_page_index`**: หน้าเอกสารที่ระบุให้กับฟิลด์ไม่มีอยู่จริงในเอกสารฐาน (เช่น ระบุหน้า 5 ในขณะที่เอกสารฐานมีเพียง 4 หน้า)
*   **`field.off_page`**: มีขอบเขตของฟิลด์บางส่วนหลุดหรือยื่นออกนอกกระดาษ
*   **`field.overlap`**: พบฟิลด์สองฟิลด์วางตำแหน่งทับซ้อนกัน (ยกเว้นฟิลด์ประเภทลายน้ำ/watermark)
*   **`format.invalid`**: การจัดรูปแบบทศนิยม ตัวคั่น หรือรูปแบบวันที่ในฟิลด์ number/date ไม่ถูกต้อง
*   **`sample.missing_required`**: ฟิลด์ที่ทำเครื่องหมายว่าจำเป็น (required) ไม่มีข้อมูลตัวอย่างสำหรับใช้ทดสอบการกรอก
*   **`font.unknown`**: ใช้ฟอนต์ที่ระบบไม่รู้จัก
*   **`font.missing_glyphs`**: ตัวอักษรภาษาไทยหรือสัญลักษณ์ในข้อมูลตัวอย่างไม่มีอยู่ในชุดแบบอักษร (Font Glyphs) ของฟอนต์ที่เลือก (ระบบมีระบบเช็กวรรณยุกต์และสระลอยของไทยอย่างละเอียดก่อนเผยแพร่)
*   **`text.hits_font_floor`**: เมื่อกรอกข้อความตัวอย่างที่ยาวที่สุด ขนาดตัวอักษรถูกปรับย่อต่ำลงจนชนขอบล่างของเกณฑ์การอ่านออกขั้นต่ำ (Legibility Floor) ที่ **7pt** สำหรับอักษรภาษาไทย (อักษรไทยมีสระและวรรณยุกต์ซ้อนระดับ จึงมีขนาดขั้นต่ำเพื่อการอ่านออกจริงที่สูงกว่าภาษาอังกฤษ) ระบบจะบล็อกเพื่อให้ผู้ใช้ออกแบบขยายขนาดกรอบข้อความแทนการฝืนบีบอักษรให้เล็กลงจนอ่านไม่ออกบนเอกสารที่เซ็นสัญญาจริง
*   **`text.overflows_height`**: ข้อความยาวเกินกว่าความสูงของกล่อง ทำให้ล้นขอบกล่องที่กำหนด
*   **`expression.unresolved`**: ชื่อที่อยู่ในวงเล็บปีกกา `{}` ซึ่ง **จะหาค่าไม่เจอตอนสร้างเอกสารจริง** ไม่ว่าจะเป็นฟิลด์ที่สะกดผิด (`{loanAmont}`) หรือตัวแปรที่ระบบไม่รองรับ (`Array`, `Date`, `Intl`) ทั้งสองกรณีจะพิมพ์ข้อความ `{...}` ดิบๆ ลงบนเอกสารที่ส่งให้ลูกค้า จึงบล็อกการเผยแพร่ไว้ — ส่วนชื่อที่หาค่าเจอจริง (ฟิลด์ในสัญญา, `currentPage`, `totalPages`, `date`, `dateTime`, `Math`, `Number`, `String`, `Boolean`, `Object`, `JSON`, `parseInt`, `parseFloat`, `isNaN` และตัวแปรใน arrow function ที่คุณประกาศเอง) จะไม่ถูกจับ ดูข้อ 7
*   **`expression.on_editable_field`**: ฟิลด์ที่มีนิพจน์อยู่ แต่ถูกติ๊ก **“แก้ไขได้”** — ระบบจะ**ไม่คำนวณนิพจน์นั้นเลย** มันจะไปรอค่าจากระบบภายนอกแทน ซึ่งไม่มีใครถูกขอให้ส่งมา นิพจน์จึงตายเงียบๆ โดยเอกสารยังพิมพ์ออกมาได้ตามปกติ (แต่ผิด) — เอาเครื่องหมายถูกออกจาก “แก้ไขได้” เพื่อให้ฟิลด์กลับมาคำนวณค่าเอง ดูตัวอย่างช่องติ๊กในข้อ 7
*   **`expression.optional_field`**: นิพจน์เรียกใช้ฟังก์ชันของระบบ (เช่น `bahtText`) กับฟิลด์ที่**ไม่ได้ตั้งเป็น “ที่จำเป็น”** — ถ้าระบบภายนอกไม่ส่งค่านั้นมา เอกสารจะพิมพ์สูตรดิบๆ ลงบนสัญญาตรงช่องจำนวนเงิน ให้ติ๊ก “ที่จำเป็น” ที่ฟิลด์นั้น ระบบจะได้ปฏิเสธคำขอที่ไม่ส่งค่ามา (400) ตั้งแต่ก่อนสร้างเอกสาร
    (การเปรียบเทียบธรรมดา เช่น `{allow === "Y" ? …}` **ไม่ถูกจับ** — ฟิลด์ที่ไม่ส่งค่ามาจะกลายเป็นค่าว่าง ซึ่งเทียบได้ตามปกติ)
*   **`table.overflows_page`**: ขอบเขตตารางไหลยาวจนเลยขอบกระดาษด้านล่าง เนื่องจากระบบไม่สามารถจัดการเรื่องการเพิ่มหน้ากระดาษและขยับเลย์เอาต์ตารางแบบอัตโนมัติบนเอกสารฐานที่เป็น PDF อัปโหลดภายนอกได้ (การล้นของข้อมูลตารางจะทำให้ข้อมูลแถวด้านล่างหายไปในไฟล์จริงโดยไม่มีการเตือนความผิดพลาดที่ฝั่งเซิร์ฟเวอร์) ลินต์จึงต้องบล็อกการเผยแพร่ทันทีเพื่อให้ผู้ใช้ไปแบ่งตารางหรือขยายกล่องในหน้าการออกแบบแทน

---

## 4. การเผยแพร่และการจัดเก็บเวอร์ชันเอกสาร (Publishing and Versioning)
*   **สถานะฉบับร่าง (Draft)**: เทมเพลตที่กำลังปรับแต่งหรืออัปโหลดเอกสารฐานใหม่จะมีสถานะเป็นฉบับร่าง ซึ่งสามารถแก้ไขบันทึกได้ตลอดเวลา แต่ระบบภายนอกจะไม่สามารถดึงหรือสั่งสร้างเอกสารจากฉบับร่างนี้ได้
*   **การเผยแพร่ (Publishing)**: การส่งแบบร่างเข้ารับการตรวจสอบลินต์ หากผ่าน ระบบจะบันทึกเป็นเวอร์ชันที่เผยแพร่แล้ว (เช่น เวอร์ชัน 1, 2) ซึ่งจะถูกแช่แข็ง (Frozen) ทันที ไม่สามารถเข้าไปแก้ไขโครงสร้างของเวอร์ชันนี้ได้อีก หากต้องการเปลี่ยนเลย์เอาต์ ระบบจะบังคับสร้างแบบร่างตัวใหม่ขึ้นมาแทน
*   **การยกเลิกเผยแพร่ (Unpublish) และการลบ**: 
    การสั่งลบ (DELETE) เทมเพลตที่กำลังเผยแพร่อยู่ทำไม่ได้ (ได้รหัส `409 published_cannot_delete`) ต้อง **"ยกเลิกเผยแพร่" (Unpublish)** ก่อน ซึ่งมีผลดังนี้:
    *   เทมเพลตหายจากหน้ารายการ แก้ไขต่อไม่ได้ สร้างฉบับร่างใหม่จากมันไม่ได้
    *   **`POST /v1/generate` จะตอบ `404` ทันที** — นี่คือการ **หยุดให้บริการอย่างแท้จริง** ไม่ใช่แค่การจัดระเบียบรายการ ระบบภายนอกที่ยังเรียกเทมเพลตนี้อยู่จะทำงานล้มเหลวทันทีที่คุณกดปุ่ม **ต้องแน่ใจก่อนว่าไม่มีใครเรียกมันแล้ว**
    *   เพราะมันคือการตัดบริการจริง ปุ่มนี้จึงไม่ได้วางไว้ให้กดได้โดยง่าย — มันซ่อนอยู่หลังลูกศร ▾ ข้างปุ่ม "เผยแพร่" และกล่องยืนยันจะบอกผลลัพธ์เป็นประโยคเดียวก่อนคุณกดยืนยัน
    *   เทมเพลตที่ยกเลิกเผยแพร่แล้ว **ลบทิ้งได้** — ยกเว้นกรณีที่เคยมีเอกสารถูกสร้างจากมัน เพราะเอกสารเหล่านั้นคือหลักฐานว่าลูกค้าได้รับอะไรไป เทมเพลตที่เอกสารอ้างถึงอยู่จึงลบไม่ได้ (`409`)

---

## 5. การกรอกข้อมูลผ่านเว็บเบราว์เซอร์ (Browser Filling)
สำหรับเจ้าหน้าที่ปฏิบัติการสายธุรกิจที่ต้องการสร้างเอกสารรายฉบับด้วยตนเอง สามารถทำได้ผ่านหน้าจอการกรอกข้อมูล:
1.  เข้าลิงก์ของหน้ากรอกข้อมูลที่ `/fill/:id` (โดย `id` คือรหัสของเทมเพลต)
2.  เจ้าหน้าที่ล็อกอินเข้าระบบด้วยบัญชีตนเองผ่านระบบคุกกี้เซสชันขององค์กร ไม่ต้องคัดลอกหรือใช้งานกุญแจ API (API Key) ซึ่งมีไว้สำหรับโปรแกรมหรือเครื่องอัตโนมัติเท่านั้น
3.  หน้าจอจะโหลดหน้าเอกสารต้นฉบับจริงพร้อมฟิลด์กรอกข้อมูลแสดงทับในตำแหน่งที่ออกแบบไว้ (ผ่านระบบ `Form` ของระบบ)
4.  เจ้าหน้าที่สามารถพิมพ์ข้อมูลลงบนหน้านั้นโดยตรง ซึ่งจะได้เห็นฟอนต์และการตัดวรรณยุกต์ภาษาไทยแบบเรียลไทม์ ทำให้มั่นใจว่าเอกสารจะออกมาถูกต้อง สวยงาม และไม่มีการซ้อนทับกัน
5.  เมื่อกรอกข้อมูลเรียบร้อยแล้ว คลิกปุ่ม "สร้างเอกสาร" (Generate) ระบบจะเรียกใช้ API ซิงค์ประมวลผลเอกสาร PDF และส่งกลับมาให้เบราว์เซอร์ดาวน์โหลดไฟล์เก็บไว้ในคอมพิวเตอร์ทันทีโดยใช้ชื่อเดียวกับชื่อของเทมเพลตนั้นๆ

---

## 6. ระบบเอกสาร ระยะเวลาเก็บรักษา และบันทึกการตรวจสอบย้อนกลับ (Audit Log)
*   **ระยะเวลาการเก็บรักษาเอกสาร (Retention Policy)**: เอกสาร PDF ที่สร้างผ่านโหมดอัปโหลดแบบไม่ประสานเวลา (Async Mode) จะถูกจัดเก็บไว้ในถังข้อมูลบนระบบออนเพรมของสถาบันการเงินตามกำหนด (ค่าเริ่มต้น 30 วัน กำหนดผ่าน `DOCUMENT_TTL_DAYS`) และจะถูกลบออกแบบถาวรเมื่อหมดอายุ หากระบบภายนอกพยายามดาวน์โหลดเอกสารที่หมดอายุแล้วจะได้รับรหัสข้อผิดพลาดกลับไปเป็น `410 Gone`
*   **บันทึกการตรวจสอบย้อนกลับที่ไม่เก็บข้อมูลลูกค้า (Append-Only & No-PII Audit Log)**:
    เพื่อความสอดคล้องกับกฎหมายคุ้มครองข้อมูลส่วนบุคคล (PDPA) บันทึกการตรวจสอบย้อนกลับ (Audit Log) ของ Thinker Docs จะ **ไม่มีการเก็บข้อมูลส่วนตัวของลูกค้า (PII)** เช่น ชื่อ หมายเลขบัตรประชาชน หรือจำนวนเงินใดๆ ทั้งสิ้น
    และสำหรับโหมดการสร้างเอกสารแบบประสานเวลา (Sync Mode) ที่ไม่ได้เก็บไฟล์ PDF ไว้บนระบบเลย ระบบจะยังคงทำบันทึกระบุประเภทเทมเพลต เวอร์ชัน วันที่ เวลา ผู้ขอสร้าง (API Key ตัวไหน) และเก็บ **รหัสแฮช SHA-256 ของไฟล์ PDF** ที่ส่งออกไป 
    ประโยชน์คือ เมื่อเกิดกรณีพิพาทในภายหลัง สถาบันการเงินสามารถนำไฟล์ PDF ที่ผู้ใช้ถืออยู่มาทำการเข้ารหัสแฮช SHA-256 แล้วนำไปเปรียบเทียบกับบันทึกการตรวจสอบในระบบเพื่อยืนยันความถูกต้องของเอกสารได้ทันที โดยที่ระหว่างนั้นระบบ Thinker Docs ไม่จำเป็นต้องเก็บไฟล์จริงหรือข้อมูล PII ของลูกค้าไว้ให้เสี่ยงต่อการรั่วไหลแต่อย่างใด
    ทั้งนี้ บันทึกการตรวจสอบเป็นระบบต่อท้ายข้อมูลเท่านั้น (Append-only) ไม่มีเมนูแก้ไขหรือลบโดยผู้ใช้งาน และบันทึกการถูกปฏิเสธสิทธิ์ข้ามทีม (`outcome: denied`) จะถูกรวบรวมมาไว้เพื่อใช้ตรวจหาการโจมตีหรือการใช้คีย์รั่วไหลเป็นประจำทุกสัปดาห์
*   **การเปิดดู กรอง และส่งออกบันทึก (หน้า `บันทึกตรวจสอบ`)**:
    ผู้ดูแลทีม (admin) เปิดดูบันทึกได้จากเมนู **บันทึกตรวจสอบ** ในแถบด้านซ้าย หน้านี้แสดงรายการทีละหน้า (เรียงจากใหม่ไปเก่า) พร้อมชุดตัวกรองด้านบน:
    *   **ช่วงเวลา** — เลือกแบบสำเร็จรูป (24 ชั่วโมง / 7 วัน / 30 วัน / 90 วัน) หรือเลือก **"กำหนดเอง"** เพื่อระบุวันและเวลา **เริ่มต้น–สิ้นสุด** ได้เอง ผ่านปฏิทินและตัวเลื่อนเลือกเวลา (ชั่วโมง:นาที) — เหมาะเวลาต้องการดูเฉพาะเหตุการณ์ในกรอบเวลาที่เจาะจง เช่น ช่วงที่เกิดปัญหา
    *   **การกระทำ** — กรองตามชนิดเหตุการณ์ (เช่น `generate`, `apikey.create`, `member.role_change`) และปุ่ม **"เฉพาะที่ถูกปฏิเสธ"** เพื่อดูเฉพาะเหตุการณ์ที่ถูกปฏิเสธสิทธิ์ (`outcome: denied`) — คือคำถามที่ควรตรวจทุกสัปดาห์นั่นเอง
    *   ปุ่ม **"ล้างตัวกรอง"** จะปรากฏเมื่อมีตัวกรองที่ไม่ใช่ค่าเริ่มต้น กดเพื่อรีเซ็ตทุกตัวกรองกลับเป็น 7 วัน
    *   ปุ่ม **"ส่งออก CSV"** ดาวน์โหลดบันทึก **เฉพาะแถวที่ตรงกับตัวกรองที่เลือกอยู่ในขณะนั้น** เป็นไฟล์ CSV — ฝังรหัส UTF-8 BOM ให้ Microsoft Excel อ่านภาษาไทยได้ถูกต้อง และใช้เวลารูปแบบ ISO 8601 ที่เรียงลำดับได้ เหมาะสำหรับนำไปตรวจสอบหรือทำรายงานภายนอก การส่งออกครั้งหนึ่งจำกัดสูงสุด 100,000 แถว หากต้องการมากกว่านั้นให้แบ่งช่วงเวลาให้แคบลงแล้วส่งออกทีละช่วง

---

## 7. นิพจน์ (Expression) — ข้อความที่เทมเพลตคำนวณเอง

**นิพจน์** คือข้อความที่ฟิลด์คิดค่าขึ้นมาเอง จากค่าที่ฟิลด์อื่นมีอยู่แล้ว ระบบภายนอกไม่ต้องส่งมา
ลากฟิลด์ **นิพจน์ (Expression)** ลงบนหน้ากระดาษ แล้วพิมพ์ว่า

```
เรียน คุณ{fullName}
```

ตอนสร้างเอกสารจริงมันจะพิมพ์ออกมาเป็น `เรียน คุณสมชาย` — อะไรที่อยู่ในวงเล็บปีกกา `{ }` จะถูกคำนวณ ส่วนที่อยู่นอกวงเล็บพิมพ์ออกมาตามที่เห็น

### ทำไมต้องแยกเป็นฟิลด์ชนิดหนึ่งต่างหาก

ระบบคำนวณ `{...}` ได้มาโดยตลอด แต่มีเงื่อนไขซ่อนอยู่ 2 ข้อที่ไม่มีอะไรบนหน้าจอบอก คือ **(1)** ข้อความต้องอยู่ใน `content` (ข้อความที่ *คุณ* พิมพ์ลงเทมเพลต ไม่ใช่ช่องข้อมูลตัวอย่าง) และ **(2)** ฟิลด์นั้นต้องเป็นแบบ **อ่านอย่างเดียว (read-only)** ก่อน

ถ้าฟิลด์แก้ไขได้ ระบบจะ **ไม่สนใจ `content` เลย** แล้วไปพิมพ์ค่าที่ระบบภายนอกส่งมาแทน — แปลว่านิพจน์บนฟิลด์ที่แก้ไขได้จะ **เงียบสนิท ไม่ทำงาน และไม่ฟ้องอะไรเลย**

พอทำเป็น "ชนิดฟิลด์" เงื่อนไขทั้งสองข้อก็เป็นจริงตั้งแต่วินาทีที่ลากลงมา คุณไม่ต้องรู้กฎพวกนี้เลย

ด้วยเหตุผลเดียวกัน **หัว/ท้ายกระดาษ** และ **ลายน้ำ** ก็ใช้นิพจน์ได้ (เพราะมันอ่านอย่างเดียวเหมือนกัน) — พิมพ์ `หน้า {currentPage} จาก {totalPages}` ในท้ายกระดาษได้เลย ทำงานเองทุกหน้า

### สิ่งที่ใส่ใน `{ }` ได้

| | |
|---|---|
| **ฟิลด์ของคุณเอง** | ชื่อฟิลด์ใดก็ได้ที่อยู่ในสัญญาของเทมเพลต — `{loanAmount}` |
| **เลขหน้า** | `{currentPage}`, `{totalPages}` — นับถูกทุกหน้า แม้จะมีการแนบสแกนเพิ่มท้ายเล่ม |
| **วันที่วันนี้** | `{date}` → `2026/07/14` · `{dateTime}` → `2026/07/14 13:39` |
| **คณิตศาสตร์และเงื่อนไข** | `Math`, `Number`, `String`, `Boolean`, `Object`, `JSON`, `parseInt`, `parseFloat`, `isNaN`, `+ - * /` และ `? :` |
| **แถวในตาราง** | ฟิลด์ตารางส่งมาเป็นอาเรย์ของแถว ใช้ `map` / `reduce` / `join` กับ arrow function ได้ |
| **ฟิลด์นิพจน์อื่น** | นิพจน์อ่านค่าที่นิพจน์ตัวอื่นคำนวณไว้แล้วได้ (ดู "ต่อกันเป็นทอด" ข้างล่าง) |

### ต่อกันเป็นทอด — วิธีทำยอดรวมท้ายใบแจ้งหนี้

นิพจน์อ่านค่าที่ **นิพจน์ตัวอื่นคำนวณเสร็จแล้ว** ได้ ทำให้แตกยอดเป็นขั้นๆ ได้ ไม่ต้องยัดทุกอย่างไว้ในบรรทัดเดียว

```
ฟิลด์ subtotal :  {orders.reduce((sum, item) => sum + Number(item[1]) * Number(item[2]), 0)}   → 2000
ฟิลด์ tax      :  {Number(subtotal) * 0.07}                                                    → 140
ฟิลด์ total    :  {Number(subtotal) + Number(tax)}                                             → 2140
```

**ลำดับการวางฟิลด์ไม่สำคัญ** — `total` อ้างถึง `tax` ที่วางไว้ทีหลังก็ได้ ระบบคำนวณให้ถูกเอง

ข้อแม้เดียวคือฟิลด์ที่ถูกอ้างถึงต้องเป็นแบบ **อ่านอย่างเดียว** (นิพจน์ · หัว/ท้ายกระดาษ · ลายน้ำ · ข้อความที่ตั้งเป็นอ่านอย่างเดียว) ซึ่งฟิลด์นิพจน์เป็นอยู่แล้วโดยธรรมชาติ

### กฎข้อเดียวที่จะทำให้คุณเจ็บตัว

**ต้องห่อฟิลด์ด้วย `String(...)` หรือ `Number(...)` ก่อน ถึงจะเรียก method ต่อท้ายได้**

```
{loanAmount.padStart(9, "0")}          ✗ พิมพ์ดิบออกมา: {loanAmount.padStart(9, "0")}
{String(loanAmount).padStart(9, "0")}  ✓ พิมพ์ 001250000
```

ค่าฟิลด์ที่ว่างเปล่ายังไม่ใช่ string จริงในสายตาระบบ การเรียก method โดยตรงจึงล้มเหลว แต่เมื่อห่อด้วยฟังก์ชันแล้วจึงใช้งานได้ — **นี่คือสาเหตุอันดับหนึ่งที่นิพจน์ซึ่งดูถูกต้องกลับพิมพ์ออกมาเป็นข้อความที่ผิดเพี้ยน**

### สูตรสำเร็จ (ทดสอบกับไลบรารีตัวจริงมาแล้วทุกบรรทัด)

```
เรียน คุณ{fullName}                                              เรียน คุณสมชาย
หน้า {currentPage} จาก {totalPages}                              หน้า 1 จาก 2

{String(loanAmount).replace(/\B(?=(\d{3})+(?!\d))/g, ",")} บาท   1,250,000 บาท    ← เงินบาท ใส่ลูกน้ำ
{Number(loanAmount).toFixed(2)}                                  1250000.00       ← ทศนิยม 2 ตำแหน่ง
{Math.round(loanAmount / 12)} บาท/เดือน                          104167 บาท/เดือน
{Number(amount) > 1000000 ? "วงเงินสูง" : "วงเงินปกติ"}            วงเงินสูง         ← ข้อความตามเงื่อนไข

{orders.reduce((sum, item) => sum + Number(item[1]) * Number(item[2]), 0)}
                                                                 2000             ← ยอดรวมท้ายตาราง
```

ระบบ **ไม่มี** `toLocaleString` และ **ไม่มี** `Intl` (อ่านหัวข้อถัดไป) ดังนั้นสูตร regex ข้างบน **คือ** วิธีใส่ลูกน้ำในจำนวนเงินบาท คัดลอกไปใช้ได้เลยตามนั้น

### ตัวอย่างจริง — ช่องติ๊ก "อนุญาต / ไม่อนุญาต" ที่ตรงข้ามกันเสมอ

ระบบภายนอกส่งมาแค่ค่าเดียว (`"Y"` หรือ `"N"`) แล้วให้เอกสารติ๊กช่องให้ถูกเอง ใช้ **3 ฟิลด์**:

| ฟิลด์ | ชนิด | แก้ไขได้ | ข้อความในฟิลด์ |
|---|---|---|---|
| `allow` | **ข้อความ** | ✅ ติ๊ก | *(ว่างไว้)* — API ส่ง `"Y"` / `"N"` มาลงตรงนี้ |
| `boxYes` | ช่องติ๊ก | ❌ **ไม่ติ๊ก** | `{allow === "Y" ? "true" : "false"}` |
| `boxNo` | ช่องติ๊ก | ❌ **ไม่ติ๊ก** | `{allow === "N" ? "true" : "false"}` |

ผลลัพธ์บนเอกสารจริง:

```
API ส่ง  allow: "Y"   →   ☑ อนุญาต      ☐ ไม่อนุญาต
API ส่ง  allow: "N"   →   ☐ อนุญาต      ☑ ไม่อนุญาต
API ไม่ส่ง allow มาเลย →   ☐ อนุญาต      ☐ ไม่อนุญาต     ← ยังไม่ได้ตอบ ก็ต้องไม่ติ๊กอะไรเลย
```

**สองช่องนี้ตรงข้ามกันโดยโครงสร้าง** ติ๊กพร้อมกันทั้งคู่ไม่ได้ และเว้นว่างทั้งคู่ได้เฉพาะตอนที่ยังไม่มีคำตอบ
บนหนังสือยินยอม ช่องที่ติ๊กทั้งสองอันคือเอกสารที่บังคับใช้ไม่ได้ — ตรงนี้จึงเป็นไปไม่ได้ตั้งแต่แรก

**สามเรื่องที่คนทำพลาดบ่อยที่สุด — และระบบจะบล็อกให้ทั้งหมด:**

1. **เอานิพจน์ไปใส่ที่ฟิลด์ `allow` เอง** (ให้มันอ้างถึงตัวเอง) — ไม่ได้ `allow` คือ*ข้อมูลดิบ*ที่ระบบภายนอกส่งมา
   ส่วนช่องติ๊กคือ*ผลลัพธ์*ที่คิดจากข้อมูลนั้น คนละหน้าที่กัน ต้องแยกฟิลด์
2. **ติ๊ก "แก้ไขได้" ที่ช่องติ๊กสองอันนั้น** — นิพจน์จะ**ตายทันที** ระบบจะเลิกคำนวณแล้วไปรอค่าจากระบบภายนอก
   ซึ่งไม่มีใครส่งมาให้ (ระบบตรวจจะฟ้อง `expression.on_editable_field`)
3. **ใช้ "ช่องติ๊ก" เป็นตัวรับค่า `allow`** — ช่องติ๊กรับได้แค่ `"true"` / `"false"` เท่านั้น รับ `"Y"` / `"N"` ไม่ได้
   `allow` จึงต้องเป็นฟิลด์ **ข้อความ**

**ไม่อยากให้ตัว `"Y"` ปรากฏบนกระดาษ?** ตั้ง **ความทึบ (opacity) = 0** ให้ฟิลด์ `allow` แล้ววางไว้มุมกระดาษ —
มันจะยังอยู่ในสัญญาฟิลด์ให้ระบบภายนอกส่งค่ามาได้ แต่ไม่ปรากฏบนเอกสารที่พิมพ์ออกมา

### สิ่งที่ใช้ไม่ได้ — และทำไมระบบถึงต้องบล็อกคุณ

ระบบไม่รองรับการใช้งาน `Array`, `Date` และตระกูล `encodeURI`/`decodeURI` ซึ่งหากนำมาใช้งาน ระบบจะไม่แจ้งข้อผิดพลาด (error) ใดๆ แต่จะพิมพ์ข้อความดิบ `{...}` ลงบนเอกสารจริงโดยตรง:

```
{Array.isArray(orders)}             → พิมพ์ "{Array.isArray(orders)}" ลงบน PDF
{new Date().getFullYear() + 543}    → พิมพ์ "{new Date().getFullYear() + 543}"
{new Intl.NumberFormat(...)}        → พิมพ์ "{new Intl.NumberFormat(...)}"
{loanAmont}                         → พิมพ์ "{loanAmont}"  ← สะกดผิดตัวเดียว
```

นี่คืออันตรายทั้งหมดของนิพจน์: **นิพจน์ที่ทำงานผิดพลาดไม่ใช่ error แต่คือเอกสารที่ผิด** — พิมพ์เสร็จแล้ว เซ็นแล้ว ส่งถึงมือลูกค้าแล้ว กว่าจะรู้ตัว

`expression.unresolved` จึงบล็อกการเผยแพร่ทุกกรณีข้างบน รวมถึงคำที่สะกดผิดด้วย **ถ้าระบบทักชื่อตัวแปรที่คุณมั่นใจว่าถูก มันกำลังบอกคุณว่าตัวนั้นจะพิมพ์ดิบลงเอกสาร — เชื่อมันไว้ก่อน**

ด้วยหลักการเดียวกัน `Math.round` ใช้ได้ แต่ `eval`, `constructor` และ `process` ถูกกล่องทรายของระบบปฏิเสธทิ้งทั้งหมด — นิพจน์เอื้อมไปแตะเซิร์ฟเวอร์ที่มันรันอยู่ไม่ได้

### ค่ามาจากไหน

ฟิลด์นิพจน์ **ไม่อยู่ใน API contract** และไม่มีการขอให้ระบบภายนอกส่งค่านี้มา เพราะมันอ่านค่าจากฟิลด์ที่เขาส่งมาอยู่แล้ว การเพิ่มนิพจน์ลงเทมเพลตจึง **ไม่กระทบ JSON ที่ผู้เรียกส่งเข้ามาเลย** ค่าจะถูกคำนวณตอนสร้างเอกสาร ทุกหน้าที่มันปรากฏ
