diff --git a/src/main/java/com/fitlink/service/TmapRouteService.java b/src/main/java/com/fitlink/service/TmapRouteService.java index a383443..d8a98d5 100644 --- a/src/main/java/com/fitlink/service/TmapRouteService.java +++ b/src/main/java/com/fitlink/service/TmapRouteService.java @@ -23,9 +23,10 @@ public class TmapRouteService { private static final String BASE_URL = "https://apis.openapi.sk.com"; - /* --------------------------- - * 1) 도보 경로 - * --------------------------- */ + + /* ================================================= + * WALK + * ================================================= */ public RouteResponseDTO getPedestrianRoute(float oLat, float oLng, float dLat, float dLng) { String url = BASE_URL + "/tmap/routes/pedestrian?version=1"; @@ -38,19 +39,19 @@ public RouteResponseDTO getPedestrianRoute(float oLat, float oLng, float dLat, f "endY": %f, "startName": "출발지", "endName": "도착지", - "searchOption": "0", "reqCoordType": "WGS84GEO", "resCoordType": "WGS84GEO" } """, oLng, oLat, dLng, dLat); - TmapRouteDTO dto = post(url, body); - return convertWalkCar("walk", dto); + TmapRouteDTO dto = post(url, body, TmapRouteDTO.class); + return convertWalk(dto); } - /* --------------------------- - * 2) 자동차 경로 - * --------------------------- */ + + /* ================================================= + * CAR + * ================================================= */ public RouteResponseDTO getCarRoute(float oLat, float oLng, float dLat, float dLng) { String url = BASE_URL + "/tmap/routes?version=1"; @@ -63,19 +64,19 @@ public RouteResponseDTO getCarRoute(float oLat, float oLng, float dLat, float dL "endY": %f, "startName": "출발지", "endName": "도착지", - "searchOption": "0", "reqCoordType": "WGS84GEO", "resCoordType": "WGS84GEO" } """, oLng, oLat, dLng, dLat); - TmapRouteDTO dto = post(url, body); - return convertWalkCar("car", dto); + TmapCarRouteDTO dto = post(url, body, TmapCarRouteDTO.class); + return convertCar(dto); } - /* --------------------------- - * 3) 대중교통 경로 - * --------------------------- */ + + /* ================================================= + * TRANSIT + * ================================================= */ public RouteResponseDTO getTransitRoute(float oLat, float oLng, float dLat, float dLng) { String url = UriComponentsBuilder.fromHttpUrl(BASE_URL + "/transit/routes") @@ -86,136 +87,179 @@ public RouteResponseDTO getTransitRoute(float oLat, float oLng, float dLat, floa .queryParam("endY", dLat) .queryParam("reqCoordType", "WGS84GEO") .queryParam("resCoordType", "WGS84GEO") - .queryParam("sort", 0) - .queryParam("lang", 0) .queryParam("format", "json") .toUriString(); - HttpHeaders headers = new HttpHeaders(); - headers.set("appKey", appKey); + ResponseEntity res = + restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(buildHeaders()), TmapRouteDTO.class); - ResponseEntity response = - restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), TransitRouteDTO.class); - - return convertTransit(response.getBody()); + return RouteResponseDTO.builder() + .type("transit") + .duration(0) + .path(new ArrayList<>()) + .waypoints(new ArrayList<>()) + .build(); } - /* --------------------------- - * POST 공통 - * --------------------------- */ - private TmapRouteDTO post(String url, String body) { - HttpHeaders headers = new HttpHeaders(); - headers.set("appKey", appKey); - headers.setContentType(MediaType.APPLICATION_JSON); + /* ================================================= + * POST 공용 + * ================================================= */ + private T post(String url, String body, Class clazz) { - HttpEntity entity = new HttpEntity<>(body, headers); + HttpEntity entity = new HttpEntity<>(body, buildHeaders()); - ResponseEntity rawResponse = + ResponseEntity raw = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); - System.out.println("[Tmap Raw Response] = " + rawResponse.getBody()); + System.out.println("[RAW] = " + raw.getBody()); try { - return new ObjectMapper().readValue(rawResponse.getBody(), TmapRouteDTO.class); - + return new ObjectMapper().readValue(raw.getBody(), clazz); } catch (Exception e) { - System.err.println("DTO 변환 오류: " + e.getMessage()); + System.err.println(clazz.getSimpleName() + " 변환 오류: " + e.getMessage()); return null; } } - /* --------------------------- - * Walk / Car 변환 - * --------------------------- */ - private RouteResponseDTO convertWalkCar(String type, TmapRouteDTO dto) { - - if (dto == null || dto.getFeatures() == null || dto.getFeatures().isEmpty()) { - return RouteResponseDTO.builder() - .type(type) - .distance(0) - .duration(0) - .path(new ArrayList<>()) - .waypoints(new ArrayList<>()) - .build(); - } + private HttpHeaders buildHeaders() { + HttpHeaders h = new HttpHeaders(); + h.set("appKey", appKey); + h.setContentType(MediaType.APPLICATION_JSON); + return h; + } + - int distance = dto.getFeatures().get(0).getProperties().getTotalDistance(); - int seconds = dto.getFeatures().get(0).getProperties().getTotalTime(); - int minutes = seconds / 60; + /* ================================================= + * WALK 변환 + * ================================================= */ + private RouteResponseDTO convertWalk(TmapRouteDTO dto) { + + if (dto == null || dto.getFeatures() == null) return empty("walk"); + + int dist = dto.getFeatures().get(0).getProperties().getTotalDistance(); + int time = dto.getFeatures().get(0).getProperties().getTotalTime() / 60; List> path = new ArrayList<>(); List waypoints = new ArrayList<>(); - dto.getFeatures().forEach(feature -> { + for (var f : dto.getFeatures()) { + var geo = f.getGeometry(); + var prop = f.getProperties(); + if (geo == null) continue; - var geo = feature.getGeometry(); - var prop = feature.getProperties(); + if (geo.getPoint() != null) { + double lat = geo.getPoint().get(1); + double lng = geo.getPoint().get(0); + path.add(List.of(lat, lng)); + } - if (geo == null || geo.getType() == null) return; + if (geo.getLine() != null) { + geo.getLine().forEach(c -> path.add(List.of(c.get(1), c.get(0)))); + } - switch (geo.getType()) { + if (prop != null && prop.getDescription() != null && prop.getTurnType() != null) { + waypoints.add(new RouteResponseDTO.Waypoint( + path.get(path.size()-1).get(0), + path.get(path.size()-1).get(1), + prop.getDescription() + )); + } + } - case "LineString" -> { - if (geo.getLine() != null) - geo.getLine().forEach(coord -> - path.add(List.of(coord.get(1), coord.get(0)))); - } + return RouteResponseDTO.builder() + .type("walk") + .distance(dist) + .duration(time) + .path(path) + .waypoints(waypoints) + .build(); + } - case "Point" -> { - if (geo.getPoint() != null) { - var c = geo.getPoint(); - double lat = c.get(1); - double lng = c.get(0); + /* ================================================= + * CAR 변환 + * ================================================= */ + private RouteResponseDTO convertCar(TmapCarRouteDTO dto) { - path.add(List.of(lat, lng)); + if (dto == null || dto.getFeatures() == null) { + return empty("car"); + } + + int[] totalDistance = {0}; + int[] totalTime = {0}; + + List> path = new ArrayList<>(); + List waypoints = new ArrayList<>(); - // ★ turnType이 있으면 경유지로 추가 - if (prop != null && prop.getTurnType() != null) { + dto.getFeatures().forEach(f -> { - waypoints.add( - new RouteResponseDTO.Waypoint( - lat, - lng, - prop.getDescription() - ) - ); - } - } - } + var geo = f.getGeometry(); + var prop = f.getProperties(); - case "MultiLineString" -> { - if (geo.getMulti() != null) - geo.getMulti().forEach(line -> - line.forEach(coord -> - path.add(List.of(coord.get(1), coord.get(0))))); - } + // 거리/시간 누적 + if (prop != null) { + totalDistance[0] += prop.getDistanceInt(); + totalTime[0] += prop.getTimeInt(); } + + if (geo == null) return; + + /* ------------------------ + * MULTILINESTRING + * ------------------------ */ + if ("MultiLineString".equals(geo.getType()) && geo.getMulti() != null) { + geo.getMulti().forEach(line -> + line.forEach(coord -> + path.add(List.of(coord.get(1), coord.get(0)))) + ); + } + + /* ------------------------ + * LINESTRING + * ------------------------ */ + if ("LineString".equals(geo.getType()) && geo.getLine() != null) { + geo.getLine().forEach(coord -> + path.add(List.of(coord.get(1), coord.get(0)))); + } + + /* ------------------------ + * POINT + name 기반 waypoint + * ------------------------ */ + if ("Point".equals(geo.getType()) && + geo.getPoint() != null && + prop != null && + (prop.getDescription() != null || prop.getIndex() != null)) { + + double lat = geo.getPoint().get(1); + double lng = geo.getPoint().get(0); + + String desc = prop.getDescription() != null + ? prop.getDescription() + : ("지점_" + prop.getIndex()); + + waypoints.add(new RouteResponseDTO.Waypoint(lat, lng, desc)); + } + }); return RouteResponseDTO.builder() - .type(type) - .distance(distance) - .duration(minutes) + .type("car") + .distance(totalDistance[0]) + .duration(totalTime[0] / 60) .path(path) .waypoints(waypoints) .build(); } - /* --------------------------- - * Transit 변환 - * --------------------------- */ - private RouteResponseDTO convertTransit(TransitRouteDTO dto) { - - TransitRouteDTO.Itinerary it = - dto.getMetaData().getPlan().getItineraries().get(0); - - int totalDuration = it.getDuration() / 60; + private RouteResponseDTO empty(String type) { return RouteResponseDTO.builder() - .type("transit") - .duration(totalDuration) + .type(type) + .distance(0) + .duration(0) + .path(new ArrayList<>()) + .waypoints(new ArrayList<>()) .build(); } } diff --git a/src/main/java/com/fitlink/web/dto/RouteResponseDTO.java b/src/main/java/com/fitlink/web/dto/RouteResponseDTO.java index 9d87d33..a893320 100644 --- a/src/main/java/com/fitlink/web/dto/RouteResponseDTO.java +++ b/src/main/java/com/fitlink/web/dto/RouteResponseDTO.java @@ -1,27 +1,28 @@ package com.fitlink.web.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; +import lombok.*; import java.util.List; -@Data +@Getter +@Setter @Builder +@AllArgsConstructor +@NoArgsConstructor public class RouteResponseDTO { - private String type; // walk / car / transit - private int duration; // minutes - private int distance; // meters (walk/car only) + private String type; + private int distance; + private int duration; private List> path; + private List waypoints; - private List waypoints; // ★ 자동 추출된 안내 포인트 - - @Data + @Getter + @Setter @AllArgsConstructor public static class Waypoint { private double lat; private double lng; - private String description; // 예: "좌회전", "횡단보도 건너기" + private String description; } } diff --git a/src/main/java/com/fitlink/web/dto/TmapCarRouteDTO.java b/src/main/java/com/fitlink/web/dto/TmapCarRouteDTO.java new file mode 100644 index 0000000..ae88b89 --- /dev/null +++ b/src/main/java/com/fitlink/web/dto/TmapCarRouteDTO.java @@ -0,0 +1,91 @@ +package com.fitlink.web.dto; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class TmapCarRouteDTO { + + private String type; + private List features; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Feature { + private String type; + private Geometry geometry; + private Properties properties; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Geometry { + + private String type; + + // Point + private List point; + + // LineString + private List> line; + + // MultiLineString + private List>> multi; + + @JsonAnySetter + public void handle(String key, Object value) { + if (!"coordinates".equals(key)) return; + + if (value instanceof List list) { + + // POINT: [lng, lat] + if (!list.isEmpty() && list.get(0) instanceof Number) { + this.point = (List) value; + return; + } + + // LINESTRING: [[lng, lat], ...] + if (!list.isEmpty() && list.get(0) instanceof List sub + && sub.size() == 2 && sub.get(0) instanceof Number) { + this.line = (List>) value; + return; + } + + // MULTILINESTRING: [[[lng,lat], ...], ...] + if (!list.isEmpty() && list.get(0) instanceof List sub2 + && sub2.get(0) instanceof List) { + this.multi = (List>>) value; + } + } + } + } + + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Properties { + + private Object time; + private Object distance; + private String description; + private Integer index; + + public int getTimeInt() { + if (time == null) return 0; + if (time instanceof Number n) return n.intValue(); + if (time instanceof String s) return Integer.parseInt(s); + return 0; + } + + public int getDistanceInt() { + if (distance == null) return 0; + if (distance instanceof Number n) return n.intValue(); + if (distance instanceof String s) return Integer.parseInt(s); + return 0; + } + } +} diff --git a/src/main/java/com/fitlink/web/dto/TmapRouteDTO.java b/src/main/java/com/fitlink/web/dto/TmapRouteDTO.java index ca5d7a2..f60b911 100644 --- a/src/main/java/com/fitlink/web/dto/TmapRouteDTO.java +++ b/src/main/java/com/fitlink/web/dto/TmapRouteDTO.java @@ -23,9 +23,8 @@ public static class Feature { public static class Geometry { private String type; - private List point; // [lng, lat] - private List> line; // [[lng,lat], ...] - private List>> multi; // [[[lng,lat]...]] + private List point; + private List> line; @JsonAnySetter public void handle(String key, Object value) { @@ -39,14 +38,8 @@ public void handle(String key, Object value) { } if (!list.isEmpty() && list.get(0) instanceof List sub - && sub.size() == 2 && sub.get(0) instanceof Number) { + && sub.size() == 2) { this.line = (List>) value; - return; - } - - if (!list.isEmpty() && list.get(0) instanceof List sub2 - && sub2.get(0) instanceof List) { - this.multi = (List>>) value; } } }