Tmap API를 이용한 교통시간 계산 로직 (2)

이얏호·2025년 10월 31일

https://velog.io/@yeonho03/Tmap-API%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EA%B5%90%ED%86%B5%EC%8B%9C%EA%B0%84-%EA%B3%84%EC%82%B0-%EB%A1%9C%EC%A7%81-1

지난 번에 작성한 글을 이어서 작성하겠다.

1. 주소 -> 좌표로 변환

  • 사용자가 입력한 주소 또는 회사 주소를 Tmap의 Full-Text Geocoding API를 이용하여 좌표로 변환한다.
    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단계인 지오코딩 로직은 완성이다.

2. 통근 시간 계산 - 교통수단이 자동차일 경우

이전에 지오코딩을 통해 받은 두 좌표(출발지, 도착지) 와 유저가 입력한 최대 통근 시간을 기반으로, 도로 경로 소요 시간을 계산한다.

    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));
    }

이후 통근 시간으로 필터링해서 공고를 저장하면 된다.

0개의 댓글