From 38f34b717f55eca148aa4bcab7715ed204070280 Mon Sep 17 00:00:00 2001 From: leesj Date: Tue, 19 Nov 2024 13:01:03 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[Feat]=20OPEN=20API=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../domain/openApi/OpenApiWebClient.java | 9 ++ .../domain/openApi/OpenApiWebClientImpl.java | 44 ++++++++++ .../openApi/controller/OpenApiController.java | 25 ++++++ .../openApi/dto/OpenApiResponseDTO.java | 46 +++++++++++ .../openApi/exception/OpenApiErrorCode.java | 24 ++++++ .../openApi/exception/OpenApiException.java | 10 +++ .../openApi/service/OpenApiQueryService.java | 8 ++ .../service/OpenApiQueryServiceImpl.java | 82 +++++++++++++++++++ src/main/resources/application.yml | 6 +- 10 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/umc7th/domain/openApi/OpenApiWebClient.java create mode 100644 src/main/java/com/example/umc7th/domain/openApi/OpenApiWebClientImpl.java create mode 100644 src/main/java/com/example/umc7th/domain/openApi/controller/OpenApiController.java create mode 100644 src/main/java/com/example/umc7th/domain/openApi/dto/OpenApiResponseDTO.java create mode 100644 src/main/java/com/example/umc7th/domain/openApi/exception/OpenApiErrorCode.java create mode 100644 src/main/java/com/example/umc7th/domain/openApi/exception/OpenApiException.java create mode 100644 src/main/java/com/example/umc7th/domain/openApi/service/OpenApiQueryService.java create mode 100644 src/main/java/com/example/umc7th/domain/openApi/service/OpenApiQueryServiceImpl.java diff --git a/build.gradle b/build.gradle index a222006..3d9f6ce 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ dependencies { // OAuth2 login implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - // WebClient + // WebClient(WebFlux) implementation 'org.springframework.boot:spring-boot-starter-webflux' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/example/umc7th/domain/openApi/OpenApiWebClient.java b/src/main/java/com/example/umc7th/domain/openApi/OpenApiWebClient.java new file mode 100644 index 0000000..12c5ab4 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/OpenApiWebClient.java @@ -0,0 +1,9 @@ +package com.example.umc7th.domain.openApi; + +import org.springframework.web.reactive.function.client.WebClient; + +public interface OpenApiWebClient { + // 한국 관광정보를 가져올 수 있는 WebClient를 반환하는 메소드 정의 + WebClient getKoreanTourWebClient(String language); + // 아래에 메소드를 추가하면서 확장해나갈 수 있겠죠? +} diff --git a/src/main/java/com/example/umc7th/domain/openApi/OpenApiWebClientImpl.java b/src/main/java/com/example/umc7th/domain/openApi/OpenApiWebClientImpl.java new file mode 100644 index 0000000..d4fe8d2 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/OpenApiWebClientImpl.java @@ -0,0 +1,44 @@ +package com.example.umc7th.domain.openApi; + +import com.example.umc7th.domain.openApi.exception.OpenApiErrorCode; +import com.example.umc7th.domain.openApi.exception.OpenApiException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import org.springframework.web.util.DefaultUriBuilderFactory; +import reactor.netty.http.client.HttpClient; +import java.time.Duration; + +@Component +@Slf4j +public class OpenApiWebClientImpl implements OpenApiWebClient { + + @Override + public WebClient getKoreanTourWebClient(String language) { + if (language.equals("korean")) { + return getWebClient("https://apis.data.go.kr/B551011/KorService1"); + } + else if (language.equals("english")) { + return getWebClient("https://apis.data.go.kr/B551011/EngService1"); + } + else { + throw new OpenApiException(OpenApiErrorCode.UNSUPPORTED_LANGUAGE); + } + } + + private WebClient getWebClient(String baseUrl) { + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofMillis(20000)); + + return WebClient.builder() + .baseUrl(baseUrl) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .filter((request, next) -> { + log.info("Web Client Request: "+ request.url()); + return next.exchange(request); + }) + .build(); + } +} diff --git a/src/main/java/com/example/umc7th/domain/openApi/controller/OpenApiController.java b/src/main/java/com/example/umc7th/domain/openApi/controller/OpenApiController.java new file mode 100644 index 0000000..79d2e50 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/controller/OpenApiController.java @@ -0,0 +1,25 @@ +package com.example.umc7th.domain.openApi.controller; + +import com.example.umc7th.domain.openApi.dto.OpenApiResponseDTO; +import com.example.umc7th.domain.openApi.service.OpenApiQueryService; +import com.example.umc7th.global.apiPayload.CustomResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class OpenApiController { + + private final OpenApiQueryService openApiQueryService; + + @GetMapping("/searchStay") + public CustomResponse controller( + @RequestParam(name = "arrange", defaultValue = "A") String arrange, + @RequestParam(name = "page", defaultValue = "1") int page, + @RequestParam(name = "offset", defaultValue = "10") int offset) { + + return CustomResponse.onSuccess(openApiQueryService.searchStay(arrange, page, offset)); + } +} diff --git a/src/main/java/com/example/umc7th/domain/openApi/dto/OpenApiResponseDTO.java b/src/main/java/com/example/umc7th/domain/openApi/dto/OpenApiResponseDTO.java new file mode 100644 index 0000000..f4dabe8 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/dto/OpenApiResponseDTO.java @@ -0,0 +1,46 @@ +package com.example.umc7th.domain.openApi.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +public class OpenApiResponseDTO { + + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Getter + // 해당 어노테이션으로 json 값을 Parsing할 때 필드가 없는 경우 무시하여 에러가 터지는 것을 방지, 한번 없이 돌려보시면 이해가 더 잘 되실겁니다. + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SearchStayResponseDTO { + // 아래 변수는 Api 명세서의 응답을 보고 그대로 받고 싶은 값들만 똑같은 이름으로 만들어줍니다. + private String addr1; + private String title; + private String tel; + private String contentid; + private String contenttypeid; + private String createdtime; + private String firstimage; + private String firstimage2; + private String mapx; + private String mapy; + } + + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Getter + public static class SearchStayResponseListDTO { + private List item; + + public static SearchStayResponseListDTO from(List list) { + return SearchStayResponseListDTO.builder() + .item(list) + .build(); + } + } +} diff --git a/src/main/java/com/example/umc7th/domain/openApi/exception/OpenApiErrorCode.java b/src/main/java/com/example/umc7th/domain/openApi/exception/OpenApiErrorCode.java new file mode 100644 index 0000000..081ee9a --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/exception/OpenApiErrorCode.java @@ -0,0 +1,24 @@ +package com.example.umc7th.domain.openApi.exception; + +import com.example.umc7th.global.apiPayload.CustomResponse; +import com.example.umc7th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +@Getter +public enum OpenApiErrorCode implements BaseErrorCode { + + UNSUPPORTED_LANGUAGE(HttpStatus.NOT_FOUND, "OPENAPI400", "지원되지 않는 언어입니다."); + + private final HttpStatus status; + private final String code; + private final String message; + + + @Override + public CustomResponse getErrorResponse() { + return null; + } +} diff --git a/src/main/java/com/example/umc7th/domain/openApi/exception/OpenApiException.java b/src/main/java/com/example/umc7th/domain/openApi/exception/OpenApiException.java new file mode 100644 index 0000000..a03fa0a --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/exception/OpenApiException.java @@ -0,0 +1,10 @@ +package com.example.umc7th.domain.openApi.exception; + +import com.example.umc7th.global.apiPayload.code.BaseErrorCode; +import com.example.umc7th.global.apiPayload.exception.GeneralException; + +public class OpenApiException extends GeneralException { + public OpenApiException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/example/umc7th/domain/openApi/service/OpenApiQueryService.java b/src/main/java/com/example/umc7th/domain/openApi/service/OpenApiQueryService.java new file mode 100644 index 0000000..f32f1e9 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/service/OpenApiQueryService.java @@ -0,0 +1,8 @@ +package com.example.umc7th.domain.openApi.service; + +import com.example.umc7th.domain.openApi.dto.OpenApiResponseDTO; + +public interface OpenApiQueryService { + + OpenApiResponseDTO.SearchStayResponseListDTO searchStay(String arrange, int page, int offset); +} diff --git a/src/main/java/com/example/umc7th/domain/openApi/service/OpenApiQueryServiceImpl.java b/src/main/java/com/example/umc7th/domain/openApi/service/OpenApiQueryServiceImpl.java new file mode 100644 index 0000000..16eb65b --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/service/OpenApiQueryServiceImpl.java @@ -0,0 +1,82 @@ +package com.example.umc7th.domain.openApi.service; + + +import com.example.umc7th.domain.openApi.OpenApiWebClient; +import com.example.umc7th.domain.openApi.dto.OpenApiResponseDTO; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OpenApiQueryServiceImpl implements OpenApiQueryService { + + // WebClient를 가져오기 위한 빈 주입 + private final OpenApiWebClient openApiWebClient; + + @Value("${openapi.tour.serviceKey}") + private String serviceKey; // 인증 키 + + @Override + public OpenApiResponseDTO.SearchStayResponseListDTO searchStay(String arrange, int page, int offset) { + // Web Client 가져오기 + WebClient webClient = openApiWebClient.getKoreanTourWebClient("korean"); + Mono mono = webClient.get() // get method 사용 + // UriBuilder를 이용하여 Endpoint와 Query Param 설정 + .uri(uri -> uri + .path("/searchStay1") + .queryParam("numOfRows", offset) + .queryParam("pageNo", page) + .queryParam("MobileOS", "ETC") + .queryParam("MobileApp", "AppTest") + .queryParam("_type", "json") + .queryParam("arrange", arrange) + .queryParam("serviceKey", serviceKey) + .build()) + // 응답을 가져오기 위한 method (.onStatus()를 이용해서 Http 상태코드에 따라 다르게 처리해줄 수 있음) + .retrieve() + // 응답에서 body만 String 타입으로 가져오기 (ResponseEntity 중 Object만 String 형식으로 가져오기) + .bodyToMono(String.class) + // String 값을 메소드로 매핑하여 OpenApiResponseDTO.SearchStayResponseListDTO로 변경하기 + .map(this::toSearchStayResponseListDTO) + // 에러가 발생한 경우 log를 찍도록 + .doOnError(e -> log.error("Open Api 에러 발생: " + e.getMessage())) + // 성공한 경우에도 log를 찍도록 + .doOnSuccess(s -> log.info("관광 정보를 가져오는데 성공했습니다.")) + ; + + // block()을 사용해서 응답을 바로 가져오도록 + return mono.block(); + } + + private OpenApiResponseDTO.SearchStayResponseListDTO toSearchStayResponseListDTO(String response) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + // item으로 담을 list 선언 + List list = new ArrayList<>(); + // JsonNode 형식으로 응답을 읽고 item이 담긴 배열만 읽고 싶기에 item이 있는 배열까지 들어가기 + JsonNode jsonNode = objectMapper.readTree(response).path("response").path("body").path("items").path("item"); + // item 하나씩 처리 + for (JsonNode node : jsonNode) { + // item 하나씩 읽어서 OpenApiResponseDTO.SearchStayResponseDTO로 변경해서 List에 추가 + list.add(objectMapper.convertValue(node, OpenApiResponseDTO.SearchStayResponseDTO.class)); + } + // 응답을 만들어서 반환 + return OpenApiResponseDTO.SearchStayResponseListDTO.from(list); + } catch (Exception e) { + // 에러 처리 + e.fillInStackTrace(); + } + return OpenApiResponseDTO.SearchStayResponseListDTO.from(null); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3cea533..de82bd4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,4 +39,8 @@ Jwt: secret: ${JWT_SECRET} token: access-expiration-time: 3600000 # Milliseconds for 1 hour - refresh-expiration-time: 2592000000 # Milliseconds for 30 days \ No newline at end of file + refresh-expiration-time: 2592000000 # Milliseconds for 30 days + +openapi: + tour: + serviceKey: ${SERVICE_KEY} \ No newline at end of file