Merge pull request '다운로드 호출 로그관리 작업' (#274) from feat/infer_dev_260107 into develop

Reviewed-on: https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice/pulls/274
This commit is contained in:
2026-01-19 17:12:09 +09:00
4 changed files with 136 additions and 6 deletions

View File

@@ -0,0 +1,89 @@
package com.kamco.cd.kamcoback.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.auth.CustomUserDetails;
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.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Component
public class FileDownloadInteceptor implements HandlerInterceptor {
private final AuditLogRepository auditLogRepository;
private final MenuService menuService;
@Autowired private ObjectMapper objectMapper;
public FileDownloadInteceptor(AuditLogRepository auditLogRepository, MenuService menuService) {
this.auditLogRepository = auditLogRepository;
this.menuService = menuService;
}
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 파일 다운로드 API만 필터링
if (!request.getRequestURI().contains("/download")) {
return;
}
Long userId = extractUserId(request);
String ip = ApiLogFunction.getClientIp(request);
List<?> list = menuService.getFindAll();
List<MenuDto.Basic> 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();
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);
AuditLogEntity log =
AuditLogEntity.forFileDownload(
userId,
request.getRequestURI(),
Objects.requireNonNull(basic).getMenuUid(),
ip,
response.getStatus());
auditLogRepository.save(log);
}
private Long extractUserId(HttpServletRequest request) {
if (request.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
&& auth.getPrincipal() instanceof CustomUserDetails userDetails) {
return userDetails.getMember().getId();
}
return null;
}
}

View File

@@ -10,11 +10,18 @@ import org.locationtech.jts.geom.Polygon;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final FileDownloadInteceptor fileDownloadInteceptor;
public WebConfig(FileDownloadInteceptor fileDownloadInteceptor) {
this.fileDownloadInteceptor = fileDownloadInteceptor;
}
@Bean
public ObjectMapper objectMapper() {
SimpleModule module = new SimpleModule();
@@ -29,4 +36,11 @@ public class WebConfig implements WebMvcConfigurer {
return Jackson2ObjectMapperBuilder.json().modulesToInstall(module).build();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(fileDownloadInteceptor)
.addPathPatterns("/api/inference/download/**"); // 추론 파일 다운로드 API만 //TODO 추후 추가
}
}

View File

@@ -5,6 +5,7 @@ import com.kamco.cd.kamcoback.log.dto.EventType;
import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import jakarta.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -47,7 +48,7 @@ public class ApiLogFunction {
String method = request.getMethod().toUpperCase();
String uri = request.getRequestURI().toLowerCase();
// URL 기반 DOWNLOAD/PRINT 분류
// URL 기반 DOWNLOAD/PRINT 분류 -> /download는 FileDownloadInterceptor로 옮김
if (uri.contains("/download") || uri.contains("/export")) {
return EventType.DOWNLOAD;
}
@@ -121,13 +122,15 @@ public class ApiLogFunction {
public static String getUriMenuInfo(List<MenuDto.Basic> menuList, String uri) {
MenuDto.Basic m =
String normalizedUri = uri.replace("/api", "");
MenuDto.Basic basic =
menuList.stream()
.filter(menu -> menu.getMenuApiUrl() != null && uri.contains(menu.getMenuApiUrl()))
.findFirst()
.filter(
menu -> menu.getMenuUrl() != null && normalizedUri.startsWith(menu.getMenuUrl()))
.max(Comparator.comparingInt(m -> m.getMenuUrl().length()))
.orElse(null);
return m != null ? m.getMenuUid() : "SYSTEM";
return basic != null ? basic.getMenuUid() : "SYSTEM";
}
public static String cutRequestBody(String value) {

View File

@@ -4,7 +4,14 @@ import com.kamco.cd.kamcoback.log.dto.AuditLogDto;
import com.kamco.cd.kamcoback.log.dto.EventStatus;
import com.kamco.cd.kamcoback.log.dto.EventType;
import com.kamco.cd.kamcoback.postgres.CommonCreateEntity;
import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -14,6 +21,7 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "tb_audit_log")
public class AuditLogEntity extends CommonCreateEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "audit_log_uid", nullable = false)
@@ -62,6 +70,22 @@ public class AuditLogEntity extends CommonCreateEntity {
this.errorLogUid = errorLogUid;
}
/** 파일 다운로드 이력 생성 */
public static AuditLogEntity forFileDownload(
Long userId, String requestUri, String menuUid, String ip, int httpStatus) {
return new AuditLogEntity(
userId,
EventType.DOWNLOAD, // 이벤트 타입 고정
httpStatus < 400 ? EventStatus.FAILED : EventStatus.SUCCESS, // 성공 여부
menuUid,
ip,
requestUri,
null, // requestBody 없음
null // errorLogUid 없음
);
}
public AuditLogDto.Basic toDto() {
return new AuditLogDto.Basic(
this.id,