[Spring] 네이버 검색 API를 활용한 맛집 List 만들기

WOOK JONG KIM·2022년 11월 16일
0

패캠_java&Spring

목록 보기
50/103
post-thumbnail

Memory CRUD DB

jpa 사용하지 않고 간단하게 Repository 구현

Memory Db 인터페이스

public interface MemoryDbRepositoryIfs<T> {
    Optional<T> findById(int index);
    T save(T entity);
    void deleteById(int index);
    List<T> listAll();
}

Memory Db 추상 클래스

// 와일드 카드를 통해 MemoryDbEntity 상속 받은 것만 타입으로 지정 가능
abstract public class MemoryDbRepositoryAbstract<T extends MemoryDbEntity> implements MemoryDbRepositoryIfs<T>{

    private final List<T> db = new ArrayList<>();
    private int index = 0;

    @Override
    public Optional<T> findById(int index) {
        return db.stream().filter(it -> it.getIndex() == index).findFirst();
    }

    @Override
    public T save(T entity) {

        var OptionalEntity = db.stream().filter(it -> it.getIndex() == entity.getIndex()).findFirst();
        //이미 데이터 있는 경우, 없는 경우
        if(OptionalEntity.isEmpty()){
            index++;
            entity.setIndex(index);
            db.add(entity);
            return entity;

        }else{
            var preIndex = OptionalEntity.get().getIndex();
            entity.setIndex(preIndex);

            deleteById(preIndex);
            db.add(entity);
            return entity;
        }

    }

    @Override
    public void deleteById(int index) {
        var optionalEntity = db.stream().filter(it -> it.getIndex() == index).findFirst();
        if(optionalEntity.isPresent()){
            db.remove(optionalEntity.get());
        }
    }

    @Override
    public List<T> listAll() {
        return db;
    }
}

MemoryDb

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemoryDbEntity {
    protected int index;
}

맛집 정보 Entity

@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class WishListEntity extends MemoryDbEntity {

    private String title;       //음식명
    private String category;    // 카테고리
    private String address;     // 주소
    private String readAddress; // 도로
    private String homePageLink; // 홈페이지 주소
    private String imageLink; // 음식, 가제 이미지 주소
    private boolean isVisit; // 방문 여부
    private int visitCount; // 방문 횟수
    private LocalDateTime lastVisitDate; // 마지막 방문 일자

}

맛집 Repository

@Repository
public class WishListRepository extends MemoryDbRepositoryAbstract<WishListEntity> {
}

Memory Db Test 코드

@SpringBootTest
public class WishListRepositoryTest {

    @Autowired
    private WishListRepository wishListRepository;

    private WishListEntity create(){
        var wishList = new WishListEntity();
        wishList.setTitle("title");
        wishList.setCategory("category");
        wishList.setAddress("address");
        wishList.setReadAddress("readAddress");
        wishList.setHomePageLink("");
        wishList.setImageLink("");
        wishList.setVisit(false);
        wishList.setVisitCount(0);
        wishList.setLastVisitDate(null);

        return wishList;
    }

    @Test
    public void saveTest(){
        var wishListEntity = create();
        var expected = wishListRepository.save(wishListEntity);

        Assertions.assertNotNull(expected);
        Assertions.assertEquals(1, expected.getIndex());
    }

    @Test
    public void updateTest(){
        var wishListEntity = create();
        var expected = wishListRepository.save(wishListEntity);

        expected.setTitle("update test");
        var saveEntity = wishListRepository.save(expected);

        Assertions.assertEquals("update test", saveEntity.getTitle());
        Assertions.assertEquals(1, wishListRepository.listAll().size());
    }

    @Test
    public void findByIdTest(){
        var wishListEntity = create();
        wishListRepository.save(wishListEntity);

        var expected = wishListRepository.findById(1);

        Assertions.assertEquals(true, expected.isPresent());
        Assertions.assertEquals(1, expected.get().getIndex());
    }

    @Test
    public void deleteTest(){
        var wishListEntity = create();
        wishListRepository.save(wishListEntity);

        wishListRepository.deleteById(1);

        int count = wishListRepository.listAll().size();

        Assertions.assertEquals(0, count);
    }

    @Test
    public void ListAllTest(){
        var wishListEntity1 = create();
        wishListRepository.save(wishListEntity1);

        var wishListEntity2 = create();
        wishListRepository.save(wishListEntity2);

        int count = wishListRepository.listAll().size();

        Assertions.assertEquals(2, count);
    }
}

Rest API 개발

application.yaml

naver:
  url:
    search:
      local : https://openapi.naver.com/v1/search/local.json
      image: https://openapi.naver.com/v1/search/image

  client:
    id: RZPgllftwhnJ8xRtXmxk
    secret: 

지역 검색 요청

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchLocalReq {

    private String query = "";

    private int display = 1;

    private int start = 1;

    private String sort = "random";

    // 나중에 queryparams에 인자로 사용하기 위해 MultiValueMap 사용
    public MultiValueMap<String,String> toMultiValueMap(){
        var map = new LinkedMultiValueMap<String, String>();

        map.add("query", query);
        map.add("display", String.valueOf(display));
        map.add("start", String.valueOf(start));
        map.add("sort", sort);

        return map;
    }
}

지역 검색 응답

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchLocalRes {

    private String lastBuildDate;
    private int total;
    private int start;
    private int display;
    private List<SearchLocalItem> items;


    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class SearchLocalItem{
        private String title;
        private String category;
        private String link;
        private String description;
        private String telephone;
        private String address;
        private String roadAddress;
        private int mapx;
        private int mapy;
    }
}

searchLocal 메서드

	@Value("${naver.client.id}")
    private String naverClientId;

    @Value("${naver.client.secret}")
    private String naverClientSecret;

    @Value("${naver.url.search.local}")
    private String naverLocalSearchUrl;

    @Value("${naver.url.search.image}")
    private String naverImageSearchUrl;

    public SearchLocalRes searchLocal(SearchLocalReq searchLocalReq){
        URI uri = UriComponentsBuilder.fromUriString(naverLocalSearchUrl)
                .queryParams(searchLocalReq.toMultiValueMap())
                .build()
                .encode()
                .toUri();

        var headers = new HttpHeaders();
        headers.set("X-Naver-Client-Id", naverClientId);
        headers.set("X-Naver-Client-Secret", naverClientSecret);
        headers.setContentType(MediaType.APPLICATION_JSON);

        var httpEntity = new HttpEntity<>(headers);
        var responseType = new ParameterizedTypeReference<SearchLocalRes>(){};

        var responseEntity= new RestTemplate().exchange(
                uri,
                HttpMethod.GET,
                httpEntity,
                responseType
        );

        return responseEntity.getBody();

    }

searchImage 메서드
-> 이때 Image req,res 위와 비슷

public SearchImageRes searchImage(SearchImageReq searchImageReq){

        URI uri = UriComponentsBuilder.fromUriString(naverImageSearchUrl)
                .queryParams(searchImageReq.toMultiValueMap())
                .build()
                .encode()
                .toUri();

        var headers = new HttpHeaders();
        headers.set("X-Naver-Client-Id", naverClientId);
        headers.set("X-Naver-Client-Secret", naverClientSecret);
        headers.setContentType(MediaType.APPLICATION_JSON);

        var httpEntity = new HttpEntity<>(headers);
        var responseType = new ParameterizedTypeReference<SearchImageRes>(){};

        var responseEntity = new RestTemplate().exchange(
                uri, HttpMethod.GET, httpEntity, responseType
        );

        return responseEntity.getBody();
    }

WishListDto

@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class WishListDto {

    private int index;
    private String title;       //음식명
    private String category;    // 카테고리
    private String address;     // 주소
    private String roadAddress; // 도로
    private String homePageLink; // 홈페이지 주소
    private String imageLink; // 음식, 가제 이미지 주소
    private boolean isVisit; // 방문 여부
    private int visitCount; // 방문 횟수
    private LocalDateTime lastVisitDate; // 마지막 방문 일자

}

Service 구현

@Service
@RequiredArgsConstructor
public class WishListService {

    private final NaverClient naverClient;

    private final WishListRepository wishListRepository;

    public WishListDto search(String query){

        // 지역 검색
        var searchLocalReq = new SearchLocalReq();
        searchLocalReq.setQuery(query);

        var searchLocalRes = naverClient.searchLocal(searchLocalReq);

        if(searchLocalRes.getTotal() > 0){
            var localItem = searchLocalRes.getItems().stream().findFirst().get();

            //괄호 쳐진거 다 없앰
            var imageQuery = localItem.getTitle().replaceAll("<[^>]*>", "");
            var searchImageReq = new SearchImageReq();
            searchImageReq.setQuery(imageQuery);

            // 이미지 검색
            var searchImageRes = naverClient.searchImage(searchImageReq);

            // 결과 리턴
            if(searchImageRes.getTotal() > 0){
                var imageItem = searchImageRes.getItems().stream().findFirst().get();

                var result = new WishListDto();
                result.setTitle(localItem.getTitle());
                result.setCategory(localItem.getCategory());
                result.setAddress(localItem.getAddress());
                result.setRoadAddress(localItem.getRoadAddress());
                result.setHomePageLink(localItem.getLink());
                result.setImageLink(imageItem.getLink());

                return result;
            }
        }
        return new WishListDto();
    }

    public WishListDto add(WishListDto wishListDto) {
        var entity = dtoToEntity(wishListDto);
        var saveEntity = wishListRepository.save(entity);
        return entityToDto(saveEntity);
    }

    private WishListEntity dtoToEntity(WishListDto wishListDto){
        var entity = new WishListEntity();
        entity.setIndex(wishListDto.getIndex());
        entity.setTitle(wishListDto.getTitle());
        entity.setCategory(wishListDto.getCategory());
        entity.setAddress(wishListDto.getAddress());
        entity.setRoadAddress(wishListDto.getRoadAddress());
        entity.setHomePageLink(wishListDto.getHomePageLink());
        entity.setImageLink(wishListDto.getImageLink());
        entity.setVisit(wishListDto.isVisit());
        entity.setVisitCount(wishListDto.getVisitCount());
        entity.setLastVisitDate(wishListDto.getLastVisitDate());

        return entity;
    }

    private WishListDto entityToDto(WishListEntity wishListEntity){
        var dto = new WishListDto();

        dto.setIndex(wishListEntity.getIndex());
        dto.setTitle(wishListEntity.getTitle());
        dto.setCategory(wishListEntity.getCategory());
        dto.setAddress(wishListEntity.getAddress());
        dto.setRoadAddress(wishListEntity.getRoadAddress());
        dto.setHomePageLink(wishListEntity.getHomePageLink());
        dto.setImageLink(wishListEntity.getImageLink());
        dto.setVisit(wishListEntity.isVisit());
        dto.setVisitCount(wishListEntity.getVisitCount());
        dto.setLastVisitDate(wishListEntity.getLastVisitDate());

        return dto;
    }

    public List<WishListDto> findAll() {

        return wishListRepository.findAll()
                .stream().map(it -> entityToDto(it)).collect(Collectors.toList());
    }

    public void delete(int index) {
        wishListRepository.deleteById(index);
    }

    public void addVisit(int index){
        var wishItem = wishListRepository.findById(index);
        if(wishItem.isPresent()){
            var item = wishItem.get();

            item.setVisit(true);
            item.setVisitCount(item.getVisitCount() + 1);
        }
    }
}

Controller

@RestController
@Slf4j
@RequestMapping("/api/restaurant")
@RequiredArgsConstructor // final이나 NotNull이 붙은 필드의 생성자 자동 생성
public class ApiController {

    private final WishListService wishListService;

    @GetMapping("/search")
    public WishListDto search(@RequestParam String query){
        return wishListService.search(query);
    }

    // 위시리스트에 추가
    @PostMapping("")
    public WishListDto add(@RequestBody WishListDto wishListDto){
        log.info("{}", wishListDto);
        return wishListService.add(wishListDto);
    }

    // 위시리스트의 모든것
    @GetMapping("/all")
    public List<WishListDto> findAll(){
        return wishListService.findAll();
    }
    
    // 위시 리스트에서 제거
    @DeleteMapping("/{index}")
    public void delete(@PathVariable int index){
        wishListService.delete(index);
    }

    //방문 횟수
    @PostMapping("/{index}")
    public void addVisit(@PathVariable int index){
        wishListService.addVisit(index);
    }
}

Front

main.js

(function ($) {

    // 검색 결과 vue object
    var search_result = new Vue({
        el: '#search-result',
        data: {
            search_result : {}
        },
        method: {
            wishButton: function (event) {
                console.log("add");
            }
        }
    });

    // 맛집 목록 vue object
    var wish_list = new Vue({
        el: '#wish-list',
        data: {
            wish_list : {}
        },
        methods: {
            addVisit: function (index) {
                $.ajax({
                    type: "POST" ,
                    async: true ,
                    url: `/api/restaurant/${index}`,
                    timeout: 3000
                });

                getWishList();
            },
            deleteWish: function (index) {
                $.ajax({
                    type: "DELETE" ,
                    async: true ,
                    url: `/api/restaurant/${index}`,
                    timeout: 3000
                });
                getWishList();
            }
        }
    });

    // search
    $("#searchButton").click(function () {
        const query = $("#searchBox").val();
        $.get(`/api/restaurant/search?query=${query}`, function (response) {
            search_result.search_result = response;
            $('#search-result').attr('style','visible');
        });
    });

    // Enter
    $("#searchBox").keydown(function(key) {
        if (key.keyCode === 13) {
            const query = $("#searchBox").val();
            $.get(`/api/restaurant/search?query=${query}`, function (response) {
                search_result.search_result = response;
                $('#search-result').attr('style','visible');
            });
        }
    });

    $("#wishButton").click(function () {
        $.ajax({
            type: "POST" ,
            async: true ,
            url: "/api/restaurant",
            timeout: 3000,
            data: JSON.stringify(search_result.search_result),
            contentType: "application/json",
            error: function (request, status, error) {

            },
            success: function (response, status, request) {
                getWishList();
            }
        });
    });

    function getWishList(){
        $.get(`/api/restaurant/all`, function (response) {
            wish_list.wish_list = response;
        });
    }

    $(document).ready(function () {
        console.log("init")
    });

})(jQuery);

main.html

<!DOCTYPE html>
<html lang="ko" xmlns:v-bind="http://www.w3.org/1999/xhtml" xmlns:v-on="http://www.w3.org/1999/xhtml">

<head>
  <meta charset="UTF-8">
  <title>맛집 WISH LIST</title>
</head>

<body>
<br/>

<div class="container">
  <!-- search -->
  <div class="row">
    <div class="col-sm-6 col-md-8">
      <input id="searchBox" style="height: 46px" class="form-control form-control-lg" type="text" placeholder="맛집을 검색해주세요 ex.(판교 갈비집)" value="갈비집">
    </div>
    <div class="col-sm-6 col-md-4">
      <button id="searchButton" type="button" class="btn btn-primary btn-lg" style="width: 100%">검색</button>
    </div>
  </div>

  <br/>
  <!-- search result -->
  <div class="row" id="search-result" style="visibility: hidden">
    <div class="col-sm-6 col-md-8">
      <img id="wish_image" v-bind:src="search_result.imageLink" alt="..." class="img-thumbnail" style="min-width: 100%; min-height: 100%;">
    </div>
    <div class="col-sm-6 col-md-4">
      <ul class="list-group list-group-flush">
        <li class="list-group-item" id="wish_title">{{search_result.title}}</li>
        <li class="list-group-item" id="wish_category">{{search_result.category}}</li>
        <li class="list-group-item" id="wish_address">{{search_result.address}}</li>
        <li class="list-group-item" id="wish_road_address">{{search_result.roadAddress}}</li>
        <li class="list-group-item"><a id="wish_link" target="_blank" v-bind:href="search_result.homePageLink">홈페이지</a> </li>
      </ul>
      <button id="wishButton" type="button" class="btn btn-primary btn-lg" style="width: 96%; position: absolute; bottom: 0">위시리스트 추가</button>
    </div>
  </div>

  <br/><br/><br/>

  <div class="row">
    <div class="alert alert-info col-sm-12 col-md-12" style="text-align: center">
      나의 맛집 리스트
    </div>
  </div>

  <br/>
  <div id="wish-list">
    <div v-for="wish in wish_list">
      <br/><hr/>
      <div class="row">
        <div class="col-sm-6 col-md-8">
          <img v-bind:src="wish.imageLink"
               alt="..."
               class="img-thumbnail"
               style="min-width: 100%;
                         min-height: 100%;"
          >
        </div>
        <div class="col-sm-6 col-md-4">
          <ul class="list-group list-group-flush">
            <li class="list-group-item">장소 : {{wish.title}}</li>
            <li class="list-group-item">Category : {{wish.category}}</li>
            <li class="list-group-item">주소 : {{wish.address}}</li>
            <li class="list-group-item">도로명 : {{wish.roadAddress}}</li>
            <li class="list-group-item">방명여부 : {{wish.visit}}</li>
            <li class="list-group-item">마지막 방문일자 : {{wish.lastVisitDate}}</li>
            <li class="list-group-item">방문횟수 : {{wish.visitCount}}</li>
            <li class="list-group-item">
              <a href="http://imf0010.cafe24.com/m/imf0020">홈페이지</a>
            </li>
            <li class="list-group-item">
              <button v-on:click="addVisit(wish.index)" type="button" class="btn btn-primary btn-lg" style="width: 100%;">방문 추가</button>
              <br/><br/>
              <button v-on:click="deleteWish(wish.index)" type="button" class="btn btn-primary btn-lg" style="width: 100%;">위시리스트 삭제</button>
            </li>
            <li class="list-group-item"></li>
          </ul>
        </div>
        <br/>
      </div>
      <hr>
    </div>
  </div>
</div>  <!-- container end -->



</body>


<!-- jQuery (부트스트랩의 자바스크립트 플러그인을 위해 필요합니다) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>

<!-- CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">

<!-- 합쳐지고 최소화된 최신 자바스크립트 -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>

<!-- 개발버전, 도움되는 콘솔 경고를 포함. -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<script src="/js/main.js"></script>
</html>

pageController

@Controller
@RequestMapping("/pages")
public class PageController {
    
    @GetMapping("/main")
    public ModelAndView main(){
        return new ModelAndView("main");
    }
}
profile
Journey for Backend Developer

0개의 댓글