Skip to content

Commit 7e0ac82

Browse files
committed
feat(like): 좋아요 도메인 구현 및 멱등성 처리 추가
- Like 엔티티 구현 - 좋아요 등록/취소 로직 및 중복 방지 (멱등성) 구현 - Product.likeCount 증가/감소 연동 - LikeRepository 인터페이스 정의 - Like 단위 /통 합 테스트 작성
1 parent 04ff345 commit 7e0ac82

8 files changed

Lines changed: 486 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.loopers.application.like;
2+
3+
import com.loopers.domain.like.LikeService;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.transaction.annotation.Transactional;
7+
8+
/**
9+
* packageName : com.loopers.application.like
10+
* fileName : LikeFacade
11+
* author : byeonsungmun
12+
* date : 2025. 11. 14.
13+
* description :
14+
* ===========================================
15+
* DATE AUTHOR NOTE
16+
* -------------------------------------------
17+
* 2025. 11. 14. byeonsungmun 최초 생성
18+
*/
19+
@Component
20+
@RequiredArgsConstructor
21+
@Transactional
22+
public class LikeFacade {
23+
24+
private final LikeService likeService;
25+
26+
public void createLike(String userId, Long productId) {
27+
likeService.like(userId, productId);
28+
}
29+
30+
public void deleteLike(String userId, Long productId) {
31+
likeService.unlike(userId, productId);
32+
}
33+
}
34+
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.loopers.domain.like;
2+
3+
import com.loopers.support.error.CoreException;
4+
import com.loopers.support.error.ErrorType;
5+
import jakarta.persistence.*;
6+
import lombok.Getter;
7+
8+
import java.time.LocalDateTime;
9+
10+
/**
11+
* packageName : com.loopers.domain.like
12+
* fileName : Like
13+
* author : byeonsungmun
14+
* date : 2025. 11. 11.
15+
* description :
16+
* ===========================================
17+
* DATE AUTHOR NOTE
18+
* -------------------------------------------
19+
* 2025. 11. 11. byeonsungmun 최초 생성
20+
*/
21+
@Entity
22+
@Table(name = "product_like")
23+
@Getter
24+
public class Like {
25+
@Id
26+
@GeneratedValue(strategy = GenerationType.IDENTITY)
27+
private Long id;
28+
29+
@Column(name = "ref_user_id", nullable = false)
30+
private String userId;
31+
32+
@Column(name = "ref_product_id", nullable = false)
33+
private Long productId;
34+
35+
@Column(nullable = false)
36+
private LocalDateTime createdAt;
37+
38+
protected Like() {}
39+
40+
private Like(String userId, Long productId) {
41+
this.userId = requireValidUserId(userId);
42+
this.productId = requireValidProductId(productId);
43+
this.createdAt = LocalDateTime.now();
44+
}
45+
46+
public static Like create(String userId, Long productId) {
47+
return new Like(userId, productId);
48+
}
49+
50+
private String requireValidUserId(String userId) {
51+
if (userId == null || userId.isEmpty()) {
52+
throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID는 필수입니다.");
53+
}
54+
return userId;
55+
}
56+
57+
private Long requireValidProductId(Long productId) {
58+
if (productId == null || productId <= 0) {
59+
throw new CoreException(ErrorType.BAD_REQUEST, "상품 ID는 필수입니다.");
60+
}
61+
return productId;
62+
}
63+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.loopers.domain.like;
2+
3+
import java.util.Optional;
4+
5+
/**
6+
* packageName : com.loopers.domain.like
7+
* fileName : LikeRepository
8+
* author : byeonsungmun
9+
* date : 2025. 11. 12.
10+
* description :
11+
* ===========================================
12+
* DATE AUTHOR NOTE
13+
* -------------------------------------------
14+
* 2025. 11. 12. byeonsungmun 최초 생성
15+
*/
16+
public interface LikeRepository {
17+
18+
Optional<Like> findByUserIdAndProductId(String userId, Long productId);
19+
20+
void save(Like like);
21+
22+
void delete(Like like);
23+
24+
long countByProductId(Long productId);
25+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.loopers.domain.like;
2+
3+
import com.loopers.domain.product.ProductRepository;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.transaction.annotation.Transactional;
7+
8+
/**
9+
* packageName : com.loopers.application.like
10+
* fileName : LikeService
11+
* author : byeonsungmun
12+
* date : 2025. 11. 12.
13+
* description :
14+
* ===========================================
15+
* DATE AUTHOR NOTE
16+
* -------------------------------------------
17+
* 2025. 11. 12. byeonsungmun 최초 생성
18+
*/
19+
@Component
20+
@RequiredArgsConstructor
21+
public class LikeService {
22+
23+
private final LikeRepository likeRepository;
24+
private final ProductRepository productRepository;
25+
26+
@Transactional
27+
public void like(String userId, Long productId) {
28+
if (likeRepository.findByUserIdAndProductId(userId, productId).isPresent()) return;
29+
30+
Like like = Like.create(userId, productId);
31+
likeRepository.save(like);
32+
productRepository.incrementLikeCount(productId);
33+
}
34+
35+
@Transactional
36+
public void unlike(String userId, Long productId) {
37+
likeRepository.findByUserIdAndProductId(userId, productId)
38+
.ifPresent(like -> {
39+
likeRepository.delete(like);
40+
productRepository.decrementLikeCount(productId);
41+
});
42+
}
43+
44+
@Transactional(readOnly = true)
45+
public long countByProductId(Long productId) {
46+
return likeRepository.countByProductId(productId);
47+
}
48+
49+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.loopers.infrastructure.like;
2+
3+
import com.loopers.domain.like.Like;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
import java.util.Optional;
7+
8+
/**
9+
* packageName : com.loopers.infrastructure.like
10+
* fileName : LikeJpaRepository
11+
* author : byeonsungmun
12+
* date : 2025. 11. 12.
13+
* description :
14+
* ===========================================
15+
* DATE AUTHOR NOTE
16+
* -------------------------------------------
17+
* 2025. 11. 12. byeonsungmun 최초 생성
18+
*/
19+
public interface LikeJpaRepository extends JpaRepository<Like, Long> {
20+
Optional<Like> findByUserIdAndProductId(String userId, Long productId);
21+
22+
long countByProductId(Long productId);
23+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.loopers.infrastructure.like;
2+
3+
import com.loopers.domain.like.Like;
4+
import com.loopers.domain.like.LikeRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Component;
7+
8+
import java.util.Optional;
9+
10+
/**
11+
* packageName : com.loopers.infrastructure.like
12+
* fileName : LikeRepositoryImpl
13+
* author : byeonsungmun
14+
* date : 2025. 11. 12.
15+
* description :
16+
* ===========================================
17+
* DATE AUTHOR NOTE
18+
* -------------------------------------------
19+
* 2025. 11. 12. byeonsungmun 최초 생성
20+
*/
21+
@Component
22+
@RequiredArgsConstructor
23+
public class LikeRepositoryImpl implements LikeRepository {
24+
25+
private final LikeJpaRepository likeJpaRepository;
26+
27+
@Override
28+
public Optional<Like> findByUserIdAndProductId(String userId, Long productId) {
29+
return likeJpaRepository.findByUserIdAndProductId(userId, productId);
30+
}
31+
32+
@Override
33+
public void save(Like like) {
34+
likeJpaRepository.save(like);
35+
}
36+
37+
@Override
38+
public void delete(Like like) {
39+
likeJpaRepository.delete(like);
40+
}
41+
42+
@Override
43+
public long countByProductId(Long productId) {
44+
return likeJpaRepository.countByProductId(productId);
45+
}
46+
}

0 commit comments

Comments
 (0)