diff --git a/check-in-request-MS/build.gradle b/check-in-request-MS/build.gradle index 48d2bee..834464f 100644 --- a/check-in-request-MS/build.gradle +++ b/check-in-request-MS/build.gradle @@ -30,9 +30,8 @@ dependencies { //db - //implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - - //implementation 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'com.mysql:mysql-connector-j' //test testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/common/exception/DomainException.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/common/exception/DomainException.java new file mode 100644 index 0000000..b0c2a7d --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/common/exception/DomainException.java @@ -0,0 +1,10 @@ +package com.example.checkinrequestMS.PlaceAPI.common.exception; + + + +public class DomainException extends RuntimeException{ + + public DomainException(String errorMessage) { + super(errorMessage); + } +} diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/common/exception/WebException.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/common/exception/WebException.java new file mode 100644 index 0000000..4e76f4e --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/common/exception/WebException.java @@ -0,0 +1,10 @@ +package com.example.checkinrequestMS.PlaceAPI.common.exception; + + + +public class WebException extends RuntimeException{ + + public WebException(String errorMessage){ + super(errorMessage); + } +} diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/common/exception/response/ExceptionResponse.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/common/exception/response/ExceptionResponse.java new file mode 100644 index 0000000..65f3825 --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/common/exception/response/ExceptionResponse.java @@ -0,0 +1,15 @@ +package com.example.checkinrequestMS.PlaceAPI.common.exception.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import org.springframework.http.HttpStatus; + +@Getter +@ToString +@AllArgsConstructor +public class ExceptionResponse { + private HttpStatus status; + private String message; +} + diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/Place.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/Place.java index edbe808..e298cb3 100644 --- a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/Place.java +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/Place.java @@ -1,15 +1,28 @@ package com.example.checkinrequestMS.PlaceAPI.domain; import com.fasterxml.jackson.databind.JsonNode; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.validator.constraints.UniqueElements; +import org.springframework.context.annotation.Primary; + +import java.util.Objects; @Getter @Setter(AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity public class Place { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "place_id", nullable = false) + private Long id; + @Column(unique = true) private String placeName; - private String addressName; + private String address; private String roadAddressName; private String categoryName; private String phone; @@ -17,9 +30,16 @@ public class Place { private double x; private double y; + public static Place createEmptyPlace(){ + return Place.builder().build(); + } + public static Place createEmptyPlaceWithOnlyId(Long id){ + return Place.builder().id(id).build(); + } + public void setValues(JsonNode document){ this.setPlaceName(document.get("place_name").asText()); - this.setAddressName(document.get("address_name").asText()); + this.setAddress(document.get("address_name").asText()); this.setRoadAddressName(document.get("road_address_name").asText()); this.setCategoryName(document.get("category_name").asText()); this.setPhone(document.get("phone").asText()); @@ -27,5 +47,13 @@ public void setValues(JsonNode document){ this.setX(document.get("x").asDouble()); this.setY(document.get("y").asDouble()); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Place)) return false; + + Place p = (Place) o; + return Objects.equals(placeName, p.getPlaceName()); + } } diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/exceptions/place/PlaceErrorCode.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/exceptions/place/PlaceErrorCode.java new file mode 100644 index 0000000..82814f2 --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/exceptions/place/PlaceErrorCode.java @@ -0,0 +1,15 @@ +package com.example.checkinrequestMS.PlaceAPI.domain.exceptions.place; + + + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PlaceErrorCode { + + NO_PLACE_INFO("가게 정보가 없습니다."); + + private final String detail; +} diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/exceptions/place/PlaceException.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/exceptions/place/PlaceException.java new file mode 100644 index 0000000..c66e210 --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/exceptions/place/PlaceException.java @@ -0,0 +1,12 @@ +package com.example.checkinrequestMS.PlaceAPI.domain.exceptions.place; + +import com.example.checkinrequestMS.PlaceAPI.common.exception.DomainException; + + +public class PlaceException extends DomainException { + + + public PlaceException(PlaceErrorCode errorCode) { + super(errorCode.getDetail()); + } +} diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/SearchPlaceService.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/SearchPlaceService.java index 4076162..1f190be 100644 --- a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/SearchPlaceService.java +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/SearchPlaceService.java @@ -1,22 +1,35 @@ package com.example.checkinrequestMS.PlaceAPI.domain.service; import com.example.checkinrequestMS.PlaceAPI.domain.Place; -import com.example.checkinrequestMS.PlaceAPI.domain.service.tools.KakaoParser; -import com.example.checkinrequestMS.PlaceAPI.web.rest.KakaoAPIRequest; -import com.fasterxml.jackson.core.JsonProcessingException; +import com.example.checkinrequestMS.PlaceAPI.domain.exceptions.place.PlaceException; +import com.example.checkinrequestMS.PlaceAPI.domain.service.tools.KakaoAPIStoreInfoSaver; +import com.example.checkinrequestMS.PlaceAPI.infra.PlaceRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; +import static com.example.checkinrequestMS.PlaceAPI.domain.exceptions.place.PlaceErrorCode.NO_PLACE_INFO; + + @Service @RequiredArgsConstructor +@Slf4j public class SearchPlaceService { - private final KakaoParser parser; + private final PlaceRepository storeRepository; + private final KakaoAPIStoreInfoSaver storeInfoBalancer; + + @Transactional + public List searchWithKeyword(String query, double x, double y, int radius) { - public List searchWithKeyword(String query, double x, double y, int radius) throws JsonProcessingException { - StringBuilder response = KakaoAPIRequest.getStoreInfo(query, x, y, radius); - return parser.parsePlaceInfo(response); + storeInfoBalancer.balanceKeyWordSearch(query, x, y, radius); + List places = storeRepository.getStoresByNameAndRadius(x, y, radius) + .orElseThrow(() -> new PlaceException(NO_PLACE_INFO)); + + return places; } + } diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoAPIStoreInfoSaver.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoAPIStoreInfoSaver.java new file mode 100644 index 0000000..3e242d8 --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoAPIStoreInfoSaver.java @@ -0,0 +1,56 @@ +package com.example.checkinrequestMS.PlaceAPI.domain.service.tools; + +import com.example.checkinrequestMS.PlaceAPI.domain.Place; +import com.example.checkinrequestMS.PlaceAPI.infra.PlaceRepository; +import com.example.checkinrequestMS.PlaceAPI.web.restAPI.KakaoStoreAPIRequest; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class KakaoAPIStoreInfoSaver { + + private final PlaceRepository storeRepository; + private final KakaoStoreAPIRequest kakaoAPIRequest; + + //check: 이후 다양한 쿼리에서 저장 가능하게 하도록 하려고 합니다. **현재 getStoresByNameAndRadius도 이름은 뺴고 좌표의 범위로만 검색합니다. 이후 수정하겠습니다! + public void balanceKeyWordSearch(String query, double x, double y, int radius){ + String response = kakaoAPIRequest.getStoreInfo(query, x, y, radius); + + List placesFromAPI = parsePlaceInfo(response); //Brute Force 이후 범위를 경도/위도 조합을 스캔하며 저장해서 끝난 부분은 다시 검색 안해도 됨. + List placesFromDB = storeRepository.getStoresByNameAndRadius(x, y, radius).get(); + List list = placesFromAPI.stream().filter(place -> placesFromDB.stream().noneMatch(a -> place.equals(a))).collect(Collectors.toList()); + + if (list.isEmpty()) { + return; + } + for (Place place : list) { + storeRepository.save(place); + } + + } + private List parsePlaceInfo(String response){ + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(response); + + List places = new ArrayList<>(); + for (JsonNode document : rootNode) { + Place place = Place.createEmptyPlace(); + place.setValues(document); + places.add(place); + } + return places; + }catch (JsonProcessingException e){ + throw new RuntimeException("Place로 변환 중 에러."); + } + } + +} diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoParser.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoParser.java deleted file mode 100644 index 4708aad..0000000 --- a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.checkinrequestMS.PlaceAPI.domain.service.tools; - -import com.example.checkinrequestMS.PlaceAPI.domain.Place; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; - -@Component -public class KakaoParser { - //fixme: 이렇게 tools 폴더를 만들어 써도 되는지 모르겠습니다. service랑은 성격이 다른것 같아서 격리하였습니다. - public List parsePlaceInfo(StringBuilder response) throws JsonProcessingException { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode rootNode = objectMapper.readTree(response.toString()); - JsonNode documents = rootNode.get("documents"); - - List places = new ArrayList<>(); - for(JsonNode document : documents){ - Place place = new Place(); - place.setValues(document); - places.add(place); - } - return places; - } - - - - - -} diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/infra/PlaceRepository.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/infra/PlaceRepository.java new file mode 100644 index 0000000..a2efc20 --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/infra/PlaceRepository.java @@ -0,0 +1,13 @@ +package com.example.checkinrequestMS.PlaceAPI.infra; + +import com.example.checkinrequestMS.PlaceAPI.domain.Place; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface PlaceRepository extends JpaRepository { + @Query(value = "SELECT * FROM place p WHERE (POWER(:x - p.x, 2) + POWER(:y - p.y, 2)) <= POWER(:radius/POWER(10,5), 2)", nativeQuery = true) + Optional> getStoresByNameAndRadius(double x, double y, int radius); +}//이름은 아직 미구현. diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/exceptions/ExceptionController.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/exceptions/ExceptionController.java new file mode 100644 index 0000000..63154d5 --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/exceptions/ExceptionController.java @@ -0,0 +1,27 @@ +package com.example.checkinrequestMS.PlaceAPI.web.exceptions; + +import com.example.checkinrequestMS.PlaceAPI.common.exception.DomainException; +import com.example.checkinrequestMS.PlaceAPI.common.exception.WebException; +import com.example.checkinrequestMS.PlaceAPI.common.exception.response.ExceptionResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ExceptionController { + + @ExceptionHandler({ + DomainException.class + }) + public ResponseEntity domainException(final DomainException e){ + return ResponseEntity.badRequest().body(new ExceptionResponse(HttpStatus.BAD_REQUEST, e.getMessage())); + } + + @ExceptionHandler({ + WebException.class + }) + public ResponseEntity webException(final WebException e){ + return ResponseEntity.badRequest().body(new ExceptionResponse(HttpStatus.BAD_REQUEST, e.getMessage())); + } +} \ No newline at end of file diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/exceptions/kakaoMap/KakaoStoreAPIErrorCode.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/exceptions/kakaoMap/KakaoStoreAPIErrorCode.java new file mode 100644 index 0000000..9d46014 --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/exceptions/kakaoMap/KakaoStoreAPIErrorCode.java @@ -0,0 +1,22 @@ +package com.example.checkinrequestMS.PlaceAPI.web.exceptions.kakaoMap; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum KakaoStoreAPIErrorCode{ + + + URL_CONNECTION_ERROR("URL 연결 중 오류가 발생했습니다. URL 주소나 파라미터를 확인해 주세요."), + URL_CONNECTION_ERROR_SPECIFIC("URL 연결 중 오류가 발생했습니다. 에러메시지: "), + URL_PROTOCOL_EXCEPTION("적절한 HTTP 메서드를 선택해 주세요."), + DATA_STREAM_EXCEPTION("데이터 스트림을 읽는 중 오류가 발생했습니다."), + META_PARSING_ERROR("KakaoAPI의 Meta 정보 파싱 작업 중 오류가 발생했습니다."), + FIRST_PAGE_PARSING_ERROR("첫 페이지의 가게 정보 파싱 작업 중 오류가 발생했습니다."), + ADDITIONAL_PARSING_ERROR("추가 페이지의 가게 정보 파싱 작업 중 오류가 발생했습니다."); + //API_CONNECTION_ERROR("API 연결 중 오류가 발생했습니다. 에러 메세지: ", connectionMessage), + + + private final String detail; +} diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/exceptions/kakaoMap/KakaoStoreAPIException.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/exceptions/kakaoMap/KakaoStoreAPIException.java new file mode 100644 index 0000000..81bc63f --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/exceptions/kakaoMap/KakaoStoreAPIException.java @@ -0,0 +1,16 @@ +package com.example.checkinrequestMS.PlaceAPI.web.exceptions.kakaoMap; + +import com.example.checkinrequestMS.PlaceAPI.common.exception.WebException; + +public class KakaoStoreAPIException extends WebException { + + public KakaoStoreAPIException(KakaoStoreAPIErrorCode errorCode) { + super(errorCode.getDetail()); + } + public KakaoStoreAPIException(KakaoStoreAPIErrorCode errorCode, String errorMessage){ + super(errorCode.getDetail() + errorMessage); + } + + + +} diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/rest/KakaoAPIRequest.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/rest/KakaoAPIRequest.java deleted file mode 100644 index e95f8e8..0000000 --- a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/rest/KakaoAPIRequest.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.example.checkinrequestMS.PlaceAPI.web.rest; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.List; - -@Component -public class KakaoAPIRequest { - - private static String apiKey = "a0dc4e7625b15b5b4cef4e0a028119b3"; - - public static StringBuilder getStoreInfo(String query, double x, double y, int radius){ - try { - String apiURL = "https://dapi.kakao.com/v2/local/search/keyword.json" - + "?page=1&size=15&sort=accuracy" - + "&query=" + URLEncoder.encode("맛집", "UTF-8") - + "&x=" + x - + "&y=" + y - + "&radius=" + radius; - - HttpURLConnection con = connection(apiURL); - StringBuilder response = bufferConnection(con); - return response; - //jsonParse(response); - }catch (JsonProcessingException e){ - e.printStackTrace(); - }catch (MalformedURLException e){ - e.printStackTrace(); - } - catch (IOException e){ - e.printStackTrace(); - } - catch (Exception e) { - e.printStackTrace(); - } - return null; - } - private static HttpURLConnection connection(String apiURL) throws IOException, MalformedURLException { - URL url = new URL(apiURL); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("GET"); - con.setRequestProperty("Authorization", "KakaoAK " + apiKey); - return con; - - } - private static StringBuilder bufferConnection(HttpURLConnection con) throws IOException { - int responseCode = con.getResponseCode(); - BufferedReader br; - if (responseCode == 200) { - br = new BufferedReader(new InputStreamReader(con.getInputStream())); - } else { - br = new BufferedReader(new InputStreamReader(con.getErrorStream())); - } - - String inputLine; - StringBuilder response = new StringBuilder(); - while ((inputLine = br.readLine()) != null) { - response.append(inputLine); - } - br.close(); - - return response; - - } - - -} diff --git a/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/restAPI/KakaoStoreAPIRequest.java b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/restAPI/KakaoStoreAPIRequest.java new file mode 100644 index 0000000..90db85a --- /dev/null +++ b/check-in-request-MS/src/main/java/com/example/checkinrequestMS/PlaceAPI/web/restAPI/KakaoStoreAPIRequest.java @@ -0,0 +1,157 @@ +package com.example.checkinrequestMS.PlaceAPI.web.restAPI; + +import com.example.checkinrequestMS.PlaceAPI.web.exceptions.kakaoMap.KakaoStoreAPIException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.*; + +import static com.example.checkinrequestMS.PlaceAPI.web.exceptions.kakaoMap.KakaoStoreAPIErrorCode.*; + +@Component +public class KakaoStoreAPIRequest { + + @Value("${kakao-map-api.key}") + private String apiKey; + + //check: unchecked exception에 대해 메시징 이외의 예외 방법 생각해보기. + // 이 부분은 이후에 추가하겠습니다. + public String getStoreInfo(String query, double x, double y, int radius) { + + int page = 1; + URL apiURL = getRequestURL(query, x, y, radius, page); + + String currentResponse = connectAndReadByOnePage(apiURL); + String collectedResponse = parseFirstPage(currentResponse); + double num = checkLeftPage(currentResponse); + if (num <= 1) { + return collectedResponse; + } + int repeat = (int) Math.ceil(num / 15); + + for (int i = 1; i < repeat; i++) { + page++; + currentResponse = connectAndReadByOnePage(getRequestURL(query, x, y, radius, page)); + collectedResponse = addPages(collectedResponse, currentResponse); + } + return collectedResponse; + } + private URL getRequestURL(String query, double x, double y, int radius, int page){ + try { + URL apiURI = UriComponentsBuilder.fromHttpUrl("https://dapi.kakao.com/v2/local/search/keyword.json") + .queryParam("sort", "accuracy") + .queryParam("query", query) + .queryParam("x", x) + .queryParam("y", y) + .queryParam("radius", 50) + .queryParam("size", 15) + .queryParam("page", page) + .build() + .encode() + .toUri().toURL(); + return apiURI; + } catch (MalformedURLException e) { + throw new KakaoStoreAPIException(URL_CONNECTION_ERROR); + } + + } + + private String addPages(String collectedResponse, String currentResponse) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode initialNode = objectMapper.readTree(collectedResponse); + ArrayNode initialNodes = (ArrayNode) initialNode; + + JsonNode newResponseNode = objectMapper.readTree(currentResponse); + ArrayNode newNodes = (ArrayNode) newResponseNode.get("documents"); + + for (JsonNode newNode : newNodes) { + initialNodes.add(newNode); + } + return objectMapper.writeValueAsString(initialNodes); + + } catch (JsonProcessingException e) { + throw new KakaoStoreAPIException(ADDITIONAL_PARSING_ERROR); + } + } + + public String parseFirstPage(String jsonResponse) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode responseNode = objectMapper.readTree(jsonResponse); + JsonNode documentsNode = responseNode.get("documents"); + if(documentsNode == null){ + throw new KakaoStoreAPIException(URL_CONNECTION_ERROR_SPECIFIC, responseNode.get("message").toString()); + } + return objectMapper.writeValueAsString(documentsNode); + } catch (JsonProcessingException e) { + throw new KakaoStoreAPIException(FIRST_PAGE_PARSING_ERROR); + } + } + + private int checkLeftPage(String response) { + JsonNode responseNode = null; + try { + ObjectMapper objectMapper = new ObjectMapper(); + responseNode = objectMapper.readTree(response); + JsonNode meta = responseNode.get("meta"); + System.out.println(meta); + + return meta.get("total_count").asInt(); + } catch (JsonProcessingException e) { + throw new KakaoStoreAPIException(META_PARSING_ERROR); + } + } + + private String connectAndReadByOnePage(URL apiURL) { + HttpURLConnection con = connection(apiURL); + String response = readWithBuffer(con); + return response; + } + private HttpURLConnection connection(URL apiURL) { + try { + HttpURLConnection con = (HttpURLConnection) apiURL.openConnection(); + con.setRequestMethod("GET"); + con.setRequestProperty("Authorization", "KakaoAK " + apiKey); + return con; + } catch (IOException e) { + throw new KakaoStoreAPIException(URL_PROTOCOL_EXCEPTION); + } + } + + private String readWithBuffer(HttpURLConnection con) { + try { + int responseCode = con.getResponseCode(); + + BufferedReader br; + if (responseCode == 200) { + br = new BufferedReader(new InputStreamReader(con.getInputStream())); + } else { + br = new BufferedReader(new InputStreamReader(con.getErrorStream())); + } + + String inputLine; + StringBuilder response = new StringBuilder(); + while ((inputLine = br.readLine()) != null) { + response.append(inputLine); + } + br.close(); + System.out.println(response); + return response.toString(); + } catch (IOException e) { + throw new KakaoStoreAPIException(DATA_STREAM_EXCEPTION); + } + + } + + +} diff --git a/check-in-request-MS/src/main/resources/application.yml b/check-in-request-MS/src/main/resources/application.yml index 3ab60b2..5f926d9 100644 --- a/check-in-request-MS/src/main/resources/application.yml +++ b/check-in-request-MS/src/main/resources/application.yml @@ -1,2 +1,19 @@ -kakao: - apikey: a0dc4e7625b15b5b4cef4e0a028119b3 +spring: + application: + name: member-context + datasource: + url: jdbc:mysql://localhost:3306/place?useSSL=false&useUnicode=true&allowPublicKeyRetrieval=true + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: 1234 + jpa: + show-sql: true + hibernate: + ddl-auto: create + database-platform: org.hibernate.dialect.MySQLDialect + generate-ddl: true +server: + port: 8081 + +kakao-map-api: + key: a0dc4e7625b15b5b4cef4e0a028119b3 diff --git a/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/SearchPlaceServiceTest.java b/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/SearchPlaceServiceTest.java new file mode 100644 index 0000000..73adb25 --- /dev/null +++ b/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/SearchPlaceServiceTest.java @@ -0,0 +1,52 @@ +package com.example.checkinrequestMS.PlaceAPI.domain.service; + +import com.example.checkinrequestMS.PlaceAPI.domain.Place; +import com.example.checkinrequestMS.PlaceAPI.domain.service.tools.KakaoAPIStoreInfoSaver; +import com.example.checkinrequestMS.PlaceAPI.infra.PlaceRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SearchPlaceServiceTest { + + @InjectMocks + SearchPlaceService sut; + + @Mock + PlaceRepository storeRepository; + + @Mock + KakaoAPIStoreInfoSaver infoSaver; + + + @Test + void searchWithKeyword() { + //given + String query = "맛집"; + double x = 126.98561429978552; + double y = 37.56255453417897; + int radius = 50; + + List places = mock(); + doNothing().when(infoSaver).balanceKeyWordSearch(query, x, y, radius); + given(storeRepository.getStoresByNameAndRadius(x, y, radius)).willReturn(Optional.of(places)); + + //when + List returnedPlaces = sut.searchWithKeyword(query, x, y, radius); + + //then + verify(infoSaver).balanceKeyWordSearch(query, x, y, radius); + verify(storeRepository).getStoresByNameAndRadius(x, y, radius); + assertEquals(places, returnedPlaces); + } +} \ No newline at end of file diff --git a/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/parseServiceTest.java b/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/parseServiceTest.java deleted file mode 100644 index 8595466..0000000 --- a/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/parseServiceTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.example.checkinrequestMS.PlaceAPI.domain.service; - -import com.example.checkinrequestMS.PlaceAPI.domain.Place; -import com.example.checkinrequestMS.PlaceAPI.domain.service.tools.KakaoParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.springframework.boot.test.context.SpringBootTest; - -import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; - -@SpringBootTest -class parseServiceTest { - - @InjectMocks - SearchPlaceService sut; - - @Mock - KakaoParser parser; - - @Test - void parse() throws JsonProcessingException { - //given - String query = "맛집"; - double x = 126.98561429978552; - double y = 37.56255453417897; - int radius = 50; - - //when - sut.searchWithKeyword(query, x, y, radius); - - //then - verify(parser).parsePlaceInfo(any(StringBuilder.class)); - - } - -} \ No newline at end of file diff --git a/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoAPIStoreInfoSaverTest.java b/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoAPIStoreInfoSaverTest.java new file mode 100644 index 0000000..b8afff2 --- /dev/null +++ b/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoAPIStoreInfoSaverTest.java @@ -0,0 +1,94 @@ +package com.example.checkinrequestMS.PlaceAPI.domain.service.tools; + +import com.example.checkinrequestMS.PlaceAPI.domain.Place; +import com.example.checkinrequestMS.PlaceAPI.infra.PlaceRepository; +import com.example.checkinrequestMS.PlaceAPI.web.restAPI.KakaoStoreAPIRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class KakaoAPIStoreInfoSaverTest { + + @InjectMocks + KakaoAPIStoreInfoSaver sut; + + @Mock + private PlaceRepository storeRepository; + + @Mock + private KakaoStoreAPIRequest kakaoAPIRequest; + + private String response; + + @BeforeEach + void setUp() { + response = + "[{\"address_name\":\"서울 중구 명동2가 25-36\",\"category_group_code\":\"FD6\",\"category_group_name\":\"음식점\",\"category_name\":\"음식점 > 분식\",\"distance\":\"0\",\"id\":\"10332413\",\"phone\":\"02-776-5348\",\"place_name\":\"명동교자 본점\",\"place_url\":\"http://place.map.kakao.com/10332413\",\"road_address_name\":\"서울 중구 명동10길 29\",\"x\":\"126.98561429978552\",\"y\":\"37.56255453417897\"}," + + "{\"address_name\":\"서울 중구 명동2가 4-2\",\"category_group_code\":\"FD6\",\"category_group_name\":\"음식점\",\"category_name\":\"음식점 > 한식\",\"distance\":\"42\",\"id\":\"26853115\",\"phone\":\"02-3789-9292\",\"place_name\":\"순남시래기 명동직영점\",\"place_url\":\"http://place.map.kakao.com/26853115\",\"road_address_name\":\"서울 중구 명동10길 35-20\",\"x\":\"126.985784007806\",\"y\":\"37.5629131513515\"}]"; + } + + @Test + @DisplayName("API에서 받은 정보 모두 저장") + void add_all_from_API() { + //given + List placesFromDB = mock(); + + given(kakaoAPIRequest.getStoreInfo(anyString(), anyDouble(), anyDouble(), anyInt())).willReturn(response); + given(storeRepository.getStoresByNameAndRadius(anyDouble(), anyDouble(), anyInt())).willReturn(Optional.of(placesFromDB)); + + //when + sut.balanceKeyWordSearch("맛집", 126.98561429978552, 37.56255453417897, 50); + + //then + verify(storeRepository, times(2)).save(any(Place.class)); + } + @Test + @DisplayName("API에서 받은 정보 중 1개 없어서 저장") + void add_only_one_from_API() { + //given + Place place1 = mock(); + given(place1.getPlaceName()).willReturn("명동교자 본점"); + List placesFromDB = spy(Arrays.asList(place1)); + + given(kakaoAPIRequest.getStoreInfo(anyString(), anyDouble(), anyDouble(), anyInt())).willReturn(response); + given(storeRepository.getStoresByNameAndRadius(anyDouble(), anyDouble(), anyInt())).willReturn(Optional.of(placesFromDB)); + + //when + sut.balanceKeyWordSearch("맛집", 126.98561429978552, 37.56255453417897, 50); + + //then + verify(storeRepository, times(1)).save(any(Place.class)); + } + @Test + @DisplayName("API에서 받은 정보가 DB에 이미 있는 경우 저장하지 않음") + void add_none_from_API(){ + //given + Place place1 = mock(); + given(place1.getPlaceName()).willReturn("명동교자 본점"); + Place place2 = mock(); + given(place2.getPlaceName()).willReturn("순남시래기 명동직영점"); + List placesFromDB = spy(Arrays.asList(place1, place2)); + + given(kakaoAPIRequest.getStoreInfo(anyString(), anyDouble(), anyDouble(), anyInt())).willReturn(response); + given(storeRepository.getStoresByNameAndRadius(anyDouble(), anyDouble(), anyInt())).willReturn(Optional.of(placesFromDB)); + + //when + sut.balanceKeyWordSearch("맛집", 126.98561429978552, 37.56255453417897, 50); + + //then + verify(storeRepository, times(0)).save(any(Place.class)); + } +} \ No newline at end of file diff --git a/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoParserTest.java b/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoParserTest.java deleted file mode 100644 index 6d51586..0000000 --- a/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/domain/service/tools/KakaoParserTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.checkinrequestMS.PlaceAPI.domain.service.tools; - -import com.example.checkinrequestMS.PlaceAPI.domain.Place; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class KakaoParserTest { - - @Test - void parsePlaceInfo() { - } - private void print(List places) { - for (Place place : places) { - System.out.println("장소명: " + place.getPlaceName()); - System.out.println("주소: " + place.getAddressName()); - System.out.println("도로명 주소: " + place.getRoadAddressName()); - System.out.println("카테고리: " + place.getCategoryName()); - System.out.println("전화번호: " + place.getPhone()); - System.out.println("장소 URL: " + place.getPlaceUrl()); - System.out.println("X 좌표: " + place.getX()); - System.out.println("Y 좌표: " + place.getY()); - System.out.println("--------------------"); - } - - // 메타 정보 출력 - //JsonNode meta = rootNode.get("meta"); - //System.out.println("총 검색 결과 수: " + meta.get("total_count").asInt()); - //System.out.println("페이지 결과 여부: " + (meta.get("is_end").asBoolean() ? "마지막 페이지" : "더 있음")); - } -} \ No newline at end of file diff --git a/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/web/rest/KakaoAPIRequestTest.java b/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/web/rest/KakaoAPIRequestTest.java index 3913060..aac7774 100644 --- a/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/web/rest/KakaoAPIRequestTest.java +++ b/check-in-request-MS/src/test/java/com/example/checkinrequestMS/PlaceAPI/web/rest/KakaoAPIRequestTest.java @@ -1,17 +1,38 @@ package com.example.checkinrequestMS.PlaceAPI.web.rest; +import com.example.checkinrequestMS.PlaceAPI.web.restAPI.KakaoStoreAPIRequest; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest +@Disabled +// Kakao API를 요청해보는 테스트 코드 입니다. +// Kakao API에서의 변경에 따라 테스트가 깨질 가능성이 있어 Disabled 처리하였습니다. class KakaoAPIRequestTest { + @Autowired + KakaoStoreAPIRequest kakaoAPIRequest; + @Test - void test() { - StringBuilder response = KakaoAPIRequest.getStoreInfo("맛집", 126.98561429978552, 37.56255453417897, 50); - System.out.println(response.toString()); + @DisplayName("카카오 맵 API 가게정보 기능 정상 통신 됨") + void getStoreInfo() { + //given + String query = "맛집"; + double x = 126.98561429978552; + double y = 37.56255453417897; + int radius = 50; + + //when + String response = kakaoAPIRequest.getStoreInfo(query, x, y, radius); + + //then + response = response.replace("[", "").replace("]", ""); + String[] all = response.split("}"); + assertEquals(25, all.length); } } \ No newline at end of file diff --git a/check-in-request-MS/src/test/resources/application.yml b/check-in-request-MS/src/test/resources/application.yml index 3ab60b2..8b636e9 100644 --- a/check-in-request-MS/src/test/resources/application.yml +++ b/check-in-request-MS/src/test/resources/application.yml @@ -1,2 +1,19 @@ -kakao: - apikey: a0dc4e7625b15b5b4cef4e0a028119b3 +spring: + application: + name: member-context + datasource: + url: jdbc:mysql://localhost:3306/place?useSSL=false&useUnicode=true&allowPublicKeyRetrieval=true + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: 1234 + jpa: + show-sql: true + hibernate: + ddl-auto: none #개발 환경이라 create, update로도 가끔 쓰고 있습니다. + database-platform: org.hibernate.dialect.MySQLDialect + generate-ddl: true +server: + port: 8081 + +kakao-map-api: + key: a0dc4e7625b15b5b4cef4e0a028119b3 diff --git a/member-context/src/test/java/com/membercontext/memberAPI/application/aop/authentication/AuthenticationAspectTest.java b/member-context/src/test/java/com/membercontext/memberAPI/application/aop/authentication/AuthenticationAspectTest.java index 38d5d4b..4fe88c6 100644 --- a/member-context/src/test/java/com/membercontext/memberAPI/application/aop/authentication/AuthenticationAspectTest.java +++ b/member-context/src/test/java/com/membercontext/memberAPI/application/aop/authentication/AuthenticationAspectTest.java @@ -137,7 +137,6 @@ void authenticateAOP_No_Cookie() throws Exception { assertTrue(result.getResolvedException() instanceof MemberException); assertEquals("이력을 찾을 수 없습니다. 로그인이 필요합니다.", result.getResolvedException().getMessage()); }); - //fixme: exception을 이렇게 처리하는게 맞을 지. verify(authenticationAspect, times(1)).authenticate(); } diff --git a/member-context/src/test/java/com/membercontext/memberAPI/infrastructure/encryption/JavaCrpytoUtilTest.java b/member-context/src/test/java/com/membercontext/memberAPI/infrastructure/encryption/JavaCrpytoUtilTest.java index 11379b3..2b41603 100644 --- a/member-context/src/test/java/com/membercontext/memberAPI/infrastructure/encryption/JavaCrpytoUtilTest.java +++ b/member-context/src/test/java/com/membercontext/memberAPI/infrastructure/encryption/JavaCrpytoUtilTest.java @@ -15,7 +15,7 @@ class JavaCrpytoUtilTest { @Autowired private JavaCryptoUtil javaCryptoUtil; - //todo: 단위 테스트 작성. + //check: 단위 테스트 작성. @Test void encrypt_With_IV(){ //given