eCourt-Doc-Edit/CLAUDE.md
Suphonchai Phoonsawat 4f54af4e24 v9.2.1: re-enable ms-word on all platforms + real filenames in URLs
User clarified: with the new open-link URLs (https + no query string +
ending .docx), pressing the button DID open the file in Word 16.101+
on their Mac — only the OLD URL forms (http, query strings) were
ignored. The previous disable-on-Mac was based on terminal tests that
hit the old forms / cold-start races, so it removed working behavior.

- useVersionOpen: fireWordScheme = openInWord !== 'off' (default ''
  and 'on' fire on ALL platforms; only explicit 'off' is download-only)
- Notice updated: "กำลังเปิดไฟล์ใน Word ให้อัตโนมัติ — หากไม่เปิด
  ไฟล์อยู่ที่โฟลเดอร์ Downloads แล้ว" (covers cold-start drop)
- Docs corrected with v9.2.1 header (old forms were the broken ones;
  new form confirmed working via real usage) + correction markers on
  the v9.2 test record + CLAUDE.md rule 13 rewritten
- web 60/60 + build; QA reviewer FAIL round 1 (5 doc/comment minors,
  all fixed) → PASS round 2

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:33:12 +07:00

106 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 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 89 / add-in 66 / web 60 + 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. **URL สำหรับ `ms-word:ofe` ต้องเป็น https + ไม่มี query string + จบ .docx/.pdf** (v9.2.1): Word for Mac 16.101+ (ก.ย. 2026) เมิน URL แบบเก่า (http เช่น presigned ของ MinIO, หรือมี query string) — แต่ open-link ของ backend แบบ `/api/open/{t}/{e}/.../{fileName}` ใช้ได้จริงแม้ Word 16.101+ (ยืนยันจากการใช้งานจริง) · เว็บยิง ms-word ให้ Word เปิดไฟล์ + ดาวน์โหลดสำรองทันทีทุกครั้ง (`NUXT_PUBLIC_OPEN_IN_WORD=off` ปิดได้) · cold-start ครั้งแรกของ Word URL event อาจหลุด (เด้งหน้าเลือกไฟล์) — ไฟล์สำรองอยู่ Downloads แล้วเสมอ · 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)
```bash
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
# ⚠️ sideload บน Mac: ปิด Word ทุกหน้าต่าง (Cmd+Q) ก่อน `npm start` — Word อ่าน manifest ตอน
# startup เท่านั้น ถ้าเปิดค้างไว้ manifest ใหม่จะไม่ถูกโหลด (ต้อง stop/start) · ปล่อย dev server
# รันทิ้งไว้ตลอด session (ถ้าตาย task pane โหลดไม่ได้) · แก้โค้ด add-in ใช้ hot reload ได้เลยไม่ต้อง restart
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`.