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);
}
}
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);
}
}
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");
}
}