Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 그룹 이름
Expand Down Expand Up @@ -66,24 +70,6 @@ public CustomResponse<ArticleResponseDTO.ArticlePreviewListDTO> getArticles(@Req
@RequestParam(value = "offset", defaultValue = "10") Integer offset) {
Slice<Article> 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<ArticleResponseDTO.ArticlePreviewListDTO> getArticlesByCursor(
@RequestParam(required = false) LocalDateTime lastCreatedAt,
Pageable pageable) {

List<Article> articles = articleQueryService.getArticlesByCreatedAtLessThan(lastCreatedAt, pageable);
int totalCount = (int) articleRepository.count(); // 전체 게시글 수 조회

return CustomResponse.onSuccess(ArticleResponseDTO.ArticlePreviewListDTO.from(articles, totalCount, pageable.getPageSize()));
}

/** 게시물 수정 API */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public static ArticlePreviewListDTO from(Slice<Article> articles) {

.build();
}

}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,5 +10,4 @@

public interface ArticleQueryService {
Article getArticle(Long id);
Slice<Article> getArticles(String query, Long cursor, Integer offset);
}
Slice<Article> getArticles(String query, Long cursor, Integer offset);}
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,4 @@ public Article getArticle(Long id) {
new ArticleException(ArticleErrorCode.NOT_FOUND));
}

@Override
public List<Article> getArticlesByCreatedAtLessThan(LocalDateTime createdAt, Pageable pageable) {
// 생성 날짜 기준으로 게시글 조회
return articleRepository.findByCreatedAtLessThan(createdAt, pageable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,6 @@ public CustomResponse<ReplyResponseDTO.ReplyPreviewListDTO> getReplies(@PathVari
// 페이지와 오프셋을 기반으로 특정 게시글의 댓글 목록을 조회하여 응답 DTO로 변환
Page<Reply> 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<ReplyResponseDTO.ReplyPreviewListDTO> getReplies(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {

Page<Reply> replyPage = replyQueryService.getRepliesWithPagination(page, size);

// 응답 DTO 변환 및 페이지네이션 정보 설정
ReplyResponseDTO.ReplyPreviewListDTO response = ReplyConverter.toReplyPreviewListDTO(
replyPage.getContent(),
replyPage.getSize(),
replyPage.getNumber(),
(int) replyPage.getTotalElements()
);

return CustomResponse.onSuccess(response);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -44,4 +46,5 @@ public static ReplyResponseDTO.ReplyPreviewListDTO toReplyPreviewListDTO(Page<Re
.totalPage(replies.getTotalPages())
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@
public interface ReplyQueryService {
Page<Reply> getReplies(Long articleId, Integer page, Integer offset);
Reply getReply(Long id);
Page<Reply> getRepliesWithPagination(int page, int size);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,4 @@ public Page<Reply> getReplies(Long articleId, Integer page, Integer offset) {
return replyRepository.findAllByArticleIsOrderByCreatedAtDesc(article, pageable);
}

@Override
public Page<Reply> getRepliesWithPagination(int page, int size) {
// 페이지 요청을 생성하고, 댓글을 페이지로 조회하여 반환
Pageable pageable = PageRequest.of(page, size);
return replyRepository.findAllByOrderByCreatedAtDesc(pageable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.umc7th.global.openApi;

import org.springframework.web.reactive.function.client.WebClient;

public interface OpenApiWebClient {

public WebClient getKoreanTourWebClient();

}
Original file line number Diff line number Diff line change
@@ -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();
}


}
Original file line number Diff line number Diff line change
@@ -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<OpenApiResponseDTO.SearchStayResponseListDTO> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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<SearchStayResponseDTO> item;

public static SearchStayResponseListDTO from(List<SearchStayResponseDTO> list) {
return SearchStayResponseListDTO.builder()
.item(list)
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<OpenApiResponseDTO.SearchStayResponseListDTO> 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> 중 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<OpenApiResponseDTO.SearchStayResponseDTO> 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);
}

}
7 changes: 6 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
refresh-expiration-time: 2592000000 # Milliseconds for 30 days

openapi:
baseUrl: https://api.visitkorea.or.kr
tour:
serviceKey: ${SERVICE_KEY}