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 errors = getErrors(request); String serverName = getServerName(request.serverId()); String title = generateTitle(serverName, request); StringBuilder html = new StringBuilder(); html.append("\n"); html.append("\n"); html.append("\n"); html.append(" \n"); html.append(" \n"); html.append(" ").append(escapeHtml(title)).append("\n"); html.append(" \n"); html.append("\n"); html.append("\n"); // Header html.append("
\n"); html.append("

LogHunter 에러 리포트

\n"); html.append("

").append(escapeHtml(title)).append("

\n"); html.append("

생성일시: ").append(LocalDateTime.now().format(DATE_FORMAT)).append("

\n"); html.append("
\n"); // Summary html.append("
\n"); html.append("

요약

\n"); html.append(" \n"); html.append("
\n"); // Error List html.append("
\n"); html.append("

에러 목록

\n"); if (errors.isEmpty()) { html.append("

검출된 에러가 없습니다.

\n"); } else { for (int i = 0; i < errors.size(); i++) { ErrorLogDto error = errors.get(i); html.append(renderErrorHtml(error, i + 1)); } } html.append("
\n"); // Footer html.append("
\n"); html.append("

Generated by LogHunter

\n"); html.append("
\n"); html.append("\n"); html.append(""); 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 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 getErrors(ExportRequest request) { return errorLogService.search( request.serverId(), request.patternId(), request.severity(), request.filePath(), 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 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("
\n"); sb.append("
\n"); sb.append(" #").append(index).append("\n"); sb.append(" ") .append(error.getSeverity()).append("\n"); sb.append(" ").append(formatDateTime(error.getOccurredAt())).append("\n"); sb.append("
\n"); sb.append("
\n"); sb.append(" 서버: ").append(escapeHtml(error.getServerName())).append("\n"); sb.append(" 패턴: ").append(escapeHtml(error.getPatternName())).append("\n"); sb.append(" 파일: ").append(escapeHtml(error.getFilePath())).append("\n"); sb.append(" 라인: ").append(error.getLineNumber()).append("\n"); sb.append("
\n"); sb.append("
\n"); sb.append(" 요약: ").append(escapeHtml(error.getSummary())).append("\n"); sb.append("
\n"); sb.append("
\n"); sb.append(" 컨텍스트:\n"); sb.append("
").append(escapeHtml(error.getContext())).append("
\n"); sb.append("
\n"); sb.append("
\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, String filePath, LocalDateTime startDate, LocalDateTime endDate, String keyword ) {} public record ExportResult( String filename, byte[] content, String contentType ) {} }