PathVariable 을 통한 상세 메뉴 얻기

JungWooLee·2022년 9월 13일
0

SpringBoot ToyProject

목록 보기
14/14

이어서 하기

음식점 id를 통하여 JPA 로 findMenuById 하였을때 반환값이 null 이라면 새롭게 스크래핑 할 수 있도록 한다


MenuRepository

public interface MenuRepository extends JpaRepository<Menu, String> {
    Optional<List<Menu>> findMenuByRestaurant(long restaurantId);
}
  • Optional 을 통하여 RestaurantId를 가진 칼럼이 존재하지 않는 경우 스크래핑할 수 있도록 한다

실패

	@Test
    public void findByIdIfNotExistScrap() throws Exception {
        // given
        List<Long> ids = Arrays.asList(100047979l, 1020517697l, 1094455540l, 1101323127l);
        try {
            for (Long id : ids) {
                Restaurant restaurant = restaurantsRepository
                        .findById(id)
                        .orElseThrow(() -> new IllegalArgumentException("restaurant cant be null"));
                List<Menu> menu = menuRepository
                        .findMenuByRestaurant(id)
                        .orElseGet(() -> crawlMenuDatas(id, restaurant));
                menuRepository.saveAll(menu);
            }
        } catch (Exception err) {
            System.out.println(err.getMessage());
        }
    }
  • 메뉴 정보를 불러오는 것 까지는 완료되지만 List<Menu> 로 불러오면서 Optional 기능이 적용되고 이로인해 만약 List<Menu> 가 find 되지 않을시 List 는 empty 값으로 처리되기 때문에 orElse에 거쳐지지 않는다
  • Optional<List<Entity>> 를 적용하고자 한다면 반환값은 List / not List 가 되어야 한다. 하지만 위 경우에는 빈 리스트가 반환되기 때문에 결국 orElse로 걸쳐질 수 없다
  • 결국 생각해보면 JPA에서 List<?> 로 반환되는 값은 null일 경우가 없기때문에 궂이 Optional로 지정할 필요가 없다. 즉 아래와 같이 바꿔 적용 List<Menu> findMenuByRestaurant(Restaurant restaurant);

Repository Test

	@Test
    public void findByIdIfNotExistScrap() throws Exception {
        // given
        List<Menu> allMenus = new ArrayList<>();
        List<Long> ids = Arrays.asList(11678758l,
                11678838l,
                11679306l,
                11679353l,
                11679393l,11679455l);
        try {
            for (Long id : ids) {
                Restaurant restaurant = restaurantsRepository
                        .findById(id)
                        .orElseThrow(() -> new IllegalArgumentException("restaurant cant be null"));
                List<Menu> menus = menuRepository
                        .findMenuByRestaurant(restaurant);
                if(menus == null || menus.isEmpty()){
                    // 리스트가 비어있다면 스크래핑
                    crawlMenuDatas(id, restaurant);
                }
                allMenus.addAll(menus);
            }
            menuRepository.saveAll(allMenus);
        } catch (Exception err) {
            System.out.println(err.getMessage());
        }
    }

    public List<Menu> crawlMenuDatas(long restaurantId, Restaurant entity) {
        List<Menu> menus = new ArrayList<>();
        try {
            long id = restaurantId;
            String url = String.format("/restaurant/%d/menu/list", id);
            String _url = HOST_v1 + url;

            HttpHeaders httpHeaders = utility.getDefaultHeader();

            HttpEntity requestMessage = new HttpEntity(httpHeaders);
            ResponseEntity response = null;
            // when

            response = restTemplate.exchange(
                    _url,
                    HttpMethod.GET,
                    requestMessage,
                    String.class);


            // 음식점 정보들 파싱
            Document doc = Jsoup.parse((String) response.getBody());

            Element scriptElement = doc.getElementsByTag("script").get(2);
            String innerJson = scriptElement.childNode(0).toString();
            int start = innerJson.indexOf("window.__APOLLO_STATE__");
            int end = innerJson.indexOf("window.__PLACE_STATE__");
            // JSON으로 파싱
            JSONObject target = new JSONObject(innerJson.substring(start, end).substring(25));

            JSONArray jsonArray = target.names();
            List<String> restaurantList = new ArrayList<>();
            for (int i = 0; i < jsonArray.length(); i++) {
                String possible = jsonArray.get(i).toString();
                // 레스토랑 정보를 갖고 있는 곳은 RestaurantListSummary:XXXXXX 의 형태를 띄며 한번의 스크래핑에서 50개의 결과값이 나오게 된다
                if (possible.contains("Menu")) {
                    restaurantList.add(possible);
                }
            }
//
            for (String s : restaurantList) {
                // 해당 JObject와 Response 객체간의 매핑
                MenuResponseDto mapped_data = gson.fromJson(target.get(s).toString(), MenuResponseDto.class);
                Menu data = Menu.builder()
                        .id(mapped_data.getId())
                        .restaurant(entity)
                        .description(mapped_data.getDescription())
                        .name(mapped_data.getName())
                        .priority(mapped_data.getPriority())
                        .build();
                menus.add(data);
            }
        }
        catch (Exception err){
            System.out.println(err.getMessage());
        }
        return menus;
    }
  • 앞서 음식점 정보가 있다고 가정하고 진행합니다.
  • 음식점 정보는 있으나 메뉴 정보가 없을 때 스크래핑을 진행합니다
  • 스크래핑 후 엔티티를 저장합니다

MenuDTO

@Getter
@Setter
@ToString
public class MenuDTO {

    private String name;
    private int price;
    private String[] images;
    private String description;
    private String id;
    private int priority;

    public MenuDTO(Menu entity) {
        this.name = entity.getName();
        this.price = entity.getPrice();
        this.images = entity.getImages().toArray(String[]::new);
        this.description = entity.getDescription();
        this.id = entity.getId();
        this.priority = entity.getPriority();
    }
}
  • 엔티티를 그대로 뷰에서 사용하는 것은 매우 위험합니다. 사용자의 악의 적인 조작으로 엔티티의 수정이 일어날 수 있기 때문 (그래서 엔티티는 Setter를 지정하지 않음)
  • 엔티티를 DTO 생성자를 통하여 생성하여 View 에 뿌려줍니다

Service

public List<MenuDTO> findMenusById(long restaurantId) {
        List<MenuDTO> result = null;
        Restaurant restaurant = restaurantsRepository
                .findById(restaurantId)
                .orElseThrow(() -> new IllegalArgumentException("잘못된 접근입니다."));
        List<Menu> menus = menuRepository
                .findMenuByRestaurant(restaurant);
        if(menus == null || menus.isEmpty()){
            // 리스트가 비어있다면
            result = crawlMenuDatas(restaurantId, restaurant);
        }
        else{
            // 만약 리스트가 비어있지 않다면 원래 데이터를 DTO로 변환
            result = menus.stream()
                    .map(menu -> new MenuDTO(menu)).collect(Collectors.toList());
        }
        return result;
    }
  • 리스트가 비어있다면 새로운 데이터를 크롤링 하도록 합니다
  • 이후에 DTO 로 변환 후 반환합니다

Controller

	@GetMapping("/{id}")
    public String findById(@PathVariable long id, Model model){
        List<MenuDTO> menus = restaurantService.findMenusById(id);
        model.addAttribute("menus", menus);
        return "restaurant-detail";
    }
  • 매핑 되는 주소는 restaurant/{id} 입니다
  • @PathVariable을 통하여 id 값을 갖고 서비스단에서 비즈니스 로직을 처리합니다
  • 비즈니스 로직 수행 후 restaurant-detail.html 로 이동합니다

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>
                </tr>
                </thead>
                <tbody>
                <tr th:each="menuDTO, status:${menus}">
                    <td th:text="${status.index}"></td>
                    <!--                        <td><img id="imgId" th:src="${restaurantDTO.image_Url}" alt="첨부이미지" /></td>-->
                    <td th:text="${menuDTO.name}"></td>
                    <td th:text="${menuDTO.price}"></td>
<!--                    <td th:text="${MenuResponseDto.images}"></td>-->
                    <td th:text="${menuDTO.description}"></td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

결과 조회

0개의 댓글