eCourt-Doc-Edit/README.md
Suphonchai Phoonsawat 0148f48204 Phase 7-8
2026-08-20 13:51:42 +07:00

341 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 1 อัตโนมัติ**
- 🗂️ **Template management** — upload ไฟล์ .docx ที่มี content controls แล้วระบบ **detect fields อัตโนมัติ** ตั้งชื่อ/ชนิด/required ต่อ field ผ่านหน้าเว็บ
- 📋 **รายการเอกสารต่อ template + detail** — ดูเอกสารทุกฉบับที่สร้างจาก template, ค่า fields ปัจจุบัน และ version history (ขยายแต่ละ version เห็นค่าตอนนั้น) บนหน้าเว็บ
- ⟳ **Re-generate** — สร้างเอกสารใหม่จากค่าปัจจุบันของเอกสารเดิม (แก้ไขค่าได้ก่อน) — เป็น v1 ของเอกสารใหม่ **เอกสารต้นทางและ version history ไม่ถูกแตะ**
- 📌 **ตรวจจับ tracked document** — เปิดไฟล์ใน Word (Desktop Win/Mac หรือ Online) แล้ว add-in รู้ทันทีว่าไฟล์นี้อยู่ในระบบ พร้อมแสดง version history
- 💾 **Save & Version** — ปุ่มใน task pane สร้าง version ใหม่ (stamp metadata + อัปโหลด snapshot ไป backend)
- 🔄 **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 1 แล้ว → เปิดใน 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](docs/acceptance-review.md) |
**ใช้ได้แล้วตอนนี้:** ครบทั้ง 3 ส่วน — backend API ครบทุก endpoint (version + template + generate + field sync + รายการเอกสาร/detail), add-in ครบทั้ง services และ task pane UI ทดสอบจริงบน Word for Mac แล้ว, web portal ใช้งานได้ทุกหน้า (login → จัดการ template → สร้างเอกสาร → ดูรายการ/detail → re-generate) · **tests ครบชุด 125 ตัว** (backend integration 42 รันบน infra จริง + addin 47 + web 36) · ไล่ AC 1–15 ครบแล้ว — เหลือยืนยันด้วยมือเฉพาะงานที่ทำแทนไม่ได้ (Word Online/Windows, click-through บน browser จริง — ดูท้าย [acceptance-review.md](docs/acceptance-review.md))
## เอกสาร
- [specs/spec-officejs.md](specs/spec-officejs.md) — spec เต็มรูปแบบ: functional requirements (FR-1–12), data models, acceptance criteria (1–15)
- [docs/plans/officejs-implementation-plan.md](docs/plans/officejs-implementation-plan.md) — implementation plan แบ่ง 9 phases พร้อม definition of done
- [docs/acceptance-review.md](docs/acceptance-review.md) — ผลตรวจ acceptance criteria ทั้ง 15 ข้อ พร้อมปัญหาที่เจอและรายการที่เหลือต้องยืนยันด้วยมือ
สำหรับผู้เริ่มพัฒนา: อ่าน spec ก่อน แล้วตามด้วย plan (เริ่มจาก Phase 0)
## การพัฒนา (local)
### Prerequisites
- Docker Desktop
- .NET 8 SDK + `dotnet-ef` tool (สำหรับ migration):
```bash
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 จากที่นี่):
```bash
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)
```bash
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:9001 | `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` จาก `.env` |
| PostgreSQL | `localhost:5432` | `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](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
```bash
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)
POST /api/documents/{documentId}/versions # อัปโหลด version ใหม่ + field sync
GET /api/documents/{documentId} # detail: ข้อมูลเอกสาร + template + ค่าล่าสุดทุก field (FR-12)
GET /api/documents/{documentId}/versions # version history ใหม่ → เก่า (พร้อม snapshot fields)
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)
POST /api/documents/generate # { templateId, values } → เอกสาร v1 พร้อม 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/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)
- `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 เป็น v1 → 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 ให้อัตโนมัติ):
```bash
python3 scripts/dev-token.py # ปริ้น access token
python3 scripts/dev-token.py --clip # หรือคัดลอกเข้า clipboard
TOKEN=$(python3 scripts/dev-token.py) # เก็บใส่ตัวแปร
```
แล้วลอง upload:
```bash
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 อัตโนมัติ:**
```bash
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 / ลำดับ):
```bash
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 1 พร้อม metadata:**
```bash
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\"}}"
# → { "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 แล้ว):
```bash
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"}'
```
แล้วดู `GET /api/documents/$DOC_ID/versions` — แต่ละ version มี snapshot fields ของตัวเอง หากลบ content control ออกจาก Word ก่อน upload field นั้นจะเห็น `"missing": true` โดยค่าเดิมยังคงอยู่ (ไม่ error)
### 3) Add-in (ครบทั้ง services layer และ task pane UI)
```bash
cd addin
npx office-addin-dev-certs install # ครั้งแรกครั้งเดียว (https localhost)
npm run build # webpack build + type-check ทั้ง program
npm test # Jest — unit tests (47 ตัว)
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/](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`), getVersions, downloadVersion + getDownloadLink (presigned) |
| `versionFlow.ts` | orchestration ของ Start Versioning / Save & Version ตามลำดับ 7 ขั้นตอนของ spec |
หน้า login ของ add-in คือ [addin/src/taskpane/auth-callback.html](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 ในพื้นที่):
```bash
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 นี้เลย เห็น v1 โดยไม่ต้องกดอะไร) / ไม่ตรง → ปุ่ม **Start Versioning This Document**
- **Save & Version (FR-2)** — confirm dialog → 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 นั้น (ขอ 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
```bash
cd web && npm run dev # http://localhost:5173 (ห้ามใช้ 3000 — ชน add-in)
cd web && npm test # Vitest — unit tests (36 ตัว)
```
เปิด <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`)/required/ลำดับ + แทนที่ไฟล์ (บอก tag ที่เพิ่ม/หาย) |
| `/templates/{id}/documents` | **รายการเอกสารที่สร้างจาก template** — จำนวน version, กิจกรรมล่าสุด, เข้าไปดู detail |
| `/documents/create/{templateId}` | dynamic form ตาม field definitions (validate ฝั่ง client + backend) → สร้างเป็น v1 + ดาวน์โหลด |
| `/documents/{documentId}` | **detail เอกสาร** — ค่า fields ปัจจุบัน + version history (กดขยายแถวเห็นค่า 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/required ให้เรียบร้อย → กลับมาหน้าหลักเพื่อสร้างเอกสาร
**Re-generate (สร้างเอกสารใหม่จากข้อมูลเดิม):** ที่หน้า detail ของเอกสาร กด **"⟳ สร้างเอกสารใหม่จากข้อมูลนี้"** → เข้าฟอร์มสร้างเอกสารโดย prefill ค่าปัจจุบันของทุก field ไว้ให้แก้ไขก่อน → กดสร้าง → ได้**เอกสารใหม่เป็น v1** (documentId ใหม่) โดยเอกสารต้นทางและ version history ของมันไม่ถูกแตะ · template ที่ deactivate ไปแล้วจะกดไม่ได้ (ปุ่มถูก disable พร้อมเหตุผล)
Theme toggle (light/dark) อยู่มุมขวาบนของทุกหน้า — จำค่าไว้หลัง reload
**Config:** ค่า default ชี้ dev ในพื้นที่อยู่ใน `nuxt.config.ts` — override ผ่าน env ได้ทั้งหมด (ดู [web/.env.example](web/.env.example)): `NUXT_PUBLIC_API_BASE_URL`, `NUXT_PUBLIC_KEYCLOAK_AUTHORITY`, `NUXT_PUBLIC_KEYCLOAK_CLIENT_ID`
## Tests
ชุด test ทั้งหมดของระบบ — รวม **125 ตัว** ครอบทุก flow ตาม acceptance criteria:
```bash
# 1) backend integration (42 ตัว) — ต้องมี Docker รันอยู่
cd backend
dotnet test
dotnet test --filter "FullyQualifiedName~FieldSyncTests" # รันเฉพาะชุด (Auth/UploadVersion/Template/Generate/FieldSync)
# 2) add-in unit (47 ตัว) — mock Word.run / Office.context.ui / fetch / Keycloak
cd addin && npm test
npx jest src/services/versionFlow.test.ts # รันเดี่ยว
# 3) web unit (36 ตัว)
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` ลงไฟล์ + v1 + 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)
ไฟล์ .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 ขั้น
## 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 | xargs kill`) แล้ว `npm start` ใหม่ทุกครั้งที่แก้ config |
| `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 ทุกตัว)