From 09754f9e40a0927017b78474be5ee5c4bc1671a9 Mon Sep 17 00:00:00 2001 From: hyoin Date: Mon, 11 Aug 2025 22:30:07 +0900 Subject: [PATCH 1/7] =?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 2/7] =?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 3/7] =?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 4/7] =?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 5/7] =?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 6/7] =?UTF-8?q?[refactor]=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=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 7/7] =?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) {