From 480754f0ad39977388fffcbddbd6de87347529ac Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Wed, 20 Aug 2025 01:41:45 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20DTO?= =?UTF-8?q?=EC=97=90=EC=84=9C=20storeId=20=EC=A4=91=EB=B3=B5=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/Centralthon/domain/route/web/dto/RouteReq.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java b/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java index 578b74c..5b1dafd 100644 --- a/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java +++ b/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Getter; +import org.hibernate.validator.constraints.UniqueElements; import java.util.List; @@ -17,5 +18,6 @@ public class RouteReq{ @NotEmpty(message = "storeIds 리스트가 비어있습니다.") @Size(max = 8, message = "경유지는 최대 8개까지만 지정할 수 있습니다.") + @UniqueElements(message = "storeIds에 중복 값이 포함되어 있습니다.") private List storeIds; } From c279ce1f7cef95611bf0a8a4445a865cbe98deea Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Wed, 20 Aug 2025 02:57:38 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=EC=99=80=20sto?= =?UTF-8?q?reId=20=EA=B0=84=20=EB=A7=A4=ED=95=91=20=EC=95=88=EC=A0=95?= =?UTF-8?q?=EC=84=B1=20=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/route/service/RouteServiceImpl.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/Centralthon/domain/route/service/RouteServiceImpl.java b/src/main/java/com/example/Centralthon/domain/route/service/RouteServiceImpl.java index c97ab5c..a321387 100644 --- a/src/main/java/com/example/Centralthon/domain/route/service/RouteServiceImpl.java +++ b/src/main/java/com/example/Centralthon/domain/route/service/RouteServiceImpl.java @@ -32,11 +32,16 @@ public RouteRes findOptimalPath(RouteReq req) { List stores = storeRepository.findAllById(req.getStoreIds()); if (stores.isEmpty()) throw new StoreNotFoundException(); - List nodes = new ArrayList<>(); + List nodes = new ArrayList<>(stores.size() + 1); + List indexToStoreId = new ArrayList<>(stores.size() + 1); + nodes.add(new LocationRes(req.getUserLng(), req.getUserLat())); - nodes.addAll(stores.stream() - .map(s -> new LocationRes(s.getLongitude(), s.getLatitude())) - .toList()); + indexToStoreId.add(null); + + for (Store s : stores) { + nodes.add(new LocationRes(s.getLongitude(), s.getLatitude())); + indexToStoreId.add(s.getId()); + } // 1) 보행자 경로 행렬/세그먼트 사전 계산 PedMatrix pm = pedMatrixBuilder.build(nodes); @@ -53,7 +58,7 @@ public RouteRes findOptimalPath(RouteReq req) { // 5) storeId 순서 변환 List idOrder = order.stream() .filter(i -> i != 0) - .map(i -> stores.get(i - 1).getId()) + .map(indexToStoreId::get) .toList(); return new RouteRes( From 53028d4adde4bf59b83a7144a56bf6cc0548b59b Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Thu, 21 Aug 2025 16:56:22 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20API?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=20=EB=B0=8F=20=EC=95=88=EC=A0=95=EC=84=B1=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../route/service/PedMatrixBuilder.java | 34 ++++++++++++++----- .../domain/route/web/dto/RouteReq.java | 2 +- src/main/resources/application.properties | 8 ++++- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/Centralthon/domain/route/service/PedMatrixBuilder.java b/src/main/java/com/example/Centralthon/domain/route/service/PedMatrixBuilder.java index 350b85b..6cefd10 100644 --- a/src/main/java/com/example/Centralthon/domain/route/service/PedMatrixBuilder.java +++ b/src/main/java/com/example/Centralthon/domain/route/service/PedMatrixBuilder.java @@ -7,10 +7,13 @@ import com.example.Centralthon.global.util.geo.GeoUtils; import io.netty.handler.timeout.ReadTimeoutException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClientRequestException; import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; import java.time.Duration; import java.util.*; @@ -23,9 +26,17 @@ public class PedMatrixBuilder { private final PedestrianRoutingPort routingPort; - private static final Duration PER_CALL_TIMEOUT = Duration.ofSeconds(3); - private static final int RETRIES = 3; - private static final Duration RETRY_DELAY = Duration.ofMillis(250); + @Value("${route.ped.per-call-timeout:3s}") + private Duration perCallTimeout; + + @Value("${route.ped.retries:3}") + private int retries; + + @Value("${route.ped.retry-delay:250ms}") + private Duration retryDelay; + + @Value("${route.ped.concurrency:8}") + private int defaultConcurrency; public PedMatrix build(List nodes) { // 전체 노드 수 (0 = 사용자, 1..n = 가게) @@ -50,11 +61,11 @@ public PedMatrix build(List nodes) { LocationRes b = nodes.get(jj); Mono call = routingPort.fetchSegment(a, b) - .timeout(PER_CALL_TIMEOUT) + .timeout(perCallTimeout) // 일시적 오류만 3회 재시도 - .retryWhen(reactor.util.retry.Retry - .fixedDelay(RETRIES, RETRY_DELAY) - .filter(this::isTransientError)) + .retryWhen(Retry.fixedDelay(retries, retryDelay) + .filter(this::isTransientError) + .jitter(0.5)) // 실패 시 하버사인 폴백 .onErrorResume(ex -> { fallbackCount.incrementAndGet(); @@ -71,7 +82,14 @@ public PedMatrix build(List nodes) { calls.add(call); } } - Mono.when(calls).block(); + int pairs = calls.size(); + int concurrency = Math.min(defaultConcurrency, Math.max(1, pairs)); + + // 동시에 최대 concurrency개만 실행 + Flux.fromIterable(calls) + .flatMap(m -> m, concurrency) + .then() + .block(); // 모든 세그먼트가 폴백 시 예외 처리 int totalCalls = calls.size(); diff --git a/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java b/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java index 5b1dafd..85e2fe6 100644 --- a/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java +++ b/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java @@ -17,7 +17,7 @@ public class RouteReq{ private Double userLng; @NotEmpty(message = "storeIds 리스트가 비어있습니다.") - @Size(max = 8, message = "경유지는 최대 8개까지만 지정할 수 있습니다.") + @Size(max = 5, message = "경유지는 최대 5개까지만 지정할 수 있습니다.") @UniqueElements(message = "storeIds에 중복 값이 포함되어 있습니다.") private List storeIds; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e73e320..cee39d0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -23,4 +23,10 @@ springdoc.api-docs.path=/api-docs springdoc.group-configs[0].group=default springdoc.group-configs[0].paths-to-match=/** springdoc.api-docs.enabled=true -springdoc.swagger-ui.enabled=true \ No newline at end of file +springdoc.swagger-ui.enabled=true + +# TmapBuilder +route.ped.per-call-timeout=3s +route.ped.retries=3 +route.ped.retry-delay=250ms +route.ped.concurrency=8 \ No newline at end of file From 36ad824f4576e7ff8799457730bbe9df5fcb0cc1 Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Thu, 21 Aug 2025 18:45:52 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20=EC=88=9C=ED=9A=8C=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../route/client/TmapPedestrianParser.java | 27 ++++++++++--------- .../route/service/PedMatrixBuilder.java | 3 +++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/example/Centralthon/domain/route/client/TmapPedestrianParser.java b/src/main/java/com/example/Centralthon/domain/route/client/TmapPedestrianParser.java index 5f112c5..0b26e54 100644 --- a/src/main/java/com/example/Centralthon/domain/route/client/TmapPedestrianParser.java +++ b/src/main/java/com/example/Centralthon/domain/route/client/TmapPedestrianParser.java @@ -14,6 +14,9 @@ public class TmapPedestrianParser { public PedSegment parsePedestrian(Map pedRes) { long totalDistance = 0L; long totalDuration = 0L; + boolean gotDistance = false; + boolean gotDuration = false; + List path = new ArrayList<>(); List> features = @@ -22,20 +25,18 @@ public PedSegment parsePedestrian(Map pedRes) { // 1) 총거리/시간 추출 for (Map f : features) { Map props = (Map) f.get("properties"); - if (props == null) continue; - - Object d = props.get("totalDistance"); - long candDist = asLong(d); - if (candDist > 0) totalDistance = candDist; - - Object t = props.get("totalTime"); - long candTime = asLong(t); - if (candTime > 0) totalDuration = candTime; - if (totalDistance > 0 && totalDuration > 0) break; - } + if(props != null){ + if(!gotDistance){ + long candDist = asLong(props.get("totalDistance")); + if (candDist > 0) { totalDistance = candDist; gotDistance = true; } + } + if(!gotDuration){ + long candTime = asLong(props.get("totalTime")); + if (candTime > 0) { totalDuration = candTime; gotDuration = true; } + } + } - // 2) LineString 경로 좌표 이어붙이기 (중복점 제거) - for (Map f : features) { + // 2) LineString 경로 좌표 이어붙이기 (중복점 제거) Map geom = (Map) f.get("geometry"); if (geom == null) continue; String type = String.valueOf(geom.get("type")); diff --git a/src/main/java/com/example/Centralthon/domain/route/service/PedMatrixBuilder.java b/src/main/java/com/example/Centralthon/domain/route/service/PedMatrixBuilder.java index 6cefd10..adb9f82 100644 --- a/src/main/java/com/example/Centralthon/domain/route/service/PedMatrixBuilder.java +++ b/src/main/java/com/example/Centralthon/domain/route/service/PedMatrixBuilder.java @@ -7,6 +7,7 @@ import com.example.Centralthon.global.util.geo.GeoUtils; import io.netty.handler.timeout.ReadTimeoutException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClientRequestException; @@ -23,6 +24,7 @@ @Component @RequiredArgsConstructor +@Slf4j public class PedMatrixBuilder { private final PedestrianRoutingPort routingPort; @@ -68,6 +70,7 @@ public PedMatrix build(List nodes) { .jitter(0.5)) // 실패 시 하버사인 폴백 .onErrorResume(ex -> { + log.warn("API 호출 실패, 원인 : {}",ex.getMessage()); fallbackCount.incrementAndGet(); double dMeter = GeoUtils.calculateDistance(a.lat(), a.lng(), b.lat(), b.lng()); return Mono.just(new PedSegment(Math.round(dMeter), 0L, List.of(a, b))); From 84766c7dec01b357d10bff18958e74795ecdb652 Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Thu, 21 Aug 2025 20:28:14 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20reafctor=20:=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=97=B0=EC=82=B0=20?= =?UTF-8?q?=EC=A4=84=EC=9D=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/Centralthon/domain/route/algo/TspSolver.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/example/Centralthon/domain/route/algo/TspSolver.java b/src/main/java/com/example/Centralthon/domain/route/algo/TspSolver.java index a759ed5..473e1be 100644 --- a/src/main/java/com/example/Centralthon/domain/route/algo/TspSolver.java +++ b/src/main/java/com/example/Centralthon/domain/route/algo/TspSolver.java @@ -2,6 +2,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; +import java.util.stream.IntStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -11,6 +12,10 @@ public final class TspSolver { public static List solveOpenTour(double[][] d) { int k = d.length; + if (k <= 1) { + return IntStream.range(0, k).boxed().toList(); // [ ] or [0] + } + // 초기화 작업 boolean[] used = new boolean[k]; List path = new ArrayList<>(); @@ -43,6 +48,7 @@ private static List twoOpt(List tour, double[][] d) { int n = tour.size(); while (improved) { improved = false; + outer: for (int i = 1; i < n - 2; i++) { for (int k = i + 1; k < n - 1; k++) { double delta = @@ -53,6 +59,7 @@ private static List twoOpt(List tour, double[][] d) { if (delta < -1e-6) { Collections.reverse(tour.subList(i, k + 1)); improved = true; + break outer; } } } From 85a43b2ab1b79649a1772299f0d971117227e582 Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Sat, 23 Aug 2025 15:27:38 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20rout?= =?UTF-8?q?eReq=20=EC=9C=84=EB=8F=84,=20=EA=B2=BD=EB=8F=84=EA=B0=92=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Centralthon/domain/route/web/dto/RouteReq.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java b/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java index 85e2fe6..bf76909 100644 --- a/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java +++ b/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java @@ -1,8 +1,6 @@ package com.example.Centralthon.domain.route.web.dto; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.*; import lombok.Getter; import org.hibernate.validator.constraints.UniqueElements; @@ -10,9 +8,13 @@ @Getter public class RouteReq{ + @DecimalMin(value = "-90.0", message = "위도는 -90 이상이어야 합니다.") + @DecimalMax(value = "90.0", message = "위도는 90 이하이어야 합니다.") @NotNull(message = "userLat을 입력해주세요.") private Double userLat; + @DecimalMin(value = "-180.0", message = "경도는 -180 이상이어야 합니다.") + @DecimalMax(value = "180.0", message = "경도는 180 이하이어야 합니다.") @NotNull(message = "userLng을 입력해주세요.") private Double userLng; From 0d43dbf5dc170a0ade9f93f35990f514a9cc78f4 Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Sun, 24 Aug 2025 04:24:33 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20Requ?= =?UTF-8?q?estParam=20=EC=9C=84=EB=8F=84,=20=EA=B2=BD=EB=8F=84=EA=B0=92=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/menu/web/controller/MenuApi.java | 10 ++++++---- .../menu/web/controller/MenuController.java | 10 ++++++---- .../domain/route/web/dto/RouteReq.java | 2 +- .../domain/store/web/controller/StoreApi.java | 10 ++++++---- .../store/web/controller/StoreController.java | 10 ++++++---- .../exception/GlobalExceptionHandler.java | 17 +++++++++++++++++ .../global/validation/Latitude.java | 18 ++++++++++++++++++ .../global/validation/Longitude.java | 18 ++++++++++++++++++ 8 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/example/Centralthon/global/validation/Latitude.java create mode 100644 src/main/java/com/example/Centralthon/global/validation/Longitude.java diff --git a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java index c9797ae..b96a3b4 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java +++ b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuApi.java @@ -4,6 +4,8 @@ import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; import com.example.Centralthon.global.response.SuccessResponse; +import com.example.Centralthon.global.validation.Latitude; +import com.example.Centralthon.global.validation.Longitude; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -56,9 +58,9 @@ public interface MenuApi { ) ResponseEntity>> nearbyMenus( @Parameter(name = "lat", description = "사용자 위도", example = "37.468355", required = true) - @RequestParam("lat") Double lat, + @RequestParam("lat") @Latitude Double lat, @Parameter(name = "lng", description = "사용자 경도", example = "127.039073", required = true) - @RequestParam("lng") Double lng); + @RequestParam("lng") @Longitude Double lng); @Operation( summary = "특정 메뉴 판매 가게 조회", @@ -104,9 +106,9 @@ ResponseEntity>> storesByMenu( @Parameter(name = "name", description = "메뉴 이름(공백 포함하여 완전일치)", example = "두부 조림", required = true) @RequestParam("name") String name, @Parameter(name = "lat", description = "사용자 위도", example = "37.468355", required = true) - @RequestParam("lat") Double lat, + @RequestParam("lat") @Latitude Double lat, @Parameter(name = "lng", description = "사용자 경도", example = "127.039073", required = true) - @RequestParam("lng") Double lng); + @RequestParam("lng") @Longitude Double lng); @Operation( summary = "메뉴 상세 조회", diff --git a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java index 7f068ab..2ec717b 100644 --- a/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java +++ b/src/main/java/com/example/Centralthon/domain/menu/web/controller/MenuController.java @@ -7,6 +7,8 @@ import com.example.Centralthon.global.external.ai.web.dto.GetTipReq; import com.example.Centralthon.global.external.ai.web.dto.GetTipRes; import com.example.Centralthon.global.response.SuccessResponse; +import com.example.Centralthon.global.validation.Latitude; +import com.example.Centralthon.global.validation.Longitude; import jakarta.validation.Valid; import com.example.Centralthon.domain.menu.web.dto.NearbyMenusRes; @@ -29,8 +31,8 @@ public class MenuController implements MenuApi { @GetMapping("") @Override public ResponseEntity>> nearbyMenus( - @RequestParam("lat") Double lat, - @RequestParam("lng") Double lng) { + @RequestParam("lat") @Latitude Double lat, + @RequestParam("lng") @Longitude Double lng) { List menus = menuService.nearbyMenus(lat,lng); @@ -42,8 +44,8 @@ public ResponseEntity>> nearbyMenus( @Override public ResponseEntity>> storesByMenu( @RequestParam("name") String name, - @RequestParam("lat") Double lat, - @RequestParam("lng") Double lng) { + @RequestParam("lat") @Latitude Double lat, + @RequestParam("lng") @Longitude Double lng) { List stores = menuService.storesByMenu(name,lat,lng); diff --git a/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java b/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java index bf76909..6309f69 100644 --- a/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java +++ b/src/main/java/com/example/Centralthon/domain/route/web/dto/RouteReq.java @@ -19,7 +19,7 @@ public class RouteReq{ private Double userLng; @NotEmpty(message = "storeIds 리스트가 비어있습니다.") - @Size(max = 5, message = "경유지는 최대 5개까지만 지정할 수 있습니다.") + @Size(max = 8, message = "경유지는 최대 8개까지만 지정할 수 있습니다.") @UniqueElements(message = "storeIds에 중복 값이 포함되어 있습니다.") private List storeIds; } diff --git a/src/main/java/com/example/Centralthon/domain/store/web/controller/StoreApi.java b/src/main/java/com/example/Centralthon/domain/store/web/controller/StoreApi.java index 92f0098..d997b09 100644 --- a/src/main/java/com/example/Centralthon/domain/store/web/controller/StoreApi.java +++ b/src/main/java/com/example/Centralthon/domain/store/web/controller/StoreApi.java @@ -6,6 +6,8 @@ import com.example.Centralthon.domain.store.web.dto.NearbyStoresRes; import com.example.Centralthon.domain.store.web.dto.StoreMenusRes; import com.example.Centralthon.global.response.SuccessResponse; +import com.example.Centralthon.global.validation.Latitude; +import com.example.Centralthon.global.validation.Longitude; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; @@ -60,9 +62,9 @@ public interface StoreApi { ) ResponseEntity>> nearbyStores( @Parameter(name = "lat", description = "사용자 위도", example = "37.468355", required = true) - @RequestParam("lat") Double lat, + @RequestParam("lat") @Latitude Double lat, @Parameter(name = "lng", description = "사용자 경도", example = "127.039073", required = true) - @RequestParam("lng") Double lng); + @RequestParam("lng") @Longitude Double lng); @Operation( summary = "가게에서 판매 중인 메뉴 조회", @@ -117,7 +119,7 @@ ResponseEntity> getStoreMenus( @Parameter(name = "storeId", in = ParameterIn.PATH, description = "가게 ID", example = "1", required = true) @PathVariable Long storeId, @Parameter(name = "lat", in = ParameterIn.QUERY, description = "사용자 위도", example = "37.468355", required = true) - @RequestParam("lat") Double lat, + @RequestParam("lat") @Latitude Double lat, @Parameter(name = "lng", in = ParameterIn.QUERY, description = "사용자 경도", example = "127.039073", required = true) - @RequestParam("lng") Double lng); + @RequestParam("lng") @Longitude Double lng); } diff --git a/src/main/java/com/example/Centralthon/domain/store/web/controller/StoreController.java b/src/main/java/com/example/Centralthon/domain/store/web/controller/StoreController.java index 6ffff81..dfe2421 100644 --- a/src/main/java/com/example/Centralthon/domain/store/web/controller/StoreController.java +++ b/src/main/java/com/example/Centralthon/domain/store/web/controller/StoreController.java @@ -5,6 +5,8 @@ import com.example.Centralthon.domain.store.web.dto.NearbyStoresRes; import com.example.Centralthon.domain.store.web.dto.StoreMenusRes; import com.example.Centralthon.global.response.SuccessResponse; +import com.example.Centralthon.global.validation.Latitude; +import com.example.Centralthon.global.validation.Longitude; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -22,8 +24,8 @@ public class StoreController implements StoreApi { @GetMapping("") @Override public ResponseEntity>> nearbyStores( - @RequestParam("lat") Double lat, - @RequestParam("lng") Double lng){ + @RequestParam("lat") @Latitude Double lat, + @RequestParam("lng") @Longitude Double lng){ List stores = storeService.nearbyStores(lat, lng); @@ -35,8 +37,8 @@ public ResponseEntity>> nearbyStores( @Override public ResponseEntity> getStoreMenus( @PathVariable Long storeId, - @RequestParam("lat") Double lat, - @RequestParam("lng") Double lng){ + @RequestParam("lat") @Latitude Double lat, + @RequestParam("lng") @Longitude Double lng){ StoreMenusRes menus = storeService.getStoreMenus(storeId, lat, lng); diff --git a/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java index 68c1624..9ca3076 100644 --- a/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/Centralthon/global/exception/GlobalExceptionHandler.java @@ -15,10 +15,12 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException; +import java.util.List; @RestControllerAdvice @@ -106,6 +108,21 @@ public ResponseEntity> handleMissingRequestParam(MissingServlet return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse); } + // @RequestParam 검증 실패 (Validation) + @ExceptionHandler(HandlerMethodValidationException.class) + public ResponseEntity> handleHandlerMethodValidation(HandlerMethodValidationException e) { + log.error("HandlerMethodValidationException : {}", e.getMessage(), e); + + List names = e.getParameterValidationResults().stream() + .map(r -> r.getMethodParameter().getParameterName()) + .distinct() + .toList(); + String msg = "잘못된 요청 파라미터: " + String.join(", ", names); + + ErrorResponse errorResponse = ErrorResponse.of(ErrorResponseCode.INVALID_HTTP_MESSAGE_PARAMETER,msg); + return ResponseEntity.status(errorResponse.getHttpStatus()).body(errorResponse); + } + // 나머지 예외 처리 @ExceptionHandler(Exception.class) public ResponseEntity> handleException(Exception e) { diff --git a/src/main/java/com/example/Centralthon/global/validation/Latitude.java b/src/main/java/com/example/Centralthon/global/validation/Latitude.java new file mode 100644 index 0000000..794187a --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/validation/Latitude.java @@ -0,0 +1,18 @@ +package com.example.Centralthon.global.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import java.lang.annotation.*; + +@Constraint(validatedBy = {}) +@Target({ ElementType.PARAMETER, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@DecimalMin(value = "-90.0") +@DecimalMax(value = "90.0") +public @interface Latitude { + String message() default "유효하지 않은 위도입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/example/Centralthon/global/validation/Longitude.java b/src/main/java/com/example/Centralthon/global/validation/Longitude.java new file mode 100644 index 0000000..156cbff --- /dev/null +++ b/src/main/java/com/example/Centralthon/global/validation/Longitude.java @@ -0,0 +1,18 @@ +package com.example.Centralthon.global.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import java.lang.annotation.*; + +@Constraint(validatedBy = {}) +@Target({ ElementType.PARAMETER, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@DecimalMin(value = "-180.0") +@DecimalMax(value = "180.0") +public @interface Longitude { + String message() default "유효하지 않은 경도입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} From 7efba750082f52b0f2c176dbbb4100cae29b849f Mon Sep 17 00:00:00 2001 From: oroi2009 Date: Sun, 24 Aug 2025 18:26:30 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor=20:=20Path?= =?UTF-8?q?Stitcher=20main=EA=B3=BC=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Centralthon/domain/route/algo/PathStitcher.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/example/Centralthon/domain/route/algo/PathStitcher.java b/src/main/java/com/example/Centralthon/domain/route/algo/PathStitcher.java index 4116656..163bf99 100644 --- a/src/main/java/com/example/Centralthon/domain/route/algo/PathStitcher.java +++ b/src/main/java/com/example/Centralthon/domain/route/algo/PathStitcher.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -23,6 +24,12 @@ public static CombinedPath stitch(List order, Map p = seg.path(); + + if (i > j && p != null && p.size() >= 2) { + p = new ArrayList<>(p); + Collections.reverse(p); + } + if (merged.isEmpty()) merged.addAll(p); else { if (!p.isEmpty() && !merged.isEmpty()