No description
Find a file
Suphonchai Phoonsawat b3c1c667f7 Phase 12: draft generation (FR-16)
ไฟล์ร่าง (draft) — generate docx จาก template แบบ stateless ไม่สร้าง
Document/version; stamp เฉพาะ custom property TemplateId เพื่อให้ add-in
เห็นเป็น untracked; Start Versioning ส่ง form field templateId ให้ backend
ผูก template ตอน auto-create — เอกสารเริ่มที่ v1 (ไม่มี v0) + field sync เต็มระบบ

- Backend: POST /api/templates/{id}/generate-draft (200 คืนไฟล์) +
  DocxGenerationService.GenerateDraft + แยก TemplateFieldValidation ใช้ร่วมกับ
  generate + UploadVersion รับ templateId (non-GUID/unknown → 400,
  deactivated → ผูกได้, document เดิม → เฉย) + CORS expose Content-Disposition
- Add-in: readTrackedProperties อ่าน TemplateId + ส่ง templateId เฉพาะ
  startVersioning + hint ไฟล์ร่างใน task pane
- Web: ปุ่ม "สร้างไฟล์ร่าง" บนฟอร์มเดิม + downloadFile รับ init (POST → blob)
- Docs: spec v8 (FR-16, AC-19) + Phase 12 + CONTEXT.md + ADR 0002 +
  acceptance-review AC-19
- Tests: backend 82 / add-in 66 / web 59 + build ครบ; QA reviewer PASS
  (2 rounds); ค้าง manual บน Word จริง

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 10:51:13 +07:00
.vscode Add launch and task configurations for .NET Core development 2026-08-22 09:24:03 +07:00
addin Phase 12: draft generation (FR-16) 2026-09-10 10:51:13 +07:00
backend Phase 12: draft generation (FR-16) 2026-09-10 10:51:13 +07:00
docs Phase 12: draft generation (FR-16) 2026-09-10 10:51:13 +07:00
keycloak implement Phase 1-2 2026-08-19 10:54:55 +07:00
scripts implement phase 5 และ fix bugs 2026-08-19 23:06:57 +07:00
specs Phase 12: draft generation (FR-16) 2026-09-10 10:51:13 +07:00
web Phase 12: draft generation (FR-16) 2026-09-10 10:51:13 +07:00
.env.example implement phase 5 และ fix bugs 2026-08-19 23:06:57 +07:00
.gitignore phase 0 2026-08-18 12:52:40 +07:00
AGENTS.md Phase 11: version PDF (FR-15) 2026-09-08 14:01:13 +07:00
CLAUDE.md Phase 12: draft generation (FR-16) 2026-09-10 10:51:13 +07:00
CONTEXT.md Phase 12: draft generation (FR-16) 2026-09-10 10:51:13 +07:00
docker-compose.yml Refactor code structure for improved readability and maintainability 2026-09-01 10:51:14 +07:00
README.md Phase 11: version PDF (FR-15) 2026-09-08 14:01:13 +07:00
T14_Template.docx Refactor code structure for improved readability and maintainability 2026-09-01 10:51:14 +07:00

e-Court Documents Versioning (Word Add-in)

ระบบจัดการ version history ของเอกสาร Word — สร้างเอกสารจาก template ผ่านเว็บ ติดตามทุกการแก้ไขผ่าน add-in ใน Word และ sync ข้อมูลสำคัญกลับฐานข้อมูลอัตโนมัติ

คุณสมบัติหลัก

  • 📝 สร้างเอกสารจาก template — กรอก form บนเว็บ → ระบบ generate .docx จาก template (แทนที่ค่าใน content controls) พร้อม stamp metadata และขึ้นเป็น version 0 ฉบับตั้งต้น
  • 🗂️ Template management — upload ไฟล์ .docx ที่มี content controls แล้วระบบ detect fields อัตโนมัติ ตั้งชื่อ/ชนิด/required ต่อ field ผ่านหน้าเว็บ
  • 📋 รายการเอกสารต่อ template + detail — ดูเอกสารทุกฉบับที่สร้างจาก template, ค่า fields ปัจจุบัน และ version history (ขยายแต่ละ version เห็นค่าตอนนั้น) บนหน้าเว็บ
  • ⟳ Re-generate — สร้างเอกสารใหม่จากค่าปัจจุบันของเอกสารเดิม (แก้ไขค่าได้ก่อน) — เป็น v0 ของเอกสารใหม่ เอกสารต้นทางและ version history ไม่ถูกแตะ
  • 📌 ตรวจจับ tracked document — เปิดไฟล์ใน Word (Desktop Win/Mac หรือ Online) แล้ว add-in รู้ทันทีว่าไฟล์นี้อยู่ในระบบ พร้อมแสดง version history
  • 💾 Save & Version — ปุ่มใน task pane สร้าง version ใหม่ (stamp metadata + อัปโหลด snapshot ไป backend)
  • 💬 คำอธิบายต่อ version (commit message) — พิมพ์ข้อความอธิบายการเปลี่ยนแปลงได้ทุกครั้งที่สร้าง version (Save & Version, Start Versioning, สร้างจากเว็บ) แล้วแสดงกับ version นั้นใน history ทุกหน้า — เว้นว่างได้
  • 🔄 Field sync — ทุกครั้งที่สร้าง version ระบบ extract ค่าจาก content controls ของเอกสารไป update fields ใน DB ให้ sync กับเนื้อหาจริงเสมอ
  • ⬇️ ดาวน์โหลด version ย้อนหลัง — ทุก version เก็บครบพร้อม SHA256 hash ตรวจสอบความถูกต้องได้

สถาปัตยกรรม

ส่วน Stack หน้าที่
Add-in (/addin) Office.js, TypeScript, React, Fluent UI Task pane ใน Word — ตรวจจับ document, Save & Version, version history, field extraction
Backend (/backend) ASP.NET Core 8, EF Core, PostgreSQL, MinIO, OpenXML Version API, template system, docx generation, JWT validation
Web Portal (/web) Nuxt.js, TailwindCSS จัดการ template + สร้างเอกสารจาก form + ดูรายการเอกสาร/detail + re-generate (light/dark theme)
Auth Keycloak (OIDC + PKCE) Public clients ทั้ง add-in และเว็บ, backend validate ผ่าน JWKS
Storage MinIO (S3-compatible) bucket docx-versions (snapshot ต่อ version) + docx-templates (ไฟล์ template) — private ทั้งคู่

Flow หลัก:

  1. Admin upload template (.docx ที่มี content controls) → ระบบ detect fields อัตโนมัติ
  2. ผู้ใช้กรอก form บนเว็บ → ได้ไฟล์ .docx ที่เป็น version 0 แล้ว → เปิดใน Word
  3. Add-in รู้จักเอกสารทันที → แก้ไขเนื้อหา → กด Save & Version → เกิด version ใหม่ + fields ใน DB sync ตามเนื้อหาล่าสุด
  4. กลับมาที่เว็บ anytime — ดูรายการเอกสารของ template / detail (ค่า fields + ทุก version) / re-generate เป็นเอกสารใหม่จากค่าปัจจุบัน

สถานะโปรเจกต์

Phase เนื้อหา สถานะ
0 Scaffold 3 โปรเจกต์ (addin / backend / web) ✅ เสร็จ
1 Local dev infra — docker-compose + Keycloak (realm, 3 clients + PKCE) + MinIO (buckets) ✅ เสร็จ
2 Backend core — version API (FR-5–7) + JWT auth (FR-8) ✅ เสร็จ
3 Template system + docx generation + field sync (FR-9–11) ✅ เสร็จ
4 Add-in services layer — auth (PKCE + Dialog API), metadata, file/API, field extraction (FR-8, FR-11) ✅ เสร็จ
5 Task pane UI — sign-in, Start Versioning, Save & Version, history (FR-1–4) ✅ เสร็จ — ทดสอบจริงบน Word for Mac แล้ว (login/tracking/Save & Version ผ่าน, แก้ปัญหาที่เจอ 4 ประเด็น — ดู Troubleshooting) เหลือยืนยัน download + AC เต็มชุด
6 Web Portal UI (FR-9–10) ✅ เสร็จ — login ผ่าน Keycloak, template management, dynamic form, theme toggle
6.4 รายการเอกสารต่อ template + document detail + re-generate (FR-12) ✅ เสร็จ (เพิ่มทีหลัง v4)
7 Tests ครบชุด (backend integration + web Vitest + addin Jest) ✅ เสร็จ — 125 tests ผ่านทั้งหมดตอนนั้น (ดูหัวข้อ Tests สำหรับตัวเลขปัจจุบัน)
8 ไล่ acceptance criteria เต็มชุด ✅ เสร็จ — ผลครบ AC-1 ถึง 15 ที่ docs/acceptance-review.md
— Web Portal redesign — design system (token layer สี/ฟอนต์/radius) + layout responsive ทุกหน้า ✅ เสร็จ (นอก spec — แผนที่ docs/plans/web-portal-redesign.md)
9 Version message — commit message ต่อ version ทุก flow (FR-13) ✅ เสร็จ (เพิ่มทีหลัง v5 — แผนที่ docs/plans/version-message.md)
10 Field editor — เลือกรูปแบบช่องกรอกต่อ field (FR-14) ✅ เสร็จ (เพิ่มทีหลัง v6 — แผนที่ docs/plans/field-editor.md)

ใช้ได้แล้วตอนนี้: ครบทั้ง 3 ส่วน — backend API ครบทุก endpoint (version + template + generate + field sync + รายการเอกสาร/detail + commit message + field editor), add-in ครบทั้ง services และ task pane UI ทดสอบจริงบน Word for Mac แล้ว, web portal ใช้งานได้ทุกหน้า (login → จัดการ template → สร้างเอกสาร → ดูรายการ/detail → re-generate) บน design system + responsive layout ใหม่ · tests ครบชุด 156 ตัว (backend integration 60 รันบน infra จริง + addin 50 + web 46) · ไล่ AC 1–17 ครบแล้ว — เหลือยืนยันด้วยมือเฉพาะงานที่ทำแทนไม่ได้ (Word Online/Windows, click-through บน browser จริง — ดูท้าย acceptance-review.md)

เอกสาร

สำหรับผู้เริ่มพัฒนา: อ่าน spec ก่อน แล้วตามด้วย plan (เริ่มจาก Phase 0)

การพัฒนา (local)

Prerequisites

  • Docker Desktop
  • .NET 8 SDK + dotnet-ef tool (สำหรับ migration):
    dotnet tool install --global dotnet-ef --version 8.0.11
    
  • Node.js LTS (add-in และ web)
  • Dev cert สำหรับ localhost (ต้องทำก่อน docker compose up — Keycloak mount cert จากที่นี่):
    cd addin && npx office-addin-dev-certs install
    
    ทำครั้งเดียวต่อเครื่อง: สร้าง CA + cert ที่ trust ไว้ใน keychain ใช้ทั้ง add-in dev server (https://localhost:3000) และ Keycloak (https://localhost:8443)

1) รัน infrastructure (PostgreSQL + MinIO + Keycloak)

cp .env.example .env      # ครั้งแรกครั้งเดียว — แก้ password ได้ตามต้องการ
docker compose up -d

เมื่อขึ้นครบแล้ว:

บริการ URL เข้าด้วย
Keycloak admin console https://localhost:8443/admin/ KC_BOOTSTRAP_ADMIN_USERNAME / KC_BOOTSTRAP_ADMIN_PASSWORD จาก .env
MinIO console http://localhost:39001 MINIO_ROOT_USER / MINIO_ROOT_PASSWORD จาก .env
PostgreSQL localhost:35432 POSTGRES_USER / POSTGRES_PASSWORD จาก .env, ฐานข้อมูล wordversioning

ทำไม Keycloak ต้องเป็น HTTPS: Office Dialog API ที่ add-in ใช้เปิดหน้า login ปฏิเสธ http:// โดยสมบูรณ์ (error: "The HTTP protocol is not supported. Use HTTPS instead") — dev จึงให้ Keycloak เสิร์ฟ HTTPS ที่ port 8443 ด้วย cert ของ office-addin-dev-certs (CA ถูก trust ในเครื่องแล้ว Word/webview จึงเชื่อ) · ตัว issuer ใน token จะเป็น https://localhost:8443/realms/word-versioning ซึ่ง backend ตั้ง Authentication:Authority ตรงกัน

Keycloak จะ import realm word-versioning อัตโนมัติ (clients 3 ตัว + test user) จาก keycloak/realm-export.json — ไฟล์นี้คือ source of truth ของ realm ฝั่ง dev: แก้ realm ให้แก้ที่ไฟล์นี้แล้วรัน docker compose rm -sf keycloak && docker compose up -d เพื่อ import ใหม่ (การแก้ผ่าน admin console จะหายเมื่อ recreate container)

Test user สำหรับล็อกอิน: testuser / Test1234! (เปลี่ยนได้ใน realm-export.json)

MinIO จะมี bucket docx-versions และ docx-templates (private ทั้งคู่) สร้างให้อัตโนมัติ

2) Backend API

cd backend/WordVersioning.Api
dotnet ef database update    # ครั้งแรก / เมื่อมี migration ใหม่
dotnet run                   # รันทั้ง http://localhost:5075 และ https://localhost:7232 (Swagger ใช้ https ก็ได้)

ทำไม dev ต้องมี endpoint HTTPS ด้วย: task pane ของ add-in เป็นหน้า https://localhost:3000 และ webview ของ Word (WKWebView บน Mac) บล็อก fetch จากหน้า https ไป http แบบเงียบๆ — error ที่เห็นคือ "Load failed" — ส่วน CORS ก็ไม่ช่วยเพราะ request ไม่เคยถึง server · dev จึงเปิด endpoint https://localhost:7232 ด้วย cert ชุดเดียวกับ Keycloak (~/.office-addin-dev-certs, ปิดได้ผ่าน DevHttps:Enabled=false ใน appsettings.Development.json) · add-in ชี้ API ที่ https ตัวนี้อัตโนมัติ

ค่าเชื่อมต่อ (DB/MinIO/Keycloak) อยู่ใน appsettings.json + appsettings.Development.json — override ผ่าน env ได้ทั้งหมด เช่น ConnectionStrings__Default, MinIO__Endpoint, Authentication__Authority

API ที่มีตอนนี้ (ทุก endpoint ต้องแนบ Authorization: Bearer <token>):

# Documents / versions (FR-5–7, 11, 12, 13)
POST  /api/documents/{documentId}/versions                      # อัปโหลด version ใหม่ + field sync + commit message
GET   /api/documents/{documentId}                               # detail: ข้อมูลเอกสาร + template + ค่าล่าสุดทุก field (FR-12)
GET   /api/documents/{documentId}/versions                      # version history ใหม่ → เก่า (snapshot fields + message ต่อ version)
GET   /api/documents/{documentId}/versions/{versionId}/download       # ดาวน์โหลด version (proxy stream จาก MinIO)
GET   /api/documents/{documentId}/versions/{versionId}/download-link  # presigned URL อายุสั้น (ให้เบราว์เซอร์โหลดตรงจาก MinIO)

# Generate จาก template (FR-10, 13)
POST  /api/documents/generate                                    # { templateId, values, message? } → เอกสาร v0 พร้อม metadata

# Templates (FR-9, 12)
GET    /api/templates                                            # รายการ (?includeInactive=true สำหรับ admin)
GET    /api/templates/{id}                                       # รายละเอียด + field definitions
GET    /api/templates/{id}/documents                             # รายการเอกสารที่สร้างจาก template นี้ (FR-12)
POST   /api/templates                                            # upload .docx → detect fields อัตโนมัติ
PUT    /api/templates/{id}                                       # แก้ metadata + field definitions (label/type/editor/required/ลำดับ)
PUT    /api/templates/{id}/file                                  # แทนที่ไฟล์ → re-detect (บอก tag ที่เพิ่ม/หาย)
DELETE /api/templates/{id}                                       # deactivate (soft-delete)
  • Upload เป็น multipart/form-data: file (.docx เท่านั้น, จำกัด 100MB) + clientVersionId (GUID) + fields (optional, JSON map tag→text สำหรับ field sync) + message (optional, commit message ยาวไม่เกิน 2048 อักขระ — FR-13 · ว่าง/whitespace ล้วน = ไม่เก็บ)
  • documentId ยังไม่มีในระบบ = สร้างใหม่อัตโนมัติ · versionNumber เพิ่มอัตโนมัติต่อเอกสาร · เก็บ SHA256 ของทุก version
  • Field sync policy (FR-11): field ที่ส่งมาขาด (ผู้ใช้ลบ content control ใน Word) → เก็บค่าเดิม + mark missing ใน snapshot ไม่ fail request · ส่งเกินนิยาม → ignore
  • Generate (FR-10): backend เปิด template ด้วย OpenXML → แทนที่ค่าใน SDT ที่ run level (รักษา formatting) → stamp custom properties (SavedByAddIn, DocumentTrackingId) → upload เป็น v0 → response { documentId, versionId, versionNumber, downloadUrl } — เปิดไฟล์ใน Word แล้ว add-in (Phase 4–5) จะรู้จักทันที

ขอ token ทดสอบ (dev)

ทุก client ใน realm บังคับ PKCE (ปิด direct grants ตาม spec) และ token ของ admin-cli ไม่มี claim aud จึงเรียก backend ไม่ผ่าน (401) — ใช้สคริปต์นี้เดิน PKCE flow จริงแทน (login ด้วย test user ให้อัตโนมัติ):

python3 scripts/dev-token.py            # ปริ้น access token
python3 scripts/dev-token.py --clip     # หรือคัดลอกเข้า clipboard
TOKEN=$(python3 scripts/dev-token.py)   # เก็บใส่ตัวแปร

แล้วลอง upload:

DOC_ID=$(uuidgen | tr 'A-Z' 'a-z')
curl -X POST http://localhost:5075/api/documents/$DOC_ID/versions \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@เอกสาร.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
  -F "clientVersionId=$(uuidgen)"
# → { "versionId": "...", "versionNumber": 1 }

ลองเล่น template → generate → field sync (ผ่าน curl — ใช้ UI บนเว็บก็ได้ ดูหัวข้อ Web Portal)

ขั้นตอนเดียวกันนี้ทำผ่านหน้าเว็บได้สะดวกกว่า (ดูหัวข้อ "4) Web Portal") — ตัวอย่าง curl นี้เผื่อใช้กับ Swagger/automation

0) เตรียมไฟล์ template — เขียน .docx ใน Word แล้วแทรก content controls ในจุดที่จะเป็น field (Developer tab → Rich Text Content Control → Properties ตั้ง Tag ให้ unique ต่อไฟล์ เช่น employee_name, salary)

1) Upload template → ระบบ detect fields อัตโนมัติ:

curl -X POST http://localhost:5075/api/templates \
  -H "Authorization: Bearer $TOKEN" \
  -F "name=สัญญาจ้างงาน" \
  -F "file=@template.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document"
# → { "id": "...", "fields": [ { "key": "employee_name", ... }, ... ] }

2) ตั้งค่า field definitions (label ที่แสดงในฟอร์ม / ชนิด text|number|date / required / ลำดับ):

curl -X PUT http://localhost:5075/api/templates/$TPL_ID \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"fields": [
        { "key": "employee_name", "label": "ชื่อ-นามสกุล", "type": "text",   "isRequired": true,  "sortOrder": 0 },
        { "key": "salary",        "label": "เงินเดือน",    "type": "number", "isRequired": true,  "sortOrder": 1 } ]}'

3) Generate เอกสาร — ได้ .docx ที่เป็น version 0 พร้อม metadata (แนบ message ได้เลย — เป็น commit message ของ v0):

curl -X POST http://localhost:5075/api/documents/generate \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"templateId\":\"$TPL_ID\",\"values\":{\"employee_name\":\"สมชาย ใจดี\",\"salary\":\"50000\"},\"message\":\"ฉบับเสนอลงนามครั้งแรก\"}"
# → { "documentId": "...", "versionId": "...", "versionNumber": 1, "downloadUrl": "/api/documents/.../download" }

curl -H "Authorization: Bearer $TOKEN" -o ไฟล์ที่สร้าง.docx \
  "http://localhost:5075$DOWNLOAD_URL"

เปิดไฟล์ที่ได้ตรวจได้ว่า: ค่าถูกทุก field (formatting ของ template คงเดิม) และมี custom properties SavedByAddIn / DocumentTrackingId (unzip ดู docProps/custom.xml หรือ Word → File → Info → Properties → Advanced Properties)

4) Field sync ตอนสร้าง version ถัดไป — upload v2 พร้อมแนบ fields (map tag→text ที่ extract จาก content controls — ฝั่ง add-in ทำให้อัตโนมัติตอนกด Save & Version แล้ว) และ message (commit message — FR-13):

curl -X POST http://localhost:5075/api/documents/$DOC_ID/versions \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@ไฟล์แก้แล้ว.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
  -F "clientVersionId=$(uuidgen)" \
  -F 'fields={"employee_name":"สมชาย ใจดี มากกว่าเดิม","salary":"60000"}' \
  -F "message=ปรับเงินเดือนตามผลประเมิน"

แล้วดู GET /api/documents/$DOC_ID/versions — แต่ละ version มี snapshot fields และ message ของตัวเอง หากลบ content control ออกจาก Word ก่อน upload field นั้นจะเห็น "missing": true โดยค่าเดิมยังคงอยู่ (ไม่ error)

3) Add-in (ครบทั้ง services layer และ task pane UI)

cd addin
npx office-addin-dev-certs install   # ครั้งแรกครั้งเดียว (https localhost)
npm run build                        # webpack build + type-check ทั้ง program
npm test                             # Jest — unit tests (50 ตัว)
npm run lint                         # eslint (ใช้ eslint.config.mjs ของโปรเจกต์)
npm run validate                     # ตรวจ manifest.xml
npm start                            # dev server https://localhost:3000 — sideload ผ่าน manifest.xml

Services layer ที่มีแล้ว (addin/src/services/) — ทุกตัวมี unit tests ครอบ (mock Word.run, Office.context.ui, fetch):

ไฟล์ หน้าที่
authService.ts OIDC Authorization Code + PKCE (S256) ผ่าน Office Dialog API, token ใน memory เท่านั้น, silent refresh, state กัน CSRF
metadataService.ts อ่าน/stamp custom properties (SavedByAddIn ฯลฯ), Custom XML Part แบบ delete-then-add (fail เงียบๆ ตาม known bug บน Mac), readContentControlFields() (FR-11)
fileService.ts ดึงไฟล์ปัจจุบันผ่าน File/Slice API ประกอบเป็น Blob — เรียก closeAsync เสมอ
apiClient.ts fetch wrapper แนบ Bearer, โดน 401 → refresh + retry ครั้งเดียว, ยัง fail → AuthRequiredError ให้ UI กลับหน้า sign-in
versioningService.ts uploadVersion (แนบ fields + message), getVersions, downloadVersion + getDownloadLink (presigned)
versionFlow.ts orchestration ของ Start Versioning / Save & Version ตามลำดับ 7 ขั้นตอนของ spec + ส่ง commit message (FR-13)

หน้า login ของ add-in คือ addin/src/taskpane/auth-callback.html — redirect target ของ Keycloak (ตรง https://localhost:3000/auth-callback.html ใน realm-export.json)

Config ฝั่ง add-in ฉีดตอน build ผ่าน webpack DefinePlugin (อ่านจาก env — default ชี้ dev ในพื้นที่):

KEYCLOAK_AUTHORITY=https://localhost:8443/realms/word-versioning \
API_BASE_URL=https://localhost:7232 \
npm run build

(ตัวแปรที่เหลือ: KEYCLOAK_CLIENT_ID — default word-addin-officejs)

Task pane UI (Phase 5) — state machine checking → signed-out → untracked → tracking:

  • Sign-in gate (FR-1) — ยังไม่ login → ปุ่ม Sign in (เปิด Keycloak ผ่าน Office Dialog API) · login แล้วตรวจ property SavedByAddIn ในไฟล์: ตรง → โหลด version history ทันที (ไฟล์ที่สร้างจากเว็บเข้า state นี้เลย เห็น v0 โดยไม่ต้องกดอะไร) / ไม่ตรง → ปุ่ม Start Versioning This Document (กดแล้วเปิด dialog ยืนยันพร้อมช่องคำอธิบายก่อนสร้าง v1 — FR-13)
  • Save & Version (FR-2) — confirm dialog พร้อมช่อง คำอธิบาย version แบบ commit message ทางเลือก (FR-13) → stamp properties → อัปเดต Custom XML Part → native save → ดึงไฟล์ + extract fields (FR-11) → upload → refresh history · ทุกขั้นมี loading + error ไม่มี unhandled rejection
  • Soft reminder (FR-3) — ระหว่าง tracking ถามทุก 30 วิ ว่ามีแก้ไขที่ยังไม่ได้ version → banner เตือน (ไม่ block)
  • Version history (FR-4) — รายการ v เวลา/ผู้สร้าง + คำอธิบาย version (ถ้ามี) + ปุ่มดาวน์โหลด version นั้น (ขอ presigned URL อายุ 2 นาทีแล้วเปิดในเบราว์เซอร์ของเครื่อง — webview ของ Word ทิ้ง blob download แบบเงียบๆ ไฟล์จึงต้องมาจากเบราว์เซอร์จริง แล้วหาได้ในโฟลเดอร์ Downloads ของเบราว์เซอร์)
  • Session หมดอายุ (AC-9) — 401 → silent refresh → ยังไม่ได้ → กลับหน้า sign-in พร้อมข้อความ

Sideload เพื่อทดสอบใน Word จริง (ต้องรัน infra + backend + npm start ก่อน):

  • Word Desktop (Mac/Windows): cd addin && npm start (ใช้ office-addin-debugging sideload ให้อัตโนมัติ) หรือ insert manual: Word → Insert → My Add-ins → Upload My Add-in → เลือก addin/manifest.xml
  • Word Online: ไฟล์ใดก็ได้ใน OneDrive/SharePoint → Insert → Office Add-ins → Upload My Add-in → เลือก addin/manifest.xml
  • ไล่ทดสอบตาม acceptance criteria: login ผ่าน dialog → เปิดไฟล์ใหม่เห็นปุ่ม Start Versioning → กดแล้วได้ v1 → แก้ไขแล้ว Save & Version → history เพิ่ม + ดาวน์โหลด version เก่าเปิดได้
  • ⚠️ ห้าม cp ทับ manifest ใน ~/Library/Containers/com.microsoft.Word/Data/Documents/wef/ — npm start สร้าง symlink ไว้ การทับทำให้ครั้งถัดไปขึ้น EEXIST — ถาวรแก้ที่ addin/manifest.xml ต้นทางแล้ว restart Word (ถ้าเจอ EEXIST แล้ว: ลบไฟล์ใน wef ทิ้งแล้ว npm start ใหม่)

4) Web Portal

cd web && npm run dev       # http://localhost:5173 (ห้ามใช้ 3000 — ชน add-in)
cd web && npm test          # Vitest — unit tests (46 ตัว)

เปิด http://localhost:5173 แล้วกดเข้าสู่ระบบด้วย testuser / Test1234! (ผ่าน Keycloak — client word-versioning-web ใช้ PKCE เหมือนกันทั้งระบบ) หน้าที่มีให้ใช้:

หน้า ทำอะไร
/ หน้าหลัก card รายการ template ที่ active → เข้าฟอร์มสร้างเอกสาร หรือดูเอกสารที่สร้างแล้วของ template นั้น
/templates ตาราง template ทั้งหมด (แสดงตัวที่ deactivate ได้) — แก้ไข / activate / deactivate / ดูรายการเอกสาร
/templates/new upload template → เห็น field list ที่ระบบ detect อัตโนมัติทันที
/templates/{id}/edit แก้ metadata + ตั้ง label/type (text|number|date)/editor (FR-14 — text เลือก textbox/textarea ได้, number/date ใช้ control ตาม type)/required/ลำดับ + แทนที่ไฟล์ (บอก tag ที่เพิ่ม/หาย)
/templates/{id}/documents รายการเอกสารที่สร้างจาก template — จำนวน version, กิจกรรมล่าสุด, เข้าไปดู detail
/documents/create/{templateId} dynamic form ตาม field definitions — control ต่อ field ตาม editor (FR-14: textarea สำหรับข้อความยาว · ช่องตัวเลข · date picker) + validate ฝั่ง client + backend + ช่อง คำอธิบาย version ทางเลือก (FR-13) → สร้างเป็น v0 + เปิดใน Word
/documents/{documentId} detail เอกสาร — ค่า fields ปัจจุบัน + version history พร้อมคำอธิบายของแต่ละ version (กดขยายแถวเห็นค่า fields ตอนนั้น + ดาวน์โหลดต่อ version) + ปุ่ม ⟳ re-generate

สร้าง template แรกจากไฟล์จริง: เขียน .docx ใน Word แล้วแทรก content controls ในจุดที่จะเป็น field (Developer tab → Rich Text Content Control → Properties ตั้ง Tag ให้ unique ต่อไฟล์ เช่น employee_name, salary) → หน้า /templates/new upload ไฟล์ → ระบบ detect tags ออกมาเป็นรายการ fields → กด "ตั้งค่าต่อ" ไปใส่ label/type/editor/required ให้เรียบร้อย (field ข้อความยาวเช่นที่อยู่/รายละเอียด เลือก editor เป็น textarea ได้ — FR-14) → กลับมาหน้าหลักเพื่อสร้างเอกสาร

Re-generate (สร้างเอกสารใหม่จากข้อมูลเดิม): ที่หน้า detail ของเอกสาร กด "⟳ สร้างเอกสารใหม่จากข้อมูลนี้" → เข้าฟอร์มสร้างเอกสารโดย prefill ค่าปัจจุบันของทุก field ไว้ให้แก้ไขก่อน (ช่องคำอธิบาย version ว่าง — เป็นเอกสารใหม่ได้ข้อความใหม่) → กดสร้าง → ได้เอกสารใหม่เป็น v0 (documentId ใหม่) โดยเอกสารต้นทางและ version history ของมันไม่ถูกแตะ · template ที่ deactivate ไปแล้วจะกดไม่ได้ (ปุ่มถูก disable พร้อมเหตุผล)

Theme toggle (light/dark) อยู่มุมขวาบนของทุกหน้า (header ติด sticky) — จำค่าไว้หลัง reload

Design system + responsive (redesign ล่าสุด): สี/ฟอนต์/radius/shadow ทั้งหมดเป็น token ผ่าน CSS variables ใน web/app/assets/css/tailwind.css (derive จาก scheme ของ admincourt.go.th — ฟอนต์ Sukhumvit Set/Sarabun, primary #0000EE, elevated ครีม) + component classes กลาง (.btn*/.input/.card/.table-card ฯลฯ) · ทุกหน้า responsive ครบ — ตารางทุกตัวเปลี่ยนเป็น card list บนจอเล็กกว่า md, sticky header + backdrop blur, ฟอร์ม stack บนมือถือ

Config: ค่า default ชี้ dev ในพื้นที่อยู่ใน nuxt.config.ts — override ผ่าน env ได้ทั้งหมด (ดู web/.env.example): NUXT_PUBLIC_API_BASE_URL, NUXT_PUBLIC_KEYCLOAK_AUTHORITY, NUXT_PUBLIC_KEYCLOAK_CLIENT_ID

Tests

ชุด test ทั้งหมดของระบบ — รวม 156 ตัว ครอบทุก flow ตาม acceptance criteria:

# 1) backend integration (60 ตัว) — ต้องมี Docker รันอยู่
cd backend
dotnet test
dotnet test --filter "FullyQualifiedName~FieldSyncTests"   # รันเฉพาะชุด (Auth/UploadVersion/Template/Generate/FieldSync/VersionMessage/FieldEditor)

# 2) add-in unit (50 ตัว) — mock Word.run / Office.context.ui / fetch / Keycloak
cd addin && npm test
npx jest src/services/versionFlow.test.ts                  # รันเดี่ยว

# 3) web unit (46 ตัว)
cd web && npm test
npx vitest run test/useTheme.spec.ts                       # รันเดี่ยว

backend integration (backend/WordVersioning.Api.IntegrationTests) — ชุดใหญ่สุด: ยก PostgreSQL + MinIO + Keycloak จริงผ่าน Testcontainers ทุกครั้งที่รัน (~1 นาทีรวมการยก infra — ไม่แตะของ dev ใน docker-compose เลย) แล้วยิง API ผ่าน WebApplicationFactory พร้อม token จริงจาก Keycloak (validate JWKS/issuer/audience เต็มขั้นตอน):

  • AuthTests — ไม่แนบ token / token ขยะ → 401 ครบทุก endpoint (AC-3)
  • UploadVersionTests — upload v1/v2 + versionNumber เพิ่ม + history เรียงใหม่→เก่า + download ได้ bytes ตรงเป๊ะ + ไฟล์ไม่ใช่ docx → 400 + CreatedByUserId ตรง sub ใน JWT (AC-5, 6, 8)
  • TemplateTests — detect SDT tags ครบทุกรูปแบบ (run เดี่ยว/multi-run/block/tag ซ้ำ) + บันทึก field defs + replace ไฟล์ (tag เพิ่ม/หาย) + deactivate แล้ว generate ไม่ได้แต่เอกสารเก่ายังใช้ได้ (AC-11)
  • GenerateTests — generate แล้วค่าครบทุก field + multi-run รวมเป็น run เดียวรักษา formatting + stamp SavedByAddIn/DocumentTrackingId ลงไฟล์ + v0 + field values ใน DB + required/type validation + FR-12 (detail merge orphan สองทาง, รายการเอกสารต่อ template) (AC-12, 15)
  • FieldSyncTests — Save & Version พร้อม fields: latest values อัปเดต + snapshot ต่อ version (AC-13) · ลบ content control → request สำเร็จ ค่าเดิมคงอยู่ + mark missing (AC-14)
  • VersionMessageTests — commit message ต่อ version (FR-13/AC-16): เก็บ + echo ใน history · ว่าง/whitespace → null · trim · เกิน 2048 → 400 ไม่มี side effect · ยาวพอดี 2048 ผ่าน · message ต่อ version เป็นของตัวเอง · generate v0 พร้อม message · generate oversize → 400 ก่อนแตะ MinIO
  • FieldEditorTests — editor ต่อ field (FR-14/AC-17): PUT editor=textarea → persist + echo · ไม่ส่ง → resolve default ตาม type + DB เก็บ null · ค่า = default → normalize เป็น null · editor ไม่รู้จัก/ไม่เข้ากับ type → 400 · tag ใหม่จาก replace ได้ default

ไฟล์ .docx ที่ใช้ตอน test สร้างใน memory ด้วย OpenXML (TestDocx.cs) — ไม่มี binary ติดใน repo · realm ของ Keycloak ทดสอบเป็นสำเนาของ dev (Fixtures/realm-word-versioning-test.json) ที่เปิด password grant ให้ client word-versioning-web เฉพาะตอน test เพื่อขอ token ได้โดยไม่ต้องเดิน PKCE

web unit (web/test/) — fieldValidation.spec.ts (validate ฟอร์มสร้างเอกสาร: required/number/date ครบ edge cases) · useTheme.spec.ts (toggle + persist + system default) · snapshot.spec.ts (normalizeSnapshot รองรับ snapshot ทั้ง 2 รูปแบบที่หน้า detail ใช้) — รันบน happy-dom โดย stub Nuxt auto-imports เบาๆ (test/setup.ts)

addin unit (addin/src/services/*.test.ts) — ตามที่มาจาก Phase 4–5: PKCE flow, metadata stamp + XML part fail-แบบ-ไม่-throw, apiClient retry-on-401, fileService ประกอบ slice ครบ 4 รูปแบบ, versionFlow 7 ขั้น + ส่ง commit message ของทั้ง Save & Version และ Start Versioning (FR-13)

Troubleshooting (จากการทดสอบจริงบน Word for Mac)

ปัญหาที่เจอตอน sideload test รอบแรกๆ พร้อมสาเหตุ/วิธีแก้ — แก้ไขในโค้ดแล้วทั้งหมด เก็บไว้เป็นบทเรียน:

อาการ สาเหตุ วิธีแก้
กด Sign in แล้ว "The HTTP protocol is not supported. Use HTTPS instead" Office Dialog API ปฏิเสธ http:// โดยสมบูรณ์ แต่ Keycloak dev เดิมรัน http://8080 Keycloak เสิร์ฟ HTTPS ที่ https://localhost:8443 ด้วย cert ของ office-addin-dev-certs (docker-compose mount ~/.office-addin-dev-certs เข้า container)
Login ผ่านแต่ทุก API call ขึ้น "Load failed" WKWebView บล็อก fetch จากหน้า https ไป http (mixed content) — task pane เป็น https://localhost:3000 แต่ API เป็น http://localhost:5075 (CORS ไม่เกี่ยว — ตรวจแล้วเรียบร้อย) backend เปิด endpoint https://localhost:7232 ด้วย cert ชุดเดียวกัน (DevHttps:* ใน appsettings.Development.json) — add-in ชี้อันนี้อัตโนมัติ
กด Start Versioning แล้ว "รูปแบบข้อมูลของ slice ไม่รองรับ" host แต่ละตัวส่ง slice.data คนละรูป (Win = ArrayBuffer/view, Mac = base64 string หรือ number[]) fileService.sliceDataToBytes() normalize ครบทั้ง 4 รูปแบบ
กดดาวน์โหลด version แล้วไม่มีไฟล์โผล่ที่ไหนเลย webview ของ Word ทิ้ง blob download (a[download]) แบบเงียบๆ ขอ presigned URL อายุ 2 นาทีจาก download-link แล้วเปิดในเบราว์เซอร์ของเครื่อง (openBrowserWindow) — ไฟล์ลงโฟลเดอร์ Downloads ของเบราว์เซอร์
แก้ webpack.config.js แล้วค่าเก่ายังติดอยู่ webpack ไม่ restart เองเมื่อ config เปลี่ยน + dev-server ตัวเก่าถือ port 3000 อยู่ (ตัวใหม่ขึ้นไม่ได้) kill dev-server เดิม (`lsof -ti :3000
npm start ขึ้น EEXIST ... wef/...manifest.xml มีการ cp ทับ symlink ของ sideload จนกลายเป็นไฟล์ธรรมดา ลบไฟล์นั้นใน wef folder ทิ้งแล้ว npm start ใหม่ (ดูคำเตือนในหัวข้อ Sideload)
รัน backend ด้วย dotnet run --no-launch-profile แล้ว API ตอบ 500 (No password has been provided) --no-launch-profile ตัด ASPNETCORE_ENVIRONMENT=Development (ปกติตั้งโดย launchSettings) ทิ้ง → ไปอ่าน appsettings.json ที่ DB password ว่าง ใส่ env เอง: ASPNETCORE_ENVIRONMENT=Development ASPNETCORE_URLS=http://localhost:5075 dotnet run --no-launch-profile หรือรัน dotnet run เฉยๆ
curl token จาก admin-cli แล้วเรียก backend ได้ 401 token ของ admin-cli ไม่มี claim aud (ไม่ได้อยู่ใน realm-export จึงไม่มี audience mapper) ใช้ python3 scripts/dev-token.py (เดิน PKCE จริงด้วย client word-versioning-web)

กติกาที่สรุปได้: ใน dev ทุก service ที่ task pane (หน้า https) จะเรียก ต้องเป็น HTTPS ด้วย cert จาก office-addin-dev-certs ทั้งหมด — Keycloak (8443) และ backend (7232)

เทคโนโลยี

  • Add-in: Office.js (WordApi 1.5+), TypeScript, React, Fluent UI, webpack — scaffold ด้วย yo office
  • Backend: .NET 8, EF Core + Npgsql (PostgreSQL), AWSSDK.S3 → MinIO (ForcePathStyle = true), DocumentFormat.OpenXml, JwtBearer
  • Web: Nuxt.js (Vue 3), TailwindCSS (darkMode: 'class')
  • Auth: Keycloak — realm word-versioning, clients: word-addin-officejs, word-versioning-web, word-addin-vsto (public + PKCE S256 ทุกตัว)

Version PDF (FR-15 / spec v7)

เว็บ generate/re-generate เป็น v0 ฉบับตั้งต้น ไม่มี PDF. Start Versioning และ Save & Version สร้าง v1+ พร้อม PDF จาก Word (PdfFile 1.1 runtime guard; ไม่เพิ่ม requirement ใน manifest). PDF capture/upload เป็น best-effort: docx สำเร็จได้แม้ PDF ล้ม และ task pane เตือนตาม hasPdf จาก backend.

DocumentVersion.PdfObjectKey nullable เก็บ sibling .pdf ใน bucket เดิม; history มี hasPdf. /download และ /download-link รับ ?format=pdf, ไม่มี PDF → 404. Presigned URLs กำหนดชื่อไฟล์ไทย + _vN; PDF inline, docx attachment. เว็บเปิด Word ผ่าน ms-word:ofe|u|<url> พร้อมดาวน์โหลดสำรอง; PDF เปิดแท็บใหม่. Add-in ใช้ openBrowserWindow เท่านั้น. ข้อมูลเก่าไม่ backfill หรือเปลี่ยนเลข.