diff --git a/.github/workflows/cd-develop.yml b/.github/workflows/cd-develop.yml index f4251d64..48881ed1 100644 --- a/.github/workflows/cd-develop.yml +++ b/.github/workflows/cd-develop.yml @@ -69,16 +69,19 @@ jobs: run: | echo "๐Ÿงน Cleaning up all existing containers" - # Stop and remove specific containers + # Stop and remove specific containers (๋กœ๊ทธ๋Š” ๋ณผ๋ฅจ์— ๋ณด์กด๋จ) sudo docker stop github-actions-demo || true sudo docker rm github-actions-demo || true sudo docker stop elasticsearch || true sudo docker rm elasticsearch || true + echo "๐Ÿ“‹ Ensuring log directory exists on host" + sudo mkdir -p /var/log/wayble + sudo chmod 755 /var/log/wayble - echo "๐Ÿงฏ Cleaning up unused Docker networks" - sudo docker system prune -f || true + echo "๐Ÿงฏ Cleaning up unused Docker networks (excluding volumes)" + sudo docker system prune -f --volumes=false || true - name: Create Docker network if not exists run: | @@ -184,6 +187,18 @@ jobs: exit 1 fi + # ๋กœ๊ทธ ํŒŒ์ผ ์ƒํƒœ ํ™•์ธ + echo "=== Log Directory Status ===" + ls -la /var/log/wayble/ || echo "Log directory not found" + + if [ -f "/var/log/wayble/wayble-error.log" ]; then + echo "โœ… Error log file exists" + echo "๐Ÿ“Š Error log file size: $(du -h /var/log/wayble/wayble-error.log | cut -f1)" + echo "๐Ÿ“… Last modified: $(stat -c %y /var/log/wayble/wayble-error.log)" + else + echo "โ„น๏ธ No error log file yet (normal for new deployment)" + fi + # โœ… ๋ฐฐํฌ ์„ฑ๊ณต ์•Œ๋ฆผ (Discord) - name: Send success webhook to Discord if: success() @@ -204,7 +219,6 @@ jobs: - # on: #์ด ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ์–ธ์ œ ์‹คํ–‰๋ ์ง€ ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ์ •์˜ํ•จ. # pull_request: # types : [closed] #๋ˆ„๊ตฐ๊ฐ€๊ฐ€ Pull request๋ฅผ ๋‹ซ์•˜์„ ๋•Œ ์‹คํ–‰๋จ. diff --git a/Dockerfile b/Dockerfile index ba724222..fb3ffbf6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,12 @@ FROM openjdk:17 # ์ธ์ž ์„ค์ • - JAR_File ARG JAR_FILE=build/libs/*.jar +# ๋กœ๊ทธ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ ๋ฐ ๊ถŒํ•œ ์„ค์ • +RUN mkdir -p /app/logs && chmod 755 /app/logs + +# ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • +WORKDIR /app + # jar ํŒŒ์ผ ๋ณต์ œ COPY ${JAR_FILE} app.jar diff --git a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java index ce6ace69..22288ba5 100644 --- a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java @@ -38,7 +38,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(ApplicationException.class) public ResponseEntity handleApplicationException(ApplicationException e, WebRequest request) { - // ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ ๋กœ๊ทธ ๊ธฐ๋ก (๊ฐ„๊ฒฐํ•˜๊ฒŒ) + String path = ((ServletWebRequest) request).getRequest().getRequestURI(); String method = ((ServletWebRequest) request).getRequest().getMethod(); @@ -47,36 +47,31 @@ public ResponseEntity handleApplicationException(ApplicationExce CommonResponse commonResponse = CommonResponse.error(e.getErrorCase()); - HttpStatus status = HttpStatus.valueOf(e.getErrorCase().getHttpStatusCode()); - //sendToDiscord(e, request, status); - return ResponseEntity .status(e.getErrorCase().getHttpStatusCode()) .body(commonResponse); } - @ExceptionHandler(value = MethodArgumentNotValidException.class) - public ResponseEntity handleValidException(BindingResult bindingResult, - MethodArgumentNotValidException ex, - WebRequest request) { - String message = bindingResult.getAllErrors().get(0).getDefaultMessage(); + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidException(MethodArgumentNotValidException ex, WebRequest request) { + String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage(); // ์—๋Ÿฌ ๋กœ๊ทธ ๊ธฐ๋ก String path = ((ServletWebRequest) request).getRequest().getRequestURI(); String method = ((ServletWebRequest) request).getRequest().getMethod(); String errorLocation = getErrorLocation(ex); - log.error("Validation Exception ๋ฐœ์ƒ - Method: {}, Path: {}, Message: {}, Location: {}", - method, path, message, errorLocation, ex); + log.warn("Validation Exception - Method: {}, Path: {}, Message: {}, Location: {}", + method, path, message, errorLocation); CommonResponse commonResponse = CommonResponse.error(400, message); - - sendToDiscord(ex, request, HttpStatus.BAD_REQUEST); + return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(commonResponse); } + /** * ๋ชจ๋“  ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ */ @@ -107,6 +102,12 @@ private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status) return; } + // ํŠน์ • ์˜ˆ์™ธ ํƒ€์ž… ๋ฐ ๊ฒฝ๋กœ์— ๋Œ€ํ•œ Discord ์•Œ๋ฆผ ์ œ์™ธ + if (shouldSkipDiscordNotification(ex, path)) { + log.debug("Discord ์•Œ๋ฆผ ์ œ์™ธ - Exception: {}, Path: {}", ex.getClass().getSimpleName(), path); + return; + } + // Embed ํ•„๋“œ ๊ตฌ์„ฑ DiscordWebhookPayload.Embed embed = new DiscordWebhookPayload.Embed( "๐Ÿšจ ์„œ๋ฒ„ ์—๋Ÿฌ ๋ฐœ์ƒ", @@ -136,6 +137,108 @@ private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status) } } + /** + * Discord ์•Œ๋ฆผ์„ ๋ณด๋‚ด์ง€ ์•Š์„ ์˜ˆ์™ธ์ธ์ง€ ํŒ๋‹จ + */ + private boolean shouldSkipDiscordNotification(Exception ex, String path) { + String exceptionName = ex.getClass().getSimpleName(); + String message = ex.getMessage(); + + // 1. NoResourceFoundException ์ œ์™ธ (static resource ์š”์ฒญ) + if ("NoResourceFoundException".equals(exceptionName)) { + return true; + } + + // 2. ํŠน์ • ๊ฒฝ๋กœ ํŒจํ„ด ์ œ์™ธ + if (isIgnoredPath(path)) { + return true; + } + + // 3. ๋ด‡์ด๋‚˜ ํฌ๋กค๋Ÿฌ ์š”์ฒญ์œผ๋กœ ์ธํ•œ ์—๋Ÿฌ ์ œ์™ธ + if (isBotOrCrawlerRequest(message)) { + return true; + } + + // 4. ๊ธฐํƒ€ ๋ถˆํ•„์š”ํ•œ ์˜ˆ์™ธ๋“ค + if (isIgnoredException(exceptionName, message)) { + return true; + } + + return false; + } + + /** + * ๋ฌด์‹œํ•  ๊ฒฝ๋กœ์ธ์ง€ ํ™•์ธ + */ + private boolean isIgnoredPath(String path) { + String[] ignoredPaths = { + "/favicon.ico", + "/index.html", + "/robots.txt", + "/sitemap.xml", + "/apple-touch-icon", + "/.well-known/", + "/wp-admin/", + "/admin/", + "/phpmyadmin/", + "/xmlrpc.php", + "/.env", + "/config.php" + }; + + for (String ignoredPath : ignoredPaths) { + if (path.contains(ignoredPath)) { + return true; + } + } + + return false; + } + + /** + * ๋ด‡์ด๋‚˜ ํฌ๋กค๋Ÿฌ ์š”์ฒญ์ธ์ง€ ํ™•์ธ + */ + private boolean isBotOrCrawlerRequest(String message) { + if (message == null) return false; + + String[] botIndicators = { + "No static resource", + "Could not resolve view", + "favicon", + "robots.txt" + }; + + for (String indicator : botIndicators) { + if (message.contains(indicator)) { + return true; + } + } + + return false; + } + + /** + * ๋ฌด์‹œํ•  ์˜ˆ์™ธ์ธ์ง€ ํ™•์ธ + */ + private boolean isIgnoredException(String exceptionName, String message) { + // ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ์ข…๋ฃŒ ๊ด€๋ จ + if ("ClientAbortException".equals(exceptionName) || + "BrokenPipeException".equals(exceptionName)) { + return true; + } + + // ํƒ€์ž„์•„์›ƒ ๊ด€๋ จ (๋„ˆ๋ฌด ๋นˆ๋ฒˆํ•œ ๊ฒฝ์šฐ) + if (message != null && ( + message.contains("Connection timed out") || + message.contains("Read timed out") || + message.contains("Connection reset") + )) { + return true; + } + + return false; + } + /** * ์˜ˆ์™ธ์˜ ์ŠคํƒํŠธ๋ ˆ์ด์Šค์—์„œ ์‹ค์ œ ์—๋Ÿฌ ๋ฐœ์ƒ ์œ„์น˜๋ฅผ ์ถ”์ถœ */ diff --git a/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java b/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java index d0ba3c76..09413004 100644 --- a/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java +++ b/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java @@ -12,6 +12,7 @@ public enum WalkingErrorCase implements ErrorCase { GRAPH_FILE_NOT_FOUND(500, 8002, "๊ทธ๋ž˜ํ”„ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), GRAPH_INIT_FAILED(500, 8003, "๊ทธ๋ž˜ํ”„ ์ดˆ๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), NODE_NOT_FOUND(400, 8004, "ํ•ด๋‹น ์œ„๋„, ๊ฒฝ๋„ ๊ทผ์ฒ˜์˜ ๋…ธ๋“œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + DISTANCE_LIMIT_EXCEEDED(400, 8005, "๊ฒฝ๋กœ๊ฐ€ ํ—ˆ์šฉ ์ตœ๋Œ€ ๊ฑฐ๋ฆฌ(30km)๋ฅผ ์ดˆ๊ณผํ•˜์˜€์Šต๋‹ˆ๋‹ค."), ; private final Integer httpStatusCode; diff --git a/src/main/java/com/wayble/server/direction/init/GraphInit.java b/src/main/java/com/wayble/server/direction/init/GraphInit.java index bc6fd01b..9aa13f6b 100644 --- a/src/main/java/com/wayble/server/direction/init/GraphInit.java +++ b/src/main/java/com/wayble/server/direction/init/GraphInit.java @@ -29,6 +29,10 @@ public class GraphInit { private Map> adjacencyList; private Map nodeMap; private Map markerMap; + private Set rampMarkers = Collections.emptySet(); + + private static final double RAMP_PENALTY = 5.0; + private static final double MARKER_DISCOUNT = 0.5; @PostConstruct public void init() { @@ -66,7 +70,12 @@ private Map> buildAdjacencyList() { for (Edge edge : edges) { boolean isWaybleMarker = markerMap.containsKey(edge.from()) || markerMap.containsKey(edge.to()); - double distance = isWaybleMarker ? edge.length() * 0.5 : edge.length(); + boolean isRamp = rampMarkers.contains(edge.from()) || rampMarkers.contains(edge.to()); + + double distance = edge.length(); + + if (isRamp) distance *= RAMP_PENALTY; + else if (isWaybleMarker) distance *= MARKER_DISCOUNT; // ์–‘๋ฐฉํ–ฅ adjacencyList.computeIfAbsent(edge.from(), k -> new ArrayList<>()) diff --git a/src/main/java/com/wayble/server/direction/service/WalkingService.java b/src/main/java/com/wayble/server/direction/service/WalkingService.java index 460c714b..f384e347 100644 --- a/src/main/java/com/wayble/server/direction/service/WalkingService.java +++ b/src/main/java/com/wayble/server/direction/service/WalkingService.java @@ -27,6 +27,8 @@ public class WalkingService { private final GraphInit graphInit; private final WaybleDijkstraService waybleDijkstraService; + private static final double NODE_SEARCH_RADIUS = 1000; + public TMapParsingResponse callTMapApi(TMapRequest request) { try { TMapResponse response = tMapClient.response(request); @@ -52,6 +54,7 @@ public WayblePathResponse findWayblePath( private long findNearestNode(double lat, double lon) { return graphInit.getNodeMap().values().stream() + .filter(node -> HaversineUtil.haversine(lat, lon, node.lat(), node.lon()) <= NODE_SEARCH_RADIUS) .min(Comparator.comparingDouble( node -> HaversineUtil.haversine(lat, lon, node.lat(), node.lon()) )) diff --git a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java index 949fccce..3a5b837c 100644 --- a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java +++ b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java @@ -1,10 +1,13 @@ package com.wayble.server.direction.service; +import com.wayble.server.common.exception.ApplicationException; import com.wayble.server.direction.dto.response.WayblePathResponse; import com.wayble.server.direction.entity.Edge; import com.wayble.server.direction.entity.Node; import com.wayble.server.direction.entity.type.Type; +import com.wayble.server.direction.exception.WalkingErrorCase; import com.wayble.server.direction.init.GraphInit; +import com.wayble.server.direction.service.util.HaversineUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -18,12 +21,20 @@ public class WaybleDijkstraService { // 11cm์˜ ์˜ค์ฐจ ํ—ˆ์šฉ private static final double TOLERANCE = 0.000001; + private static final double MAX_DISTANCE = 30000.0; public WayblePathResponse createWayblePath(long start, long end) { List path = dijkstra(start, end); Map markerMap = graphInit.getMarkerMap(); - int totalDistance = (int) Math.round(calculateDistance(path)); + List polyline = createPolyLine(path); + double totalDistanceMeters = calculateDistance(polyline); + + if (totalDistanceMeters >= MAX_DISTANCE) { + throw new ApplicationException(WalkingErrorCase.DISTANCE_LIMIT_EXCEEDED); + } + int totalDistance = (int) Math.round(totalDistanceMeters); + // ๋…ธ๋“œ ๊ฐ„ 5์ดˆ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ถ”๊ฐ€ (ํšก๋‹จ ๋ณด๋„, ๋ณดํ–‰์ž ์ƒํ™ฉ ๋“ฑ ๋ฐ˜์˜) int totalTime = (int) Math.round(calculateTime(path)) + path.size() * 5; @@ -34,15 +45,12 @@ public WayblePathResponse createWayblePath(long start, long end) { return new WayblePathResponse.WayblePoint(node.lat(), node.lon(), type); }).toList(); - List polyline = createPolyLine(path); - return WayblePathResponse.of(totalDistance, totalTime, wayblePoints, polyline); } private List createPolyLine(List path) { List polyline = new ArrayList<>(); Map> adjacencyList = graphInit.getGraph(); - double[] last = null; for (int i = 0; i < path.size() - 1; i++) { long from = path.get(i); @@ -54,12 +62,9 @@ private List createPolyLine(List path) { .orElse(null); // ์ขŒํ‘œ ์ค‘๋ณต ์ œ๊ฑฐ (๋™์ผ ์ขŒํ‘œ๊ฐ€ ์—ฐ์†๋  ์‹œ, ์ถ”๊ฐ€ X) - if (edge != null && edge.geometry() != null) { - for (double[] coord : edge.geometry()) { - if (last == null || isDifferent(last, coord)) { - polyline.add(coord); - last = coord; - } + if (edge != null && edge.geometry() != null && !edge.geometry().isEmpty()) { + for (double[] coords : edge.geometry()) { + deleteDuplicateCoords(polyline, coords); } } else { Node fromNode = graphInit.getNodeMap().get(from); @@ -68,19 +73,29 @@ private List createPolyLine(List path) { double[] fromCoord = new double[]{fromNode.lon(), fromNode.lat()}; double[] toCoord = new double[]{toNode.lon(), toNode.lat()}; - // ์ค‘๋ณต ํ™•์ธ ํ›„, ์ค‘๋ณต X์ผ ๋•Œ๋งŒ ์ถ”๊ฐ€ - if (last == null || isDifferent(last, fromCoord)) { - polyline.add(fromCoord); - } - if (last == null || isDifferent(last, toCoord)) { - polyline.add(toCoord); - last = toCoord; - } + deleteDuplicateCoords(polyline, fromCoord); + deleteDuplicateCoords(polyline, toCoord); } } return polyline; } + private void deleteDuplicateCoords(List polyline, double[] coords) { + int n = polyline.size(); + + // ์—ฐ์† ์ค‘๋ณต ์ขŒํ‘œ ์ œ๊ฑฐ + if (!polyline.isEmpty() && isClose(polyline.get(n - 1), coords)) return; + + // ๊ณผ๊ฑฐ์˜ ์ขŒํ‘œ๋กœ ๋Œ์•„์˜ฌ ๊ฒฝ์šฐ, ํ•ด๋‹น ์ขŒํ‘œ ์ œ๊ฑฐ + for (int i = n - 2; i >= 0; i--) { + if (isClose(polyline.get(i), coords)) { + polyline.subList(i + 1, n).clear(); + return; + } + } + polyline.add(coords.clone()); + } + private double calculateTime(List path) { double averageSpeed = 1.0; double totalTime = 0.0; @@ -103,17 +118,14 @@ private double calculateTime(List path) { return totalTime; } - private double calculateDistance(List path) { + private double calculateDistance(List polyline) { double totalDistance = 0.0; - for (int i = 0; i < path.size() - 1; i++) { - long from = path.get(i); - long to = path.get(i + 1); - totalDistance += graphInit.getGraph().getOrDefault(from, List.of()).stream() - .filter(edge -> edge.to() == to) - .findFirst() - .map(Edge::length) - .orElse(0.0); + for (int i = 1; i < polyline.size(); i++) { + totalDistance += HaversineUtil.haversine( + polyline.get(i - 1)[1], polyline.get(i - 1)[0], + polyline.get(i)[1], polyline.get(i)[0] + ); } return totalDistance; } @@ -152,7 +164,7 @@ private List dijkstra(long start, long end) { return path; } - private boolean isDifferent(double[] a, double[] b) { - return Math.abs(a[0] - b[0]) > TOLERANCE || Math.abs(a[1] - b[1]) > TOLERANCE; + private boolean isClose(double[] a, double[] b) { + return Math.abs(a[0] - b[0]) <= TOLERANCE && Math.abs(a[1] - b[1]) <= TOLERANCE; } } diff --git a/src/main/java/com/wayble/server/explore/controller/WaybleFacilitySearchController.java b/src/main/java/com/wayble/server/explore/controller/WaybleFacilitySearchController.java new file mode 100644 index 00000000..077ba1c0 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/controller/WaybleFacilitySearchController.java @@ -0,0 +1,30 @@ +package com.wayble.server.explore.controller; + +import com.wayble.server.common.response.CommonResponse; +import com.wayble.server.explore.dto.facility.WaybleFacilityConditionDto; +import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; +import com.wayble.server.explore.service.WaybleFacilityDocumentService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@Validated +@RequestMapping("/api/v1/facilities/search") +public class WaybleFacilitySearchController { + private final WaybleFacilityDocumentService waybleFacilityDocumentService; + + @GetMapping("") + public CommonResponse> findNearbyFacilities( + @Valid @ModelAttribute WaybleFacilityConditionDto conditionDto + ) { + return CommonResponse.success(waybleFacilityDocumentService.findNearbyFacilityDocuments(conditionDto)); + } +} diff --git a/src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java b/src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java index a757b94d..886f7d23 100644 --- a/src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java +++ b/src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java @@ -6,6 +6,7 @@ import com.wayble.server.explore.service.WaybleZoneRecommendService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -24,8 +25,9 @@ public CommonResponse> getWaybleZonePersona @Valid @ModelAttribute WaybleZoneRecommendConditionDto conditionDto, @RequestParam(name = "size", defaultValue = "1") int size) { + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); List result = waybleZoneRecommendService.getWaybleZonePersonalRecommend( - conditionDto.userId(), + userId, conditionDto.latitude(), conditionDto.longitude(), size diff --git a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityConditionDto.java b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityConditionDto.java new file mode 100644 index 00000000..5c774574 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityConditionDto.java @@ -0,0 +1,21 @@ +package com.wayble.server.explore.dto.facility; + +import com.wayble.server.explore.entity.FacilityType; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; + +public record WaybleFacilityConditionDto( + @DecimalMin(value = "-90.0", message = "์œ„๋„๋Š” -90.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "90.0", message = "์œ„๋„๋Š” 90.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @NotNull(message = "์œ„๋„ ์ž…๋ ฅ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + Double latitude, + + @DecimalMin(value = "-180.0", message = "๊ฒฝ๋„๋Š” -180.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "180.0", message = "๊ฒฝ๋„๋Š” 180.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @NotNull(message = "๊ฒฝ๋„ ์ž…๋ ฅ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + Double longitude, + + FacilityType facilityType +) { +} diff --git a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityRegisterDto.java b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityRegisterDto.java new file mode 100644 index 00000000..311c2fc0 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityRegisterDto.java @@ -0,0 +1,23 @@ +package com.wayble.server.explore.dto.facility; + +import com.wayble.server.explore.entity.FacilityType; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +@Builder +public record WaybleFacilityRegisterDto ( + @DecimalMin(value = "-90.0", message = "์œ„๋„๋Š” -90.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "90.0", message = "์œ„๋„๋Š” 90.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @NotNull(message = "์œ„๋„ ์ž…๋ ฅ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + Double latitude, + + @DecimalMin(value = "-180.0", message = "๊ฒฝ๋„๋Š” -180.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "180.0", message = "๊ฒฝ๋„๋Š” 180.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @NotNull(message = "๊ฒฝ๋„ ์ž…๋ ฅ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + Double longitude, + + FacilityType facilityType +){ +} diff --git a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java new file mode 100644 index 00000000..3bbd399d --- /dev/null +++ b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java @@ -0,0 +1,23 @@ +package com.wayble.server.explore.dto.facility; + +import com.wayble.server.explore.entity.FacilityType; +import com.wayble.server.explore.entity.WaybleFacilityDocument; +import lombok.AccessLevel; +import lombok.Builder; + +@Builder(access = AccessLevel.PRIVATE) +public record WaybleFacilityResponseDto( + Double latitude, + + Double longitude, + + FacilityType facilityType +) { + public static WaybleFacilityResponseDto from(WaybleFacilityDocument facilityDocument) { + return WaybleFacilityResponseDto.builder() + .latitude(facilityDocument.getLocation().getLat()) + .longitude(facilityDocument.getLocation().getLon()) + .facilityType(facilityDocument.getFacilityType()) + .build(); + } +} diff --git a/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendConditionDto.java b/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendConditionDto.java index a4fa46cd..30f7333c 100644 --- a/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendConditionDto.java +++ b/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendConditionDto.java @@ -15,9 +15,6 @@ public record WaybleZoneRecommendConditionDto( @DecimalMin(value = "-180.0", message = "๊ฒฝ๋„๋Š” -180.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") @DecimalMax(value = "180.0", message = "๊ฒฝ๋„๋Š” 180.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") @NotNull(message = "๊ฒฝ๋„ ์ž…๋ ฅ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - Double longitude, - - @NotNull(message = "์œ ์ € ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - Long userId + Double longitude ) { } diff --git a/src/main/java/com/wayble/server/explore/entity/FacilityType.java b/src/main/java/com/wayble/server/explore/entity/FacilityType.java new file mode 100644 index 00000000..c3ab5b30 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/entity/FacilityType.java @@ -0,0 +1,6 @@ +package com.wayble.server.explore.entity; + +public enum FacilityType { + ELEVATOR, + RAMP +} diff --git a/src/main/java/com/wayble/server/explore/entity/WaybleFacilityDocument.java b/src/main/java/com/wayble/server/explore/entity/WaybleFacilityDocument.java new file mode 100644 index 00000000..56dcf3b5 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/entity/WaybleFacilityDocument.java @@ -0,0 +1,27 @@ +package com.wayble.server.explore.entity; + +import com.wayble.server.direction.entity.transportation.Facility; +import com.wayble.server.explore.dto.facility.WaybleFacilityRegisterDto; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.GeoPointField; +import org.springframework.data.elasticsearch.core.geo.GeoPoint; + +@ToString +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Document(indexName = "wayble_facility_document", createIndex = true) +public class WaybleFacilityDocument { + @Id + @Field(name = "id") + private String id; + + @GeoPointField + private GeoPoint location; + + private FacilityType facilityType; +} diff --git a/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityDocumentRepository.java b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityDocumentRepository.java new file mode 100644 index 00000000..ad5af65e --- /dev/null +++ b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityDocumentRepository.java @@ -0,0 +1,10 @@ +package com.wayble.server.explore.repository.facility; + +import com.wayble.server.explore.entity.WaybleFacilityDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +import java.util.List; + +public interface WaybleFacilityDocumentRepository extends ElasticsearchRepository { + List findAll(); +} diff --git a/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java new file mode 100644 index 00000000..bb794d46 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java @@ -0,0 +1,101 @@ +package com.wayble.server.explore.repository.facility; + +import co.elastic.clients.elasticsearch._types.GeoLocation; +import co.elastic.clients.elasticsearch._types.SortOptions; +import co.elastic.clients.elasticsearch._types.SortOrder; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import com.wayble.server.explore.dto.facility.WaybleFacilityConditionDto; +import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; +import com.wayble.server.explore.entity.WaybleFacilityDocument; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.elasticsearch.client.elc.NativeQuery; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class WaybleFacilityQuerySearchRepository { + + private final ElasticsearchOperations operations; + private static final IndexCoordinates INDEX = IndexCoordinates.of("wayble_facility_document"); + private static final int LIMIT = 50; + + /** + * ์œ„๋„, ๊ฒฝ๋„, ์‹œ์„ค ํƒ€์ž…์„ ๋ฐ”ํƒ•์œผ๋กœ WaybleFacilityDocument๋ฅผ ๊ฑฐ๋ฆฌ์ˆœ์œผ๋กœ N๊ฐœ ๋ฐ˜ํ™˜ + */ + public List findNearbyFacilitiesByType( + WaybleFacilityConditionDto condition) { + + double radius = 10.0; // ๊ธฐ๋ณธ ๋ฐ˜๊ฒฝ 5km + String radiusWithUnit = radius + "km"; + + // ์‹œ์„ค ํƒ€์ž…์— ๋”ฐ๋ฅธ ์ฟผ๋ฆฌ ์กฐ๊ฑด ์ƒ์„ฑ + Query query = Query.of(q -> q + .bool(b -> { + // ์‹œ์„ค ํƒ€์ž… ์กฐ๊ฑด ์ถ”๊ฐ€ + if (condition.facilityType() != null) { + b.must(m -> m + .term(t -> t + .field("facilityType.keyword") + .value(condition.facilityType().name()) + ) + ); + } + + // ์œ„์น˜ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ: ์ค‘์‹ฌ ์ขŒํ‘œ ๊ธฐ์ค€ ๋ฐ˜๊ฒฝ ํ•„ํ„ฐ๋ง + b.filter(f -> f + .geoDistance(gd -> gd + .field("location") + .location(loc -> loc + .latlon(ll -> ll + .lat(condition.latitude()) + .lon(condition.longitude()) + ) + ) + .distance(radiusWithUnit) + ) + ); + + return b; + }) + ); + + // ๊ฑฐ๋ฆฌ ๊ธฐ์ค€ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ + SortOptions geoSort = SortOptions.of(s -> s + .geoDistance(gds -> gds + .field("location") + .location(GeoLocation.of(gl -> gl + .latlon(ll -> ll + .lat(condition.latitude()) + .lon(condition.longitude()) + ) + )) + .order(SortOrder.Asc) + ) + ); + + // Elasticsearch ์ฟผ๋ฆฌ ๊ตฌ์„ฑ + NativeQuery nativeQuery = NativeQuery.builder() + .withQuery(query) + .withSort(geoSort) + .withPageable(PageRequest.of(0, LIMIT)) + .build(); + + // ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰ + SearchHits hits = + operations.search(nativeQuery, WaybleFacilityDocument.class, INDEX); + + // ๊ฒฐ๊ณผ๋ฅผ Document ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜ + return hits.stream() + .map(hit -> { + WaybleFacilityDocument doc = hit.getContent(); + return WaybleFacilityResponseDto.from(doc); + }) + .toList(); + } +} diff --git a/src/main/java/com/wayble/server/explore/service/WaybleFacilityDocumentService.java b/src/main/java/com/wayble/server/explore/service/WaybleFacilityDocumentService.java new file mode 100644 index 00000000..3d52e258 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/service/WaybleFacilityDocumentService.java @@ -0,0 +1,22 @@ +package com.wayble.server.explore.service; + +import com.wayble.server.explore.dto.facility.WaybleFacilityConditionDto; +import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; +import com.wayble.server.explore.repository.facility.WaybleFacilityQuerySearchRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class WaybleFacilityDocumentService { + + private final WaybleFacilityQuerySearchRepository waybleFacilityQuerySearchRepository; + + public List findNearbyFacilityDocuments(WaybleFacilityConditionDto dto) { + return waybleFacilityQuerySearchRepository.findNearbyFacilitiesByType(dto); + } +} diff --git a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java index a944dc54..d17a09ea 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -18,13 +18,12 @@ import java.util.List; @RestController -@RequestMapping("/api/v1/users/{userId}/places") +@RequestMapping("/api/v1/users/places") @RequiredArgsConstructor public class UserPlaceController { private final UserPlaceService userPlaceService; - @PostMapping @Operation(summary = "์œ ์ € ์žฅ์†Œ ์ €์žฅ", description = "์œ ์ €๊ฐ€ ์›จ์ด๋ธ”์กด์„ ์žฅ์†Œ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.") @ApiResponses({ @@ -34,18 +33,9 @@ public class UserPlaceController { @ApiResponse(responseCode = "403", description = "๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.") }) public CommonResponse saveUserPlace( - @PathVariable Long userId, @RequestBody @Valid UserPlaceRequestDto request ) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (!(authentication.getPrincipal() instanceof Long)) { - throw new ApplicationException(UserErrorCase.FORBIDDEN); - } - Long tokenUserId = (Long) authentication.getPrincipal(); - if (!userId.equals(tokenUserId)) { - throw new ApplicationException(UserErrorCase.FORBIDDEN); - } - + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); userPlaceService.saveUserPlace(userId, request); // userId ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊น€ return CommonResponse.success("์žฅ์†Œ๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } @@ -61,16 +51,8 @@ public CommonResponse saveUserPlace( @ApiResponse(responseCode = "403", description = "๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.") }) public CommonResponse> getUserPlaces( - @PathVariable Long userId ) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (!(authentication.getPrincipal() instanceof Long)) { - throw new ApplicationException(UserErrorCase.FORBIDDEN); - } - Long tokenUserId = (Long) authentication.getPrincipal(); - if (!userId.equals(tokenUserId)) { - throw new ApplicationException(UserErrorCase.FORBIDDEN); - } + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); List places = userPlaceService.getUserPlaces(userId); return CommonResponse.success(places); } diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceListResponseDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceListResponseDto.java index 73f155b9..9a8d79ee 100644 --- a/src/main/java/com/wayble/server/user/dto/UserPlaceListResponseDto.java +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceListResponseDto.java @@ -6,17 +6,17 @@ @Builder public record UserPlaceListResponseDto( - Long place_id, + Long placeId, String title, - WaybleZoneDto wayble_zone + WaybleZoneDto waybleZone ) { @Builder public record WaybleZoneDto( - Long wayble_zone_id, + Long waybleZoneId, String name, String category, double rating, String address, - String image_url + String imageUrl ) {} } \ No newline at end of file diff --git a/src/main/java/com/wayble/server/user/service/UserPlaceService.java b/src/main/java/com/wayble/server/user/service/UserPlaceService.java index 6581e153..8200715e 100644 --- a/src/main/java/com/wayble/server/user/service/UserPlaceService.java +++ b/src/main/java/com/wayble/server/user/service/UserPlaceService.java @@ -78,16 +78,16 @@ public List getUserPlaces(Long userId) { String imageUrl = waybleZone.getMainImageUrl(); return UserPlaceListResponseDto.builder() - .place_id(userPlace.getId()) + .placeId(userPlace.getId()) .title(userPlace.getTitle()) - .wayble_zone( + .waybleZone( UserPlaceListResponseDto.WaybleZoneDto.builder() - .wayble_zone_id(waybleZone.getId()) + .waybleZoneId(waybleZone.getId()) .name(waybleZone.getZoneName()) .category(waybleZone.getZoneType().toString()) .rating(waybleZone.getRating()) .address(waybleZone.getAddress().toFullAddress()) - .image_url(imageUrl) + .imageUrl(imageUrl) .build() ) .build(); diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index ef41b877..246c2b0b 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -34,7 +34,7 @@ 50MB 90 1GB - true + false [%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n%ex{2} diff --git a/src/main/resources/wayble_markers.json b/src/main/resources/wayble_markers.json index 37300c8d..e161284c 100644 --- a/src/main/resources/wayble_markers.json +++ b/src/main/resources/wayble_markers.json @@ -123,13 +123,13 @@ "id": 1740825183, "lat": 37.49930939062552, "lon": 127.02269235121697, - "type": "WHEELCHAIR_LIFT" + "type": "RAMP" }, { "id": 1012654209, "lat": 37.47030346142741, "lon": 127.02480372021024, - "type": "WHEELCHAIR_CHARGER" + "type": "RAMP" }, { "id": 4487534788, @@ -159,7 +159,7 @@ "id": 6875654572, "lat": 37.49975423821268, "lon": 127.02250552924745, - "type": "NONE" + "type": "RAMP" }, { "id": 1709551788, @@ -279,7 +279,7 @@ "id": 2747865798, "lat": 37.479060974875445, "lon": 126.99193969295382, - "type": "NONE" + "type": "RAMP" }, { "id": 9869178896, diff --git a/src/test/java/com/wayble/server/direction/service/WalkingServiceTest.java b/src/test/java/com/wayble/server/direction/service/WalkingServiceTest.java new file mode 100644 index 00000000..db06bbbc --- /dev/null +++ b/src/test/java/com/wayble/server/direction/service/WalkingServiceTest.java @@ -0,0 +1,105 @@ +package com.wayble.server.direction.service; + +import com.wayble.server.direction.dto.response.WayblePathResponse; +import com.wayble.server.direction.entity.Edge; +import com.wayble.server.direction.entity.Node; +import com.wayble.server.direction.entity.type.Type; +import com.wayble.server.direction.init.GraphInit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +public class WalkingServiceTest { + + @Mock + private GraphInit graphInit; + + @Mock + WaybleDijkstraService waybleDijkstraService; + + @InjectMocks + private WalkingService walkingService; + + @BeforeEach + void setUp() { + Map nodeMap = Map.of( + 1L, new Node(1L, 37.1, 127.1), + 2L, new Node(2L, 37.2, 127.1), + 3L, new Node(3L, 37.3, 127.1), + 4L, new Node(4L, 37.2, 127.2), + 5L, new Node(5L, 37.3, 127.2) + ); + + Map> adjacencyList = Map.of( + 1L, List.of(new Edge(1, 2, 100, List.of())), + 2L, List.of(new Edge(2, 3, 100, List.of()), new Edge(2, 4, 120, List.of())), + 3L, List.of(new Edge(3, 5, 90, List.of())), + 4L, List.of(new Edge(4, 5, 120, List.of())) + ); + + Map markerMap = Map.of( + 2L, Type.RAMP, + 3L, Type.NONE, + 4L, Type.ELEVATOR + ); + + List points = List.of( + new WayblePathResponse.WayblePoint(37.1, 127.1, Type.NONE), + new WayblePathResponse.WayblePoint(37.2, 127.1, Type.RAMP), + new WayblePathResponse.WayblePoint(37.2, 127.2, Type.ELEVATOR), + new WayblePathResponse.WayblePoint(37.3, 127.2, Type.NONE) + ); + + List polyline = List.of( + new double[]{37.1, 127.1}, + new double[]{37.2, 127.1}, + new double[]{37.2, 127.2}, + new double[]{37.3, 127.2} + ); + + when(graphInit.getNodeMap()).thenReturn(nodeMap); + // when(graphInit.getMarkerMap()).thenReturn(markerMap); + // when(graphInit.getGraph()).thenReturn(adjacencyList); + when(waybleDijkstraService.createWayblePath(anyLong(), anyLong())) + .thenReturn(WayblePathResponse.of(340, 300, points, polyline)); + } + + @Test + @DisplayName("๊ฒฝ์‚ฌ๋กœ๊ฐ€ ์žˆ์„ ๋•Œ์˜ ์›จ์ด๋ธ” ์ถ”์ฒœ ๊ฒฝ๋กœ") + void rampWayblePathTest() { + // given + double startLat = 37.1; + double startLon = 127.1; + double endLat = 37.3; + double endLon = 127.2; + + // when + WayblePathResponse response = walkingService.findWayblePath( + startLat, startLon, endLat, endLon + ); + + // then + assertNotNull(response); + + assertEquals( + List.of( + new WayblePathResponse.WayblePoint(37.1, 127.1, Type.NONE), + new WayblePathResponse.WayblePoint(37.2, 127.1, Type.RAMP), + new WayblePathResponse.WayblePoint(37.2, 127.2, Type.ELEVATOR), + new WayblePathResponse.WayblePoint(37.3, 127.2, Type.NONE) + ), response.points() + ); + } +} diff --git a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java new file mode 100644 index 00000000..a12691d4 --- /dev/null +++ b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java @@ -0,0 +1,266 @@ +package com.wayble.server.explore; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wayble.server.common.config.security.jwt.JwtTokenProvider; +import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; +import com.wayble.server.explore.entity.FacilityType; +import com.wayble.server.explore.entity.WaybleFacilityDocument; +import com.wayble.server.explore.repository.facility.WaybleFacilityDocumentRepository; +import com.wayble.server.user.entity.Gender; +import com.wayble.server.user.entity.LoginType; +import com.wayble.server.user.entity.User; +import com.wayble.server.user.entity.UserType; +import com.wayble.server.user.repository.UserRepository; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.elasticsearch.core.geo.GeoPoint; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@AutoConfigureMockMvc +public class WaybleFacilityApiIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Autowired + private UserRepository userRepository; + + @Autowired + private WaybleFacilityDocumentRepository waybleFacilityDocumentRepository; + + private static final double LATITUDE = 37.5435480; + + private static final double LONGITUDE = 126.9518410; + + private static final double RADIUS = 20.0; + + private static final int SAMPLES = 100; + + private static final String baseUrl = "/api/v1/facilities/search"; + + private Long userId; + + private String token; + + @BeforeAll + public void setup() { + User testUser = User.createUserWithDetails( + "testUser", "testUsername", UUID.randomUUID() + "@email", "password", + LocalDate.now(), Gender.MALE, LoginType.KAKAO, UserType.DISABLED + ); + + userRepository.save(testUser); + userId = testUser.getId(); + token = jwtTokenProvider.generateToken(userId, "ROLE_USER"); + + for(int i = 1; i <= SAMPLES; i++) { + Map points = makeRandomPoint(); + + WaybleFacilityDocument rampDocument = WaybleFacilityDocument.builder() + .id(UUID.randomUUID().toString()) + .location(new GeoPoint(points.get("latitude"), points.get("longitude"))) + .facilityType(FacilityType.RAMP) + .build(); + + WaybleFacilityDocument elevatorDocument = WaybleFacilityDocument.builder() + .id(UUID.randomUUID().toString()) + .location(new GeoPoint(points.get("latitude"), points.get("longitude"))) + .facilityType(FacilityType.ELEVATOR) + .build(); + + waybleFacilityDocumentRepository.save(rampDocument); + waybleFacilityDocumentRepository.save(elevatorDocument); + } + } + + @AfterAll + public void teardown() { + waybleFacilityDocumentRepository.deleteAll(); + userRepository.deleteById(userId); + } + + @Test + public void checkDataExists() { + List all = waybleFacilityDocumentRepository.findAll(); + assertThat(all.size()).isGreaterThan(0); + + for (WaybleFacilityDocument doc : all) { + assertThat(doc.getId()).isNotNull(); + assertThat(doc.getLocation()).isNotNull(); + assertThat(doc.getFacilityType()).isNotNull(); + System.out.println(doc); + } + } + + + @Test + @DisplayName("์ขŒํ‘œ๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ๊ฐ€๊นŒ์šด ๊ฒฝ์‚ฌ๋กœ ์กฐํšŒ ํ…Œ์ŠคํŠธ") + public void findNearbyRampFacilities() throws Exception { + MvcResult result = mockMvc.perform(get(baseUrl) + .header("Authorization", "Bearer " + token) + .param("latitude", String.valueOf(LATITUDE)) + .param("longitude", String.valueOf(LONGITUDE)) + .param("facilityType", FacilityType.RAMP.name()) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonNode root = objectMapper.readTree(json); + JsonNode dataNode = root.get("data"); + + System.out.println("==== ์‘๋‹ต ๊ฒฐ๊ณผ ===="); + System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); + + List dtoList = objectMapper.convertValue( + dataNode, + new TypeReference<>() {} + ); + + assertThat(dtoList).isNotEmpty(); + + // null ๊ฐ’ ๊ฒ€์ฆ + for (WaybleFacilityResponseDto dto : dtoList) { + assertThat(dto.latitude()).isNotNull(); + assertThat(dto.longitude()).isNotNull(); + assertThat(dto.facilityType()).isNotNull(); + assertThat(dto.facilityType()).isEqualTo(FacilityType.RAMP); + System.out.println(dto); + } + + // ๊ฑฐ๋ฆฌ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ + if (dtoList.size() > 1) { + for (int i = 0; i < dtoList.size() - 1; i++) { + WaybleFacilityResponseDto current = dtoList.get(i); + WaybleFacilityResponseDto next = dtoList.get(i + 1); + + double currentDistance = haversine(LATITUDE, LONGITUDE, + current.latitude(), current.longitude()); + double nextDistance = haversine(LATITUDE, LONGITUDE, + next.latitude(), next.longitude()); + + assertThat(currentDistance).isLessThanOrEqualTo(nextDistance); + System.out.printf("Index %d: Distance = %.3f km%n", i, currentDistance); + } + // ๋งˆ์ง€๋ง‰ ์š”์†Œ์˜ ๊ฑฐ๋ฆฌ๋„ ์ถœ๋ ฅ + WaybleFacilityResponseDto last = dtoList.get(dtoList.size() - 1); + double lastDistance = haversine(LATITUDE, LONGITUDE, + last.latitude(), last.longitude()); + System.out.printf("Index %d: Distance = %.3f km%n", dtoList.size() - 1, lastDistance); + } + } + + @Test + @DisplayName("์ขŒํ‘œ๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ๊ฐ€๊นŒ์šด ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์กฐํšŒ ํ…Œ์ŠคํŠธ") + public void findNearbyElevatorFacilities() throws Exception { + MvcResult result = mockMvc.perform(get(baseUrl) + .header("Authorization", "Bearer " + token) + .param("latitude", String.valueOf(LATITUDE)) + .param("longitude", String.valueOf(LONGITUDE)) + .param("facilityType", FacilityType.ELEVATOR.name()) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonNode root = objectMapper.readTree(json); + JsonNode dataNode = root.get("data"); + + System.out.println("==== ์‘๋‹ต ๊ฒฐ๊ณผ ===="); + System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); + + List dtoList = objectMapper.convertValue( + dataNode, + new TypeReference<>() {} + ); + + assertThat(dtoList).isNotEmpty(); + + // null ๊ฐ’ ๊ฒ€์ฆ + for (WaybleFacilityResponseDto dto : dtoList) { + assertThat(dto.latitude()).isNotNull(); + assertThat(dto.longitude()).isNotNull(); + assertThat(dto.facilityType()).isNotNull(); + assertThat(dto.facilityType()).isEqualTo(FacilityType.ELEVATOR); + System.out.println(dto); + } + + // ๊ฑฐ๋ฆฌ์ˆœ ์ •๋ ฌ ๊ฒ€์ฆ + if (dtoList.size() > 1) { + for (int i = 0; i < dtoList.size() - 1; i++) { + WaybleFacilityResponseDto current = dtoList.get(i); + WaybleFacilityResponseDto next = dtoList.get(i + 1); + + double currentDistance = haversine(LATITUDE, LONGITUDE, + current.latitude(), current.longitude()); + double nextDistance = haversine(LATITUDE, LONGITUDE, + next.latitude(), next.longitude()); + + assertThat(currentDistance).isLessThanOrEqualTo(nextDistance); + System.out.printf("Index %d: Distance = %.3f km%n", i, currentDistance); + } + // ๋งˆ์ง€๋ง‰ ์š”์†Œ์˜ ๊ฑฐ๋ฆฌ๋„ ์ถœ๋ ฅ + WaybleFacilityResponseDto last = dtoList.get(dtoList.size() - 1); + double lastDistance = haversine(LATITUDE, LONGITUDE, + last.latitude(), last.longitude()); + System.out.printf("Index %d: Distance = %.3f km%n", dtoList.size() - 1, lastDistance); + } + } + + private double haversine(double lat1, double lon1, double lat2, double lon2) { + final int R = 6_371; // ์ง€๊ตฌ ๋ฐ˜์ง€๋ฆ„ (km) + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + double a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon/2) * Math.sin(dLon/2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; + } + + private Map makeRandomPoint() { + double radiusDeg = RADIUS / 111.0; + + Random rnd = new Random(); + + double u = rnd.nextDouble(); + double v = rnd.nextDouble(); + double w = radiusDeg * Math.sqrt(u); + double t = 2 * Math.PI * v; + + double latOffset = w * Math.cos(t); + double lngOffset = w * Math.sin(t) / Math.cos(Math.toRadians(LATITUDE)); + + double randomLat = LATITUDE + latOffset; + double randomLng = LONGITUDE + lngOffset; + + return Map.of("latitude", randomLat, "longitude", randomLng); + } +}