eCourt-Doc-Edit/CLAUDE.md
Suphonchai Phoonsawat ee4d8573ee Phase 12 v9.2: download-first + backend open-links (Word 16.101+ broke ms-word)
การทดสอบ 6 รอบบนเครื่องจริง (Mac, Word 16.101) พิสูจน์ว่า ms-word:ofe
ถูกเมินแบบไม่ deterministic ทุกรูปแบบ (http/https/query/ไม่มี query/
file:// — Word ไม่ยิง request ถึง server เลย เด้งหน้าเลือกไฟล์แทน)
open -a Word <local file> ใช้ได้แต่เรียกจากเว็บไม่ได้ (sandbox +
backend รัน server กลางใน production) → สรุปทางเดียวที่ deterministic
คือ download-first

- Backend: OpenLinkService (HMAC อายุสั้น ValidateOnStart) +
  OpenController /api/open/{t}/{e}/v/{did}/{vid}/file.docx และ /d/{objectKey}
  — ทุกอย่างอยู่ใน path ไม่มี query string (Word เมิน URL มี query),
  [AllowAnonymous] ด้วยลายเซ็นแทน Bearer, stream จาก MinIO + disposition
  ชื่อไทยเหมือนเดิม · download-link + generate-draft ออก open-link
  แทน presigned ของ MinIO (http) · ลบ dead code presigned ทิ้ง
- Web: openDocxUrl = ดาวน์โหลดทันทีทุกครั้ง + ยิง ms-word ควบคู่แบบ
  best-effort + notice "ไฟล์อยู่ Downloads" — เอา heuristic สลับแอป
  ออก (โดนหน้าเลือกไฟล์ของ Word หลอกจนไม่ดาวน์โหลด)
- Spec v9.2 + ตารางทดสอบเครื่องจริงใน acceptance-review + CLAUDE.md
  กฎ 13 (ms-word = best-effort เท่านั้น) + ทางเลือก helper อนาคต
- backend 86 / web 58 / add-in 66 + build ครบ; QA reviewer PASS
  (1 round, minors 4 แก้ครบ)

หมายเหตุ deploy: ต้องตั้ง OpenLinks__Secret (ค่าลับจริง) และ
OpenLinks__BaseUrl (https domain ของ backend) — fail ดังตั้งแต่สตาร์ทถ้าไม่ตั้ง

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 13:39:23 +07:00

16 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

โปรเจกต์นี้คืออะไร

ระบบ Word Versioning (ชื่อที่แสดงใน UI: e-Court Documents Versioning): จัดการ version history ของไฟล์ .docx — สร้างเอกสารจาก template ผ่านเว็บ, ตรวจจับ tracked document ใน Word, บันทึก version ผ่านปุ่ม Save & Version และ sync ค่า fields กลับ DB ประกอบด้วย 3 ส่วน:

ส่วน Stack หน้าที่
/addin Office.js + TypeScript + React + Fluent UI (yo office) Task pane ใน Word Desktop/Online — detect, Save & Version, history, field extraction
/backend ASP.NET Core 8 + EF Core + PostgreSQL + MinIO (AWSSDK.S3) + OpenXML Version API, template system, docx generation, JWT validation
/web Nuxt.js + TailwindCSS (light/dark theme) จัดการ template + กรอก form สร้างเอกสารเป็น version 0 ฉบับตั้งต้น

Infra ทั้งหมดรันผ่าน docker-compose.yml: PostgreSQL + MinIO + Keycloak (OIDC, 3 public clients พร้อม PKCE: word-addin-officejs, word-versioning-web, word-addin-vsto)

สถานะปัจจุบัน: Phase 0–12 — FR-16 ไฟล์ร่าง: automated QA PASS (backend 86 / add-in 66 / web 58 + build ครบ · v9.2: download-first + open-link ของ backend), ค้าง manual บน Word จริง · Phase 0–11 เสร็จครบ (FR-15 ผ่าน QA + manual บน Word for Mac 2026-09-08) — ค้างยืนยันบน Word for Windows / Word on the web ตามรายการใน docs/acceptance-review.md

เอกสารหลัก (อ่านก่อนเริ่มงานใดๆ)

  • specs/spec-officejs.md — source of truth ของ requirements (FR-1–16, acceptance criteria 1–19, data models, platform constraints)
  • docs/plans/officejs-implementation-plan.md — implementation plan แบ่ง 13 phases (0–12) พร้อม checklist, DoD, dependency ระหว่าง phase และตารา mapping Phase↔FR↔AC
  • docs/acceptance-review.md — ผลตรวจ AC 1–19 พร้อมหลักฐาน (test ไหนคุ้มข้อไหน) + รายการที่เหลือต้องยืนยันด้วยมือ

เมื่อ requirement เปลี่ยน: แก้ spec ก่อน แล้ว sync ไปยัง plan

กฎที่ห้ามละเมิด (ข้อจำกัดแพลตฟอร์มจริง — สรุปจาก spec ส่วน 2 และ 9)

  1. ห้ามออกแบบ flow ที่ดัก native save (Ctrl+S / File>Save) — Office.js ไม่มี before-save event ที่ cancel ได้ ใช้ปุ่มใน task pane เท่านั้น
  2. Custom Document Properties = primary flag ตรวจจับ tracked document; Custom XML Part = secondary เท่านั้น (ต้อง delete แล้ว add ใหม่ทุกครั้ง, มี known bug บน Mac บาง build — ต้อง catch แบบไม่ throw)
  3. ห้าม redirect iframe ของ task pane ไป Keycloak ตรงๆ — ต้องใช้ Office Dialog API (displayDialogAsync + messageParent); ส่วน /web เป็น top-level page ใช้ OIDC redirect ปกติ
  4. Token เก็บใน memory เท่านั้น — ห้าม localStorage · ของ /web ที่ persist ได้มีเฉพาะ theme preference (localStorage) และ refresh token + PKCE verifier (sessionStorage อายุสั้น — จำเป็นเพื่อข้าม redirect/reload ตามที่แผนอนุญาต)
  5. Backend คือ source of truth ของ version history และ field values
  6. Field key = content control (SDT) tag ใช้ค่าเดียวกันทั้ง generate (backend) / extract (add-in) / sync (DB)
  7. Docx generation + metadata stamping ทำฝั่ง backend เท่านั้น (OpenXML, แก้ที่ run level ใน SDT เพื่อรักษา formatting)
  8. Ports: add-in = https://localhost:3000, web = http://localhost:5173 (ห้ามชนกัน), backend CORS ต้องอนุญาตทั้งคู่
  9. Config (DB/MinIO/Keycloak) แยกเป็น env/appsettings เท่านั้น — ห้าม hardcode; userId เอาจาก JWT claims เท่านั้น ห้ามรับจาก client
  10. ทุก Office.js call ต้อง catch — ห้ามปล่อย unhandled promise rejection
  11. Dev: ทุก service ที่ task pane (หน้า https) เรียกต้องเป็น HTTPS ด้วย cert ของ office-addin-dev-certs — Keycloak = https://localhost:8443 (Office Dialog API ปฏิเสธ http:// โดยสมบูรณ์), backend = https://localhost:7232 + http://localhost:5075 (WKWebView บล็อก mixed content → "Load failed") — รัน npx office-addin-dev-certs install ก่อน docker compose up
  12. ดาวน์โหลดไฟล์จาก add-in ใช้ลิงก์เปิดอายุสั้นจาก download-link (open-link ของ backend) + Office.context.ui.openBrowserWindow เท่านั้น — webview ของ Word ทิ้ง blob download (a[download]) แบบเงียบๆ ไฟล์ไม่ถูกเขียนที่ไหนเลย; และห้าม cp ทับ manifest ใน wef folder (npm start ใช้ symlink — ทับแล้วครั้งหน้า EEXIST, ให้แก้ที่ addin/manifest.xml ต้นทาง)
  13. ms-word:ofe ใช้เป็น best-effort เท่านั้น — ห้ามพึ่งเป็นกลไกหลัก (v9.2): Word for Mac 16.101+ (ก.ย. 2026) เมิน URL ทุกรูปแบบ (http/https/query/ไม่มี query/file://) โดยเด้งหน้าเลือกไฟล์และไม่ยิง request ถึง server เลย — การเปิดไฟล์จากเว็บต้อง download-first (ดาวน์โหลดทันที + ยิง ms-word ควบคู่) · ลิงก์เปิดไฟล์ใช้ open-link ของ backend (/api/open/{t}/{e}/... HMAC ใน path ไม่มี query string — ฝั่ง MinIO presigned เป็น http ใช้กับ Word ไม่ได้) · prod ต้องตั้ง OpenLinks__Secret/OpenLinks__BaseUrl (https)
  14. slice.data จาก getSliceAsync มีหลายรูปแบบตาม host (ArrayBuffer / view / base64 string / number[]) — ต้องผ่าน fileService.sliceDataToBytes() เสมอ ห้าม assume รูปเดียว
  15. @nuxtjs/tailwindcss บน Nuxt 4 ต้องระบุ cssPath: '~/assets/css/tailwind.css' ใน nuxt.config.ts เสมอ — default แบบ relative resolve ต่อ rootDir แล้ว fallback ไปไฟล์ default ของ tailwindcss package แบบเงียบ (token/component classes ทั้งหมดหายจาก build โดยไม่มี error) · แก้ tailwind.config.ts แล้วต้อง restart dev server · สีทั้งหมดเป็น CSS variables RGB triplet ใน tailwind.css — ห้ามใส่ hex (break /opacity utilities เงียบๆ)

สถาปัตยกรรม — flow หลักที่ต้องเข้าใจ

Create → v0 (FR-10): web form → POST /api/documents/generate → OpenXML แทนที่ค่าใน SDT + stamp SavedByAddIn/DocumentTrackingId ลงไฟล์ → สร้าง Document + DocumentFieldValue + upload v0 → ผู้ใช้โหลดไฟล์ไปเปิดใน Word → add-in ตรวจพบ tracked doc ทันทีเห็น v0

Save & Version (FR-2): ปุ่มใน task pane → dialog ยืนยันพร้อมช่อง commit message ทางเลือก (FR-13) → stamp properties → อัปเดต XML part (delete-then-add, fail ได้แบบ silent) → document.save() → getFileAsync → POST version พร้อม fields map จาก content controls (FR-11) + message → backend update DocumentFieldValue (latest) + FieldsSnapshotJson (ต่อ version)

Version message (FR-13): commit message ทางเลือกต่อ version (≤2048, ว่าง/whitespace → null + trim) รับจากทุก flow ที่สร้าง version (upload multipart + generate JSON) — เก็บที่ DocumentVersion.Message เท่านั้น ไม่ฝังลงไฟล์ docx (backend คือ source of truth) · แสดงใน history ทุกหน้า (web ตาราง+การ์ดมือถือ, add-in panel) · เกิน 2048 → 400 ตรวจก่อนแตะ MinIO

Field sync policy (FR-11): field ขาด (ผู้ใช้ลบ control) → เก็บค่าเดิม + mark missing ไม่ fail request; ส่งมาเกินนิยาม → ignore

เอกสารของ template (FR-12): GET /api/templates/{id}/documents (รายการต่อ template) + GET /api/documents/{id} (detail + ค่า fields ล่าสุด — merge นิยาม × ค่าแบบรองรับ orphan สองทาง) · re-generate ทำฝั่งเว็บ: ฟอร์มสร้างเอกสาร prefilled จากค่าปัจจุบัน (?from={documentId}) แล้วผ่าน endpoint generate เดิม → เอกสารใหม่ v0 เอกสารต้นทางไม่ถูกแตะ

MinIO: bucket docx-versions (snapshot ต่อ version, key {documentId}/v{N:D4}_{clientVersionId}.docx) + docx-templates (ไฟล์ template) — private ทั้งคู่, เข้าถึงผ่าน backend เท่านั้น (proxy stream หรือ presigned URL อายุสั้นจาก download-link — ซึ่ง add-in ใช้เปิดในเบราว์เซอร์ของเครื่อง)

Template: template.docx author ด้วย Word (ใส่ content controls) → upload → backend parse SDT tags → admin ตั้ง label/type/editor/required → delete = deactivate (soft) เพราะ document เก่ายังอ้างอยู่

Field editor (FR-14): TemplateField.Editor (nullable, null = default ของ type: text→textbox, number→number, date→date; text เลือก textarea ได้) — presentation metadata ล้วน: คุมแค่ control ที่ฟอร์มสร้างเอกสาร render (web/app/utils/fieldEditor.ts), validation ของค่ายังยึด Type, ค่า field เป็น string ทั้ง flow ไม่แตะ generate/sync/docx/add-in · ค่า = default → DB เก็บ null แต่ API คืน resolved เสมอ

คำสั่ง (ใช้ได้หลัง scaffold ใน Phase 0)

docker compose up -d                                # infra: PostgreSQL + MinIO + Keycloak(https:8443)
python3 scripts/dev-token.py                        # token PKCE จริงสำหรับทดสอบ backend/Swagger (admin-cli ไม่มี aud ใช้ไม่ได้)

# backend (http://localhost:5075 + https://localhost:7232 — dev เปิด https อัตโนมัติด้วย dev cert)
cd backend/WordVersioning.Api
dotnet run                                          # API + swagger (ผ่าน launchSettings — เปิด browser ให้)
ASPNETCORE_ENVIRONMENT=Development ASPNETCORE_URLS=http://localhost:5075 dotnet run --no-launch-profile   # แบบไม่เปิด browser — ต้องใส่ env เอง ไม่งั้นไปอ่าน appsettings.json (DB password ว่าง) → 500
dotnet ef migrations add <Name>

# backend tests (integration — ต้องมี Docker รันอยู่: Testcontainers ยก Postgres+MinIO+Keycloak เองทุก run ~1 นาที)
cd backend
dotnet test                                                       # slnx ครอบทั้ง API + tests (60 tests)
dotnet test --filter "FullyQualifiedName~GenerateTests"           # รัน test เดี่ยว

# add-in (https://localhost:3000)
cd addin
npx office-addin-dev-certs install                  # ครั้งแรกครั้งเดียว — ทำก่อน docker compose up
npm run build                                       # webpack build
npm start                                           # dev server — sideload ผ่าน manifest (symlink)
npm test
npx jest src/services/metadataService.test.ts       # รัน test เดี่ยว

# web portal (http://localhost:5173 — ห้ามใช้ 3000)
cd web
npm run dev
npm test
npx vitest run test/useTheme.spec.ts                # รัน test เดี่ยว

Version PDF (FR-15 / spec v7)

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

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

ไฟล์ร่าง (FR-16 / spec v8 · ปรับ v9)

ปุ่ม "สร้างไฟล์ร่าง" บนฟอร์มเดิม → POST /api/templates/{id}/generate-draft — ไม่สร้าง Document/version ใน DB; generate แล้วเก็บชั่วคราวใน bucket docx-versions ใต้ prefix drafts/ (lifecycle ลบรายวัน — Minio__DraftRetentionDays, backend ตั้ง rule ตอน startup) แล้วคืน open-link อายุสั้นของ backend (HMAC ใน path — ดูกฎ 13) ให้เว็บ download-first ทันที + ยิง ms-word:ofe ควบคู่ (useVersionOpen.openDocxUrl) — validate ชุดเดียวกับ generate, ไม่มี message. ไฟล์ถูก stamp custom property TemplateId ตัวเดียว (ไม่มี SavedByAddIn/DocumentTrackingId) จึงเป็น untracked ในสายตา add-in. เมื่อกด Start Versioning, add-in อ่าน TemplateId แล้วแนบ form field templateId (ใช้เฉพาะตอน auto-create Document; validate ทุกครั้งที่ส่ง: ไม่ใช่ GUID/ไม่พบ → 400, deactivated → ผูกได้) → Document ผูก template + v1 (ไม่มี v0) + field sync เต็มระบบ. การแทนที่ค่าเขียนที่ run ข้อความ placeholder จริง (run ที่ถือ w:hint="cs"+w:cs — template ไทยแตก placeholder เป็น [/ข้อความ/] หลาย run) และ SDT เปล่าสืบทอด rPr จาก sdtPr ไม่งั้น Word ตัดคำไทยผิด. การตัดสินใจ: docs/adr/0002-draft-stateless.md.