From 2d2b55efcde3833a2e86425852d9813af556b6e2 Mon Sep 17 00:00:00 2001 From: teddy Date: Wed, 11 Feb 2026 16:23:24 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=80=EC=9A=A9=EB=9F=89=20=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=EB=A1=9C=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../download/DownloadAuditEventListener.java | 92 ++++++++++++++++ .../download/dto/DownloadAuditEvent.java | 11 ++ .../config/FileDownloadInteceptor.java | 104 ++++++------------ .../InferenceResultApiController.java | 2 +- .../scheduler/config/AsyncConfig.java | 11 ++ 5 files changed, 150 insertions(+), 70 deletions(-) create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/download/DownloadAuditEventListener.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/download/dto/DownloadAuditEvent.java diff --git a/src/main/java/com/kamco/cd/kamcoback/common/download/DownloadAuditEventListener.java b/src/main/java/com/kamco/cd/kamcoback/common/download/DownloadAuditEventListener.java new file mode 100644 index 00000000..c0aeb418 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/download/DownloadAuditEventListener.java @@ -0,0 +1,92 @@ +package com.kamco.cd.kamcoback.common.download; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kamco.cd.kamcoback.common.download.dto.DownloadAuditEvent; +import com.kamco.cd.kamcoback.menu.dto.MenuDto; +import com.kamco.cd.kamcoback.menu.service.MenuService; +import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity; +import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class DownloadAuditEventListener { + + private final AuditLogRepository auditLogRepository; + private final MenuService menuService; + private final ObjectMapper objectMapper; + + @Async("auditLogExecutor") + @Transactional(propagation = Propagation.REQUIRES_NEW) + @EventListener + public void onDownloadAudit(DownloadAuditEvent ev) { + try { + String menuUid = resolveMenuUid(ev.normalizedUri()); + if (menuUid == null) { + // menuUid null 불가 -> 스킵 + log.warn( + "MenuUid not resolved. skip audit. uri={}, normalized={}", + ev.requestUri(), + ev.normalizedUri()); + return; + } + + AuditLogEntity logEntity = + AuditLogEntity.forFileDownload( + ev.userId(), ev.requestUri(), menuUid, ev.ip(), ev.status(), ev.downloadUuid()); + + auditLogRepository.save(logEntity); + + } catch (Exception e) { + // 본 요청과 분리되어야 함 + log.warn("Download audit save failed. uri={}, err={}", ev.requestUri(), e.toString()); + } + } + + private String resolveMenuUid(String normalizedUri) { + try { + List list = menuService.getFindAll(); + + List basics = + list.stream() + .map( + item -> { + if (item instanceof LinkedHashMap map) { + return objectMapper.convertValue(map, MenuDto.Basic.class); + } else if (item instanceof MenuDto.Basic dto) { + return dto; + } + return null; + }) + .filter(Objects::nonNull) + .toList(); + + MenuDto.Basic basic = + basics.stream() + .filter(m -> m.getMenuUrl() != null && normalizedUri.startsWith(m.getMenuUrl())) + .max(Comparator.comparingInt(m -> m.getMenuUrl().length())) + .orElse(null); + + if (basic == null) return null; + + String menuUidStr = basic.getMenuUid(); // ← String + if (menuUidStr == null || menuUidStr.isBlank()) return null; + + return menuUidStr; // ← Long 변환 + + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/download/dto/DownloadAuditEvent.java b/src/main/java/com/kamco/cd/kamcoback/common/download/dto/DownloadAuditEvent.java new file mode 100644 index 00000000..c19c8925 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/download/dto/DownloadAuditEvent.java @@ -0,0 +1,11 @@ +package com.kamco.cd.kamcoback.common.download.dto; + +import java.util.UUID; + +public record DownloadAuditEvent( + Long userId, + String requestUri, + String normalizedUri, + String ip, + int status, + UUID downloadUuid) {} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/FileDownloadInteceptor.java b/src/main/java/com/kamco/cd/kamcoback/config/FileDownloadInteceptor.java index 62687e9e..dde74774 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/FileDownloadInteceptor.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/FileDownloadInteceptor.java @@ -1,98 +1,64 @@ package com.kamco.cd.kamcoback.config; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.kamco.cd.kamcoback.common.utils.HeaderUtil; +import com.kamco.cd.kamcoback.common.download.dto.DownloadAuditEvent; import com.kamco.cd.kamcoback.common.utils.UserUtil; import com.kamco.cd.kamcoback.config.api.ApiLogFunction; -import com.kamco.cd.kamcoback.menu.dto.MenuDto; -import com.kamco.cd.kamcoback.menu.service.MenuService; -import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity; -import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository; +import jakarta.servlet.DispatcherType; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Objects; import java.util.UUID; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @Slf4j @Component +@RequiredArgsConstructor public class FileDownloadInteceptor implements HandlerInterceptor { - private final AuditLogRepository auditLogRepository; - private final MenuService menuService; + private final ApplicationEventPublisher publisher; private final UserUtil userUtil; - @Autowired private ObjectMapper objectMapper; - - public FileDownloadInteceptor( - AuditLogRepository auditLogRepository, MenuService menuService, UserUtil userUtil) { - this.auditLogRepository = auditLogRepository; - this.menuService = menuService; - this.userUtil = userUtil; - } - @Override - public boolean preHandle( - HttpServletRequest request, HttpServletResponse response, Object handler) { + public void afterCompletion( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { - if (!request.getRequestURI().contains("/download")) return true; + String uri = request.getRequestURI(); + if (uri == null || !uri.contains("/download")) return; + if (request.getDispatcherType() != DispatcherType.REQUEST) return; - if (request.getDispatcherType() != jakarta.servlet.DispatcherType.REQUEST) { - return true; - } - - saveLog(request, response); - - return true; - } - - private void saveLog(HttpServletRequest request, HttpServletResponse response) { - // 파일 다운로드 API만 필터링 - if (!request.getRequestURI().contains("/download")) { + Long userId; + try { + userId = userUtil.getId(); + if (userId == null) return; // userId null 불가면 스킵 + } catch (Exception e) { + log.warn("Download audit userId resolve failed. uri={}, err={}", uri, e.toString()); return; } - Long userId = userUtil.getId(); String ip = ApiLogFunction.getClientIp(request); + int status = response.getStatus(); + String normalizedUri = uri.replace("/api", ""); - List list = menuService.getFindAll(); - List result = - list.stream() - .map( - item -> { - if (item instanceof LinkedHashMap map) { - return objectMapper.convertValue(map, MenuDto.Basic.class); - } else if (item instanceof MenuDto.Basic dto) { - return dto; - } else { - throw new IllegalStateException("Unsupported cache type: " + item.getClass()); - } - }) - .toList(); + UUID downloadUuid = extractUuidFromUri(uri); + if (downloadUuid == null) { + log.warn("Download UUID parse failed. uri={}", uri); + return; // downloadUuid null 불가 -> 스킵 + } - String normalizedUri = request.getRequestURI().replace("/api", ""); - MenuDto.Basic basic = - result.stream() - .filter( - menu -> menu.getMenuUrl() != null && normalizedUri.startsWith(menu.getMenuUrl())) - .max(Comparator.comparingInt(m -> m.getMenuUrl().length())) - .orElse(null); + publisher.publishEvent( + new DownloadAuditEvent(userId, uri, normalizedUri, ip, status, downloadUuid)); + } - AuditLogEntity log = - AuditLogEntity.forFileDownload( - userId, - request.getRequestURI(), - Objects.requireNonNull(basic).getMenuUid(), - ip, - response.getStatus(), - UUID.fromString(HeaderUtil.get(request, "kamco-download-uuid"))); - - auditLogRepository.save(log); + private UUID extractUuidFromUri(String uri) { + try { + String[] parts = uri.split("/"); + String last = parts[parts.length - 1]; + return UUID.fromString(last); + } catch (Exception e) { + return null; + } } } diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java index 91ab9525..c985ee11 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java @@ -414,7 +414,7 @@ public class InferenceResultApiController { downloadReq.setStartDate(strtDttm); downloadReq.setEndDate(endDttm); downloadReq.setSearchValue(searchValue); - downloadReq.setRequestUri("/api/inference/download-audit/download/" + uuid); + downloadReq.setRequestUri("/api/inference/download/" + uuid); return ApiResponseDto.ok(inferenceResultService.getDownloadAudit(searchReq, downloadReq)); } diff --git a/src/main/java/com/kamco/cd/kamcoback/scheduler/config/AsyncConfig.java b/src/main/java/com/kamco/cd/kamcoback/scheduler/config/AsyncConfig.java index 77b99a93..e2eebdac 100644 --- a/src/main/java/com/kamco/cd/kamcoback/scheduler/config/AsyncConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/scheduler/config/AsyncConfig.java @@ -20,4 +20,15 @@ public class AsyncConfig { ex.initialize(); return ex; } + + @Bean(name = "auditLogExecutor") + public Executor auditLogExecutor() { + ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); + exec.setCorePoolSize(2); + exec.setMaxPoolSize(8); + exec.setQueueCapacity(2000); + exec.setThreadNamePrefix("auditlog-"); + exec.initialize(); + return exec; + } }