diff --git a/dap-tool-report/build.gradle b/dap-tool-report/build.gradle
new file mode 100644
index 00000000..679e803e
--- /dev/null
+++ b/dap-tool-report/build.gradle
@@ -0,0 +1,16 @@
+plugins {
+ id 'org.springframework.boot'
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
+ implementation 'com.github.javaparser:javaparser-core:3.26.3'
+ implementation 'org.apache.poi:poi-ooxml:5.3.0'
+
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/ToolReportApplication.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/ToolReportApplication.java
new file mode 100644
index 00000000..80b797a8
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/ToolReportApplication.java
@@ -0,0 +1,25 @@
+package io.shinhanlife.dap.report;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @package io.shinhanlife.dap.report
+ * @className ToolReportApplication
+ * @description 툴 소스를 읽기 전용으로 분석하여 Excel 보고서를 생성하는 독립 애플리케이션
+ * @author 0986406
+ * @create 2026.08.07
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.08.07 0986406 최초생성
+ *
+ */
+@SpringBootApplication
+public class ToolReportApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ToolReportApplication.class, args);
+ }
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/application/ToolReportService.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/application/ToolReportService.java
new file mode 100644
index 00000000..830d0df3
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/application/ToolReportService.java
@@ -0,0 +1,48 @@
+package io.shinhanlife.dap.report.application;
+
+import io.shinhanlife.dap.report.excel.ToolReportExcelWriter;
+import io.shinhanlife.dap.report.model.ToolReportModel;
+import io.shinhanlife.dap.report.model.ToolSummary;
+import io.shinhanlife.dap.report.source.ToolDetailAnalyzer;
+import io.shinhanlife.dap.report.source.ToolSourceDiscovery;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.springframework.stereotype.Service;
+
+/** 툴 조회, 선택 검증, 분석 및 Excel 출력을 조정한다. */
+@Service
+public class ToolReportService {
+
+ private final ToolSourceDiscovery discovery;
+ private final ToolDetailAnalyzer analyzer;
+ private final ToolReportExcelWriter excelWriter;
+
+ public ToolReportService(ToolSourceDiscovery discovery, ToolDetailAnalyzer analyzer,
+ ToolReportExcelWriter excelWriter) {
+ this.discovery = discovery;
+ this.analyzer = analyzer;
+ this.excelWriter = excelWriter;
+ }
+
+ public List listTools() {
+ return discovery.discover();
+ }
+
+ public byte[] createExcel(List selectedNames) {
+ if (selectedNames == null || selectedNames.isEmpty()) {
+ throw new IllegalArgumentException("보고서에 포함할 툴을 하나 이상 선택해야 합니다.");
+ }
+ Map available = new LinkedHashMap<>();
+ listTools().forEach(tool -> available.put(tool.name(), tool));
+ List unknown = selectedNames.stream().filter(name -> !available.containsKey(name)).distinct().toList();
+ if (!unknown.isEmpty()) {
+ throw new IllegalArgumentException("존재하지 않는 툴이 포함되어 있습니다: " + unknown);
+ }
+ List reports = selectedNames.stream().distinct()
+ .map(available::get)
+ .map(analyzer::analyze)
+ .toList();
+ return excelWriter.write(reports);
+ }
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/config/ReportConfiguration.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/config/ReportConfiguration.java
new file mode 100644
index 00000000..4ec2986f
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/config/ReportConfiguration.java
@@ -0,0 +1,10 @@
+package io.shinhanlife.dap.report.config;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/** 보고서 모듈 설정. */
+@Configuration
+@EnableConfigurationProperties(ReportProperties.class)
+public class ReportConfiguration {
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/config/ReportProperties.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/config/ReportProperties.java
new file mode 100644
index 00000000..5641e204
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/config/ReportProperties.java
@@ -0,0 +1,8 @@
+package io.shinhanlife.dap.report.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/** 보고서 소스 위치와 출력 정책 설정. */
+@ConfigurationProperties(prefix = "report")
+public record ReportProperties(String sourceRoot, String outputFilenamePrefix) {
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriter.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriter.java
new file mode 100644
index 00000000..5a2356c9
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriter.java
@@ -0,0 +1,255 @@
+package io.shinhanlife.dap.report.excel;
+
+import io.shinhanlife.dap.report.model.FieldDefinition;
+import io.shinhanlife.dap.report.model.ToolReportModel;
+import io.shinhanlife.dap.report.model.ToolSummary;
+import java.awt.Color;
+import java.io.ByteArrayOutputStream;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.FillPatternType;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.VerticalAlignment;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.xssf.usermodel.DefaultIndexedColorMap;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFColor;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.stereotype.Component;
+
+/** 모든 보고서 시트에 동일한 전문 I/O 문서 디자인을 적용한다. */
+@Component
+public class ToolReportExcelWriter {
+
+ private static final int HEADER_ROW = 7;
+ private static final int FIRST_DATA_ROW = 8;
+
+ public byte[] write(List reports) {
+ try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+ Styles styles = new Styles(workbook);
+ writeSummary(workbook, reports, styles);
+ writeFields(workbook, reports, styles);
+ writeDiagnostics(workbook, reports, styles);
+ workbook.write(output);
+ return output.toByteArray();
+ } catch (Exception exception) {
+ throw new IllegalStateException("Excel 보고서 생성에 실패했습니다.", exception);
+ }
+ }
+
+ private void writeSummary(Workbook workbook, List reports, Styles styles) {
+ String[] headers = {"No.", "툴명", "제목", "설명", "카테고리", "연계 ID", "등록", "승인 필요",
+ "Read Only", "Destructive", "Idempotent", "Open World", "Request", "Response", "UseCase", "원천 파일"};
+ Sheet sheet = workbook.createSheet("툴 기본정보");
+ decorateSheet(sheet, "MCP Tool Catalog Report", "선택 툴 기본정보", reports, headers.length, styles);
+ header(sheet, headers, styles);
+ int rowIndex = FIRST_DATA_ROW;
+ int sequence = 1;
+ for (ToolReportModel report : reports) {
+ ToolSummary tool = report.tool();
+ Row row = sheet.createRow(rowIndex++);
+ Object[] values = {sequence++, tool.name(), tool.title(), tool.description(), tool.categoryKey(),
+ tool.mappingId(), yn(tool.register()), yn(tool.requiresApproval()), yn(tool.readOnlyHint()),
+ yn(tool.destructiveHint()), yn(tool.idempotentHint()), yn(tool.openWorldHint()),
+ tool.requestType(), tool.responseType(), tool.useCaseClass(), tool.sourceFile()};
+ values(row, values, styles.body);
+ row.getCell(0).setCellStyle(styles.bodyCenter);
+ }
+ finishTable(sheet, rowIndex, headers.length,
+ new int[]{7, 30, 24, 45, 12, 18, 10, 12, 12, 12, 12, 12, 20, 20, 25, 55});
+ }
+
+ private void writeFields(Workbook workbook, List reports, Styles styles) {
+ String[] headers = {"No.", "툴명", "원천 종류", "방향", "소유 타입", "필드명", "데이터 타입",
+ "필수", "설명", "제약조건 / 어노테이션", "원천 파일"};
+ Sheet sheet = workbook.createSheet("수집 필드");
+ decorateSheet(sheet, "Tool Source I/O Report", "어노테이션 · DTO · 전문 필드", reports, headers.length, styles);
+ header(sheet, headers, styles);
+ int rowIndex = FIRST_DATA_ROW;
+ int sequence = 1;
+ for (ToolReportModel report : reports) {
+ for (FieldDefinition field : report.fields()) {
+ Row row = sheet.createRow(rowIndex++);
+ Object[] values = {sequence++, field.toolName(), field.sourceKind(), field.direction(),
+ field.ownerType(), field.fieldName(), field.dataType(),
+ field.required() == null ? "" : yn(field.required()), field.description(),
+ field.constraints(), field.sourceFile()};
+ values(row, values, styles.body);
+ row.getCell(0).setCellStyle(styles.bodyCenter);
+ row.getCell(2).setCellStyle(styles.bodyCenter);
+ row.getCell(3).setCellStyle(styles.bodyCenter);
+ row.getCell(7).setCellStyle(styles.bodyCenter);
+ }
+ }
+ finishTable(sheet, rowIndex, headers.length,
+ new int[]{7, 30, 16, 11, 24, 24, 18, 9, 45, 55, 60});
+ }
+
+ private void writeDiagnostics(Workbook workbook, List reports, Styles styles) {
+ String[] headers = {"No.", "툴명", "수준", "진단 내용"};
+ Sheet sheet = workbook.createSheet("분석 결과");
+ decorateSheet(sheet, "Tool Analysis Report", "소스 분석 및 정합성 진단", reports, headers.length, styles);
+ header(sheet, headers, styles);
+ int rowIndex = FIRST_DATA_ROW;
+ int sequence = 1;
+ for (ToolReportModel report : reports) {
+ if (report.diagnostics().isEmpty()) {
+ Row row = sheet.createRow(rowIndex++);
+ values(row, new Object[]{sequence++, report.tool().name(), "정상", "분석 경고 없음"}, styles.body);
+ row.getCell(0).setCellStyle(styles.bodyCenter);
+ row.getCell(2).setCellStyle(styles.bodyCenter);
+ } else {
+ for (String diagnostic : report.diagnostics()) {
+ Row row = sheet.createRow(rowIndex++);
+ values(row, new Object[]{sequence++, report.tool().name(), "경고", diagnostic}, styles.warning);
+ row.getCell(0).setCellStyle(styles.warningCenter);
+ row.getCell(2).setCellStyle(styles.warningCenter);
+ }
+ }
+ }
+ finishTable(sheet, rowIndex, headers.length, new int[]{7, 30, 12, 80});
+ }
+
+ private void decorateSheet(Sheet sheet, String titleText, String reportType,
+ List reports, int columnCount, Styles styles) {
+ sheet.setDisplayGridlines(false);
+ sheet.setAutobreaks(true);
+ sheet.getPrintSetup().setLandscape(true);
+ sheet.getPrintSetup().setFitWidth((short) 1);
+ sheet.getPrintSetup().setFitHeight((short) 0);
+ sheet.setFitToPage(true);
+
+ Row title = sheet.createRow(0);
+ title.setHeightInPoints(24);
+ sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, columnCount - 1));
+ Cell titleCell = title.createCell(0);
+ titleCell.setCellValue(titleText);
+ titleCell.setCellStyle(styles.title);
+
+ metadataRow(sheet, 2, "보고서 구분", reportType, columnCount, styles);
+ metadataRow(sheet, 3, "선택 툴 수", reports.size(), columnCount, styles);
+ metadataRow(sheet, 4, "생성 일시",
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), columnCount, styles);
+ }
+
+ private void metadataRow(Sheet sheet, int rowIndex, String label, Object value,
+ int columnCount, Styles styles) {
+ Row row = sheet.createRow(rowIndex);
+ row.setHeightInPoints(19);
+ int labelEnd = Math.min(1, columnCount - 1);
+ if (labelEnd > 0) sheet.addMergedRegion(new CellRangeAddress(rowIndex, rowIndex, 0, labelEnd));
+ Cell labelCell = row.createCell(0);
+ labelCell.setCellValue(label);
+ labelCell.setCellStyle(styles.metaLabel);
+ int valueStart = labelEnd + 1;
+ if (valueStart < columnCount - 1) {
+ sheet.addMergedRegion(new CellRangeAddress(rowIndex, rowIndex, valueStart, columnCount - 1));
+ }
+ Cell valueCell = row.createCell(valueStart);
+ if (value instanceof Number number) valueCell.setCellValue(number.doubleValue());
+ else valueCell.setCellValue(value == null ? "" : String.valueOf(value));
+ valueCell.setCellStyle(styles.metaValue);
+ }
+
+ private void header(Sheet sheet, String[] headers, Styles styles) {
+ Row row = sheet.createRow(HEADER_ROW);
+ row.setHeightInPoints(30);
+ for (int column = 0; column < headers.length; column++) {
+ Cell cell = row.createCell(column);
+ cell.setCellValue(headers[column]);
+ cell.setCellStyle(styles.header);
+ }
+ }
+
+ private void finishTable(Sheet sheet, int rowIndex, int columnCount, int[] characterWidths) {
+ widths(sheet, characterWidths);
+ sheet.createFreezePane(0, FIRST_DATA_ROW);
+ sheet.setAutoFilter(new CellRangeAddress(HEADER_ROW, Math.max(HEADER_ROW, rowIndex - 1), 0, columnCount - 1));
+ sheet.setRepeatingRows(new CellRangeAddress(HEADER_ROW, HEADER_ROW, -1, -1));
+ }
+
+ private void values(Row row, Object[] values, CellStyle style) {
+ row.setHeightInPoints(19);
+ for (int column = 0; column < values.length; column++) {
+ Cell cell = row.createCell(column);
+ Object value = values[column];
+ if (value instanceof Number number) cell.setCellValue(number.doubleValue());
+ else cell.setCellValue(value == null ? "" : String.valueOf(value));
+ cell.setCellStyle(style);
+ }
+ }
+
+ private void widths(Sheet sheet, int[] characterWidths) {
+ for (int i = 0; i < characterWidths.length; i++) {
+ sheet.setColumnWidth(i, Math.min(255, characterWidths[i]) * 256);
+ }
+ }
+
+ private String yn(boolean value) {
+ return value ? "Y" : "N";
+ }
+
+ private static final class Styles {
+ private final CellStyle title;
+ private final CellStyle metaLabel;
+ private final CellStyle metaValue;
+ private final CellStyle header;
+ private final CellStyle body;
+ private final CellStyle bodyCenter;
+ private final CellStyle warning;
+ private final CellStyle warningCenter;
+
+ private Styles(Workbook workbook) {
+ title = style(workbook, "#262626", "#FFFFFF", 12, true, HorizontalAlignment.CENTER, false);
+ metaLabel = style(workbook, "#333333", "#FFFFFF", 10, true, HorizontalAlignment.CENTER, false);
+ metaValue = style(workbook, "#FFFFFF", "#222222", 10, false, HorizontalAlignment.LEFT, false);
+ header = style(workbook, "#404040", "#FFFFFF", 9, true, HorizontalAlignment.CENTER, true);
+ body = style(workbook, "#FFFFFF", "#222222", 9, false, HorizontalAlignment.LEFT, true);
+ bodyCenter = style(workbook, "#FFFFFF", "#222222", 9, false, HorizontalAlignment.CENTER, true);
+ warning = style(workbook, "#FFF2CC", "#7F6000", 9, false, HorizontalAlignment.LEFT, true);
+ warningCenter = style(workbook, "#FFF2CC", "#7F6000", 9, true, HorizontalAlignment.CENTER, true);
+ }
+
+ private CellStyle style(Workbook workbook, String fill, String fontColor, int size, boolean bold,
+ HorizontalAlignment alignment, boolean borders) {
+ XSSFCellStyle style = (XSSFCellStyle) workbook.createCellStyle();
+ style.setFillForegroundColor(color(fill));
+ style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+ style.setAlignment(alignment);
+ style.setVerticalAlignment(VerticalAlignment.CENTER);
+ style.setWrapText(true);
+ Font font = workbook.createFont();
+ font.setFontName("Carlito");
+ font.setFontHeightInPoints((short) size);
+ font.setBold(bold);
+ ((org.apache.poi.xssf.usermodel.XSSFFont) font).setColor(color(fontColor));
+ style.setFont(font);
+ if (borders) applyBorders(style);
+ return style;
+ }
+
+ private void applyBorders(CellStyle style) {
+ style.setBorderTop(BorderStyle.THIN);
+ style.setBorderBottom(BorderStyle.THIN);
+ style.setBorderLeft(BorderStyle.THIN);
+ style.setBorderRight(BorderStyle.THIN);
+ short borderColor = org.apache.poi.ss.usermodel.IndexedColors.GREY_25_PERCENT.getIndex();
+ style.setTopBorderColor(borderColor);
+ style.setBottomBorderColor(borderColor);
+ style.setLeftBorderColor(borderColor);
+ style.setRightBorderColor(borderColor);
+ }
+
+ private XSSFColor color(String hex) {
+ return new XSSFColor(Color.decode(hex), new DefaultIndexedColorMap());
+ }
+ }
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/FieldDefinition.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/FieldDefinition.java
new file mode 100644
index 00000000..be17e51e
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/FieldDefinition.java
@@ -0,0 +1,15 @@
+package io.shinhanlife.dap.report.model;
+
+/** DTO, JSON Schema 또는 원천 전문에서 수집한 필드 정의. */
+public record FieldDefinition(
+ String toolName,
+ String sourceKind,
+ String direction,
+ String ownerType,
+ String fieldName,
+ String dataType,
+ Boolean required,
+ String description,
+ String constraints,
+ String sourceFile) {
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolReportModel.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolReportModel.java
new file mode 100644
index 00000000..e5fb6caa
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolReportModel.java
@@ -0,0 +1,7 @@
+package io.shinhanlife.dap.report.model;
+
+import java.util.List;
+
+/** 수집 단계와 Excel 출력 단계를 분리하는 표준 중간 모델. */
+public record ToolReportModel(ToolSummary tool, List fields, List diagnostics) {
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolReportRequest.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolReportRequest.java
new file mode 100644
index 00000000..1e073971
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolReportRequest.java
@@ -0,0 +1,7 @@
+package io.shinhanlife.dap.report.model;
+
+import java.util.List;
+
+/** 사용자가 선택한 툴 보고서 생성 요청. */
+public record ToolReportRequest(List toolNames) {
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolSummary.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolSummary.java
new file mode 100644
index 00000000..9d7a37ea
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolSummary.java
@@ -0,0 +1,22 @@
+package io.shinhanlife.dap.report.model;
+
+/** UI 선택 목록과 보고서 기본정보에 사용하는 툴 요약. */
+public record ToolSummary(
+ String name,
+ String title,
+ String description,
+ String categoryKey,
+ String mappingId,
+ boolean register,
+ boolean requiresApproval,
+ boolean readOnlyHint,
+ boolean destructiveHint,
+ boolean idempotentHint,
+ boolean openWorldHint,
+ String requestType,
+ String responseType,
+ String useCaseClass,
+ String sourceFile,
+ String inputSchemaResource,
+ String outputSchemaResource) {
+}
diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/presentation/ToolReportController.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/presentation/ToolReportController.java
new file mode 100644
index 00000000..3d4597b1
--- /dev/null
+++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/presentation/ToolReportController.java
@@ -0,0 +1,62 @@
+package io.shinhanlife.dap.report.presentation;
+
+import io.shinhanlife.dap.report.application.ToolReportService;
+import io.shinhanlife.dap.report.config.ReportProperties;
+import io.shinhanlife.dap.report.model.ToolReportRequest;
+import io.shinhanlife.dap.report.model.ToolSummary;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Map;
+import org.springframework.http.ContentDisposition;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/** 툴 선택 목록과 Excel 다운로드 API. */
+@RestController
+@RequestMapping("/api")
+public class ToolReportController {
+
+ private final ToolReportService service;
+ private final ReportProperties properties;
+
+ public ToolReportController(ToolReportService service, ReportProperties properties) {
+ this.service = service;
+ this.properties = properties;
+ }
+
+ @GetMapping("/report-tools")
+ public List tools() {
+ return service.listTools();
+ }
+
+ @PostMapping(value = "/tool-reports/excel",
+ produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ public ResponseEntity excel(@RequestBody ToolReportRequest request) {
+ byte[] content = service.createExcel(request.toolNames());
+ String prefix = properties.outputFilenamePrefix() == null || properties.outputFilenamePrefix().isBlank()
+ ? "tool-report" : properties.outputFilenamePrefix();
+ String filename = prefix + "-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")) + ".xlsx";
+ ContentDisposition disposition = ContentDisposition.attachment()
+ .filename(filename, StandardCharsets.UTF_8)
+ .build();
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString())
+ .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
+ .contentLength(content.length)
+ .body(content);
+ }
+
+ @ExceptionHandler(IllegalArgumentException.class)
+ public ResponseEntity