Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,4 +15,17 @@ public interface EdgeRepository extends JpaRepository<Edge, Long> {
"JOIN FETCH e.endNode " +
"LEFT JOIN FETCH e.route")
List<Edge> 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<Edge> findEdgesInBoundingBox(
@Param("minLat") double minLat,
@Param("maxLat") double maxLat,
@Param("minLon") double minLon,
@Param("maxLon") double maxLon
);
Comment on lines +19 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

OR → AND로 변경해 공간 필터링 효과 증대(불필요한 엣지 로드 감소)

현재 조건은 시작/끝 중 하나만 박스 내면 매칭되어, 박스 외부 노드와 연결된 엣지도 대거 조회됩니다. 결국 서비스 레벨에서 nodeIds로 다시 거르지만, DB/네트워크/메모리 낭비가 커 OOM 완화 목적에 역행할 수 있습니다. 두 끝점 모두 박스 내인 엣지만 가져오도록 AND로 바꾸는 것을 권장합니다.

-           "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)")
+           "WHERE (s.latitude BETWEEN :minLat AND :maxLat AND s.longitude BETWEEN :minLon AND :maxLon) AND " +
+           "(en.latitude BETWEEN :minLat AND :maxLat AND en.longitude BETWEEN :minLon AND :maxLon)")

추가로, Node(latitude, longitude) 컬럼 인덱싱 시 실효가 더 커집니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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<Edge> findEdgesInBoundingBox(
@Param("minLat") double minLat,
@Param("maxLat") double maxLat,
@Param("minLon") double minLon,
@Param("maxLon") double maxLon
);
@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) AND " +
"(en.latitude BETWEEN :minLat AND :maxLat AND en.longitude BETWEEN :minLon AND :maxLon)")
List<Edge> findEdgesInBoundingBox(
@Param("minLat") double minLat,
@Param("maxLat") double maxLat,
@Param("minLon") double minLon,
@Param("maxLon") double maxLon
);
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/direction/repository/EdgeRepository.java
around lines 19 to 30, the JPQL WHERE clause currently uses OR to match edges
whose start OR end node lies inside the bounding box, causing many edges with
one endpoint outside the box to be loaded; change the OR to AND so only edges
whose startNode AND endNode are both inside the box are returned, and update any
tests accordingly; additionally, add or ensure database indexes on Node.latitude
and Node.longitude (or a composite spatial index if supported) to improve filter
performance and reduce memory/IO overhead.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node, Long> {

@Query("SELECT n FROM Node n WHERE " +
"n.latitude BETWEEN :minLat AND :maxLat AND " +
"n.longitude BETWEEN :minLon AND :maxLon")
List<Node> findNodesInBoundingBox(
@Param("minLat") double minLat,
@Param("maxLat") double maxLat,
@Param("minLon") double minLon,
@Param("maxLon") double maxLon
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean> isLowFloor = new ArrayList<>();
Integer dispatchInterval = null;

Expand Down Expand Up @@ -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++;
Expand All @@ -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<Boolean> 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);
}

Expand Down Expand Up @@ -165,6 +195,7 @@ private StationSearchResponse fetchStationByName(String stationName) {

private StationSearchResponse.StationItem findClosestStation(List<StationSearchResponse.StationItem> stations, Double x, Double y) {
if (stations == null || stations.isEmpty()) {
log.warn("❌ 정류소 목록이 비어있음");
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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){

Expand Down Expand Up @@ -88,25 +91,54 @@ private TransportationResponseDto.Route createRoute(List<TransportationResponseD
}

private List<List<TransportationResponseDto.Step>> findMultipleTransportationRoutes(Node startTmp, Node endTmp){
// 1. 데이터 로드
List<Node> nodes = new ArrayList<>(nodeRepository.findAll());
List<Edge> 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<Node> nodes = nodeRepository.findNodesInBoundingBox(
boundingBox[0], boundingBox[1], boundingBox[2], boundingBox[3]
);
List<Edge> 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<List<TransportationResponseDto.Step>> 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<List<TransportationResponseDto.Step>> findMultipleOptimalRoutes(
Expand Down Expand Up @@ -312,15 +344,18 @@ private TransportationGraphDto buildGraph(List<Node> nodes, List<Edge> edges, No
Map<Long, List<Edge>> graph = new HashMap<>();
Map<Pair<Long, Long>, 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<Long> 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;

Expand All @@ -331,7 +366,8 @@ private TransportationGraphDto buildGraph(List<Node> nodes, List<Edge> 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);

Expand All @@ -342,7 +378,7 @@ private TransportationGraphDto buildGraph(List<Node> nodes, List<Edge> edges, No
weightMap.put(Pair.of(startId, endId), weight);
}

// 3. 출발지/도착지 도보 연결 추가
// 4. 출발지/도착지 도보 연결 추가
addOriginDestinationWalkConnections(graph, weightMap, nodes, startTmp, endTmp);

return new TransportationGraphDto(graph, weightMap);
Expand Down Expand Up @@ -393,8 +429,21 @@ private void addOriginDestinationWalkConnections(Map<Long, List<Edge>> graph, Ma
}

private List<Node> findNearbyNodes(List<Node> 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;
})
Expand Down Expand Up @@ -680,7 +729,82 @@ private List<TransportationResponseDto.Step> mergeConsecutiveRoutes(List<Edge> p
i = j;
}

return mergedSteps;
// 환승 시 walk step 추가
return addTransferWalkSteps(mergedSteps, pathEdges);
}

private List<TransportationResponseDto.Step> addTransferWalkSteps(List<TransportationResponseDto.Step> steps, List<Edge> pathEdges) {
List<TransportationResponseDto.Step> 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<Edge> 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) {
Expand Down