package com.zioinfo.mall.common; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; 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; import org.springframework.web.servlet.resource.NoResourceFoundException; /** * 전역 예외 처리. * *

보안 불변 규칙: 스택트레이스를 응답에 절대 노출하지 않는다. * 에러 코드 + 요약 메시지만 반환한다. */ @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-MALL-413: 파일 크기 초과 (최대 20MB)"); } @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()); } /** 정적 리소스 미존재 — 500 으로 감싸지 않고 깔끔히 404 반환 (SPA 폴백 이후의 실제 미스). */ @ExceptionHandler(NoResourceFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ApiResponse handleNoResource(NoResourceFoundException e) { log.warn("리소스 없음: {}", e.getResourcePath()); return ApiResponse.fail("ERR-MALL-404: 리소스를 찾을 수 없습니다"); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiResponse handleGeneral(Exception e) { log.error("시스템 오류", e); return ApiResponse.fail("ERR-SYS-001"); } }