diff --git a/src/main/java/com/wayble/server/ServerApplication.java b/src/main/java/com/wayble/server/ServerApplication.java index 5a3f21ac..ec170f39 100644 --- a/src/main/java/com/wayble/server/ServerApplication.java +++ b/src/main/java/com/wayble/server/ServerApplication.java @@ -2,6 +2,7 @@ import com.wayble.server.common.client.tmap.TMapProperties; import com.wayble.server.direction.external.kric.KricProperties; +import com.wayble.server.direction.external.opendata.OpenDataProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration; @@ -16,7 +17,7 @@ ) @EnableJpaAuditing @EnableScheduling -@EnableConfigurationProperties({TMapProperties.class, KricProperties.class}) +@EnableConfigurationProperties({TMapProperties.class, KricProperties.class, OpenDataProperties.class}) @EnableElasticsearchRepositories(basePackages = {"com.wayble.server.explore.repository", "com.wayble.server.logging.repository", "com.wayble.server.direction.repository"}) @EntityScan(basePackages = "com.wayble.server") public class ServerApplication { diff --git a/src/main/java/com/wayble/server/common/config/HttpClientConfig.java b/src/main/java/com/wayble/server/common/config/HttpClientConfig.java new file mode 100644 index 00000000..508a12e7 --- /dev/null +++ b/src/main/java/com/wayble/server/common/config/HttpClientConfig.java @@ -0,0 +1,31 @@ +package com.wayble.server.common.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import java.net.http.HttpClient; +import java.time.Duration; + +@Configuration +@RequiredArgsConstructor +public class HttpClientConfig { + + @Value("${http.client.connect-timeout:10}") + private int connectTimeout; + + @Value("${http.client.request-timeout:30}") + private int requestTimeout; + + @Bean + public HttpClient httpClient() { + return HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(connectTimeout)) + .build(); + } + + @Bean + public Duration httpRequestTimeout() { + return Duration.ofSeconds(requestTimeout); + } +} diff --git a/src/main/java/com/wayble/server/direction/controller/TransportationController.java b/src/main/java/com/wayble/server/direction/controller/TransportationController.java index d7a3f58c..a4eb9a38 100644 --- a/src/main/java/com/wayble/server/direction/controller/TransportationController.java +++ b/src/main/java/com/wayble/server/direction/controller/TransportationController.java @@ -1,8 +1,8 @@ package com.wayble.server.direction.controller; import com.wayble.server.common.response.CommonResponse; -import com.wayble.server.direction.dto.TransportationRequestDto; -import com.wayble.server.direction.dto.TransportationResponseDto; +import com.wayble.server.direction.dto.request.TransportationRequestDto; +import com.wayble.server.direction.dto.response.TransportationResponseDto; import com.wayble.server.direction.service.TransportationService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java b/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java deleted file mode 100644 index 44255d4f..00000000 --- a/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.wayble.server.direction.dto; - -import com.wayble.server.direction.entity.DirectionType; -import org.springframework.lang.Nullable; -import io.swagger.v3.oas.annotations.media.Schema; - -import java.util.List; - -@Schema(description = "대중교통 길찾기 응답 DTO") -public record TransportationResponseDto( - List routes, - PageInfo pageInfo -) { - public record Step( - DirectionType mode, // 예: START, WALK, SUBWAY, BUS, FINISH - @Nullable String routeName, - @Nullable NodeInfo information, - String from, - String to - ) {} - - public record PageInfo( - Integer nextCursor, - boolean hasNext - ) {} - - public record NodeInfo( - List wheelchair, - List elevator, - Boolean accessibleRestroom - ) {} - - public record LocationInfo( - Double latitude, - Double Longitude - ) {} -} diff --git a/src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java b/src/main/java/com/wayble/server/direction/dto/request/TransportationRequestDto.java similarity index 87% rename from src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java rename to src/main/java/com/wayble/server/direction/dto/request/TransportationRequestDto.java index 9d2a0230..f3b308c2 100644 --- a/src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java +++ b/src/main/java/com/wayble/server/direction/dto/request/TransportationRequestDto.java @@ -1,4 +1,4 @@ -package com.wayble.server.direction.dto; +package com.wayble.server.direction.dto.request; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/wayble/server/direction/dto/response/BusInfo.java b/src/main/java/com/wayble/server/direction/dto/response/BusInfo.java new file mode 100644 index 00000000..71910c57 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/dto/response/BusInfo.java @@ -0,0 +1,11 @@ +package com.wayble.server.direction.dto.response; + +import java.util.List; + +public record BusInfo(List buses, String stationName) { + public record BusArrival( + String busNumber, + String arrival1, + String arrival2 + ) {} +} diff --git a/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java new file mode 100644 index 00000000..f6c8ea61 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java @@ -0,0 +1,57 @@ +package com.wayble.server.direction.dto.response; + +import com.wayble.server.direction.entity.DirectionType; +import org.springframework.lang.Nullable; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "대중교통 길찾기 응답 DTO") +public record TransportationResponseDto( + List routes, + PageInfo pageInfo +) { + public record Step( + DirectionType mode, // 예: START, WALK, SUBWAY, BUS, FINISH + @Nullable List moveInfo, // 같은 Step으로 이동한 정류장(Node) 정보 (중간 정류장만) + @Nullable String routeName, + Integer moveNumber, // 같은 Step(route)로 이동한 횟수 + @Nullable BusInfo busInfo, // 버스일 경우에만 생성, 이외의 경우 null + @Nullable SubwayInfo subwayInfo, // 지하철일 경우에만 생성, 이외의 경우 null + String from, + String to + ) {} + + public record PageInfo( + Integer nextCursor, + boolean hasNext + ) {} + + public record MoveInfo( + String nodeName // 정류장(Node)의 stationName + ){} + + public record BusInfo( + boolean isShuttleBus, // routeName에 "마포" 포함시 true + @Nullable List isLowFloor, // Open API(busType1,busType2) 기반 저상 여부 리스트 + @Nullable Integer dispatchInterval // Open API(term) 기반 배차간격 + ){} + + public record SubwayInfo( + List wheelchair, + List elevator, + Boolean accessibleRestroom + ) {} + + public record LocationInfo( + Double latitude, + Double longitude + ) {} + + // 지하철 시설 정보 묶음 (서비스 내부에서 사용) + public record NodeInfo( + List wheelchair, + List elevator, + Boolean accessibleRestroom + ) {} +} diff --git a/src/main/java/com/wayble/server/direction/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..c47819c5 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/service/BusInfoService.java @@ -0,0 +1,203 @@ +package com.wayble.server.direction.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wayble.server.direction.external.opendata.OpenDataProperties; +import com.wayble.server.direction.external.opendata.dto.OpenDataResponse; +import com.wayble.server.direction.external.opendata.dto.StationSearchResponse; +import com.wayble.server.direction.repository.RouteRepository; +import com.wayble.server.direction.dto.response.TransportationResponseDto; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.URI; +import java.time.Duration; + +@Service +@Slf4j +@RequiredArgsConstructor +public class BusInfoService { + + private final HttpClient httpClient; + private final OpenDataProperties openDataProperties; + private final RouteRepository routeRepository; + + public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long busId, Double x, Double y) { + List isLowFloor = new ArrayList<>(); + Integer dispatchInterval = null; + + boolean isShuttleBus = false; + if (busId != null) { + var route = routeRepository.findById(busId); + isShuttleBus = route.isPresent() && route.get().getRouteName().contains("마포"); + } + + try { + // 1. 정류소명으로 정류소 검색 + StationSearchResponse stationSearchResponse = fetchStationByName(stationName); + if (stationSearchResponse == null || stationSearchResponse.msgBody() == null || + stationSearchResponse.msgBody().itemList() == null || + stationSearchResponse.msgBody().itemList().isEmpty()) { + log.warn("정류소를 찾을 수 없습니다: {}", stationName); + return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); + } + + // 2. 여러 정류소가 나올 때, 가장 가까운 정류소 찾기 + StationSearchResponse.StationItem closestStation = findClosestStation( + stationSearchResponse.msgBody().itemList(), x, y); + + if (closestStation == null) { + log.warn("가장 가까운 정류소를 찾을 수 없습니다: {}", stationName); + return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); + } + + // 3. 정류소 ID로 버스 도착 정보 조회 + OpenDataResponse openDataResponse = fetchArrivals(Long.parseLong(closestStation.stId()), busId); + if (openDataResponse == null || openDataResponse.msgBody() == null || + openDataResponse.msgBody().itemList() == null) { + log.warn("버스 도착 정보를 찾을 수 없습니다: {}", closestStation.stId()); + return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); + } + + // 4. 버스 정보 추출 + int count = 0; + for (OpenDataResponse.Item item : openDataResponse.msgBody().itemList()) { + if (count >= 1) break; // busId가 null일 때는 최대 1개 노선만 + + // busType1과 busType2 추가 + isLowFloor.add("1".equals(item.busType1())); + isLowFloor.add("1".equals(item.busType2())); + + // term을 정수로 변환 + try { + dispatchInterval = Integer.parseInt(item.term()); + } catch (NumberFormatException e) { + dispatchInterval = 0; + } + + count++; + } + + } catch (Exception e) { + log.error("버스 정보 조회 중 오류 발생: {}", e.getMessage()); + return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); + } + + return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval); + } + + private OpenDataResponse fetchArrivals(Long stationId, Long busId) { + try { + String serviceKey = openDataProperties.encodedKey(); + + String uri = openDataProperties.baseUrl() + + openDataProperties.endpoints().arrivals() + + "?serviceKey=" + serviceKey + + "&stId=" + stationId + + "&resultType=json"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Accept", openDataProperties.accept()) + .GET() + .timeout(Duration.ofSeconds(openDataProperties.timeout())) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + OpenDataResponse originalResponse = new ObjectMapper().readValue(response.body(), OpenDataResponse.class); + + // busId가 맞는 버스만 필터링 + if (busId != null && originalResponse != null && originalResponse.msgBody() != null && + originalResponse.msgBody().itemList() != null) { + + List filteredItems = originalResponse.msgBody().itemList().stream() + .filter(item -> busId.toString().equals(item.busRouteId())) + .collect(Collectors.toList()); + + return new OpenDataResponse( + originalResponse.comMsgHeader(), + originalResponse.msgHeader(), + new OpenDataResponse.MsgBody(filteredItems) + ); + } + + return originalResponse; + + } catch (Exception e) { + log.error("버스 도착 정보 조회 중 예외 발생: {}", e.getMessage()); + return null; + } + } + + private StationSearchResponse fetchStationByName(String stationName) { + try { + String serviceKey = openDataProperties.encodedKey(); + + String uri = openDataProperties.baseUrl() + + openDataProperties.endpoints().stationByName() + + "?serviceKey=" + serviceKey + + "&stSrch=" + stationName + + "&resultType=json"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Accept", openDataProperties.accept()) + .GET() + .timeout(Duration.ofSeconds(openDataProperties.timeout())) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return new ObjectMapper().readValue(response.body(), StationSearchResponse.class); + + } catch (Exception e) { + log.error("정류소 검색 중 예외 발생: {}", e.getMessage()); + return null; + } + } + + private StationSearchResponse.StationItem findClosestStation(List stations, Double x, Double y) { + if (stations == null || stations.isEmpty()) { + return null; + } + + StationSearchResponse.StationItem closestStation = null; + double minDistance = Double.MAX_VALUE; + + for (StationSearchResponse.StationItem station : stations) { + try { + // tmX, tmY가 숫자인지 확인하고 파싱 + String tmXStr = station.tmX(); + String tmYStr = station.tmY(); + + if (tmXStr == null || tmYStr == null || tmXStr.trim().isEmpty() || tmYStr.trim().isEmpty()) { + log.warn("정류소 좌표가 null이거나 비어있음: {}", station.stNm()); + continue; + } + + double stationX = Double.parseDouble(tmXStr); + double stationY = Double.parseDouble(tmYStr); + + double distance = Math.sqrt(Math.pow(stationX - x, 2) + Math.pow(stationY - y, 2)); + + if (distance < minDistance) { + minDistance = distance; + closestStation = station; + } + } catch (NumberFormatException e) { + log.warn("정류소 좌표 파싱 실패 - {}: tmX={}, tmY={}, error={}", + station.stNm(), station.tmX(), station.tmY(), e.getMessage()); + continue; + } + } + + return closestStation; + } +} diff --git a/src/main/java/com/wayble/server/direction/service/FacilityService.java b/src/main/java/com/wayble/server/direction/service/FacilityService.java index 834f7907..fbd4450c 100644 --- a/src/main/java/com/wayble/server/direction/service/FacilityService.java +++ b/src/main/java/com/wayble/server/direction/service/FacilityService.java @@ -1,6 +1,6 @@ package com.wayble.server.direction.service; -import com.wayble.server.direction.dto.TransportationResponseDto; +import com.wayble.server.direction.dto.response.TransportationResponseDto; import com.wayble.server.direction.entity.transportation.Facility; import com.wayble.server.direction.external.kric.dto.KricToiletRawItem; import com.wayble.server.direction.external.kric.dto.KricToiletRawResponse; diff --git a/src/main/java/com/wayble/server/direction/service/TransportationService.java b/src/main/java/com/wayble/server/direction/service/TransportationService.java index fa19da79..7146e9a1 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; // 빈 리스트 반환 } @@ -249,25 +251,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, + null, + 0, + null, + null, fromName, toName )); @@ -275,30 +274,89 @@ 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 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; + } + } 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 - 1; + mergedSteps.add(new TransportationResponseDto.Step( currentType, - currentRouteName, - currentInfo, + moveInfoList, + routeName, + moveNumber, + busInfo, + subwayInfo, fromName, toName ));