package research.loghunter.service; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import research.loghunter.dto.ErrorLogDto; import research.loghunter.dto.FileTreeDto; import research.loghunter.entity.ErrorLog; import research.loghunter.entity.Server; import research.loghunter.repository.ErrorLogRepository; import research.loghunter.repository.ScannedFileRepository; import research.loghunter.repository.ServerRepository; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class ErrorLogService { private final ErrorLogRepository errorLogRepository; private final ScannedFileRepository scannedFileRepository; private final ServerRepository serverRepository; public Page search( Long serverId, Long patternId, String severity, String filePath, LocalDateTime startDate, LocalDateTime endDate, String keyword, int page, int size ) { Pageable pageable = PageRequest.of(page, size); Page errorLogs = errorLogRepository.searchErrors( serverId, patternId, severity, filePath, startDate, endDate, keyword, pageable); return errorLogs.map(this::toDto); } public Page findByServerId(Long serverId, int page, int size) { Pageable pageable = PageRequest.of(page, size); Page errorLogs = errorLogRepository.findByServerIdOrderByOccurredAtDesc(serverId, pageable); return errorLogs.map(this::toDto); } public ErrorLogDto findById(Long id) { return errorLogRepository.findById(id) .map(this::toDto) .orElseThrow(() -> new RuntimeException("ErrorLog not found: " + id)); } /** * 트리 구조 데이터 조회 */ public List getFileTree() { // 파일별 에러 통계 조회 List stats = errorLogRepository.getFileErrorStats(); // 서버별로 그룹핑 Map> serverGroups = new LinkedHashMap<>(); for (Object[] stat : stats) { Long serverId = ((Number) stat[0]).longValue(); serverGroups.computeIfAbsent(serverId, k -> new ArrayList<>()).add(stat); } List result = new ArrayList<>(); for (Map.Entry> entry : serverGroups.entrySet()) { Long serverId = entry.getKey(); List serverStats = entry.getValue(); String serverName = (String) serverStats.get(0)[1]; // 경로별로 그룹핑 Map> pathGroups = new LinkedHashMap<>(); int serverTotalErrors = 0; for (Object[] stat : serverStats) { String filePath = (String) stat[2]; int errorCount = ((Number) stat[3]).intValue(); int criticalCount = ((Number) stat[4]).intValue(); int errorLevelCount = ((Number) stat[5]).intValue(); int warnCount = ((Number) stat[6]).intValue(); // 경로와 파일명 분리 int lastSlash = filePath.lastIndexOf('/'); String path = lastSlash > 0 ? filePath.substring(0, lastSlash) : "/"; String fileName = lastSlash > 0 ? filePath.substring(lastSlash + 1) : filePath; FileTreeDto.FileNode fileNode = FileTreeDto.FileNode.builder() .filePath(filePath) .fileName(fileName) .errorCount(errorCount) .criticalCount(criticalCount) .errorLevelCount(errorLevelCount) .warnCount(warnCount) .build(); pathGroups.computeIfAbsent(path, k -> new ArrayList<>()).add(fileNode); serverTotalErrors += errorCount; } // PathNode 생성 List pathNodes = new ArrayList<>(); for (Map.Entry> pathEntry : pathGroups.entrySet()) { List files = pathEntry.getValue(); int pathTotalErrors = files.stream().mapToInt(FileTreeDto.FileNode::getErrorCount).sum(); pathNodes.add(FileTreeDto.PathNode.builder() .path(pathEntry.getKey()) .totalErrorCount(pathTotalErrors) .files(files) .build()); } result.add(FileTreeDto.ServerNode.builder() .serverId(serverId) .serverName(serverName) .totalErrorCount(serverTotalErrors) .paths(pathNodes) .build()); } return result; } /** * 서버별 파일 목록 조회 */ public List getFilesByServer(Long serverId) { if (serverId == null) { return errorLogRepository.findDistinctFilePaths(); } return errorLogRepository.findDistinctFilePathsByServerId(serverId); } /** * 선택한 ID들의 에러 삭제 */ @Transactional public int deleteByIds(List ids) { if (ids == null || ids.isEmpty()) { return 0; } return errorLogRepository.deleteByIdIn(ids); } /** * 파일 삭제 (에러로그 + 스캔기록) */ @Transactional public Map deleteFileAndErrors(Long serverId, String filePath) { int deletedErrors = errorLogRepository.deleteByServerIdAndFilePath(serverId, filePath); int deletedFiles = scannedFileRepository.deleteByServerIdAndFilePath(serverId, filePath); return Map.of( "success", true, "deletedErrors", deletedErrors, "deletedScannedFiles", deletedFiles ); } /** * 서버+파일 기준 에러 삭제 */ @Transactional public int deleteByServerAndFile(Long serverId, String filePath) { return errorLogRepository.deleteByServerIdAndFilePath(serverId, filePath); } private ErrorLogDto toDto(ErrorLog errorLog) { return ErrorLogDto.builder() .id(errorLog.getId()) .serverId(errorLog.getServer().getId()) .serverName(errorLog.getServer().getName()) .patternId(errorLog.getPattern().getId()) .patternName(errorLog.getPattern().getName()) .filePath(errorLog.getFilePath()) .lineNumber(errorLog.getLineNumber()) .summary(errorLog.getSummary()) .context(errorLog.getContext()) .severity(errorLog.getSeverity()) .occurredAt(errorLog.getOccurredAt()) .createdAt(errorLog.getCreatedAt()) .build(); } }