phase 0
This commit is contained in:
commit
41573b3c4f
54 changed files with 32839 additions and 0 deletions
438
docs/plans/officejs-implementation-plan.md
Normal file
438
docs/plans/officejs-implementation-plan.md
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
# Implementation Plan: Word Versioning Add-in (Office.js + .NET 8)
|
||||
|
||||
> แผนงานสร้างระบบจาก spec: [`specs/spec-officejs.md`](../../specs/spec-officejs.md)
|
||||
> สถานะ: รอเริ่ม implement (repo ยังไม่มีโค้ด)
|
||||
> วันที่แผน: 2026-08-18 (ฉบับแก้ไข v2: เพิ่ม FR-9 ถึง FR-11 — ระบบ template, สร้างเอกสาร v1 ผ่านเว็บ และ field sync)
|
||||
|
||||
---
|
||||
|
||||
## 1. ภาพรวม
|
||||
|
||||
ระบบ 3 ส่วนที่ทำงานร่วมกัน:
|
||||
|
||||
| ส่วน | เทคโนโลยี | หน้าที่ |
|
||||
|---|---|---|
|
||||
| **Add-in** (`/addin`) | Office.js, TypeScript, React, Fluent UI, webpack (scaffold จาก `yo office`) | Task pane ใน Word Desktop (Win/Mac) + Word Online — ตรวจจับ tracked document, ปุ่ม Save & Version, แสดง version history, จัดการ auth, extract fields จากเอกสาร |
|
||||
| **Backend** (`/backend/WordVersioning.Api`) | ASP.NET Core 8, EF Core, **PostgreSQL**, AWSSDK.S3 → **MinIO**, DocumentFormat.OpenXml | เก็บ version metadata + ไฟล์ .docx ใน MinIO, validate JWT, ระบบ template + generate เอกสาร + field sync |
|
||||
| **Web Portal** (`/web`) | **Nuxt.js + TailwindCSS** (รองรับ light/dark theme) | เว็บแยกสำหรับ (1) จัดการ template (2) กรอก form สร้างเอกสารจาก template → ได้ .docx ที่เป็น **version 1** พร้อม metadata แล้ว |
|
||||
| **Auth** | **Keycloak** (OIDC) | Public clients + PKCE (ทั้ง add-in และเว็บ), backend validate ผ่าน JWKS |
|
||||
|
||||
### ข้อตกลงที่ยืนยันแล้ว
|
||||
- **Database = PostgreSQL** (เลือกจาก spec ที่เปิดไว้ให้เลือก SQL Server หรือ PostgreSQL) — ใช้ provider `Npgsql.EntityFrameworkCore.PostgreSQL`
|
||||
- **Scope** = Office.js add-in + .NET backend + Web Portal ตามเอกสารนี้ (spec มีอ้างถึง `spec-vsto.md` แต่ไฟล์ยังไม่มี — แผนนี้เพียง pre-register Keycloak client `word-addin-vsto` ไว้ตามที่ spec กำหนด ไม่มีงาน VSTO)
|
||||
- หน้าจอสร้างเอกสารครั้งแรก = **เว็บแยก (standalone)** — กรอกนอก Word → ดาวน์โหลด .docx ที่ backend stamp metadata + สร้าง v1 ไว้แล้ว → เปิดใน Word แล้ว add-in รู้จักทันที
|
||||
- Template = **หลาย template + หน้าจัดการเต็มรูปแบบ** (CRUD + นิยาม field ต่อ template)
|
||||
- Field sync = **เฉพาะ fields จาก template** (ผูกกับ content controls) ไม่เก็บ full text
|
||||
- **FR-9 ถึง FR-11 เป็น requirement ที่เพิ่มภายหลัง spec เดิม** — spec ได้รับการ update เป็นฉบับ v2 แล้ว (ครอบ FR-1–11 + AC-1–14) ให้ถือ spec เป็น source of truth ของ requirements
|
||||
|
||||
---
|
||||
|
||||
## 2. กติกาพื้นฐานที่ทุก phase ต้องยึด
|
||||
|
||||
กติกาเหล่านี้เป็นข้อจำกัดของแพลตฟอร์ม ห้ามละเมิดไม่ว่าจะ implement phase ไหน:
|
||||
|
||||
1. **ห้ามออกแบบ flow ที่พึ่งการดัก native save** (Ctrl+S / File>Save) — Office.js ไม่มี before-save event ที่ cancel ได้ ต้องใช้ explicit action (ปุ่มใน task pane / ribbon) เท่านั้น
|
||||
2. **Custom Document Properties = primary flag** สำหรับตรวจจับว่าไฟล์ผ่าน add-in นี้
|
||||
3. **Custom XML Part = secondary storage เท่านั้น** — แก้ไม่ได้ตรงๆ ต้อง `delete()` แล้ว `add()` ใหม่ทุกครั้ง และมี known bug ที่อาจไม่ persist บน Mac บาง build → ระบบต้องทำงานถูกต้องแม้ XML part ล้มเหลวแบบ silent
|
||||
4. **ห้าม redirect iframe ของ task pane ไป Keycloak ตรงๆ** (โดน `X-Frame-Options`/CSP บล็อก) — ต้องใช้ Office Dialog API (`displayDialogAsync`) + `messageParent`; ส่วน `/web` เป็น top-level page ใช้ OIDC redirect ปกติได้
|
||||
5. **Token เก็บใน memory เท่านั้น** — ห้าม localStorage; ถ้าจำเป็นต้อง persist ใช้ `sessionStorage` อายุสั้น (ข้อยกเว้น: theme preference ของ `/web` เก็บ localStorage ได้เพราะไม่ใช่ข้อมูลรับรอง)
|
||||
6. **Backend คือ source of truth** ของ version history และ field values เสมอ
|
||||
7. **Config ทุกอย่าง (MinIO, Keycloak, DB) แยกเป็น env/`appsettings.json`** ห้าม hardcode
|
||||
8. **ทุก Office.js call ต้อง catch** และแสดง error ที่เข้าใจง่ายใน task pane — ห้ามปล่อย unhandled promise rejection
|
||||
9. **Field key = content control (SDT) tag** ต้อง unique ต่อ template — ใช้ tag เดียวกันทั้งตอน generate (backend), ตอน extract (add-in) และตอน sync (DB)
|
||||
10. **Generation + metadata stamping ทำฝั่ง backend เท่านั้น** (OpenXML SDK) เพื่อให้ v1 + custom properties เกิดพร้อมกันแบบ atomic — ไฟล์ที่โหลดจากเว็บถูกต้องสมบูรณ์ตั้งแต่ครั้งแรก ไม่ต้องพึ่ง client ตั้งค่าใดๆ
|
||||
11. **⚠️ Port ห้ามชนกัน:** add-in dev server = `https://localhost:3000` (ตาม yo office) → Nuxt dev server ต้องตั้ง **port 5173** ใน `nuxt.config.ts` ห้ามใช้ default 3000
|
||||
|
||||
---
|
||||
|
||||
## 3. โครงสร้างไฟล์เป้าหมาย
|
||||
|
||||
```
|
||||
/word-addin
|
||||
/addin # Phase 0, 4, 5
|
||||
manifest.xml
|
||||
/src
|
||||
/taskpane
|
||||
taskpane.html
|
||||
taskpane.tsx
|
||||
auth-callback.html
|
||||
/components
|
||||
VersionHistoryPanel.tsx
|
||||
ConfirmVersionDialog.tsx
|
||||
/services
|
||||
metadataService.ts # custom properties + custom XML part + content controls
|
||||
versioningService.ts
|
||||
fileService.ts
|
||||
authService.ts
|
||||
apiClient.ts
|
||||
/models
|
||||
versionRecord.ts
|
||||
package.json
|
||||
webpack.config.js
|
||||
/backend # Phase 0, 2, 3
|
||||
/WordVersioning.Api
|
||||
Program.cs
|
||||
/Controllers
|
||||
DocumentsController.cs # FR-5–7 + generate endpoint + field sync
|
||||
TemplatesController.cs # FR-9
|
||||
/Models
|
||||
Document.cs # + TemplateId
|
||||
DocumentVersion.cs # + FieldsSnapshotJson
|
||||
Template.cs
|
||||
TemplateField.cs
|
||||
DocumentFieldValue.cs
|
||||
/Data
|
||||
AppDbContext.cs
|
||||
/Services
|
||||
ObjectStorageService.cs
|
||||
HashService.cs
|
||||
DocxGenerationService.cs # FR-10: แทนที่ SDT + stamp custom properties
|
||||
appsettings.json
|
||||
WordVersioning.Api.csproj
|
||||
/web # Phase 0, 6
|
||||
nuxt.config.ts # devServer port 5173
|
||||
tailwind.config.ts # darkMode: 'class'
|
||||
/pages
|
||||
index.vue # หน้าหลัก: เลือก template เพื่อสร้างเอกสาร
|
||||
templates/index.vue # รายการ template (admin)
|
||||
templates/new.vue # upload template + ตั้งค่า field defs
|
||||
templates/[id]/edit.vue # แก้ไข template + field defs
|
||||
documents/create/[templateId].vue # dynamic form สร้างเอกสาร (FR-10)
|
||||
/composables
|
||||
useAuth.ts # OIDC PKCE + browser redirect ปกติ
|
||||
useApi.ts # fetch wrapper + Bearer token + refresh
|
||||
useTheme.ts # light/dark toggle + persist + system default
|
||||
/assets/css/tailwind.css
|
||||
package.json
|
||||
/keycloak # Phase 1
|
||||
realm-export.json
|
||||
docker-compose.yml # Phase 1
|
||||
README.md # Phase 8
|
||||
/specs /docs # มีอยู่แล้ว
|
||||
```
|
||||
|
||||
**ลำดับ dependency:** `0 → 1 → 2 → 3` เป็นเส้นหลัก จากนั้นแยก 2 สายทำขนานกันได้: `3 → 4 → 5` (add-in) และ `3 → 6` (web); **Phase 7 (tests)** ทำขนานกับ 4–6 ได้; **Phase 8** ปิดท้าย
|
||||
|
||||
---
|
||||
|
||||
## Functional Requirements เพิ่มเติม (นอก spec เดิม)
|
||||
|
||||
### FR-9: Template management (backend + web)
|
||||
- CRUD template: upload ไฟล์ .docx (มี content controls), ตั้ง name/description/active flag
|
||||
- ตอน upload/replaces ไฟล์: backend **parse content controls (SDT tags) ออกมาเป็น field list อัตโนมัติ** ด้วย OpenXML
|
||||
- Admin ตั้งค่าต่อ field: label, type (`text` / `number` / `date`), required, ลำดับ — เก็บเป็น `TemplateField`
|
||||
- ลบ template = **soft-delete (deactivate) เท่านั้น** เพราะ document เก่ายังอ้างอยู่; เอกสารที่สร้างแล้วใช้ template ที่ deactivate ได้ต่อเนื่อง
|
||||
- ไฟล์ template เก็บใน MinIO bucket `docx-templates` (private)
|
||||
|
||||
### FR-10: สร้างเอกสารจาก template ผ่านเว็บ
|
||||
```
|
||||
POST /api/documents/generate { templateId, values: { <fieldKey>: <value> } }
|
||||
```
|
||||
- Validate: required fields ครบ, type ถูก (number/date parse ได้)
|
||||
- Backend `DocxGenerationService`: โหลด template จาก MinIO → **แทนที่ข้อความใน SDT แต่ละ field (แก้ที่ run level เพื่อรักษา formatting)** → **stamp Custom Properties** (`SavedByAddIn` = ADDIN_SIGNATURE_GUID, `DocumentTrackingId` = GUID ใหม่) ลงในไฟล์ด้วย OpenXML
|
||||
- สร้าง `Document` (ผูก `TemplateId`) + `DocumentFieldValue` ทุก field + **upload เป็น version 1** + snapshot fields ของ v1
|
||||
- Response: `{ documentId, versionId, versionNumber, downloadUrl }` → ผู้ใช้ดาวน์โหลด .docx ไปเปิดใน Word → **FR-1 เดิมทำงานทันที** (add-in ตรวจพบ tracked document เห็น v1 โดยไม่ต้องกด Start Versioning)
|
||||
|
||||
### FR-11: Field sync จาก add-in → DB
|
||||
- ตอน Save & Version: add-in อ่าน `context.document.contentControls` ทั้งหมด → ส่ง map `{ tag: text }` แนบกับ `POST .../versions`
|
||||
- Backend เทียบกับ `TemplateField` ของ template ที่ document ผูกอยู่ → update **latest value ต่อ field** (`DocumentFieldValue`) + เก็บ **snapshot ต่อ version** (`DocumentVersion.FieldsSnapshotJson` — ใช้ diff ใน history ได้)
|
||||
- field ที่หาไม่เจอในเอกสาร (ผู้ใช้ลบ content control ไป) → **เก็บค่าเดิมไว้ + mark missing** ใน snapshot แต่**ไม่ fail ทั้ง flow**
|
||||
- การ sync ผูกกับ JWT ของผู้กด Save & Version (`UpdatedByUserId` จาก claims ตาม FR-8 เดิม)
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Scaffold โครงสร้างโปรเจกต์
|
||||
|
||||
**เป้า:** ได้โครงที่ build/run ได้ทั้งสามส่วน ก่อนใส่ logic จริง
|
||||
|
||||
- [x] ตรวจ prerequisites: Node LTS + Yeoman + `generator-office`, .NET 8 SDK, Docker
|
||||
- [x] `yo office` เลือก task pane, TypeScript, React → ย้าย output มาไว้ที่ `/addin`
|
||||
- [x] `dotnet new webapi -n WordVersioning.Api` → `/backend/WordVersioning.Api`
|
||||
- [x] เพิ่ม packages: `Npgsql.EntityFrameworkCore.PostgreSQL`, `Microsoft.EntityFrameworkCore.Design`, `AWSSDK.S3`, `Microsoft.AspNetCore.Authentication.JwtBearer`, `DocumentFormat.OpenXml`
|
||||
- [x] Scaffold `/web`: Nuxt.js + TailwindCSS — ตั้ง **`devServer.port = 5173`** ใน `nuxt.config.ts` (ห้ามชน add-in ที่ 3000), `darkMode: 'class'` ใน tailwind config, โครง `useTheme.ts` (toggle + persist localStorage + default ตาม `prefers-color-scheme`)
|
||||
- [x] สร้าง `.gitignore` (root) ครอบทั้ง node_modules/bin/obj/.nuxt/env แล้ว `git init`
|
||||
- [x] ปรับ `manifest.xml` ฐานจาก yo office ให้ dev server ชี้ `https://localhost:3000`
|
||||
|
||||
หมายเหตุจากการทำจริง (2026-08-18):
|
||||
- คำสั่ง scaffold: `npx yo office react addin word --ts` (generator-office ปัจจุบันใช้ projectType `react` แทน flag `--framework react`)
|
||||
- .NET packages ที่รองรับ net8.0 ต้องตรึง `--version 8.0.11` (Npgsql.EFCore / EFCore.Design / JwtBearer) เพราะ latest (10.x) รองรับเฉพาะ net10.0; AWSSDK.S3 4.0.102.1 + DocumentFormat.OpenXml 3.5.1 ใช้ latest ได้
|
||||
- Nuxt เวอร์ชันที่ได้ = 4.5.2 → โครง `app/` directory (`app/pages`, `app/composables`, `app/assets/css`) แทน root-level ตามแผนเดิม — ความหมายเดียวกัน
|
||||
- ทดสอบ DoD ผ่านทั้งสามส่วน (webpack compiled successfully / Build succeeded 0 errors / หน้าแรก render ที่ 5173 พร้อม dark: variants แบบ class strategy)
|
||||
|
||||
**Deliverable:** โครง 3 โปรเจกต์ตาม section 3
|
||||
|
||||
**DoD:** `npm run build` ใน `/addin` ผ่าน + `dotnet build` ใน `/backend` ผ่าน + `npm run dev` ใน `/web` เปิดหน้าแรกได้ที่ `http://localhost:5173` พร้อม toggle light/dark ทำงาน
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Local dev infrastructure (docker-compose + Keycloak + MinIO)
|
||||
|
||||
**เป้า:** รัน infra ทั้งชุดด้วยคำสั่งเดียว — ตรง spec ส่วน 11 (backend ข้อ 6, 8)
|
||||
|
||||
- [ ] `docker-compose.yml` กำหนด services:
|
||||
- `db` — `postgres:16` (volume สำหรับข้อมูล)
|
||||
- `minio` — `minio/minio` (console เปิดด้วย) + init service สร้าง bucket **`docx-versions`** และ **`docx-templates`** แบบ **private** ทั้งคู่
|
||||
- `keycloak` — `quay.io/keycloak/keycloak` start-dev + import realm อัตโนมัติ
|
||||
- `api` — build จาก `/backend` (รอ Phase 2 เสร็จค่อย enable ก็ได้)
|
||||
- [ ] `/keycloak/realm-export.json`:
|
||||
- realm `word-versioning`
|
||||
- client `word-addin-officejs` — public, Standard Flow, **PKCE S256 บังคับ**, ไม่มี secret, redirect URI `https://localhost:3000/auth-callback.html`
|
||||
- client `word-versioning-web` — public, Standard Flow, PKCE S256, redirect URI `http://localhost:5173/*` (web ทุก route เพราะใช้ redirect ปกติ)
|
||||
- client `word-addin-vsto` — public + PKCE (pre-register ตาม spec)
|
||||
- สร้าง test user แรกไว้ใน realm export เลยก็ได้
|
||||
- [ ] กำหนด env defaults (เช่น ใน `.env.example`): MinIO endpoint/keys, Keycloak admin password, DB password
|
||||
|
||||
**DoD:** `docker compose up` แล้ว (1) เข้า Keycloak admin console เห็น realm `word-versioning` + clients ครบ 3 ตัว (2) เข้า MinIO console เห็น bucket `docx-versions` + `docx-templates` ที่เป็น private
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Backend core — FR-5, FR-6, FR-7, FR-8 (ฝั่ง server)
|
||||
|
||||
**เป้า:** API ครบทุก endpoint ของ spec เดิม พร้อม auth + storage จริง — ตรง spec ส่วน 11 (backend ข้อ 1–5)
|
||||
|
||||
### 2.1 Data layer
|
||||
- [ ] `Models/Document.cs`, `Models/DocumentVersion.cs` ตาม spec ส่วน 7 + เพิ่ม `TemplateId` (Document) และ `FieldsSnapshotJson` (DocumentVersion) สำหรับ FR-10/11
|
||||
- [ ] `Data/AppDbContext.cs` (Npgsql) + unique index `(DocumentId, VersionNumber)`
|
||||
- [ ] EF migration แรก
|
||||
|
||||
### 2.2 Services
|
||||
- [ ] `IObjectStorageService` + `ObjectStorageService` — `AmazonS3Client` ตั้ง `ServiceURL` = MinIO endpoint, **`ForcePathStyle = true`** (จำเป็นสำหรับ MinIO), methods: `PutAsync(bucket, key, stream, contentType)`, `GetStreamAsync(bucket, key)`, config (`Endpoint`, `AccessKey`, `SecretKey`, `UseSSL`) จาก `appsettings.json`/env; `BucketName` แยกเป็น `VersionsBucket` + `TemplatesBucket`
|
||||
- [ ] `HashService` — SHA256 คำนวณตอน stream ผ่าน (ไม่ต้อง load ทั้งไฟล์ใน memory ถ้าไหว)
|
||||
|
||||
### 2.3 Auth (FR-8 ฝั่ง backend)
|
||||
- [ ] `AddAuthentication(JwtBearer)` config `Authority` = Keycloak realm URL, `Audience` ครอบทั้ง client ของ add-in และ web (หรือใช้ realm-level validation ตามที่ config ได้), `RequireHttpsMetadata = false` (dev เท่านั้น — คอมเมนต์ไว้ชัดว่า prod ต้อง true)
|
||||
- [ ] `[Authorize]` บน `DocumentsController` และ `TemplatesController` ทั้ง controller
|
||||
- [ ] Helper ดึง userId/username จาก claims `sub` / `preferred_username` — **ห้ามรับค่านี้จาก client**
|
||||
|
||||
### 2.4 Endpoints (`DocumentsController`)
|
||||
- [ ] `POST /api/documents/{documentId}/versions` (FR-5) — multipart: `file` + `clientVersionId` + **`fields` (JSON map tag→text, เตรียมทางไว้ให้ FR-11)**; auto-create `Document` ถ้ายังไม่มี; versionNumber = max+1 ต่อ document (มี unique index รองรับ concurrency); object key `{documentId}/v{versionNumber:D4}_{clientVersionId}.docx`; คำนวณ + เก็บ SHA256; response `{ versionId, versionNumber }`
|
||||
- [ ] `GET /api/documents/{documentId}/versions` (FR-6) — เรียงใหม่ → เก่า (คืน snapshot fields ต่อ version ด้วย)
|
||||
- [ ] `GET /api/documents/{documentId}/versions/{versionId}/download` (FR-7) — **proxy stream** จาก MinIO (ไม่ expose MinIO ตรงๆ) พร้อม content-type ที่ถูกต้อง
|
||||
- [ ] Validation: reject content-type ที่ไม่ใช่ docx, จำกัด request size **100MB** (`RequestSizeLimit` / Kestrel config)
|
||||
|
||||
### 2.5 Cross-cutting
|
||||
- [ ] CORS อนุญาต **`https://localhost:3000` (add-in) และ `http://localhost:5173` (web)**
|
||||
- [ ] Logging ทุก upload/download/generate: documentId, versionId, userId
|
||||
- [ ] `appsettings.json` + env overrides ครบทุก config (DB, MinIO, Keycloak)
|
||||
|
||||
**DoD:** `dotnet run` + Swagger: (1) เรียก endpoint ไหนก็ได้โดยไม่แนบ token → **401** (2) ขอ token จาก Keycloak ด้วย test user แล้ว upload ไฟล์ docx จริง → มี record ใน Postgres + object ใน MinIO + download กลับมาเปิดได้ (AC-3 ผ่าน)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Backend template & generation — FR-9, FR-10 (ใหม่)
|
||||
|
||||
**เป้า:** ระบบ template ครบวงจร — upload พร้อม auto-detect fields + generate เอกสาร v1 พร้อม metadata
|
||||
|
||||
### 3.1 Entities + migrations
|
||||
- [ ] `Template` — Id (Guid), Name, Description, ObjectKey (ใน bucket `docx-templates`), IsActive (soft-delete), CreatedAt/UpdatedAt
|
||||
- [ ] `TemplateField` — Id, TemplateId, Key (= SDT tag), Label, Type (`text|number|date`), IsRequired, SortOrder; unique `(TemplateId, Key)`
|
||||
- [ ] `DocumentFieldValue` — DocumentId, FieldKey, Value (string), UpdatedAt, UpdatedByUserId, SourceVersionId; unique `(DocumentId, FieldKey)` = latest value ต่อ field
|
||||
- [ ] `Document` เพิ่ม `TemplateId?` + migration
|
||||
|
||||
### 3.2 TemplatesController (FR-9)
|
||||
- [ ] `GET /api/templates` — list (filter active/all)
|
||||
- [ ] `POST /api/templates` — multipart: ไฟล์ .docx + name/description → upload ลง MinIO + **parse SDT tags คืนเป็น field list ที่ detect ได้** (response ให้ admin ไปตั้ง label/type/required ต่อ)
|
||||
- [ ] `PUT /api/templates/{id}` — แก้ metadata + บันทึก field definitions
|
||||
- [ ] `PUT /api/templates/{id}/file` — แทนที่ไฟล์ (re-parse fields, เตือนถ้า tag หายไป)
|
||||
- [ ] `DELETE /api/templates/{id}` = deactivate (soft-delete)
|
||||
|
||||
### 3.3 DocxGenerationService (FR-10)
|
||||
- [ ] โหลด template จาก MinIO → เปิดด้วย `DocumentFormat.OpenXml`
|
||||
- [ ] แทนที่ข้อความใน SDT แต่ละ field **ที่ run level** (รักษา run properties/formatting ของ template — ไม่ทับทั้ง paragraph)
|
||||
- [ ] Stamp Custom Properties ผ่าน `CustomFilePropertiesPart`: `SavedByAddIn` = `ADDIN_SIGNATURE_GUID` (ค่าคงที่เดียวกับที่ add-in ใช้ตรวจ), `DocumentTrackingId` = Guid ใหม่
|
||||
- [ ] `POST /api/documents/generate { templateId, values }`:
|
||||
1. Validate required + type
|
||||
2. Generate ไฟล์จาก template
|
||||
3. คำนวณ SHA256 + upload เป็น **version 1** (`ObjectStorageService`)
|
||||
4. สร้าง `Document` (TemplateId, FileName จาก template name + timestamp) + `DocumentFieldValue` ทุก field + `DocumentVersion.FieldsSnapshotJson` ของ v1
|
||||
5. Response `{ documentId, versionId, versionNumber, downloadUrl }`
|
||||
|
||||
### 3.4 Field sync บน upload endpoint (FR-11 ฝั่ง backend)
|
||||
- [ ] ขยาย `POST /api/documents/{documentId}/versions`: รับ `fields` (map tag→text) → เทียบกับ `TemplateField` ของ template ที่ document ผูกอยู่ → update `DocumentFieldValue` (latest value) + เขียน `DocumentVersion.FieldsSnapshotJson` ของ version นั้น
|
||||
- [ ] field ที่ส่งมาขาด (ผู้ใช้ลบ content control) → เก็บค่าเดิม + mark missing ใน snapshot **ไม่ fail request**; field ที่ส่งมาเกินนิยาม → ignore
|
||||
|
||||
**DoD:** upload template จริง (เช่น สัญญาจ้างงานที่มี content controls) → detect fields ครบ → เรียก generate ผ่าน Swagger → ได้ docx ที่ (1) เปิดใน Word แล้วค่าถูกทุก field formatting ไม่พัง (2) Word บอก custom properties มี `SavedByAddIn`/`DocumentTrackingId` (ตรวจใน Advanced Properties หรือเปิดไฟล์ด้วย unzip) (3) DB มี Document + v1 + field values (AC-11, 12 ส่วน backend ผ่าน) และ (4) เรียก upload version ถัดไปพร้อม `fields` ที่เปลี่ยนค่า → latest values + snapshot อัปเดตถูก / ขาด field → ค่าเดิมคงอยู่ไม่ error (AC-13, 14 ส่วน backend ผ่าน)
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Add-in services layer — FR-8 (ฝั่ง client), FR-11 (extraction), โครง FR-1/2
|
||||
|
||||
**เป้า:** logic ทั้งหมดที่ UI จะเรียกใช้ + unit test — ตรง spec ส่วน 11 (frontend ข้อ 2, 3, 5, 6)
|
||||
|
||||
### 4.1 Auth (`authService.ts`, `auth-callback.html`)
|
||||
- [ ] สร้าง `code_verifier` (random 43–128 chars) + `code_challenge` = base64url(SHA256(verifier))
|
||||
- [ ] สร้าง authorization URL: Keycloak endpoint + client_id + redirect_uri (`auth-callback.html`) + response_type=code + scope=openid + state + code_challenge (S256)
|
||||
- [ ] เปิด `Office.context.ui.displayDialogAsync(url, {height: 60, width: 30})` + `Dialog.addEventHandler(Office.EventType.DialogMessageReceived)` รับ message กลับ
|
||||
- [ ] `auth-callback.html` — parse `code`/`state` จาก URL, `Office.context.ui.messageParent(JSON.stringify({code, state}))` แล้ว `window.close()`
|
||||
- [ ] แลก token ที่ Keycloak token endpoint ด้วย verifier (public client ไม่พึ่ง backend)
|
||||
- [ ] เก็บ `access_token`/`refresh_token` **ใน JS variable ของ task pane session**; expose `isTokenValid()`, `getAccessToken()`
|
||||
- [ ] Silent refresh ก่อนหมดอายุ + ถ้า refresh token ตาย/ถูก revoke → คืนสถานะ "ต้อง login ใหม่" แบบ graceful (ไม่ error ค้าง)
|
||||
- [ ] ป้องกัน CSRF ด้วยการเทียบ `state`
|
||||
|
||||
### 4.2 Metadata (`metadataService.ts`)
|
||||
- [ ] อ่าน Custom Properties: `SavedByAddIn`, `DocumentTrackingId`, `LastVersionId`, `LastVersionTimestamp`
|
||||
- [ ] เขียน/stamp properties (ใช้ property `add()` ของใหม่ทับ — ไม่มีปัญหาแบบ XML part)
|
||||
- [ ] XML part ฝั่งอ่าน: หา part ที่ namespace `urn:myaddin:versioning` → parse เป็น list
|
||||
- [ ] XML part ฝั่งเขียน: **delete ของเดิมทั้งหมดก่อนค่อย `add()` ใหม่** + **catch error แบบไม่ throw** (known bug บน Mac — เขียนไม่ได้ก็ปล่อยผ่านเพราะ backend คือ source of truth)
|
||||
- [ ] Constants: `ADDIN_SIGNATURE_GUID`
|
||||
- [ ] **(FR-11) `readContentControlFields()`** — `Word.run` → `context.document.contentControls` → `load("tag, text")` → คืน `{ [tag]: text }`; เอกสารที่ไม่มี content controls → คืน `{}` ไม่ error
|
||||
|
||||
### 4.3 File + API (`fileService.ts`, `apiClient.ts`, `versioningService.ts`)
|
||||
- [ ] `fileService` — `getFileAsync(Office.FileType.Compressed)` → อ่าน slice ทีละชิ้น → ประกอบเป็น Blob → **เรียก `closeAsync` เสมอ** (จำกัดจำนวน file object ที่เปิดพร้อมกันได้)
|
||||
- [ ] `apiClient` — wrapper fetch ที่แนบ `Authorization: Bearer <token>` ทุก request; โดน 401 → สั่ง refresh token แล้ว retry ครั้งเดียว; ยัง 401 → ส่งสัญญาณให้ UI กลับไปหน้า sign-in
|
||||
- [ ] `versioningService` — `uploadVersion(documentId, blob, clientVersionId, fields)`, `getVersions(documentId)`, `getDownloadUrl(...)`/download — **fields = map จาก `readContentControlFields()`**
|
||||
- [ ] `models/versionRecord.ts` — type ตรงกับ response ของ backend (รวม snapshot fields ต่อ version)
|
||||
|
||||
### 4.4 Manifest
|
||||
- [ ] `manifest.xml` ประกาศ requirement sets ให้ครอบ `WordApi 1.5` (customProperties, customXmlParts, `document.save()`) + `DialogAPI`
|
||||
|
||||
**DoD:** `npm run build` ผ่าน + unit tests ผ่าน (Jest: mock `Word.run`, `Office.context.ui`, `fetch`, Keycloak token response) — ครอบ PKCE flow, delete-then-add XML part, apiClient retry-on-401, fileService ประกอบ slice, **readContentControlFields แปลง contentControls → map ถูกต้อง (รวมกรณี control ซ้ำ tag → เอาตัวแรก + tag ว่าง → ข้าม)**
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Task pane UI — FR-1, FR-2, FR-3, FR-4, FR-11 (ส่วนส่งค่า)
|
||||
|
||||
**เป้า:** UI ครบทุก flow — ตรง spec ส่วน 11 (frontend ข้อ 4)
|
||||
|
||||
- [ ] `taskpane.tsx` state machine หลัก: `checking → signed-out → untracked → tracking`
|
||||
- [ ] **Sign-in gate (FR-1):** `Office.onReady()` → เช็ค token ก่อน — ไม่มี/หมดอายุ → หน้า "Sign in" (ปุ่มเรียก authService); มี → เช็ค property `SavedByAddIn` เทียบ `ADDIN_SIGNATURE_GUID`
|
||||
- ตรง → อ่าน `DocumentTrackingId` → `getVersions()` → เข้าสถานะ `tracking` (**ก็คือ flow ของไฟล์ที่สร้างจากเว็บ — เปิดปุ๊บเห็น v1 ทันที ไม่ต้อง Start Versioning**)
|
||||
- ไม่ตรง → ปุ่ม **"Start Versioning This Document"**
|
||||
- [ ] **Start Versioning:** stamp metadata รอบแรก (`SavedByAddIn`, `DocumentTrackingId` ใหม่) → upload version แรก → สลับเป็น `tracking`
|
||||
- [ ] `ConfirmVersionDialog.tsx` — ยืนยัน "สร้าง version ใหม่ของเอกสารนี้หรือไม่?"
|
||||
- [ ] **Save & Version flow (FR-2) ตามลำดับ spec แบบเคร่งครัด:**
|
||||
1. เปิด confirm dialog
|
||||
2. Stamp properties: `SavedByAddIn`, `DocumentTrackingId`, `LastVersionId`, `LastVersionTimestamp`
|
||||
3. อัปเดต XML part (delete-then-add, fail ได้แบบ silent)
|
||||
4. `context.document.save(Word.SaveBehavior.save)`
|
||||
5. `fileService` ดึงไฟล์ปัจจุบัน + **`readContentControlFields()` ดึงค่า fields ปัจจุบัน (FR-11)**
|
||||
6. `POST /api/documents/{documentId}/versions` — แนบ `fields` ด้วย
|
||||
7. แสดงผล + refresh history
|
||||
- ทุก step มี loading state + error message + **ห้ามปล่อย unhandled rejection**
|
||||
- [ ] `VersionHistoryPanel.tsx` (FR-4) — list version number/timestamp/ผู้สร้าง + ปุ่ม "Download this version" (`GET .../download`)
|
||||
- [ ] **Soft reminder (FR-3):** polling/debounce ทุก ~30 วินาทีเช็ค `context.document.saved` — `false` ต่อเนื่องเกิน threshold → banner "มีการแก้ไขที่ยังไม่ได้ version" (ไม่ block ใคร)
|
||||
- [ ] จัดการ token หมดอายุกลาง session: 401 → silent refresh → ยังไม่ได้ → กลับหน้า sign-in อย่างเนียน (AC-9)
|
||||
- [ ] Fluent UI components ให้หน้าตาเข้ากับ Office
|
||||
|
||||
**DoD:** sideload บน Word Desktop (Mac ที่มีอยู่ + Windows ถ้ามี) และ Word Online → ครบ flow: login ผ่าน dialog, start versioning, save & version, เห็น history, download ไฟล์ย้อนหลังเปิดได้ปกติ (AC-1, 2, 4, 5, 6, 7, 8 ผ่าน) + แก้ค่าใน content control แล้ว Save & Version → DB field values เปลี่ยนตาม (AC-13) + เปิดไฟล์ที่ generate จากเว็บ (Phase 6 เสร็จ) → เห็นเป็น tracked + v1 ทันที (AC-12)
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Web Portal — FR-9 (UI), FR-10 (UI) *(ใหม่)*
|
||||
|
||||
**เป้า:** เว็บ Nuxt + Tailwind สำหรับจัดการ template และสร้างเอกสาร ทุกหน้ารองรับ light/dark theme
|
||||
|
||||
### 6.1 พื้นฐาน
|
||||
- [ ] `useAuth.ts` — OIDC PKCE + **browser redirect ปกติ** (top-level page ใช้ได้เลย ไม่ต้องมี Dialog API), รับ code ที่ route callback, แลก token, silent refresh, token ใน memory
|
||||
- [ ] `useApi.ts` — fetch wrapper + Bearer + auto refresh ตอน 401
|
||||
- [ ] `useTheme.ts` — toggle light/dark (สลับ class บน `<html>`), persist ใน localStorage, default ตาม `prefers-color-scheme`
|
||||
- [ ] Layout หลัก: header + theme toggle + สถานะ login (ทุกหน้าใช้ร่วม)
|
||||
|
||||
### 6.2 จัดการ template (FR-9 UI)
|
||||
- [ ] `templates/index.vue` — ตารางรายการ (name, description, จำนวน fields, active) + ปุ่ม deactivate/activate
|
||||
- [ ] `templates/new.vue` + `templates/[id]/edit.vue` — upload/แทนที่ไฟล์ .docx → โชว์ **field list ที่ระบบ detect อัตโนมัติ** → ฟอร์มตั้ง label/type (text/number/date)/required/ลำดับ → บันทึก
|
||||
|
||||
### 6.3 สร้างเอกสาร (FR-10 UI)
|
||||
- [ ] `index.vue` — card รายการ template ที่ active → เลือกแล้วไปหน้าสร้าง
|
||||
- [ ] `documents/create/[templateId].vue` — **dynamic form สร้างจาก field defs** (input type ตาม field type: text/number/date picker), validate required/type ฝั่ง client + แสดง error จาก backend
|
||||
- [ ] กดสร้าง → แสดงผล: documentId, version 1 + **ปุ่มดาวน์โหลด .docx** + คำแนะนำสั้นๆ ("เปิดไฟล์นี้ใน Word แล้วใช้ add-in ต่อ")
|
||||
|
||||
**DoD:** login ผ่าน Keycloak บนเว็บได้ → สร้าง/แก้ template จากไฟล์จริง detect fields ถูก → กรอก form สร้างเอกสาร ได้ docx ครบค่า → เปิดใน Word แล้ว add-in รู้จักทันทีเห็น v1 (AC-11, 12 เต็มรูปแบบ) + theme toggle ทำงานทุกหน้าและจำค่าได้หลัง reload
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Tests
|
||||
|
||||
**เป้า:** ความมั่นใจก่อนเรียกว่าเสร็จ — ตรง spec ส่วน 11 (frontend ข้อ 7, backend ข้อ 7) + ครอบ features ใหม่
|
||||
|
||||
- [ ] **Frontend unit (Jest, ทำพร้อม Phase 4–5):**
|
||||
- authService: PKCE pair ถูก format, state validation, token exchange, refresh flow
|
||||
- metadataService: stamp ครบ 4 properties, XML delete-then-add, XML fail แล้วไม่ throw, **readContentControlFields แปลง map ถูก (tag ซ้ำ/ว่าง/ไม่มี control)**
|
||||
- apiClient: แนบ Bearer, retry หลัง refresh, ยอมแพ้หลัง retry ครั้งที่ 2
|
||||
- fileService: ประกอบ slice หลากขนาดเป็น Blob, เรียก closeAsync
|
||||
- [ ] **Backend integration (xUnit + Testcontainers):** รัน PostgreSQL + MinIO + Keycloak จริง
|
||||
- ขอ token ผ่าน Keycloak admin API สร้าง test user
|
||||
- upload → มี record + object; upload ซ้ำ document → versionNumber เพิ่ม; download → bytes ตรง; ไม่มี token → 401; ไฟล์ไม่ใช่ docx → 400; `CreatedByUserId` ตรงกับ `sub` ใน JWT
|
||||
- **Template:** upload docx มี SDT → detect tags ครบ; บันทึก field defs; deactivate แล้ว generate ไม่ได้แต่ document เก่ายังโหลดได้
|
||||
- **Generate:** แทนที่ค่าครบทุก field + เปิดผลลัพธ์ด้วย OpenXML ตรวจ SDT text ตรง + **custom properties มี `SavedByAddIn`/`DocumentTrackingId`** + ได้ v1 + field values ใน DB; required ขาด → 400; type ผิด → 400
|
||||
- **Field sync:** upload version พร้อม `fields` → latest values อัปเดต + snapshot เก็บต่อ version; **ส่ง map ขาด field → ค่าเดิมคงอยู่ + mark missing + request สำเร็จ (AC-14)**
|
||||
- [ ] **Web (Vitest):** validation ของ dynamic form (required/type) + useTheme logic
|
||||
|
||||
**DoD:** `npm test` (addin), `npm test` (web) และ `dotnet test` เขียวทั้งคู่
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: README + ตรวจ acceptance criteria
|
||||
|
||||
- [ ] `README.md`: วิธีรัน local dev (docker compose, dotnet run, npm start, **npm run dev ของ /web**), วิธี sideload add-in บน Desktop (Win/Mac) + Word Online, ค่า connection config, เข้า MinIO console, เข้า Keycloak admin console, สร้าง test user แรก, **วิธีสร้าง template แรก (ตัวอย่างไฟล์ docx ที่มี content controls) และสร้างเอกสารจากเว็บ** — ตรง spec ส่วน 11 (backend ข้อ 9)
|
||||
- [ ] ไล่ checklist acceptance criteria ทีละข้อ พร้อมจดผล/ปัญหาที่เจอ
|
||||
|
||||
---
|
||||
|
||||
## 4. ตาราง mapping: Phase ↔ FR ↔ Acceptance Criteria
|
||||
|
||||
| Phase | FR ที่ implement | Acceptance criteria ที่ตรวจได้ |
|
||||
|---|---|---|
|
||||
| 0 | — | — (โครง) |
|
||||
| 1 | FR-8 (Keycloak setup รวม web client) | — (infra) |
|
||||
| 2 | FR-5, FR-6, FR-7, FR-8 (backend) | AC-3 |
|
||||
| 3 | FR-9, FR-10 (backend) | AC-11, AC-12 (ส่วน API) |
|
||||
| 4 | FR-8 (client auth), FR-11 (extraction), โครง FR-1/2 | AC-2 (ส่วน dialog) |
|
||||
| 5 | FR-1, FR-2, FR-3, FR-4, FR-11 (ส่วนส่งค่า) | AC-1, 2, 4, 5, 6, 7, 8, 9, 10, 13 |
|
||||
| 6 | FR-9 (UI), FR-10 (UI) | AC-11, AC-12 (เต็มรูปแบบ) |
|
||||
| 7 | — | คุ้มกัน regression ทุก AC (รวม AC-13, 14) |
|
||||
| 8 | — | ทบทวน AC-1 ถึง AC-14 ทั้งหมด |
|
||||
|
||||
**AC เพิ่มเติม (นอก spec เดิม):**
|
||||
- **AC-11:** upload template .docx ที่มี content controls → ระบบ detect fields อัตโนมัติ + บันทึก field definitions ได้
|
||||
- **AC-12:** กรอก form บนเว็บ → ดาวน์โหลด .docx ที่ค่าครบถูกต้อง → เปิดใน Word แล้ว add-in รายงานเป็น tracked document เห็น version 1 ทันที **โดยไม่ต้องกด Start Versioning**
|
||||
- **AC-13:** แก้ค่าใน content control ที่ Word แล้วกด Save & Version → field value ใน DB เปลี่ยนตาม + snapshot ต่อ version ถูกเก็บ
|
||||
- **AC-14:** ลบ content control ออกจาก Word แล้วกด Save & Version → flow ยังสำเร็จ ค่าเดิมของ field นั้นคงอยู่ (mark missing) ไม่ error
|
||||
|
||||
---
|
||||
|
||||
## 5. ความเสี่ยงและ mitigation
|
||||
|
||||
| ความเสี่ยง | ผลกระทบ | Mitigation |
|
||||
|---|---|---|
|
||||
| Custom XML Part fail แบบ silent บน Word for Mac | history ในไฟล์หาย | ออกแบบไว้แล้ว: XML part เป็น secondary, backend คือ source of truth; metadataService catch ไม่ throw (AC-10) |
|
||||
| ผู้ใช้เผลอ save ด้วย Ctrl+S แทนปุ่ม | ไม่มี version ใหม่ | ห้ามดักไม่ได้ตามข้อจำกัดแพลตฟอร์ม → FR-3 banner เตือนเมื่อ `document.saved === false` นานเกิน threshold |
|
||||
| Access token หมดอายุกลางใช้งาน | request ล้ม 401 | apiClient/useApi auto-refresh + retry; refresh token ตาย → กลับหน้า sign-in เนียนๆ (AC-9) |
|
||||
| ไฟล์ .docx ขนาดใหญ่ | upload ช้า/ล้ม | limit 100MB, stream upload ลง MinIO, SHA256 คำนวณระหว่าง stream |
|
||||
| `getFileAsync` กิน memory/เปิดค้าง | resource leak ใน Word | เรียก `closeAsync` เสมอใน finally |
|
||||
| Keycloak login โดน iframe blocker | login ไม่ได้เลย | ใช้ Dialog API ตั้งแต่ดีไซน์ (FR-8); `/web` เป็น top-level ไม่กระทบ |
|
||||
| versionNumber race (สอง client upload พร้อมกัน) | version ซ้ำ → ผิด | unique index `(DocumentId, VersionNumber)` + retry ตอนชน constraint |
|
||||
| **OpenXML replace ทำ formatting พัง** | เอกสารที่ generate หน้าตาผิดจาก template | แก้ที่ **run level ใน SDT** (ไม่ทับ paragraph) + integration test ด้วย template จริงหลายรูปแบบ |
|
||||
| **ผู้ใช้ลบ/แก้ content control ใน Word** | field sync ไม่ครบ/ไม่ตรง | FR-11 policy: field หาย → เก็บค่าเดิม + mark missing, ไม่ fail (AC-14); tag ซ้ำ → เอาตัวแรก |
|
||||
| **Template ถูกใช้กับ document เก่าจำนวนมาก** | ลบ template แล้วอ้างอิงพัง | soft-delete (IsActive) เท่านั้น; Document เก่ายังถือ TemplateId เดิมได้ |
|
||||
| **Keycloak 3 clients + CORS 2 origins** | auth พังเฉพาะบาง client | realm-export ครบทุก client ตั้งแต่ Phase 1; backend CORS ใส่ทั้ง add-in และ web |
|
||||
| **Port ชนกัน (Nuxt default 3000 = add-in)** | dev รันชนกันสับสน | บังคับ Nuxt ที่ **5173** ใน `nuxt.config.ts` ตั้งแต่ Phase 0 |
|
||||
|
||||
---
|
||||
|
||||
## 6. คำสั่งที่ใช้ประจำ (เมื่อ implement จริง)
|
||||
|
||||
```bash
|
||||
# infra
|
||||
docker compose up -d
|
||||
|
||||
# backend
|
||||
cd backend/WordVersioning.Api
|
||||
dotnet run # รัน + swagger ที่ http://localhost:5xxx
|
||||
dotnet ef migrations add <Name>
|
||||
dotnet test
|
||||
|
||||
# add-in (https://localhost:3000)
|
||||
cd addin
|
||||
npm install
|
||||
npm run build # webpack build
|
||||
npm start # dev server — sideload ผ่าน manifest นี้
|
||||
npm test # jest
|
||||
npx office-addin-dev-certs install # ครั้งแรกครั้งเดียว เพื่อให้ https localhost ใช้ได้
|
||||
|
||||
# web portal (http://localhost:5173 — ห้ามใช้ 3000 เพราะชน add-in)
|
||||
cd web
|
||||
npm install
|
||||
npm run dev
|
||||
npm test # vitest
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue