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:
parent
b3c1c667f7
commit
70f71f4c5c
23 changed files with 408 additions and 241 deletions
|
|
@ -32,7 +32,9 @@ public static class TestDocx
|
|||
new RunProperties(new Bold()),
|
||||
new Text("[ชื่อพนักงาน]")))),
|
||||
|
||||
// SDT multi-run คนละ formatting — generate ต้องเขียนค่าที่ run แรก (รักษา bold) และลบ text ของ run หลัง
|
||||
// SDT multi-run คนละ formatting — generate เขียนค่าที่ run ข้อความยาวสุด (v9) และลบ text ของอีก run
|
||||
// ⚠️ สอง run ยาวเท่ากัน (8/8 อักขระ) โดยเจตนา: tie → run แรก (bold) ชนะเพราะ OrderByDescending
|
||||
// เสถียร — GenerateTests/DraftTests ใช้จุดนี้เป็น regression guard ของพฤติกรรม tie ห้ามแก้ความยาว
|
||||
new Paragraph(SdtRun("position",
|
||||
new Run(
|
||||
new RunProperties(new Bold()),
|
||||
|
|
@ -83,6 +85,49 @@ public static class TestDocx
|
|||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>template แบบที่พบใน template ไทยจริง (เช่น T14): placeholder แตกเป็น [ / ข้อความไทย / ]
|
||||
/// หลาย run โดย run ข้อความไทยถือ complex script flags (w:hint="cs" + w:cs) ต่างจาก run ปีกกา
|
||||
/// — และมี SDT เปล่าที่มี rPr อยู่ใน sdtPr เท่านั้น (formatting ที่ผู้แต่งกำหนดให้เนื้อหา)</summary>
|
||||
public static byte[] BuildThaiTemplateDocx()
|
||||
{
|
||||
RunFonts ThaiFonts(bool hintCs) => new()
|
||||
{
|
||||
Ascii = "TH SarabunPSK",
|
||||
HighAnsi = "TH SarabunPSK",
|
||||
ComplexScript = "TH SarabunPSK",
|
||||
Hint = hintCs ? FontTypeHintValues.ComplexScript : null,
|
||||
};
|
||||
|
||||
// แบบ T14 หน้ารับส่ง: [ / รายละเอียด / ] — ค่าที่แทนที่ต้องได้ formatting ของ run ข้อความไทย
|
||||
var detail = new Paragraph(new SdtRun(
|
||||
new SdtProperties(new Tag { Val = "detail1" }),
|
||||
new SdtContentRun(
|
||||
new Run(
|
||||
new RunProperties(ThaiFonts(false), new FontSize { Val = "36" }),
|
||||
new Text("[")),
|
||||
new Run(
|
||||
new RunProperties(ThaiFonts(true), new FontSize { Val = "36" }, new ComplexScript()),
|
||||
new Text("รายละเอียด")),
|
||||
new Run(
|
||||
new RunProperties(ThaiFonts(false), new FontSize { Val = "36" }),
|
||||
new Text("]")))));
|
||||
|
||||
// SDT เปล่า: formatting อยู่ที่ sdtPr/rPr เท่านั้น — run ใหม่ต้องสืบทอดมา ไม่ใช่ default font
|
||||
var remark = new Paragraph(new SdtRun(
|
||||
new SdtProperties(
|
||||
new RunProperties(ThaiFonts(true), new FontSize { Val = "36" }),
|
||||
new Tag { Val = "remark1" }),
|
||||
new SdtContentRun()));
|
||||
|
||||
var buffer = new MemoryStream();
|
||||
using (var doc = WordprocessingDocument.Create(buffer, WordprocessingDocumentType.Document))
|
||||
{
|
||||
var main = doc.AddMainDocumentPart();
|
||||
main.Document = new Document(new Body(detail, remark));
|
||||
}
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>อ่านข้อความรวมของ SDT ตาม tag (คืน "" เมื่อไม่มี tag นั้นในไฟล์) — ตรวจผลลัพธ์ generate</summary>
|
||||
public static string ReadSdtText(byte[] docx, string tag)
|
||||
{
|
||||
|
|
@ -109,7 +154,7 @@ public static class TestDocx
|
|||
}
|
||||
|
||||
/// <summary>อ่านข้อความ+formatting ของแต่ละ run ใน SDT (ตาม tag) — ใช้ตรวจว่า generate
|
||||
/// รวม multi-run เป็น run เดียวแล้วยังรักษา run properties (bold/italic) ของ run แรกไว้</summary>
|
||||
/// รวม multi-run เหลือ run ที่ถือค่าแล้วยังรักษา run properties (bold/italic) ของ run นั้นไว้ (v9: run ข้อความยาวสุด)</summary>
|
||||
public static List<(string Text, bool Bold, bool Italic)> ReadSdtRuns(byte[] docx, string tag)
|
||||
{
|
||||
using var doc = WordprocessingDocument.Open(new MemoryStream(docx), false);
|
||||
|
|
@ -140,6 +185,36 @@ public static class TestDocx
|
|||
?.VTLPWSTR?.Text;
|
||||
}
|
||||
|
||||
/// <summary>ข้อมูล run หนึ่งตัวใน SDT (ตาม tag): ข้อความรวม + hint=cs ของ rFonts + flag complex script (w:cs)
|
||||
/// + ฟอนต์ ascii — ใช้ตรวจว่าค่าที่แทนที่ได้ formatting ของ run ข้อความไทยจริง (FR-16 v9)</summary>
|
||||
public record SdtRunInfo(string Text, bool CsHint, bool ComplexScript, string? AsciiFont);
|
||||
|
||||
public static List<SdtRunInfo> ReadSdtRunInfo(byte[] docx, string tag)
|
||||
{
|
||||
using var doc = WordprocessingDocument.Open(new MemoryStream(docx), false);
|
||||
var main = doc.MainDocumentPart!;
|
||||
foreach (var root in new OpenXmlPartRootElement?[] { main.Document }
|
||||
.Concat(main.HeaderParts.Select(h => (OpenXmlPartRootElement?)h.Header))
|
||||
.Concat(main.FooterParts.Select(f => (OpenXmlPartRootElement?)f.Footer)))
|
||||
{
|
||||
if (root is null) continue;
|
||||
foreach (var sdtTag in root.Descendants<Tag>())
|
||||
{
|
||||
if (sdtTag.Val?.Value != tag) continue;
|
||||
var sdt = sdtTag.Parent?.Parent;
|
||||
var content = sdt?.ChildElements.FirstOrDefault(c => c.LocalName == "sdtContent");
|
||||
return content?.Descendants<Run>()
|
||||
.Select(run => new SdtRunInfo(
|
||||
string.Concat(run.Descendants<Text>().Select(t => t.Text)),
|
||||
run.RunProperties?.RunFonts?.Hint?.Value == FontTypeHintValues.ComplexScript,
|
||||
run.RunProperties?.ComplexScript is not null,
|
||||
run.RunProperties?.RunFonts?.Ascii?.Value))
|
||||
.ToList() ?? [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private static SdtRun SdtRun(string tag, params Run[] runs) => new(
|
||||
new SdtProperties(new Tag { Val = tag }),
|
||||
new SdtContentRun(runs));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue