Skip to content

Commit bb14bff

Browse files
committed
feat: 좋아요 API 구현
- 좋아요 등록/취소 (멱등 동작)
1 parent 1f48c85 commit bb14bff

9 files changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.loopers.application.like;
2+
3+
import com.loopers.domain.like.Like;
4+
import com.loopers.domain.like.LikeRepository;
5+
import com.loopers.domain.product.ProductRepository;
6+
import com.loopers.domain.user.UserRepository;
7+
import com.loopers.interfaces.api.like.LikeV1Dto;
8+
import com.loopers.support.error.CoreException;
9+
import com.loopers.support.error.ErrorType;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.stereotype.Component;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
@Component
15+
@RequiredArgsConstructor
16+
public class LikeFacade {
17+
private final LikeRepository likeRepository;
18+
private final UserRepository userRepository;
19+
private final ProductRepository productRepository;
20+
21+
@Transactional
22+
public LikeInfo doLike(LikeV1Dto.LikeRequest request) {
23+
/*
24+
- [ ] 사용자 검증
25+
- [ ] 상품 검증
26+
- [ ] 좋아요 등록 (멱등)
27+
*/
28+
Long userId = request.userId();
29+
Long productId = request.productId();
30+
31+
userRepository.findById(userId).orElseThrow(
32+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 유저입니다.")
33+
);
34+
35+
productRepository.findById(productId).orElseThrow(
36+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 상품입니다.")
37+
);
38+
39+
return likeRepository.findByUserIdAndProductId(userId, productId)
40+
.map(LikeInfo::from)
41+
.orElseGet(() -> {
42+
Like newLike = request.toEntity();
43+
likeRepository.save(newLike);
44+
45+
return LikeInfo.from(newLike);
46+
});
47+
}
48+
49+
@Transactional
50+
public void doUnlike(Long userId, Long productId) {
51+
/*
52+
- [ ] 사용자 검증
53+
- [ ] 상품 검증
54+
- [ ] 좋아요 취소 (멱등)
55+
*/
56+
57+
userRepository.findById(userId).orElseThrow(
58+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 유저입니다.")
59+
);
60+
61+
productRepository.findById(productId).orElseThrow(
62+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 상품입니다.")
63+
);
64+
65+
likeRepository.findByUserIdAndProductId(userId, productId)
66+
.ifPresent(likeRepository::delete);
67+
68+
}
69+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.loopers.application.like;
2+
3+
import com.loopers.domain.like.Like;
4+
5+
public record LikeInfo(Long id, Long userId, Long productId) {
6+
public static LikeInfo from(Like like) {
7+
return new LikeInfo(
8+
like.getId(),
9+
like.getUserId(),
10+
like.getProductId()
11+
);
12+
}
13+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.loopers.domain.like;
2+
3+
import com.loopers.domain.BaseEntity;
4+
import jakarta.persistence.Column;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.Table;
7+
import lombok.AccessLevel;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
11+
@Getter
12+
@Entity
13+
@Table(name = "product_like")
14+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
15+
public class Like extends BaseEntity {
16+
@Column(name = "ref_user_id", nullable = false)
17+
private Long userId;
18+
19+
@Column(name = "ref_product_id", nullable = false)
20+
private Long productId;
21+
22+
public Like(Long userId, Long productId) {
23+
this.userId = userId;
24+
this.productId = productId;
25+
}
26+
27+
public boolean isAlreadyLike(Like like) {
28+
if (like != null) {
29+
return true;
30+
}
31+
return false;
32+
}
33+
34+
35+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.loopers.domain.like;
2+
3+
import java.util.Optional;
4+
5+
public interface LikeRepository {
6+
Optional<Like> findByUserIdAndProductId(Long userId, Long productId);
7+
8+
Like save(Like like);
9+
10+
void delete(Like like);
11+
12+
int countByProductId(Long productId);
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
public interface LikeJpaRepository extends JpaRepository<Like, Long> {
9+
Optional<Like> findByUserIdAndProductId(Long userId, Long productId);
10+
11+
int countByProductId(Long productId);
12+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
@Component
11+
@RequiredArgsConstructor
12+
public class LikeRepositoryImpl implements LikeRepository {
13+
private final LikeJpaRepository likeJpaRepository;
14+
15+
@Override
16+
public Optional<Like> findByUserIdAndProductId(Long userId, Long productId) {
17+
return likeJpaRepository.findByUserIdAndProductId(userId, productId);
18+
}
19+
20+
@Override
21+
public Like save(Like like) {
22+
return likeJpaRepository.save(like);
23+
}
24+
25+
@Override
26+
public void delete(Like like) {
27+
likeJpaRepository.delete(like);
28+
}
29+
30+
@Override
31+
public int countByProductId(Long productId) {
32+
return likeJpaRepository.countByProductId(productId);
33+
}
34+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.loopers.interfaces.api.like;
2+
3+
4+
import com.loopers.interfaces.api.ApiResponse;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.tags.Tag;
7+
8+
9+
@Tag(name = "Like V1 API", description = "Like API 입니다.")
10+
public interface LikeV1ApiSpec {
11+
@Operation(summary = "좋아요 등록")
12+
ApiResponse<LikeV1Dto.LikeResponse> doLike (LikeV1Dto.LikeRequest request);
13+
14+
@Operation(summary = "좋아요 취소")
15+
void doUnLike (Long userId, Long productId);
16+
17+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.loopers.interfaces.api.like;
2+
3+
import com.loopers.application.like.LikeFacade;
4+
import com.loopers.application.like.LikeInfo;
5+
import com.loopers.interfaces.api.ApiResponse;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.web.bind.annotation.DeleteMapping;
8+
import org.springframework.web.bind.annotation.PostMapping;
9+
import org.springframework.web.bind.annotation.RequestBody;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.RequestParam;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@RestController
15+
@RequestMapping("/api/v1/likes")
16+
@RequiredArgsConstructor
17+
public class LikeV1Controller implements LikeV1ApiSpec {
18+
private final LikeFacade likeFacade;
19+
20+
@PostMapping
21+
@Override
22+
public ApiResponse<LikeV1Dto.LikeResponse> doLike(@RequestBody LikeV1Dto.LikeRequest request) {
23+
LikeInfo info = likeFacade.doLike(request);
24+
LikeV1Dto.LikeResponse response = LikeV1Dto.LikeResponse.from(info);
25+
26+
return ApiResponse.success(response);
27+
}
28+
29+
@DeleteMapping
30+
@Override
31+
public void doUnLike(@RequestParam Long userId, @RequestParam Long productId) {
32+
likeFacade.doUnlike(userId, productId);
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.loopers.interfaces.api.like;
2+
3+
import com.loopers.application.like.LikeInfo;
4+
import com.loopers.domain.like.Like;
5+
6+
public class LikeV1Dto {
7+
public record LikeResponse(Long id, Long userId, Long productId) {
8+
public static LikeResponse from(LikeInfo info) {
9+
return new LikeResponse(
10+
info.id(),
11+
info.userId(),
12+
info.productId()
13+
);
14+
}
15+
}
16+
17+
public record LikeRequest(Long userId, Long productId) {
18+
public Like toEntity() {
19+
return new Like(
20+
userId,
21+
productId
22+
);
23+
}
24+
}
25+
26+
public record UnLikeRequest(Long userId, Long productId) {
27+
public Like toEntity() {
28+
return new Like(
29+
userId,
30+
productId
31+
);
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)