เพิ่ม editor ต่อ template field — ผู้ดูแลเลือกรูปแบบช่องกรอกในฟอร์มสร้างเอกสารได้: text → textbox/textarea, number/date → control ตาม type (presentation metadata ล้วน — validation ยังยึด Type, ค่า field เป็น string ทั้ง flow, ไม่แตะ generate/sync/add-in) - Backend: TemplateField.Editor (nullable, null = default ของ type) + migration AddTemplateFieldEditor + validate ตาม type ใน PUT templates (ค่าไม่รู้จัก/ไม่เข้ากับ type → 400, normalize-on-write, API คืน resolved เสมอ) - Web: util fieldEditor.ts + editor select หน้าแก้ template (desktop+mobile, ซ่อนเมื่อ type มีทางเดียว) + ฟอร์มสร้างเอกสาร render ตาม editor + แก้ prefill re-generate ของ field date จาก ISO timestamp เต็ม (input[type=date] รับเฉพาะ yyyy-MM-dd เดิมแสดงว่าง) - Docs: spec v6 (FR-14/AC-17) + plan Phase 10 + sub-plan field-editor.md + acceptance-review AC-17 + README/CLAUDE.md - Tests: backend 60/60 (FieldEditorTests ใหม่ 8 กรณี) · web 46/46 + build ผ่าน Co-Authored-By: Claude <noreply@anthropic.com>
49 KiB
Spec: Word Versioning Add-in (Office.js Cross-Platform + .NET Backend)
ฉบับแก้ไข v6 (2026-08-22): เพิ่ม FR-14 — editor ต่อ field (รูปแบบช่องกรอกในฟอร์มสร้างเอกสาร):
textเลือกtextbox/textareaได้,number/dateใช้ control ตาม type — presentation metadata เท่านั้น ค่า field ยังเป็น string ทั้ง flow ฉบับแก้ไข v5 (2026-08-20): เพิ่ม FR-13 — คำอธิบายต่อ version ("commit message") พิมพ์ได้ตอนสร้าง version ทุก flow (Save & Version, Start Versioning, สร้างจากเว็บ) + แสดงใน version history ทุกหน้า ฉบับแก้ไข v4 (2026-08-20): เพิ่ม FR-12 — รายการเอกสารต่อ template + document detail + re-generate (Web Portal) ฉบับแก้ไข v3 (2026-08-19): เพิ่มข้อจำกัดแพลตฟอร์มที่พบจากการทดสอบจริงบน Word for Mac (dialog บังคับ HTTPS, mixed content, รูปแบบ slice.data หลายแบบ, webview ทิ้ง blob download) + กลไกดาวน์โหลด version ผ่าน presigned link ฉบับแก้ไข v2 (2026-08-18): เพิ่ม Web Portal (Nuxt.js) + Template System (FR-9 ถึง FR-11) + field sync — implementation plan อยู่ที่docs/plans/officejs-implementation-plan.md
1. ภาพรวม
สร้างระบบ 3 ส่วนที่ทำงานร่วมกัน:
- Office Add-in (Office.js, TypeScript) — รันได้บน Word Desktop (Windows/Mac) และ Word Online โดยไม่ต้อง build แยก
- .NET 8 Web API Backend — เก็บ version metadata ใน DB และไฟล์ .docx จริงใน MinIO (S3-compatible object storage) + ระบบ template และ generate เอกสาร
- Web Portal (Nuxt.js + TailwindCSS) — เว็บแยกสำหรับจัดการ template และกรอก form สร้างเอกสารครั้งแรกจาก template โดยเป็น version 1 อัตโนมัติ
ความสามารถหลัก (เทียบเท่า VSTO แต่ปรับ flow ให้เข้ากับข้อจำกัดของแพลตฟอร์ม):
- ตรวจจับได้ว่าไฟล์ .docx ที่เปิดอยู่เคยถูกบันทึกโดย add-in นี้หรือไม่
- มีปุ่ม "Save & Version" ใน Task Pane ให้ผู้ใช้กดเพื่อยืนยันสร้าง version (แทนการดัก native save เพราะ Office.js ไม่มี before-save event)
- เมื่อกด Save & Version จะ stamp tag ลง metadata ของไฟล์ + อัปโหลด snapshot ไป backend + extract ค่าจาก content controls ไป update fields ใน DB ให้ sync (FR-11)
- Backend เก็บ version history แบบ query ได้, ดาวน์โหลด version ย้อนหลังได้
- จัดการ template เอกสาร (.docx ที่มี content controls) ผ่านหน้าเว็บ — upload แล้วระบบ detect fields อัตโนมัติ (FR-9)
- สร้างเอกสารครั้งแรกจาก template: กรอก form บนเว็บ → backend generate .docx + stamp metadata + สร้าง version 1 อัตโนมัติ → เปิดใน Word แล้ว add-in รู้จักทันที (FR-10)
ขอบเขต: รองรับ Word Desktop (Win/Mac) + Word on the Web
2. ⚠️ ข้อจำกัดของแพลตฟอร์มที่ต้องออกแบบรองรับ
- ไม่มี native "before save" / "before close" event ที่ cancel ได้ ใน Word JS API (ต่างจาก VSTO) → ห้ามออกแบบ flow ที่พึ่งพาการดัก Ctrl+S หรือ File>Save ของผู้ใช้ ต้องใช้ explicit action (ปุ่มใน task pane / ribbon command) แทน
- Custom XML Parts แก้ไขตรงๆ ไม่ได้ — ต้อง
delete()ของเดิมแล้วadd()ใหม่ทุกครั้งที่จะอัปเดต - Custom XML Parts มี known bug บางแพลตฟอร์ม (เช่น ไม่ persist หลัง save/reopen บน Excel/Word for Mac บาง build) → ใช้เป็น secondary storage เท่านั้น อย่าพึ่งเป็นแหล่งข้อมูลเดียว
- Custom Document Properties เสถียรกว่าและ cross-platform ดีกว่า → ใช้เป็น primary flag สำหรับตรวจจับว่าไฟล์ผ่าน add-in นี้หรือไม่
- Manifest ต้อง support ทั้ง requirement set ที่
document.customXmlParts,document.properties.customProperties, และdocument.save()(WordApi 1.5+) ใช้งานได้
ข้อจำกัดเพิ่มเติมจากการทดสอบจริงบน Word for Mac (v3):
- Office Dialog API ปฏิเสธ URL ที่เป็น http:// โดยสมบูรณ์ (error "The HTTP protocol is not supported. Use HTTPS instead") → หน้า login ของ IdP ต้องเสิร์ฟ HTTPS เสมอแม้ตอน dev
- Webview ของ host (โดยเฉพาะ WKWebView ใน Word for Mac) บล็อก fetch จากหน้า https ไป http แบบเงียบๆ (mixed content — error "Load failed", request ไม่เคยถึง server, CORS ไม่เกี่ยว) → ทุก service ที่ task pane (หน้า https) จะเรียกต้องเป็น HTTPS ทั้งหมด
- ข้อมูล slice จาก
getSliceAsyncมีได้หลายรูปแบบตาม host/แพลตฟอร์ม — ArrayBuffer, typed-array view, base64 string, หรือnumber[]ธรรมดา → ต้อง normalize ครบทุกกรณีก่อนประกอบเป็นไฟล์ - Webview ของ host ทิ้ง blob download (
a[download]+ object URL) แบบเงียบๆ — ไฟล์ไม่ถูกเขียนที่ไหนเลย → การดาวน์โหลดไฟล์จาก add-in ต้องขอ presigned URL อายุสั้นจาก backend แล้วเปิดในเบราว์เซอร์ของเครื่อง (Office.context.ui.openBrowserWindow) แทน
3. เทคโนโลยี
Add-in (frontend)
- Office Add-in project scaffold จาก
yo office(Yeoman generator) - TypeScript + React (task pane UI)
- Office.js / Word JavaScript API (
office-js, ใช้@types/office-js) - Fluent UI React components (ให้เข้ากับ Office look & feel)
Web Portal (เว็บแยก) — เพิ่มเติม v2
- Nuxt.js (Vue 3) + TailwindCSS — ทุกหน้ารองรับ light/dark theme (toggle + persist + default ตาม
prefers-color-scheme, TailwinddarkMode: 'class') - Auth ผ่าน Keycloak client
word-versioning-web(public + PKCE) ด้วย OIDC browser redirect ปกติ — เป็น top-level page จึงไม่ต้องใช้ Office Dialog API - Dev server ที่ port 5173 (ห้ามใช้ 3000 เพราะชนกับ add-in dev server)
Backend
- ASP.NET Core 8 Web API
- Entity Framework Core + PostgreSQL (provider
Npgsql.EntityFrameworkCore.PostgreSQL) - DocumentFormat.OpenXml (OpenXML SDK) — parse content controls จาก template, แทนที่ค่าตอน generate, stamp Custom Properties ลงไฟล์
- MinIO สำหรับเก็บไฟล์ .docx จริง — ใช้ AWS SDK for .NET (
AWSSDK.S3) เชื่อมต่อผ่าน S3-compatible API (MinIO รองรับ S3 API เต็มรูปแบบ) โดย config endpoint ชี้ไปที่ MinIO instance แทน AWS จริง - ออกแบบผ่าน interface (
IObjectStorageService) เพื่อ swap ไป provider อื่นที่รองรับ S3 API ได้ในอนาคต (AWS S3, Azure Blob ผ่าน S3-compat gateway, ฯลฯ) โดยไม่กระทบ business logic - Auth: Keycloak (OpenID Connect) — Add-in ทำ OIDC login flow ผ่าน Keycloak แล้วได้ access token (JWT) มาแนบกับทุก request ไป backend, backend validate token ผ่าน Keycloak's public key (JWKS endpoint)
4. โครงสร้างโปรเจกต์ที่ต้องการ
/word-versioning-addin
/addin # Office.js task pane project (yo office scaffold)
manifest.xml
/src
/taskpane
taskpane.html
taskpane.ts
auth-callback.html # หน้ารับ redirect จาก Keycloak ภายใน Dialog API
/components
VersionHistoryPanel.tsx
ConfirmVersionDialog.tsx
/services
metadataService.ts # custom properties + custom XML part logic
versioningService.ts # เรียก backend API
fileService.ts # getFileAsync/slice → Blob
authService.ts # Keycloak OIDC + PKCE login flow, token refresh
apiClient.ts # แนบ Authorization: Bearer <token> ทุก request
/models
versionRecord.ts
package.json
webpack.config.js
/backend
/WordVersioning.Api
Program.cs
/Controllers
DocumentsController.cs # FR-5 ถึง FR-7 + generate (FR-10) + field sync (FR-11)
TemplatesController.cs # FR-9 template CRUD + auto-detect fields
/Models
Document.cs # + TemplateId
DocumentVersion.cs # + FieldsSnapshotJson
Template.cs
TemplateField.cs
DocumentFieldValue.cs
/Data
AppDbContext.cs
/Services
ObjectStorageService.cs # wrapper เรียก MinIO ผ่าน AWSSDK.S3 (bucket docx-versions + docx-templates)
HashService.cs
DocxGenerationService.cs # FR-10: แทนที่ค่าใน SDT + stamp custom properties
appsettings.json # เก็บ MinIO endpoint/keys, Keycloak Authority/Audience
WordVersioning.Api.csproj
/web # Web Portal — เพิ่มเติม v2 (Nuxt.js + TailwindCSS)
nuxt.config.ts # devServer port 5173
tailwind.config.ts # darkMode: 'class'
/pages # เลือก template สร้างเอกสาร / จัดการ template / dynamic form
# + รายการเอกสารต่อ template + detail เอกสาร + re-generate (FR-12, v4)
/composables # useAuth (PKCE redirect), useApi, useTheme (light/dark)
/utils # oidc (PKCE helpers), fieldValidation, snapshot — pure functions ไว้ unit test
package.json
/keycloak
realm-export.json # pre-configured realm/client สำหรับ local dev import อัตโนมัติ
docker-compose.yml # รัน MinIO + Keycloak + backend + DB สำหรับ local dev
5. Functional Requirements — Add-in (Frontend)
FR-1: ตรวจจับไฟล์ที่เคยบันทึกจาก add-in นี้
- เมื่อ
Office.onReady()ให้เช็คสถานะ login ก่อน (มี valid access token ใน session หรือไม่) — ถ้ายังไม่ login ให้แสดงปุ่ม "Sign in" (trigger login flow ตาม FR-8) ก่อนเข้าถึงฟีเจอร์อื่น - ตรวจ Custom Document Property
SavedByAddInเทียบกับค่าคงที่ADDIN_SIGNATURE_GUID - ถ้าตรง → โหลด
DocumentTrackingIdจาก properties แล้วเรียก backend (แนบ Bearer token) เพื่อดึง version history มาแสดงใน task pane - ถ้าไม่ตรง/ไม่มี → แสดง UI "เริ่มต้น track เอกสารนี้" (ปุ่ม "Start Versioning This Document")
FR-2: ปุ่ม "Save & Version" (แทนการดัก native save)
Flow เมื่อกด:
- แสดง dialog ยืนยัน ("สร้าง version ใหม่ของเอกสารนี้หรือไม่?")
- Stamp Custom Document Properties:
SavedByAddIn,DocumentTrackingId(สร้างใหม่ถ้ายังไม่มี),LastVersionId,LastVersionTimestamp - อัปเดต Custom XML Part สำหรับ version history (delete-then-add pattern)
- เรียก
context.document.save(Word.SaveBehavior.save)เพื่อ trigger native save - ดึงไฟล์ปัจจุบันด้วย File/Slice API (
Office.context.document.getFileAsync) - อัปโหลดไปยัง backend endpoint
POST /api/documents/{documentId}/versionsพร้อมแนบค่า fields ที่ extract จาก content controls ของเอกสาร (FR-11) - แสดงผลลัพธ์ (success/error) ใน task pane, refresh version history list
FR-3: Soft reminder เมื่อยังไม่ได้ version
- เนื่องจากดัก native save ไม่ได้ ให้ใช้
Document.addHandlerAsync(Office.EventType.DocumentSelectionChanged)หรือ polling เบาๆ (เช่น debounce ทุก 30 วินาที) ตรวจcontext.document.saved— ถ้าfalseต่อเนื่องนานเกิน threshold ให้แสดง banner ใน task pane เตือนว่า "มีการแก้ไขที่ยังไม่ได้ version" (ไม่ใช่ hard block)
FR-4: Version History Panel
- แสดงรายการ version จาก backend: version number, timestamp, ผู้สร้าง
- ปุ่ม "Download this version" → เรียก
GET /api/documents/{documentId}/versions/{versionId}/download
6. Functional Requirements — Backend API, Template System & Web Portal
FR-5: Upload version
POST /api/documents/{documentId}/versions
Content-Type: multipart/form-data
Body: file (docx binary), clientVersionId (guid)
- ถ้า
documentIdยังไม่มีใน DB ให้สร้างDocumentrecord ใหม่ - อัปโหลดไฟล์ไป MinIO bucket (เช่น
docx-versions) ที่ object key:{documentId}/v{versionNumber:D4}_{clientVersionId}.docx - คำนวณ SHA256 hash ของไฟล์ เก็บลง DB (ใช้ตรวจสอบไฟล์ซ้ำ/integrity ภายหลัง)
- บันทึก
DocumentVersionrecord (version number = auto increment ต่อ document) - Response:
{ versionId, versionNumber }
FR-6: ดึง version history
GET /api/documents/{documentId}/versions
- คืน list ของ
DocumentVersionเรียงจากใหม่ไปเก่า
FR-7: ดาวน์โหลด version ที่ระบุ
GET /api/documents/{documentId}/versions/{versionId}/download # proxy stream (curl/Swagger)
GET /api/documents/{documentId}/versions/{versionId}/download-link # presigned URL อายุสั้น (add-in ใช้อันนี้)
download— stream ไฟล์จาก MinIO กลับไป พร้อม content-type ที่ถูกต้องdownload-link— ออก presigned URL อายุสั้น (config ได้, default ~2 นาที, clamp 30 วิ–10 นาที) ให้ client เปิดในเบราว์เซอร์ของเครื่อง — เป็นกลไกหลักของ add-in เพราะ webview ของ host ทิ้ง blob download แบบเงียบๆ (v3) · เข้าถึง MinIO โดยตรงโดยไม่ต้องแนบ token แต่หมดอายุทันทีเมื่อเกินเวลา
FR-8: Auth (Keycloak / OpenID Connect)
Keycloak setup
- สร้าง Realm แยกสำหรับระบบนี้ (เช่น
word-versioning) - สร้าง Client 3 ตัว:
word-addin-officejs— public client,Standard Flow(Authorization Code) เปิด, บังคับ PKCE (S256), ปิด client secret (เพราะรันบน browser/task pane เข้าถึงได้)word-addin-vsto— public client เช่นกัน สำหรับ desktop app (ดูรายละเอียด flow ในspec-vsto.md)word-versioning-web— public client สำหรับ Web Portal, บังคับ PKCE (S256), redirect URIhttp://localhost:5173/*— ใช้ OIDC browser redirect ปกติ (top-level page)
- Redirect URI ของ
word-addin-officejsต้องชี้ไปหน้า redirect ภายใน add-in เอง (เช่นhttps://localhost:3000/auth-callback.htmlตอน dev)
Login flow ฝั่ง Office Add-in (Office.js)
Task pane รันอยู่ใน iframe — Keycloak login page ปกติส่ง header
X-Frame-Options/CSP กัน embed ใน iframe อยู่แล้ว จึง ห้าม redirect iframe ตรงๆ ไป Keycloak ต้องใช้ Office Dialog API แทน:
- Task pane เรียก
Office.context.ui.displayDialogAsync(keycloakAuthUrl, { height: 60, width: 30 })เปิด dialog window แยก (ไม่ใช่ iframe) ไปหน้า Keycloak login (Authorization Code + PKCE) - ผู้ใช้ login ใน dialog นั้น → Keycloak redirect กลับมาที่
auth-callback.html(หน้าเล็กๆ ที่รันอยู่ใน dialog) auth-callback.htmlเรียกOffice.context.ui.messageParent(JSON.stringify({ code, state }))ส่ง authorization code กลับไปที่ task pane แล้วปิด dialog เอง (Office.context.ui.messageParent+window.close())- Task pane รับ code → แลก token ที่ Keycloak token endpoint ด้วย PKCE verifier (ไม่ต้องพึ่ง backend ช่วยแลก เพราะเป็น public client)
- เก็บ
access_token/refresh_tokenไว้ใน memory (JS variable) ของ task pane session ห้ามเก็บใน localStorage (เสี่ยง XSS/ไม่หายเมื่อปิด task pane) — ถ้าต้อง persist ข้าม session ให้ใช้sessionStorageเท่านั้นและ set อายุสั้น - ก่อนหมดอายุ access token ให้ silent-refresh ด้วย
refresh_tokenผ่าน token endpoint
Backend (ASP.NET Core)
- ใช้
Microsoft.AspNetCore.Authentication.JwtBearerconfig:builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "http://keycloak:8080/realms/word-versioning"; options.Audience = "word-addin-officejs"; // หรือ resource server audience ที่ตั้งไว้ options.RequireHttpsMetadata = false; // dev เท่านั้น, prod ต้อง true }); - Backend validate token ผ่าน Keycloak's JWKS endpoint อัตโนมัติ (ไม่ต้อง hardcode public key)
- ทุก endpoint ใน
DocumentsControllerต้องมี[Authorize] - ดึง
userId/usernameจาก JWT claims (sub,preferred_username) มาใช้แทนการรับจาก client ตรงๆ (กัน client ปลอมค่าCreatedByUserId)
FR-9: Template management (เพิ่มเติม v2)
- CRUD template: upload ไฟล์ .docx template (ที่มี content controls), ตั้ง name/description/active flag
- ตอน upload/replaces ไฟล์ ให้ backend parse content controls (SDT tags) ออกมาเป็น field list อัตโนมัติ ด้วย OpenXML
- Admin ตั้งค่าต่อ field ผ่าน Web Portal: label, type (
text/number/date), required, ลำดับ, editor (FR-14) - ลบ template = soft-delete (deactivate) เท่านั้น เพราะ document เก่ายังอ้างอยู่ — เอกสารที่สร้างแล้วใช้ template ที่ deactivate ต่อได้
- ไฟล์ template เก็บใน MinIO bucket
docx-templates(private)
POST /api/templates # upload + auto-detect fields (multipart: file + name + description)
GET /api/templates # รายการ (filter active/all)
PUT /api/templates/{id} # แก้ metadata + บันทึก field definitions
PUT /api/templates/{id}/file # แทนที่ไฟล์ (re-detect fields, เตือนถ้า tag หายไป)
DELETE /api/templates/{id} # deactivate (soft-delete)
FR-10: สร้างเอกสารจาก template ผ่าน Web Portal (เพิ่มเติม v2)
POST /api/documents/generate { templateId, values: { <fieldKey>: <value> } }
- Validate ก่อน: required fields ครบ, type ถูก (number/date parse ได้)
DocxGenerationService(OpenXML): โหลด template จาก MinIO → แทนที่ข้อความใน SDT แต่ละ field ที่ run level (รักษา formatting ของ template) → stamp Custom Properties (SavedByAddIn= GUID ของ add-in,DocumentTrackingId= GUID ใหม่) ลงไฟล์ — ทำฝั่ง backend เท่านั้น เพื่อให้ v1 + metadata เกิดพร้อมกันแบบ atomic ไฟล์ที่ดาวน์โหลดถูกต้องสมบูรณ์ตั้งแต่ครั้งแรก- สร้าง
Documentrecord (ผูกTemplateId) +DocumentFieldValueทุก field + อัปโหลดเป็น version 1 + snapshot fields ของ v1 - Response
{ documentId, versionId, versionNumber, downloadUrl }→ ผู้ใช้ดาวน์โหลด .docx ไปเปิดใน Word → FR-1 ทำงานทันที (เป็น tracked document เห็น v1 โดยไม่ต้องกด Start Versioning)
FR-11: Field sync — update fields ใน DB ตอนสร้าง version (เพิ่มเติม v2)
- ขยาย
POST /api/documents/{documentId}/versionsให้รับfields(map tag→text ที่ add-in extract มาตาม FR-2 ขั้นตอนที่ 6) - เทียบกับ
TemplateFieldของ template ที่ document ผูกอยู่ → update latest value ต่อ field (DocumentFieldValue) + เก็บ snapshot ต่อ version (DocumentVersion.FieldsSnapshotJson— ใช้ diff ใน history ได้) - field ที่ส่งมาขาด (ผู้ใช้ลบ content control ใน Word) → เก็บค่าเดิม + mark missing ใน snapshot แต่ไม่ fail request; ส่งมาเกินนิยาม → ignore
UpdatedByUserIdมาจาก JWT claims ของผู้กด Save & Version เสมอ
FR-12: รายการเอกสารต่อ template + Document detail + Re-generate (เพิ่มเติม v4)
GET /api/templates/{templateId}/documents— รายการเอกสารที่สร้างจาก template หนึ่งตัว (ชื่อไฟล์, จำนวน version, version ล่าสุด + เวลา) เรียงตามกิจกรรมล่าสุด — ใช้ได้แม้ template ถูก deactivate (เอกสารเก่ายังต้องเข้าถึงได้ตาม FR-9)GET /api/documents/{documentId}— detail ของเอกสาร: ข้อมูลหัวเรื่อง + template + ค่าล่าสุดของทุก field (นิยามจากTemplateFieldmerge กับDocumentFieldValue) — รองรับ template เปลี่ยนนิยามหลังเอกสารสร้างแล้ว: field ที่เพิ่มเข้าทีหลังแสดงค่าว่าง, ค่าที่ key ไม่มีในนิยามปัจจุบันยังแสดงโดยไม่มี label- หน้าเว็บ (Web Portal): หน้ารายการเอกสารของ template + หน้า detail ที่แสดงค่า fields ปัจจุบันและ version history (ขยายแต่ละ version เห็น snapshot ค่าตอนนั้น + ดาวน์โหลด)
- Re-generate: จากหน้า detail ของเอกสารที่สร้างจาก template → เข้าฟอร์มสร้างเอกสารโดย prefill ค่าล่าสุดของทุก field ให้แก้ไขก่อน → สร้างเป็น เอกสารใหม่ (v1 ใหม่ + documentId ใหม่) ผ่าน endpoint generate เดิม — เอกสารต้นทางและ version history ของมันไม่ถูกแตะ (ไม่เกิด version ใหม่) — template ที่ deactivate จะ re-generate ไม่ได้ตามกติกาของ FR-10
FR-13: คำอธิบายต่อ version — commit message (เพิ่มเติม v5)
- Field ชื่อ
message— string ทางเลือก (null ได้) ยาวไม่เกิน 2048 อักขระ เก็บที่DocumentVersion.Message— ผู้ใช้พิมพ์อธิบายการเปลี่ยนแปลงของ version นั้นแบบเดียวกับ git commit message - รับจากทุก flow ที่สร้าง version:
POST /api/documents/{documentId}/versions— multipart form fieldmessage(เพิ่มจากfile/clientVersionId/fieldsเดิม)POST /api/documents/generate— JSON propertymessage(เพิ่มจากtemplateId/valuesเดิม)
- ค่าว่าง/whitespace ล้วน → เก็บ
null(trim ก่อนเก็บ — นโยบายเดียวกับTemplate.Description) · เกิน 2048 อักขระ → 400 - แสดงใน version history ทุกที่: ตาราง + การ์ดมือถือของ Web Portal (FR-12) และ
VersionHistoryPanelของ add-in (FR-4) - Add-in: dialog ยืนยันมี textarea ทางเลือก — ทั้ง Save & Version (FR-2) และ Start Versioning (FR-1 ซึ่ง v5 นี้เพิ่ม dialog ยืนยันให้ flow เริ่ม track ก่อนอัปโหลด v1)
- Web Portal: ฟอร์มสร้างเอกสาร (FR-10) มี textarea ทางเลือก — re-generate ตาม FR-12 ไม่ prefill ข้อความ (เป็นเอกสารใหม่ ได้ข้อความใหม่)
- ไม่ฝังลงไฟล์ .docx (custom properties หรือ custom XML part) — backend คือ source of truth ของ version history ตามกติกา และเลี่ยงปัญหา backward-compat กับไฟล์เก่า
FR-14: Field editor — รูปแบบช่องกรอกต่อ field (เพิ่มเติม v6)
- ผู้ดูแลเลือก editor (รูปแบบ control ในฟอร์มสร้างเอกสารของ Web Portal) ต่อ field ได้ โดยเป็น metadata ที่แยกจาก
Type(ซึ่งคุม validation):text→textbox(default) หรือtextarea— สำหรับข้อความหลายบรรทัดnumber→number(ช่องตัวเลข — ทางเดียว)date→date(date picker — ทางเดียว)
PUT /api/templates/{id}:editorเป็นทางเลือก — ไม่ส่ง/ว่าง = default ของ type · ค่าที่ไม่รู้จักหรือไม่เข้ากับ type → 400 · เก็บค่าที่เท่ากับ default เป็นnull(normalize-on-write) แต่ API คืนค่า resolved เสมอ- field ที่ detect อัตโนมัติตอน upload/replace ได้ editor = default ของ type (
textbox) - presentation metadata ล้วน — ไม่กระทบ validation หรือ generate (validation ยึด
Typeเท่านั้น) ค่า field เป็น string ทั้ง flow ไม่แตะDocumentFieldValue/FieldsSnapshotJson/ไฟล์ docx/add-in
7. Data Model (Backend)
public class Document
{
public Guid Id { get; set; }
public string FileName { get; set; }
public string AddInSignature { get; set; }
public Guid? TemplateId { get; set; } // เอกสารที่สร้างจาก template (FR-10) — null ถ้า track เอกสารเปล่า
public List<DocumentVersion> Versions { get; set; } = new();
}
public class DocumentVersion
{
public Guid Id { get; set; }
public Guid DocumentId { get; set; }
public int VersionNumber { get; set; }
public string ObjectKey { get; set; } // MinIO object key (bucket/path)
public string ContentHash { get; set; }
public string CreatedByUserId { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string? FieldsSnapshotJson { get; set; } // snapshot ค่า fields ของ version นี้ (FR-11)
public string? Message { get; set; } // คำอธิบาย version จากผู้ใช้ แบบ commit message (FR-13) — ยาวสูงสุด 2048
}
public class Template // เพิ่มเติม v2 (FR-9)
{
public Guid Id { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public string ObjectKey { get; set; } // ไฟล์ .docx ใน bucket docx-templates
public bool IsActive { get; set; } = true; // soft-delete ผ่าน flag นี้
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public List<TemplateField> Fields { get; set; } = new();
}
public class TemplateField
{
public Guid Id { get; set; }
public Guid TemplateId { get; set; }
public string Key { get; set; } // = content control (SDT) tag — unique ต่อ template
public string Label { get; set; } // ชื่อที่แสดงใน form
public string Type { get; set; } // text | number | date
public string? Editor { get; set; } // รูปแบบช่องกรอก (FR-14): textbox|textarea สำหรับ text · number · date — null = default ของ Type
public bool IsRequired { get; set; }
public int SortOrder { get; set; }
}
public class DocumentFieldValue // latest value ต่อ field ของ document (FR-11)
{
public Guid DocumentId { get; set; }
public string FieldKey { get; set; } // = TemplateField.Key
public string Value { get; set; }
public Guid SourceVersionId { get; set; } // version ล่าสุดที่อัปเดตค่านี้
public string UpdatedByUserId { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}
8. Data Model (Frontend / metadata ในไฟล์ docx)
Custom Document Properties:
| Key | Value |
|---|---|
SavedByAddIn |
GUID คงที่ของ add-in — ค่าที่ปักหมุด: 9B7A3C51-4E6F-4D2A-8B5C-1E0F2A3B4C5D (backend: Services/AddInMetadata.AddInSignatureGuid, add-in: ADDIN_SIGNATURE_GUID ต้องเป็นค่านี้เป๊ะ) |
DocumentTrackingId |
GUID เฉพาะของเอกสาร (ผูกกับ documentId ฝั่ง backend) |
LastVersionId |
GUID ของ version ล่าสุด — คือ clientVersionId ที่ client generate (ต้อง stamp ลงไฟล์ก่อน save/upload จึงจะติดไปกับ snapshot; versionId จริงของ backend ได้มาหลังไฟล์ถูก capture ไปแล้ว — backend คือ source of truth ของ id จริง) |
LastVersionTimestamp |
ISO-8601 UTC timestamp |
Custom XML Part (namespace urn:myaddin:versioning) — เก็บ compact history เสริม (ไม่ใช่ source of truth, backend คือ source of truth):
<VersionHistory xmlns="urn:myaddin:versioning">
<Version id="..." number="3" timestamp="..." user="..."/>
<Version id="..." number="2" timestamp="..." user="..."/>
</VersionHistory>
9. Non-Functional Requirements
- Manifest ต้อง sideload ได้บน Word Desktop (Win/Mac) และ Word Online เพื่อ dev/test ได้ทั้งสามแพลตฟอร์ม
- Backend endpoint upload ต้อง handle ไฟล์ขนาดใหญ่ (จำกัด request size ให้เหมาะสม เช่น 100MB) และ reject content-type ที่ไม่ใช่ docx
- ทุก error จาก Office.js call ต้อง catch และแสดงข้อความที่เข้าใจง่ายใน task pane (อย่าปล่อย unhandled promise rejection)
- Backend ต้องมี CORS config อนุญาต origin ของ add-in (
https://localhost:3000สำหรับ dev) - Logging ฝั่ง backend: log ทุก upload/download พร้อม documentId, versionId, userId
- MinIO bucket ต้องสร้างแบบ private (ไม่ public read) — เข้าถึงไฟล์ได้ผ่าน backend API เท่านั้น (proxy stream หรือ presigned URL ที่หมดอายุสั้นๆ)
- Config การเชื่อมต่อ MinIO (
Endpoint,AccessKey,SecretKey,BucketName,UseSSL) ต้องแยกเป็น environment variable/appsettings.jsonไม่ hardcode ในโค้ด เพื่อ deploy ไป production MinIO cluster ได้โดยไม่แก้โค้ด - Config Keycloak (
Authority,Audience,ClientIdของแต่ละ client) ต้องแยกเป็น environment variable/appsettings.jsonเช่นกัน ไม่ hardcode - ถ้า access token หมดอายุระหว่างใช้งาน task pane ต้อง silent-refresh อัตโนมัติ ถ้า refresh token หมดอายุ/ถูก revoke ให้ redirect กลับไป login flow ใหม่แบบ graceful (ไม่ใช่ error ค้าง)
เพิ่มเติม v2 (Template System & Web Portal)
- Field key = content control (SDT) tag ต้อง unique ต่อ template — ใช้ tag เดียวกันทั้งตอน generate (backend), ตอน extract (add-in) และตอน sync (DB)
- การ generate + stamp metadata ต้องทำฝั่ง backend เท่านั้น (OpenXML) — ไฟล์ที่ดาวน์โหลดจากเว็บต้องถูกต้องสมบูรณ์ตั้งแต่ครั้งแรก ไม่พึ่ง client ตั้งค่าใดๆ
- OpenXML replace ต้องแก้ที่ run level ใน SDT เพื่อรักษา formatting ของ template — ห้ามทับทั้ง paragraph
- Web Portal ทุกหน้ารองรับ light/dark theme (toggle + persist + default ตาม
prefers-color-scheme) - Dev server ports ห้ามชนกัน: add-in =
https://localhost:3000, Web Portal =http://localhost:5173(ตั้งในnuxt.config.ts) - Dev ต้องเสิร์ฟ HTTPS ด้วย cert ของ
office-addin-dev-certsทุก service ที่ task pane เรียก (v3): Keycloak =https://localhost:8443(dialog บังคับ HTTPS), backend =https://localhost:7232เพิ่มคู่กับhttp://localhost:5075(กัน mixed content) — ต้องรันnpx office-addin-dev-certs installก่อนdocker compose upเพราะ Keycloak mount cert จาก~/.office-addin-dev-certs - Backend CORS ต้องอนุญาตทั้ง origin ของ add-in (
https://localhost:3000) และ Web Portal (http://localhost:5173)
10. Acceptance Criteria
- Sideload add-in บน Word Desktop และ Word Online ได้ทั้งคู่ ไม่มี error ตอนโหลด task pane
- เปิด task pane ครั้งแรกโดยยังไม่ login → เห็นปุ่ม "Sign in", กดแล้วเปิด dialog ไป Keycloak login page ได้ถูกต้อง, login สำเร็จแล้ว dialog ปิดเองและ task pane ได้ token มาใช้งาน
- เรียก backend endpoint โดยไม่แนบ token → ได้ 401 Unauthorized
- เปิดไฟล์ .docx ใหม่ที่ไม่เคย track (หลัง login แล้ว) → task pane แสดงปุ่ม "Start Versioning This Document"
- กด "Start Versioning" → stamp metadata ครั้งแรก, สร้าง
Documentrecord ใน backend, อัปโหลด version 1 สำเร็จ,CreatedByUserIdใน DB ตรงกับsub/preferred_usernameจาก JWT ของผู้ login - ปิดแล้วเปิดไฟล์เดิมใหม่ (ทั้งบน Windows และ Mac) → task pane ตรวจพบว่าเป็น tracked document และโหลด version history จาก backend ได้ถูกต้อง
- แก้ไขเอกสารแล้วกด "Save & Version" → มี version ใหม่เพิ่มใน backend,
LastVersionId/LastVersionTimestampใน properties อัปเดตถูกต้อง - ดาวน์โหลด version เก่าจาก panel ได้ไฟล์ที่เปิดด้วย Word ได้ปกติ และเนื้อหาตรงกับตอน upload
- ปล่อย session ทิ้งไว้จนกว่า access token หมดอายุ แล้วลองใช้งานต่อ → ต้อง silent-refresh สำเร็จโดยผู้ใช้ไม่รู้สึกสะดุด (ไม่ต้อง login ใหม่)
- ทดสอบบน Word for Mac: Custom XML Part อาจ fail แบบ silent ตามที่ระบุใน spec — ต้องมี fallback ที่ระบบยังทำงานถูกต้องแม้ XML part เขียนไม่สำเร็จ (เพราะ backend คือ source of truth)
- (v2) upload template .docx ที่มี content controls ผ่านหน้าเว็บ → ระบบ detect fields อัตโนมัติ + บันทึก field definitions ได้
- (v2) กรอก form บนเว็บสร้างเอกสาร → ดาวน์โหลด .docx ที่ค่าครบถูกต้อง formatting ไม่พัง → เปิดใน Word แล้ว add-in รายงานเป็น tracked document เห็น version 1 ทันที โดยไม่ต้องกด Start Versioning
- (v2) แก้ค่าใน content control ที่ Word แล้วกด Save & Version → field value ใน DB เปลี่ยนตาม + snapshot ต่อ version ถูกเก็บ
- (v2) ลบ content control ออกจาก Word แล้วกด Save & Version → flow ยังสำเร็จ ค่าเดิมของ field นั้นคงอยู่ (mark missing) ไม่ error
- (v4) เข้าหน้ารายการเอกสารของ template บนเว็บ → เห็นเอกสารที่สร้างจาก template นั้นครบ พร้อมจำนวน version และเวลาล่าสุด · เข้าหน้า detail → เห็นค่าทุก field ปัจจุบัน + version history (ขยายเห็นค่าต่อ version, ดาวน์โหลดได้) · กด re-generate แล้วสร้างเอกสารใหม่เป็น v1 โดยเอกสารต้นทางไม่มี version ใหม่
- (v5) พิมพ์คำอธิบายตอนสร้าง version ได้จากทุก flow (Save & Version, Start Versioning, สร้างจากเว็บ) → ข้อความถูกเก็บกับ version นั้นใน DB และแสดงใน version history ทุกหน้า (web ตาราง + การ์ดมือถือ, add-in panel) · เว้นว่างได้ (ไม่ส่ง/ช่องว่างล้วน = ไม่มีข้อความ ไม่ error)
- (v6) admin เลือก editor ของ field type
textเป็นtextareaได้ผ่านหน้าจัดการ template → ฟอร์มสร้างเอกสารแสดงเป็น textarea · fieldnumber/dateใช้ control ตาม type (ช่องตัวเลข / date picker) · ส่ง editor ที่ไม่รู้จักหรือไม่เข้ากับ type → 400 · ไม่ส่ง editor → ได้ default ของ type · ค่า field และการ generate ไม่เปลี่ยนจากเดิม
11. งานที่ Claude Code ควรทำ
Frontend
- Scaffold Office Add-in project ด้วย
yo office(task pane, TypeScript, React) - Implement
metadataService.ts,fileService.ts,versioningService.ts,authService.ts,apiClient.ts—metadataServiceรวมการอ่าน content controls เป็น map{tag: text}สำหรับ field sync (FR-11) authService.tsต้อง implement: build Keycloak authorization URL + PKCE code_verifier/code_challenge, เปิดOffice.context.ui.displayDialogAsync, รับ code กลับผ่านmessageParent, แลก token, เก็บ token in-memory, silent refresh- Implement
VersionHistoryPanel.tsx,ConfirmVersionDialog.tsx, และหน้า sign-in state - เขียน
auth-callback.html(หน้าเล็กๆ รับ redirect จาก Keycloak แล้วmessageParentกลับ) - เขียน manifest.xml ให้ครบ requirement sets ที่ใช้
- เขียน unit test สำหรับ service layer (mock
Word.run,fetch, และ mock Keycloak token response) - (v5)
ConfirmVersionDialogเพิ่ม textarea ทางเลือก (commit message) + ให้ Start Versioning ใช้ dialog ด้วย + ส่งmessageผ่านversionFlow/versioningService+ แสดงในVersionHistoryPanel(FR-13)
Backend
- Scaffold ASP.NET Core 8 Web API project
- Implement
AppDbContext, migrations - Config JWT Bearer authentication ให้ validate ผ่าน Keycloak Authority/JWKS ตาม FR-8
- Implement
DocumentsControllerตาม FR-5 ถึง FR-7 พร้อม[Authorize]ทุก endpoint, ดึงuserIdจาก JWT claims - Implement
ObjectStorageService(interfaceIObjectStorageService) เชื่อมต่อ MinIO ผ่านAWSSDK.S3— ตั้งServiceURLชี้ไป MinIO endpoint,ForcePathStyle = true(จำเป็นสำหรับ MinIO) - เขียน
realm-export.jsonสำหรับ Keycloak local dev — pre-config realmword-versioning, clientword-addin-officejs(public, PKCE) และword-addin-vsto(public, PKCE) - เขียน integration test สำหรับ upload/download version flow (รัน MinIO + Keycloak จริงผ่าน testcontainers หรือ docker-compose ตอนเทส, ใช้ Keycloak admin API สร้าง test user + ขอ token มาแนบ request)
- เขียน
docker-compose.ymlสำหรับ local dev: MinIO + Keycloak (import realm-export.json อัตโนมัติ) + DB container + backend - เขียน README อธิบาย local dev setup (sideload add-in, run backend, connection string, เข้า MinIO console, เข้า Keycloak admin console, สร้าง test user แรก)
- (v2) Implement
TemplatesController+ entitiesTemplate/TemplateField/DocumentFieldValue+ migrations (FR-9) รวม parse SDT tags ตอน upload - (v2) Implement
DocxGenerationService+POST /api/documents/generate(FR-10) — แทนที่ SDT ที่ run level + stamp custom properties + สร้าง Document + v1 + field values ใน flow เดียว - (v2) ขยาย upload endpoint ให้รับ
fieldsแล้ว syncDocumentFieldValue+ snapshot ต่อ version ตาม missing-field policy (FR-11) - (v2) เขียน integration test ครอบ template detect / generate / field sync (รวมกรณี field หาย → ค่าเดิม + mark missing)
- (v5) เพิ่ม
DocumentVersion.Message+ migrationAddVersionMessage+ รับ/คืนmessageทั้ง upload/generate/history + ตรวจความยาว 2048 (FR-13) - (v6) เพิ่ม
TemplateField.Editor+ migrationAddTemplateFieldEditor+ validate editor ตาม type ในPUT /api/templates+ คืน editor ในTemplateFieldDto(FR-14)
Web Portal (เพิ่มเติม v2)
- Scaffold Nuxt.js + TailwindCSS — dev port 5173,
darkMode: 'class',useTheme(light/dark toggle + persist + system default) useAuth(PKCE browser redirect ผ่าน Keycloak clientword-versioning-web) +useApi(Bearer + silent refresh)- หน้าจัดการ template: list / upload (โชว์ fields ที่ detect อัตโนมัติ) / แก้ field definitions / deactivate
- หน้าสร้างเอกสาร: dynamic form สร้างจาก field definitions (input type ตาม field type) + validate + ดาวน์โหลดผลลัพธ์
- เขียน unit test ของ form validation + useTheme (Vitest)
- (v4/FR-12) หน้ารายการเอกสารของ template + หน้า detail ของเอกสาร (ค่า fields ปัจจุบัน + version history พร้อม snapshot ต่อ version + ดาวน์โหลดต่อ version) + re-generate ผ่านฟอร์ม prefilled ค่าปัจจุบัน
- (v5) ฟอร์มสร้างเอกสารเพิ่ม textarea ทางเลือก (commit message) + แสดง
messageใน version history (FR-13) - (v6) หน้าแก้ field definitions เพิ่มตัวเลือก editor ต่อ field (เลือกได้เฉพาะ type
text) + ฟอร์มสร้างเอกสาร render ตาม editor (textarea/ช่องตัวเลข/date picker/textbox) + utilfieldEditor(FR-14)