From a58e63e741615879e430d05c815c752506173cc0 Mon Sep 17 00:00:00 2001 From: 1026hz <1026hzz@gmail.com> Date: Thu, 21 Nov 2024 13:24:24 +0900 Subject: [PATCH] =?UTF-8?q?[FEATURE]=20OpenApi=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../article/controller/ArticleController.java | 22 +---- .../article/dto/ArticleResponseDTO.java | 1 + .../service/query/ArticleQueryService.java | 4 +- .../query/ArticleQueryServiceImpl.java | 5 -- .../reply/controller/ReplyController.java | 25 ------ .../reply/converter/ReplyConverter.java | 3 + .../service/query/ReplyQueryService.java | 1 - .../service/query/ReplyQueryServiceImpl.java | 6 -- .../global/openApi/OpenApiWebClient.java | 9 +++ .../global/openApi/OpenApiWebClientImpl.java | 41 ++++++++++ .../openApi/controller/OpenApiController.java | 23 ++++++ .../openApi/dto/OpenApiResponseDTO.java | 46 +++++++++++ .../openApi/service/OpenApiQueryService.java | 8 ++ .../service/OpenApiQueryServiceImpl.java | 81 +++++++++++++++++++ src/main/resources/application.yml | 7 +- 16 files changed, 227 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/example/umc7th/global/openApi/OpenApiWebClient.java create mode 100644 src/main/java/com/example/umc7th/global/openApi/OpenApiWebClientImpl.java create mode 100644 src/main/java/com/example/umc7th/global/openApi/controller/OpenApiController.java create mode 100644 src/main/java/com/example/umc7th/global/openApi/dto/OpenApiResponseDTO.java create mode 100644 src/main/java/com/example/umc7th/global/openApi/service/OpenApiQueryService.java create mode 100644 src/main/java/com/example/umc7th/global/openApi/service/OpenApiQueryServiceImpl.java diff --git a/build.gradle b/build.gradle index 24607afb..aebf16dd 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.2' implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' + + // WebFlux + implementation 'org.springframework.boot:spring-boot-starter-webflux' } tasks.named('test') { diff --git a/src/main/java/com/example/umc7th/domain/article/controller/ArticleController.java b/src/main/java/com/example/umc7th/domain/article/controller/ArticleController.java index 8ef8e5b7..78e1d2ba 100644 --- a/src/main/java/com/example/umc7th/domain/article/controller/ArticleController.java +++ b/src/main/java/com/example/umc7th/domain/article/controller/ArticleController.java @@ -13,9 +13,13 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; +import java.util.List; + @RestController @RequiredArgsConstructor @Tag(name = "게시글 API") // Swagger에 표시될 API 그룹 이름 @@ -66,24 +70,6 @@ public CustomResponse getArticles(@Req @RequestParam(value = "offset", defaultValue = "10") Integer offset) { Slice
articles = articleQueryService.getArticles(query, cursor, offset); return CustomResponse.onSuccess(ArticleResponseDTO.ArticlePreviewListDTO.from(articles)); - }*/ - - /** - * 커서 기반 게시글 조회 API - * @param lastCreatedAt (이전 게시글의 생성 날짜) - * @param pageable (페이지네이션 정보) - * @return 생성 날짜 기준으로 게시글을 조회한 후 성공 응답을 CustomResponse 형태로 반환 - */ - @GetMapping("/articles/cursor") - @Operation(summary = "커서 기반 게시글 조회 API", description = "생성 날짜 기준으로 게시글 조회하는 API") - public CustomResponse getArticlesByCursor( - @RequestParam(required = false) LocalDateTime lastCreatedAt, - Pageable pageable) { - - List
articles = articleQueryService.getArticlesByCreatedAtLessThan(lastCreatedAt, pageable); - int totalCount = (int) articleRepository.count(); // 전체 게시글 수 조회 - - return CustomResponse.onSuccess(ArticleResponseDTO.ArticlePreviewListDTO.from(articles, totalCount, pageable.getPageSize())); } /** 게시물 수정 API */ diff --git a/src/main/java/com/example/umc7th/domain/article/dto/ArticleResponseDTO.java b/src/main/java/com/example/umc7th/domain/article/dto/ArticleResponseDTO.java index 96ecdf09..9adf0d86 100644 --- a/src/main/java/com/example/umc7th/domain/article/dto/ArticleResponseDTO.java +++ b/src/main/java/com/example/umc7th/domain/article/dto/ArticleResponseDTO.java @@ -89,6 +89,7 @@ public static ArticlePreviewListDTO from(Slice
articles) { .build(); } + } } diff --git a/src/main/java/com/example/umc7th/domain/article/service/query/ArticleQueryService.java b/src/main/java/com/example/umc7th/domain/article/service/query/ArticleQueryService.java index 4498f720..b9b69c1b 100644 --- a/src/main/java/com/example/umc7th/domain/article/service/query/ArticleQueryService.java +++ b/src/main/java/com/example/umc7th/domain/article/service/query/ArticleQueryService.java @@ -1,6 +1,7 @@ package com.example.umc7th.domain.article.service.query; import com.example.umc7th.domain.article.entity.Article; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import java.time.LocalDateTime; @@ -9,5 +10,4 @@ public interface ArticleQueryService { Article getArticle(Long id); - Slice
getArticles(String query, Long cursor, Integer offset); -} + Slice
getArticles(String query, Long cursor, Integer offset);} diff --git a/src/main/java/com/example/umc7th/domain/article/service/query/ArticleQueryServiceImpl.java b/src/main/java/com/example/umc7th/domain/article/service/query/ArticleQueryServiceImpl.java index e13dad16..f13d0149 100644 --- a/src/main/java/com/example/umc7th/domain/article/service/query/ArticleQueryServiceImpl.java +++ b/src/main/java/com/example/umc7th/domain/article/service/query/ArticleQueryServiceImpl.java @@ -58,9 +58,4 @@ public Article getArticle(Long id) { new ArticleException(ArticleErrorCode.NOT_FOUND)); } - @Override - public List
getArticlesByCreatedAtLessThan(LocalDateTime createdAt, Pageable pageable) { - // 생성 날짜 기준으로 게시글 조회 - return articleRepository.findByCreatedAtLessThan(createdAt, pageable); - } } \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/domain/reply/controller/ReplyController.java b/src/main/java/com/example/umc7th/domain/reply/controller/ReplyController.java index dd8a09c9..c3217939 100644 --- a/src/main/java/com/example/umc7th/domain/reply/controller/ReplyController.java +++ b/src/main/java/com/example/umc7th/domain/reply/controller/ReplyController.java @@ -41,31 +41,6 @@ public CustomResponse getReplies(@PathVari // 페이지와 오프셋을 기반으로 특정 게시글의 댓글 목록을 조회하여 응답 DTO로 변환 Page replies = replyQueryService.getReplies(articleId, page, offset); return CustomResponse.onSuccess(ReplyConverter.toReplyPreviewListDTO(replies)); - }*/ - - /** - * 댓글 전체 조회 API (Offset 기반 페이지네이션) - * @param page 페이지 번호 - * @param size 한 페이지당 댓글 수 - * @return 조회된 페이지네이션 댓글 목록을 CustomResponse로 반환 - */ - @GetMapping - @Operation(summary = "댓글 전체 조회 API", description = "Offset 기반 페이지네이션을 통해 댓글 전체를 조회하는 API") - public CustomResponse getReplies( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { - - Page replyPage = replyQueryService.getRepliesWithPagination(page, size); - - // 응답 DTO 변환 및 페이지네이션 정보 설정 - ReplyResponseDTO.ReplyPreviewListDTO response = ReplyConverter.toReplyPreviewListDTO( - replyPage.getContent(), - replyPage.getSize(), - replyPage.getNumber(), - (int) replyPage.getTotalElements() - ); - - return CustomResponse.onSuccess(response); } diff --git a/src/main/java/com/example/umc7th/domain/reply/converter/ReplyConverter.java b/src/main/java/com/example/umc7th/domain/reply/converter/ReplyConverter.java index 647aebd3..5ef992b7 100644 --- a/src/main/java/com/example/umc7th/domain/reply/converter/ReplyConverter.java +++ b/src/main/java/com/example/umc7th/domain/reply/converter/ReplyConverter.java @@ -6,6 +6,8 @@ import com.example.umc7th.domain.reply.entity.Reply; import org.springframework.data.domain.Page; +import java.util.List; + /** Reply 엔티티와 DTO 간의 변환을 담당하여, 서비스 로직에서 DTO를 생성하고 반환할 수 있도록 도와줌 */ public class ReplyConverter { @@ -44,4 +46,5 @@ public static ReplyResponseDTO.ReplyPreviewListDTO toReplyPreviewListDTO(Page getReplies(Long articleId, Integer page, Integer offset); Reply getReply(Long id); - Page getRepliesWithPagination(int page, int size); } \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/domain/reply/service/query/ReplyQueryServiceImpl.java b/src/main/java/com/example/umc7th/domain/reply/service/query/ReplyQueryServiceImpl.java index ece6c93e..dc5ecc12 100644 --- a/src/main/java/com/example/umc7th/domain/reply/service/query/ReplyQueryServiceImpl.java +++ b/src/main/java/com/example/umc7th/domain/reply/service/query/ReplyQueryServiceImpl.java @@ -47,10 +47,4 @@ public Page getReplies(Long articleId, Integer page, Integer offset) { return replyRepository.findAllByArticleIsOrderByCreatedAtDesc(article, pageable); } - @Override - public Page getRepliesWithPagination(int page, int size) { - // 페이지 요청을 생성하고, 댓글을 페이지로 조회하여 반환 - Pageable pageable = PageRequest.of(page, size); - return replyRepository.findAllByOrderByCreatedAtDesc(pageable); - } } \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/global/openApi/OpenApiWebClient.java b/src/main/java/com/example/umc7th/global/openApi/OpenApiWebClient.java new file mode 100644 index 00000000..12ab8c93 --- /dev/null +++ b/src/main/java/com/example/umc7th/global/openApi/OpenApiWebClient.java @@ -0,0 +1,9 @@ +package com.example.umc7th.global.openApi; + +import org.springframework.web.reactive.function.client.WebClient; + +public interface OpenApiWebClient { + + public WebClient getKoreanTourWebClient(); + +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/global/openApi/OpenApiWebClientImpl.java b/src/main/java/com/example/umc7th/global/openApi/OpenApiWebClientImpl.java new file mode 100644 index 00000000..fddebf8e --- /dev/null +++ b/src/main/java/com/example/umc7th/global/openApi/OpenApiWebClientImpl.java @@ -0,0 +1,41 @@ +package com.example.umc7th.global.openApi; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +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 { + @Value("${openapi.baseUrl}") + private String baseUrl; + @Override + public WebClient getKoreanTourWebClient() { + // 연결 설정 + // TCP 연결 시 응답 시간 초과 값을 설정 + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofMillis(20000)); + + // Uri를 build하는 factory 생성 (baseUrl을 WebClient 대신 여기에 포함하도록) + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl); + // Uri factory에 인코딩 모드를 NONE으로 바꾸어 인코딩하지 않도록해줍니다. + factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); + + return WebClient.builder() + .uriBuilderFactory(factory) + .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/global/openApi/controller/OpenApiController.java b/src/main/java/com/example/umc7th/global/openApi/controller/OpenApiController.java new file mode 100644 index 00000000..c5dfcfad --- /dev/null +++ b/src/main/java/com/example/umc7th/global/openApi/controller/OpenApiController.java @@ -0,0 +1,23 @@ +package com.example.umc7th.global.openApi.controller; + +import com.example.umc7th.global.apiPayload.CustomResponse; +import com.example.umc7th.global.openApi.dto.OpenApiResponseDTO; +import com.example.umc7th.global.openApi.service.OpenApiQueryService; +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)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/global/openApi/dto/OpenApiResponseDTO.java b/src/main/java/com/example/umc7th/global/openApi/dto/OpenApiResponseDTO.java new file mode 100644 index 00000000..2a3a07d4 --- /dev/null +++ b/src/main/java/com/example/umc7th/global/openApi/dto/OpenApiResponseDTO.java @@ -0,0 +1,46 @@ +package com.example.umc7th.global.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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/global/openApi/service/OpenApiQueryService.java b/src/main/java/com/example/umc7th/global/openApi/service/OpenApiQueryService.java new file mode 100644 index 00000000..b74dcef0 --- /dev/null +++ b/src/main/java/com/example/umc7th/global/openApi/service/OpenApiQueryService.java @@ -0,0 +1,8 @@ +package com.example.umc7th.global.openApi.service; + +import com.example.umc7th.global.openApi.dto.OpenApiResponseDTO; + +public interface OpenApiQueryService { + + OpenApiResponseDTO.SearchStayResponseListDTO searchStay(String arrange, int page, int offset); +} diff --git a/src/main/java/com/example/umc7th/global/openApi/service/OpenApiQueryServiceImpl.java b/src/main/java/com/example/umc7th/global/openApi/service/OpenApiQueryServiceImpl.java new file mode 100644 index 00000000..2ad54acf --- /dev/null +++ b/src/main/java/com/example/umc7th/global/openApi/service/OpenApiQueryServiceImpl.java @@ -0,0 +1,81 @@ +package com.example.umc7th.global.openApi.service; + +import com.example.umc7th.global.openApi.OpenApiWebClient; +import com.example.umc7th.global.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(); + 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); + } + +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 623164a8..2268e9ea 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,4 +15,9 @@ 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: + baseUrl: https://api.visitkorea.or.kr + tour: + serviceKey: ${SERVICE_KEY} \ No newline at end of file