diff --git a/build.gradle b/build.gradle index 75c77adf..3e0ad970 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,9 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' testCompileOnly 'org.projectlombok:lombok' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // web client + implementation 'org.springframework.boot:spring-boot-starter-webflux' } tasks.named('test') { diff --git a/src/main/java/com/wayble/server/ServerApplication.java b/src/main/java/com/wayble/server/ServerApplication.java index f7a2d6c0..8259112f 100644 --- a/src/main/java/com/wayble/server/ServerApplication.java +++ b/src/main/java/com/wayble/server/ServerApplication.java @@ -1,8 +1,10 @@ package com.wayble.server; +import com.wayble.server.direction.external.tmap.TMapProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @@ -11,6 +13,7 @@ ) @EnableJpaAuditing @EnableElasticsearchRepositories(basePackages = "com.wayble.server.explore.repository") +@EnableConfigurationProperties(TMapProperties.class) public class ServerApplication { public static void main(String[] args) { diff --git a/src/main/java/com/wayble/server/common/config/WebClientConfig.java b/src/main/java/com/wayble/server/common/config/WebClientConfig.java new file mode 100644 index 00000000..afe087b9 --- /dev/null +++ b/src/main/java/com/wayble/server/common/config/WebClientConfig.java @@ -0,0 +1,27 @@ +package com.wayble.server.common.config; + +import com.wayble.server.direction.external.tmap.TMapProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +@RequiredArgsConstructor +public class WebClientConfig { + + private final TMapProperties tMapProperties; + + @Bean + public WebClient webClient() { + return WebClient.builder() + .build(); + } + + @Bean + public WebClient tMapWebClient() { + return WebClient.builder() + .baseUrl(tMapProperties.baseUrl()) + .build(); + } +} diff --git a/src/main/java/com/wayble/server/direction/controller/WalkingController.java b/src/main/java/com/wayble/server/direction/controller/WalkingController.java new file mode 100644 index 00000000..352bf691 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/controller/WalkingController.java @@ -0,0 +1,34 @@ +package com.wayble.server.direction.controller; + +import com.wayble.server.common.response.CommonResponse; +import com.wayble.server.direction.controller.swagger.WalkingSwagger; +import com.wayble.server.direction.external.tmap.dto.request.TMapRequest; +import com.wayble.server.direction.external.tmap.dto.response.TMapParsingResponse; +import com.wayble.server.direction.service.WalkingService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/directions/walking") +public class WalkingController implements WalkingSwagger { + + private final WalkingService walkingService; + + @Override + @GetMapping() + public CommonResponse callTMapApi( + @RequestParam double startX, + @RequestParam double startY, + @RequestParam double endX, + @RequestParam double endY, + @RequestParam String startName, + @RequestParam String endName + ) { + TMapRequest request = new TMapRequest(startX, startY, endX, endY, startName, endName); + return CommonResponse.success(walkingService.callTMapApi(request)); + } +} diff --git a/src/main/java/com/wayble/server/direction/controller/swagger/WalkingSwagger.java b/src/main/java/com/wayble/server/direction/controller/swagger/WalkingSwagger.java new file mode 100644 index 00000000..33ec333c --- /dev/null +++ b/src/main/java/com/wayble/server/direction/controller/swagger/WalkingSwagger.java @@ -0,0 +1,31 @@ +package com.wayble.server.direction.controller.swagger; + +import com.wayble.server.common.response.CommonResponse; +import com.wayble.server.direction.external.tmap.dto.response.TMapParsingResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RequestParam; + +@Tag(name = "[도보]", description = "도보 길찾기 관련 API") +public interface WalkingSwagger { + @Operation( + summary = "도보 최적 경로 길찾기 API", + description = "T MAP API를 호출하여 도보 최적 경로 길찾기를 진행합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "T MAP API 호출이 성공적으로 실행되었습니다." + ) + }) + CommonResponse callTMapApi( + @RequestParam double startX, + @RequestParam double startY, + @RequestParam double endX, + @RequestParam double endY, + @RequestParam String startName, + @RequestParam String endName + ); +} diff --git a/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java b/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java new file mode 100644 index 00000000..a37c6267 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java @@ -0,0 +1,17 @@ +package com.wayble.server.direction.exception; + +import com.wayble.server.common.exception.ErrorCase; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum WalkingErrorCase implements ErrorCase { + + T_MAP_API_FAILED(500, 8001, "T MAP API 호출에 실패했습니다."), + ; + + private final Integer httpStatusCode; + private final Integer errorCode; + private final String message; +} diff --git a/src/main/java/com/wayble/server/direction/external/tmap/TMapClient.java b/src/main/java/com/wayble/server/direction/external/tmap/TMapClient.java new file mode 100644 index 00000000..de82b483 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/tmap/TMapClient.java @@ -0,0 +1,27 @@ +package com.wayble.server.direction.external.tmap; + +import com.wayble.server.direction.external.tmap.dto.request.TMapRequest; +import com.wayble.server.direction.external.tmap.dto.response.TMapResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +@Component +@RequiredArgsConstructor +public class TMapClient { + + private final WebClient tMapWebClient; + private final TMapProperties tMapProperties; + + public TMapResponse response(TMapRequest request) { + return tMapWebClient.post() + .uri(tMapProperties.baseUrl()) + .header("appKey", tMapProperties.secretKey()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .bodyToMono(TMapResponse.class) + .block(); + } +} diff --git a/src/main/java/com/wayble/server/direction/external/tmap/TMapProperties.java b/src/main/java/com/wayble/server/direction/external/tmap/TMapProperties.java new file mode 100644 index 00000000..16589a2c --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/tmap/TMapProperties.java @@ -0,0 +1,10 @@ +package com.wayble.server.direction.external.tmap; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "tmap") +public record TMapProperties( + String secretKey, + String baseUrl +) { +} diff --git a/src/main/java/com/wayble/server/direction/external/tmap/dto/request/TMapRequest.java b/src/main/java/com/wayble/server/direction/external/tmap/dto/request/TMapRequest.java new file mode 100644 index 00000000..a10f9d12 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/tmap/dto/request/TMapRequest.java @@ -0,0 +1,14 @@ +package com.wayble.server.direction.external.tmap.dto.request; + +import lombok.Builder; + +@Builder +public record TMapRequest( + double startX, + double startY, + double endX, + double endY, + String startName, + String endName +) { +} diff --git a/src/main/java/com/wayble/server/direction/external/tmap/dto/response/TMapParsingResponse.java b/src/main/java/com/wayble/server/direction/external/tmap/dto/response/TMapParsingResponse.java new file mode 100644 index 00000000..028b06b9 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/tmap/dto/response/TMapParsingResponse.java @@ -0,0 +1,74 @@ +package com.wayble.server.direction.external.tmap.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.util.List; + +@Builder +@Schema(description = "도보 최적 경로 T MAP API") +public record TMapParsingResponse( + @Schema(description = "총 거리", example = "909") + int totalDistance, + + @Schema(description = "총 소요 시간", example = "676") + int totalTime, + + @Schema( + description = "경로 단계", + example = "[{\"type\":\"point\",\"name\":\"강남역\",\"description\":\"보행자도로를 따라 32m 이동\"}]" + ) + List steps +) { + + @Schema(description = "경로 단계") + public record Step( + @Schema(description = "단계 타입 (point - 지점, line - 도로)", example = "point") + String type, + + @Schema(description = "지점 또는 도로명", example = "강남역") + String name, + + @Schema( + description = "설명", + example = "강남역에서 좌측 횡단보도를 건넌 후 보행자도로를 따라 32m 이동" + ) + String description, + + @Schema( + description = "해당 포인트 좌표 (type - point)", + example = "{\"longitude\":127.0241571,\"latitude\":37.5037355}" + ) + Coordinate coordinate, + + @Schema( + description = "좌표 리스트 (type - line)", + example = "[{\"longitude\":127.0241571,\"latitude\":37.5037355},{\"longitude\":127.0315678,\"latitude\":37.5067709}]" + ) + List coordinates, + + @Schema(description = "턴 타입", example = "212") + Integer turnType, + + @Schema( + description = "포인트 타입 (SP - 출발, EP - 도착, GP - 경유지)", + example = "SP" + ) + String pointType, + + @Schema(description = "해당 구간 거리", example = "32") + Integer distance, + + @Schema(description = "해당 구간 소요 시간", example = "21") + Integer time + ) {} + + @Schema(description = "좌표") + public record Coordinate( + @Schema(description = "경도", example = "127.02415714643489") + double longitude, + + @Schema(description = "위도", example = "37.503735591581") + double latitude + ) {} +} diff --git a/src/main/java/com/wayble/server/direction/external/tmap/dto/response/TMapResponse.java b/src/main/java/com/wayble/server/direction/external/tmap/dto/response/TMapResponse.java new file mode 100644 index 00000000..3d8d4ff9 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/tmap/dto/response/TMapResponse.java @@ -0,0 +1,43 @@ +package com.wayble.server.direction.external.tmap.dto.response; + +import java.util.List; + +public record TMapResponse( + String type, + List features +) { + + public record Feature( + String type, + Geometry geometry, + Properties properties + ) {} + + public record Geometry( + String type, + List coordinates + ) {} + + public record Properties( + Integer totalDistance, + Integer totalTime, + Integer index, + Integer pointIndex, + String name, + String description, + String direction, + String nearPoiName, + String nearPoiX, + String nearPoiY, + String intersectionName, + String facilityType, + String facilityName, + Integer turnType, + String pointType, + Integer distance, + Integer time, + Integer roadType, + Integer categoryRoadType, + Integer lineIndex + ) {} +} diff --git a/src/main/java/com/wayble/server/direction/external/tmap/mapper/TMapMapper.java b/src/main/java/com/wayble/server/direction/external/tmap/mapper/TMapMapper.java new file mode 100644 index 00000000..2111d396 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/external/tmap/mapper/TMapMapper.java @@ -0,0 +1,85 @@ +package com.wayble.server.direction.external.tmap.mapper; + +import com.wayble.server.direction.external.tmap.dto.response.TMapParsingResponse; +import com.wayble.server.direction.external.tmap.dto.response.TMapResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class TMapMapper { + + private static final String GEOMETRY_TYPE_POINT = "Point"; + private static final String GEOMETRY_TYPE_LINESTRING = "LineString"; + private static final String POINT_TYPE_START = "SP"; + private static final String STEP_TYPE_POINT = "point"; + private static final String STEP_TYPE_LINE = "line"; + + public TMapParsingResponse parseResponse(TMapResponse response) { + List steps = new ArrayList<>(); + int totalDistance = 0; + int totalTime = 0; + + for (TMapResponse.Feature feature : response.features()) { + TMapResponse.Geometry geometry = feature.geometry(); + TMapResponse.Properties properties = feature.properties(); + + // 건물일 경우 + if (GEOMETRY_TYPE_POINT.equalsIgnoreCase(geometry.type())) { + List coordinates = geometry.coordinates(); + + if (coordinates.size() >= 2) { + double longitude = ((Number) coordinates.get(0)).doubleValue(); + double latitude = ((Number) coordinates.get(1)).doubleValue(); + + // 출발지일 경우 + if (POINT_TYPE_START.equals(properties.pointType())) { + totalDistance = properties.totalDistance() != null ? properties.totalDistance() : 0; + totalTime = properties.totalTime() != null ? properties.totalTime() : 0; + } + + TMapParsingResponse.Step step = new TMapParsingResponse.Step( + STEP_TYPE_POINT, + properties.name(), + properties.description(), + new TMapParsingResponse.Coordinate(longitude, latitude), + null, + properties.turnType(), + properties.pointType(), + null, + null + ); + steps.add(step); + } + // 도로일 경우 + } else if (GEOMETRY_TYPE_LINESTRING.equalsIgnoreCase(geometry.type())) { + List coordinates = new ArrayList<>(); + + for (Object object : geometry.coordinates()) { + if (object instanceof List pointList && pointList.size() >= 2) { + double longitude = ((Number) pointList.get(0)).doubleValue(); + double latitude = ((Number) pointList.get(1)).doubleValue(); + coordinates.add(new TMapParsingResponse.Coordinate(longitude, latitude)); + } + } + + TMapParsingResponse.Step step = new TMapParsingResponse.Step( + STEP_TYPE_LINE, + properties.name(), + properties.description(), + null, + coordinates, + null, + null, + properties.distance(), + properties.time() + ); + steps.add(step); + } + } + return new TMapParsingResponse(totalDistance, totalTime, steps); + } +} diff --git a/src/main/java/com/wayble/server/direction/service/WalkingService.java b/src/main/java/com/wayble/server/direction/service/WalkingService.java new file mode 100644 index 00000000..c7c92494 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/service/WalkingService.java @@ -0,0 +1,32 @@ +package com.wayble.server.direction.service; + +import com.wayble.server.common.exception.ApplicationException; +import com.wayble.server.direction.exception.WalkingErrorCase; +import com.wayble.server.direction.external.tmap.TMapClient; +import com.wayble.server.direction.external.tmap.dto.request.TMapRequest; +import com.wayble.server.direction.external.tmap.dto.response.TMapParsingResponse; +import com.wayble.server.direction.external.tmap.dto.response.TMapResponse; +import com.wayble.server.direction.external.tmap.mapper.TMapMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class WalkingService { + + private final TMapClient tMapClient; + private final TMapMapper tMapMapper; + + public TMapParsingResponse callTMapApi(TMapRequest request) { + try { + TMapResponse response = tMapClient.response(request); + log.info("🎉 T MAP API 호출 성공"); + return tMapMapper.parseResponse(response); + } catch (Exception e) { + log.error("🚨 T MAP API 호출 실패: {}", e.getMessage()); + throw new ApplicationException(WalkingErrorCase.T_MAP_API_FAILED); + } + } +}