diff --git a/src/main/java/com/wayble/server/ServerApplication.java b/src/main/java/com/wayble/server/ServerApplication.java index 2729f1c1..9cb92d1f 100644 --- a/src/main/java/com/wayble/server/ServerApplication.java +++ b/src/main/java/com/wayble/server/ServerApplication.java @@ -1,6 +1,7 @@ package com.wayble.server; import com.wayble.server.common.client.tmap.TMapProperties; +import com.wayble.server.direction.external.kric.KricProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration; @@ -15,10 +16,8 @@ ) @EnableJpaAuditing @EnableScheduling -@EnableElasticsearchRepositories(basePackages = { - "com.wayble.server.explore.repository", "com.wayble.server.direction.repository", "com.wayble.server.logging.repository" -}) -@EnableConfigurationProperties(TMapProperties.class) +@EnableConfigurationProperties({TMapProperties.class, KricProperties.class}) +@EnableElasticsearchRepositories(basePackages = {"com.wayble.server.explore.repository", "com.wayble.server.logging.repository"}) @EntityScan(basePackages = "com.wayble.server") public class ServerApplication { diff --git a/src/main/java/com/wayble/server/common/config/RestTemplateConfig.java b/src/main/java/com/wayble/server/common/config/RestTemplateConfig.java deleted file mode 100644 index ec2795b2..00000000 --- a/src/main/java/com/wayble/server/common/config/RestTemplateConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.wayble.server.common.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestTemplate; - -@Configuration -public class RestTemplateConfig { - - @Bean - public RestTemplate restTemplate() { - return new RestTemplate(); - } -} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/common/config/SecurityConfig.java b/src/main/java/com/wayble/server/common/config/SecurityConfig.java index 3ec980f2..2c96a974 100644 --- a/src/main/java/com/wayble/server/common/config/SecurityConfig.java +++ b/src/main/java/com/wayble/server/common/config/SecurityConfig.java @@ -45,7 +45,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/v1/users/reissue", "/api/v1/users/logout", "/api/v1/auth/**", - "/api/v1/directions/**", "/swagger-ui/**", "/v3/api-docs/**", "/", diff --git a/src/main/java/com/wayble/server/common/config/WebClientConfig.java b/src/main/java/com/wayble/server/common/config/WebClientConfig.java index db69178a..8a8d4380 100644 --- a/src/main/java/com/wayble/server/common/config/WebClientConfig.java +++ b/src/main/java/com/wayble/server/common/config/WebClientConfig.java @@ -1,6 +1,7 @@ package com.wayble.server.common.config; import com.wayble.server.common.client.tmap.TMapProperties; +import com.wayble.server.direction.external.kric.KricProperties; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -11,6 +12,7 @@ public class WebClientConfig { private final TMapProperties tMapProperties; + private final KricProperties kricProperties; @Bean public WebClient webClient() { @@ -24,4 +26,11 @@ public WebClient tMapWebClient() { .baseUrl(tMapProperties.baseUrl()) .build(); } + + @Bean + public WebClient kricWebClient() { + return WebClient.builder() + .baseUrl(kricProperties.baseUrl()) + .build(); + } } 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 97d662fb..d7a3f58c 100644 --- a/src/main/java/com/wayble/server/direction/controller/TransportationController.java +++ b/src/main/java/com/wayble/server/direction/controller/TransportationController.java @@ -10,7 +10,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -51,8 +51,8 @@ public class TransportationController { ) } ) - @GetMapping("/") - public CommonResponse getDirections( + @PostMapping("/") + public CommonResponse findDirections( @RequestBody TransportationRequestDto request ){ TransportationResponseDto data = transportationService.findRoutes(request); 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 02518eac..44255d4f 100644 --- a/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java +++ b/src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java @@ -1,10 +1,9 @@ package com.wayble.server.direction.dto; import com.wayble.server.direction.entity.DirectionType; -import io.micrometer.common.lang.Nullable; +import org.springframework.lang.Nullable; import io.swagger.v3.oas.annotations.media.Schema; -import java.time.ZonedDateTime; import java.util.List; @Schema(description = "대중교통 길찾기 응답 DTO") @@ -14,8 +13,8 @@ public record TransportationResponseDto( ) { public record Step( DirectionType mode, // 예: START, WALK, SUBWAY, BUS, FINISH - String routeName,// mode에 따라 null일 수 있음 - NodeInfo information, // mode에 따라 null일 수 있음 + @Nullable String routeName, + @Nullable NodeInfo information, String from, String to ) {} @@ -33,6 +32,6 @@ public record NodeInfo( public record LocationInfo( Double latitude, - Double Longtitude + Double Longitude ) {} } 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 c9ca1b23..952d30d9 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 @@ -7,7 +7,7 @@ @Entity @Getter -@Builder(access = AccessLevel.PUBLIC) +@Builder(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "edge") @@ -34,4 +34,14 @@ public class Edge { @ManyToOne @JoinColumn(name = "route_id") private Route route; + + public static Edge createEdge(Long id, Node startNode, Node endNode, DirectionType edgeType) { + return Edge.builder() + .id(id) + .edgeType(edgeType) + .startNode(startNode) + .endNode(endNode) + .route(null) + .build(); + } } 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 9136df34..3b5029a6 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 @@ -49,9 +49,10 @@ public class Node { @OneToOne(mappedBy = "node") private Facility facility_id; - public Node(Long id, String stationName, double latitude, double longitude) { + public Node(Long id, String stationName, DirectionType nodeType, double latitude, double longitude) { this.id = id; this.stationName = stationName; + this.nodeType = nodeType; this.latitude = latitude; this.longitude = longitude; } diff --git a/src/main/java/com/wayble/server/direction/exception/DirectionErrorCase.java b/src/main/java/com/wayble/server/direction/exception/DirectionErrorCase.java index bd5bdd2f..e7289c69 100644 --- a/src/main/java/com/wayble/server/direction/exception/DirectionErrorCase.java +++ b/src/main/java/com/wayble/server/direction/exception/DirectionErrorCase.java @@ -11,7 +11,8 @@ public enum DirectionErrorCase implements ErrorCase { PATH_NOT_FOUND(400, 4001, "해당하는 경로를 찾을 수 없습니다."), ES_INDEXING_FAILED(500, 4002, "ElasticSearch 인덱싱에 실패했습니다."), HISTORY_NOT_FOUND(400, 4004, "검색 기록이 없습니다."), - ; + DISTANCE_TOO_FAR(400, 4002, "거리가 30km 이상입니다."), + PATH_NOT_FOUND(400, 4001, "해당하는 경로를 찾을 수 없습니다."); private final Integer httpStatusCode; private final Integer errorCode; diff --git a/src/main/java/com/wayble/server/direction/external/kric/KricProperties.java b/src/main/java/com/wayble/server/direction/external/kric/KricProperties.java new file mode 100644 index 00000000..f716f287 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/kric/KricProperties.java @@ -0,0 +1,10 @@ +package com.wayble.server.direction.external.kric; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "kric.api") +public record KricProperties( + String key, + String baseUrl +) { +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/direction/dto/toilet/KricToiletRawBody.java b/src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawBody.java similarity index 64% rename from src/main/java/com/wayble/server/direction/dto/toilet/KricToiletRawBody.java rename to src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawBody.java index 2557887c..3af1db3f 100644 --- a/src/main/java/com/wayble/server/direction/dto/toilet/KricToiletRawBody.java +++ b/src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawBody.java @@ -1,4 +1,4 @@ -package com.wayble.server.direction.dto.toilet; +package com.wayble.server.direction.external.kric.dto; import java.util.List; diff --git a/src/main/java/com/wayble/server/direction/dto/toilet/KricToiletRawItem.java b/src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawItem.java similarity index 66% rename from src/main/java/com/wayble/server/direction/dto/toilet/KricToiletRawItem.java rename to src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawItem.java index 77e5407d..ddd6f918 100644 --- a/src/main/java/com/wayble/server/direction/dto/toilet/KricToiletRawItem.java +++ b/src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawItem.java @@ -1,4 +1,4 @@ -package com.wayble.server.direction.dto.toilet; +package com.wayble.server.direction.external.kric.dto; import lombok.Getter; diff --git a/src/main/java/com/wayble/server/direction/dto/toilet/KricToiletRawResponse.java b/src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawResponse.java similarity index 63% rename from src/main/java/com/wayble/server/direction/dto/toilet/KricToiletRawResponse.java rename to src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawResponse.java index 3fd3d6dd..b0b3faac 100644 --- a/src/main/java/com/wayble/server/direction/dto/toilet/KricToiletRawResponse.java +++ b/src/main/java/com/wayble/server/direction/external/kric/dto/KricToiletRawResponse.java @@ -1,4 +1,4 @@ -package com.wayble.server.direction.dto.toilet; +package com.wayble.server.direction.external.kric.dto; import java.util.List; diff --git a/src/main/java/com/wayble/server/direction/repository/FacilityRepository.java b/src/main/java/com/wayble/server/direction/repository/FacilityRepository.java index ebee3aa2..7fe4a12d 100644 --- a/src/main/java/com/wayble/server/direction/repository/FacilityRepository.java +++ b/src/main/java/com/wayble/server/direction/repository/FacilityRepository.java @@ -1,9 +1,12 @@ package com.wayble.server.direction.repository; import com.wayble.server.direction.entity.transportation.Facility; -import org.springframework.data.jpa.repository.JpaRepository; -public interface FacilityRepository extends JpaRepository { +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +public interface FacilityRepository extends JpaRepository { + Optional findByNodeId(Long nodeId); } diff --git a/src/main/java/com/wayble/server/direction/service/EdgeService.java b/src/main/java/com/wayble/server/direction/service/EdgeService.java deleted file mode 100644 index 1421555a..00000000 --- a/src/main/java/com/wayble/server/direction/service/EdgeService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.wayble.server.direction.service; - -import com.wayble.server.direction.entity.DirectionType; -import com.wayble.server.direction.entity.transportation.*; -import org.springframework.stereotype.Service; - -@Service -public class EdgeService { - public Edge createEdge(Long id, Node startNode, Node endNode, DirectionType edgeType) { - - Edge edge = Edge.builder() - .id(id) - .edgeType(edgeType) - .startNode(startNode) - .endNode(endNode) - .route(null) - .build(); - - return edge; - } -} 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 a43e6ec9..834f7907 100644 --- a/src/main/java/com/wayble/server/direction/service/FacilityService.java +++ b/src/main/java/com/wayble/server/direction/service/FacilityService.java @@ -1,18 +1,17 @@ package com.wayble.server.direction.service; import com.wayble.server.direction.dto.TransportationResponseDto; -import com.wayble.server.direction.dto.toilet.KricToiletRawItem; -import com.wayble.server.direction.dto.toilet.KricToiletRawResponse; 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; import com.wayble.server.direction.repository.FacilityRepository; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; +import lombok.extern.slf4j.Slf4j; + import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; +import com.wayble.server.direction.external.kric.KricProperties; import java.util.ArrayList; import java.util.HashMap; @@ -20,17 +19,17 @@ import java.util.Map; import java.util.stream.Collectors; + @Service +@Slf4j @RequiredArgsConstructor public class FacilityService { private final FacilityRepository facilityRepository; - private final RestTemplate restTemplate; + private final WebClient kricWebClient; + private final KricProperties kricProperties; - @Value("${kric.api.key}") - private String kricApiKey; - - public TransportationResponseDto.NodeInfo getNodeInfo(Long NodeId){ - Facility facility = facilityRepository.findById(NodeId).orElse(null); + public TransportationResponseDto.NodeInfo getNodeInfo(Long nodeId){ + Facility facility = facilityRepository.findByNodeId(nodeId).orElse(null); List wheelchair = new ArrayList<>(); List elevator = new ArrayList<>(); Boolean accessibleRestroom = false; @@ -68,21 +67,29 @@ public TransportationResponseDto.NodeInfo getNodeInfo(Long NodeId){ } private Map getToiletInfo(Facility facility){ - String url = UriComponentsBuilder.fromHttpUrl("https://data.kric.go.kr/api/vulnerableUserInfo/stationDisabledToilet") - .queryParam("serviceKey", kricApiKey) + String uri = UriComponentsBuilder.fromPath("/api/vulnerableUserInfo/stationDisabledToilet") + .queryParam("serviceKey", kricProperties.key()) .queryParam("format", "json") - .queryParam("railOprIsttCd", facility.getRailOprLsttCd()) + .queryParam("railOprLsttCd", facility.getRailOprLsttCd()) .queryParam("lnCd", facility.getLnCd()) .queryParam("stinCd", facility.getStinCd()) .toUriString(); - ResponseEntity response = restTemplate.exchange( - url, - HttpMethod.GET, - null, - new ParameterizedTypeReference() {} - ); + + List items; + try{ + KricToiletRawResponse response = kricWebClient + .get() + .uri(uri) + .retrieve() + .bodyToMono(KricToiletRawResponse.class) + .block(); - List items = response.getBody().body().item(); + items = response.body().item(); + + } catch(Exception e){ + log.info("역사 화장실 api 호출 중 에러 발생: {}: {}", uri, e.getCause()); + return new HashMap<>(); + } // 역별로 화장실 존재 여부 추출 (중복 제거) Map stationToiletMap = new HashMap<>(); @@ -91,7 +98,9 @@ private Map getToiletInfo(Facility facility){ int toiletCount = 0; try { toiletCount = Integer.parseInt(item.toltNum()); - } catch (NumberFormatException ignored) {} + } catch (NumberFormatException e) { + log.warn("지하철 역 토이렛 개수 파싱 실패. 지하철역 번호 {}: {}", stinCd, item.toltNum(), e); + } stationToiletMap.put(stinCd, stationToiletMap.getOrDefault(stinCd, false) || toiletCount > 0); } 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 205f5696..fa19da79 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -13,31 +13,43 @@ import org.springframework.stereotype.Service; import java.util.*; -import java.util.Objects; 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 public class TransportationService { private final NodeRepository nodeRepository; private final EdgeRepository edgeRepository; - private final EdgeService edgeService; private final FacilityService facilityService; 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; + public TransportationResponseDto findRoutes(TransportationRequestDto request){ TransportationRequestDto.Location origin = request.origin(); TransportationRequestDto.Location destination = request.destination(); - Node start = new Node(-1L, origin.name(), origin.latitude(), origin.longitude()); - Node end = new Node(-2L, destination.name(), destination.latitude(), destination.longitude()); + // 거리 검증 (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 = returnDijstra(start, end); + List steps = returnDijkstra(start, end); int startIndex = (request.cursor() != null) ? request.cursor() : 0; @@ -56,7 +68,7 @@ public TransportationResponseDto findRoutes(TransportationRequestDto request){ } - private List returnDijstra(Node startTmp, Node endTmp){ + private List returnDijkstra(Node startTmp, Node endTmp){ // 실제 노드·엣지 조회 및 컬렉션 복제 nodes = new ArrayList<>(nodeRepository.findAll()); @@ -85,11 +97,11 @@ private List returnDijstra(Node startTmp, Node e Map, Integer> weightMap = new HashMap<>(); // 출발지 -> 가장 가까운 정류장 (도보) - Edge startToStation = edgeService.createEdge(-1L, startTmp, nearestToStart, DirectionType.WALK); + Edge startToStation = Edge.createEdge(-1L, startTmp, nearestToStart, DirectionType.WALK); edges.add(startToStation); // 가장 가까운 정류장 -> 도착지 (도보) - Edge stationToEnd = edgeService.createEdge(-2L, nearestToEnd, endTmp, DirectionType.WALK); + Edge stationToEnd = Edge.createEdge(-2L, nearestToEnd, endTmp, DirectionType.WALK); edges.add(stationToEnd); // 모든 엣지의 가중치 계산 @@ -106,19 +118,19 @@ private List returnDijstra(Node startTmp, Node e int weight = (int)(haversine( from.getLatitude(), from.getLongitude(), to.getLatitude(), to.getLongitude() - ) * 1000); + ) * METER_CONVERSION); weightMap.put(Pair.of(from.getId(), to.getId()), weight); } // 그래프 빌드 및 Dijkstra 호출 Map> graph = buildGraph(nodes, edges); - List result = runDijstra(graph, startTmp, endTmp, weightMap); + List result = runDijkstra(graph, startTmp, endTmp, weightMap); return result; } - private List runDijstra( + private List runDijkstra( Map> graph, Node start, Node end, Map, Integer> weightMap ){ @@ -167,7 +179,7 @@ private List runDijstra( (int)(haversine( edge.getStartNode().getLatitude(), edge.getStartNode().getLongitude(), edge.getEndNode().getLatitude(), edge.getEndNode().getLongitude() - ) * 1000) + ) * METER_CONVERSION) ); // 간단한 경로 선호를 위한 가중치 조정 @@ -179,11 +191,11 @@ private List runDijstra( prevEdgeForCurr.getEdgeType() != edge.getEdgeType() && prevEdgeForCurr.getEdgeType() != DirectionType.WALK && edge.getEdgeType() != DirectionType.WALK) { - weight += 2000; // 환승 패널티 대폭 증가 + weight += TRANSFER_PENALTY; // 환승 패널티 대폭 증가 } // 단계 수 패널티 (경로 단계가 많을수록 불이익) - weight += 500; // 각 단계마다 추가 비용 대폭 증가 + weight += STEP_PENALTY; // 각 단계마다 추가 비용 대폭 증가 int alt = distance.get(curr.getId()) + weight; if (alt < distance.get(neighbor.getId())) { @@ -201,7 +213,7 @@ private List runDijstra( Set backtrackVisited = new HashSet<>(); if (distance.get(end.getId()) == Integer.MAX_VALUE) { - System.out.println("⚠️ 경로를 찾을 수 없음: 도착지에 도달할 수 없음"); + log.warn("경로를 찾을 수 없음: 도착지에 도달할 수 없음"); return steps; // 빈 리스트 반환 } @@ -209,14 +221,12 @@ private List runDijstra( List pathEdges = new ArrayList<>(); while (current != null && !current.equals(start)) { if (backtrackVisited.contains(current.getId())) { - System.out.println("⚠️ 순환 감지: " + current.getStationName()); break; } backtrackVisited.add(current.getId()); Edge edge = prevEdge.get(current.getId()); if (edge == null) { - System.out.println("⚠️ 이전 엣지가 null: " + current.getStationName()); break; } pathEdges.add(0, edge); @@ -247,8 +257,10 @@ private List mergeConsecutiveRoutes(List p // 시작 노드 - String fromName = (currentEdge.getStartNode() != null) ? currentEdge.getStartNode().getStationName() : "Unknown"; - String toName = (currentEdge.getEndNode() != null) ? currentEdge.getEndNode().getStationName() : "Unknown"; + 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) { @@ -304,7 +316,7 @@ private Map> buildGraph(List nodes, List edges) { if (nodeId != null) { graph.put(nodeId, new ArrayList<>()); } else { - System.out.println("❗ ID가 null인 node 발견: " + node.getStationName()); + log.warn("ID가 null인 node 발견: " + node.getStationName()); } } for (Edge edge : edges) {