update
This commit is contained in:
285
src/main/java/research/loghunter/service/ExportService.java
Normal file
285
src/main/java/research/loghunter/service/ExportService.java
Normal file
@@ -0,0 +1,285 @@
|
||||
package research.loghunter.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import research.loghunter.dto.ErrorLogDto;
|
||||
import research.loghunter.entity.Server;
|
||||
import research.loghunter.repository.ServerRepository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ExportService {
|
||||
|
||||
private final ErrorLogService errorLogService;
|
||||
private final ServerRepository serverRepository;
|
||||
private final SettingService settingService;
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter FILE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||
|
||||
/**
|
||||
* HTML 리포트 생성
|
||||
*/
|
||||
public ExportResult exportHtml(ExportRequest request) {
|
||||
List<ErrorLogDto> errors = getErrors(request);
|
||||
String serverName = getServerName(request.serverId());
|
||||
String title = generateTitle(serverName, request);
|
||||
|
||||
StringBuilder html = new StringBuilder();
|
||||
html.append("<!DOCTYPE html>\n");
|
||||
html.append("<html lang=\"ko\">\n");
|
||||
html.append("<head>\n");
|
||||
html.append(" <meta charset=\"UTF-8\">\n");
|
||||
html.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
|
||||
html.append(" <title>").append(escapeHtml(title)).append("</title>\n");
|
||||
html.append(" <style>\n");
|
||||
html.append(getHtmlStyles());
|
||||
html.append(" </style>\n");
|
||||
html.append("</head>\n");
|
||||
html.append("<body>\n");
|
||||
|
||||
// Header
|
||||
html.append(" <div class=\"header\">\n");
|
||||
html.append(" <h1>LogHunter 에러 리포트</h1>\n");
|
||||
html.append(" <p class=\"subtitle\">").append(escapeHtml(title)).append("</p>\n");
|
||||
html.append(" <p class=\"generated\">생성일시: ").append(LocalDateTime.now().format(DATE_FORMAT)).append("</p>\n");
|
||||
html.append(" </div>\n");
|
||||
|
||||
// Summary
|
||||
html.append(" <div class=\"summary\">\n");
|
||||
html.append(" <h2>요약</h2>\n");
|
||||
html.append(" <ul>\n");
|
||||
html.append(" <li>총 에러 수: <strong>").append(errors.size()).append("</strong>건</li>\n");
|
||||
html.append(" <li>CRITICAL: <strong>").append(countBySeverity(errors, "CRITICAL")).append("</strong>건</li>\n");
|
||||
html.append(" <li>ERROR: <strong>").append(countBySeverity(errors, "ERROR")).append("</strong>건</li>\n");
|
||||
html.append(" <li>WARN: <strong>").append(countBySeverity(errors, "WARN")).append("</strong>건</li>\n");
|
||||
html.append(" </ul>\n");
|
||||
html.append(" </div>\n");
|
||||
|
||||
// Error List
|
||||
html.append(" <div class=\"error-list\">\n");
|
||||
html.append(" <h2>에러 목록</h2>\n");
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
html.append(" <p class=\"no-data\">검출된 에러가 없습니다.</p>\n");
|
||||
} else {
|
||||
for (int i = 0; i < errors.size(); i++) {
|
||||
ErrorLogDto error = errors.get(i);
|
||||
html.append(renderErrorHtml(error, i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
html.append(" </div>\n");
|
||||
|
||||
// Footer
|
||||
html.append(" <div class=\"footer\">\n");
|
||||
html.append(" <p>Generated by LogHunter</p>\n");
|
||||
html.append(" </div>\n");
|
||||
|
||||
html.append("</body>\n");
|
||||
html.append("</html>");
|
||||
|
||||
String filename = "loghunter_report_" + LocalDateTime.now().format(FILE_DATE_FORMAT) + ".html";
|
||||
return new ExportResult(filename, html.toString().getBytes(), "text/html");
|
||||
}
|
||||
|
||||
/**
|
||||
* TXT 리포트 생성
|
||||
*/
|
||||
public ExportResult exportTxt(ExportRequest request) {
|
||||
List<ErrorLogDto> errors = getErrors(request);
|
||||
String serverName = getServerName(request.serverId());
|
||||
String title = generateTitle(serverName, request);
|
||||
|
||||
StringBuilder txt = new StringBuilder();
|
||||
txt.append("=".repeat(80)).append("\n");
|
||||
txt.append("LogHunter 에러 리포트\n");
|
||||
txt.append("=".repeat(80)).append("\n\n");
|
||||
|
||||
txt.append("제목: ").append(title).append("\n");
|
||||
txt.append("생성일시: ").append(LocalDateTime.now().format(DATE_FORMAT)).append("\n\n");
|
||||
|
||||
// Summary
|
||||
txt.append("-".repeat(40)).append("\n");
|
||||
txt.append("요약\n");
|
||||
txt.append("-".repeat(40)).append("\n");
|
||||
txt.append("총 에러 수: ").append(errors.size()).append("건\n");
|
||||
txt.append(" - CRITICAL: ").append(countBySeverity(errors, "CRITICAL")).append("건\n");
|
||||
txt.append(" - ERROR: ").append(countBySeverity(errors, "ERROR")).append("건\n");
|
||||
txt.append(" - WARN: ").append(countBySeverity(errors, "WARN")).append("건\n\n");
|
||||
|
||||
// Error List
|
||||
txt.append("-".repeat(40)).append("\n");
|
||||
txt.append("에러 목록\n");
|
||||
txt.append("-".repeat(40)).append("\n\n");
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
txt.append("검출된 에러가 없습니다.\n");
|
||||
} else {
|
||||
for (int i = 0; i < errors.size(); i++) {
|
||||
ErrorLogDto error = errors.get(i);
|
||||
txt.append(renderErrorTxt(error, i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
txt.append("\n").append("=".repeat(80)).append("\n");
|
||||
txt.append("Generated by LogHunter\n");
|
||||
txt.append("=".repeat(80)).append("\n");
|
||||
|
||||
String filename = "loghunter_report_" + LocalDateTime.now().format(FILE_DATE_FORMAT) + ".txt";
|
||||
return new ExportResult(filename, txt.toString().getBytes(), "text/plain");
|
||||
}
|
||||
|
||||
private List<ErrorLogDto> getErrors(ExportRequest request) {
|
||||
return errorLogService.search(
|
||||
request.serverId(),
|
||||
request.patternId(),
|
||||
request.severity(),
|
||||
request.startDate(),
|
||||
request.endDate(),
|
||||
request.keyword(),
|
||||
0,
|
||||
10000 // 최대 10000건
|
||||
).getContent();
|
||||
}
|
||||
|
||||
private String getServerName(Long serverId) {
|
||||
if (serverId == null) return "전체 서버";
|
||||
return serverRepository.findById(serverId)
|
||||
.map(Server::getName)
|
||||
.orElse("알 수 없음");
|
||||
}
|
||||
|
||||
private String generateTitle(String serverName, ExportRequest request) {
|
||||
StringBuilder title = new StringBuilder();
|
||||
title.append(serverName);
|
||||
|
||||
if (request.startDate() != null || request.endDate() != null) {
|
||||
title.append(" (");
|
||||
if (request.startDate() != null) {
|
||||
title.append(request.startDate().toLocalDate());
|
||||
}
|
||||
title.append(" ~ ");
|
||||
if (request.endDate() != null) {
|
||||
title.append(request.endDate().toLocalDate());
|
||||
}
|
||||
title.append(")");
|
||||
}
|
||||
|
||||
return title.toString();
|
||||
}
|
||||
|
||||
private long countBySeverity(List<ErrorLogDto> errors, String severity) {
|
||||
return errors.stream()
|
||||
.filter(e -> severity.equals(e.getSeverity()))
|
||||
.count();
|
||||
}
|
||||
|
||||
private String renderErrorHtml(ErrorLogDto error, int index) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(" <div class=\"error-item\">\n");
|
||||
sb.append(" <div class=\"error-header\">\n");
|
||||
sb.append(" <span class=\"index\">#").append(index).append("</span>\n");
|
||||
sb.append(" <span class=\"severity ").append(error.getSeverity().toLowerCase()).append("\">")
|
||||
.append(error.getSeverity()).append("</span>\n");
|
||||
sb.append(" <span class=\"time\">").append(formatDateTime(error.getOccurredAt())).append("</span>\n");
|
||||
sb.append(" </div>\n");
|
||||
sb.append(" <div class=\"error-meta\">\n");
|
||||
sb.append(" <span><strong>서버:</strong> ").append(escapeHtml(error.getServerName())).append("</span>\n");
|
||||
sb.append(" <span><strong>패턴:</strong> ").append(escapeHtml(error.getPatternName())).append("</span>\n");
|
||||
sb.append(" <span><strong>파일:</strong> ").append(escapeHtml(error.getFilePath())).append("</span>\n");
|
||||
sb.append(" <span><strong>라인:</strong> ").append(error.getLineNumber()).append("</span>\n");
|
||||
sb.append(" </div>\n");
|
||||
sb.append(" <div class=\"error-summary\">\n");
|
||||
sb.append(" <strong>요약:</strong> ").append(escapeHtml(error.getSummary())).append("\n");
|
||||
sb.append(" </div>\n");
|
||||
sb.append(" <div class=\"error-context\">\n");
|
||||
sb.append(" <strong>컨텍스트:</strong>\n");
|
||||
sb.append(" <pre>").append(escapeHtml(error.getContext())).append("</pre>\n");
|
||||
sb.append(" </div>\n");
|
||||
sb.append(" </div>\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String renderErrorTxt(ErrorLogDto error, int index) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[#").append(index).append("] ").append(error.getSeverity()).append("\n");
|
||||
sb.append("발생시간: ").append(formatDateTime(error.getOccurredAt())).append("\n");
|
||||
sb.append("서버: ").append(error.getServerName()).append("\n");
|
||||
sb.append("패턴: ").append(error.getPatternName()).append("\n");
|
||||
sb.append("파일: ").append(error.getFilePath()).append("\n");
|
||||
sb.append("라인: ").append(error.getLineNumber()).append("\n");
|
||||
sb.append("요약: ").append(error.getSummary()).append("\n");
|
||||
sb.append("컨텍스트:\n");
|
||||
sb.append(error.getContext()).append("\n");
|
||||
sb.append("-".repeat(60)).append("\n\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatDateTime(LocalDateTime dt) {
|
||||
return dt != null ? dt.format(DATE_FORMAT) : "-";
|
||||
}
|
||||
|
||||
private String escapeHtml(String str) {
|
||||
if (str == null) return "";
|
||||
return str.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
private String getHtmlStyles() {
|
||||
return """
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Malgun Gothic', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.header { background: #2c3e50; color: white; padding: 30px; border-radius: 8px; margin-bottom: 20px; }
|
||||
.header h1 { margin-bottom: 10px; }
|
||||
.header .subtitle { font-size: 18px; opacity: 0.9; }
|
||||
.header .generated { font-size: 14px; opacity: 0.7; margin-top: 10px; }
|
||||
.summary { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.summary h2 { margin-bottom: 15px; color: #2c3e50; }
|
||||
.summary ul { list-style: none; }
|
||||
.summary li { padding: 8px 0; border-bottom: 1px solid #eee; }
|
||||
.summary li:last-child { border-bottom: none; }
|
||||
.error-list { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.error-list h2 { margin-bottom: 20px; color: #2c3e50; }
|
||||
.error-item { border: 1px solid #ddd; border-radius: 8px; margin-bottom: 15px; overflow: hidden; }
|
||||
.error-header { background: #f8f9fa; padding: 12px 15px; display: flex; align-items: center; gap: 15px; }
|
||||
.error-header .index { font-weight: bold; color: #666; }
|
||||
.error-header .severity { padding: 4px 10px; border-radius: 4px; font-size: 12px; font-weight: bold; color: white; }
|
||||
.error-header .severity.critical { background: #c0392b; }
|
||||
.error-header .severity.error { background: #e74c3c; }
|
||||
.error-header .severity.warn { background: #f39c12; }
|
||||
.error-header .time { margin-left: auto; color: #666; font-size: 14px; }
|
||||
.error-meta { padding: 12px 15px; background: #fafafa; display: flex; flex-wrap: wrap; gap: 20px; font-size: 14px; }
|
||||
.error-summary { padding: 12px 15px; border-bottom: 1px solid #eee; }
|
||||
.error-context { padding: 12px 15px; }
|
||||
.error-context pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; line-height: 1.5; margin-top: 10px; white-space: pre-wrap; }
|
||||
.no-data { text-align: center; color: #666; padding: 40px; }
|
||||
.footer { text-align: center; padding: 20px; color: #666; font-size: 14px; }
|
||||
""";
|
||||
}
|
||||
|
||||
// DTOs
|
||||
public record ExportRequest(
|
||||
Long serverId,
|
||||
Long patternId,
|
||||
String severity,
|
||||
LocalDateTime startDate,
|
||||
LocalDateTime endDate,
|
||||
String keyword
|
||||
) {}
|
||||
|
||||
public record ExportResult(
|
||||
String filename,
|
||||
byte[] content,
|
||||
String contentType
|
||||
) {}
|
||||
}
|
||||
Reference in New Issue
Block a user