This commit is contained in:
2026-01-06 21:44:36 +09:00
parent ceec1ad7a9
commit 716cf63f73
98 changed files with 6997 additions and 538 deletions

View 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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
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
) {}
}