Skip to content

Commit

Permalink
Merge pull request #23 from f-lab-edu/009-Kakao-Saving-Store-Info
Browse files Browse the repository at this point in the history
[009-Kakao-Saving-Store-Info] 가게 정보 저장 및 검색 1차
  • Loading branch information
ScottSung7 authored Aug 4, 2024
2 parents 82eefdd + 04a50bf commit fb62386
Show file tree
Hide file tree
Showing 25 changed files with 618 additions and 212 deletions.
5 changes: 2 additions & 3 deletions check-in-request-MS/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.checkinrequestMS.PlaceAPI.common.exception;



public class DomainException extends RuntimeException{

public DomainException(String errorMessage) {
super(errorMessage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.checkinrequestMS.PlaceAPI.common.exception;



public class WebException extends RuntimeException{

public WebException(String errorMessage){
super(errorMessage);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}

Original file line number Diff line number Diff line change
@@ -1,31 +1,59 @@
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;
private String placeUrl;
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());
this.setPlaceUrl(document.get("place_url").asText());
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());
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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<Place> searchWithKeyword(String query, double x, double y, int radius) {

public List<Place> 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<Place> places = storeRepository.getStoresByNameAndRadius(x, y, radius)
.orElseThrow(() -> new PlaceException(NO_PLACE_INFO));

return places;
}

}
Original file line number Diff line number Diff line change
@@ -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<Place> placesFromAPI = parsePlaceInfo(response); //Brute Force 이후 범위를 경도/위도 조합을 스캔하며 저장해서 끝난 부분은 다시 검색 안해도 됨.
List<Place> placesFromDB = storeRepository.getStoresByNameAndRadius(x, y, radius).get();
List<Place> 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<Place> parsePlaceInfo(String response){
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(response);

List<Place> 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로 변환 중 에러.");
}
}

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<Place, Long> {
@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<List<Place>> getStoresByNameAndRadius(double x, double y, int radius);
}//이름은 아직 미구현.
Original file line number Diff line number Diff line change
@@ -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<ExceptionResponse> domainException(final DomainException e){
return ResponseEntity.badRequest().body(new ExceptionResponse(HttpStatus.BAD_REQUEST, e.getMessage()));
}

@ExceptionHandler({
WebException.class
})
public ResponseEntity<ExceptionResponse> webException(final WebException e){
return ResponseEntity.badRequest().body(new ExceptionResponse(HttpStatus.BAD_REQUEST, e.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}



}
Loading

0 comments on commit fb62386

Please sign in to comment.