diff --git a/src/main/java/ybe/mini/travelserver/global/api/TourAPIController.java b/src/main/java/ybe/mini/travelserver/global/api/TourAPIController.java deleted file mode 100644 index b88879e..0000000 --- a/src/main/java/ybe/mini/travelserver/global/api/TourAPIController.java +++ /dev/null @@ -1,46 +0,0 @@ -package ybe.mini.travelserver.global.api; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/tour-api") -public class TourAPIController { - TourAPIService tourAPIService; - - public TourAPIController(TourAPIService tourAPIService) { - this.tourAPIService = tourAPIService; - } - - @GetMapping("/search") - public ResponseEntity bringAccommodations( - @RequestParam(required = false, defaultValue = "_") String keyword, - @RequestParam(required = false, defaultValue = "1") int pageNo, - @RequestParam(required = false, defaultValue = "10") int numOfRows, - @RequestParam(required = false) String areaCode - ) { - return ResponseEntity.ok( - tourAPIService.bringAccommodationsForSearch( - pageNo, - numOfRows, - keyword, - areaCode - ) - ); - } - - @GetMapping("/accommodation/{contentId}") - public ResponseEntity bringAccommodation(@PathVariable long contentId) { - return ResponseEntity.ok( - TourAPIUtils.bringAccommodationDetail(contentId) - ); - } - - @GetMapping("/accommodation/{contentId}/rooms") - public ResponseEntity bringRooms(@PathVariable long contentId) { - return ResponseEntity.ok( - TourAPIUtils.bringRoom(contentId) - ); - } - -} diff --git a/src/main/java/ybe/mini/travelserver/global/api/TourAPIProperties.java b/src/main/java/ybe/mini/travelserver/global/api/TourAPIProperties.java index 6c946d1..65fac87 100644 --- a/src/main/java/ybe/mini/travelserver/global/api/TourAPIProperties.java +++ b/src/main/java/ybe/mini/travelserver/global/api/TourAPIProperties.java @@ -8,13 +8,10 @@ public class TourAPIProperties { public static final String KEY_DECODED = "0ON3kBtDxd0KYGW5spO8inNljADd/IqfnS/l3nMWq3EkARl20N3MmZVYlSvH3Y8V5fEAo7Seucd5pR7Ebm2Phg=="; public static final String KEY_ENCODED = "0ON3kBtDxd0KYGW5spO8inNljADd%2FIqfnS%2Fl3nMWq3EkARl20N3MmZVYlSvH3Y8V5fEAo7Seucd5pR7Ebm2Phg%3D%3D"; - public static final String BASE_URL = "http://apis.data.go.kr/B551011/KorService1/"; + public static final String BASE_URL = "https://apis.data.go.kr/B551011/KorService1/"; public static final String SEARCH_KEYWORD = "searchKeyword1"; - public static final String DETAIL_INTRO = "detailIntro1"; public static final String DETAIL_INFO = "detailInfo1"; - public static final String SEARCH_STAY = "searchStay1"; - public static final String CONTENT_TYPE_ID = "32"; public static final String MOBILE_OS = "ETC"; public static final String MOBILE_APP = "TravelAPP"; diff --git a/src/main/java/ybe/mini/travelserver/global/api/TourAPIService.java b/src/main/java/ybe/mini/travelserver/global/api/TourAPIService.java index f1e57bb..2c77188 100644 --- a/src/main/java/ybe/mini/travelserver/global/api/TourAPIService.java +++ b/src/main/java/ybe/mini/travelserver/global/api/TourAPIService.java @@ -6,142 +6,73 @@ import ybe.mini.travelserver.domain.accommodation.entity.Accommodation; import ybe.mini.travelserver.domain.accommodation.entity.Location; import ybe.mini.travelserver.domain.room.entity.Room; -import ybe.mini.travelserver.global.api.dto.DetailInfoResponse; -import ybe.mini.travelserver.global.api.dto.DetailIntroResponse; -import ybe.mini.travelserver.global.api.dto.SearchKeywordResponse; +import ybe.mini.travelserver.global.api.dto.AccommodationTourAPIResponse; +import ybe.mini.travelserver.global.api.dto.RoomTourAPIResponse; +import ybe.mini.travelserver.global.exception.api.NoAccommodationsFromAPIException; +import ybe.mini.travelserver.global.exception.api.NoRoomsFromAPIException; import java.util.ArrayList; import java.util.List; -import java.util.Optional; @Slf4j @Service @RequiredArgsConstructor public class TourAPIService { - public Accommodation bringAccommodation( - long accommodationId, - String keyword + String keyword, + String code ) { - var accommodationDetailResponse = TourAPIUtils.bringAccommodationDetail(accommodationId); - var accommodationsSimpleSearchResponse = TourAPIUtils.bringAccommodations(keyword); + var accommodationsSimpleSearchResponse = TourAPIUtils.bringAccommodation(keyword, code); - var detailItems = accommodationDetailResponse.response().body().items().item(); - var keywordItems = accommodationsSimpleSearchResponse.response().body().items().item(); + var body = accommodationsSimpleSearchResponse.response().body(); - if (keywordItems.isEmpty() || detailItems.isEmpty()) { - throw new IllegalArgumentException("숙소 정보가 없습니다."); + if (body.totalCount() == 0) { + throw new NoAccommodationsFromAPIException(); } - var keywordItem = keywordItems.get(0); - var detailItem = detailItems.get(0); + var items = body.items().item(); - return generateAccommodation( - accommodationId, - keywordItem, - detailItem - ); - } - - public Accommodation bringAccommodation(String keyword) { - var accommodationsSimpleSearchResponse = TourAPIUtils.bringAccommodations(keyword); - - var keywordItems = accommodationsSimpleSearchResponse.response().body().items().item(); - - if (keywordItems.isEmpty()) { - throw new IllegalArgumentException("숙소 정보가 없습니다."); - } - var keywordItem = keywordItems.get(0); - long accommodationId = Long.parseLong(keywordItem.contentid()); + var item = items.get(0); - var accommodationDetailResponse = TourAPIUtils.bringAccommodationDetail(accommodationId); - var detailItems = accommodationDetailResponse.response().body().items().item(); - var detailItem = detailItems.get(0); + long accommodationId = Long.parseLong(item.contentid()); return generateAccommodation( accommodationId, - keywordItem, - detailItem + item ); } - public List bringRooms(long accommodationId) { - DetailInfoResponse detailInfoResponse = TourAPIUtils.bringRoom(accommodationId); - - var roomItems = detailInfoResponse.response().body().items().item(); - if (roomItems.isEmpty()) { - throw new IllegalArgumentException("객실 정보가 없습니다."); - } - - List rooms = new ArrayList<>(); - for (var roomItem : roomItems) { - Room room = Room.builder() - .roomTypeId(Long.valueOf(roomItem.roomcode())) - .name(roomItem.roomtitle()) - .description(roomItem.roomintro()) - .price(Integer.parseInt(roomItem.roomoffseasonminfee1())) - .image(roomItem.roomimg1()) - .stock(Integer.parseInt(roomItem.roomcount())) - .capacity(Integer.parseInt(roomItem.roommaxcount())) - .accommodation( - Accommodation.builder() - .id(accommodationId) - .build() - ) - .build(); - - rooms.add(room); - } - - return rooms; - } - - public Room bringRoom( - long accommodationId, - long roomTypeId - ) { - List rooms = bringRooms(accommodationId); - - for (var room : rooms) { - if (room.getRoomTypeId() == roomTypeId) { - return room; - } - } - - throw new IllegalArgumentException("객실 정보가 없습니다."); - } - - public List bringAccommodationsForSearch( + public List bringAccommodations( int pageNo, int numOfRows, String keyword, - String areaCode + String code ) { var accommodationsSimpleSearchResponse = TourAPIUtils.bringAccommodations( pageNo, numOfRows, keyword, - areaCode + code ); - var searchedItems = accommodationsSimpleSearchResponse.response().body().items().item(); + var body = accommodationsSimpleSearchResponse.response().body(); - if (searchedItems.isEmpty()) { - throw new IllegalArgumentException("숙소 정보가 없습니다."); + if (body.totalCount() == 0) { + throw new NoAccommodationsFromAPIException(); } + var items = body.items().item(); List accommodations = new ArrayList<>(); - for (var item : searchedItems) { + for (var item : items) { long accommodationId = Long.parseLong(item.contentid()); accommodations.add( generateAccommodation( accommodationId, - item, - null + item ) ); } @@ -151,18 +82,12 @@ public List bringAccommodationsForSearch( private static Accommodation generateAccommodation( long accommodationId, - SearchKeywordResponse.Item keywordItem, - DetailIntroResponse.Item detailItem + AccommodationTourAPIResponse.Item keywordItem ) { - String subfacility = Optional.ofNullable(detailItem) - .map(DetailIntroResponse.Item::subfacility) - .orElse(""); - return Accommodation.builder() .id(accommodationId) .name(keywordItem.title()) .image(keywordItem.firstimage()) - .description(subfacility) .location(Location.builder() .address(keywordItem.addr1()) .latitude(Double.valueOf(keywordItem.mapy())) @@ -172,4 +97,54 @@ private static Accommodation generateAccommodation( .build()) .build(); } + + public List bringRooms(long accommodationId) { + RoomTourAPIResponse roomTourAPIResponse = TourAPIUtils.bringRoom(accommodationId); + + var body = roomTourAPIResponse.response().body(); + + if (body.totalCount() == 0) { + throw new NoRoomsFromAPIException(); + } + + var items = body.items().item(); + + + List rooms = new ArrayList<>(); + for (var item : items) { + Room room = Room.builder() + .roomTypeId(Long.valueOf(item.roomcode())) + .name(item.roomtitle()) + .description(item.roomintro()) + .price(Integer.parseInt(item.roomoffseasonminfee1())) + .image(item.roomimg1()) + .stock(Integer.parseInt(item.roomcount())) + .capacity(Integer.parseInt(item.roommaxcount())) + .accommodation( + Accommodation.builder() + .id(accommodationId) + .build() + ) + .build(); + + rooms.add(room); + } + + return rooms; + } + + public Room bringRoom( + long accommodationId, + long roomTypeId + ) { + List rooms = bringRooms(accommodationId); + + for (var room : rooms) { + if (room.getRoomTypeId() == roomTypeId) { + return room; + } + } + + throw new NoRoomsFromAPIException(); + } } \ No newline at end of file diff --git a/src/main/java/ybe/mini/travelserver/global/api/TourAPIUtils.java b/src/main/java/ybe/mini/travelserver/global/api/TourAPIUtils.java index c507389..882d4e4 100644 --- a/src/main/java/ybe/mini/travelserver/global/api/TourAPIUtils.java +++ b/src/main/java/ybe/mini/travelserver/global/api/TourAPIUtils.java @@ -3,12 +3,21 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.*; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.util.StreamUtils; +import org.springframework.web.client.HttpMessageConverterExtractor; import org.springframework.web.client.RestTemplate; -import ybe.mini.travelserver.global.api.dto.DetailInfoResponse; -import ybe.mini.travelserver.global.api.dto.DetailIntroResponse; -import ybe.mini.travelserver.global.api.dto.SearchKeywordResponse; - +import org.springframework.web.util.DefaultUriBuilderFactory; +import ybe.mini.travelserver.global.api.dto.AccommodationTourAPIResponse; +import ybe.mini.travelserver.global.api.dto.RoomTourAPIResponse; +import ybe.mini.travelserver.global.exception.api.WrongCallBackException; + +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Objects; import static ybe.mini.travelserver.global.api.TourAPIProperties.*; @@ -18,13 +27,19 @@ public class TourAPIUtils { private static final RestTemplate restTemplate = new RestTemplate(); + static { + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); + factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); + restTemplate.setUriTemplateHandler(factory); + } + private static StringBuilder buildCommonUrl(String endpoint) { StringBuilder url = new StringBuilder(BASE_URL + endpoint); url.append("?MobileOS=").append(MOBILE_OS); url.append("&MobileApp=").append(MOBILE_APP); url.append("&_type=").append(RENDER_TYPE); url.append("&contentTypeId=").append(CONTENT_TYPE_ID); - url.append("&serviceKey=").append(KEY_DECODED); + url.append("&serviceKey=").append(KEY_ENCODED); return url; } @@ -35,70 +50,70 @@ private static T fetchDataFromAPI( ) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(headers); - ResponseEntity responseEntity = restTemplate.exchange( + log.warn("Tour API 요청 URL : {}", url); + + return restTemplate.execute( url, HttpMethod.GET, - entity, - responseType - ); - - return responseEntity.getBody(); - } - - public static DetailIntroResponse bringAccommodationDetail(long contentId) { - StringBuilder url = buildCommonUrl(DETAIL_INTRO); - url.append("&contentId=").append(contentId); - - return fetchDataFromAPI( - url.toString(), - DetailIntroResponse.class + requestCallback -> { + }, + clientHttpResponse -> { + MediaType contentType = clientHttpResponse.getHeaders().getContentType(); + if (contentType != null && contentType.includes(MediaType.TEXT_XML)) { + String body = StreamUtils.copyToString(clientHttpResponse.getBody(), Charset.defaultCharset()); + log.error("공공포텅 오류 XML 반환 : {}", body); + throw new WrongCallBackException(); + } + + return new HttpMessageConverterExtractor<>(responseType, restTemplate.getMessageConverters()) + .extractData(clientHttpResponse); + }, + entity ); } - public static DetailInfoResponse bringRoom(long contentId) { + public static RoomTourAPIResponse bringRoom(long contentId) { StringBuilder url = buildCommonUrl(DETAIL_INFO); url.append("&contentId=").append(contentId); return fetchDataFromAPI( url.toString(), - DetailInfoResponse.class + RoomTourAPIResponse.class ); } - public static SearchKeywordResponse bringAccommodations( + public static AccommodationTourAPIResponse bringAccommodations( int pageNo, int numOfRows, String keyword, - String areaCode + String code ) { StringBuilder url = buildCommonUrl(SEARCH_KEYWORD); - url.append("&keyword=").append(keyword == null ? "_" : keyword); + url.append("&keyword=").append(keyword == null ? "_" : URLEncoder.encode(keyword, StandardCharsets.UTF_8)); if (pageNo != 0 && numOfRows != 0) { url.append("&pageNo=").append(pageNo); url.append("&numOfRows=").append(numOfRows); } - if (!Objects.isNull(areaCode)) { - url.append("&areaCode=").append(areaCode); + + if (!Objects.isNull(code)) { + url.append("&areaCode=").append(code); } url.append("&arrange=").append("R"); return fetchDataFromAPI( url.toString(), - SearchKeywordResponse.class + AccommodationTourAPIResponse.class ); } - public static SearchKeywordResponse bringAccommodations(String keyword) { - return bringAccommodations(1, 1, keyword, null); - } - - public static SearchKeywordResponse bringAccommodations(int pageNo, int numOfRows) { - return bringAccommodations(pageNo, numOfRows, null, null); + public static AccommodationTourAPIResponse bringAccommodation(String keyword, String code) { + return bringAccommodations(1, 1, keyword, code); } } \ No newline at end of file diff --git a/src/main/java/ybe/mini/travelserver/global/api/dto/SearchKeywordResponse.java b/src/main/java/ybe/mini/travelserver/global/api/dto/AccommodationTourAPIResponse.java similarity index 92% rename from src/main/java/ybe/mini/travelserver/global/api/dto/SearchKeywordResponse.java rename to src/main/java/ybe/mini/travelserver/global/api/dto/AccommodationTourAPIResponse.java index 50b9616..2205214 100644 --- a/src/main/java/ybe/mini/travelserver/global/api/dto/SearchKeywordResponse.java +++ b/src/main/java/ybe/mini/travelserver/global/api/dto/AccommodationTourAPIResponse.java @@ -1,12 +1,11 @@ package ybe.mini.travelserver.global.api.dto; import ybe.mini.travelserver.global.api.dto.common.Response; - /** * 숙박 간단 정보 키워드 검색 * 키워드로 검색을하며 전체별 타입정보별 목록을 조회한다 */ -public record SearchKeywordResponse(Response response) { +public record AccommodationTourAPIResponse(Response response) { public record Item( String addr1, String addr2, diff --git a/src/main/java/ybe/mini/travelserver/global/api/dto/DetailIntroResponse.java b/src/main/java/ybe/mini/travelserver/global/api/dto/DetailIntroResponse.java deleted file mode 100644 index 5255804..0000000 --- a/src/main/java/ybe/mini/travelserver/global/api/dto/DetailIntroResponse.java +++ /dev/null @@ -1,45 +0,0 @@ -package ybe.mini.travelserver.global.api.dto; - -import ybe.mini.travelserver.global.api.dto.common.Response; - -/** - * 숙박 상세 정보 조회 - * 상세소개 쉬는날, 개장기간 등 내역을 조회하는 기능 - */ -public record DetailIntroResponse(Response response) { - public record Item( - String contentid,//콘텐츠ID - String contenttypeid,//콘텐츠타입ID - String goodstay,//굿스테이여부 - String benikia,//베니키아여부 - String hanok,//한옥여부 - String roomcount,//객실수 - String roomtype,//객실유형 - String refundregulation,//환불규정 - String checkintime,//입실시간 - String checkouttime,//퇴실시간 - String chkcooking,//객실내취사여부 - String seminar,//세미나실여부 - String sports,//스포츠시설여부 - String sauna,//사우나실여부 - String beauty,//뷰티시설정보 - String beverage,//식음료장여부 - String karaoke,//노래방여부 - String barbecue,//바비큐장여부 - String campfire,//캠프파이어여부 - String bicycle,//자전거대여여부 - String fitness,//휘트니스센터여부 - String publicpc,//공용 PC실여부 - String publicbath,//공용샤워실여부 - String subfacility,//부대시설 (기타) - String foodplace,//식음료장 - String reservationurl,//예약안내홈페이지 - String pickup,//픽업서비스 - String infocenterlodging,//문의및안내 - String parkinglodging,//주차시설 - String reservationlodging,//예약안내 - String scalelodging,//규모 - String accomcountlodging//수용가능인원 - ) { - } -} diff --git a/src/main/java/ybe/mini/travelserver/global/api/dto/DetailInfoResponse.java b/src/main/java/ybe/mini/travelserver/global/api/dto/RoomTourAPIResponse.java similarity index 98% rename from src/main/java/ybe/mini/travelserver/global/api/dto/DetailInfoResponse.java rename to src/main/java/ybe/mini/travelserver/global/api/dto/RoomTourAPIResponse.java index 7cb27c5..c57589b 100644 --- a/src/main/java/ybe/mini/travelserver/global/api/dto/DetailInfoResponse.java +++ b/src/main/java/ybe/mini/travelserver/global/api/dto/RoomTourAPIResponse.java @@ -1,6 +1,5 @@ package ybe.mini.travelserver.global.api.dto; - import ybe.mini.travelserver.global.api.dto.common.Response; /** @@ -8,7 +7,7 @@ * 추가 관광정보 상세내역을 조회한다. * 상세반복정보를 안내URL의 국문관광정보 상세 매뉴얼 문서를 참고하시기 바랍니다. */ -public record DetailInfoResponse(Response response) { +public record RoomTourAPIResponse(Response response) { public record Item( String contentid,//콘텐츠ID String contenttypeid,//콘텐츠타입ID diff --git a/src/main/java/ybe/mini/travelserver/global/exception/api/NoAccommodationsFromAPIException.java b/src/main/java/ybe/mini/travelserver/global/exception/api/NoAccommodationsFromAPIException.java new file mode 100644 index 0000000..d7c3bf7 --- /dev/null +++ b/src/main/java/ybe/mini/travelserver/global/exception/api/NoAccommodationsFromAPIException.java @@ -0,0 +1,4 @@ +package ybe.mini.travelserver.global.exception.api; + +public class NoAccommodationsFromAPIException extends RuntimeException { +} diff --git a/src/main/java/ybe/mini/travelserver/global/exception/api/NoRoomsFromAPIException.java b/src/main/java/ybe/mini/travelserver/global/exception/api/NoRoomsFromAPIException.java new file mode 100644 index 0000000..a42c13c --- /dev/null +++ b/src/main/java/ybe/mini/travelserver/global/exception/api/NoRoomsFromAPIException.java @@ -0,0 +1,5 @@ +package ybe.mini.travelserver.global.exception.api; + +public class NoRoomsFromAPIException extends RuntimeException { +} + diff --git a/src/main/java/ybe/mini/travelserver/global/exception/api/TourAPIErrorMessage.java b/src/main/java/ybe/mini/travelserver/global/exception/api/TourAPIErrorMessage.java new file mode 100644 index 0000000..f7bd0bc --- /dev/null +++ b/src/main/java/ybe/mini/travelserver/global/exception/api/TourAPIErrorMessage.java @@ -0,0 +1,19 @@ +package ybe.mini.travelserver.global.exception.api; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import ybe.mini.travelserver.global.exception.ErrorMessage; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE; + +@Getter +@AllArgsConstructor +public enum TourAPIErrorMessage implements ErrorMessage { + NO_ACCOMMODATIONS_FROM_API(BAD_REQUEST, "API로부터 숙소를 가져오지 못했습니다."), + NO_ROOMS_FROM_API(BAD_REQUEST, "API로부터 객실을 가져오지 못했습니다."), + WRONG_CALLBACK(SERVICE_UNAVAILABLE, "잘못된 콜백입니다."); + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/ybe/mini/travelserver/global/exception/api/TourAPIExceptionHandler.java b/src/main/java/ybe/mini/travelserver/global/exception/api/TourAPIExceptionHandler.java new file mode 100644 index 0000000..edd9883 --- /dev/null +++ b/src/main/java/ybe/mini/travelserver/global/exception/api/TourAPIExceptionHandler.java @@ -0,0 +1,25 @@ +package ybe.mini.travelserver.global.exception.api; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ybe.mini.travelserver.global.exception.ProblemDetailCreator; + +@RestControllerAdvice +public class TourAPIExceptionHandler extends ProblemDetailCreator { + protected TourAPIExceptionHandler() { + super("TourAPI 처리 실패"); + } + + @ExceptionHandler(NoAccommodationsFromAPIException.class) + public ProblemDetail handleNotGatheredAccommodationsFromAPIException(HttpServletRequest request) { + return createProblemDetail(TourAPIErrorMessage.NO_ACCOMMODATIONS_FROM_API, request); + } + + @ExceptionHandler(NoRoomsFromAPIException.class) + public ProblemDetail handleNotGatheredRoomsFromAPIException(HttpServletRequest request) { + return createProblemDetail(TourAPIErrorMessage.NO_ROOMS_FROM_API, request); + } + +} \ No newline at end of file diff --git a/src/main/java/ybe/mini/travelserver/global/exception/api/WrongCallBackException.java b/src/main/java/ybe/mini/travelserver/global/exception/api/WrongCallBackException.java new file mode 100644 index 0000000..51d5512 --- /dev/null +++ b/src/main/java/ybe/mini/travelserver/global/exception/api/WrongCallBackException.java @@ -0,0 +1,4 @@ +package ybe.mini.travelserver.global.exception.api; + +public class WrongCallBackException extends RuntimeException { +}