Phase 12 v9: draft opens in Word + Thai word-breaking fix (FR-16)

แก้ตามผลใช้งานจริง 2 ประเด็น:
1. เปิดใน Word ทันที — generate-draft เปลี่ยนจากคืนไฟล์ตรงๆ เป็นเก็บ
   ชั่วคราวใต้ drafts/ (lifecycle ลบรายวัน, config Minio__DraftRetentionDays)
   + คืน presigned URL ให้เว็บเปิดผ่าน ms-word:ofe พร้อมลิงก์สำรอง
   (useVersionOpen.openDocxUrl แยกแกนเปิด docx URL ใช้ร่วมกับ flow v0)
2. ตัดคำไทย — T14 แตก placeholder เป็น [ / ข้อความ / ] โดย run ข้อความ
   ถือ w:hint="cs" + w:cs เท่านั้น; เดิมเขียนค่าลง run ปีกกาทำให้ Word
   จัดหน้าไทยผิด — แก้เป็นเขียนที่ run ข้อความยาวสุด + SDT เปล่าสืบทอด
   rPr จาก sdtPr (ใช้ร่วมทั้ง generate และ generate-draft)

- ถอดของที่เลิกใช้: CORS expose Content-Disposition, downloadFile init,
  useApi.spec · Spec v9 + ADR 0002 Amendment + acceptance-review AC-19
- Tests: backend 84 / web 56 + build; QA reviewer PASS (2 rounds, 10 findings
  แก้ครบ — รวม bug จริงที่ test ใหม่จับได้: lifecycle Rules null ทำ startup
  ตั้ง rule ล้มแบบเงียบ) · ค้าง manual บน Word จริง

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Suphonchai Phoonsawat 2026-09-10 11:49:16 +07:00
parent b3c1c667f7
commit 70f71f4c5c
23 changed files with 408 additions and 241 deletions

View file

@ -5,8 +5,8 @@
import type { DocumentDetail, GenerateDocumentResponse, TemplateDetail } from '~/types/api'
const route = useRoute()
const { apiJson, downloadFile } = useApi()
const { openingVersionId, downloadError, fallbackUrl, openNotice, openVersion, resetOpen } = useVersionOpen()
const { apiJson } = useApi()
const { openingVersionId, downloadError, fallbackUrl, openNotice, openVersion, openDocxUrl, resetOpen } = useVersionOpen()
const templateId = computed(() => String(route.params.templateId))
const detail = ref<TemplateDetail | null>(null)
@ -82,7 +82,7 @@ async function load() {
/** validate ฝั่ง client (required/type) ก่อนยิง backend — backend ตรวจซ้ำอีกชั้นเสมอ */
async function submit() {
submitError.value = null
downloadError.value = null
resetOpen() // เคลียร์ state ของการเปิดไฟล์ครั้งก่อน (รวม fallbackUrl/openNotice ของไฟล์ร่างที่อาจค้าง)
draftSuccess.value = null
draftError.value = null
issues.value = Object.fromEntries(
@ -105,7 +105,8 @@ async function submit() {
}
}
/** FR-16 — สร้างไฟล์ร่าง (draft): validate ชุดเดียวกันแล้ว POST ค่าไปขอไฟล์กลับมาดาวน์โหลดทันที
/** FR-16 — สร้างไฟล์ร่าง (draft): validate ชุดเดียวกันแล้ว POST ค่าไปขอ presigned URL
* แล้วเปิดใน Word ทันทีผ่าน ms-word:ofe พร้อมลิงก์ดาวน์โหลดสำรอง (เหมือน flow เปิด v0)
* backend ไม่สร้าง Document/version — เอกสารจะเกิดตอนกด Start Versioning ใน Word (เป็น v1 พร้อมผูก template นี้) */
async function submitDraft() {
draftSuccess.value = null
@ -118,13 +119,17 @@ async function submitDraft() {
draftSubmitting.value = true
try {
await downloadFile(`/api/templates/${templateId.value}/generate-draft`, `${detail.value?.name ?? 'เอกสาร'}.docx`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ values: values.value }),
})
const link = (await apiJson<{ url: string; expiresAt: string }>(
`/api/templates/${templateId.value}/generate-draft`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ values: values.value }),
},
))!
openDocxUrl(link.url)
draftSuccess.value =
'ดาวน์โหลดไฟล์ร่างแล้ว — เปิดไฟล์ใน Word แล้วกด Start Versioning This Document เพื่อเริ่ม track (เอกสารจะถูกผูกกับ template นี้และบันทึกเป็น version 1)'
'กำลังเปิดไฟล์ร่างใน Word — เปิด task pane ของ add-in แล้วกด Start Versioning This Document เพื่อเริ่ม track (เอกสารจะถูกผูกกับ template นี้และบันทึกเป็น version 1)'
} catch (err) {
draftError.value = err instanceof Error ? err.message : 'สร้างไฟล์ร่างไม่สำเร็จ'
} finally {
@ -301,8 +306,13 @@ onMounted(load)
<AlertBox v-if="submitError" type="error">{{ submitError }}</AlertBox>
<!-- FR-16 — ไฟล์ร่าง: ดาวน์โหลดสำเร็จแล้วฟอร์มอยู่ต่อ ผู้ใช้เปิดไฟล์ใน Word แล้วค่อย Start Versioning -->
<!-- FR-16 — ไฟล์ร่าง: เปิดใน Word แล้วฟอร์มอยู่ต่อ พร้อมลิงก์สำรอง (อายุสั้นตาม presigned URL) -->
<AlertBox v-if="draftSuccess" type="success">{{ draftSuccess }}</AlertBox>
<AlertBox v-if="fallbackUrl && (draftSuccess || openNotice)">
<p v-if="openNotice">{{ openNotice }}</p>
<a :href="fallbackUrl" target="_blank" rel="noopener noreferrer" class="link">ดาวน์โหลดไฟล์ร่างสำรอง (.docx)</a>
<p class="text-xs text-muted">ลิงก์มีอายุสั้น หากหมดอายุให้กดสร้างไฟล์ร่างอีกครั้ง</p>
</AlertBox>
<AlertBox v-if="draftError" type="error">{{ draftError }}</AlertBox>
<div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
@ -323,7 +333,7 @@ onMounted(load)
</button>
</div>
<p class="text-xs text-muted">
ไฟล์ร่าง = ได้ .docx ทันทีโดยยังไม่เกิดเอกสารในระบบ — เริ่ม versioning ตอนเปิดใน Word (version 1)
ไฟล์ร่าง = เปิดใน Word ทันทีโดยยังไม่เกิดเอกสารในระบบ — เริ่ม versioning ตอนกด Start Versioning ใน Word (version 1)
</p>
</form>
</template>