diff --git a/backend/src/main/java/com/zioinfo/mall/common/GlobalExceptionHandler.java b/backend/src/main/java/com/zioinfo/mall/common/GlobalExceptionHandler.java index cac60a9..149cd12 100644 --- a/backend/src/main/java/com/zioinfo/mall/common/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/zioinfo/mall/common/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ 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; /** * 전역 예외 처리. @@ -39,6 +40,14 @@ public class GlobalExceptionHandler { 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) { diff --git a/backend/src/main/java/com/zioinfo/mall/config/SpaForwardController.java b/backend/src/main/java/com/zioinfo/mall/config/SpaForwardController.java new file mode 100644 index 0000000..f081b9f --- /dev/null +++ b/backend/src/main/java/com/zioinfo/mall/config/SpaForwardController.java @@ -0,0 +1,31 @@ +package com.zioinfo.mall.config; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * SPA(React Router) 폴백 — 클라이언트 라우트 딥링크/새로고침 지원. + * + *

스토어프론트(/, /product/**, /cart, /checkout ...) 와 관리자(/admin/**) 는 + * 모두 단일 index.html 위에서 React Router 가 처리한다. 서버에 해당 정적 파일이 + * 없으므로, API/정적자원이 아닌 모든 GET 요청을 index.html 로 forward 한다. + * + *

제외 대상: /api/**, /actuator/**, /ws/**, /swagger-ui/**, /v3/**, + * 그리고 점(.)이 포함된 실제 정적 리소스(*.js, *.css, *.jpg, *.png ...)는 + * Spring 기본 ResourceHandler 가 먼저 처리하므로 여기로 오지 않는다. + */ +@Controller +public class SpaForwardController { + + // 점(.)이 없는 단일/다중 세그먼트 경로 → SPA 라우트로 간주하고 index.html 로 forward. + // 정규식: 한 개 이상의 경로 세그먼트, 각 세그먼트에 '.' 미포함. + @RequestMapping(value = { + "/admin", "/admin/**", + "/{path:^(?!api|actuator|ws|swagger-ui|v3|assets|img|images|static)[^.]*}", + "/{path:^(?!api|actuator|ws|swagger-ui|v3|assets|img|images|static)[^.]*}/**" + }) + public String forwardSpa() { + return "forward:/index.html"; + } +}