지난 번에 작성한 글을 이어서 작성하겠다.
public GeocodePoint geocodeAddress(String address) {
TmapGeocodingResponseDto response = callGeocodingApi(address);
return parseGeocodeResponse(response);
}
private TmapGeocodingResponseDto callGeocodingApi(String address) {
return tmapWebClient.get()
.uri(uriBuilder -> uriBuilder
.path("tmap/geo/fullAddrGeo")
.queryParam("version", 1)
.queryParam("format", "json")
.queryParam("fullAddr", address)
.queryParam("coordType", coordType)
.queryParam("addressFlag", "F00")
.queryParam("page", 1)
.queryParam("count", 20)
.build())
.retrieve()
.onStatus(s -> s.is4xxClientError() || s.is5xxServerError(),
resp -> resp.bodyToMono(String.class)
.defaultIfEmpty("")
.map(body -> new BadRequestHandler(ErrorStatus.TMAP_GEOCODING_MAPPING_FAILED)))
.bodyToMono(TmapGeocodingResponseDto.class)
.block();
}
이전에 구현한 tmapWebClient로 Tmap API를 불러온다.
이때 사용한 query param은 공식문서를 참고하여 작성했다.
version: API 버전 정보
coordType: 좌표 타입 (나는 경위도로 변환하기에, WGS84GEO를 사용하였다)
fullAddr: 사용자가 입력한 주소 또는 회사 주소
addressFlag: 주소 구분 코드
해당 API로 응답값을 가져오면, 이제 내가 사용할 좌표값만 parsing한다.
private GeocodePoint parseGeocodeResponse(TmapGeocodingResponseDto dto) {
validateGeocodeResponse(dto);
var coordinate = dto.getCoordinateInfo().getCoordinate().get(0);
String lat = coordinate.getLat();
String lon = coordinate.getLon();
if (isBlankCoordinate(lat, lon)) {
return tryParseEntrCoordinate(coordinate);
}
return new GeocodePoint(Double.parseDouble(lat), Double.parseDouble(lon));
}
만약 정상 좌표(lat, lon)가 비어 있거나 Null/Blank일 경우, 예비 좌표(latEntr, lonEntr) 등을 사용하거나 예외 처리 로직도 추가해야한다.
private GeocodePoint tryParseEntrCoordinate(TmapGeocodingResponseDto.Coordinate coordinate) {
String latEntr = coordinate.getLatEntr();
String lonEntr = coordinate.getLonEntr();
if (latEntr != null && !latEntr.isBlank() && lonEntr != null && !lonEntr.isBlank()) {
return new GeocodePoint(Double.parseDouble(latEntr), Double.parseDouble(lonEntr));
}
throw new NotFoundHandler(ErrorStatus.TMAP_COORDINATE_NOT_FOUND);
}
이렇게 하면 1단계인 지오코딩 로직은 완성이다.
이전에 지오코딩을 통해 받은 두 좌표(출발지, 도착지) 와 유저가 입력한 최대 통근 시간을 기반으로, 도로 경로 소요 시간을 계산한다.
public Integer getTransitDurationSeconds(GeocodePoint start, GeocodePoint end, int maxCommuteMinutes) {
TmapResponseDto.TmapTransitResDto response = callTransitRouteApi(start, end, maxCommuteMinutes);
return extractTransitDurationSeconds(response);
}
//타임머신 자동차 길 안내 api
private TmapResponseDto.TmapRouteResDto callDrivingRouteApi(GeocodePoint start, GeocodePoint end) {
Map<String, Object> routesInfo = buildDrivingRouteRequest(start, end);
return tmapWebClient.post()
.uri(uriBuilder -> uriBuilder
.path("tmap/routes/prediction")
.queryParam("version", 1)
.queryParam("format", "json")
.queryParam("resCoordType", coordType)
.queryParam("reqCoordType", coordType)
.queryParam("sort", "index")
.queryParam("totalValue", 2)
.build())
.bodyValue(Map.of("routesInfo", routesInfo))
.retrieve()
.onStatus(s -> s.is4xxClientError() || s.is5xxServerError(),
resp -> resp.bodyToMono(String.class)
.defaultIfEmpty("")
.map(body -> new BadRequestHandler(ErrorStatus.TMAP_DRIVING_MAPPING_FAILED)))
.bodyToMono(TmapResponseDto.TmapRouteResDto.class)
.block();
}
✅타임머신 자동차 길 안내 api를 사용한 이유
기획파트와 함께 로직을 구상하면서, 주 출근 시간대인 아침 9시에 도착하는 걸 목표로, 교통 시간을 계산하고 싶었다.
그래서 자동차 경로 안내 api와 달리 타임머신 자동차 길 안내는 도착 시간을 정할 수 있어 해당 api를 사용하였다.
따라서 미리 시간대를 계산한 후에, routesInfo로 파라미터를 추가하였다. 시간대를 계산하는 로직은 다음과 같다.
private Map<String, Object> buildDrivingRouteRequest(GeocodePoint start, GeocodePoint end) {
String arrivalKst = timeFormatter.formatISOKST();
Map<String, Object> departure = Map.of(
"name", "출발지",
"lon", start.getLongitude(),
"lat", start.getLatitude(),
"depSearchFlag", "03"
);
Map<String, Object> destination = Map.of(
"name", "도착지",
"lon", end.getLongitude(),
"lat", end.getLatitude(),
"destSearchFlag", "03"
);
Map<String, Object> routesInfo = new HashMap<>();
routesInfo.put("departure", departure);
routesInfo.put("destination", destination);
routesInfo.put("predictionType", "departure");
routesInfo.put("predictionTime", arrivalKst);
routesInfo.put("trafficInfo", "Y");
routesInfo.put("searchOption", "00");
return routesInfo;
}
public String formatISOKST() {
ZonedDateTime now = ZonedDateTime.now(KST);
ZonedDateTime arrivalKst = ZonedDateTime.of(now.toLocalDate().plusDays(1), LocalTime.of(9, 0), KST);
return arrivalKst.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"));
}
api를 통해 응답값을 갖고 오면, 우리는 통근 시간만 필요하니 이를 추출한다.
private Integer extractCarDurationSeconds(TmapResponseDto.TmapRouteResDto res) {
if (res == null || res.getFeatures() == null || res.getFeatures().isEmpty()) {
throw new NotFoundHandler(ErrorStatus.TMAP_DRIVING_EMPTY);}
return res.getFeatures().stream()
.map(TmapResponseDto.TmapRouteResDto.Feature::getProperties)
.filter(Objects::nonNull)
.map(TmapResponseDto.TmapRouteResDto.Properties::getTotalTime)
.filter(Objects::nonNull)
.min(Comparator.naturalOrder())
.orElseThrow(() -> new BadRequestHandler(ErrorStatus.TMAP_DRIVING_MAPPING_FAILED));
}
이후 통근 시간으로 필터링해서 공고를 저장하면 된다.