package com.zioinfo.mes.common; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.multipart.MaxUploadSizeExceededException; /** * 전역 예외 처리. * *

보안 불변 규칙: 스택트레이스를 응답에 절대 노출하지 않는다. * 에러 코드 + 요약 메시지만 반환한다. */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MaxUploadSizeExceededException.class) @ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE) public ApiResponse handleMaxSize(MaxUploadSizeExceededException e) { log.warn("업로드 크기 초과: {}", e.getMessage()); return ApiResponse.fail("ERR-MES-413: 파일 크기 초과 (최대 20MB)"); } @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public ApiResponse handleAccessDenied(AccessDeniedException e) { log.warn("권한 거부: {}", e.getMessage()); return ApiResponse.fail("ERR-MES-403: 권한이 없습니다"); } @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResponse handleIllegalArg(IllegalArgumentException e) { log.warn("잘못된 요청: {}", e.getMessage()); return ApiResponse.fail(e.getMessage()); } @ExceptionHandler(RuntimeException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResponse handleRuntime(RuntimeException e) { // 스택트레이스 미노출 — 에러 코드/요약만 반환 log.warn("업무 오류: {}", e.getMessage()); return ApiResponse.fail(e.getMessage()); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiResponse handleGeneral(Exception e) { log.error("시스템 오류", e); return ApiResponse.fail("ERR-SYS-001"); } }