eCourt-Doc-Edit/specs/spec-officejs.md
Suphonchai Phoonsawat 25c2ce53cd Phase 3
2026-08-19 12:15:20 +07:00

35 KiB

Spec: Word Versioning Add-in (Office.js Cross-Platform + .NET Backend)

ฉบับแก้ไข 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 ส่วนที่ทำงานร่วมกัน:

  1. Office Add-in (Office.js, TypeScript) — รันได้บน Word Desktop (Windows/Mac) และ Word Online โดยไม่ต้อง build แยก
  2. .NET 8 Web API Backend — เก็บ version metadata ใน DB และไฟล์ .docx จริงใน MinIO (S3-compatible object storage) + ระบบ template และ generate เอกสาร
  3. Web Portal (Nuxt.js + TailwindCSS) — เว็บแยกสำหรับจัดการ template และกรอก form สร้างเอกสารครั้งแรกจาก template โดยเป็น version 1 อัตโนมัติ

ความสามารถหลัก (เทียบเท่า VSTO แต่ปรับ flow ให้เข้ากับข้อจำกัดของแพลตฟอร์ม):

  1. ตรวจจับได้ว่าไฟล์ .docx ที่เปิดอยู่เคยถูกบันทึกโดย add-in นี้หรือไม่
  2. มีปุ่ม "Save & Version" ใน Task Pane ให้ผู้ใช้กดเพื่อยืนยันสร้าง version (แทนการดัก native save เพราะ Office.js ไม่มี before-save event)
  3. เมื่อกด Save & Version จะ stamp tag ลง metadata ของไฟล์ + อัปโหลด snapshot ไป backend + extract ค่าจาก content controls ไป update fields ใน DB ให้ sync (FR-11)
  4. Backend เก็บ version history แบบ query ได้, ดาวน์โหลด version ย้อนหลังได้
  5. จัดการ template เอกสาร (.docx ที่มี content controls) ผ่านหน้าเว็บ — upload แล้วระบบ detect fields อัตโนมัติ (FR-9)
  6. สร้างเอกสารครั้งแรกจาก 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+) ใช้งานได้

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, Tailwind darkMode: '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
    /composables                 # useAuth (PKCE redirect), useApi, useTheme (light/dark)
    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 เมื่อกด:

  1. แสดง dialog ยืนยัน ("สร้าง version ใหม่ของเอกสารนี้หรือไม่?")
  2. Stamp Custom Document Properties: SavedByAddIn, DocumentTrackingId (สร้างใหม่ถ้ายังไม่มี), LastVersionId, LastVersionTimestamp
  3. อัปเดต Custom XML Part สำหรับ version history (delete-then-add pattern)
  4. เรียก context.document.save(Word.SaveBehavior.save) เพื่อ trigger native save
  5. ดึงไฟล์ปัจจุบันด้วย File/Slice API (Office.context.document.getFileAsync)
  6. อัปโหลดไปยัง backend endpoint POST /api/documents/{documentId}/versions พร้อมแนบค่า fields ที่ extract จาก content controls ของเอกสาร (FR-11)
  7. แสดงผลลัพธ์ (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 ให้สร้าง Document record ใหม่
  • อัปโหลดไฟล์ไป MinIO bucket (เช่น docx-versions) ที่ object key: {documentId}/v{versionNumber:D4}_{clientVersionId}.docx
  • คำนวณ SHA256 hash ของไฟล์ เก็บลง DB (ใช้ตรวจสอบไฟล์ซ้ำ/integrity ภายหลัง)
  • บันทึก DocumentVersion record (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
  • Stream ไฟล์จาก MinIO กลับไป พร้อม content-type ที่ถูกต้อง (ใช้ presigned URL หรือ proxy stream ผ่าน backend ก็ได้ — แนะนำ proxy stream เพื่อคุม auth ที่ endpoint เดียว ไม่ต้อง expose MinIO ตรงๆ ให้ client)

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 URI http://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 แทน:

  1. Task pane เรียก Office.context.ui.displayDialogAsync(keycloakAuthUrl, { height: 60, width: 30 }) เปิด dialog window แยก (ไม่ใช่ iframe) ไปหน้า Keycloak login (Authorization Code + PKCE)
  2. ผู้ใช้ login ใน dialog นั้น → Keycloak redirect กลับมาที่ auth-callback.html (หน้าเล็กๆ ที่รันอยู่ใน dialog)
  3. auth-callback.html เรียก Office.context.ui.messageParent(JSON.stringify({ code, state })) ส่ง authorization code กลับไปที่ task pane แล้วปิด dialog เอง (Office.context.ui.messageParent + window.close())
  4. Task pane รับ code → แลก token ที่ Keycloak token endpoint ด้วย PKCE verifier (ไม่ต้องพึ่ง backend ช่วยแลก เพราะเป็น public client)
  5. เก็บ access_token/refresh_token ไว้ใน memory (JS variable) ของ task pane session ห้ามเก็บใน localStorage (เสี่ยง XSS/ไม่หายเมื่อปิด task pane) — ถ้าต้อง persist ข้าม session ให้ใช้ sessionStorage เท่านั้นและ set อายุสั้น
  6. ก่อนหมดอายุ access token ให้ silent-refresh ด้วย refresh_token ผ่าน token endpoint

Backend (ASP.NET Core)

  • ใช้ Microsoft.AspNetCore.Authentication.JwtBearer config:
    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, ลำดับ
  • ลบ 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 ไฟล์ที่ดาวน์โหลดถูกต้องสมบูรณ์ตั้งแต่ครั้งแรก
  • สร้าง Document record (ผูก 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 เสมอ

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 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 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 ล่าสุด
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)
  • Backend CORS ต้องอนุญาตทั้ง origin ของ add-in (https://localhost:3000) และ Web Portal (http://localhost:5173)

10. Acceptance Criteria

  1. Sideload add-in บน Word Desktop และ Word Online ได้ทั้งคู่ ไม่มี error ตอนโหลด task pane
  2. เปิด task pane ครั้งแรกโดยยังไม่ login → เห็นปุ่ม "Sign in", กดแล้วเปิด dialog ไป Keycloak login page ได้ถูกต้อง, login สำเร็จแล้ว dialog ปิดเองและ task pane ได้ token มาใช้งาน
  3. เรียก backend endpoint โดยไม่แนบ token → ได้ 401 Unauthorized
  4. เปิดไฟล์ .docx ใหม่ที่ไม่เคย track (หลัง login แล้ว) → task pane แสดงปุ่ม "Start Versioning This Document"
  5. กด "Start Versioning" → stamp metadata ครั้งแรก, สร้าง Document record ใน backend, อัปโหลด version 1 สำเร็จ, CreatedByUserId ใน DB ตรงกับ sub/preferred_username จาก JWT ของผู้ login
  6. ปิดแล้วเปิดไฟล์เดิมใหม่ (ทั้งบน Windows และ Mac) → task pane ตรวจพบว่าเป็น tracked document และโหลด version history จาก backend ได้ถูกต้อง
  7. แก้ไขเอกสารแล้วกด "Save & Version" → มี version ใหม่เพิ่มใน backend, LastVersionId/LastVersionTimestamp ใน properties อัปเดตถูกต้อง
  8. ดาวน์โหลด version เก่าจาก panel ได้ไฟล์ที่เปิดด้วย Word ได้ปกติ และเนื้อหาตรงกับตอน upload
  9. ปล่อย session ทิ้งไว้จนกว่า access token หมดอายุ แล้วลองใช้งานต่อ → ต้อง silent-refresh สำเร็จโดยผู้ใช้ไม่รู้สึกสะดุด (ไม่ต้อง login ใหม่)
  10. ทดสอบบน Word for Mac: Custom XML Part อาจ fail แบบ silent ตามที่ระบุใน spec — ต้องมี fallback ที่ระบบยังทำงานถูกต้องแม้ XML part เขียนไม่สำเร็จ (เพราะ backend คือ source of truth)
  11. (v2) upload template .docx ที่มี content controls ผ่านหน้าเว็บ → ระบบ detect fields อัตโนมัติ + บันทึก field definitions ได้
  12. (v2) กรอก form บนเว็บสร้างเอกสาร → ดาวน์โหลด .docx ที่ค่าครบถูกต้อง formatting ไม่พัง → เปิดใน Word แล้ว add-in รายงานเป็น tracked document เห็น version 1 ทันที โดยไม่ต้องกด Start Versioning
  13. (v2) แก้ค่าใน content control ที่ Word แล้วกด Save & Version → field value ใน DB เปลี่ยนตาม + snapshot ต่อ version ถูกเก็บ
  14. (v2) ลบ content control ออกจาก Word แล้วกด Save & Version → flow ยังสำเร็จ ค่าเดิมของ field นั้นคงอยู่ (mark missing) ไม่ error

11. งานที่ Claude Code ควรทำ

Frontend

  1. Scaffold Office Add-in project ด้วย yo office (task pane, TypeScript, React)
  2. Implement metadataService.ts, fileService.ts, versioningService.ts, authService.ts, apiClient.ts — metadataService รวมการอ่าน content controls เป็น map {tag: text} สำหรับ field sync (FR-11)
  3. authService.ts ต้อง implement: build Keycloak authorization URL + PKCE code_verifier/code_challenge, เปิด Office.context.ui.displayDialogAsync, รับ code กลับผ่าน messageParent, แลก token, เก็บ token in-memory, silent refresh
  4. Implement VersionHistoryPanel.tsx, ConfirmVersionDialog.tsx, และหน้า sign-in state
  5. เขียน auth-callback.html (หน้าเล็กๆ รับ redirect จาก Keycloak แล้ว messageParent กลับ)
  6. เขียน manifest.xml ให้ครบ requirement sets ที่ใช้
  7. เขียน unit test สำหรับ service layer (mock Word.run, fetch, และ mock Keycloak token response)

Backend

  1. Scaffold ASP.NET Core 8 Web API project
  2. Implement AppDbContext, migrations
  3. Config JWT Bearer authentication ให้ validate ผ่าน Keycloak Authority/JWKS ตาม FR-8
  4. Implement DocumentsController ตาม FR-5 ถึง FR-7 พร้อม [Authorize] ทุก endpoint, ดึง userId จาก JWT claims
  5. Implement ObjectStorageService (interface IObjectStorageService) เชื่อมต่อ MinIO ผ่าน AWSSDK.S3 — ตั้ง ServiceURL ชี้ไป MinIO endpoint, ForcePathStyle = true (จำเป็นสำหรับ MinIO)
  6. เขียน realm-export.json สำหรับ Keycloak local dev — pre-config realm word-versioning, client word-addin-officejs (public, PKCE) และ word-addin-vsto (public, PKCE)
  7. เขียน integration test สำหรับ upload/download version flow (รัน MinIO + Keycloak จริงผ่าน testcontainers หรือ docker-compose ตอนเทส, ใช้ Keycloak admin API สร้าง test user + ขอ token มาแนบ request)
  8. เขียน docker-compose.yml สำหรับ local dev: MinIO + Keycloak (import realm-export.json อัตโนมัติ) + DB container + backend
  9. เขียน README อธิบาย local dev setup (sideload add-in, run backend, connection string, เข้า MinIO console, เข้า Keycloak admin console, สร้าง test user แรก)
  10. (v2) Implement TemplatesController + entities Template/TemplateField/DocumentFieldValue + migrations (FR-9) รวม parse SDT tags ตอน upload
  11. (v2) Implement DocxGenerationService + POST /api/documents/generate (FR-10) — แทนที่ SDT ที่ run level + stamp custom properties + สร้าง Document + v1 + field values ใน flow เดียว
  12. (v2) ขยาย upload endpoint ให้รับ fields แล้ว sync DocumentFieldValue + snapshot ต่อ version ตาม missing-field policy (FR-11)
  13. (v2) เขียน integration test ครอบ template detect / generate / field sync (รวมกรณี field หาย → ค่าเดิม + mark missing)

Web Portal (เพิ่มเติม v2)

  1. Scaffold Nuxt.js + TailwindCSS — dev port 5173, darkMode: 'class', useTheme (light/dark toggle + persist + system default)
  2. useAuth (PKCE browser redirect ผ่าน Keycloak client word-versioning-web) + useApi (Bearer + silent refresh)
  3. หน้าจัดการ template: list / upload (โชว์ fields ที่ detect อัตโนมัติ) / แก้ field definitions / deactivate
  4. หน้าสร้างเอกสาร: dynamic form สร้างจาก field definitions (input type ตาม field type) + validate + ดาวน์โหลดผลลัพธ์
  5. เขียน unit test ของ form validation + useTheme (Vitest)