From a6758a5896a66ad21013300810bbf145becb1e5d Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 17:33:16 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[feat]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A1=B4=EC=9E=AC=20=EA=B2=80=EC=A6=9D=20service,?= =?UTF-8?q?=20controller=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../explore/controller/WaybleZoneSearchController.java | 8 ++++++++ .../dto/search/request/WaybleZoneSearchConditionDto.java | 1 - .../server/explore/service/WaybleZoneSearchService.java | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java b/src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java index dfbe0eff..55c09e5d 100644 --- a/src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java +++ b/src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java @@ -67,6 +67,14 @@ public CommonResponse> findMostLikesWaybleZo )); } + @GetMapping("/validate") + public CommonResponse findIsValidWaybleZone( + @Valid @ModelAttribute WaybleZoneSearchConditionDto conditionDto + ) + { + return CommonResponse.success(waybleZoneSearchService.isValidWaybleZone(conditionDto)); + } + @PostMapping("") public CommonResponse registerDocumentFromDto(@RequestBody WaybleZoneRegisterDto registerDto) { waybleZoneDocumentService.saveDocumentFromDto(registerDto); diff --git a/src/main/java/com/wayble/server/explore/dto/search/request/WaybleZoneSearchConditionDto.java b/src/main/java/com/wayble/server/explore/dto/search/request/WaybleZoneSearchConditionDto.java index 92ff24aa..72a7d86f 100644 --- a/src/main/java/com/wayble/server/explore/dto/search/request/WaybleZoneSearchConditionDto.java +++ b/src/main/java/com/wayble/server/explore/dto/search/request/WaybleZoneSearchConditionDto.java @@ -19,7 +19,6 @@ public record WaybleZoneSearchConditionDto( @NotNull(message = "경도 입력은 필수입니다.") Double longitude, - @DecimalMin(value = "0.1", message = "검색 반경은 100미터 이상이어야 합니다.") Double radiusKm, @Size(min = 2, message = "zoneName은 최소 2글자 이상이어야 합니다.") diff --git a/src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java b/src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java index 8179752d..43f81071 100644 --- a/src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java +++ b/src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java @@ -42,4 +42,8 @@ public List searchMostLikesWaybleZoneByDistrict(S return waybleZoneRepository.findTop3likesWaybleZonesByDistrict(district); } + + public WaybleZoneSearchResponseDto isValidWaybleZone(WaybleZoneSearchConditionDto condition) { + return waybleZoneQuerySearchRepository.findSimilarWaybleZone(condition); + } } From 29a7e210a0a1febc6736d810c29a271063908b8c Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 17:33:24 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[feat]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A1=B4=EC=9E=AC=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleZoneQuerySearchRepository.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java b/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java index 75255127..60ca3f5e 100644 --- a/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java +++ b/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java @@ -123,4 +123,99 @@ public Slice searchWaybleZonesByCondition(WaybleZon return new SliceImpl<>(dtos, pageable, hasNext); } + + /** + * 30m 이내이고 이름이 유사한 WaybleZone 찾기 + * @param cond 검색 조건 (위도, 경도, 이름 포함) + * @return 조건에 맞는 첫 번째 결과 또는 null + */ + public WaybleZoneSearchResponseDto findSimilarWaybleZone(WaybleZoneSearchConditionDto cond) { + if (cond.zoneName() == null || cond.zoneName().isBlank()) { + return null; + } + + // 30m 이내 검색 + Query query = Query.of(q -> q + .bool(b -> { + // 이름 유사도 검색 (fuzzy + match 조합) + b.should(s -> s + .match(m -> m + .field("zoneName") + .query(cond.zoneName()) + .boost(2.0f) // 정확한 매치에 높은 점수 + ) + ); + b.should(s -> s + .fuzzy(f -> f + .field("zoneName") + .value(cond.zoneName()) + .fuzziness("AUTO") // 오타 허용 + .boost(1.5f) + ) + ); + // 부분 매치도 포함 (공백 제거 후 검색) + String cleanedName = cond.zoneName().replaceAll("\\s+", ""); + b.should(s -> s + .wildcard(w -> w + .field("zoneName") + .value("*" + cleanedName + "*") + .boost(1.0f) + ) + ); + + // 최소 하나의 should 조건은 만족해야 함 + b.minimumShouldMatch("1"); + + // 30m 이내 필터 + b.filter(f -> f + .geoDistance(gd -> gd + .field("address.location") + .location(loc -> loc + .latlon(ll -> ll + .lat(cond.latitude()) + .lon(cond.longitude()) + ) + ) + .distance("30m") + ) + ); + return b; + }) + ); + + // 정렬: 점수 + 거리 조합 + SortOptions scoreSort = SortOptions.of(s -> s.score(sc -> sc.order(SortOrder.Desc))); + SortOptions geoSort = SortOptions.of(s -> s + .geoDistance(gds -> gds + .field("address.location") + .location(GeoLocation.of(gl -> gl + .latlon(ll -> ll + .lat(cond.latitude()) + .lon(cond.longitude()) + ) + )) + .order(SortOrder.Asc) + ) + ); + + NativeQuery nativeQuery = NativeQuery.builder() + .withQuery(query) + .withSort(scoreSort) + .withSort(geoSort) + .withPageable(PageRequest.of(0, 1)) // 첫 번째 결과만 + .build(); + + SearchHits hits = + operations.search(nativeQuery, WaybleZoneDocument.class, INDEX); + + if (hits.isEmpty()) { + return null; + } + + WaybleZoneDocument doc = hits.getSearchHit(0).getContent(); + Double distanceInMeters = (Double) hits.getSearchHit(0).getSortValues().get(1); // 거리는 두 번째 정렬값 + Double distanceInKm = distanceInMeters / 1000.0; + + return WaybleZoneSearchResponseDto.from(doc, distanceInKm); + } } \ No newline at end of file From 818b537c620c159313efd8642972d2d7ce754452 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 17:33:32 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[feat]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A1=B4=EC=9E=AC=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleZoneSearchApiIntegrationTest.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java index 36e5e30d..390b4fb1 100644 --- a/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java @@ -530,6 +530,83 @@ public void findMostLikesWaybleZoneByDistrict() throws Exception{ } } + @Test + @DisplayName("위도, 경도, 이름 정보를 바탕으로 웨이블존이 맞는지 여부 반환 테스트") + public void findIsValidWaybleZoneTest () throws Exception{ + List waybleZoneList = waybleZoneRepository.findAll(); + WaybleZone waybleZone = waybleZoneList.get(0); + String zoneName = waybleZone.getZoneName(); + MvcResult result = mockMvc.perform(get(baseUrl + "/validate") + .header("Authorization", "Bearer " + token) + .param("latitude", String.valueOf(waybleZone.getAddress().getLatitude())) + .param("longitude", String.valueOf(waybleZone.getAddress().getLongitude())) + .param("zoneName", zoneName.substring(0, 2)) + .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))); + + WaybleZoneSearchResponseDto dto = + objectMapper.convertValue( + dataNode, + new TypeReference<>() {} + ); + + // 검증 로직 + assertThat(dto).isNotNull(); + + // 반환된 결과가 유효한 WaybleZoneSearchResponseDto인지 확인 + WaybleZoneInfoResponseDto infoDto = dto.waybleZoneInfo(); + assertThat(infoDto).isNotNull(); + assertThat(infoDto.zoneId()).isNotNull(); + assertThat(infoDto.zoneName()).isNotNull(); + assertThat(infoDto.zoneType()).isNotNull(); + assertThat(infoDto.latitude()).isNotNull(); + assertThat(infoDto.longitude()).isNotNull(); + + // 거리 검증 (30m 이내여야 함) + assertThat(dto.distance()) + .withFailMessage("반환된 거리(%.5f km)가 30m(0.03 km)를 초과합니다", dto.distance()) + .isLessThanOrEqualTo(0.03); + + // 이름 유사성 검증 + String requestedName = zoneName.substring(0, 2); + String foundName = infoDto.zoneName(); + assertThat(foundName) + .withFailMessage("반환된 이름(%s)이 요청한 이름(%s)과 유사하지 않습니다", foundName, requestedName) + .satisfiesAnyOf( + name -> assertThat(name).contains(requestedName), + name -> assertThat(name.replaceAll("\\s+", "")).contains(requestedName.replaceAll("\\s+", "")), + name -> assertThat(requestedName).contains(name.substring(0, Math.min(2, name.length()))) + ); + + // 정확한 거리 계산 검증 + double expectedDistance = haversine( + waybleZone.getAddress().getLatitude(), + waybleZone.getAddress().getLongitude(), + infoDto.latitude(), + infoDto.longitude() + ); + + // 허용 오차: 0.05 km (≈50m) + assertThat(dto.distance()) + .withFailMessage("계산된 거리(%.5f km)와 반환된 거리(%.5f km)가 다릅니다", + expectedDistance, dto.distance()) + .isCloseTo(expectedDistance, offset(0.05)); + + System.out.println(" 요청한 이름: " + requestedName); + System.out.println(" 찾은 이름: " + foundName); + System.out.println(" 거리: " + String.format("%.3f km", dto.distance())); + System.out.println(" 위치: " + infoDto.latitude() + ", " + infoDto.longitude()); + } + private double haversine(double lat1, double lon1, double lat2, double lon2) { final int R = 6_371; // 지구 반지름 (km) double dLat = Math.toRadians(lat2 - lat1); From 912904bdba327f2bac1fa8598580eb11ea076989 Mon Sep 17 00:00:00 2001 From: hyoin Date: Thu, 14 Aug 2025 19:24:00 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[feat]=20=EB=8B=A4=EC=A4=91=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EC=B2=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/TransportationResponseDto.java | 7 +- .../service/TransportationService.java | 219 +++++++++++++++--- 2 files changed, 198 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java index f6c8ea61..1cb9c64b 100644 --- a/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java +++ b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java @@ -8,9 +8,14 @@ @Schema(description = "대중교통 길찾기 응답 DTO") public record TransportationResponseDto( - List routes, + List routes, PageInfo pageInfo ) { + public record Route( + Integer routeIndex, // 경로 인덱스 + List steps // 해당 경로의 단계들 + ) {} + public record Step( DirectionType mode, // 예: START, WALK, SUBWAY, BUS, FINISH @Nullable List moveInfo, // 같은 Step으로 이동한 정류장(Node) 정보 (중간 정류장만) diff --git a/src/main/java/com/wayble/server/direction/service/TransportationService.java b/src/main/java/com/wayble/server/direction/service/TransportationService.java index e42f8a8c..50e4759d 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -38,6 +38,7 @@ public class TransportationService { private static final int ORIGIN_DESTINATION_WALK_DISTANCE = 1000; // 출발지/도착지에서 정류장까지 도보 연결 가능 거리 (m) private static final int MAX_NEARBY_NODES = 5; // 출발지/도착지 주변에서 고려할 최대 정류장 수 private static final int MAX_DIJKSTRA_VISITS = 5000; // 다익스트라 알고리즘에서 방문할 수 있는 최대 노드 수 (무한 루프 방지) + private static final int MAX_ROUTES = 5; // 찾을 최대 경로 수 public TransportationResponseDto findRoutes(TransportationRequestDto request){ @@ -54,27 +55,39 @@ public TransportationResponseDto findRoutes(TransportationRequestDto request){ Node start = Node.createNode(-1L, origin.name(), DirectionType.FROM_WAYPOINT ,origin.latitude(), origin.longitude()); Node end = Node.createNode(-2L, destination.name(), DirectionType.TO_WAYPOINT,destination.latitude(), destination.longitude()); - // 3. 경로 찾기 - List steps = findTransportationRoute(start, end); + // 3. 여러 경로 찾기 + List> allRoutes = findMultipleTransportationRoutes(start, end); // 4. 페이징 처리 int startIndex = (request.cursor() != null) ? request.cursor() : 0; - int pageSize = request.size() != null ? request.size() : steps.size(); - int endIndex = Math.min(startIndex + pageSize, steps.size()); - boolean hasNext = endIndex < steps.size(); + int pageSize = (request.size() != null) ? request.size() : 5; // 기본값 5로 설정 + int endIndex = Math.min(startIndex + pageSize, allRoutes.size()); + boolean hasNext = endIndex < allRoutes.size(); Integer nextCursor = hasNext ? endIndex : null; TransportationResponseDto.PageInfo pageInfo = new TransportationResponseDto.PageInfo(nextCursor, hasNext); // 경로를 찾지 못한 경우 처리 - if (steps.isEmpty()) { + if (allRoutes.isEmpty()) { throw new ApplicationException(PATH_NOT_FOUND); } - return new TransportationResponseDto(steps, pageInfo); + // 페이징된 경로들을 Route 객체로 변환 + List routeList = new ArrayList<>(); + List> pagedRoutes = allRoutes.subList(startIndex, endIndex); + for (int i = 0; i < pagedRoutes.size(); i++) { + List route = pagedRoutes.get(i); + TransportationResponseDto.Route routeObj = createRoute(route, startIndex + i + 1); + routeList.add(routeObj); + } + + return new TransportationResponseDto(routeList, pageInfo); } + private TransportationResponseDto.Route createRoute(List steps, int routeIndex) { + return new TransportationResponseDto.Route(routeIndex, steps); + } - private List findTransportationRoute(Node startTmp, Node endTmp){ + private List> findMultipleTransportationRoutes(Node startTmp, Node endTmp){ // 1. 데이터 로드 List nodes = new ArrayList<>(nodeRepository.findAll()); List edges = new ArrayList<>(edgeRepository.findAllWithNodesAndRoute()); @@ -91,13 +104,12 @@ private List findTransportationRoute(Node startT nodes.add(startTmp); nodes.add(endTmp); - // 4. 그래프 빌드 및 최적 경로 찾기 + // 4. 그래프 빌드 및 여러 경로 찾기 TransportationGraphDto graphData = buildGraph(nodes, edges, startTmp, endTmp); - return findOptimalRoute(graphData.graph(), startTmp, endTmp, graphData.weightMap(), nodes, nearestToStart, nearestToEnd); + return findMultipleOptimalRoutes(graphData.graph(), startTmp, endTmp, graphData.weightMap(), nodes, nearestToStart, nearestToEnd); } - - private List findOptimalRoute( + private List> findMultipleOptimalRoutes( Map> graph, Node startTmp, Node endTmp, @@ -121,26 +133,179 @@ private List findOptimalRoute( return new ArrayList<>(); } - // 2. 다익스트라 알고리즘으로 최적 경로 찾기 - List route = runDijkstra(graph, startNode, endNode, weightMap, nodes); + // 2. 여러 경로 찾기 + List> allRoutes = findMultipleRoutes(graph, startNode, endNode, weightMap, nodes); + + // 3. 경로 필터링 및 정렬 + return filterAndSortRoutes(allRoutes); + } + + private List> findMultipleRoutes( + Map> graph, + Node start, + Node end, + Map, Integer> weightMap, + List nodes) { + + List> routes = new ArrayList<>(); + + // 1. 기본 다익스트라로 첫 번째 경로 찾기 + List firstRoute = runDijkstra(graph, start, end, weightMap, nodes); + if (!firstRoute.isEmpty()) { + routes.add(firstRoute); + } + + // 2. 효율적인 다중 경로 찾기 - 한 번의 탐색으로 여러 경로 생성 + if (!firstRoute.isEmpty()) { + List> alternativeRoutes = findAlternativeRoutesEfficiently( + graph, start, end, weightMap, nodes, firstRoute + ); + routes.addAll(alternativeRoutes); + } - if (!route.isEmpty()) { - // 3. 대중교통 포함 여부 확인 - boolean hasPublicTransport = route.stream() - .anyMatch(step -> step.mode() == DirectionType.BUS || step.mode() == DirectionType.SUBWAY); + return routes; + } + + private List> findAlternativeRoutesEfficiently( + Map> graph, + Node start, + Node end, + Map, Integer> weightMap, + List nodes, + List firstRoute) { + + List> alternativeRoutes = new ArrayList<>(); + + // 첫 번째 경로에서 실제 사용된 엣지들을 추출 + Set> usedEdges = extractActualEdgesFromRoute(firstRoute, graph); + + // 최대 4개의 추가 경로 찾기 + for (int i = 0; i < 4 && alternativeRoutes.size() < MAX_ROUTES - 1; i++) { + // 실제 사용된 엣지들에만 패널티를 적용한 가중치 맵 생성 + Map, Integer> penalizedWeightMap = createActualEdgePenalizedWeightMap(weightMap, usedEdges, i + 1); + + // 다익스트라로 새로운 경로 찾기 + List newRoute = runDijkstra(graph, start, end, penalizedWeightMap, nodes); - if (!hasPublicTransport) { - return new ArrayList<>(); + if (newRoute.isEmpty()) { + break; } - // 4. 환승 횟수 검증 (4회 이상 제외) - int transferCount = calculateTransferCount(route); - if (transferCount >= 4) { - return new ArrayList<>(); + // 첫 번째 경로와 동일한지 확인 + if (areRoutesIdentical(newRoute, firstRoute)) { + continue; } + + // 새로운 경로에서 사용된 엣지들도 추가 + Set> newUsedEdges = extractActualEdgesFromRoute(newRoute, graph); + usedEdges.addAll(newUsedEdges); + + alternativeRoutes.add(newRoute); } - return route; + return alternativeRoutes; + } + + + + + + private Set> extractActualEdgesFromRoute(List route, Map> graph) { + Set> usedEdges = new HashSet<>(); + + for (TransportationResponseDto.Step step : route) { + String fromName = step.from(); + String toName = step.to(); + + for (Map.Entry> entry : graph.entrySet()) { + Long nodeId = entry.getKey(); + List edges = entry.getValue(); + + for (Edge edge : edges) { + Node fromNode = edge.getStartNode(); + Node toNode = edge.getEndNode(); + + if ((fromNode.getStationName().equals(fromName) && toNode.getStationName().equals(toName)) || + (fromNode.getStationName().equals(toName) && toNode.getStationName().equals(fromName))) { + usedEdges.add(Pair.of(fromNode.getId(), toNode.getId())); + usedEdges.add(Pair.of(toNode.getId(), fromNode.getId())); + } + } + } + } + + return usedEdges; + } + + private Map, Integer> createActualEdgePenalizedWeightMap(Map, Integer> originalWeightMap, Set> usedEdges, int routeIndex) { + Map, Integer> penalizedWeightMap = new HashMap<>(); + + for (Map.Entry, Integer> entry : originalWeightMap.entrySet()) { + Pair edge = entry.getKey(); + int weight = entry.getValue(); + + if (usedEdges.contains(edge)) { + int penalty = routeIndex * 100000; + penalizedWeightMap.put(edge, weight + penalty); + } else { + penalizedWeightMap.put(edge, weight); + } + } + + return penalizedWeightMap; + } + + private boolean areRoutesIdentical(List route1, List route2) { + // 두 경로가 완전히 동일한지 확인 + if (route1.size() != route2.size()) { + return false; + } + + for (int i = 0; i < route1.size(); i++) { + TransportationResponseDto.Step step1 = route1.get(i); + TransportationResponseDto.Step step2 = route2.get(i); + + if (step1.mode() != step2.mode() || + !Objects.equals(step1.from(), step2.from()) || + !Objects.equals(step1.to(), step2.to()) || + !Objects.equals(step1.routeName(), step2.routeName())) { + return false; + } + } + + return true; + } + + private List> filterAndSortRoutes(List> routes) { + return routes.stream() + .filter(route -> { + // 대중교통 포함 여부 확인 + boolean hasPublicTransport = route.stream() + .anyMatch(step -> step.mode() == DirectionType.BUS || step.mode() == DirectionType.SUBWAY); + + if (!hasPublicTransport) { + return false; + } + + // 환승 횟수 검증 (4회 이상 제외) + int transferCount = calculateTransferCount(route); + return transferCount < 4; + }) + .sorted(Comparator + .>comparingInt(this::calculateTransferCount) + .thenComparingInt(this::calculateWalkDistance)) + .limit(MAX_ROUTES) + .collect(Collectors.toList()); + } + + private int calculateWalkDistance(List route) { + return route.stream() + .filter(step -> step.mode() == DirectionType.WALK) + .mapToInt(step -> { + // 간단한 도보 거리 추정 (실제로는 정확한 거리 계산 필요) + return 500; // 기본값 + }) + .sum(); } private TransportationGraphDto buildGraph(List nodes, List edges, Node startTmp, Node endTmp) { @@ -457,9 +622,9 @@ private List mergeConsecutiveRoutes(List p return new ArrayList<>(); } } - } catch (Exception e) { + } catch (Exception e) { log.info("버스 정보 조회 실패: {}", e.getMessage()); - } + } } else if (currentType == DirectionType.SUBWAY) { try { if (currentEdge.getStartNode() != null) {