본문 바로가기
프로젝트/카카오 쇼핑하기 web

카카오 쇼핑하기 클론프로젝트 #4

by 성장하고픈개발자 2023. 8. 22.
728x90
728x90

이번엔 좋아요 기능을 추가해봤다!!

예를들면 이런 하트 기능...

기존 er diagram에서 테이블을 하나 추가해줬다.

사용자가 , 어떤 상품마다 좋아요를 눌렀는지에 대한 데이터를 저장해줘야한다고 생각했기 때문에 Like 라는 테이블을 따로 만들어주었다.

package com.example.kakao.like;

//import 생략..

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name="like_tb", indexes = {
        @Index(name = "like_user_id_idx", columnList = "user_id"),
        @Index(name = "like_product_id_idx", columnList = "product_id")
})
public class Like {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;

    @OneToOne
    private Product product;

    @Builder
    public Like(int id, User user, Product product){
        this.id = id;
        this.user = user;
        this.product = product;
    }
}

사용자는 여러 상품에 좋아요를 누를 수 있으니 ManyToOne으로 매핑해줬고 상품 하나에 좋아요를 두번 누를 순 없기때문에 (...좋아요 한번 누르면 취소하고싶어도 못함ㅋ ㅋ) OneToOne으로 매핑해줬다. 또 불필요한 쿼리가 생성되지 않게 LazyLoading 으로 설정했다.

 

내가 설계한 REST API

  •  좋아요를 추가하는 기능에는 딱히 응답body가 필요없을 거라고 생각해서 정상적으로 추가됐으면 null을 출력하는 형식으로 작성했다.
  • 좋아요 목록을 조회하는 기능에는 userId로 좋아요 목록을 조회할 수 있게 했으며 본인의 좋아요 목록만 볼 수 있게 시큐리티설정을 해뒀다.

요청, 응답 DTO

public class LikeRequest {

    @Getter
    @Setter
    public static class LikeAddDTO{
        @NotNull
        private int userId;
        @NotNull
        private int productId;
    }
}

public class LikeResponse {

    @Getter
    @Setter
    public static class FindAllDTO{
        private int id;
        private String productName;
        private String description;
        private String image;
        private int price;

        public FindAllDTO(Like like) {
            this.id = like.getProduct().getId();
            this.productName = like.getProduct().getProductName();
            this.description = like.getProduct().getDescription();
            this.image = like.getProduct().getImage();
            this.price = like.getProduct().getPrice();
        }

    }


}

컨트롤러

@RestController
@RequiredArgsConstructor
public class LikeRestController {

    private final LikeService likeService;

    @PostMapping("/like/add")
    public ResponseEntity<?> likeAdd(@RequestBody @Valid LikeRequest.LikeAddDTO requestDTO, Errors errors, @AuthenticationPrincipal CustomUserDetails userDetails){
	likeService.likeAdd(requestDTO, userDetails.getUser());
        ApiUtils.ApiResult<?> apiResult = ApiUtils.success(null);
        return ResponseEntity.ok(apiResult);
    }

    @GetMapping("/like/{id}")
    public ResponseEntity<?> findAllLikes(@AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable int id){
        List<LikeResponse.FindAllDTO> responseDTO = likeService.findAllLikes(userDetails.getUser(),id);
        ApiUtils.ApiResult<?> apiResult = ApiUtils.success(responseDTO);
        return ResponseEntity.ok(apiResult);
    }
}

서비스

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class LikeService {

    private final LikeJPARepository likeJPARepository;
    private final UserJPARepository userJPARepository;
    private final ProductJPARepository productJPARepository;

    @Transactional
    public void likeAdd(LikeRequest.LikeAddDTO requestDTO, User sessionUser){
        int userId = requestDTO.getUserId();
        int productId = requestDTO.getProductId();

        User userPS = userJPARepository.findById(userId).orElseThrow(
                ()-> new Exception400("존재하지 않는 사용자입니다.")
        );

        if(sessionUser.getId() != requestDTO.getUserId()){
            throw new Exception401("인증되지 않은 사용자입니다.");
        }

        //이미 좋아요 누른 상품인데 또 눌렀을 경우
        Optional<Like> likeOP  = likeJPARepository.findByProductId(productId, userId);
        if(likeOP.isPresent()){
            throw new Exception400("이미 좋아요를 누른 상품입니다.");
        }

        //존재하지 않는 상품 id로 좋아요를 시도했을 때
        Product productPS = productJPARepository.findById(productId).orElseThrow(
                ()-> new Exception404("존재하지 않는 상품입니다")
        );

        Like like = Like.builder()
                .user(userPS)
                .product(productPS)
                .build();

        likeJPARepository.save(like);
    }

    @Transactional
    public List<LikeResponse.FindAllDTO> findAllLikes(User sessionUser, int id){
        if(sessionUser.getId()!= id){
            throw new Exception401("권한이 없습니다.");
        }

        //userId 가 2인 user의 likeList를 가져온다.
        List<Like> likeList = likeJPARepository.findProductsByUserId(id);

        List<LikeResponse.FindAllDTO> responseDTOs = likeList.stream()
                .map(like -> new LikeResponse.FindAllDTO(like))
                .collect(Collectors.toList());

        return responseDTOs;

    }
}

발생할 수 있는 모든 예외들을 처리해줬다.

 

좋아요 추가 기능

  • userId와 productId를 가져온 후 DB에서 userId가 있는지 확인하고 없으면 예외처리
  • 요청DTO에서 보낸 userId 데이터와 본인 userId 데이터가 다르면 예외처리
  • 이미 좋아요를 누른 상품일 경우 예외처리
  • 존재하지 않는 상품 id로 좋아요를 누르려는 접근을 시도했을 때 예외처리

 

 

좋아요 목록 조회 기능

  • 본인의 userId가 아닌 경우 예외처리
  • 좋아요 목록을 가져오고 stream으로 응답DTO 데이터 가공해서 리턴

postman으로 응답 확인

userId가 1인 사용자가 2, 4, 10번 상품들을 좋아요 추가 후 테이블 상태

like/1 GET 요청 후 응답

다른 예외들 눈으로 확인했고 일일히 첨부하기 힘들기 때문에 생략 ,,


실행된 쿼리 확인

장바구니 추가

//유저 조회

Hibernate: 
    select
        user0_.id as id1_7_0_,
        user0_.email as email2_7_0_,
        user0_.password as password3_7_0_,
        user0_.roles as roles4_7_0_,
        user0_.username as username5_7_0_ 
    from
        user_tb user0_ 
    where
        user0_.id=?

//유저가 동일 상품에 좋아요 누르는걸 예외처리하기 위해 한번 조회

Hibernate: 
    select
        like0_.id as id1_3_,
        like0_.product_id as product_2_3_,
        like0_.user_id as user_id3_3_ 
    from
        like_tb like0_ 
    where
        like0_.product_id=? 
        and like0_.user_id=?
        
 //상품 정보를 불러오기 위해 조회 (가격이나 상품이름...등)
        

Hibernate: 
    select
        product0_.id as id1_6_0_,
        product0_.description as descript2_6_0_,
        product0_.image as image3_6_0_,
        product0_.price as price4_6_0_,
        product0_.product_name as product_5_6_0_ 
    from
        product_tb product0_ 
    where
        product0_.id=?

//like 테이블에 삽입

Hibernate: 
    insert 
    into
        like_tb
        (id, product_id, user_id) 
    values
        (default, ?, ?)

 

내가 만든 최선의 쿼리다...

나중에 코드를 다 뜯어고쳐서라도 개선할 방법이 있는지 생각해봐야겠다.

 

장바구니 목록 조회

Hibernate: 
    select
        like0_.id as id1_3_0_,
        product1_.id as id1_6_1_,
        like0_.product_id as product_2_3_0_,
        like0_.user_id as user_id3_3_0_,
        product1_.description as descript2_6_1_,
        product1_.image as image3_6_1_,
        product1_.price as price4_6_1_,
        product1_.product_name as product_5_6_1_ 
    from
        like_tb like0_ 
    inner join
        product_tb product1_ 
            on like0_.product_id=product1_.id 
    where
        like0_.user_id=?

product테이블을 join fetch 하여 n+1문제를 해결했다.

 

 

후기

여러번의 익셉션 처리로 인해 로직 작성이 약간 헷갈렸지만 그래도 잘 해결한 것 같아서 뿌듯하다 ^~^ 

728x90
728x90