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 543628f..faf1434 100644 --- a/src/main/java/com/wayble/server/direction/repository/EdgeRepository.java +++ b/src/main/java/com/wayble/server/direction/repository/EdgeRepository.java @@ -3,6 +3,7 @@ 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.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -14,4 +15,17 @@ public interface EdgeRepository extends JpaRepository { "JOIN FETCH e.endNode " + "LEFT JOIN FETCH e.route") List findAllWithNodesAndRoute(); + + @Query("SELECT DISTINCT e FROM Edge e " + + "JOIN FETCH e.startNode s " + + "JOIN FETCH e.endNode en " + + "LEFT JOIN FETCH e.route " + + "WHERE (s.latitude BETWEEN :minLat AND :maxLat AND s.longitude BETWEEN :minLon AND :maxLon) OR " + + "(en.latitude BETWEEN :minLat AND :maxLat AND en.longitude BETWEEN :minLon AND :maxLon)") + List findEdgesInBoundingBox( + @Param("minLat") double minLat, + @Param("maxLat") double maxLat, + @Param("minLon") double minLon, + @Param("maxLon") double maxLon + ); } diff --git a/src/main/java/com/wayble/server/direction/repository/NodeRepository.java b/src/main/java/com/wayble/server/direction/repository/NodeRepository.java index 7ec4e4d..9fe3cb6 100644 --- a/src/main/java/com/wayble/server/direction/repository/NodeRepository.java +++ b/src/main/java/com/wayble/server/direction/repository/NodeRepository.java @@ -2,6 +2,20 @@ import com.wayble.server.direction.entity.transportation.Node; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface NodeRepository extends JpaRepository { + + @Query("SELECT n FROM Node n WHERE " + + "n.latitude BETWEEN :minLat AND :maxLat AND " + + "n.longitude BETWEEN :minLon AND :maxLon") + List findNodesInBoundingBox( + @Param("minLat") double minLat, + @Param("maxLat") double maxLat, + @Param("minLon") double minLon, + @Param("maxLon") double maxLon + ); } 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 c47819c..669abd4 100644 --- a/src/main/java/com/wayble/server/direction/service/BusInfoService.java +++ b/src/main/java/com/wayble/server/direction/service/BusInfoService.java @@ -29,6 +29,11 @@ public class BusInfoService { private final RouteRepository routeRepository; public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long busId, Double x, Double y) { + // 나중에 서비스키 문제 해결되면 이 함수 호출 제거 + return createDummyBusInfo(stationName, busId, x, y); + + // 실제 API 호출 코드 (현재 주석 처리) + /* List isLowFloor = new ArrayList<>(); Integer dispatchInterval = null; @@ -79,6 +84,7 @@ public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long bus dispatchInterval = Integer.parseInt(item.term()); } catch (NumberFormatException e) { dispatchInterval = 0; + log.warn("⚠️ 배차간격 파싱 실패: {}", item.term()); } count++; @@ -89,6 +95,30 @@ public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long bus return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null); } + return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval); + */ + + } + + // 나중에 이 함수 제거 + private TransportationResponseDto.BusInfo createDummyBusInfo(String stationName, Long busId, Double x, Double y) { + log.info("🎭 더미 BusInfo 생성 - stationName: {}, busId: {}, x: {}, y: {}", stationName, busId, x, y); + + // 셔틀버스 여부 확인 (기존 로직 유지) + boolean isShuttleBus = false; + if (busId != null) { + var route = routeRepository.findById(busId); + isShuttleBus = route.isPresent() && route.get().getRouteName().contains("마포"); + } + + // 랜덤 더미 데이터 생성 + List isLowFloor = new ArrayList<>(); + isLowFloor.add(Math.random() < 0.7); + isLowFloor.add(Math.random() < 0.5); + + Integer dispatchInterval = (int) (Math.random() * 15) + 1; + + return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval); } @@ -165,6 +195,7 @@ private StationSearchResponse fetchStationByName(String stationName) { private StationSearchResponse.StationItem findClosestStation(List stations, Double x, Double y) { if (stations == null || stations.isEmpty()) { + log.warn("❌ 정류소 목록이 비어있음"); return null; } 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 ecb9a88..70e4575 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -39,6 +39,9 @@ public class TransportationService { private static final int MAX_NEARBY_NODES = 5; // 출발지/도착지 주변에서 고려할 최대 정류장 수 private static final int MAX_DIJKSTRA_VISITS = 5000; // 다익스트라 알고리즘에서 방문할 수 있는 최대 노드 수 (무한 루프 방지) private static final int MAX_ROUTES = 5; // 찾을 최대 경로 수 + + // 공간 필터링 + private static final double SPATIAL_BUFFER_KM = 15.0; // 지작점/도착점 주변 15km public TransportationResponseDto findRoutes(TransportationRequestDto request){ @@ -88,25 +91,54 @@ private TransportationResponseDto.Route createRoute(List> findMultipleTransportationRoutes(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) { + try { + // 1. 공간 필터링을 사용한 데이터 로드 + double[] boundingBox = calculateBoundingBox(startTmp, endTmp); + List nodes = nodeRepository.findNodesInBoundingBox( + boundingBox[0], boundingBox[1], boundingBox[2], boundingBox[3] + ); + List edges = edgeRepository.findEdgesInBoundingBox( + boundingBox[0], boundingBox[1], boundingBox[2], boundingBox[3] + ); + + log.debug("Spatial filtering loaded {} nodes and {} edges", nodes.size(), edges.size()); + + // 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); + + // 4. 그래프 빌드 및 여러 경로 찾기 + TransportationGraphDto graphData = buildGraph(nodes, edges, startTmp, endTmp); + List> result = findMultipleOptimalRoutes( + graphData.graph(), startTmp, endTmp, graphData.weightMap(), nodes, nearestToStart, nearestToEnd + ); + + // 5. 메모리 정리 (명시적으로 null 설정) + nodes.clear(); + edges.clear(); + + return result; + } catch (OutOfMemoryError e) { + log.error("Out of memory error in transportation route finding: {}", e.getMessage()); throw new ApplicationException(PATH_NOT_FOUND); } + } + + private double[] calculateBoundingBox(Node start, Node end) { + double minLat = Math.min(start.getLatitude(), end.getLatitude()) - SPATIAL_BUFFER_KM / 111.0; + double maxLat = Math.max(start.getLatitude(), end.getLatitude()) + SPATIAL_BUFFER_KM / 111.0; + double minLon = Math.min(start.getLongitude(), end.getLongitude()) - SPATIAL_BUFFER_KM / (111.0 * Math.cos(Math.toRadians(start.getLatitude()))); + double maxLon = Math.max(start.getLongitude(), end.getLongitude()) + SPATIAL_BUFFER_KM / (111.0 * Math.cos(Math.toRadians(start.getLatitude()))); - // 3. 임시 노드 추가 - nodes.add(startTmp); - nodes.add(endTmp); - - // 4. 그래프 빌드 및 여러 경로 찾기 - TransportationGraphDto graphData = buildGraph(nodes, edges, startTmp, endTmp); - return findMultipleOptimalRoutes(graphData.graph(), startTmp, endTmp, graphData.weightMap(), nodes, nearestToStart, nearestToEnd); + return new double[]{minLat, maxLat, minLon, maxLon}; } private List> findMultipleOptimalRoutes( @@ -312,15 +344,18 @@ private TransportationGraphDto buildGraph(List nodes, List edges, No 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<>()); - } + // 1. 노드 ID를 Set으로 변환해 빠른 검색 + Set nodeIds = nodes.stream() + .map(Node::getId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + // 2. 노드 초기화 + for (Long nodeId : nodeIds) { + graph.put(nodeId, new ArrayList<>()); } - // 2. 기존 엣지 추가 및 가중치 계산 + // 3. 기존 엣지 추가 및 가중치 계산 (필터링된 노드만) for (Edge edge : edges) { if (edge == null) continue; @@ -331,7 +366,8 @@ private TransportationGraphDto buildGraph(List nodes, List edges, No Long startId = start.getId(); Long endId = end.getId(); - if (!graph.containsKey(startId)) continue; + // 공간 필터링된 노드들만 처리 + if (!nodeIds.contains(startId) || !nodeIds.contains(endId)) continue; graph.get(startId).add(edge); @@ -342,7 +378,7 @@ private TransportationGraphDto buildGraph(List nodes, List edges, No weightMap.put(Pair.of(startId, endId), weight); } - // 3. 출발지/도착지 도보 연결 추가 + // 4. 출발지/도착지 도보 연결 추가 addOriginDestinationWalkConnections(graph, weightMap, nodes, startTmp, endTmp); return new TransportationGraphDto(graph, weightMap); @@ -393,8 +429,21 @@ private void addOriginDestinationWalkConnections(Map> graph, Ma } private List findNearbyNodes(List nodes, double lat, double lon, int maxDistanceMeters) { + // 대략적인 거리 필터링 + double maxDistanceKm = maxDistanceMeters / 1000.0; + return nodes.stream() .filter(node -> { + // 빠른 거리 계산 (대략적) + double latDiff = Math.abs(lat - node.getLatitude()); + double lonDiff = Math.abs(lon - node.getLongitude()); + + // 필터링 (1도 ≈ 111km) + if (latDiff > maxDistanceKm / 111.0 || lonDiff > maxDistanceKm / (111.0 * Math.cos(Math.toRadians(lat)))) { + return false; + } + + // 정확한 거리 계산 double distance = haversine(lat, lon, node.getLatitude(), node.getLongitude()) * METER_CONVERSION; return distance <= maxDistanceMeters; }) @@ -680,7 +729,82 @@ private List mergeConsecutiveRoutes(List p i = j; } - return mergedSteps; + // 환승 시 walk step 추가 + return addTransferWalkSteps(mergedSteps, pathEdges); + } + + private List addTransferWalkSteps(List steps, List pathEdges) { + List result = new ArrayList<>(); + + for (int i = 0; i < steps.size(); i++) { + TransportationResponseDto.Step currentStep = steps.get(i); + result.add(currentStep); + + // 마지막 step이 아니고, 현재 step이 walk가 아닌 경우 + if (i < steps.size() - 1 && currentStep.mode() != DirectionType.WALK) { + TransportationResponseDto.Step nextStep = steps.get(i + 1); + + // 다음 step도 walk가 아닌 경우 (bus -> subway, subway -> bus 등) + if (nextStep.mode() != DirectionType.WALK) { + // 환승 walk step 추가 + String transferFrom = currentStep.to(); + String transferTo = nextStep.from(); + + // 이전 step의 도착지와 다음 step의 출발지 사이의 직선거리 계산 + int walkDistance = calculateTransferWalkDistance(transferFrom, transferTo, pathEdges); + + TransportationResponseDto.Step walkStep = new TransportationResponseDto.Step( + DirectionType.WALK, + null, + null, + walkDistance, + null, + null, + transferFrom, + transferTo + ); + + result.add(walkStep); + } + } + } + + return result; + } + + private int calculateTransferWalkDistance(String fromStation, String toStation, List pathEdges) { + // pathEdges에서 해당 정류장의 노드 정보 찾기 + Node fromNode = null; + Node toNode = null; + + for (Edge edge : pathEdges) { + if (edge.getStartNode() != null && edge.getStartNode().getStationName() != null && + edge.getStartNode().getStationName().equals(fromStation)) { + fromNode = edge.getStartNode(); + } + if (edge.getEndNode() != null && edge.getEndNode().getStationName() != null && + edge.getEndNode().getStationName().equals(fromStation)) { + fromNode = edge.getEndNode(); + } + if (edge.getStartNode() != null && edge.getStartNode().getStationName() != null && + edge.getStartNode().getStationName().equals(toStation)) { + toNode = edge.getStartNode(); + } + if (edge.getEndNode() != null && edge.getEndNode().getStationName() != null && + edge.getEndNode().getStationName().equals(toStation)) { + toNode = edge.getEndNode(); + } + } + + if (fromNode != null && toNode != null) { + double distanceKm = haversine( + fromNode.getLatitude(), fromNode.getLongitude(), + toNode.getLatitude(), toNode.getLongitude() + ); + return (int) (distanceKm * 1000); // km를 m로 변환 + } + + return 0; // 노드를 찾지 못한 경우 } private String getNodeName(Node node) {