문서 생성 고도화 1차: 네이티브 워드·엑셀·HTML 경로 정렬 및 품질 보강

- Word/Excel/HTML 스킬을 Python 우회 중심에서 AX 네이티브 문서 도구 우선 경로로 재작성했습니다.

- DocumentPlannerTool의 보고서·제안서·분석 문서 아웃라인을 Executive Summary, Business Case, Decision Ask, Appendix 중심의 업무형 구조로 확장했습니다.

- DocumentAssemblerTool의 DOCX 조립 경로에서 표·목록·콜아웃·소제목 같은 HTML/Markdown 구조를 더 보존하도록 개선했습니다.

- ExcelSkill에 summary_sheet를 추가해 KPI·핵심 인사이트·후속 과제를 담은 요약 시트를 상세 데이터 시트 앞에 생성할 수 있게 했습니다.

- HtmlSkill에 comparison, roadmap, matrix 구조화 섹션을 추가하고 sections 중심 호출 스키마를 정리했습니다.

- DocumentAssemblerSemanticTests, ExcelSkillSummarySheetTests, HtmlSkillConsultingSectionsTests, DocumentPlannerBusinessDocumentTests를 추가했습니다.

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_phase1\\ -p:IntermediateOutputPath=obj\\verify_doc_phase1\

- 검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter 문서_고도화_테스트_5건 -p:OutputPath=bin\\verify_doc_phase1_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_phase1_tests\
This commit is contained in:
2026-04-14 21:02:08 +09:00
parent 0b6d60e959
commit d9cb02f3c4
14 changed files with 1002 additions and 466 deletions

View File

@@ -0,0 +1,70 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using DocumentFormat.OpenXml.Packaging;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class DocumentAssemblerSemanticTests
{
[Fact]
public async Task ExecuteAsync_ForDocx_ShouldPreserveTablesListsAndCallouts()
{
var workDir = Path.Combine(Path.GetTempPath(), "ax-doc-assemble-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
try
{
var tool = new DocumentAssemblerTool();
var context = new AgentContext
{
WorkFolder = workDir,
Permission = "Auto",
OperationMode = "external",
};
var args = JsonDocument.Parse(
"""
{
"path": "assembled.docx",
"title": "운영 개선 보고서",
"format": "docx",
"sections": [
{
"heading": "1. Executive Summary",
"level": 1,
"content": "<p>핵심 메시지 요약입니다.</p><ul><li>첫 항목</li><li>둘째 항목</li></ul><table><tr><th>구분</th><th>값</th></tr><tr><td>매출</td><td>120</td></tr></table><div class=\"callout-info\"><strong>핵심 메모</strong><p>즉시 조치가 필요합니다.</p></div>"
}
]
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
var outputPath = Path.Combine(workDir, "assembled.docx");
File.Exists(outputPath).Should().BeTrue();
using var doc = WordprocessingDocument.Open(outputPath, false);
var body = doc.MainDocumentPart?.Document.Body;
body.Should().NotBeNull();
body!.Descendants<DocumentFormat.OpenXml.Wordprocessing.Table>().Should().NotBeEmpty();
body.InnerText.Should().Contain("첫 항목");
body.InnerText.Should().Contain("즉시 조치가 필요합니다.");
body.InnerText.Should().Contain("매출");
}
finally
{
try
{
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
}
catch
{
}
}
}
}

View File

@@ -0,0 +1,40 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class DocumentPlannerBusinessDocumentTests
{
[Fact]
public async Task ExecuteAsync_ForProposal_ShouldReturnConsultingProposalStructure()
{
var tool = new DocumentPlannerTool();
var context = new AgentContext
{
WorkFolder = Path.GetTempPath(),
Permission = "Auto",
OperationMode = "external",
};
var args = JsonDocument.Parse(
"""
{
"topic": "2026 운영 혁신 제안서",
"document_type": "proposal",
"target_pages": 8
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
result.Output.Should().Contain("Executive Summary");
result.Output.Should().Contain("Business Case");
result.Output.Should().Contain("Risks & Mitigations");
result.Output.Should().Contain("Decision Ask");
result.Output.Should().Contain("Appendix");
}
}

View File

@@ -0,0 +1,83 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class ExcelSkillSummarySheetTests
{
[Fact]
public async Task ExecuteAsync_WithSummarySheet_ShouldCreateExecutiveSheet()
{
var workDir = Path.Combine(Path.GetTempPath(), "ax-xlsx-summary-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
try
{
var tool = new ExcelSkill();
var context = new AgentContext
{
WorkFolder = workDir,
Permission = "Auto",
OperationMode = "external",
};
var args = JsonDocument.Parse(
"""
{
"path": "review.xlsx",
"sheet_name": "Detail",
"headers": ["항목", "값", "증감"],
"rows": [
["Revenue", "120", "12%"],
["Cost", "80", "-4%"]
],
"summary_sheet": {
"name": "Summary",
"title": "2026 운영 리뷰",
"subtitle": "핵심 KPI와 후속 과제",
"kpis": [
{ "label": "Revenue", "value": "120", "trend": "YoY +12%", "note": "Strong" }
],
"highlights": ["수익성 개선", "비용 효율화"],
"actions": ["우선 과제 확정", "월간 리뷰 체계화"]
}
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
var outputPath = Path.Combine(workDir, "review.xlsx");
File.Exists(outputPath).Should().BeTrue();
using var doc = SpreadsheetDocument.Open(outputPath, false);
var sheets = doc.WorkbookPart?.Workbook.Sheets?.Elements<Sheet>().ToList();
sheets.Should().NotBeNull();
var actualSheets = sheets ?? throw new InvalidOperationException("Workbook sheets were not created.");
actualSheets.Should().HaveCount(2);
actualSheets[0].Name?.Value.Should().Be("Summary");
actualSheets[1].Name?.Value.Should().Be("Detail");
var summaryWorksheet = (WorksheetPart)doc.WorkbookPart!.GetPartById(actualSheets[0].Id!);
var firstCell = summaryWorksheet.Worksheet.Descendants<Cell>().FirstOrDefault(c => c.CellReference == "A1");
firstCell.Should().NotBeNull();
firstCell!.CellValue!.Text.Should().Be("2026 운영 리뷰");
}
finally
{
try
{
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
}
catch
{
}
}
}
}

View File

@@ -0,0 +1,80 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class HtmlSkillConsultingSectionsTests
{
[Fact]
public async Task ExecuteAsync_WithConsultingSections_ShouldRenderAdvancedBlocks()
{
var workDir = Path.Combine(Path.GetTempPath(), "ax-html-consulting-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
try
{
var tool = new HtmlSkill();
var context = new AgentContext
{
WorkFolder = workDir,
Permission = "Auto",
OperationMode = "external",
};
var args = JsonDocument.Parse(
"""
{
"path": "consulting.html",
"title": "전략 리뷰",
"body": "",
"sections": [
{
"type": "comparison",
"title": "옵션 비교",
"items": [
{ "name": "Option A", "summary": "빠른 실행", "pros": "도입이 쉽다", "cons": "확장성 제약", "verdict": "Fast" }
]
},
{
"type": "roadmap",
"title": "실행 로드맵",
"phases": [
{ "title": "Phase 1", "detail": "진단 및 설계", "timeline": "0-30일", "owner": "PMO" }
]
},
{
"type": "matrix",
"title": "우선순위 매트릭스",
"quadrants": [
{ "title": "Quick Wins", "items": ["자동화 과제", "리포트 표준화"] },
{ "title": "Strategic Bets", "items": ["플랫폼 재구성"] }
]
}
]
}
""").RootElement;
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
result.Success.Should().BeTrue();
var html = File.ReadAllText(Path.Combine(workDir, "consulting.html"));
html.Should().Contain("comparison-grid");
html.Should().Contain("roadmap-block");
html.Should().Contain("matrix-grid");
}
finally
{
try
{
if (Directory.Exists(workDir))
Directory.Delete(workDir, true);
}
catch
{
}
}
}
}