?? planner/assembler ??? 2? ??
?? ??: - Word/HTML/Excel ?? ?? ??? PPT ??? planner ?? ??? ? ??? ??? ?? ?? ??? ?????. - ??? ???? document_plan ???? ?? ??? ? ??? xlsx scaffold ??? ?????. ?? ????: - DocumentPlannerTool? format:xlsx ??? ???? summary_sheet + sheets ??? excel_create scaffold? ????? ??????. - DocumentPlannerTool? ?? ?? ??? workbook/tracker/dashboard/scorecard ???? ????? ??????. - DocumentAssemblerTool? DOCX ?? ??? cover_subtitle, TOC, header/footer ??? ???? ?? ???? DOCX ?? ?? ??? ??????. - DocumentAssemblerTool? HTML ?? ??? ArtifactQualityReviewService? ??? score ?? ?? ??? ????? ??????. - kpi-workbook ???? document_plan ??? ??? complex workbook ?? ? planner ??? ??? ? ?? ????. - ?? ?? ???? DocumentPlannerWorkbookScaffoldTests, DocumentAssemblerDocxFeaturesTests? ??????. - README.md? docs/DEVELOPMENT.md? 2026-04-14 22:14 (KST) ?? ?? ??? ??????. ?? ??: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_planning2\\ -p:IntermediateOutputPath=obj\\verify_doc_planning2\\ : ?? 0 / ?? 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentPlannerWorkbookScaffoldTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerBusinessDocumentTests|ExcelSkillExecutiveSummaryLinkTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests" -p:OutputPath=bin\\verify_doc_planning_tests3\\ -p:IntermediateOutputPath=obj\\verify_doc_planning_tests3\\ : ?? 7
This commit is contained in:
12
README.md
12
README.md
@@ -1788,8 +1788,16 @@ MIT License
|
|||||||
- [PptxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PptxSkill.cs)는 `issue_tree`, `before_after`, `decision_matrix`, `risk_heatmap`, `benefit_waterfall`, `operating_model`, `appendix_evidence` 같은 컨설팅형 레이아웃을 생성 전에 네이티브 레이아웃으로 정규화하고, 결과 메시지에 deck planning/quality review 요약을 함께 반환합니다.
|
- [PptxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PptxSkill.cs)는 `issue_tree`, `before_after`, `decision_matrix`, `risk_heatmap`, `benefit_waterfall`, `operating_model`, `appendix_evidence` 같은 컨설팅형 레이아웃을 생성 전에 네이티브 레이아웃으로 정규화하고, 결과 메시지에 deck planning/quality review 요약을 함께 반환합니다.
|
||||||
- [pptx-creator.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/pptx-creator.skill.md)는 `document_plan -> deck brief -> pptx_create` 흐름을 중심으로 재작성했고, [strategy-deck.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/strategy-deck.skill.md), [board-update.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/board-update.skill.md), [pmo-steering.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/pmo-steering.skill.md), [sales-review-deck.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/sales-review-deck.skill.md), [operating-model-deck.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/operating-model-deck.skill.md)를 추가해 목적형 deck 진입점을 늘렸습니다.
|
- [pptx-creator.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/pptx-creator.skill.md)는 `document_plan -> deck brief -> pptx_create` 흐름을 중심으로 재작성했고, [strategy-deck.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/strategy-deck.skill.md), [board-update.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/board-update.skill.md), [pmo-steering.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/pmo-steering.skill.md), [sales-review-deck.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/sales-review-deck.skill.md), [operating-model-deck.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/operating-model-deck.skill.md)를 추가해 목적형 deck 진입점을 늘렸습니다.
|
||||||
- 테스트로 [DeckPlanningServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckPlanningServiceTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs), [PptxSkillAutoRepairTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/PptxSkillAutoRepairTests.cs)를 추가했습니다.
|
- 테스트로 [DeckPlanningServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckPlanningServiceTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs), [PptxSkillAutoRepairTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/PptxSkillAutoRepairTests.cs)를 추가했습니다.
|
||||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\` 경고 0 / 오류 0
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\` 경고 0 / 오류 0
|
||||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckPlanningServiceTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_ppt_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3_tests\\` 통과 5
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckPlanningServiceTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_ppt_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3_tests\\` 통과 5
|
||||||
|
|
||||||
|
업데이트: 2026-04-14 22:14 (KST)
|
||||||
|
- 문서 생성 고도화 2차 planner/assembler 보강을 반영했습니다. `DocumentPlannerTool`은 이제 `format: xlsx`를 직접 지원하고, 문서 개요뿐 아니라 `excel_create`에 바로 넘길 수 있는 `summary_sheet + sheets` 워크북 scaffold를 생성합니다.
|
||||||
|
- `DocumentAssemblerTool`은 DOCX 조립 시 `cover_subtitle`, `toc`, `header`, `footer`를 실제 Word 문서에 반영하도록 확장했고, 결과 메시지에 DOCX/HTML 품질 리뷰 요약을 함께 붙여 산출물 완성도를 바로 확인할 수 있게 했습니다.
|
||||||
|
- `kpi-workbook.skill.md`는 complex workbook 시나리오에서 `document_plan -> excel_create` 경로를 사용할 수 있도록 planner 도구 허용 목록을 확장했습니다.
|
||||||
|
- 테스트로 `DocumentPlannerWorkbookScaffoldTests`, `DocumentAssemblerDocxFeaturesTests`를 추가했고, 기존 문서/워크북/HTML 회귀 테스트와 함께 검증했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_planning2\\ -p:IntermediateOutputPath=obj\\verify_doc_planning2\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentPlannerWorkbookScaffoldTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerBusinessDocumentTests|ExcelSkillExecutiveSummaryLinkTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests" -p:OutputPath=bin\\verify_doc_planning_tests3\\ -p:IntermediateOutputPath=obj\\verify_doc_planning_tests3\\` 통과 7
|
||||||
????: 2026-04-14 22:00 (KST)
|
????: 2026-04-14 22:00 (KST)
|
||||||
- PPT 3? ?? ??? ??????. DeckPlanningService, DeckQualityReviewService, PptxSkill? planning/quality summary ??? ??? PPT ?? ?? ??? ???? ?? ??? ?? ?? ??? ??? ?????.
|
- PPT 3? ?? ??? ??????. DeckPlanningService, DeckQualityReviewService, PptxSkill? planning/quality summary ??? ??? PPT ?? ?? ??? ???? ?? ??? ?? ?? ??? ??? ?????.
|
||||||
- ??: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\ ?? 0 / ?? 0
|
- ??: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\ ?? 0 / ?? 0
|
||||||
|
|||||||
@@ -862,3 +862,13 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
|
|||||||
- 테스트로 `DeckPlanningServiceTests`, `DeckQualityReviewServiceTests`, `PptxSkillAutoRepairTests`를 추가했고, 기존 `PptxSkillConsultingDeckTests`와 함께 검증했습니다.
|
- 테스트로 `DeckPlanningServiceTests`, `DeckQualityReviewServiceTests`, `PptxSkillAutoRepairTests`를 추가했고, 기존 `PptxSkillConsultingDeckTests`와 함께 검증했습니다.
|
||||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\` 경고 0 / 오류 0
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_phase3\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3\\` 경고 0 / 오류 0
|
||||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckPlanningServiceTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_ppt_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3_tests\\` 통과 5
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DeckPlanningServiceTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_ppt_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_phase3_tests\\` 통과 5
|
||||||
|
|
||||||
|
업데이트: 2026-04-14 22:14 (KST)
|
||||||
|
- 문서 planner/assembler 고도화 2차를 반영했습니다. [DocumentPlannerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs)는 `format: xlsx`를 지원하고, 분석/제안 시나리오에서 `summary_sheet + sheets` 구조의 `excel_create` scaffold를 직접 생성합니다.
|
||||||
|
- 같은 파일의 포맷 해석 로직은 `xlsx`, `excel`, `workbook`, `tracker`, `dashboard`, `scorecard` 계열 의도를 먼저 감지해 워크북 경로로 보내도록 보강했습니다.
|
||||||
|
- [DocumentAssemblerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs)는 DOCX 조립 시 `cover_subtitle`, `toc`, `header`, `footer`를 실제 OpenXML 문서에 반영하고, 구조화 HTML을 Word 블록으로 조립한 뒤 품질 리뷰 점수를 함께 반환합니다.
|
||||||
|
- 같은 도구의 HTML 조립 경로도 공통 [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs)와 연결해 score/strengths/issues 기준의 요약을 돌려주도록 정리했습니다.
|
||||||
|
- [kpi-workbook.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/kpi-workbook.skill.md)는 complex workbook 생성 시 planner 경로를 열기 위해 `document_plan`을 허용 도구에 추가했습니다.
|
||||||
|
- 테스트로 [DocumentPlannerWorkbookScaffoldTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentPlannerWorkbookScaffoldTests.cs), [DocumentAssemblerDocxFeaturesTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocumentAssemblerDocxFeaturesTests.cs)를 추가했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_planning2\\ -p:IntermediateOutputPath=obj\\verify_doc_planning2\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "DocumentPlannerWorkbookScaffoldTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|DocumentPlannerBusinessDocumentTests|ExcelSkillExecutiveSummaryLinkTests|HtmlSkillConsultingSectionsTests|DocxSkillTemplateFeaturesTests" -p:OutputPath=bin\\verify_doc_planning_tests3\\ -p:IntermediateOutputPath=obj\\verify_doc_planning_tests3\\` 통과 7
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
using DocumentFormat.OpenXml.Packaging;
|
||||||
|
using DocumentFormat.OpenXml.Wordprocessing;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AxCopilot.Tests.Services;
|
||||||
|
|
||||||
|
public class DocumentAssemblerDocxFeaturesTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_ForDocx_ShouldIncludeCoverTocHeaderAndFooter()
|
||||||
|
{
|
||||||
|
var workDir = Path.Combine(Path.GetTempPath(), "ax-doc-assemble-features-" + 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-features.docx",
|
||||||
|
"title": "Operating Review",
|
||||||
|
"format": "docx",
|
||||||
|
"toc": true,
|
||||||
|
"cover_subtitle": "Q2 Steering Committee",
|
||||||
|
"header": "Internal Use Only",
|
||||||
|
"footer": "AX Copilot {page}",
|
||||||
|
"sections": [
|
||||||
|
{ "heading": "1. Executive Summary", "level": 1, "content": "<p>Summary.</p>" },
|
||||||
|
{ "heading": "2. Current State", "level": 1, "content": "<ul><li>Finding A</li><li>Finding B</li></ul>" },
|
||||||
|
{ "heading": "3. Recommendation", "level": 1, "content": "<div class=\"callout-info\"><strong>Decision</strong><p>Approve phase 1.</p></div>" },
|
||||||
|
{ "heading": "4. Appendix", "level": 1, "content": "<table><tr><th>Metric</th><th>Value</th></tr><tr><td>NPS</td><td>61</td></tr></table>" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""").RootElement;
|
||||||
|
|
||||||
|
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
result.Output.Should().Contain("DOCX 품질 리뷰");
|
||||||
|
|
||||||
|
var outputPath = Path.Combine(workDir, "assembled-features.docx");
|
||||||
|
File.Exists(outputPath).Should().BeTrue();
|
||||||
|
|
||||||
|
using var doc = WordprocessingDocument.Open(outputPath, false);
|
||||||
|
doc.MainDocumentPart.Should().NotBeNull();
|
||||||
|
doc.MainDocumentPart!.HeaderParts.Should().NotBeEmpty();
|
||||||
|
doc.MainDocumentPart.FooterParts.Should().NotBeEmpty();
|
||||||
|
doc.MainDocumentPart.Document.Body!.InnerText.Should().Contain("Q2 Steering Committee");
|
||||||
|
doc.MainDocumentPart.Document.Body.Descendants<FieldCode>().Should().Contain(field => field.Text.Contains("TOC"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(workDir))
|
||||||
|
Directory.Delete(workDir, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AxCopilot.Tests.Services;
|
||||||
|
|
||||||
|
public class DocumentPlannerWorkbookScaffoldTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_ForXlsxFormat_ShouldReturnWorkbookScaffold()
|
||||||
|
{
|
||||||
|
var tool = new DocumentPlannerTool();
|
||||||
|
var context = new AgentContext
|
||||||
|
{
|
||||||
|
WorkFolder = Path.GetTempPath(),
|
||||||
|
Permission = "Auto",
|
||||||
|
OperationMode = "external",
|
||||||
|
};
|
||||||
|
|
||||||
|
var args = JsonDocument.Parse(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"topic": "2026 KPI operating review",
|
||||||
|
"document_type": "analysis",
|
||||||
|
"format": "xlsx",
|
||||||
|
"target_pages": 6
|
||||||
|
}
|
||||||
|
""").RootElement;
|
||||||
|
|
||||||
|
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
result.Output.Should().Contain("excel_create");
|
||||||
|
result.Output.Should().Contain("\"summary_sheet\"");
|
||||||
|
result.Output.Should().Contain("\"sheets\"");
|
||||||
|
result.Output.Should().Contain("Metrics");
|
||||||
|
result.Output.Should().Contain("Actions");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,7 +120,7 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
switch (format)
|
switch (format)
|
||||||
{
|
{
|
||||||
case "docx":
|
case "docx":
|
||||||
resultMsg = AssembleDocx(fullPath, title, sections, headerText, footerText);
|
resultMsg = AssembleDocx(fullPath, title, sections, useToc, coverSubtitle, headerText, footerText);
|
||||||
break;
|
break;
|
||||||
case "markdown":
|
case "markdown":
|
||||||
resultMsg = AssembleMarkdown(fullPath, title, sections);
|
resultMsg = AssembleMarkdown(fullPath, title, sections);
|
||||||
@@ -231,13 +231,15 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
||||||
|
|
||||||
var issues = ValidateBasic(sb.ToString());
|
var issues = ValidateBasic(sb.ToString());
|
||||||
|
var review = ArtifactQualityReviewService.ReviewHtml(title, sb.ToString(), !string.IsNullOrWhiteSpace(coverSubtitle), toc, printReady: true);
|
||||||
|
var reviewSummary = $" HTML 품질 리뷰: {review.Score}/100 | 강점 {review.Strengths.Count} | 이슈 {review.Issues.Count}";
|
||||||
return issues.Count > 0
|
return issues.Count > 0
|
||||||
? $" ⚠ 품질 검증 이슈 {issues.Count}건: {string.Join("; ", issues)}"
|
? $"{reviewSummary} | 기본 검토 이슈 {issues.Count}건: {string.Join("; ", issues)}"
|
||||||
: " ✓ 품질 검증 통과";
|
: reviewSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string AssembleDocx(string path, string title, List<(string Heading, string Content, int Level)> sections,
|
private string AssembleDocx(string path, string title, List<(string Heading, string Content, int Level)> sections,
|
||||||
string? headerText, string? footerText)
|
bool useToc, string? coverSubtitle, string? headerText, string? footerText)
|
||||||
{
|
{
|
||||||
using var doc = DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Create(
|
using var doc = DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Create(
|
||||||
path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
|
path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
|
||||||
@@ -424,6 +426,74 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var includeCover = !string.IsNullOrWhiteSpace(coverSubtitle);
|
||||||
|
if (includeCover)
|
||||||
|
AppendAssemblerCoverPage(body, title, coverSubtitle!);
|
||||||
|
else
|
||||||
|
body.AppendChild(CreateParagraph(title, fontSize: "48", bold: true, color: "1F3A5F"));
|
||||||
|
|
||||||
|
if (!includeCover)
|
||||||
|
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph());
|
||||||
|
|
||||||
|
if (useToc && sections.Count > 3)
|
||||||
|
AppendAssemblerTableOfContents(body);
|
||||||
|
|
||||||
|
var tableCount = 0;
|
||||||
|
var listCount = 0;
|
||||||
|
var calloutCount = 0;
|
||||||
|
var highlightCount = 0;
|
||||||
|
var headings = new List<string>();
|
||||||
|
|
||||||
|
foreach (var (heading, content, level) in sections)
|
||||||
|
{
|
||||||
|
var headPara = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
|
||||||
|
var headRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
||||||
|
headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
||||||
|
{
|
||||||
|
RunFonts = KoreanFonts(),
|
||||||
|
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
||||||
|
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = level <= 1 ? "32" : "28" },
|
||||||
|
Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "2B579A" },
|
||||||
|
});
|
||||||
|
headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(heading));
|
||||||
|
headPara.AppendChild(headRun);
|
||||||
|
body.AppendChild(headPara);
|
||||||
|
headings.Add(heading);
|
||||||
|
|
||||||
|
AppendStructuredContent(content);
|
||||||
|
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph());
|
||||||
|
}
|
||||||
|
|
||||||
|
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.SectionProperties(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.PageSize { Width = 11906, Height = 16838 },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.PageMargin { Top = 1440, Right = 1440, Bottom = 1440, Left = 1440,
|
||||||
|
Header = 720, Footer = 720, Gutter = 0 }
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText))
|
||||||
|
AddAssemblerHeaderFooter(mainPart, body, headerText, footerText, showPageNumbers: true);
|
||||||
|
|
||||||
|
mainPart.Document.Save();
|
||||||
|
|
||||||
|
var review = ArtifactQualityReviewService.ReviewStructuredDocument(new StructuredDocumentReviewInput(
|
||||||
|
title,
|
||||||
|
headings.Count,
|
||||||
|
sections.Sum(section => StripHtmlTags(section.Content).Length),
|
||||||
|
tableCount,
|
||||||
|
listCount,
|
||||||
|
calloutCount,
|
||||||
|
highlightCount,
|
||||||
|
0,
|
||||||
|
includeCover,
|
||||||
|
useToc && sections.Count > 3,
|
||||||
|
false,
|
||||||
|
!string.IsNullOrWhiteSpace(headerText) || !string.IsNullOrWhiteSpace(footerText),
|
||||||
|
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "executive summary", "summary", "요약")),
|
||||||
|
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "recommendation", "proposal", "next step", "action", "권고", "실행")),
|
||||||
|
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "appendix", "reference", "supplement", "부록", "참고"))));
|
||||||
|
|
||||||
|
return $" DOCX 품질 리뷰: {review.Score}/100 | 강점 {review.Strengths.Count} | 이슈 {review.Issues.Count}";
|
||||||
|
|
||||||
void AppendStructuredContent(string rawContent)
|
void AppendStructuredContent(string rawContent)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(rawContent))
|
if (string.IsNullOrWhiteSpace(rawContent))
|
||||||
@@ -437,7 +507,7 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
}
|
}
|
||||||
|
|
||||||
normalized = Regex.Replace(normalized, @"<br\s*/?>", "\n", RegexOptions.IgnoreCase);
|
normalized = Regex.Replace(normalized, @"<br\s*/?>", "\n", RegexOptions.IgnoreCase);
|
||||||
var blockPattern = @"<table\b[^>]*>.*?</table>|<ul\b[^>]*>.*?</ul>|<ol\b[^>]*>.*?</ol>|<blockquote\b[^>]*>.*?</blockquote>|<div\b[^>]*class=""[^""]*(callout-[^""]*|comparison-grid|roadmap-block|matrix-grid)[^""]*""[^>]*>.*?</div>|<h[2-6]\b[^>]*>.*?</h[2-6]>|<p\b[^>]*>.*?</p>";
|
var blockPattern = @"<table\b[^>]*>.*?</table>|<ul\b[^>]*>.*?</ul>|<ol\b[^>]*>.*?</ol>|<blockquote\b[^>]*>.*?</blockquote>|<div\b[^>]*class=""[^""]*(callout-[^""]*|comparison-grid|roadmap-block|matrix-grid|highlight-box)[^""]*""[^>]*>.*?</div>|<h[2-6]\b[^>]*>.*?</h[2-6]>|<p\b[^>]*>.*?</p>";
|
||||||
var matches = Regex.Matches(normalized, blockPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
var matches = Regex.Matches(normalized, blockPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||||
|
|
||||||
if (matches.Count == 0)
|
if (matches.Count == 0)
|
||||||
@@ -459,14 +529,17 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
if (Regex.IsMatch(block, @"^<table", RegexOptions.IgnoreCase))
|
if (Regex.IsMatch(block, @"^<table", RegexOptions.IgnoreCase))
|
||||||
{
|
{
|
||||||
body.AppendChild(CreateTableFromHtml(block));
|
body.AppendChild(CreateTableFromHtml(block));
|
||||||
|
tableCount++;
|
||||||
}
|
}
|
||||||
else if (Regex.IsMatch(block, @"^<ul", RegexOptions.IgnoreCase))
|
else if (Regex.IsMatch(block, @"^<ul", RegexOptions.IgnoreCase))
|
||||||
{
|
{
|
||||||
AppendListBlock(block, ordered: false);
|
AppendListBlock(block, ordered: false);
|
||||||
|
listCount++;
|
||||||
}
|
}
|
||||||
else if (Regex.IsMatch(block, @"^<ol", RegexOptions.IgnoreCase))
|
else if (Regex.IsMatch(block, @"^<ol", RegexOptions.IgnoreCase))
|
||||||
{
|
{
|
||||||
AppendListBlock(block, ordered: true);
|
AppendListBlock(block, ordered: true);
|
||||||
|
listCount++;
|
||||||
}
|
}
|
||||||
else if (Regex.IsMatch(block, @"^<h", RegexOptions.IgnoreCase))
|
else if (Regex.IsMatch(block, @"^<h", RegexOptions.IgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -474,7 +547,12 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
}
|
}
|
||||||
else if (Regex.IsMatch(block, @"^<(blockquote|div)", RegexOptions.IgnoreCase))
|
else if (Regex.IsMatch(block, @"^<(blockquote|div)", RegexOptions.IgnoreCase))
|
||||||
{
|
{
|
||||||
body.AppendChild(CreateParagraph(ExtractStructuredText(block), fontSize: "21", bold: true, color: "1F3A5F", fill: "EDF4FF"));
|
var fill = Regex.IsMatch(block, "highlight-box", RegexOptions.IgnoreCase) ? "FFF7DA" : "EDF4FF";
|
||||||
|
body.AppendChild(CreateParagraph(ExtractStructuredText(block), fontSize: "21", bold: true, color: "1F3A5F", fill: fill));
|
||||||
|
if (Regex.IsMatch(block, "highlight-box", RegexOptions.IgnoreCase))
|
||||||
|
highlightCount++;
|
||||||
|
else
|
||||||
|
calloutCount++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -487,49 +565,6 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
if (cursor < normalized.Length)
|
if (cursor < normalized.Length)
|
||||||
AppendPlainText(ExtractStructuredText(normalized[cursor..]));
|
AppendPlainText(ExtractStructuredText(normalized[cursor..]));
|
||||||
}
|
}
|
||||||
|
|
||||||
var titlePara = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
|
|
||||||
var titleRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
|
||||||
titleRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
|
||||||
{
|
|
||||||
RunFonts = KoreanFonts(),
|
|
||||||
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
|
||||||
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "48" }
|
|
||||||
});
|
|
||||||
titleRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(title));
|
|
||||||
titlePara.AppendChild(titleRun);
|
|
||||||
body.AppendChild(titlePara);
|
|
||||||
|
|
||||||
// 빈 줄
|
|
||||||
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph());
|
|
||||||
|
|
||||||
foreach (var (heading, content, level) in sections)
|
|
||||||
{
|
|
||||||
var headPara = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
|
|
||||||
var headRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
|
||||||
headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
|
||||||
{
|
|
||||||
RunFonts = KoreanFonts(),
|
|
||||||
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
|
||||||
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = level <= 1 ? "32" : "28" },
|
|
||||||
Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "2B579A" },
|
|
||||||
});
|
|
||||||
headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(heading));
|
|
||||||
headPara.AppendChild(headRun);
|
|
||||||
body.AppendChild(headPara);
|
|
||||||
|
|
||||||
AppendStructuredContent(content);
|
|
||||||
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph());
|
|
||||||
}
|
|
||||||
|
|
||||||
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.SectionProperties(
|
|
||||||
new DocumentFormat.OpenXml.Wordprocessing.PageSize { Width = 11906, Height = 16838 },
|
|
||||||
new DocumentFormat.OpenXml.Wordprocessing.PageMargin { Top = 1440, Right = 1440, Bottom = 1440, Left = 1440,
|
|
||||||
Header = 720, Footer = 720, Gutter = 0 }
|
|
||||||
));
|
|
||||||
|
|
||||||
mainPart.Document.Save();
|
|
||||||
return " ✓ DOCX 조립 완료";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함).</summary>
|
/// <summary>DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함).</summary>
|
||||||
@@ -573,6 +608,235 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
return styles;
|
return styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AppendAssemblerCoverPage(DocumentFormat.OpenXml.Wordprocessing.Body body, string title, string subtitle)
|
||||||
|
{
|
||||||
|
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties
|
||||||
|
{
|
||||||
|
Justification = new DocumentFormat.OpenXml.Wordprocessing.Justification
|
||||||
|
{
|
||||||
|
Val = DocumentFormat.OpenXml.Wordprocessing.JustificationValues.Center
|
||||||
|
},
|
||||||
|
SpacingBetweenLines = new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines
|
||||||
|
{
|
||||||
|
Before = "1600",
|
||||||
|
After = "120"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Run(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "44" },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "1F3A5F" },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }),
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Text(title))));
|
||||||
|
|
||||||
|
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties
|
||||||
|
{
|
||||||
|
Justification = new DocumentFormat.OpenXml.Wordprocessing.Justification
|
||||||
|
{
|
||||||
|
Val = DocumentFormat.OpenXml.Wordprocessing.JustificationValues.Center
|
||||||
|
},
|
||||||
|
SpacingBetweenLines = new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines
|
||||||
|
{
|
||||||
|
After = "260"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Run(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "26" },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "5B6472" },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }),
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Text(subtitle))));
|
||||||
|
|
||||||
|
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties
|
||||||
|
{
|
||||||
|
Justification = new DocumentFormat.OpenXml.Wordprocessing.Justification
|
||||||
|
{
|
||||||
|
Val = DocumentFormat.OpenXml.Wordprocessing.JustificationValues.Center
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Run(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "18" },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "808080" },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }),
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Text(DateTime.Now.ToString("yyyy-MM-dd")))));
|
||||||
|
|
||||||
|
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Break
|
||||||
|
{
|
||||||
|
Type = DocumentFormat.OpenXml.Wordprocessing.BreakValues.Page
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendAssemblerTableOfContents(DocumentFormat.OpenXml.Wordprocessing.Body body)
|
||||||
|
{
|
||||||
|
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines { Before = "120", After = "120" }),
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Run(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "28" },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }),
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Text("목차"))));
|
||||||
|
|
||||||
|
var paragraph = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
|
||||||
|
paragraph.Append(new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.FieldChar
|
||||||
|
{
|
||||||
|
FieldCharType = DocumentFormat.OpenXml.Wordprocessing.FieldCharValues.Begin
|
||||||
|
}));
|
||||||
|
paragraph.Append(new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ")
|
||||||
|
{
|
||||||
|
Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve
|
||||||
|
}));
|
||||||
|
paragraph.Append(new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.FieldChar
|
||||||
|
{
|
||||||
|
FieldCharType = DocumentFormat.OpenXml.Wordprocessing.FieldCharValues.Separate
|
||||||
|
}));
|
||||||
|
paragraph.Append(new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text("Word에서 필드 업데이트를 실행하면 목차가 새로 고쳐집니다.")));
|
||||||
|
paragraph.Append(new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.FieldChar
|
||||||
|
{
|
||||||
|
FieldCharType = DocumentFormat.OpenXml.Wordprocessing.FieldCharValues.End
|
||||||
|
}));
|
||||||
|
body.Append(paragraph);
|
||||||
|
body.Append(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Break
|
||||||
|
{
|
||||||
|
Type = DocumentFormat.OpenXml.Wordprocessing.BreakValues.Page
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddAssemblerHeaderFooter(
|
||||||
|
DocumentFormat.OpenXml.Packaging.MainDocumentPart mainPart,
|
||||||
|
DocumentFormat.OpenXml.Wordprocessing.Body body,
|
||||||
|
string? headerText,
|
||||||
|
string? footerText,
|
||||||
|
bool showPageNumbers)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(headerText))
|
||||||
|
{
|
||||||
|
var headerPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.HeaderPart>();
|
||||||
|
var header = new DocumentFormat.OpenXml.Wordprocessing.Header();
|
||||||
|
var paragraph = new DocumentFormat.OpenXml.Wordprocessing.Paragraph(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Run(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunProperties(
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "18" },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "808080" },
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }),
|
||||||
|
new DocumentFormat.OpenXml.Wordprocessing.Text(headerText)));
|
||||||
|
paragraph.ParagraphProperties = new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties
|
||||||
|
{
|
||||||
|
Justification = new DocumentFormat.OpenXml.Wordprocessing.Justification
|
||||||
|
{
|
||||||
|
Val = DocumentFormat.OpenXml.Wordprocessing.JustificationValues.Right
|
||||||
|
}
|
||||||
|
};
|
||||||
|
header.Append(paragraph);
|
||||||
|
headerPart.Header = header;
|
||||||
|
|
||||||
|
var sectionProperties = body.Elements<DocumentFormat.OpenXml.Wordprocessing.SectionProperties>().LastOrDefault()
|
||||||
|
?? body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.SectionProperties());
|
||||||
|
sectionProperties.Elements<DocumentFormat.OpenXml.Wordprocessing.HeaderReference>().ToList().ForEach(reference => reference.Remove());
|
||||||
|
sectionProperties.Append(new DocumentFormat.OpenXml.Wordprocessing.HeaderReference
|
||||||
|
{
|
||||||
|
Type = DocumentFormat.OpenXml.Wordprocessing.HeaderFooterValues.Default,
|
||||||
|
Id = mainPart.GetIdOfPart(headerPart)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(footerText) || showPageNumbers)
|
||||||
|
{
|
||||||
|
var footerPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.FooterPart>();
|
||||||
|
var footer = new DocumentFormat.OpenXml.Wordprocessing.Footer();
|
||||||
|
var paragraph = new DocumentFormat.OpenXml.Wordprocessing.Paragraph
|
||||||
|
{
|
||||||
|
ParagraphProperties = new DocumentFormat.OpenXml.Wordprocessing.ParagraphProperties
|
||||||
|
{
|
||||||
|
Justification = new DocumentFormat.OpenXml.Wordprocessing.Justification
|
||||||
|
{
|
||||||
|
Val = DocumentFormat.OpenXml.Wordprocessing.JustificationValues.Center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var displayText = string.IsNullOrWhiteSpace(footerText) ? "AX Copilot" : footerText!;
|
||||||
|
if (showPageNumbers)
|
||||||
|
{
|
||||||
|
if (displayText.Contains("{page}", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var parts = displayText.Split("{page}", StringSplitOptions.None);
|
||||||
|
paragraph.Append(CreateAssemblerFooterRun(parts[0]));
|
||||||
|
paragraph.Append(CreateAssemblerPageNumberRun());
|
||||||
|
if (parts.Length > 1)
|
||||||
|
paragraph.Append(CreateAssemblerFooterRun(parts[1]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paragraph.Append(CreateAssemblerFooterRun(displayText + " · "));
|
||||||
|
paragraph.Append(CreateAssemblerPageNumberRun());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paragraph.Append(CreateAssemblerFooterRun(displayText));
|
||||||
|
}
|
||||||
|
|
||||||
|
footer.Append(paragraph);
|
||||||
|
footerPart.Footer = footer;
|
||||||
|
|
||||||
|
var sectionProperties = body.Elements<DocumentFormat.OpenXml.Wordprocessing.SectionProperties>().LastOrDefault()
|
||||||
|
?? body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.SectionProperties());
|
||||||
|
sectionProperties.Elements<DocumentFormat.OpenXml.Wordprocessing.FooterReference>().ToList().ForEach(reference => reference.Remove());
|
||||||
|
sectionProperties.Append(new DocumentFormat.OpenXml.Wordprocessing.FooterReference
|
||||||
|
{
|
||||||
|
Type = DocumentFormat.OpenXml.Wordprocessing.HeaderFooterValues.Default,
|
||||||
|
Id = mainPart.GetIdOfPart(footerPart)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DocumentFormat.OpenXml.Wordprocessing.Run CreateAssemblerFooterRun(string text) =>
|
||||||
|
new(new DocumentFormat.OpenXml.Wordprocessing.Text(text) { Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve })
|
||||||
|
{
|
||||||
|
RunProperties = new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
||||||
|
{
|
||||||
|
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "16" },
|
||||||
|
Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "999999" },
|
||||||
|
RunFonts = new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static DocumentFormat.OpenXml.Wordprocessing.Run CreateAssemblerPageNumberRun()
|
||||||
|
{
|
||||||
|
var run = new DocumentFormat.OpenXml.Wordprocessing.Run
|
||||||
|
{
|
||||||
|
RunProperties = new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
||||||
|
{
|
||||||
|
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "16" },
|
||||||
|
Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "999999" },
|
||||||
|
RunFonts = new DocumentFormat.OpenXml.Wordprocessing.RunFonts { Ascii = "맑은 고딕", HighAnsi = "맑은 고딕", EastAsia = "맑은 고딕" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run.Append(new DocumentFormat.OpenXml.Wordprocessing.FieldChar
|
||||||
|
{
|
||||||
|
FieldCharType = DocumentFormat.OpenXml.Wordprocessing.FieldCharValues.Begin
|
||||||
|
});
|
||||||
|
run.Append(new DocumentFormat.OpenXml.Wordprocessing.FieldCode(" PAGE ")
|
||||||
|
{
|
||||||
|
Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve
|
||||||
|
});
|
||||||
|
run.Append(new DocumentFormat.OpenXml.Wordprocessing.FieldChar
|
||||||
|
{
|
||||||
|
FieldCharType = DocumentFormat.OpenXml.Wordprocessing.FieldCharValues.End
|
||||||
|
});
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
private string AssembleMarkdown(string path, string title, List<(string Heading, string Content, int Level)> sections)
|
private string AssembleMarkdown(string path, string title, List<(string Heading, string Content, int Level)> sections)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
["format"] = new()
|
["format"] = new()
|
||||||
{
|
{
|
||||||
Type = "string",
|
Type = "string",
|
||||||
Description = "Output document format: auto, html, docx, markdown. Default: auto (resolved from settings and request intent)",
|
Description = "Output document format: auto, html, docx, markdown, xlsx. Default: auto (resolved from settings and request intent)",
|
||||||
Enum = ["auto", "html", "docx", "markdown"]
|
Enum = ["auto", "html", "docx", "markdown", "xlsx"]
|
||||||
},
|
},
|
||||||
["sections_hint"] = new()
|
["sections_hint"] = new()
|
||||||
{
|
{
|
||||||
@@ -124,7 +124,7 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
int targetPages, int totalWords, List<SectionPlan> sections, bool highQuality)
|
int targetPages, int totalWords, List<SectionPlan> sections, bool highQuality)
|
||||||
{
|
{
|
||||||
var safeTitle = SanitizeFileName(topic);
|
var safeTitle = SanitizeFileName(topic);
|
||||||
var ext = format switch { "docx" => ".docx", "markdown" => ".md", _ => ".html" };
|
var ext = format switch { "docx" => ".docx", "markdown" => ".md", "xlsx" => ".xlsx", _ => ".html" };
|
||||||
var suggestedFileName = $"{safeTitle}{ext}";
|
var suggestedFileName = $"{safeTitle}{ext}";
|
||||||
var label = highQuality ? "[고품질]" : "[표준]";
|
var label = highQuality ? "[고품질]" : "[표준]";
|
||||||
|
|
||||||
@@ -134,6 +134,8 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
return ExecuteWithMarkdownScaffold(topic, suggestedFileName, sections, targetPages, totalWords, label);
|
return ExecuteWithMarkdownScaffold(topic, suggestedFileName, sections, targetPages, totalWords, label);
|
||||||
case "docx":
|
case "docx":
|
||||||
return ExecuteWithDocxScaffold(topic, suggestedFileName, sections, targetPages, totalWords, label);
|
return ExecuteWithDocxScaffold(topic, suggestedFileName, sections, targetPages, totalWords, label);
|
||||||
|
case "xlsx":
|
||||||
|
return ExecuteWithExcelScaffold(topic, suggestedFileName, docType, sections, targetPages, totalWords, label);
|
||||||
default: // html
|
default: // html
|
||||||
return ExecuteWithHtmlScaffold(topic, suggestedFileName, sections, targetPages, totalWords, label, mood);
|
return ExecuteWithHtmlScaffold(topic, suggestedFileName, sections, targetPages, totalWords, label, mood);
|
||||||
}
|
}
|
||||||
@@ -294,15 +296,54 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
return Task.FromResult(ToolResult.Ok(output.ToString()));
|
return Task.FromResult(ToolResult.Ok(output.ToString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>excel_create 즉시 호출 가능한 workbook 골격 반환.</summary>
|
||||||
|
private Task<ToolResult> ExecuteWithExcelScaffold(string topic, string fileName, string docType,
|
||||||
|
List<SectionPlan> sections, int targetPages, int totalWords, string label)
|
||||||
|
{
|
||||||
|
var summarySection = sections.FirstOrDefault();
|
||||||
|
var recommendationSection = sections.FirstOrDefault(s =>
|
||||||
|
ContainsAny(s.Heading, "Recommendation", "Decision Ask", "Roadmap", "Action", "Ask", "권고", "실행", "요청"));
|
||||||
|
var summarySheet = new
|
||||||
|
{
|
||||||
|
name = "Summary",
|
||||||
|
title = topic,
|
||||||
|
subtitle = $"{GetDocTypeLabel(docType)} workbook",
|
||||||
|
kpis = BuildWorkbookKpis(summarySection),
|
||||||
|
highlights = (summarySection?.KeyPoints ?? []).Take(3).ToArray(),
|
||||||
|
actions = (recommendationSection?.KeyPoints ?? summarySection?.KeyPoints ?? []).Take(3).ToArray(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var sheets = BuildWorkbookSheets(docType, sections);
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
path = fileName,
|
||||||
|
theme = "professional",
|
||||||
|
summary_sheet = summarySheet,
|
||||||
|
sheets,
|
||||||
|
};
|
||||||
|
|
||||||
|
var output = new StringBuilder();
|
||||||
|
output.AppendLine($"📋 워크북 개요 생성 완료 {label} ({sheets.Count}개 상세 시트, {targetPages}페이지/{totalWords}단어 기준)");
|
||||||
|
output.AppendLine();
|
||||||
|
output.AppendLine("## 즉시 실행: excel_create 호출 파라미터");
|
||||||
|
output.AppendLine(JsonSerializer.Serialize(payload, _jsonOptions));
|
||||||
|
output.AppendLine();
|
||||||
|
output.AppendLine("⚠ 아래 placeholder 값과 샘플 행은 실제 수치/일정/담당자로 교체하세요.");
|
||||||
|
output.AppendLine("⚠ Summary 시트의 highlights/actions는 핵심 메시지 중심으로 유지하고, 상세 시트는 수치/근거 중심으로 채우세요.");
|
||||||
|
|
||||||
|
return Task.FromResult(ToolResult.Ok(output.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 싱글패스 + 폴더 데이터 활용: 개요 반환 + LLM이 데이터 읽고 직접 저장 ──
|
// ─── 싱글패스 + 폴더 데이터 활용: 개요 반환 + LLM이 데이터 읽고 직접 저장 ──
|
||||||
|
|
||||||
private Task<ToolResult> ExecuteSinglePassWithData(string topic, string docType, string format, string mood,
|
private Task<ToolResult> ExecuteSinglePassWithData(string topic, string docType, string format, string mood,
|
||||||
int targetPages, int totalWords, List<SectionPlan> sections, string folderDataUsage, string refSummary)
|
int targetPages, int totalWords, List<SectionPlan> sections, string folderDataUsage, string refSummary)
|
||||||
{
|
{
|
||||||
var ext = format switch { "docx" => ".docx", "markdown" => ".md", _ => ".html" };
|
var ext = format switch { "docx" => ".docx", "markdown" => ".md", "xlsx" => ".xlsx", _ => ".html" };
|
||||||
var createTool = format switch { "docx" => "docx_create", "markdown" => "file_write", _ => "html_create" };
|
var createTool = format switch { "docx" => "docx_create", "markdown" => "file_write", "xlsx" => "excel_create", _ => "html_create" };
|
||||||
var safeTitle = SanitizeFileName(topic);
|
var safeTitle = SanitizeFileName(topic);
|
||||||
var suggestedPath = $"{safeTitle}{ext}";
|
var suggestedPath = $"{safeTitle}{ext}";
|
||||||
|
var artifactLabel = format == "xlsx" ? "workbook" : "document";
|
||||||
|
|
||||||
// reference_summary가 이미 있으면 데이터 읽기 단계를 건너뛸 수 있음
|
// reference_summary가 이미 있으면 데이터 읽기 단계를 건너뛸 수 있음
|
||||||
var hasRefData = !string.IsNullOrWhiteSpace(refSummary);
|
var hasRefData = !string.IsNullOrWhiteSpace(refSummary);
|
||||||
@@ -327,8 +368,10 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
step2 = hasRefData
|
step2 = hasRefData
|
||||||
? "(skipped)"
|
? "(skipped)"
|
||||||
: "Summarize the key findings from the folder documents relevant to the topic.",
|
: "Summarize the key findings from the folder documents relevant to the topic.",
|
||||||
step3 = $"Write the COMPLETE document content covering ALL sections above, incorporating folder data.",
|
step3 = format == "xlsx"
|
||||||
step4 = $"Save the document using {createTool} with the path '{suggestedPath}'. " +
|
? "Create the COMPLETE workbook structure using validated data, including summary sheet, detailed sheets, and follow-up actions."
|
||||||
|
: $"Write the COMPLETE {artifactLabel} content covering ALL sections above, incorporating folder data.",
|
||||||
|
step4 = $"Save the {artifactLabel} using {createTool} with the path '{suggestedPath}'. " +
|
||||||
"Write ALL sections in a SINGLE tool call. Do NOT split into multiple calls.",
|
"Write ALL sections in a SINGLE tool call. Do NOT split into multiple calls.",
|
||||||
preferred_mood = format == "html" ? mood : null as string,
|
preferred_mood = format == "html" ? mood : null as string,
|
||||||
note = "Minimize LLM calls. Read data → write complete document → save. Maximum 3 iterations."
|
note = "Minimize LLM calls. Read data → write complete document → save. Maximum 3 iterations."
|
||||||
@@ -707,11 +750,13 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
private static string ResolveDocumentFormat(string? preferred, string docType, string topic)
|
private static string ResolveDocumentFormat(string? preferred, string docType, string topic)
|
||||||
{
|
{
|
||||||
var normalized = (preferred ?? "auto").Trim().ToLowerInvariant();
|
var normalized = (preferred ?? "auto").Trim().ToLowerInvariant();
|
||||||
if (normalized is "html" or "docx" or "markdown")
|
if (normalized is "html" or "docx" or "markdown" or "xlsx")
|
||||||
return normalized;
|
return normalized;
|
||||||
|
|
||||||
var intent = $"{docType} {topic}".ToLowerInvariant();
|
var intent = $"{docType} {topic}".ToLowerInvariant();
|
||||||
|
|
||||||
|
if (ContainsAny(intent, "xlsx", "excel", "workbook", "tracker", "dashboard", "scorecard", "워크북", "엑셀", "대시보드", "추적표"))
|
||||||
|
return "xlsx";
|
||||||
if (ContainsAny(intent, "docx", "word", "워드"))
|
if (ContainsAny(intent, "docx", "word", "워드"))
|
||||||
return "docx";
|
return "docx";
|
||||||
if (ContainsAny(intent, "markdown", ".md", "md ", "마크다운", "readme", "가이드", "manual", "guide", "회의록", "minutes"))
|
if (ContainsAny(intent, "markdown", ".md", "md ", "마크다운", "readme", "가이드", "manual", "guide", "회의록", "minutes"))
|
||||||
@@ -774,4 +819,134 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
public int TargetWords { get; set; } = 300;
|
public int TargetWords { get; set; } = 300;
|
||||||
public List<string> KeyPoints { get; set; } = new();
|
public List<string> KeyPoints { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class WorkbookSheetPlan
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public List<string> Headers { get; set; } = new();
|
||||||
|
public List<List<string>> Rows { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<object> BuildWorkbookKpis(SectionPlan? summarySection)
|
||||||
|
{
|
||||||
|
var source = summarySection?.KeyPoints ?? [];
|
||||||
|
return source.Take(3).Select((point, index) => (object)new
|
||||||
|
{
|
||||||
|
label = $"KPI {index + 1}",
|
||||||
|
value = "[수치]",
|
||||||
|
trend = point,
|
||||||
|
note = "Replace with validated metric",
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<WorkbookSheetPlan> BuildWorkbookSheets(string docType, List<SectionPlan> sections)
|
||||||
|
{
|
||||||
|
var plans = docType switch
|
||||||
|
{
|
||||||
|
"proposal" => new List<WorkbookSheetPlan>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "BusinessCase",
|
||||||
|
Headers = ["Workstream", "Investment", "Benefit", "Payback", "Owner"],
|
||||||
|
Rows =
|
||||||
|
[
|
||||||
|
["Initiative A", "[cost]", "[benefit]", "[months]", "[owner]"],
|
||||||
|
["Initiative B", "[cost]", "[benefit]", "[months]", "[owner]"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Roadmap",
|
||||||
|
Headers = ["Phase", "Milestone", "Timeline", "Owner", "Status"],
|
||||||
|
Rows =
|
||||||
|
[
|
||||||
|
["Phase 1", "[milestone]", "[date]", "[owner]", "Planned"],
|
||||||
|
["Phase 2", "[milestone]", "[date]", "[owner]", "Planned"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Risks",
|
||||||
|
Headers = ["Risk", "Impact", "Likelihood", "Mitigation", "Owner"],
|
||||||
|
Rows =
|
||||||
|
[
|
||||||
|
["[risk]", "High", "Medium", "[mitigation]", "[owner]"],
|
||||||
|
["[risk]", "Medium", "Low", "[mitigation]", "[owner]"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"analysis" => new List<WorkbookSheetPlan>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Metrics",
|
||||||
|
Headers = ["Metric", "Current", "Target", "Delta", "Comment"],
|
||||||
|
Rows =
|
||||||
|
[
|
||||||
|
["[metric]", "[current]", "[target]", "=[@Current]-[@Target]", "[insight]"],
|
||||||
|
["[metric]", "[current]", "[target]", "=[@Current]-[@Target]", "[insight]"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Findings",
|
||||||
|
Headers = ["Theme", "Finding", "Impact", "Evidence", "Owner"],
|
||||||
|
Rows =
|
||||||
|
[
|
||||||
|
["[theme]", "[finding]", "[impact]", "[source]", "[owner]"],
|
||||||
|
["[theme]", "[finding]", "[impact]", "[source]", "[owner]"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Actions",
|
||||||
|
Headers = ["Priority", "Action", "Owner", "Due Date", "Status"],
|
||||||
|
Rows =
|
||||||
|
[
|
||||||
|
["High", "[action]", "[owner]", "[date]", "Open"],
|
||||||
|
["Medium", "[action]", "[owner]", "[date]", "Open"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => new List<WorkbookSheetPlan>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Detail",
|
||||||
|
Headers = ["Category", "Item", "Value", "Trend", "Owner"],
|
||||||
|
Rows =
|
||||||
|
[
|
||||||
|
["[category]", "[item]", "[value]", "[trend]", "[owner]"],
|
||||||
|
["[category]", "[item]", "[value]", "[trend]", "[owner]"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Actions",
|
||||||
|
Headers = ["Action", "Owner", "Due Date", "Status", "Note"],
|
||||||
|
Rows =
|
||||||
|
[
|
||||||
|
["[action]", "[owner]", "[date]", "Open", "[note]"],
|
||||||
|
["[action]", "[owner]", "[date]", "Open", "[note]"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Evidence",
|
||||||
|
Headers = ["Section", "Key Point", "Evidence", "Source", "Comment"],
|
||||||
|
Rows = sections.Take(4).Select(section => new List<string>
|
||||||
|
{
|
||||||
|
section.Heading,
|
||||||
|
section.KeyPoints.FirstOrDefault() ?? "[key point]",
|
||||||
|
"[evidence]",
|
||||||
|
"[source]",
|
||||||
|
"[comment]",
|
||||||
|
}).ToList(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ allowed-tools:
|
|||||||
- folder_map
|
- folder_map
|
||||||
- file_read
|
- file_read
|
||||||
- data_pivot
|
- data_pivot
|
||||||
|
- document_plan
|
||||||
- excel_create
|
- excel_create
|
||||||
tabs: cowork
|
tabs: cowork
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user