From 09754f9e40a0927017b78474be5ee5c4bc1671a9 Mon Sep 17 00:00:00 2001 From: hyoin Date: Mon, 11 Aug 2025 22:30:07 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[feat]=20=EB=B2=84=EC=8A=A4=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4,=20=EC=A4=91=EA=B0=84=20=EC=97=AD=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/TransportationResponseDto.java | 22 +++++- .../service/TransportationService.java | 76 +++++++++++++------ 2 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java b/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java index 44255d4f..d881b00b 100644 --- a/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java +++ b/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java @@ -13,8 +13,11 @@ public record TransportationResponseDto( ) { public record Step( DirectionType mode, // 예: START, WALK, SUBWAY, BUS, FINISH + @Nullable List moveInfo, // 같은 Step으로 이동한 정류장(Node) 정보 (중간 정류장만) @Nullable String routeName, - @Nullable NodeInfo information, + Integer moveNumber, // 같은 Step(route)로 이동한 횟수 + @Nullable BusInfo busInfo, // 버스일 경우에만 생성, 이외의 경우 null + @Nullable SubwayInfo subwayInfo, // 지하철일 경우에만 생성, 이외의 경우 null String from, String to ) {} @@ -24,7 +27,15 @@ public record PageInfo( boolean hasNext ) {} - public record NodeInfo( + public record MoveInfo( + String nodeName // 정류장(Node)의 stationName + ){} + + public record BusInfo( + boolean isLowFloor // routeName에 "마포" 포함시 true, 그 외 버스는 false + ){} + + public record SubwayInfo( List wheelchair, List elevator, Boolean accessibleRestroom @@ -34,4 +45,11 @@ 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/service/TransportationService.java b/src/main/java/com/wayble/server/direction/service/TransportationService.java index fa19da79..5e1bafb6 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -249,25 +249,22 @@ 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()); - } - - // 시작 노드 + // 시작 노드 이름 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"; - // 도보인 경우 또는 연속된 같은 노선이 없는 경우 그대로 추가 - if (currentType == DirectionType.WALK || currentRouteName == null) { + // 도보 처리 + if (currentType == DirectionType.WALK) { mergedSteps.add(new TransportationResponseDto.Step( currentType, - currentRouteName, - currentInfo, + null, // moveInfo + null, // routeName + 1, // moveNumber + null, // busInfo + null, // subwayInfo fromName, toName )); @@ -275,30 +272,61 @@ private List mergeConsecutiveRoutes(List p continue; } - // 연속된 같은 노선 찾기 + // 동일 타입 + 동일 Route 객체 그룹화 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)) { + if (nextEdge.getEdgeType() != currentType) break; + if (!Objects.equals(currentEdge.getRoute(), nextEdge.getRoute())) break; + j++; + } + + // 그룹 마지막 엣지 기준 toName + Edge lastEdgeInGroup = pathEdges.get(j - 1); + if (lastEdgeInGroup.getEndNode() != null && lastEdgeInGroup.getEndNode().getStationName() != null) { + toName = lastEdgeInGroup.getEndNode().getStationName(); + } + + // moveInfo: 중간 정류장만 + List moveInfoList = new ArrayList<>(); + for (int k = i + 1; k < j; k++) { + Edge e = pathEdges.get(k); + if (e.getStartNode() != null && e.getStartNode().getStationName() != null) { + moveInfoList.add(new TransportationResponseDto.MoveInfo(e.getStartNode().getStationName())); + } + } + if (moveInfoList.isEmpty()) moveInfoList = null; + + // routeName + String routeName = null; + for (int k = i; k < j; k++) { + Edge e = pathEdges.get(k); + if (e.getRoute() != null && e.getRoute().getRouteName() != null) { + routeName = e.getRoute().getRouteName(); break; } - j++; } - // 마지막 엣지의 도착 노드를 최종 도착지로 설정 - if (j > i + 1) { - Edge lastEdge = pathEdges.get(j - 1); - toName = (lastEdge.getEndNode() != null) ? lastEdge.getEndNode().getStationName() : "Unknown"; + // busInfo / subwayInfo + TransportationResponseDto.BusInfo busInfo = null; + TransportationResponseDto.SubwayInfo subwayInfo = null; + if (currentType == DirectionType.BUS) { + boolean isLowFloor = routeName != null && routeName.contains("마포"); + busInfo = new TransportationResponseDto.BusInfo(isLowFloor); + } else if (currentType == DirectionType.SUBWAY) { + // 필요 시 시설 정보 연동 가능: facilityService.getNodeInfo(nodeId) -> SubwayInfo 변환 + subwayInfo = null; } + int moveNumber = j - i; + mergedSteps.add(new TransportationResponseDto.Step( currentType, - currentRouteName, - currentInfo, + moveInfoList, + routeName, + moveNumber, + busInfo, + subwayInfo, fromName, toName )); From 21b74d846d86a67cccf53b963b8f0c31101cc4a9 Mon Sep 17 00:00:00 2001 From: hyoin Date: Tue, 12 Aug 2025 01:06:29 +0900 Subject: [PATCH 02/12] =?UTF-8?q?[refactor]=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../direction/dto/{ => request}/TransportationRequestDto.java | 2 +- .../dto/{ => response}/TransportationResponseDto.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) rename src/main/java/com/wayble/server/direction/dto/{ => request}/TransportationRequestDto.java (87%) rename src/main/java/com/wayble/server/direction/dto/{ => response}/TransportationResponseDto.java (85%) 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/TransportationResponseDto.java b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java similarity index 85% rename from src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java rename to src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java index d881b00b..2b617b8e 100644 --- a/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java +++ b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java @@ -32,7 +32,9 @@ public record MoveInfo( ){} public record BusInfo( - boolean isLowFloor // routeName에 "마포" 포함시 true, 그 외 버스는 false + boolean isShuttleBus, // routeName에 "마포" 포함시 true + @Nullable List isLowFloor, // Open API(busType1,busType2) 기반 저상 여부 리스트 + @Nullable List dispatchInterval // Open API(term) 기반 배차간격 리스트 ){} public record SubwayInfo( From e6432207fd192ffb96e2658f43c60ef2425f3fa9 Mon Sep 17 00:00:00 2001 From: hyoin Date: Tue, 12 Aug 2025 01:08:58 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[refactor]=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/direction/controller/TransportationController.java | 4 ++-- .../direction/dto/response/TransportationResponseDto.java | 2 +- .../com/wayble/server/direction/service/FacilityService.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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/response/TransportationResponseDto.java b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java index 2b617b8e..05c4b0e3 100644 --- a/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java +++ b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java @@ -1,4 +1,4 @@ -package com.wayble.server.direction.dto; +package com.wayble.server.direction.dto.response; import com.wayble.server.direction.entity.DirectionType; import org.springframework.lang.Nullable; 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; From 8e3de10758b1d8ad8bbabf5202582236ded0d4c6 Mon Sep 17 00:00:00 2001 From: hyoin Date: Wed, 13 Aug 2025 11:18:54 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[feat]=20=EA=B3=B5=EA=B3=B5=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=ED=8F=AC=ED=84=B8=20api=EB=A5=BC=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=ED=95=B4=20=EB=B2=84=EC=8A=A4=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wayble/server/ServerApplication.java | 3 +- .../common/config/HttpClientConfig.java | 26 +++ .../direction/dto/response/BusInfo.java | 11 + .../response/TransportationResponseDto.java | 2 +- .../external/opendata/OpenDataProperties.java | 16 ++ .../external/opendata/dto/Arrival.java | 7 + .../opendata/dto/OpenDataResponse.java | 39 ++++ .../opendata/dto/StationSearchResponse.java | 21 ++ .../direction/repository/RouteRepository.java | 9 + .../direction/service/BusInfoService.java | 203 ++++++++++++++++++ .../service/TransportationService.java | 62 ++++-- 11 files changed, 381 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/wayble/server/common/config/HttpClientConfig.java create mode 100644 src/main/java/com/wayble/server/direction/dto/response/BusInfo.java create mode 100644 src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java create mode 100644 src/main/java/com/wayble/server/direction/external/opendata/dto/Arrival.java create mode 100644 src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java create mode 100644 src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java create mode 100644 src/main/java/com/wayble/server/direction/repository/RouteRepository.java create mode 100644 src/main/java/com/wayble/server/direction/service/BusInfoService.java 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..d0bfaadb --- /dev/null +++ b/src/main/java/com/wayble/server/common/config/HttpClientConfig.java @@ -0,0 +1,26 @@ +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(); + } +} 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 index 05c4b0e3..aba0fbe8 100644 --- a/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java +++ b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java @@ -34,7 +34,7 @@ public record MoveInfo( public record BusInfo( boolean isShuttleBus, // routeName에 "마포" 포함시 true @Nullable List isLowFloor, // Open API(busType1,busType2) 기반 저상 여부 리스트 - @Nullable List dispatchInterval // Open API(term) 기반 배차간격 리스트 + @Nullable Integer dispatchInterval // Open API(term) 기반 배차간격 ){} public record SubwayInfo( 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/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..dc114747 --- /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 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/TransportationService.java b/src/main/java/com/wayble/server/direction/service/TransportationService.java index 5e1bafb6..497ba7a6 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -1,13 +1,14 @@ 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.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.repository.EdgeRepository; import com.wayble.server.direction.repository.NodeRepository; + import lombok.RequiredArgsConstructor; import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; @@ -25,6 +26,7 @@ public class TransportationService { private final NodeRepository nodeRepository; private final EdgeRepository edgeRepository; private final FacilityService facilityService; + private final BusInfoService busInfoService; private List nodes; private List edges; @@ -195,7 +197,7 @@ private List runDijkstra( } // 단계 수 패널티 (경로 단계가 많을수록 불이익) - weight += STEP_PENALTY; // 각 단계마다 추가 비용 대폭 증가 + weight += STEP_PENALTY; int alt = distance.get(curr.getId()) + weight; if (alt < distance.get(neighbor.getId())) { @@ -213,7 +215,7 @@ private List runDijkstra( Set backtrackVisited = new HashSet<>(); if (distance.get(end.getId()) == Integer.MAX_VALUE) { - log.warn("경로를 찾을 수 없음: 도착지에 도달할 수 없음"); + log.info("경로를 찾을 수 없음: 도착지에 도달할 수 없음"); return steps; // 빈 리스트 반환 } @@ -260,11 +262,11 @@ private List mergeConsecutiveRoutes(List p if (currentType == DirectionType.WALK) { mergedSteps.add(new TransportationResponseDto.Step( currentType, - null, // moveInfo - null, // routeName - 1, // moveNumber - null, // busInfo - null, // subwayInfo + null, + null, + 0, + null, + null, fromName, toName )); @@ -307,18 +309,46 @@ private List mergeConsecutiveRoutes(List p } } - // busInfo / subwayInfo + // busInfo / subwayInfo 설정 TransportationResponseDto.BusInfo busInfo = null; TransportationResponseDto.SubwayInfo subwayInfo = null; if (currentType == DirectionType.BUS) { - boolean isLowFloor = routeName != null && routeName.contains("마포"); - busInfo = new TransportationResponseDto.BusInfo(isLowFloor); - } else if (currentType == DirectionType.SUBWAY) { - // 필요 시 시설 정보 연동 가능: facilityService.getNodeInfo(nodeId) -> SubwayInfo 변환 - subwayInfo = null; + boolean isShuttle = routeName != null && routeName.contains("마포"); // 마을버스 구분 + + Long stationId = currentEdge.getStartNode() != null ? currentEdge.getStartNode().getId() : null; + List lowFloors = null; + List intervals = null; + try { + if (stationId != null) { + TransportationResponseDto.BusInfo busInfoData = busInfoService.getBusInfo(currentEdge.getStartNode().getStationName(), null, currentEdge.getStartNode().getLatitude(), currentEdge.getStartNode().getLongitude()); + busInfo = busInfoData; + } + } catch (Exception e) { + log.error("버스 정보 조회 실패: {}", e.getMessage(), e); + } + } + else if (currentType == DirectionType.SUBWAY) { + Long stationId = currentEdge.getStartNode() != null ? currentEdge.getStartNode().getId() : null; + try { + if (stationId != null) { + TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(stationId); + subwayInfo = new TransportationResponseDto.SubwayInfo( + nodeInfo.wheelchair(), + nodeInfo.elevator(), + nodeInfo.accessibleRestroom() + ); + } + } catch (Exception e) { + log.warn("지하철역 시설 정보 조회 실패. 역 ID {}: {}", stationId, e.getMessage()); + subwayInfo = new TransportationResponseDto.SubwayInfo( + new ArrayList<>(), + new ArrayList<>(), + false + ); + } } - int moveNumber = j - i; + int moveNumber = j - i - 1; mergedSteps.add(new TransportationResponseDto.Step( currentType, From 017eaa8389207cd498802d07145ac1a3b5e162e7 Mon Sep 17 00:00:00 2001 From: hyoin Date: Wed, 13 Aug 2025 12:10:33 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[fix]=20timeout=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wayble/server/common/config/HttpClientConfig.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/wayble/server/common/config/HttpClientConfig.java b/src/main/java/com/wayble/server/common/config/HttpClientConfig.java index d0bfaadb..508a12e7 100644 --- a/src/main/java/com/wayble/server/common/config/HttpClientConfig.java +++ b/src/main/java/com/wayble/server/common/config/HttpClientConfig.java @@ -23,4 +23,9 @@ public HttpClient httpClient() { .connectTimeout(Duration.ofSeconds(connectTimeout)) .build(); } + + @Bean + public Duration httpRequestTimeout() { + return Duration.ofSeconds(requestTimeout); + } } From cc7848ea9adc3e9aaf82ab56609003509b21cbc4 Mon Sep 17 00:00:00 2001 From: hyoin Date: Wed, 13 Aug 2025 12:10:55 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[refactor]=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../direction/dto/response/TransportationResponseDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java index aba0fbe8..f6c8ea61 100644 --- a/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java +++ b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java @@ -45,7 +45,7 @@ public record SubwayInfo( public record LocationInfo( Double latitude, - Double Longitude + Double longitude ) {} // 지하철 시설 정보 묶음 (서비스 내부에서 사용) From 1aec814e8d2f7018ddfeff4532ec2c8bf34e6e5e Mon Sep 17 00:00:00 2001 From: hyoin Date: Wed, 13 Aug 2025 12:11:17 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[refactor]=20null=C2=A0=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wayble/server/direction/service/BusInfoService.java | 2 +- .../wayble/server/direction/service/TransportationService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 dc114747..c47819c5 100644 --- a/src/main/java/com/wayble/server/direction/service/BusInfoService.java +++ b/src/main/java/com/wayble/server/direction/service/BusInfoService.java @@ -86,7 +86,7 @@ public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long bus } catch (Exception e) { log.error("버스 정보 조회 중 오류 발생: {}", e.getMessage()); - return null; + return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); } return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval); 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 497ba7a6..7146e9a1 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -320,7 +320,7 @@ private List mergeConsecutiveRoutes(List p List intervals = null; try { if (stationId != null) { - TransportationResponseDto.BusInfo busInfoData = busInfoService.getBusInfo(currentEdge.getStartNode().getStationName(), null, currentEdge.getStartNode().getLatitude(), currentEdge.getStartNode().getLongitude()); + TransportationResponseDto.BusInfo busInfoData = busInfoService.getBusInfo(currentEdge.getStartNode().getStationName(), currentEdge.getRoute().getRouteId(), currentEdge.getStartNode().getLatitude(), currentEdge.getStartNode().getLongitude()); busInfo = busInfoData; } } catch (Exception e) { From 1a47c857adf524bde8d33766aefe53f42d18d1fa Mon Sep 17 00:00:00 2001 From: hyoin Date: Wed, 13 Aug 2025 20:16:27 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[fix]=20N+1=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../direction/entity/transportation/Edge.java | 6 +++--- .../direction/entity/transportation/Facility.java | 2 +- .../direction/entity/transportation/Node.java | 13 +++++++------ .../server/direction/repository/EdgeRepository.java | 10 ++++++++++ 4 files changed, 21 insertions(+), 10 deletions(-) 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..ba0f1083 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 @@ -32,22 +32,23 @@ public class Node { private Double longitude; // 출발 edge 리스트 (정류장에서 출발) - @OneToMany(mappedBy = "startNode") + @OneToMany(mappedBy = "startNode", fetch = FetchType.LAZY) private List outgoingEdges; // 도착 Edge 리스트 (정류장으로 도착) - @OneToMany(mappedBy = "endNode") + @OneToMany(mappedBy = "endNode", fetch = FetchType.LAZY) private List incomingEdges; // 이 정류장이 기점/종점인 노선 - @OneToMany(mappedBy = "startNode") + @OneToMany(mappedBy = "startNode", fetch = FetchType.LAZY) private List startRoutes; - @OneToMany(mappedBy = "endNode") + @OneToMany(mappedBy = "endNode", fetch = FetchType.LAZY) private List endRoutes; - @OneToOne(mappedBy = "node") - private Facility facility_id; + // facility_id 관계 제거 (N+1 문제 해결) + // @OneToOne(mappedBy = "node", fetch = FetchType.LAZY) + // private Facility facility_id; public Node(Long id, String stationName, DirectionType nodeType, double latitude, double longitude) { this.id = id; 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(); } From 174baca3978b9967682fb5e645ea437eaa896462 Mon Sep 17 00:00:00 2001 From: hyoin Date: Thu, 14 Aug 2025 00:33:16 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[refactor]=20=EB=B0=B0=EC=B9=98=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EC=A6=88=20=EC=A1=B0=EC=A0=88,=20createNode=20?= =?UTF-8?q?=EC=A0=95=EC=A0=81=ED=95=A8=EC=88=98=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../direction/entity/transportation/Node.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 ba0f1083..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 @@ -33,23 +35,23 @@ public class Node { // 출발 edge 리스트 (정류장에서 출발) @OneToMany(mappedBy = "startNode", fetch = FetchType.LAZY) + @BatchSize(size = 100) private List outgoingEdges; // 도착 Edge 리스트 (정류장으로 도착) @OneToMany(mappedBy = "endNode", fetch = FetchType.LAZY) + @BatchSize(size = 100) private List incomingEdges; // 이 정류장이 기점/종점인 노선 @OneToMany(mappedBy = "startNode", fetch = FetchType.LAZY) + @BatchSize(size = 100) private List startRoutes; @OneToMany(mappedBy = "endNode", fetch = FetchType.LAZY) + @BatchSize(size = 100) private List endRoutes; - // facility_id 관계 제거 (N+1 문제 해결) - // @OneToOne(mappedBy = "node", fetch = FetchType.LAZY) - // private Facility facility_id; - public Node(Long id, String stationName, DirectionType nodeType, double latitude, double longitude) { this.id = id; this.stationName = stationName; @@ -58,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; From 963443c91a7e8dd9d6b4cf2c51cdf058083719db Mon Sep 17 00:00:00 2001 From: hyoin Date: Thu, 14 Aug 2025 00:49:30 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[fix]=20step=C2=A0=EC=A4=91=20=EB=8F=84?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80,=20=ED=99=98=EC=8A=B9=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=ED=8B=B0=20=EB=B6=80=EC=97=AC,=20=ED=99=98?= =?UTF-8?q?=EC=8A=B9=20=ED=9A=9F=EC=88=98=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TransportationService.java | 570 ++++++++++++------ 1 file changed, 383 insertions(+), 187 deletions(-) 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 7146e9a1..dd39b0cb 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -1,24 +1,25 @@ package com.wayble.server.direction.service; import com.wayble.server.common.exception.ApplicationException; +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 @@ -28,32 +29,35 @@ public class TransportationService { private final FacilityService facilityService; private final BusInfoService busInfoService; - 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 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()); @@ -70,79 +74,217 @@ 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); + private void addNearbyWalkConnections(Map> graph, Map, Integer> weightMap, List nodes) { + // 1. 각 노드별로 인근 정류장 찾기 (800m 이내, 최대 3개) + for (Node node1 : nodes) { + int connectionCount = 0; + List nearbyNodes = new ArrayList<>(); + + for (Node node2 : nodes) { + if (node1.getId().equals(node2.getId())) continue; + if (connectionCount >= 3) break; + + double distance = haversine( + node1.getLatitude(), node1.getLongitude(), + node2.getLatitude(), node2.getLongitude() + ) * METER_CONVERSION; + + if (distance <= NEARBY_STATION_WALK_DISTANCE) { + nearbyNodes.add(node2); + connectionCount++; + } + } + + // 2. 거리순 정렬 후 도보 엣지 생성 + nearbyNodes.sort(Comparator.comparingDouble(node -> + haversine(node1.getLatitude(), node1.getLongitude(), + node.getLatitude(), node.getLongitude()))); + + for (Node node2 : nearbyNodes) { + double distance = haversine( + node1.getLatitude(), node1.getLongitude(), + node2.getLatitude(), node2.getLongitude() + ) * METER_CONVERSION; + + Edge walkEdge = Edge.createEdge(-3L, node1, node2, DirectionType.WALK); + graph.get(node1.getId()).add(walkEdge); + weightMap.put(Pair.of(node1.getId(), node2.getId()), (int)distance); + } + } + } - 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); @@ -150,22 +292,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(); @@ -184,19 +366,27 @@ 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; int alt = distance.get(curr.getId()) + weight; @@ -209,35 +399,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.info("경로를 찾을 수 없음: 도착지에 도달할 수 없음"); - 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) { @@ -252,94 +434,83 @@ private List mergeConsecutiveRoutes(List p Edge currentEdge = pathEdges.get(i); DirectionType currentType = currentEdge.getEdgeType(); - // 시작 노드 이름 - 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"; - + // 1. 연속된 같은 타입의 엣지들을 그룹화 + int j = i + 1; // 도보 처리 if (currentType == DirectionType.WALK) { - mergedSteps.add(new TransportationResponseDto.Step( - currentType, - null, - null, - 0, - null, - null, - fromName, - toName - )); - i++; - continue; - } - - // 동일 타입 + 동일 Route 객체 그룹화 - int j = i + 1; - while (j < pathEdges.size()) { - Edge nextEdge = pathEdges.get(j); - if (nextEdge.getEdgeType() != currentType) break; - if (!Objects.equals(currentEdge.getRoute(), nextEdge.getRoute())) break; - j++; - } - - // 그룹 마지막 엣지 기준 toName - Edge lastEdgeInGroup = pathEdges.get(j - 1); - if (lastEdgeInGroup.getEndNode() != null && lastEdgeInGroup.getEndNode().getStationName() != null) { - toName = lastEdgeInGroup.getEndNode().getStationName(); - } - - // moveInfo: 중간 정류장만 - List moveInfoList = new ArrayList<>(); - for (int k = i + 1; k < j; k++) { - Edge e = pathEdges.get(k); - if (e.getStartNode() != null && e.getStartNode().getStationName() != null) { - moveInfoList.add(new TransportationResponseDto.MoveInfo(e.getStartNode().getStationName())); + 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; + } } } - if (moveInfoList.isEmpty()) moveInfoList = null; - // routeName - String routeName = null; - for (int k = i; k < j; k++) { - Edge e = pathEdges.get(k); - if (e.getRoute() != null && e.getRoute().getRouteName() != null) { - routeName = e.getRoute().getRouteName(); - break; - } + // 2. 노드명 및 기본 정보 설정 + String fromName = getNodeName(currentEdge.getStartNode()); + String toName = getNodeName(pathEdges.get(j - 1).getEndNode()); + + if (currentType == DirectionType.WALK) { + mergedSteps.add(new TransportationResponseDto.Step( + DirectionType.WALK, null, null, 0, null, null, fromName, toName + )); + i = j; + continue; } + // 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 (currentType == DirectionType.BUS) { - boolean isShuttle = routeName != null && routeName.contains("마포"); // 마을버스 구분 - - Long stationId = currentEdge.getStartNode() != null ? currentEdge.getStartNode().getId() : null; - List lowFloors = null; - List intervals = null; try { - if (stationId != null) { - TransportationResponseDto.BusInfo busInfoData = busInfoService.getBusInfo(currentEdge.getStartNode().getStationName(), currentEdge.getRoute().getRouteId(), currentEdge.getStartNode().getLatitude(), currentEdge.getStartNode().getLongitude()); - busInfo = busInfoData; + 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(), e); + log.info("버스 정보 조회 실패: {}", e.getMessage()); } - } - else if (currentType == DirectionType.SUBWAY) { - Long stationId = currentEdge.getStartNode() != null ? currentEdge.getStartNode().getId() : null; + } else if (currentType == DirectionType.SUBWAY) { try { - if (stationId != null) { - TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(stationId); + if (currentEdge.getStartNode() != null) { + TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId()); subwayInfo = new TransportationResponseDto.SubwayInfo( - nodeInfo.wheelchair(), - nodeInfo.elevator(), + nodeInfo.wheelchair(), + nodeInfo.elevator(), nodeInfo.accessibleRestroom() ); } } catch (Exception e) { - log.warn("지하철역 시설 정보 조회 실패. 역 ID {}: {}", stationId, e.getMessage()); + log.info("지하철 정보 조회 실패: {}", e.getMessage()); subwayInfo = new TransportationResponseDto.SubwayInfo( new ArrayList<>(), new ArrayList<>(), @@ -347,7 +518,7 @@ else if (currentType == DirectionType.SUBWAY) { ); } } - + int moveNumber = j - i - 1; mergedSteps.add(new TransportationResponseDto.Step( @@ -366,31 +537,32 @@ else if (currentType == DirectionType.SUBWAY) { 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 @@ -409,4 +581,28 @@ 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().equals(nextStep.routeName())) { + transferCount++; + } else if (currentStep.mode() != nextStep.mode()) { + transferCount++; + } + } + } + return transferCount; + } } From 650b444ad84457c4b568b2288bbce8130b2e42a4 Mon Sep 17 00:00:00 2001 From: hyoin Date: Thu, 14 Aug 2025 00:50:07 +0900 Subject: [PATCH 11/12] =?UTF-8?q?[refactor]=20dto=EB=A5=BC=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/direction/dto/TransportationGraphDto.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/com/wayble/server/direction/dto/TransportationGraphDto.java 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 // 엣지별 가중치 +) {} From 89dfca83e61a568dcdf509f530bd3baf1960efd4 Mon Sep 17 00:00:00 2001 From: hyoin Date: Thu, 14 Aug 2025 01:22:02 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[refactor]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=95=A8=EC=88=98=20=EC=82=AD=EC=A0=9C,=20null=20?= =?UTF-8?q?=ED=8C=8C=EC=95=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TransportationService.java | 40 +------------------ 1 file changed, 1 insertion(+), 39 deletions(-) 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 dd39b0cb..e42f8a8c 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -183,45 +183,6 @@ private TransportationGraphDto buildGraph(List nodes, List edges, No return new TransportationGraphDto(graph, weightMap); } - private void addNearbyWalkConnections(Map> graph, Map, Integer> weightMap, List nodes) { - // 1. 각 노드별로 인근 정류장 찾기 (800m 이내, 최대 3개) - for (Node node1 : nodes) { - int connectionCount = 0; - List nearbyNodes = new ArrayList<>(); - - for (Node node2 : nodes) { - if (node1.getId().equals(node2.getId())) continue; - if (connectionCount >= 3) break; - - double distance = haversine( - node1.getLatitude(), node1.getLongitude(), - node2.getLatitude(), node2.getLongitude() - ) * METER_CONVERSION; - - if (distance <= NEARBY_STATION_WALK_DISTANCE) { - nearbyNodes.add(node2); - connectionCount++; - } - } - - // 2. 거리순 정렬 후 도보 엣지 생성 - nearbyNodes.sort(Comparator.comparingDouble(node -> - haversine(node1.getLatitude(), node1.getLongitude(), - node.getLatitude(), node.getLongitude()))); - - for (Node node2 : nearbyNodes) { - double distance = haversine( - node1.getLatitude(), node1.getLongitude(), - node2.getLatitude(), node2.getLongitude() - ) * METER_CONVERSION; - - Edge walkEdge = Edge.createEdge(-3L, node1, node2, DirectionType.WALK); - graph.get(node1.getId()).add(walkEdge); - weightMap.put(Pair.of(node1.getId(), node2.getId()), (int)distance); - } - } - } - private void addOriginDestinationWalkConnections(Map> graph, Map, Integer> weightMap, List nodes, Node startTmp, Node endTmp) { // 1. 임시 노드 생성 Node startNode = Node.createNode(-1L, startTmp.getStationName(), DirectionType.WALK, @@ -596,6 +557,7 @@ private int calculateTransferCount(List steps) { 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()) {