diff --git a/src/main/java/com/wayble/server/ServerApplication.java b/src/main/java/com/wayble/server/ServerApplication.java index 5a3f21ac..ec170f39 100644 --- a/src/main/java/com/wayble/server/ServerApplication.java +++ b/src/main/java/com/wayble/server/ServerApplication.java @@ -2,6 +2,7 @@ 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 org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration; @@ -16,7 +17,7 @@ ) @EnableJpaAuditing @EnableScheduling -@EnableConfigurationProperties({TMapProperties.class, KricProperties.class}) +@EnableConfigurationProperties({TMapProperties.class, KricProperties.class, OpenDataProperties.class}) @EnableElasticsearchRepositories(basePackages = {"com.wayble.server.explore.repository", "com.wayble.server.logging.repository", "com.wayble.server.direction.repository"}) @EntityScan(basePackages = "com.wayble.server") public class ServerApplication { diff --git a/src/main/java/com/wayble/server/common/config/HttpClientConfig.java b/src/main/java/com/wayble/server/common/config/HttpClientConfig.java new file mode 100644 index 00000000..508a12e7 --- /dev/null +++ b/src/main/java/com/wayble/server/common/config/HttpClientConfig.java @@ -0,0 +1,31 @@ +package com.wayble.server.common.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import java.net.http.HttpClient; +import java.time.Duration; + +@Configuration +@RequiredArgsConstructor +public class HttpClientConfig { + + @Value("${http.client.connect-timeout:10}") + private int connectTimeout; + + @Value("${http.client.request-timeout:30}") + private int requestTimeout; + + @Bean + public HttpClient httpClient() { + return HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(connectTimeout)) + .build(); + } + + @Bean + public Duration httpRequestTimeout() { + return Duration.ofSeconds(requestTimeout); + } +} diff --git a/src/main/java/com/wayble/server/direction/controller/TransportationController.java b/src/main/java/com/wayble/server/direction/controller/TransportationController.java index d7a3f58c..a4eb9a38 100644 --- a/src/main/java/com/wayble/server/direction/controller/TransportationController.java +++ b/src/main/java/com/wayble/server/direction/controller/TransportationController.java @@ -1,8 +1,8 @@ package com.wayble.server.direction.controller; import com.wayble.server.common.response.CommonResponse; -import com.wayble.server.direction.dto.TransportationRequestDto; -import com.wayble.server.direction.dto.TransportationResponseDto; +import com.wayble.server.direction.dto.request.TransportationRequestDto; +import com.wayble.server.direction.dto.response.TransportationResponseDto; import com.wayble.server.direction.service.TransportationService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/com/wayble/server/direction/dto/TransportationGraphDto.java b/src/main/java/com/wayble/server/direction/dto/TransportationGraphDto.java new file mode 100644 index 00000000..ebdddb47 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/dto/TransportationGraphDto.java @@ -0,0 +1,12 @@ +package com.wayble.server.direction.dto; + +import com.wayble.server.direction.entity.transportation.Edge; +import org.springframework.data.util.Pair; + +import java.util.List; +import java.util.Map; + +public record TransportationGraphDto( // 노드별 연결 정보와 엣지별 가중치를 함께 관리하기 위한 dto + Map> graph, // 노드별 연결 정보 + Map, Integer> weightMap // 엣지별 가중치 +) {} diff --git a/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java b/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java deleted file mode 100644 index 44255d4f..00000000 --- a/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.wayble.server.direction.dto; - -import com.wayble.server.direction.entity.DirectionType; -import org.springframework.lang.Nullable; -import io.swagger.v3.oas.annotations.media.Schema; - -import java.util.List; - -@Schema(description = "대중교통 길찾기 응답 DTO") -public record TransportationResponseDto( - List routes, - PageInfo pageInfo -) { - public record Step( - DirectionType mode, // 예: START, WALK, SUBWAY, BUS, FINISH - @Nullable String routeName, - @Nullable NodeInfo information, - String from, - String to - ) {} - - public record PageInfo( - Integer nextCursor, - boolean hasNext - ) {} - - public record NodeInfo( - List wheelchair, - List elevator, - Boolean accessibleRestroom - ) {} - - public record LocationInfo( - Double latitude, - Double Longitude - ) {} -} diff --git a/src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java b/src/main/java/com/wayble/server/direction/dto/request/TransportationRequestDto.java similarity index 87% rename from src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java rename to src/main/java/com/wayble/server/direction/dto/request/TransportationRequestDto.java index 9d2a0230..f3b308c2 100644 --- a/src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java +++ b/src/main/java/com/wayble/server/direction/dto/request/TransportationRequestDto.java @@ -1,4 +1,4 @@ -package com.wayble.server.direction.dto; +package com.wayble.server.direction.dto.request; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/wayble/server/direction/dto/response/BusInfo.java b/src/main/java/com/wayble/server/direction/dto/response/BusInfo.java new file mode 100644 index 00000000..71910c57 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/dto/response/BusInfo.java @@ -0,0 +1,11 @@ +package com.wayble.server.direction.dto.response; + +import java.util.List; + +public record BusInfo(List buses, String stationName) { + public record BusArrival( + String busNumber, + String arrival1, + String arrival2 + ) {} +} 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 new file mode 100644 index 00000000..f6c8ea61 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java @@ -0,0 +1,57 @@ +package com.wayble.server.direction.dto.response; + +import com.wayble.server.direction.entity.DirectionType; +import org.springframework.lang.Nullable; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "대중교통 길찾기 응답 DTO") +public record TransportationResponseDto( + List routes, + PageInfo pageInfo +) { + public record Step( + DirectionType mode, // 예: START, WALK, SUBWAY, BUS, FINISH + @Nullable List moveInfo, // 같은 Step으로 이동한 정류장(Node) 정보 (중간 정류장만) + @Nullable String routeName, + Integer moveNumber, // 같은 Step(route)로 이동한 횟수 + @Nullable BusInfo busInfo, // 버스일 경우에만 생성, 이외의 경우 null + @Nullable SubwayInfo subwayInfo, // 지하철일 경우에만 생성, 이외의 경우 null + String from, + String to + ) {} + + public record PageInfo( + Integer nextCursor, + boolean hasNext + ) {} + + public record MoveInfo( + String nodeName // 정류장(Node)의 stationName + ){} + + public record BusInfo( + boolean isShuttleBus, // routeName에 "마포" 포함시 true + @Nullable List isLowFloor, // Open API(busType1,busType2) 기반 저상 여부 리스트 + @Nullable Integer dispatchInterval // Open API(term) 기반 배차간격 + ){} + + public record SubwayInfo( + List wheelchair, + List elevator, + Boolean accessibleRestroom + ) {} + + public record LocationInfo( + Double latitude, + Double longitude + ) {} + + // 지하철 시설 정보 묶음 (서비스 내부에서 사용) + public record NodeInfo( + List wheelchair, + List elevator, + Boolean accessibleRestroom + ) {} +} 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 952d30d9..9fe6cb43 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 @@ -21,17 +21,17 @@ public class Edge { private DirectionType edgeType; // 출발 노드 - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "start_node_id") private Node startNode; // 도착 노드 - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "end_node_id") private Node endNode; // 해당 연결이 속한 노선 - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "route_id") private Route route; diff --git a/src/main/java/com/wayble/server/direction/entity/transportation/Facility.java b/src/main/java/com/wayble/server/direction/entity/transportation/Facility.java index fb8af53d..c4e62daf 100644 --- a/src/main/java/com/wayble/server/direction/entity/transportation/Facility.java +++ b/src/main/java/com/wayble/server/direction/entity/transportation/Facility.java @@ -38,7 +38,7 @@ public class Facility { @OneToMany(mappedBy = "facility", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List elevators; - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @MapsId @JoinColumn(name = "id") private Node node; diff --git a/src/main/java/com/wayble/server/direction/entity/transportation/Node.java b/src/main/java/com/wayble/server/direction/entity/transportation/Node.java index 3b5029a6..7986fc54 100644 --- a/src/main/java/com/wayble/server/direction/entity/transportation/Node.java +++ b/src/main/java/com/wayble/server/direction/entity/transportation/Node.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.Objects; +import org.hibernate.annotations.BatchSize; + import com.wayble.server.direction.entity.DirectionType; @Entity @@ -32,23 +34,24 @@ public class Node { private Double longitude; // 출발 edge 리스트 (정류장에서 출발) - @OneToMany(mappedBy = "startNode") + @OneToMany(mappedBy = "startNode", fetch = FetchType.LAZY) + @BatchSize(size = 100) private List outgoingEdges; // 도착 Edge 리스트 (정류장으로 도착) - @OneToMany(mappedBy = "endNode") + @OneToMany(mappedBy = "endNode", fetch = FetchType.LAZY) + @BatchSize(size = 100) private List incomingEdges; // 이 정류장이 기점/종점인 노선 - @OneToMany(mappedBy = "startNode") + @OneToMany(mappedBy = "startNode", fetch = FetchType.LAZY) + @BatchSize(size = 100) private List startRoutes; - @OneToMany(mappedBy = "endNode") + @OneToMany(mappedBy = "endNode", fetch = FetchType.LAZY) + @BatchSize(size = 100) private List endRoutes; - @OneToOne(mappedBy = "node") - private Facility facility_id; - public Node(Long id, String stationName, DirectionType nodeType, double latitude, double longitude) { this.id = id; this.stationName = stationName; @@ -57,6 +60,10 @@ public Node(Long id, String stationName, DirectionType nodeType, double latitude this.longitude = longitude; } + public static Node createNode(Long id, String stationName, DirectionType nodeType, double latitude, double longitude) { + return new Node(id, stationName, nodeType, latitude, longitude); + } + @Override public boolean equals(Object o) { if (this == o) return true; 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 new file mode 100644 index 00000000..6819cfb3 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java @@ -0,0 +1,16 @@ +package com.wayble.server.direction.external.opendata; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@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) {} +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/direction/external/opendata/dto/Arrival.java b/src/main/java/com/wayble/server/direction/external/opendata/dto/Arrival.java new file mode 100644 index 00000000..2cd7e711 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/opendata/dto/Arrival.java @@ -0,0 +1,7 @@ +package com.wayble.server.direction.external.opendata.dto; + +public record Arrival ( + Integer busType1, // 1이면 저상 + Integer busType2, // 1이면 저상 + Integer term // 배차 간격 +) {} 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 new file mode 100644 index 00000000..22d29076 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java @@ -0,0 +1,39 @@ +package com.wayble.server.direction.external.opendata.dto; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record OpenDataResponse ( // 버스 정류장 id를 기반으로 배차시간, 저상버스 여부를 확인하는 엔드포인트 + @JsonProperty("comMsgHeader") ComMsgHeader comMsgHeader, + @JsonProperty("msgHeader") MsgHeader msgHeader, + @JsonProperty("msgBody") MsgBody msgBody +) { + public record ComMsgHeader( + @JsonProperty("errMsg") String errMsg, + @JsonProperty("responseTime") String responseTime, + @JsonProperty("requestMsgID") String requestMsgID, + @JsonProperty("responseMsgID") String responseMsgID, + @JsonProperty("successYN") String successYN, + @JsonProperty("returnCode") String returnCode + ) {} + public record MsgHeader( + @JsonProperty("headerMsg") String headerMsg, + @JsonProperty("headerCd") String headerCd, + @JsonProperty("itemCount") Integer itemCount + ) {} + + public record MsgBody( + @JsonProperty("itemList") List itemList + ) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Item( + @JsonProperty("busType1") String busType1, + @JsonProperty("busType2") String busType2, + @JsonProperty("term") String term, + @JsonProperty("busRouteId") String busRouteId + ) {} +} \ 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 new file mode 100644 index 00000000..51143aca --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java @@ -0,0 +1,21 @@ +package com.wayble.server.direction.external.opendata.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record StationSearchResponse( // 버스 정류장 id를 검색하는 엔드포인트 + StationSearchMsgBody msgBody +) { + public record StationSearchMsgBody( + List itemList + ) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + public record StationItem( + String stId, + String stNm, + String tmX, + String tmY + ) {} +} 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 b9fd0d7f..543628fc 100644 --- a/src/main/java/com/wayble/server/direction/repository/EdgeRepository.java +++ b/src/main/java/com/wayble/server/direction/repository/EdgeRepository.java @@ -2,6 +2,16 @@ import com.wayble.server.direction.entity.transportation.Edge; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository public interface EdgeRepository extends JpaRepository { + @Query("SELECT DISTINCT e FROM Edge e " + + "JOIN FETCH e.startNode " + + "JOIN FETCH e.endNode " + + "LEFT JOIN FETCH e.route") + List findAllWithNodesAndRoute(); } diff --git a/src/main/java/com/wayble/server/direction/repository/RouteRepository.java b/src/main/java/com/wayble/server/direction/repository/RouteRepository.java new file mode 100644 index 00000000..719a5d50 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/repository/RouteRepository.java @@ -0,0 +1,9 @@ +package com.wayble.server.direction.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.wayble.server.direction.entity.transportation.Route; + +public interface RouteRepository extends JpaRepository{ + +} diff --git a/src/main/java/com/wayble/server/direction/service/BusInfoService.java b/src/main/java/com/wayble/server/direction/service/BusInfoService.java new file mode 100644 index 00000000..c47819c5 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/service/BusInfoService.java @@ -0,0 +1,203 @@ +package com.wayble.server.direction.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wayble.server.direction.external.opendata.OpenDataProperties; +import com.wayble.server.direction.external.opendata.dto.OpenDataResponse; +import com.wayble.server.direction.external.opendata.dto.StationSearchResponse; +import com.wayble.server.direction.repository.RouteRepository; +import com.wayble.server.direction.dto.response.TransportationResponseDto; + +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; + +@Service +@Slf4j +@RequiredArgsConstructor +public class BusInfoService { + + private final HttpClient httpClient; + private final OpenDataProperties openDataProperties; + private final RouteRepository routeRepository; + + public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long busId, Double x, Double y) { + List isLowFloor = new ArrayList<>(); + Integer dispatchInterval = null; + + boolean isShuttleBus = false; + if (busId != null) { + var route = routeRepository.findById(busId); + isShuttleBus = route.isPresent() && route.get().getRouteName().contains("마포"); + } + + try { + // 1. 정류소명으로 정류소 검색 + StationSearchResponse stationSearchResponse = fetchStationByName(stationName); + if (stationSearchResponse == null || stationSearchResponse.msgBody() == null || + stationSearchResponse.msgBody().itemList() == null || + stationSearchResponse.msgBody().itemList().isEmpty()) { + log.warn("정류소를 찾을 수 없습니다: {}", stationName); + return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); + } + + // 2. 여러 정류소가 나올 때, 가장 가까운 정류소 찾기 + StationSearchResponse.StationItem closestStation = findClosestStation( + stationSearchResponse.msgBody().itemList(), x, y); + + if (closestStation == null) { + log.warn("가장 가까운 정류소를 찾을 수 없습니다: {}", stationName); + return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); + } + + // 3. 정류소 ID로 버스 도착 정보 조회 + OpenDataResponse openDataResponse = fetchArrivals(Long.parseLong(closestStation.stId()), busId); + if (openDataResponse == null || openDataResponse.msgBody() == null || + openDataResponse.msgBody().itemList() == null) { + log.warn("버스 도착 정보를 찾을 수 없습니다: {}", closestStation.stId()); + return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); + } + + // 4. 버스 정보 추출 + int count = 0; + for (OpenDataResponse.Item item : openDataResponse.msgBody().itemList()) { + if (count >= 1) break; // busId가 null일 때는 최대 1개 노선만 + + // busType1과 busType2 추가 + isLowFloor.add("1".equals(item.busType1())); + isLowFloor.add("1".equals(item.busType2())); + + // term을 정수로 변환 + try { + dispatchInterval = Integer.parseInt(item.term()); + } catch (NumberFormatException e) { + dispatchInterval = 0; + } + + count++; + } + + } catch (Exception e) { + log.error("버스 정보 조회 중 오류 발생: {}", e.getMessage()); + return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); + } + + return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval); + } + + private OpenDataResponse fetchArrivals(Long stationId, Long busId) { + try { + String serviceKey = openDataProperties.encodedKey(); + + String uri = openDataProperties.baseUrl() + + openDataProperties.endpoints().arrivals() + + "?serviceKey=" + serviceKey + + "&stId=" + 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); + + // busId가 맞는 버스만 필터링 + if (busId != null && originalResponse != null && originalResponse.msgBody() != null && + originalResponse.msgBody().itemList() != null) { + + List filteredItems = originalResponse.msgBody().itemList().stream() + .filter(item -> busId.toString().equals(item.busRouteId())) + .collect(Collectors.toList()); + + return new OpenDataResponse( + originalResponse.comMsgHeader(), + originalResponse.msgHeader(), + new OpenDataResponse.MsgBody(filteredItems) + ); + } + + return originalResponse; + + } catch (Exception e) { + log.error("버스 도착 정보 조회 중 예외 발생: {}", e.getMessage()); + return null; + } + } + + private StationSearchResponse fetchStationByName(String stationName) { + try { + String serviceKey = openDataProperties.encodedKey(); + + String uri = 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); + + } catch (Exception e) { + log.error("정류소 검색 중 예외 발생: {}", e.getMessage()); + return null; + } + } + + private StationSearchResponse.StationItem findClosestStation(List stations, Double x, Double y) { + if (stations == null || stations.isEmpty()) { + return null; + } + + StationSearchResponse.StationItem closestStation = null; + double minDistance = Double.MAX_VALUE; + + 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()); + 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; + } + } catch (NumberFormatException e) { + log.warn("정류소 좌표 파싱 실패 - {}: tmX={}, tmY={}, error={}", + station.stNm(), station.tmX(), station.tmY(), e.getMessage()); + continue; + } + } + + return closestStation; + } +} 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 834f7907..fbd4450c 100644 --- a/src/main/java/com/wayble/server/direction/service/FacilityService.java +++ b/src/main/java/com/wayble/server/direction/service/FacilityService.java @@ -1,6 +1,6 @@ package com.wayble.server.direction.service; -import com.wayble.server.direction.dto.TransportationResponseDto; +import com.wayble.server.direction.dto.response.TransportationResponseDto; import com.wayble.server.direction.entity.transportation.Facility; import com.wayble.server.direction.external.kric.dto.KricToiletRawItem; import com.wayble.server.direction.external.kric.dto.KricToiletRawResponse; 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 fa19da79..e42f8a8c 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -1,23 +1,25 @@ package com.wayble.server.direction.service; import com.wayble.server.common.exception.ApplicationException; -import com.wayble.server.direction.dto.TransportationRequestDto; -import com.wayble.server.direction.dto.TransportationResponseDto; +import com.wayble.server.direction.dto.TransportationGraphDto; +import com.wayble.server.direction.dto.request.TransportationRequestDto; +import com.wayble.server.direction.dto.response.TransportationResponseDto; import com.wayble.server.direction.entity.DirectionType; import com.wayble.server.direction.entity.transportation.Edge; import com.wayble.server.direction.entity.transportation.Node; +import com.wayble.server.direction.entity.transportation.Route; import com.wayble.server.direction.repository.EdgeRepository; import com.wayble.server.direction.repository.NodeRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; import java.util.*; +import java.util.stream.Collectors; import static com.wayble.server.direction.exception.DirectionErrorCase.PATH_NOT_FOUND; import static com.wayble.server.direction.exception.DirectionErrorCase.DISTANCE_TOO_FAR; -import lombok.extern.slf4j.Slf4j; - @Slf4j @Service @RequiredArgsConstructor @@ -25,33 +27,37 @@ public class TransportationService { private final NodeRepository nodeRepository; private final EdgeRepository edgeRepository; private final FacilityService facilityService; - - private List nodes; - private List edges; - - private static final int TRANSFER_PENALTY = 2000; - private static final int STEP_PENALTY = 500; - private static final int METER_CONVERSION = 1000; - private static final double DISTANCE_CONSTRAINT = 30; + 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; // 킬로미터를 미터로 변환하는 상수 + private static final double DISTANCE_CONSTRAINT = 30; // 최대 이동 가능 거리 제한 (km) + + private static final int NEARBY_STATION_WALK_DISTANCE = 2000; // 인근 정류장 간 도보 연결 가능 거리 (미터) + 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; // 다익스트라 알고리즘에서 방문할 수 있는 최대 노드 수 (무한 루프 방지) public TransportationResponseDto findRoutes(TransportationRequestDto request){ - + TransportationRequestDto.Location origin = request.origin(); TransportationRequestDto.Location destination = request.destination(); - - // 거리 검증 (30km 제한) + // 1. 거리 검증 (30km 제한) double distance = haversine(origin.latitude(), origin.longitude(), destination.latitude(), destination.longitude()); if (distance >= DISTANCE_CONSTRAINT) { throw new ApplicationException(DISTANCE_TOO_FAR); } - Node start = new Node(-1L, origin.name(), DirectionType.FROM_WAYPOINT ,origin.latitude(), origin.longitude()); - Node end = new Node(-2L, destination.name(), DirectionType.TO_WAYPOINT,destination.latitude(), destination.longitude()); - - List steps = returnDijkstra(start, end); + // 2. 임시 노드 생성 + 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); + // 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()); @@ -68,79 +74,178 @@ public TransportationResponseDto findRoutes(TransportationRequestDto request){ } - private List returnDijkstra(Node startTmp, Node endTmp){ - - // 실제 노드·엣지 조회 및 컬렉션 복제 - nodes = new ArrayList<>(nodeRepository.findAll()); - edges = new ArrayList<>(edgeRepository.findAll()); - - // 가장 가까운 실제 정류장 찾기 (임시 노드 추가 전에) - Node nearestToStart = nodes.stream() - .min(Comparator.comparingDouble(n -> - haversine(startTmp.getLatitude(), startTmp.getLongitude(), - n.getLatitude(), n.getLongitude()))) - .orElseThrow(() -> new ApplicationException(PATH_NOT_FOUND)); - - // 도착지는 출발지와 다른 정류장을 선택 - Node nearestToEnd = nodes.stream() - .filter(n -> !n.equals(nearestToStart)) - .min(Comparator.comparingDouble(n -> - haversine(endTmp.getLatitude(), endTmp.getLongitude(), - n.getLatitude(), n.getLongitude()))) - .orElse(nearestToStart); // fallback to same station if no other option - - // 임시 노드를 리스트에 추가 + private List findTransportationRoute(Node startTmp, Node endTmp){ + // 1. 데이터 로드 + List nodes = new ArrayList<>(nodeRepository.findAll()); + List edges = new ArrayList<>(edgeRepository.findAllWithNodesAndRoute()); + + // 2. 가장 가까운 정류장 찾기 + Node nearestToStart = findNearestNode(nodes, startTmp.getLatitude(), startTmp.getLongitude()); + Node nearestToEnd = findNearestNode(nodes, endTmp.getLatitude(), endTmp.getLongitude()); + + if (nearestToStart == null || nearestToEnd == null) { + throw new ApplicationException(PATH_NOT_FOUND); + } + + // 3. 임시 노드 추가 nodes.add(startTmp); nodes.add(endTmp); - // 로컬에 가중치 보관용 Map - Map, Integer> weightMap = new HashMap<>(); + // 4. 그래프 빌드 및 최적 경로 찾기 + TransportationGraphDto graphData = buildGraph(nodes, edges, startTmp, endTmp); + return findOptimalRoute(graphData.graph(), startTmp, endTmp, graphData.weightMap(), nodes, nearestToStart, nearestToEnd); + } - // 출발지 -> 가장 가까운 정류장 (도보) - Edge startToStation = Edge.createEdge(-1L, startTmp, nearestToStart, DirectionType.WALK); - edges.add(startToStation); - // 가장 가까운 정류장 -> 도착지 (도보) - Edge stationToEnd = Edge.createEdge(-2L, nearestToEnd, endTmp, DirectionType.WALK); - edges.add(stationToEnd); + private List findOptimalRoute( + Map> graph, + Node startTmp, + Node endTmp, + Map, Integer> weightMap, + List nodes, + Node nearestToStart, + Node nearestToEnd) { + + // 1. 임시 노드 찾기 + Node startNode = nodes.stream() + .filter(node -> node.getId().equals(-1L)) + .findFirst() + .orElse(null); + + Node endNode = nodes.stream() + .filter(node -> node.getId().equals(-2L)) + .findFirst() + .orElse(null); + + if (startNode == null || endNode == null) { + return new ArrayList<>(); + } + + // 2. 다익스트라 알고리즘으로 최적 경로 찾기 + List route = runDijkstra(graph, startNode, endNode, weightMap, nodes); + + if (!route.isEmpty()) { + // 3. 대중교통 포함 여부 확인 + boolean hasPublicTransport = route.stream() + .anyMatch(step -> step.mode() == DirectionType.BUS || step.mode() == DirectionType.SUBWAY); + + if (!hasPublicTransport) { + return new ArrayList<>(); + } + + // 4. 환승 횟수 검증 (4회 이상 제외) + int transferCount = calculateTransferCount(route); + if (transferCount >= 4) { + return new ArrayList<>(); + } + } + + return route; + } - // 모든 엣지의 가중치 계산 + private TransportationGraphDto buildGraph(List nodes, List edges, Node startTmp, Node endTmp) { + Map> graph = new HashMap<>(); + Map, Integer> weightMap = new HashMap<>(); + + // 1. 노드 초기화 + for (Node node : nodes) { + Long nodeId = node.getId(); + if (nodeId != null) { + graph.put(nodeId, new ArrayList<>()); + } + } + + // 2. 기존 엣지 추가 및 가중치 계산 for (Edge edge : edges) { if (edge == null) continue; - Node from = edge.getStartNode(); - Node to = edge.getEndNode(); + Node start = edge.getStartNode(); + Node end = edge.getEndNode(); + if (start == null || end == null || start.getId() == null || end.getId() == null) continue; - if (from == null || to == null || from.getId() == null || to.getId() == null) { - continue; - } + Long startId = start.getId(); + Long endId = end.getId(); + + if (!graph.containsKey(startId)) continue; + graph.get(startId).add(edge); + int weight = (int)(haversine( - from.getLatitude(), from.getLongitude(), - to.getLatitude(), to.getLongitude() + start.getLatitude(), start.getLongitude(), + end.getLatitude(), end.getLongitude() ) * METER_CONVERSION); - - weightMap.put(Pair.of(from.getId(), to.getId()), weight); + weightMap.put(Pair.of(startId, endId), weight); } + + // 3. 출발지/도착지 도보 연결 추가 + addOriginDestinationWalkConnections(graph, weightMap, nodes, startTmp, endTmp); + + return new TransportationGraphDto(graph, weightMap); + } - // 그래프 빌드 및 Dijkstra 호출 - Map> graph = buildGraph(nodes, edges); - List result = runDijkstra(graph, startTmp, endTmp, weightMap); - - return result; + private void addOriginDestinationWalkConnections(Map> graph, Map, Integer> weightMap, List nodes, Node startTmp, Node endTmp) { + // 1. 임시 노드 생성 + Node startNode = Node.createNode(-1L, startTmp.getStationName(), DirectionType.WALK, + startTmp.getLatitude(), startTmp.getLongitude()); + Node endNode = Node.createNode(-2L, endTmp.getStationName(), DirectionType.WALK, + endTmp.getLatitude(), endTmp.getLongitude()); + + graph.put(startNode.getId(), new ArrayList<>()); + graph.put(endNode.getId(), new ArrayList<>()); + + // 2. 출발지에서 인근 정류장으로 도보 연결 + List startCandidates = findNearbyNodes(nodes, startTmp.getLatitude(), startTmp.getLongitude(), ORIGIN_DESTINATION_WALK_DISTANCE); + for (Node candidate : startCandidates) { + Edge walkEdge = Edge.createEdge(-1L, startNode, candidate, DirectionType.WALK); + graph.get(startNode.getId()).add(walkEdge); + + int weight = (int)(haversine( + startNode.getLatitude(), startNode.getLongitude(), + candidate.getLatitude(), candidate.getLongitude() + ) * METER_CONVERSION); + weightMap.put(Pair.of(startNode.getId(), candidate.getId()), weight); + } + + // 3. 인근 정류장에서 도착지로 도보 연결 + List endCandidates = findNearbyNodes(nodes, endTmp.getLatitude(), endTmp.getLongitude(), ORIGIN_DESTINATION_WALK_DISTANCE); + for (Node candidate : endCandidates) { + Edge walkEdge = Edge.createEdge(-2L, candidate, endNode, DirectionType.WALK); + + if (!graph.containsKey(candidate.getId())) { + graph.put(candidate.getId(), new ArrayList<>()); + } + graph.get(candidate.getId()).add(walkEdge); + + int weight = (int)(haversine( + candidate.getLatitude(), candidate.getLongitude(), + endNode.getLatitude(), endNode.getLongitude() + ) * METER_CONVERSION); + weightMap.put(Pair.of(candidate.getId(), endNode.getId()), weight); + } + + nodes.add(startNode); + nodes.add(endNode); } - private List runDijkstra( - Map> graph, Node start, Node end, - Map, Integer> weightMap - ){ + private List findNearbyNodes(List nodes, double lat, double lon, int maxDistanceMeters) { + return nodes.stream() + .filter(node -> { + double distance = haversine(lat, lon, node.getLatitude(), node.getLongitude()) * METER_CONVERSION; + return distance <= maxDistanceMeters; + }) + .sorted(Comparator.comparingDouble(node -> + haversine(lat, lon, node.getLatitude(), node.getLongitude()))) + .limit(MAX_NEARBY_NODES) + .collect(Collectors.toList()); + } + private List runDijkstra(Map> graph, Node start, Node end, Map, Integer> weightMap, List nodes) { + // 1. 초기화 Map distance = new HashMap<>(); Map prevEdge = new HashMap<>(); Map prevNode = new HashMap<>(); Set visited = new HashSet<>(); - // 초기화 for (Node node : nodes) { distance.put(node.getId(), Integer.MAX_VALUE); prevNode.put(node.getId(), null); @@ -148,22 +253,62 @@ private List runDijkstra( } distance.put(start.getId(), 0); - PriorityQueue pq = new PriorityQueue<>(Comparator.comparingInt(n -> distance.getOrDefault(n.getId(), Integer.MAX_VALUE))); + PriorityQueue pq = new PriorityQueue<>(Comparator.comparingInt(n -> distance.get(n.getId()))); pq.add(start); - - while (!pq.isEmpty()) { + + int visitedCount = 0; + + // 2. 다익스트라 알고리즘 실행 + while (!pq.isEmpty() && visitedCount < MAX_DIJKSTRA_VISITS) { Node curr = pq.poll(); - - if (visited.contains(curr.getId())) { - continue; - } + visitedCount++; + + if (visited.contains(curr.getId())) continue; visited.add(curr.getId()); + + if (curr.equals(end)) break; - if (curr.equals(end)) { - break; + List currentEdges = graph.getOrDefault(curr.getId(), List.of()); + + // 3. 동적 도보 연결 생성 (필요시) + boolean hasUnvisitedDirectConnection = false; + for (Edge edge : currentEdges) { + if (edge == null || edge.getEndNode() == null) continue; + Node neighbor = edge.getEndNode(); + if (!visited.contains(neighbor.getId())) { + hasUnvisitedDirectConnection = true; + break; + } } - - for (Edge edge : graph.getOrDefault(curr.getId(), List.of())) { + + if (!hasUnvisitedDirectConnection) { + List nearbyNodes = findNearbyNodes(nodes, curr.getLatitude(), curr.getLongitude(), NEARBY_STATION_WALK_DISTANCE); + for (Node nearbyNode : nearbyNodes) { + if (visited.contains(nearbyNode.getId())) continue; + + double walkDistance = haversine( + curr.getLatitude(), curr.getLongitude(), + nearbyNode.getLatitude(), nearbyNode.getLongitude() + ) * METER_CONVERSION; + + if (walkDistance <= NEARBY_STATION_WALK_DISTANCE) { + Edge walkEdge = Edge.createEdge(-3L, curr, nearbyNode, DirectionType.WALK); + currentEdges.add(walkEdge); + + int weight = (int)walkDistance + STEP_PENALTY; + int alt = distance.get(curr.getId()) + weight; + if (alt < distance.get(nearbyNode.getId())) { + distance.put(nearbyNode.getId(), alt); + prevNode.put(nearbyNode.getId(), curr); + prevEdge.put(nearbyNode.getId(), walkEdge); + pq.add(nearbyNode); + } + } + } + } + + // 4. 기존 엣지 처리 + for (Edge edge : currentEdges) { if (edge == null || edge.getEndNode() == null) continue; Node neighbor = edge.getEndNode(); @@ -182,20 +327,28 @@ private List runDijkstra( ) * METER_CONVERSION) ); - // 간단한 경로 선호를 위한 가중치 조정 int weight = baseWeight; - // 환승 패널티 (교통수단 변경 시 추가 비용) + // 환승 패널티 적용 Edge prevEdgeForCurr = prevEdge.get(curr.getId()); if (prevEdgeForCurr != null && - prevEdgeForCurr.getEdgeType() != edge.getEdgeType() && prevEdgeForCurr.getEdgeType() != DirectionType.WALK && edge.getEdgeType() != DirectionType.WALK) { - weight += TRANSFER_PENALTY; // 환승 패널티 대폭 증가 + + if (prevEdgeForCurr.getEdgeType() != edge.getEdgeType()) { + weight += TRANSFER_PENALTY; + } else { + Route prevRoute = prevEdgeForCurr.getRoute(); + Route currentRoute = edge.getRoute(); + + if (prevRoute != null && currentRoute != null && + !prevRoute.getRouteId().equals(currentRoute.getRouteId())) { + weight += TRANSFER_PENALTY; + } + } } - // 단계 수 패널티 (경로 단계가 많을수록 불이익) - weight += STEP_PENALTY; // 각 단계마다 추가 비용 대폭 증가 + weight += STEP_PENALTY; int alt = distance.get(curr.getId()) + weight; if (alt < distance.get(neighbor.getId())) { @@ -207,35 +360,27 @@ private List runDijkstra( } } - // 역추적해 경로 steps 생성 - List steps = new LinkedList<>(); - Node current = end; - Set backtrackVisited = new HashSet<>(); - + // 5. 경로 역추적 및 steps 생성 if (distance.get(end.getId()) == Integer.MAX_VALUE) { - log.warn("경로를 찾을 수 없음: 도착지에 도달할 수 없음"); - return steps; // 빈 리스트 반환 + return new LinkedList<>(); } - // 먼저 모든 엣지를 수집 List pathEdges = new ArrayList<>(); + Node current = end; + Set backtrackVisited = new HashSet<>(); + while (current != null && !current.equals(start)) { - if (backtrackVisited.contains(current.getId())) { - break; - } + if (backtrackVisited.contains(current.getId())) break; backtrackVisited.add(current.getId()); Edge edge = prevEdge.get(current.getId()); - if (edge == null) { - break; - } + if (edge == null) break; + pathEdges.add(0, edge); current = prevNode.get(current.getId()); } - // 연속된 같은 노선의 구간들을 합치기 return mergeConsecutiveRoutes(pathEdges); - } private List mergeConsecutiveRoutes(List pathEdges) { @@ -249,56 +394,101 @@ private List mergeConsecutiveRoutes(List p while (i < pathEdges.size()) { Edge currentEdge = pathEdges.get(i); DirectionType currentType = currentEdge.getEdgeType(); - String currentRouteName = (currentEdge.getRoute() != null) ? currentEdge.getRoute().getRouteName() : null; - TransportationResponseDto.NodeInfo currentInfo = null; - if (currentType == DirectionType.SUBWAY) { - currentInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId()); + + // 1. 연속된 같은 타입의 엣지들을 그룹화 + int j = i + 1; + // 도보 처리 + if (currentType == DirectionType.WALK) { + while (j < pathEdges.size() && pathEdges.get(j).getEdgeType() == DirectionType.WALK) { + j++; + } + } else { + while (j < pathEdges.size()) { + Edge nextEdge = pathEdges.get(j); + if (nextEdge.getEdgeType() != currentType) break; + + Route currentRoute = currentEdge.getRoute(); + Route nextRoute = nextEdge.getRoute(); + + if ((currentRoute == null && nextRoute == null) || + (currentRoute != null && nextRoute != null && + currentRoute.getRouteId().equals(nextRoute.getRouteId()))) { + j++; + } else { + break; + } + } } - - // 시작 노드 - String fromName = (currentEdge.getStartNode() != null && currentEdge.getStartNode().getStationName() != null) - ? currentEdge.getStartNode().getStationName() : "Unknown"; - String toName = (currentEdge.getEndNode() != null && currentEdge.getEndNode().getStationName() != null) - ? currentEdge.getEndNode().getStationName() : "Unknown"; + // 2. 노드명 및 기본 정보 설정 + String fromName = getNodeName(currentEdge.getStartNode()); + String toName = getNodeName(pathEdges.get(j - 1).getEndNode()); - // 도보인 경우 또는 연속된 같은 노선이 없는 경우 그대로 추가 - if (currentType == DirectionType.WALK || currentRouteName == null) { + if (currentType == DirectionType.WALK) { mergedSteps.add(new TransportationResponseDto.Step( - currentType, - currentRouteName, - currentInfo, - fromName, - toName + DirectionType.WALK, null, null, 0, null, null, fromName, toName )); - i++; + i = j; continue; } - // 연속된 같은 노선 찾기 - int j = i + 1; - while (j < pathEdges.size()) { - Edge nextEdge = pathEdges.get(j); - String nextRouteName = (nextEdge.getRoute() != null) ? nextEdge.getRoute().getRouteName() : null; - - // 같은 노선이 아니면 중단 - if (nextEdge.getEdgeType() != currentType || - !Objects.equals(currentRouteName, nextRouteName)) { - break; - } - j++; - } + // 3. 교통수단 상세 정보 (moveInfo) 설정 + List moveInfoList = createMoveInfoList(pathEdges, i, j); + String routeName = getRouteName(pathEdges, i, j); + // busInfo / subwayInfo 설정 + TransportationResponseDto.BusInfo busInfo = null; + TransportationResponseDto.SubwayInfo subwayInfo = null; - // 마지막 엣지의 도착 노드를 최종 도착지로 설정 - if (j > i + 1) { - Edge lastEdge = pathEdges.get(j - 1); - toName = (lastEdge.getEndNode() != null) ? lastEdge.getEndNode().getStationName() : "Unknown"; + 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.info("버스 정보 조회 실패: {}", e.getMessage()); + } + } else if (currentType == DirectionType.SUBWAY) { + try { + if (currentEdge.getStartNode() != null) { + TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId()); + subwayInfo = new TransportationResponseDto.SubwayInfo( + nodeInfo.wheelchair(), + nodeInfo.elevator(), + nodeInfo.accessibleRestroom() + ); + } + } catch (Exception e) { + log.info("지하철 정보 조회 실패: {}", e.getMessage()); + subwayInfo = new TransportationResponseDto.SubwayInfo( + new ArrayList<>(), + new ArrayList<>(), + false + ); + } } + + int moveNumber = j - i - 1; mergedSteps.add(new TransportationResponseDto.Step( currentType, - currentRouteName, - currentInfo, + moveInfoList, + routeName, + moveNumber, + busInfo, + subwayInfo, fromName, toName )); @@ -308,31 +498,32 @@ private List mergeConsecutiveRoutes(List p return mergedSteps; } - - private Map> buildGraph(List nodes, List edges) { - Map> graph = new HashMap<>(); - for (Node node : nodes) { - Long nodeId = node.getId(); - if (nodeId != null) { - graph.put(nodeId, new ArrayList<>()); - } else { - log.warn("ID가 null인 node 발견: " + node.getStationName()); + + private String getNodeName(Node node) { + return (node != null && node.getStationName() != null) ? node.getStationName() : "Unknown"; + } + + private List createMoveInfoList(List pathEdges, int start, int end) { + List moveInfoList = new ArrayList<>(); + for (int k = start + 1; k < end; k++) { + Edge e = pathEdges.get(k); + if (e.getStartNode() != null && e.getStartNode().getStationName() != null) { + moveInfoList.add(new TransportationResponseDto.MoveInfo(e.getStartNode().getStationName())); } } - for (Edge edge : edges) { - if (edge == null) continue; - - Node start = edge.getStartNode(); - if (start == null || start.getId() == null) continue; - - Long startId = start.getId(); - if (!graph.containsKey(startId)) continue; - - graph.get(startId).add(edge); + return moveInfoList.isEmpty() ? null : moveInfoList; + } + + private String getRouteName(List pathEdges, int start, int end) { + for (int k = start; k < end; k++) { + Edge e = pathEdges.get(k); + if (e.getRoute() != null && e.getRoute().getRouteName() != null) { + return e.getRoute().getRouteName(); + } } - return graph; + return null; } - + public static double haversine( double lat1, double lon1, double lat2, double lon2 @@ -351,4 +542,29 @@ public static double haversine( return R * c; // km 단위 거리 반환 } + private Node findNearestNode(List nodes, double lat, double lon) { + return nodes.stream() + .min(Comparator.comparingDouble(n -> + haversine(lat, lon, n.getLatitude(), n.getLongitude()))) + .orElse(null); + } + + private int calculateTransferCount(List steps) { + int transferCount = 0; + for (int i = 0; i < steps.size() - 1; i++) { + TransportationResponseDto.Step currentStep = steps.get(i); + TransportationResponseDto.Step nextStep = steps.get(i + 1); + + if (currentStep.mode() != DirectionType.WALK && nextStep.mode() != DirectionType.WALK) { + if (currentStep.mode() == nextStep.mode() && + currentStep.routeName() != null && nextStep.routeName() != null && + !currentStep.routeName().equals(nextStep.routeName())) { + transferCount++; + } else if (currentStep.mode() != nextStep.mode()) { + transferCount++; + } + } + } + return transferCount; + } }