diff --git a/src/main/java/com/example/umc7th/domain/member/exception/MemberErrorCode.java b/src/main/java/com/example/umc7th/domain/member/exception/MemberErrorCode.java index 117cfed..382af70 100644 --- a/src/main/java/com/example/umc7th/domain/member/exception/MemberErrorCode.java +++ b/src/main/java/com/example/umc7th/domain/member/exception/MemberErrorCode.java @@ -9,7 +9,7 @@ @AllArgsConstructor public enum MemberErrorCode implements BaseErrorCode { - NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE404_0", "해당 게시글을 찾을 수 없습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_0", "해당 사용자를 찾을 수 없습니다."), OAUTH_USER_INFO_FAIL(HttpStatus.NOT_FOUND, "MEMBER404_2", "사용자 정보 조회 실패"), OAUTH_TOKEN_FAIL(HttpStatus.BAD_REQUEST, "MEMBER400_1", "토큰 변환 실패"), OAUTH_EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER400_2", "이메일 정보를 찾을 수 없습니다."), diff --git a/src/main/java/com/example/umc7th/domain/openApi/component/OpenApiWebClient.java b/src/main/java/com/example/umc7th/domain/openApi/component/OpenApiWebClient.java new file mode 100644 index 0000000..fb0cb19 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/component/OpenApiWebClient.java @@ -0,0 +1,9 @@ +package com.example.umc7th.domain.openApi.component; + +import org.springframework.web.reactive.function.client.WebClient; + +public interface OpenApiWebClient { + // 한국 관광정보를 가져올 수 있는 WebClient를 반환하는 메소드 정의 + WebClient getTourWebClient(String language); + // 아래에 메소드를 추가하면서 확장해나갈 수 있겠죠? +} diff --git a/src/main/java/com/example/umc7th/domain/openApi/component/OpenApiWebClientImpl.java b/src/main/java/com/example/umc7th/domain/openApi/component/OpenApiWebClientImpl.java new file mode 100644 index 0000000..bd8869b --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/component/OpenApiWebClientImpl.java @@ -0,0 +1,55 @@ +package com.example.umc7th.domain.openApi.component; + +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 getTourWebClient(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); + } + } + + // 영문 API를 추가한 경우 + // @Override + // public WebClient getEnglishTourWebClient() { + // return getWebClient("https://apis.data.go.kr/B551011/EngService1"); + // } + + private WebClient getWebClient(String baseUrl) { + 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/domain/openApi/controller/OpenApiController.java b/src/main/java/com/example/umc7th/domain/openApi/controller/OpenApiController.java new file mode 100644 index 0000000..afe844a --- /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.response.OpenApiResDto; +import com.example.umc7th.domain.openApi.service.query.OpenApiQueryService; +import com.example.umc7th.global.apiPayload.CustomResponse; +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") +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/response/OpenApiResDto.java b/src/main/java/com/example/umc7th/domain/openApi/dto/response/OpenApiResDto.java new file mode 100644 index 0000000..11aa489 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/dto/response/OpenApiResDto.java @@ -0,0 +1,32 @@ +package com.example.umc7th.domain.openApi.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Builder; + +import java.util.List; + +public class OpenApiResDto { + + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) // JSON 파싱 시 필드가 없는 경우 무시 + public record SearchStayResponseDto( + String addr1, + String title, + String tel, + String contentid, + String contenttypeid, + String createdtime, + String firstimage, + String firstimage2, + String mapx, + String mapy + ) {} + + public record SearchStayResponseListDto( + List item + ) { + public static SearchStayResponseListDto from(List list) { + return new SearchStayResponseListDto(list); + } + } +} 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..cad7d18 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/exception/OpenApiErrorCode.java @@ -0,0 +1,16 @@ +package com.example.umc7th.domain.openApi.exception; + +import com.example.umc7th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum OpenApiErrorCode implements BaseErrorCode { + UNSUPPORTED_LANGUAGE(HttpStatus.NOT_FOUND, "OPENAPI400", "제공하지 않는 언어입니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} 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..afc18ce --- /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.exception.GeneralException; + +public class OpenApiException extends GeneralException { + public OpenApiException(OpenApiErrorCode code){ + super(code); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/domain/openApi/service/query/OpenApiQueryService.java b/src/main/java/com/example/umc7th/domain/openApi/service/query/OpenApiQueryService.java new file mode 100644 index 0000000..e2bec5c --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/service/query/OpenApiQueryService.java @@ -0,0 +1,7 @@ +package com.example.umc7th.domain.openApi.service.query; + +import com.example.umc7th.domain.openApi.dto.response.OpenApiResDto; + +public interface OpenApiQueryService { + OpenApiResDto.SearchStayResponseListDto searchStay(String arrange, int page, int offset); +} diff --git a/src/main/java/com/example/umc7th/domain/openApi/service/query/OpenApiQueryServiceImpl.java b/src/main/java/com/example/umc7th/domain/openApi/service/query/OpenApiQueryServiceImpl.java new file mode 100644 index 0000000..938c7b2 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openApi/service/query/OpenApiQueryServiceImpl.java @@ -0,0 +1,79 @@ +package com.example.umc7th.domain.openApi.service.query; + +import com.example.umc7th.domain.openApi.component.OpenApiWebClient; +import com.example.umc7th.domain.openApi.dto.response.OpenApiResDto; +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 { + + private final OpenApiWebClient openApiWebClient; + + @Value("${openapi.tour.serviceKey}") + private String serviceKey; + + + @Override + public OpenApiResDto.SearchStayResponseListDto searchStay(String arrange, int page, int offset) { + // Web Client 가져오기 + WebClient webClient = openApiWebClient.getTourWebClient("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 OpenApiResDto.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, OpenApiResDto.SearchStayResponseDto.class)); + } + // 응답을 만들어서 반환 + return OpenApiResDto.SearchStayResponseListDto.from(list); + } catch (Exception e) { + // 에러 처리 + e.fillInStackTrace(); + } + return OpenApiResDto.SearchStayResponseListDto.from(null); + } +} diff --git a/src/main/java/com/example/umc7th/global/config/SecurityConfig.java b/src/main/java/com/example/umc7th/global/config/SecurityConfig.java index abadd8d..8cb0e12 100644 --- a/src/main/java/com/example/umc7th/global/config/SecurityConfig.java +++ b/src/main/java/com/example/umc7th/global/config/SecurityConfig.java @@ -3,6 +3,7 @@ import com.example.umc7th.domain.member.repository.MemberRepository; import com.example.umc7th.global.jwt.exception.JwtAccessDeniedHandler; import com.example.umc7th.global.jwt.exception.JwtAuthenticationEntryPoint; +import com.example.umc7th.global.jwt.filter.JwtFilter; import com.example.umc7th.global.jwt.util.JwtProvider; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -16,13 +17,14 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - private final AuthenticationConfiguration authenticationConfiguration; + private final MemberRepository memberRepository; private final JwtProvider jwtProvider; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; @@ -90,6 +92,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .accessDeniedHandler(jwtAccessDeniedHandler) ); + http.addFilterBefore( + new JwtFilter(jwtProvider, memberRepository), + UsernamePasswordAuthenticationFilter.class + ); return http.build(); } diff --git a/src/main/java/com/example/umc7th/global/config/WebConfig.java b/src/main/java/com/example/umc7th/global/config/WebConfig.java new file mode 100644 index 0000000..9cda34a --- /dev/null +++ b/src/main/java/com/example/umc7th/global/config/WebConfig.java @@ -0,0 +1,37 @@ +package com.example.umc7th.global.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; + +@Configuration +@Slf4j +public class WebConfig { + + @Bean + public WebClient webClient() { + // 연결 설정 + // TCP 연결 시 응답 시간 초과 값을 설정 + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofMillis(20000)); + + // WebClient 생성 + return WebClient.builder() + // Base URL 설정 + .baseUrl("https://apis.data.go.kr/B551011/KorService1") + // 만들었던 연결 설정 넣어주기 + .clientConnector(new ReactorClientHttpConnector(httpClient)) + // filter를 사용해서 요청을 보낼 때 로그가 찍히도록 + .filter((request, next) -> { + log.info("Web Client Request: "+ request.url()); + return next.exchange(request); + }) + // build로 객체 생성 + .build(); + } +}