있어야 하는 기능
- 찜하기
- 찜한 목록 보기
- 취소하기
찜은 상품 detail 화면에서 동작하고, 취소하는 방법은 상품 detail 화면과 찜한 목록 화면에서 동작해야한다.
또한 상품을 찜하기 위해서는 로그인이 된 상태여야 한다.
성능 최적화는 아직 고려하지 않았다.
도메인 설계
member 객체가 like item의 list를 갖고, like item이 item과 member의 다대다 관계를 풀어준다.
UI 구현
상품 화면에서 찜 했으면 버튼 색을 칠하고, 찜하지 않았거나 로그인 상태가 아니라면 칠하지 않도록 보이게 했다.
목록도 단순하게 나열만 하고, 취소를 통해 바로 취소할 수 있도록 했다.
상품 상세 페이지 UI
ItemController.java
@GetMapping(value = "/detail")
public String itemDetailsWithMember(@AuthenticationPrincipal LoginUserDetails member, @RequestParam("itemId") Long itemId, Model model) {
Item findItem = itemService.findOne(itemId);
ItemDetailDto item = new ItemDetailDto();
item.setId(findItem.getId());
item.setName(findItem.getName());
item.setPrice(findItem.getPrice());
item.setStockQuantity(findItem.getStockQuantity());
item.setImageUrl("/images/"+findItem.getId()+".png");
model.addAttribute("item", item);
if(member!=null) {
Member findMember = memberRepository.findByLoginId(member.getUsername()).get();
List<LikeItem> likes = findMember.getLikes();
for(LikeItem likeItem : likes) {
if(likeItem.getItem().getId()==itemId) {
model.addAttribute("isLikeItem", "btn-primary");
return "item/detail";
}
}
}
model.addAttribute("isLikeItem", "btn-outline-primary");
return "item/detail";
}
상품 상세 페이지에서 상품과 관련된 view를 설정하고
@AuthenticationPrincipal로 UserDetails를 구현한 객체를 받아 로그인 한 상태인지 확인한다.
만약 로그인하지 않았거나, 찜 상품에 해당 상품이 없다면 outline만 있는 button style을 return하고,
찜 상품에 해당 상품이 있다면 primary style을 return한다.
isLikeItem attribute에 class 값을 넣어놓고
...
<button type="submit" class="btn" th:classappend="${isLikeItem}" th:formaction="@{/like}">찜</button>
...
th:classappend를 통해 동적으로 class를 적용하도록 했다.
찜한 목록 UI
LikeItemController.java
@GetMapping(value = "/likes")
public String likes(@AuthenticationPrincipal LoginUserDetails member, Model model) {
Member findMember = memberService.findUser(member.getUsername());
List<LikeItem> likeItems = findMember.getLikes();
List<Item> items = likeItems.stream().map(likeItem -> likeItem.getItem()).collect(Collectors.toList());
model.addAttribute("items", items);
return "/user/likeList";
}
로그인된 유저 정보를 갖고 찜한 아이템 목록을 불러온다.
LikeItem으로 다대다 관계를 풀어줬기 때문에 LikeItem에서 Item 정보만 가져온다.
비즈니스 로직 구현
먼저 Member Entity에 LikeItem을 추가하고 삭제할 수 있는 비즈니스 로직을 추가했다.
Member.class
public void addLikeItem(LikeItem likeItem) {
for(LikeItem like : likes) {
if(like.getItem()== likeItem.getItem()) return;
}
likes.add(likeItem);
likeItem.setMember(this);
}
public void removeLikeItem(LikeItem likeItem) {
likes.remove(likeItem);
likeItem.setMember(null);
}
사실 removeLikeItem에서 likeItem.setMember(null)은 필요가 없다.
Like Item을 지울 때 다른 부분에서 db에서도 없애도록 설계했기 때문이다.
하지만 나중의 혹시모를 리팩토링을 위해 남겨놓았다.
찜을 취소하면 db에서도 삭제하기 위해 remove 로직을 정의했다.
LikeItem을 비식별관계를 통해 정의했기 때문에 memberId와 itemId로 likeItem을 찾을 수 있는 로직도 repository에 필요하다.
LikeItemRepository.java
...
public Optional<LikeItem> findByIds(Long memberId, Long itemId) {
return em.createQuery("select li from LikeItem li" +
" where li.member.id = :memberId" +
" and li.item.id = :itemId", LikeItem.class)
.setParameter("memberId", memberId)
.setParameter("itemId", itemId)
.getResultList().stream().findAny();
}
public void remove(Long likeItemId) {
em.remove(em.find(LikeItem.class, likeItemId));
}
...
그리고 add, remove request를 처리하는 Controller이다.
LikeItemController.java
@PostMapping(value = "/like") // 상품 상세 페이지에서 요청하는 url
public String addLike(@AuthenticationPrincipal LoginUserDetails member,
@RequestParam("itemId") Long itemId){
Member findMember = memberService.findUser(member.getUsername());
Item findItem = itemService.findOne(itemId);
LikeItem findLikeItem = likeItemService.findOne(findMember.getId(), itemId);
if(findLikeItem==null) { // 찜하지 않았다면 새로 찜
LikeItem likeItem = new LikeItem();
likeItem.setItem(findItem);
findMember.addLikeItem(likeItem);
likeItemService.save(likeItem);
}
else { // 찜했다면 삭제
findMember.removeLikeItem(findLikeItem);
likeItemService.remove(findLikeItem.getId());
}
return "redirect:/items/detail?itemId="+itemId;
}
@PostMapping(value = "/like/{itemId}/cancel") // 내가 찜한 상품 목록 화면에서 취소 요청을 보내는
public String cancelLike(@AuthenticationPrincipal LoginUserDetails member,
@PathVariable("itemId") Long itemId) {
Member findMember = memberService.findUser(member.getUsername());
LikeItem likeItem = likeItemService.findOne(findMember.getId(), itemId);
findMember.removeLikeItem(likeItem);
likeItemService.remove(likeItem.getId());
return "redirect:/likes";
}
cancel은 찜한 목록 화면에서 취소 요청이 들어온 item에 대해서 취소 하도록 했고,
상품 상세 페이지에서 찜 버튼을 눌렀을 때, 이미 찜한 상품이라면 취소하도록 하고 찜하지 않았다면 새로 찜 목록에 추가하도록 했다.
사실 LikeItem에 추가할 필드가 없어서 Member와 Item을 JoinTable로 묶으려고 했지만
createQuery로 어떻게 조회해야할 지 몰라서 그냥 정석대로 LikeItem Entity를 만들어서 다대다 관계를 풀어줬다.
관계형 데이터 베이스를 제대로 공부한 적이 이번이 처음이라 애먹었다.
새로 알게된 건 UserDetails를 구현한 객체를 @AuthenticationPrincipal로 불러왔을 때 반드시 로그인한 상태가 아니라도 불러올 수 있고, null 체크를 통해 로그인 여부를 확인할 수 있다는 것이다.
'springboot' 카테고리의 다른 글
[springboot - thymeleaf] Form Dto : NullPointerException (0) | 2022.05.15 |
---|---|
[springboot - thymeleaf] 쇼핑몰 장바구니 페이지 구현 (0) | 2022.04.25 |
[thymeleaf] form 태그 아래 submit이 여러 개일 때 (0) | 2022.04.19 |
[스프링 부트 JPA 활용] 도메인 분석 설계 (0) | 2022.03.11 |
[스프링 핵심 원리] @Autowired @Qualifier @Primary (0) | 2022.03.09 |