Optional<Restaurant>
을 통하여 findById 테스트Repository
public interface RestaurantsRepository extends JpaRepository<Restaurant, Long> {
@Query(value = "select r.id from Restaurant r", nativeQuery = true)
public List<Long> findAllreturnId();
@Query(value = "SELECT R.ID, " +
"R.BUSINESS_HOURS, R.BOOKING_REVIEW_SCORE," +
"R.NAME,R.IMAGE_URL,R.CATEGORY," +
"R.RESTAURANT_TYPE,R.SAVE_COUNT," +
"R.VISITOR_REVIEW_SCORE," +
"ST_Distance_Sphere(Point(:x,:y),POINT(R.X, R.Y)) AS diff_Distance," +
"R.X," +
"R.Y," +
"R.ADDRESS " +
"FROM RESTAURANT AS R HAVING diff_Distance <= 1000 order by diff_Distance", nativeQuery = true)
public List<RestaurantDTOInterface> getRestaurantByLocation(@Param("x") Double x, @Param("y") Double y);
Optional<Restaurant> findRestaurantById(long id);
}
Optional 사용 이유 ?
각 레스토랑은 유일한 ID 값을 가지는 것을 확인하였습니다. Optional 동작시 반환값이 null 일 경우에 respository.findRestaurantById.orElse() 를 통하여 값이 없는 경우 repository에 추가할 수 있도록 합니다
Repository 테스트
@Test
@DisplayName("카테고리별 음식점 모두 스크래핑후 보내기")
@BeforeEach
public void getRestaurantData_v2 () throws Exception {
for (RestaurantType type : RestaurantType.values()) {
// 카테고리내의 모든 음식들을 크롤링
String url = "/graphql";
String _url = HOST_v2+url;
GetRestaurantRequest request = GetRestaurantRequest.builder()
.x(x)
.y(y)
.bounds("126.9738873;37.5502692;126.9980272;37.5696434")
.query("음식점")
.type(type)
.build();
String jsonOperation = naverUtility.getRestaurants(request);
HttpHeaders httpHeaders = utility.getDefaultHeader();
HttpEntity requestMessage = new HttpEntity(jsonOperation,httpHeaders);
ResponseEntity response = restTemplate.exchange(
_url,
HttpMethod.POST,
requestMessage,
String.class);
List<Restaurant> entities = new ArrayList<>();
JSONArray datas = new JSONArray(response.getBody().toString());
datas.getJSONObject(0);
JSONArray items = datas.getJSONObject(0).getJSONObject("data").getJSONObject("restaurants").getJSONArray("items");
int total = Integer.parseInt(datas.getJSONObject(0).getJSONObject("data").getJSONObject("restaurants").get("total").toString());
int maxCnt = total < 100 ? total : 100;
for (int i = 0; i < maxCnt; i++) {
GetRestaurantResponse mapped_data = gson.fromJson(items.get(i).toString(), GetRestaurantResponse.class);
//1. first map with entity : 엔티티와 매핑하기전 validation을 거친다
Restaurant restaurant = Restaurant.builder()
.id(mapped_data.getId())
.address(mapped_data.getAddress())
.category(mapped_data.getCategory() == null ? "없음" : mapped_data.getCategory())
.imageUrl(mapped_data.getImageUrl() == null ? "" : URLDecoder.decode(mapped_data.getImageUrl(), "UTF-8"))
.name(mapped_data.getName())
// .distance(utility.stringToLongDistance(mapped_data.getDistance()))
.businessHours(mapped_data.getBusinessHours())
.visitorReviewScore(mapped_data.getVisitorReviewScore() == null ? 0.0 : Double.parseDouble(mapped_data.getVisitorReviewScore()))
.saveCount(utility.stringToLongSaveCnt(mapped_data.getSaveCount()))
.bookingReviewScore(mapped_data.getBookingReviewScore())
.restaurantType(type)
.x(mapped_data.getX())
.y(mapped_data.getY())
.build();
entities.add(restaurant);
}
restaurantsRepository.saveAll(entities);
}
}
@Test
public void givenRestaurantID_whenFindallexist_thenInsertRestaurants() throws Exception{
//given
List<Restaurant> restaurants = restaurantsRepository.findAll();
//when
for (Restaurant entity : restaurants) {
// findby id
Restaurant newJoined = restaurantsRepository.findRestaurantById(entity.getId())
.orElse(restaurantsRepository.save(entity));
}
//then
}
로그확인하기
spring.jpa.properties.hibernate.show_sql=true
를 해두었기 때문에 쿼리 로그를 보면서 추가된 값이 있는지 확인합니다.
디버그 모드로 하여 중단점 잡기
delete
하여 정상적으로 없는 값일 경우 insert
가 진행되는지 확인합니다레스토랑 정보가 restaurants 변수에 담긴것을 확인한 뒤 콘솔에서 몇개를 골라 삭제합니다
하이라이트된 값을 삭제해줍니다
추가로 DB 내에서도 조회하여 확인 결과 잘 삭제되었음을 알 수 있습니다
cleanUp ( db 삭제 ) 가 일어나기 전에 중단점을 잡고 결과를 확인합니다
최종 확인
UserController
@PostMapping("/postXY")
public String postXY(AddressDTO dto
, @LoginUser SessionUser user){
user.setX(dto.getX());
user.setY(dto.getY());
User newUser = userService.saveOrUpdateXY(user);
httpSession.setAttribute("user", new SessionUser(newUser));
return "redirect:/restaurant/addNearest";
}
RestaurantController
@Controller
@RequestMapping("/restaurant")
public class RestaurantController {
@Autowired
private RestaurantService restaurantService;
@Autowired
private RestaurantsRepository restaurantsRepository;
@SneakyThrows
@GetMapping("/addNearest")
public String addNearestRestaurant(@LoginUser SessionUser user){
// 신규 주변 음식점 정보가 있다면 insert, 이후 restaurant view 로 이동
GetRestaurantRequest request = GetRestaurantRequest.builder()
.x(String.valueOf(user.getX()))
.y(String.valueOf(user.getY()))
.build();
restaurantService.getRestaurantData(request);
return "redirect:/restaurant/main"; // 메인 페이지로 이동
}
@GetMapping("/main")
public String main(@LoginUser SessionUser user
,Model model){
// 현재 위치 기준 가까운 순으로 정렬
AddressDTO request = AddressDTO.builder()
.x(user.getX())
.y(user.getY())
.build();
// 가까운 음식점 정보를 가져옴
List<RestaurantDTO> datas = restaurantService.getRestaurantDTO(request);
return "restaurant";
}
}
RestaurantService
@Override
public List<RestaurantDTO> getRestaurantDTO(AddressDTO request) {
// Point 간의 거리를 통하여 가까운 음식점 정보를 db 에서 매칭
List<RestaurantDTOInterface> interfaces = restaurantsRepository.getRestaurantByLocation(request.getX(), request.getY());
// interface -> dto 로
List<RestaurantDTO> dtos = RestaurantDTO.interfaceToDto(interfaces);
return dtos;
}
interfaceToDto
public static List<RestaurantDTO> interfaceToDto(List<RestaurantDTOInterface> dtoInterfaces){
List<RestaurantDTO> dtos = new ArrayList<>();
for (RestaurantDTOInterface dtoInterface : dtoInterfaces) {
dtos.add(RestaurantDTO.builder()
.x(dtoInterface.getX())
.y(dtoInterface.getY())
.name(dtoInterface.getName())
.id(dtoInterface.getId())
.address(dtoInterface.getAddress())
.category(dtoInterface.getCategory())
.image_Url(dtoInterface.getImage_Url())
.diff_Distance(dtoInterface.getDiff_Distance())
.business_Hours(dtoInterface.getBusiness_Hours())
.visitor_Review_Score(dtoInterface.getVisitor_Review_Score())
.save_Count(dtoInterface.getSave_Count())
.booking_Review_Score(dtoInterface.getBooking_Review_Score())
.restaurant_Type(dtoInterface.getRestaurant_Type())
.build());
}
return dtos;
}
피들러 확인
View
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<!-- <meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>-->
<!-- <meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>-->
<title>Title</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<h1>음식점 리스트</h1>
<div class="col-md-12">
<!-- 로그인 기능 영역 -->
<div class="row">
<div class="col-md-6">
<table>
<thead>
<tr>
<th>순번</th>
<!-- <th>이미지</th>-->
<th>음식점명</th>
<th>대표메뉴</th>
<th>거리(m)</th>
<th>주소</th>
<th>영업시간</th>
<th>방문자리뷰</th>
<th>예약리뷰</th>
<th>카테고리</th>
</tr>
</thead>
<tbody>
<tr th:each="restaurantDTO, status:${restaurants}">
<td th:text="${status.index}"></td>
<!-- <td><img id="imgId" th:src="${restaurantDTO.image_Url}" alt="첨부이미지" /></td>-->
<td th:text="${restaurantDTO.name}"></td>
<td th:text="${restaurantDTO.category}"></td>
<td th:text="${restaurantDTO.diff_Distance}"></td>
<td th:text="${restaurantDTO.address}"></td>
<td th:text="${restaurantDTO.business_Hours}"></td>
<td th:text="${restaurantDTO.visitor_Review_Score}"></td>
<td th:text="${restaurantDTO.booking_Review_Score}"></td>
<td th:text="${restaurantDTO.category}"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
Trouble Shooting
해당 결과를 보면 알 수 있듯 이미지 url 상에 인코딩 이슈가 있는것으로 보인다. 원인 제공처로 유추해볼 곳은 다음과 같다
1. 헤더에서의 인코딩 설정 - UTF_8 로 바꾸어 보았다 (X)public HttpHeaders getDefaultHeader(){ MediaType mediaType = new MediaType("application", "json", Charset.forName("UTF-8")); HttpHeaders httpHeaders = new HttpHeaders(); MultiValueMap<String, String> headerValues = new LinkedMultiValueMap<>(); headerValues.add(HttpHeaders.ACCEPT, "*/*"); headerValues.add(HttpHeaders.HOST, HOST_v2); headerValues.add(HttpHeaders.USER_AGENT, USER_AGENT); headerValues.add("Referer", REFERER); headerValues.add("Connection","keep-alive"); httpHeaders.addAll(headerValues); httpHeaders.setContentType(mediaType); return httpHeaders; }
- restTemplate 의 인코딩 문제 - StringHttpMessageConverter 설정을 utf8로 바꾸었다 (X)
restTemplate.getMessageConverters() .add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
- response 에는 문제가 없음을 확인하고 매핑하는 구간에 로깅하여 확인해보았다
매핑 전에는 문제없이 받아온다
- 기존 매핑
Restaurant newJoined = restaurantsRepository.findRestaurantById(mapped_data.getId()) .orElse(restaurantsRepository.save(Restaurant.builder() .id(mapped_data.getId()) .address(mapped_data.getAddress()) .category(mapped_data.getCategory() == null ? "없음" : mapped_data.getCategory()) .imageUrl(mapped_data.getImageUrl() == null ? "" : URLDecoder.decode(mapped_data.getImageUrl(), "UTF-8")) .name(mapped_data.getName()) .businessHours(mapped_data.getBusinessHours()) .visitorReviewScore(mapped_data.getVisitorReviewScore() == null ? 0.0 : Double.parseDouble(mapped_data.getVisitorReviewScore())) .saveCount(utility.stringToLongSaveCnt(mapped_data.getSaveCount())) .bookingReviewScore(mapped_data.getBookingReviewScore()) .restaurantType(type) .x(mapped_data.getX()) .y(mapped_data.getY()) .build()));
- URLDecoder.decode 를 사용하여 uricomponent 를 디코딩해주었는데 이곳에서 에러가 났음을 알 수 있었다 >> 변경 후
.imageUrl(mapped_data.getImageUrl() == null ? "" : mapped_data.getImageUrl())
- 사실 디코딩하지 않아도 url 그대로 src 설정해주면 될텐데.. 그때는 무슨 생각이였는지 모르겠다
[결과]