문서 고도화 다음 단계를 반영해 엑셀 summary와 DOCX 조립 품질을 끌어올렸습니다

핵심 수정사항:
- ExcelSkill summary_sheet에 decision_summary, scorecards, sheet_summaries를 추가해 dashboard형 summary 시트를 생성하도록 확장했습니다.
- ArtifactQualityReviewService의 workbook 리뷰 입력을 확장해 KPI/decision/detail summary 존재 여부를 품질 점수와 보완 포인트에 반영했습니다.
- DocumentAssemblerTool에 style_map 파라미터를 추가해 template 기반 DOCX 조립에서 title/heading/body 문단 스타일을 실제 Word 스타일로 매핑하도록 개선했습니다.
- DocumentAssemblerStyleMapTests, ExcelSkillDashboardSummaryTests를 추가하고 기존 ArtifactQualityReviewServiceTests를 갱신했습니다.

검증 결과:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next2\\ -p:IntermediateOutputPath=obj\\verify_doc_next2\\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests" -p:OutputPath=bin\\verify_doc_next2_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next2_tests\\ : 통과 11
This commit is contained in:
2026-04-14 23:06:53 +09:00
parent 3232db1b12
commit 1edeffa206
8 changed files with 471 additions and 56 deletions

View File

@@ -217,7 +217,10 @@ public class ExcelSkill : IAgentTool
conditionalFormattingCount,
false,
false,
summaryArg.ValueKind == JsonValueKind.Object));
summaryArg.ValueKind == JsonValueKind.Object,
false,
false,
false));
features += $"\n{review.ToToolSummary()}";
return ToolResult.Ok(
@@ -305,7 +308,10 @@ public class ExcelSkill : IAgentTool
conditionalFormattingCount,
true,
HasSummaryItems(summarySheet, "highlights"),
HasSummaryItems(summarySheet, "actions")));
HasSummaryItems(summarySheet, "actions"),
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis"),
HasStructuredSummaryContent(summarySheet, "decision_summary"),
HasSummaryItems(summarySheet, "sheet_summaries")));
features += $"\n{review.ToToolSummary()}";
return ToolResult.Ok(
@@ -428,7 +434,10 @@ public class ExcelSkill : IAgentTool
totalConditionalFormattingCount,
summarySheet.ValueKind == JsonValueKind.Object,
HasSummaryItems(summarySheet, "highlights"),
HasSummaryItems(summarySheet, "actions")));
HasSummaryItems(summarySheet, "actions"),
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis"),
HasStructuredSummaryContent(summarySheet, "decision_summary"),
HasSummaryItems(summarySheet, "sheet_summaries")));
return ToolResult.Ok(
$"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}\n{review.ToToolSummary()}",
fullPath);
@@ -471,6 +480,9 @@ public class ExcelSkill : IAgentTool
rowIndex++;
AppendDecisionSummarySection(summarySheet, sheetData, merges, ref rowIndex);
AppendScorecardSection(summarySheet, sheetData, ref rowIndex);
if (summarySheet.SafeTryGetProperty("kpis", out var kpis) && kpis.ValueKind == JsonValueKind.Array && kpis.GetArrayLength() > 0)
{
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Key KPIs", 1);
@@ -497,6 +509,7 @@ public class ExcelSkill : IAgentTool
rowIndex++;
}
AppendSheetSummarySection(summarySheet, sheetData, ref rowIndex);
AppendSummaryTextSection(summarySheet, "highlights", "Key Highlights", sheetData, merges, ref rowIndex);
AppendSummaryTextSection(summarySheet, "actions", "Next Actions", sheetData, merges, ref rowIndex);
@@ -530,6 +543,115 @@ public class ExcelSkill : IAgentTool
}
}
private static void AppendDecisionSummarySection(JsonElement summarySheet, SheetData sheetData, MergeCells merges, ref uint rowIndex)
{
if (!summarySheet.SafeTryGetProperty("decision_summary", out var decisionSummary))
return;
var appended = false;
if (decisionSummary.ValueKind == JsonValueKind.Array && decisionSummary.GetArrayLength() > 0)
{
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Decision Summary", 1);
foreach (var item in decisionSummary.EnumerateArray())
{
var label = item.SafeTryGetProperty("label", out var labelEl) ? labelEl.SafeGetString() ?? "" : "";
var value = item.SafeTryGetProperty("value", out var valueEl) ? valueEl.SafeGetString() ?? "" : "";
var owner = item.SafeTryGetProperty("owner", out var ownerEl) ? ownerEl.SafeGetString() ?? "" : "";
var row = new Row { RowIndex = rowIndex++ };
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, label, 1));
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, value, 3));
row.Append(CreateSummaryCell("C", row.RowIndex!.Value, owner, 0));
sheetData.Append(row);
appended = true;
}
}
else if (decisionSummary.ValueKind == JsonValueKind.Object)
{
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Decision Summary", 1);
foreach (var property in decisionSummary.EnumerateObject())
{
var row = new Row { RowIndex = rowIndex++ };
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, property.Name, 1));
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, property.Value.SafeGetString() ?? property.Value.ToString(), 3));
sheetData.Append(row);
appended = true;
}
}
if (appended)
rowIndex++;
}
private static void AppendScorecardSection(JsonElement summarySheet, SheetData sheetData, ref uint rowIndex)
{
JsonElement scorecards;
var hasScorecards = summarySheet.SafeTryGetProperty("scorecards", out scorecards)
&& scorecards.ValueKind == JsonValueKind.Array
&& scorecards.GetArrayLength() > 0;
if (!hasScorecards)
{
hasScorecards = summarySheet.SafeTryGetProperty("cards", out scorecards)
&& scorecards.ValueKind == JsonValueKind.Array
&& scorecards.GetArrayLength() > 0;
}
if (!hasScorecards)
return;
var headerRow = new Row { RowIndex = rowIndex++ };
headerRow.Append(CreateSummaryCell("A", headerRow.RowIndex!.Value, "Scorecards", 1));
headerRow.Append(CreateSummaryCell("B", headerRow.RowIndex!.Value, "Value", 1));
headerRow.Append(CreateSummaryCell("C", headerRow.RowIndex!.Value, "Status", 1));
headerRow.Append(CreateSummaryCell("D", headerRow.RowIndex!.Value, "Note", 1));
sheetData.Append(headerRow);
var cardIndex = 0;
foreach (var card in scorecards.EnumerateArray())
{
var row = new Row { RowIndex = rowIndex++ };
var stripeStyle = cardIndex % 2 == 0 ? (uint)0 : (uint)2;
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, card.SafeTryGetProperty("label", out var labelEl) ? labelEl.SafeGetString() ?? "" : "", 1));
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, card.SafeTryGetProperty("value", out var valueEl) ? valueEl.SafeGetString() ?? "" : "", 3));
row.Append(CreateSummaryCell("C", row.RowIndex!.Value, card.SafeTryGetProperty("status", out var statusEl) ? statusEl.SafeGetString() ?? "" : "", stripeStyle));
row.Append(CreateSummaryCell("D", row.RowIndex!.Value, card.SafeTryGetProperty("note", out var noteEl) ? noteEl.SafeGetString() ?? "" : "", stripeStyle));
sheetData.Append(row);
cardIndex++;
}
rowIndex++;
}
private static void AppendSheetSummarySection(JsonElement summarySheet, SheetData sheetData, ref uint rowIndex)
{
if (!summarySheet.SafeTryGetProperty("sheet_summaries", out var sheetSummaries)
|| sheetSummaries.ValueKind != JsonValueKind.Array
|| sheetSummaries.GetArrayLength() == 0)
return;
var headerRow = new Row { RowIndex = rowIndex++ };
headerRow.Append(CreateSummaryCell("A", headerRow.RowIndex!.Value, "Sheet", 1));
headerRow.Append(CreateSummaryCell("B", headerRow.RowIndex!.Value, "Status", 1));
headerRow.Append(CreateSummaryCell("C", headerRow.RowIndex!.Value, "Summary", 1));
headerRow.Append(CreateSummaryCell("D", headerRow.RowIndex!.Value, "Owner", 1));
sheetData.Append(headerRow);
var summaryIndex = 0;
foreach (var sheetSummary in sheetSummaries.EnumerateArray())
{
var row = new Row { RowIndex = rowIndex++ };
var stripeStyle = summaryIndex % 2 == 0 ? (uint)0 : (uint)2;
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, sheetSummary.SafeTryGetProperty("sheet", out var sheetEl) ? sheetEl.SafeGetString() ?? "" : "", 1));
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, sheetSummary.SafeTryGetProperty("status", out var statusEl) ? statusEl.SafeGetString() ?? "" : "", 3));
row.Append(CreateSummaryCell("C", row.RowIndex!.Value, sheetSummary.SafeTryGetProperty("summary", out var summaryEl) ? summaryEl.SafeGetString() ?? "" : "", stripeStyle));
row.Append(CreateSummaryCell("D", row.RowIndex!.Value, sheetSummary.SafeTryGetProperty("owner", out var ownerEl) ? ownerEl.SafeGetString() ?? "" : "", stripeStyle));
sheetData.Append(row);
summaryIndex++;
}
rowIndex++;
}
private static void AppendSummaryTextSection(JsonElement summarySheet, string key, string heading, SheetData sheetData, MergeCells merges, ref uint rowIndex)
{
if (!summarySheet.SafeTryGetProperty(key, out var items) || items.ValueKind != JsonValueKind.Array || items.GetArrayLength() == 0)
@@ -1235,6 +1357,20 @@ public class ExcelSkill : IAgentTool
&& items.GetArrayLength() > 0;
}
private static bool HasStructuredSummaryContent(JsonElement summarySheet, string propertyName)
{
if (summarySheet.ValueKind != JsonValueKind.Object
|| !summarySheet.SafeTryGetProperty(propertyName, out var value))
return false;
return value.ValueKind switch
{
JsonValueKind.Array => value.GetArrayLength() > 0,
JsonValueKind.Object => value.EnumerateObject().Any(),
_ => false,
};
}
private static string BuildFeatureList(bool isStyled, bool freezeHeader,
bool hasMerges, bool hasSummary, bool hasNumFmts, bool hasAlignments, string? theme)
{