diff --git a/src/main/java/com/wayble/server/common/config/WebClientConfig.java b/src/main/java/com/wayble/server/common/config/WebClientConfig.java index acbbf29..07e81cf 100644 --- a/src/main/java/com/wayble/server/common/config/WebClientConfig.java +++ b/src/main/java/com/wayble/server/common/config/WebClientConfig.java @@ -2,10 +2,12 @@ import com.wayble.server.common.client.tmap.TMapProperties; import com.wayble.server.direction.external.kric.KricProperties; +import com.wayble.server.direction.external.opendata.OpenDataProperties; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.DefaultUriBuilderFactory; @Configuration @RequiredArgsConstructor @@ -13,6 +15,7 @@ public class WebClientConfig { private final TMapProperties tMapProperties; private final KricProperties kricProperties; + private final OpenDataProperties openDataProperties; @Bean public WebClient webClient() { @@ -38,4 +41,14 @@ public WebClient kricWebClient() { .filter(throwable -> throwable instanceof org.springframework.web.reactive.function.client.WebClientRequestException))) .build(); } + + @Bean + public WebClient openDataWebClient() { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(openDataProperties.baseUrl()); + factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY); + return WebClient.builder() + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(512 * 1024)) + .uriBuilderFactory(factory) + .build(); + } } diff --git a/src/main/java/com/wayble/server/direction/dto/InternalStep.java b/src/main/java/com/wayble/server/direction/dto/InternalStep.java new file mode 100644 index 0000000..a41c663 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/dto/InternalStep.java @@ -0,0 +1,21 @@ +package com.wayble.server.direction.dto; + +import java.util.List; + +import com.wayble.server.direction.dto.response.TransportationResponseDto; +import com.wayble.server.direction.entity.type.DirectionType; + +// 대중교통 길찾기에 사용하기 위한 내부용 DTO +public record InternalStep( + DirectionType mode, + List moveInfo, + String routeName, + Integer moveNumber, + TransportationResponseDto.BusInfo busInfo, + TransportationResponseDto.SubwayInfo subwayInfo, + String from, + String to, + Long routeId, + NodeRef startNode, + NodeRef endNode +) {} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/direction/dto/NodeRef.java b/src/main/java/com/wayble/server/direction/dto/NodeRef.java new file mode 100644 index 0000000..d6d7f54 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/dto/NodeRef.java @@ -0,0 +1,8 @@ +package com.wayble.server.direction.dto; + +public record NodeRef( + Long id, + String stationName, + Double latitude, + Double longitude +) {} diff --git a/src/main/java/com/wayble/server/direction/entity/transportation/Edge.java b/src/main/java/com/wayble/server/direction/entity/transportation/Edge.java index 52acb79..5e4c831 100644 --- a/src/main/java/com/wayble/server/direction/entity/transportation/Edge.java +++ b/src/main/java/com/wayble/server/direction/entity/transportation/Edge.java @@ -44,4 +44,14 @@ public static Edge createEdge(Long id, Node startNode, Node endNode, DirectionTy .route(null) .build(); } + + public static Edge createEdgeWithRoute(Long id, Node startNode, Node endNode, DirectionType edgeType, Route route) { + return Edge.builder() + .id(id) + .edgeType(edgeType) + .startNode(startNode) + .endNode(endNode) + .route(route) + .build(); + } } diff --git a/src/main/java/com/wayble/server/direction/entity/transportation/Route.java b/src/main/java/com/wayble/server/direction/entity/transportation/Route.java index 42dfce4..d43d965 100644 --- a/src/main/java/com/wayble/server/direction/entity/transportation/Route.java +++ b/src/main/java/com/wayble/server/direction/entity/transportation/Route.java @@ -7,6 +7,7 @@ import lombok.*; import java.util.List; +import java.util.ArrayList; @Entity @Getter @@ -39,4 +40,15 @@ public class Route { // 휠체어 정보 @OneToMany(mappedBy = "route", fetch = FetchType.LAZY) private List wheelchairs; + + public static Route createRoute(Long routeId, String routeName, DirectionType routeType, Node startNode, Node endNode) { + return Route.builder() + .routeId(routeId) + .routeName(routeName) + .routeType(routeType) + .startNode(startNode) + .endNode(endNode) + .wheelchairs(new ArrayList<>()) + .build(); + } } diff --git a/src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java b/src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java index 6819cfb..3a7f569 100644 --- a/src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java +++ b/src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java @@ -4,12 +4,10 @@ @ConfigurationProperties(prefix = "opendata.api") public record OpenDataProperties( - String key, String baseUrl, String encodedKey, Endpoints endpoints, int timeout, - String userAgent, String accept ) { public record Endpoints(String arrivals, String stationByName) {} diff --git a/src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java b/src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java index 22d2907..63963ae 100644 --- a/src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java +++ b/src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java @@ -34,6 +34,9 @@ public record Item( @JsonProperty("busType1") String busType1, @JsonProperty("busType2") String busType2, @JsonProperty("term") String term, - @JsonProperty("busRouteId") String busRouteId + @JsonProperty("busRouteId") String busRouteId, + @JsonProperty("rtNm") String rtNm, + @JsonProperty("stNm") String stNm, + @JsonProperty("arsId") String arsId ) {} } \ No newline at end of file diff --git a/src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java b/src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java index 51143ac..bb9aa5d 100644 --- a/src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java +++ b/src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java @@ -13,6 +13,7 @@ public record StationSearchMsgBody( @JsonIgnoreProperties(ignoreUnknown = true) public record StationItem( + String arsId, String stId, String stNm, String tmX, diff --git a/src/main/java/com/wayble/server/direction/repository/EdgeBoundingBoxProjection.java b/src/main/java/com/wayble/server/direction/repository/EdgeBoundingBoxProjection.java new file mode 100644 index 0000000..66e911d --- /dev/null +++ b/src/main/java/com/wayble/server/direction/repository/EdgeBoundingBoxProjection.java @@ -0,0 +1,18 @@ +package com.wayble.server.direction.repository; + +import com.wayble.server.direction.entity.type.DirectionType; + +public interface EdgeBoundingBoxProjection { + Long getEdgeId(); + Long getStartNodeId(); + Long getEndNodeId(); + DirectionType getEdgeType(); + String getStartStationName(); + Double getStartLatitude(); + Double getStartLongitude(); + String getEndStationName(); + Double getEndLatitude(); + Double getEndLongitude(); + Long getRouteId(); + String getRouteName(); +} diff --git a/src/main/java/com/wayble/server/direction/repository/EdgeRepository.java b/src/main/java/com/wayble/server/direction/repository/EdgeRepository.java index faf1434..d0dbce8 100644 --- a/src/main/java/com/wayble/server/direction/repository/EdgeRepository.java +++ b/src/main/java/com/wayble/server/direction/repository/EdgeRepository.java @@ -1,6 +1,7 @@ package com.wayble.server.direction.repository; import com.wayble.server.direction.entity.transportation.Edge; +import com.wayble.server.direction.repository.EdgeBoundingBoxProjection; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -16,13 +17,27 @@ public interface EdgeRepository extends JpaRepository { "LEFT JOIN FETCH e.route") List findAllWithNodesAndRoute(); - @Query("SELECT DISTINCT e FROM Edge e " + - "JOIN FETCH e.startNode s " + - "JOIN FETCH e.endNode en " + - "LEFT JOIN FETCH e.route " + + @Query("SELECT " + + "e.id as edgeId, " + + "s.id as startNodeId, " + + "en.id as endNodeId, " + + "e.edgeType as edgeType, " + + "s.stationName as startStationName, " + + "s.latitude as startLatitude, " + + "s.longitude as startLongitude, " + + "en.stationName as endStationName, " + + "en.latitude as endLatitude, " + + "en.longitude as endLongitude, " + + "r.routeId as routeId, " + + "r.routeName as routeName " + + "FROM Edge e " + + "JOIN e.startNode s " + + "JOIN e.endNode en " + + "LEFT JOIN e.route r " + "WHERE (s.latitude BETWEEN :minLat AND :maxLat AND s.longitude BETWEEN :minLon AND :maxLon) OR " + - "(en.latitude BETWEEN :minLat AND :maxLat AND en.longitude BETWEEN :minLon AND :maxLon)") - List findEdgesInBoundingBox( + "(en.latitude BETWEEN :minLat AND :maxLat AND en.longitude BETWEEN :minLon AND :maxLon) " + + "ORDER BY e.id") + List findEdgesInBoundingBox( @Param("minLat") double minLat, @Param("maxLat") double maxLat, @Param("minLon") double minLon, diff --git a/src/main/java/com/wayble/server/direction/repository/FacilityRepository.java b/src/main/java/com/wayble/server/direction/repository/FacilityRepository.java index d8a12c9..77b4ab0 100644 --- a/src/main/java/com/wayble/server/direction/repository/FacilityRepository.java +++ b/src/main/java/com/wayble/server/direction/repository/FacilityRepository.java @@ -10,8 +10,5 @@ import org.springframework.data.repository.query.Param; public interface FacilityRepository extends JpaRepository { - @Query("SELECT f FROM Facility f " + - "LEFT JOIN FETCH f.lifts " + - "WHERE f.id = :nodeId") Optional findByNodeId(@Param("nodeId") Long nodeId); } diff --git a/src/main/java/com/wayble/server/direction/repository/NodeBoundingBoxProjection.java b/src/main/java/com/wayble/server/direction/repository/NodeBoundingBoxProjection.java new file mode 100644 index 0000000..b5a02b9 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/repository/NodeBoundingBoxProjection.java @@ -0,0 +1,11 @@ +package com.wayble.server.direction.repository; + +import com.wayble.server.direction.entity.type.DirectionType; + +public interface NodeBoundingBoxProjection { + Long getId(); + String getStationName(); + DirectionType getNodeType(); + Double getLatitude(); + Double getLongitude(); +} diff --git a/src/main/java/com/wayble/server/direction/repository/NodeRepository.java b/src/main/java/com/wayble/server/direction/repository/NodeRepository.java index 9fe3cb6..b5b0bc5 100644 --- a/src/main/java/com/wayble/server/direction/repository/NodeRepository.java +++ b/src/main/java/com/wayble/server/direction/repository/NodeRepository.java @@ -1,6 +1,7 @@ package com.wayble.server.direction.repository; import com.wayble.server.direction.entity.transportation.Node; +import com.wayble.server.direction.repository.NodeBoundingBoxProjection; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -9,10 +10,17 @@ public interface NodeRepository extends JpaRepository { - @Query("SELECT n FROM Node n WHERE " + + @Query("SELECT " + + "n.id as id, " + + "n.stationName as stationName, " + + "n.nodeType as nodeType, " + + "n.latitude as latitude, " + + "n.longitude as longitude " + + "FROM Node n WHERE " + "n.latitude BETWEEN :minLat AND :maxLat AND " + - "n.longitude BETWEEN :minLon AND :maxLon") - List findNodesInBoundingBox( + "n.longitude BETWEEN :minLon AND :maxLon " + + "ORDER BY n.latitude, n.longitude") + List findNodesInBoundingBox( @Param("minLat") double minLat, @Param("maxLat") double maxLat, @Param("minLon") double minLon, diff --git a/src/main/java/com/wayble/server/direction/repository/RouteRepository.java b/src/main/java/com/wayble/server/direction/repository/RouteRepository.java index 719a5d5..c052ecc 100644 --- a/src/main/java/com/wayble/server/direction/repository/RouteRepository.java +++ b/src/main/java/com/wayble/server/direction/repository/RouteRepository.java @@ -1,9 +1,14 @@ package com.wayble.server.direction.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.wayble.server.direction.entity.transportation.Route; +import java.util.Optional; public interface RouteRepository extends JpaRepository{ + @Query("SELECT r.routeName FROM Route r WHERE r.routeId = :routeId") + Optional findRouteNameById(@Param("routeId") Long routeId); } diff --git a/src/main/java/com/wayble/server/direction/repository/WheelchairInfoRepository.java b/src/main/java/com/wayble/server/direction/repository/WheelchairInfoRepository.java index 007fd89..41aea5f 100644 --- a/src/main/java/com/wayble/server/direction/repository/WheelchairInfoRepository.java +++ b/src/main/java/com/wayble/server/direction/repository/WheelchairInfoRepository.java @@ -12,4 +12,7 @@ public interface WheelchairInfoRepository extends JpaRepository findByRouteId(@Param("routeId") Long routeId); + + @Query("SELECT w.wheelchairLocation FROM Wheelchair w WHERE w.route.routeId = :routeId") + List findWheelchairLocationsByRouteId(@Param("routeId") Long routeId); } diff --git a/src/main/java/com/wayble/server/direction/service/BusInfoService.java b/src/main/java/com/wayble/server/direction/service/BusInfoService.java index 669abd4..fec024b 100644 --- a/src/main/java/com/wayble/server/direction/service/BusInfoService.java +++ b/src/main/java/com/wayble/server/direction/service/BusInfoService.java @@ -10,37 +10,36 @@ import com.wayble.server.direction.repository.RouteRepository; import com.wayble.server.direction.dto.response.TransportationResponseDto; +import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.URI; -import java.time.Duration; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; @Service @Slf4j @RequiredArgsConstructor public class BusInfoService { - private final HttpClient httpClient; + private final WebClient openDataWebClient; private final OpenDataProperties openDataProperties; private final RouteRepository routeRepository; public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long busId, Double x, Double y) { - // 나중에 서비스키 문제 해결되면 이 함수 호출 제거 - return createDummyBusInfo(stationName, busId, x, y); - // 실제 API 호출 코드 (현재 주석 처리) - /* List isLowFloor = new ArrayList<>(); Integer dispatchInterval = null; + // 나중에 서비스키 문제 해결되면 이 함수 호출 제거 + return createDummyBusInfo(stationName, busId, x, y); + + + /* boolean isShuttleBus = false; if (busId != null) { - var route = routeRepository.findById(busId); - isShuttleBus = route.isPresent() && route.get().getRouteName().contains("마포"); + var routeName = routeRepository.findRouteNameById(busId); + isShuttleBus = routeName.isPresent() && routeName.get().contains("마포"); } try { @@ -53,20 +52,28 @@ public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long bus return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); } - // 2. 여러 정류소가 나올 때, 가장 가까운 정류소 찾기 - StationSearchResponse.StationItem closestStation = findClosestStation( - stationSearchResponse.msgBody().itemList(), x, y); + // 2. 정류소가 1개면 바로 선택, 여러 개면 가장 가까운 정류소 찾기 + List stationList = stationSearchResponse.msgBody().itemList(); + StationSearchResponse.StationItem closestStation; + + if (stationList.size() == 1) { + // 정류소가 1개뿐이면 불필요한 거리 계산 및 API 호출 없이 바로 선택 + closestStation = stationList.get(0); + } else { + // 여러 정류소가 있을 때만 findClosestStation 호출 + closestStation = findClosestStation(stationList, x, y, busId); + } if (closestStation == null) { log.warn("가장 가까운 정류소를 찾을 수 없습니다: {}", stationName); return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); } - + // 3. 정류소 ID로 버스 도착 정보 조회 - OpenDataResponse openDataResponse = fetchArrivals(Long.parseLong(closestStation.stId()), busId); + OpenDataResponse openDataResponse = fetchArrivals(closestStation.arsId(), busId); if (openDataResponse == null || openDataResponse.msgBody() == null || openDataResponse.msgBody().itemList() == null) { - log.warn("버스 도착 정보를 찾을 수 없습니다: {}", closestStation.stId()); + log.warn("정류소 {}의 버스 도착 정보를 찾을 수 없습니다: {}", stationName, closestStation.arsId()); return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); } @@ -89,20 +96,19 @@ public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long bus count++; } - + } catch (Exception e) { log.error("버스 정보 조회 중 오류 발생: {}", e.getMessage()); return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); } return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval); - */ + */ } // 나중에 이 함수 제거 private TransportationResponseDto.BusInfo createDummyBusInfo(String stationName, Long busId, Double x, Double y) { - log.info("🎭 더미 BusInfo 생성 - stationName: {}, busId: {}, x: {}, y: {}", stationName, busId, x, y); // 셔틀버스 여부 확인 (기존 로직 유지) boolean isShuttleBus = false; @@ -122,33 +128,63 @@ private TransportationResponseDto.BusInfo createDummyBusInfo(String stationName, return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval); } - private OpenDataResponse fetchArrivals(Long stationId, Long busId) { + private OpenDataResponse fetchArrivals(String stationId, Long busId) { try { String serviceKey = openDataProperties.encodedKey(); - String uri = openDataProperties.baseUrl() + + String fullUri = openDataProperties.baseUrl() + openDataProperties.endpoints().arrivals() + "?serviceKey=" + serviceKey + - "&stId=" + stationId + + "&arsId=" + stationId + "&resultType=json"; - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(uri)) - .header("Accept", openDataProperties.accept()) - .GET() - .timeout(Duration.ofSeconds(openDataProperties.timeout())) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - OpenDataResponse originalResponse = new ObjectMapper().readValue(response.body(), OpenDataResponse.class); + // 먼저 raw 응답을 확인 + String rawResponse = openDataWebClient + .get() + .uri(uriBuilder -> uriBuilder + .path(openDataProperties.endpoints().arrivals()) + .queryParam("serviceKey", serviceKey) + .queryParam("arsId", stationId) + .queryParam("resultType", "json") + .build()) + .header("Accept", openDataProperties.accept()) + .retrieve() + .onStatus(status -> status.isError(), response -> { + return response.bodyToMono(String.class) + .flatMap(body -> { + log.error("공공데이터 API 호출 오류: {}", body); + return Mono.error(new RuntimeException("API 호출 실패: " + response.statusCode())); + }); + }) + .bodyToMono(String.class) + .block(); + + OpenDataResponse originalResponse = null; + if (rawResponse != null) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + originalResponse = objectMapper.readValue(rawResponse, OpenDataResponse.class); + } catch (Exception e) { + log.error("JSON 파싱 실패: {}", e.getMessage()); + } + } // busId가 맞는 버스만 필터링 if (busId != null && originalResponse != null && originalResponse.msgBody() != null && originalResponse.msgBody().itemList() != null) { + // busId로 route 정보 조회 + var routeName = routeRepository.findRouteNameById(busId); + String projectRouteName = routeName.orElse(null); + List filteredItems = originalResponse.msgBody().itemList().stream() - .filter(item -> busId.toString().equals(item.busRouteId())) + .filter(item -> { + if (projectRouteName == null || item.rtNm() == null) { + return false; + } + return projectRouteName.contains(item.rtNm()) || item.rtNm().contains(projectRouteName); + }) .collect(Collectors.toList()); return new OpenDataResponse( @@ -170,22 +206,45 @@ private StationSearchResponse fetchStationByName(String stationName) { try { String serviceKey = openDataProperties.encodedKey(); - String uri = openDataProperties.baseUrl() + + String fullUri = openDataProperties.baseUrl() + openDataProperties.endpoints().stationByName() + "?serviceKey=" + serviceKey + "&stSrch=" + stationName + "&resultType=json"; - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(uri)) - .header("Accept", openDataProperties.accept()) - .GET() - .timeout(Duration.ofSeconds(openDataProperties.timeout())) - .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - return new ObjectMapper().readValue(response.body(), StationSearchResponse.class); + String rawResponse = openDataWebClient + .get() + .uri(uriBuilder -> uriBuilder + .path(openDataProperties.endpoints().stationByName()) + .queryParam("serviceKey", serviceKey) + .queryParam("stSrch", stationName) + .queryParam("resultType", "json") + .build()) + .header("Accept", openDataProperties.accept()) + .retrieve() + .onStatus(status -> status.isError(), response -> { + return response.bodyToMono(String.class) + .flatMap(body -> { + log.error("공공데이터 API 호출 오류: {}", body); + return Mono.error(new RuntimeException("API 호출 실패: " + response.statusCode())); + }); + }) + .bodyToMono(String.class) + .block(); + + StationSearchResponse stationResponse = null; + if (rawResponse != null) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + stationResponse = objectMapper.readValue(rawResponse, StationSearchResponse.class); + } catch (Exception e) { + log.error("JSON 파싱 실패: {}", e.getMessage()); + } + } + + return stationResponse; } catch (Exception e) { log.error("정류소 검색 중 예외 발생: {}", e.getMessage()); @@ -193,42 +252,77 @@ private StationSearchResponse fetchStationByName(String stationName) { } } - private StationSearchResponse.StationItem findClosestStation(List stations, Double x, Double y) { + private StationSearchResponse.StationItem findClosestStation(List stations, Double x, Double y, Long routeId) { if (stations == null || stations.isEmpty()) { log.warn("❌ 정류소 목록이 비어있음"); return null; } - StationSearchResponse.StationItem closestStation = null; - double minDistance = Double.MAX_VALUE; - + // 1. 먼저 모든 정류소를 거리순으로 정렬 + List sortedStations = new ArrayList<>(); for (StationSearchResponse.StationItem station : stations) { try { - // tmX, tmY가 숫자인지 확인하고 파싱 String tmXStr = station.tmX(); String tmYStr = station.tmY(); - if (tmXStr == null || tmYStr == null || tmXStr.trim().isEmpty() || tmYStr.trim().isEmpty()) { - log.warn("정류소 좌표가 null이거나 비어있음: {}", station.stNm()); + if (tmXStr == null || tmYStr == null || tmXStr.trim().isEmpty() || tmYStr.trim().isEmpty() || + "null".equalsIgnoreCase(tmXStr.trim()) || "null".equalsIgnoreCase(tmYStr.trim())) { continue; } double stationX = Double.parseDouble(tmXStr); double stationY = Double.parseDouble(tmYStr); - double distance = Math.sqrt(Math.pow(stationX - x, 2) + Math.pow(stationY - y, 2)); - if (distance < minDistance) { - minDistance = distance; - closestStation = station; - } + // 거리 정보를 포함한 Wrapper 클래스 대신 정렬용 로직 사용 + sortedStations.add(station); } catch (NumberFormatException e) { - log.warn("정류소 좌표 파싱 실패 - {}: tmX={}, tmY={}, error={}", - station.stNm(), station.tmX(), station.tmY(), e.getMessage()); + log.warn("정류소 좌표 파싱 실패 - {}: tmX={}, tmY={}", station.stNm(), station.tmX(), station.tmY()); continue; } } + + // 거리순으로 정렬 + sortedStations.sort((s1, s2) -> { + try { + double d1 = Math.sqrt(Math.pow(Double.parseDouble(s1.tmX()) - x, 2) + Math.pow(Double.parseDouble(s1.tmY()) - y, 2)); + double d2 = Math.sqrt(Math.pow(Double.parseDouble(s2.tmX()) - x, 2) + Math.pow(Double.parseDouble(s2.tmY()) - y, 2)); + return Double.compare(d1, d2); + } catch (NumberFormatException e) { + return 0; + } + }); + + // 2. routeId가 있으면 가장 가까운 상위 3개 정류소에서만 해당 버스가 운행되는지 확인 + if (routeId != null) { + var route = routeRepository.findById(routeId); + String projectRouteName = route.isPresent() ? route.get().getRouteName() : null; + + if (projectRouteName != null) { + int maxStationsToCheck = Math.min(3, sortedStations.size()); // 최대 3개만 확인 + for (int i = 0; i < maxStationsToCheck; i++) { + StationSearchResponse.StationItem station = sortedStations.get(i); + try { + // 각 정류소에서 버스 정보 조회 + OpenDataResponse busResponse = fetchArrivals(station.arsId(), null); + if (busResponse != null && busResponse.msgBody() != null && busResponse.msgBody().itemList() != null) { + for (OpenDataResponse.Item item : busResponse.msgBody().itemList()) { + if (item.rtNm() != null && + (projectRouteName.contains(item.rtNm()) || item.rtNm().contains(projectRouteName))) { + + return station; + } + } + } + } catch (Exception e) { + log.warn("정류소 {}에서 버스 정보 조회 실패: {}", station.stNm(), e.getMessage()); + continue; + } + } + } + } - return closestStation; + // 해당 버스가 운행되는 정류소가 없으면 가장 가까운 정류소 선택 + return sortedStations.isEmpty() ? null : sortedStations.get(0); } } diff --git a/src/main/java/com/wayble/server/direction/service/FacilityService.java b/src/main/java/com/wayble/server/direction/service/FacilityService.java index e00d665..77715b5 100644 --- a/src/main/java/com/wayble/server/direction/service/FacilityService.java +++ b/src/main/java/com/wayble/server/direction/service/FacilityService.java @@ -16,6 +16,7 @@ import com.wayble.server.direction.repository.WheelchairInfoRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import lombok.Builder; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; @@ -45,39 +46,34 @@ public TransportationResponseDto.NodeInfo getNodeInfo(Long nodeId, Long routeId) List elevator = new ArrayList<>(); Boolean accessibleRestroom = false; - Optional nodeOpt = nodeRepository.findById(nodeId); - if (nodeOpt.isPresent()) { - Node node = nodeOpt.get(); - - if (routeId != null) { - List wheelchairs = wheelchairInfoRepository.findByRouteId(routeId); - for (Wheelchair wheelchairInfo : wheelchairs) { - String location = wheelchairInfo.getWheelchairLocation(); - if (location != null && !location.trim().isEmpty()) { - wheelchair.add(location.trim()); - } + if (routeId != null) { + List wheelchairLocations = wheelchairInfoRepository.findWheelchairLocationsByRouteId(routeId); + for (String location : wheelchairLocations) { + if (location != null && !location.trim().isEmpty()) { + wheelchair.add(location.trim()); } } - - Facility facility = facilityRepository.findByNodeId(nodeId).orElse(null); - if (facility != null) { - String stinCd = facility.getStinCd(); - String railOprLsttCd = facility.getRailOprLsttCd(); - String lnCd = facility.getLnCd(); + } + + Optional facilityOpt = facilityRepository.findByNodeId(nodeId); + if (facilityOpt.isPresent()) { + Facility facility = facilityOpt.get(); + String stinCd = facility.getStinCd(); + String railOprLsttCd = facility.getRailOprLsttCd(); + String lnCd = facility.getLnCd(); - if (stinCd != null && railOprLsttCd != null && lnCd != null) { - Map toiletInfo = getToiletInfo(facility); - accessibleRestroom = toiletInfo.getOrDefault(stinCd, false); - - elevator = getElevatorInfo(facility, routeId); - } else { - log.error("Facility 정보 누락 - nodeId: {}, stinCd: {}, railOprLsttCd: {}, lnCd: {}", - nodeId, stinCd, railOprLsttCd, lnCd); - } + if (stinCd != null && railOprLsttCd != null && lnCd != null) { + Map toiletInfo = getToiletInfo(facility); + accessibleRestroom = toiletInfo.getOrDefault(stinCd, false); + + elevator = getElevatorInfo(facility, routeId); } else { - log.error("Facility 정보 없음 - nodeId: {}", nodeId); + log.warn("Facility 정보 누락 - nodeId: {}, stinCd: {}, railOprLsttCd: {}, lnCd: {}", + nodeId, stinCd, railOprLsttCd, lnCd); } + } else { + log.debug("Facility 정보 없음 - nodeId: {}", nodeId); } return new TransportationResponseDto.NodeInfo( 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 0314bb9..6a37f97 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -1,6 +1,8 @@ package com.wayble.server.direction.service; import com.wayble.server.common.exception.ApplicationException; +import com.wayble.server.direction.dto.InternalStep; +import com.wayble.server.direction.dto.NodeRef; import com.wayble.server.direction.dto.TransportationGraphDto; import com.wayble.server.direction.dto.request.TransportationRequestDto; import com.wayble.server.direction.dto.response.TransportationResponseDto; @@ -8,7 +10,9 @@ import com.wayble.server.direction.entity.transportation.Node; import com.wayble.server.direction.entity.transportation.Route; import com.wayble.server.direction.entity.type.DirectionType; +import com.wayble.server.direction.repository.EdgeBoundingBoxProjection; import com.wayble.server.direction.repository.EdgeRepository; +import com.wayble.server.direction.repository.NodeBoundingBoxProjection; import com.wayble.server.direction.repository.NodeRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,7 +32,7 @@ public class TransportationService { private final EdgeRepository edgeRepository; private final FacilityService facilityService; private final BusInfoService busInfoService; - + private static final int TRANSFER_PENALTY = 10000; // 환승 시 추가되는 가중치 (m) private static final int STEP_PENALTY = 500; // 각 이동 단계마다 추가되는 기본 가중치 (m) private static final int METER_CONVERSION = 1000; // 킬로미터를 미터로 변환하는 상수 @@ -43,6 +47,10 @@ public class TransportationService { // 공간 필터링 private static final double SPATIAL_BUFFER_KM = 15.0; // 지작점/도착점 주변 15km + private NodeRef toNodeRef(Node node) { + return new NodeRef(node.getId(), node.getStationName(), node.getLatitude(), node.getLongitude()); + } + public TransportationResponseDto findRoutes(TransportationRequestDto request){ TransportationRequestDto.Location origin = request.origin(); @@ -59,7 +67,7 @@ public TransportationResponseDto findRoutes(TransportationRequestDto request){ Node end = Node.createNode(-2L, destination.name(), DirectionType.TO_WAYPOINT,destination.latitude(), destination.longitude()); // 3. 여러 경로 찾기 - List> allRoutes = findMultipleTransportationRoutes(start, end); + List> allRoutes = findMultipleTransportationRoutes(start, end); // 4. 페이징 처리 int startIndex = (request.cursor() != null) ? request.cursor() : 0; @@ -76,10 +84,14 @@ public TransportationResponseDto findRoutes(TransportationRequestDto request){ // 페이징된 경로들을 Route 객체로 변환 List routeList = new ArrayList<>(); - List> pagedRoutes = allRoutes.subList(startIndex, endIndex); + 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); + List internalRoute = pagedRoutes.get(i); + + // InternalStep을 TransportationResponseDto.Step으로 변환 (API 호출 포함) + List enrichedRoute = enrichRoutesWithServiceInfo(internalRoute); + + TransportationResponseDto.Route routeObj = createRoute(enrichedRoute, startIndex + i + 1); routeList.add(routeObj); } @@ -90,17 +102,75 @@ private TransportationResponseDto.Route createRoute(List> findMultipleTransportationRoutes(Node startTmp, Node endTmp){ + private List> findMultipleTransportationRoutes(Node startTmp, Node endTmp){ + List nodes = null; + List edges = null; + try { // 1. 공간 필터링을 사용한 데이터 로드 double[] boundingBox = calculateBoundingBox(startTmp, endTmp); - List nodes = nodeRepository.findNodesInBoundingBox( + List nodeRows = nodeRepository.findNodesInBoundingBox( boundingBox[0], boundingBox[1], boundingBox[2], boundingBox[3] ); - List edges = edgeRepository.findEdgesInBoundingBox( + + // 프로젝션 인터페이스를 Node 객체로 변환 + nodes = nodeRows.stream() + .map(row -> new Node( + row.getId(), // id + row.getStationName(), // stationName + row.getNodeType(), // nodeType + row.getLatitude(), // latitude + row.getLongitude() // longitude + )) + .collect(Collectors.toList()); + // 최적화된 쿼리 사용: 필요한 컬럼만 조회 + List edgeRows = edgeRepository.findEdgesInBoundingBox( boundingBox[0], boundingBox[1], boundingBox[2], boundingBox[3] ); + // 프로젝션 인터페이스를 Edge 객체로 변환 + edges = edgeRows.stream() + .map(row -> { + // Node 객체 생성 + Node startNode = Node.createNode( + row.getStartNodeId(), // startNode.id + row.getStartStationName(), // startNode.stationName + row.getEdgeType(), // edgeType + row.getStartLatitude(), // startNode.latitude + row.getStartLongitude() // startNode.longitude + ); + + Node endNode = Node.createNode( + row.getEndNodeId(), // endNode.id + row.getEndStationName(), // endNode.stationName + row.getEdgeType(), // edgeType + row.getEndLatitude(), // endNode.latitude + row.getEndLongitude() // endNode.longitude + ); + + // Route 객체 생성 (null일 수 있음) + Route route = null; + if (row.getRouteId() != null) { // routeId가 null이 아닌 경우 + route = Route.createRoute( + row.getRouteId(), // routeId + row.getRouteName(), // routeName + row.getEdgeType(), // routeType + startNode, + endNode + ); + } + + // Edge 객체 생성 + return Edge.createEdgeWithRoute( + row.getEdgeId(), // edge.id + startNode, + endNode, + row.getEdgeType(), // edgeType + route + ); + }) + .collect(Collectors.toList()); + log.debug("Spatial filtering loaded {} nodes and {} edges", nodes.size(), edges.size()); // 2. 가장 가까운 정류장 찾기 @@ -117,18 +187,30 @@ private List> findMultipleTransportationRou // 4. 그래프 빌드 및 여러 경로 찾기 TransportationGraphDto graphData = buildGraph(nodes, edges, startTmp, endTmp); - List> result = findMultipleOptimalRoutes( + List> result = findMultipleOptimalRoutes( graphData.graph(), startTmp, endTmp, graphData.weightMap(), nodes, nearestToStart, nearestToEnd ); - // 5. 메모리 정리 (명시적으로 null 설정) - nodes.clear(); - edges.clear(); return result; } catch (OutOfMemoryError e) { log.error("Out of memory error in transportation route finding: {}", e.getMessage()); throw new ApplicationException(PATH_NOT_FOUND); + } finally { + // 5. 메모리 정리 (finally 블록에서 확실히 실행) + if (nodes != null) { + nodes.clear(); + nodes = null; + } + if (edges != null) { + edges.clear(); + edges = null; + } + + // 명시적 GC 호출 + if (Runtime.getRuntime().freeMemory() < Runtime.getRuntime().totalMemory() * 0.1) { + System.gc(); + } } } @@ -141,7 +223,7 @@ private double[] calculateBoundingBox(Node start, Node end) { return new double[]{minLat, maxLat, minLon, maxLon}; } - private List> findMultipleOptimalRoutes( + private List> findMultipleOptimalRoutes( Map> graph, Node startTmp, Node endTmp, @@ -166,30 +248,31 @@ private List> findMultipleOptimalRoutes( } // 2. 여러 경로 찾기 - List> allRoutes = findMultipleRoutes(graph, startNode, endNode, weightMap, nodes); + List> allRoutes = findMultipleRoutes(graph, startNode, endNode, weightMap, nodes); // 3. 경로 필터링 및 정렬 - return filterAndSortRoutes(allRoutes); + List> result = filterAndSortRoutes(allRoutes); + return result; } - private List> findMultipleRoutes( + private List> findMultipleRoutes( Map> graph, Node start, Node end, Map, Integer> weightMap, List nodes) { - List> routes = new ArrayList<>(); + List> routes = new ArrayList<>(); // 1. 기본 다익스트라로 첫 번째 경로 찾기 - List firstRoute = runDijkstra(graph, start, end, weightMap, nodes); + List firstRoute = runDijkstra(graph, start, end, weightMap, nodes); if (!firstRoute.isEmpty()) { routes.add(firstRoute); } // 2. 효율적인 다중 경로 찾기 - 한 번의 탐색으로 여러 경로 생성 if (!firstRoute.isEmpty()) { - List> alternativeRoutes = findAlternativeRoutesEfficiently( + List> alternativeRoutes = findAlternativeRoutesEfficiently( graph, start, end, weightMap, nodes, firstRoute ); routes.addAll(alternativeRoutes); @@ -198,15 +281,15 @@ private List> findMultipleRoutes( return routes; } - private List> findAlternativeRoutesEfficiently( + private List> findAlternativeRoutesEfficiently( Map> graph, Node start, Node end, Map, Integer> weightMap, List nodes, - List firstRoute) { + List firstRoute) { - List> alternativeRoutes = new ArrayList<>(); + List> alternativeRoutes = new ArrayList<>(); // 첫 번째 경로에서 실제 사용된 엣지들을 추출 Set> usedEdges = extractActualEdgesFromRoute(firstRoute, graph); @@ -217,7 +300,7 @@ private List> findAlternativeRoutesEfficien Map, Integer> penalizedWeightMap = createActualEdgePenalizedWeightMap(weightMap, usedEdges, i + 1); // 다익스트라로 새로운 경로 찾기 - List newRoute = runDijkstra(graph, start, end, penalizedWeightMap, nodes); + List newRoute = runDijkstra(graph, start, end, penalizedWeightMap, nodes); if (newRoute.isEmpty()) { break; @@ -231,7 +314,7 @@ private List> findAlternativeRoutesEfficien // 새로운 경로에서 사용된 엣지들도 추가 Set> newUsedEdges = extractActualEdgesFromRoute(newRoute, graph); usedEdges.addAll(newUsedEdges); - + alternativeRoutes.add(newRoute); } @@ -242,10 +325,10 @@ private List> findAlternativeRoutesEfficien - private Set> extractActualEdgesFromRoute(List route, Map> graph) { + private Set> extractActualEdgesFromRoute(List route, Map> graph) { Set> usedEdges = new HashSet<>(); - for (TransportationResponseDto.Step step : route) { + for (InternalStep step : route) { String fromName = step.from(); String toName = step.to(); @@ -287,15 +370,15 @@ private Map, Integer> createActualEdgePenalizedWeightMap(Map route1, List route2) { + 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); + InternalStep step1 = route1.get(i); + InternalStep step2 = route2.get(i); if (step1.mode() != step2.mode() || !Objects.equals(step1.from(), step2.from()) || @@ -308,8 +391,8 @@ private boolean areRoutesIdentical(List route1, return true; } - private List> filterAndSortRoutes(List> routes) { - return routes.stream() + private List> filterAndSortRoutes(List> routes) { + List> filteredRoutes = routes.stream() .filter(route -> { // 대중교통 포함 여부 확인 boolean hasPublicTransport = route.stream() @@ -324,13 +407,15 @@ private List> filterAndSortRoutes(List>comparingInt(this::calculateTransferCount) + .>comparingInt(this::calculateTransferCount) .thenComparingInt(this::calculateWalkDistance)) .limit(MAX_ROUTES) .collect(Collectors.toList()); + + return filteredRoutes; } - private int calculateWalkDistance(List route) { + private int calculateWalkDistance(List route) { return route.stream() .filter(step -> step.mode() == DirectionType.WALK) .mapToInt(step -> { @@ -453,13 +538,20 @@ private List findNearbyNodes(List nodes, double lat, double lon, int .collect(Collectors.toList()); } - private List runDijkstra(Map> graph, Node start, Node end, Map, Integer> weightMap, List nodes) { - // 1. 초기화 + private List runDijkstra(Map> graph, Node start, Node end, Map, Integer> weightMap, List nodes) { + // 1. 초기화 - HashMap 대신 Array 사용으로 성능 향상 Map distance = new HashMap<>(); Map prevEdge = new HashMap<>(); Map prevNode = new HashMap<>(); Set visited = new HashSet<>(); + Map nodeMap = nodes.stream() + .collect(Collectors.toMap( + Node::getId, + node -> node, + (existing, replacement) -> existing // 중복 시 기존 값 유지 + )); + for (Node node : nodes) { distance.put(node.getId(), Integer.MAX_VALUE); prevNode.put(node.getId(), null); @@ -467,7 +559,8 @@ private List runDijkstra(Map> g } distance.put(start.getId(), 0); - PriorityQueue pq = new PriorityQueue<>(Comparator.comparingInt(n -> distance.get(n.getId()))); + PriorityQueue pq = new PriorityQueue<>(Math.min(1000, nodes.size()), + Comparator.comparingInt(n -> distance.get(n.getId()))); pq.add(start); int visitedCount = 0; @@ -582,6 +675,9 @@ private List runDijkstra(Map> g List pathEdges = new ArrayList<>(); Node current = end; Set backtrackVisited = new HashSet<>(); + + long requestId = System.currentTimeMillis(); + while (current != null && !current.equals(start)) { if (backtrackVisited.contains(current.getId())) break; @@ -594,11 +690,71 @@ private List runDijkstra(Map> g current = prevNode.get(current.getId()); } - return mergeConsecutiveRoutes(pathEdges); + return mergeConsecutiveRoutes(pathEdges, requestId); + } + + private List enrichRoutesWithServiceInfo(List steps) { + List enrichedSteps = new ArrayList<>(); + + for (InternalStep step : steps) { + TransportationResponseDto.BusInfo busInfo = null; + TransportationResponseDto.SubwayInfo subwayInfo = null; + + if (step.mode() == DirectionType.BUS) { + log.info("🚌 최종 경로 - 버스 정보 조회: from={}, to={}, routeId={}", step.from(), step.to(), step.routeId()); + + if (step.routeId() != null && step.startNode() != null) { + try { + busInfo = busInfoService.getBusInfo( + step.from(), + step.routeId(), + step.startNode().latitude(), + step.startNode().longitude() + ); + } catch (Exception e) { + log.error("버스 정보 조회 실패: {}", e.getMessage()); + } + } + } else if (step.mode() == DirectionType.SUBWAY) { + log.info("🚇 최종 경로 - 지하철 정보 조회: from={}, to={}, routeId={}", step.from(), step.to(), step.routeId()); + if (step.routeId() != null && step.startNode() != null) { + try { + TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(step.startNode().id(), step.routeId()); + subwayInfo = new TransportationResponseDto.SubwayInfo( + nodeInfo.wheelchair(), + nodeInfo.elevator(), + nodeInfo.accessibleRestroom() + ); + } catch (Exception e) { + log.error("지하철 정보 조회 실패: {}", e.getMessage()); + subwayInfo = new TransportationResponseDto.SubwayInfo( + new ArrayList<>(), + new ArrayList<>(), + false + ); + } + } + } + + // 새로운 Step 생성 (busInfo, subwayInfo 포함) + TransportationResponseDto.Step enrichedStep = new TransportationResponseDto.Step( + step.mode(), + step.moveInfo(), + step.routeName(), + step.moveNumber(), + busInfo, + subwayInfo, + step.from(), + step.to() + ); + enrichedSteps.add(enrichedStep); + } + + return enrichedSteps; } - private List mergeConsecutiveRoutes(List pathEdges) { - List mergedSteps = new ArrayList<>(); + private List mergeConsecutiveRoutes(List pathEdges, long requestId) { + List mergedSteps = new ArrayList<>(); if (pathEdges.isEmpty()) { return mergedSteps; @@ -637,6 +793,7 @@ private List mergeConsecutiveRoutes(List p // 2. 노드명 및 기본 정보 설정 String fromName = getNodeName(currentEdge.getStartNode()); String toName = getNodeName(pathEdges.get(j - 1).getEndNode()); + if (currentType == DirectionType.WALK) { int walkDistance = 0; // 미터 단위 @@ -651,8 +808,8 @@ private List mergeConsecutiveRoutes(List p walkDistance = (int) (distanceKm * 1000); // km를 m로 변환 } - mergedSteps.add(new TransportationResponseDto.Step( - DirectionType.WALK, null, null, walkDistance, null, null, fromName, toName + mergedSteps.add(new InternalStep( + DirectionType.WALK, null, null, walkDistance, null, null, fromName, toName, null, toNodeRef(walkStartNode), toNodeRef(walkEndNode) )); i = j; continue; @@ -660,62 +817,24 @@ private List mergeConsecutiveRoutes(List p // 3. 교통수단 상세 정보 (moveInfo) 설정 List moveInfoList = createMoveInfoList(pathEdges, i, j); - String routeName = getRouteName(pathEdges, i, j); - // busInfo / subwayInfo 설정 + // busInfo / subwayInfo는 나중에 설정 (최종 경로 선택 후) TransportationResponseDto.BusInfo busInfo = null; TransportationResponseDto.SubwayInfo subwayInfo = null; - - if (currentType == DirectionType.BUS) { - try { - if (currentEdge.getStartNode() != null && currentEdge.getRoute() != null) { - busInfo = busInfoService.getBusInfo( - currentEdge.getStartNode().getStationName(), - currentEdge.getRoute().getRouteId(), - currentEdge.getStartNode().getLatitude(), - currentEdge.getStartNode().getLongitude() - ); - - if (busInfo != null && - busInfo.isLowFloor() != null && !busInfo.isLowFloor().isEmpty() && - busInfo.dispatchInterval() != null && - busInfo.isLowFloor().stream().allMatch(floor -> !floor) && - busInfo.dispatchInterval() == 0) { - return new ArrayList<>(); - } - } - } catch (Exception e) { - log.error("버스 정보 조회 실패: {}", e.getMessage()); - } - } else if (currentType == DirectionType.SUBWAY) { - try { - if (currentEdge.getStartNode() != null) { - TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId()); - - subwayInfo = new TransportationResponseDto.SubwayInfo( - nodeInfo.wheelchair(), - nodeInfo.elevator(), - nodeInfo.accessibleRestroom() - ); - } else { - subwayInfo = new TransportationResponseDto.SubwayInfo( - new ArrayList<>(), - new ArrayList<>(), - false - ); - } - } catch (Exception e) { - log.error("지하철 정보 조회 실패: {}", e.getMessage()); - subwayInfo = new TransportationResponseDto.SubwayInfo( - new ArrayList<>(), - new ArrayList<>(), - false - ); - } - } int moveNumber = j - i - 1; - mergedSteps.add(new TransportationResponseDto.Step( + String routeName = getRouteName(pathEdges, i, j); + + // routeId와 Node 정보 추출 + Long routeId = null; + Node startNode = currentEdge.getStartNode(); + Node endNode = pathEdges.get(j - 1).getEndNode(); + + if (currentEdge.getRoute() != null) { + routeId = currentEdge.getRoute().getRouteId(); + } + + mergedSteps.add(new InternalStep( currentType, moveInfoList, routeName, @@ -723,7 +842,10 @@ private List mergeConsecutiveRoutes(List p busInfo, subwayInfo, fromName, - toName + toName, + routeId, + toNodeRef(startNode), + toNodeRef(endNode) )); i = j; @@ -733,16 +855,16 @@ private List mergeConsecutiveRoutes(List p return addTransferWalkSteps(mergedSteps, pathEdges); } - private List addTransferWalkSteps(List steps, List pathEdges) { - List result = new ArrayList<>(); + private List addTransferWalkSteps(List steps, List pathEdges) { + List result = new ArrayList<>(); for (int i = 0; i < steps.size(); i++) { - TransportationResponseDto.Step currentStep = steps.get(i); + InternalStep currentStep = steps.get(i); result.add(currentStep); // 마지막 step이 아니고, 현재 step이 walk가 아닌 경우 if (i < steps.size() - 1 && currentStep.mode() != DirectionType.WALK) { - TransportationResponseDto.Step nextStep = steps.get(i + 1); + InternalStep nextStep = steps.get(i + 1); // 다음 step도 walk가 아닌 경우 (bus -> subway, subway -> bus 등) if (nextStep.mode() != DirectionType.WALK) { @@ -753,7 +875,7 @@ private List addTransferWalkSteps(List addTransferWalkSteps(List nodes, double lat, double lon) { .orElse(null); } - private int calculateTransferCount(List steps) { + private int calculateTransferCount(List steps) { int transferCount = 0; DirectionType previousMode = null; String previousRouteName = null; - for (TransportationResponseDto.Step step : steps) { + for (InternalStep step : steps) { if (step.mode() != DirectionType.WALK && step.mode() != DirectionType.FROM_WAYPOINT && step.mode() != DirectionType.TO_WAYPOINT) { if (previousMode != null) { if (previousMode == step.mode() && @@ -883,4 +1008,6 @@ private int calculateTransferCount(List steps) { } return transferCount; } + + }