Skip to content

Commit f5cdfa5

Browse files
authored
[feat] 상품 찜 기능 구현 (#353)
* Work * work * work * work * Work
1 parent 15faf57 commit f5cdfa5

File tree

7 files changed

+512
-2
lines changed

7 files changed

+512
-2
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.back.domain.wishlist.controller;
2+
3+
import com.back.domain.wishlist.service.WishlistService;
4+
import com.back.global.rsData.RsData;
5+
import com.back.global.security.auth.CustomUserDetails;
6+
import io.swagger.v3.oas.annotations.tags.Tag;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
10+
import org.springframework.web.bind.annotation.*;
11+
12+
import java.util.UUID;
13+
14+
@RestController
15+
@RequiredArgsConstructor
16+
@RequestMapping("/api/wishlist/{productUuid}")
17+
@Tag(name = "상품 찜", description = "상품 찜 관련 API")
18+
public class WishlistController {
19+
20+
private final WishlistService wishlistService;
21+
22+
/** 찜 등록 */
23+
@PostMapping
24+
public ResponseEntity<RsData<UUID>> addWishlist(
25+
@PathVariable UUID productUuid,
26+
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
27+
UUID addWishlistProductUuid = wishlistService.addWishlist(productUuid, customUserDetails);
28+
return ResponseEntity.ok(RsData.of("200", "상품이 위시리스트에 추가되었습니다.", addWishlistProductUuid));
29+
}
30+
31+
/** 찜 삭제 */
32+
@DeleteMapping
33+
public ResponseEntity<RsData<UUID>> removeWishlist(
34+
@PathVariable UUID productUuid,
35+
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
36+
UUID removeWishlistProductUuid = wishlistService.removeWishlist(productUuid, customUserDetails);
37+
return ResponseEntity.ok(RsData.of("200", "상품이 위시리스트에서 제거되었습니다.", removeWishlistProductUuid));
38+
}
39+
40+
/** 상품별 찜 개수 조회 */
41+
@GetMapping("/count")
42+
public ResponseEntity<RsData<Long>> getWishlistCount(
43+
@PathVariable UUID productUuid) {
44+
Long count = wishlistService.getWishlistCount(productUuid);
45+
return ResponseEntity.ok(RsData.of("200", "상품 찜 개수 조회 성공", count));
46+
}
47+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.back.domain.wishlist.entity;
2+
3+
import com.back.domain.product.product.entity.Product;
4+
import com.back.domain.user.entity.User;
5+
import com.back.global.jpa.entity.BaseEntity;
6+
import jakarta.persistence.*;
7+
import lombok.*;
8+
import org.hibernate.annotations.OnDelete;
9+
import org.hibernate.annotations.OnDeleteAction;
10+
11+
@Entity
12+
@Getter
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@Builder
16+
@Table(name = "wishlists",
17+
uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "product_id"})}) // 중복 찜 방지
18+
public class Wishlist extends BaseEntity {
19+
20+
@ManyToOne(fetch = FetchType.LAZY)
21+
@JoinColumn(name = "user_id", nullable = false)
22+
@OnDelete(action = OnDeleteAction.CASCADE)
23+
private User user; // 유저 FK
24+
25+
@ManyToOne(fetch = FetchType.LAZY)
26+
@JoinColumn(name = "product_id", nullable = false)
27+
@OnDelete(action = OnDeleteAction.CASCADE)
28+
private Product product; // 상품 FK
29+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.back.domain.wishlist.repository;
2+
3+
import com.back.domain.wishlist.entity.Wishlist;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface WishlistRepository extends JpaRepository<Wishlist, Long> {
7+
// 찜 등록 여부 조회
8+
boolean existsByUserIdAndProductId(Long userId, Long productId);
9+
// 찜 삭제
10+
void deleteByUserIdAndProductId(Long userId, Long productId);
11+
// 상품별 찜 개수 조회
12+
Long countByProductId(Long productId);
13+
14+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.back.domain.wishlist.service;
2+
3+
import com.back.domain.product.product.entity.Product;
4+
import com.back.domain.product.product.repository.ProductRepository;
5+
import com.back.domain.user.entity.User;
6+
import com.back.domain.user.repository.UserRepository;
7+
import com.back.domain.wishlist.entity.Wishlist;
8+
import com.back.domain.wishlist.repository.WishlistRepository;
9+
import com.back.global.exception.ServiceException;
10+
import com.back.global.security.auth.CustomUserDetails;
11+
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
15+
16+
import java.util.UUID;
17+
18+
@Service
19+
@RequiredArgsConstructor
20+
@Slf4j
21+
public class WishlistService {
22+
23+
private final WishlistRepository wishlistRepository;
24+
private final UserRepository userRepository;
25+
private final ProductRepository productRepository;
26+
27+
/** 찜 등록 */
28+
@Transactional
29+
public UUID addWishlist(UUID productUuid, CustomUserDetails customUserDetails) {
30+
User user = validateAndGetUser(customUserDetails.getUser());
31+
Product product = validateAndGetProduct(productUuid);
32+
validateDuplicateWishlist(user.getId(), product.getId());
33+
34+
Wishlist wishlist = Wishlist.builder()
35+
.user(user)
36+
.product(product)
37+
.build();
38+
39+
return wishlistRepository.save(wishlist).getProduct().getProductUuid();
40+
}
41+
42+
/** 찜 삭제 */
43+
@Transactional
44+
public UUID removeWishlist(UUID productUuid, CustomUserDetails customUserDetails) {
45+
User user = validateAndGetUser(customUserDetails.getUser());
46+
Product product = validateAndGetProduct(productUuid);
47+
if (!wishlistRepository.existsByUserIdAndProductId(user.getId(), product.getId())) {
48+
throw new ServiceException("404", "위시리스트 항목을 찾을 수 없습니다.");
49+
}
50+
wishlistRepository.deleteByUserIdAndProductId(user.getId(), product.getId());
51+
return product.getProductUuid();
52+
}
53+
54+
/** 상품별 찜 개수 조회 */
55+
public Long getWishlistCount(UUID productUuid) {
56+
Product product = validateAndGetProduct(productUuid);
57+
return wishlistRepository.countByProductId(product.getId());
58+
}
59+
60+
/** Validation 메서드 */
61+
// 사용자 검증
62+
private User validateAndGetUser(User user){
63+
return userRepository.findById(user.getId())
64+
.orElseThrow(() -> new ServiceException("404", "사용자를 찾을 수 없습니다."));
65+
}
66+
// 상품 검증
67+
private Product validateAndGetProduct(UUID productUuid) {
68+
return productRepository.findByProductUuid(productUuid)
69+
.orElseThrow(() -> new ServiceException("404", "존재하지 않는 상품입니다. UUID: " + productUuid));
70+
}
71+
// 중복 찜 검증
72+
private void validateDuplicateWishlist(Long userId, Long productId) {
73+
if (wishlistRepository.existsByUserIdAndProductId(userId, productId)) {
74+
throw new ServiceException("409", "이미 위시리스트에 추가된 상품입니다.");
75+
}
76+
}
77+
}

src/main/java/com/back/global/security/config/SecurityConfig.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
9090
// 공개 API
9191
.requestMatchers("/public/**").permitAll()
9292

93-
// 상품,카테고리,태그 조회 / 상품 파일 다운로드(테스트용) / 상품 상세 조회 / 상품 상세-작가 정보 조회 / 메인페이지에서 주제별 상품 조회 / 검색 - 로그인 없이 접근 허용
94-
.requestMatchers(HttpMethod.GET, "/api/products","/api/products/*", "/api/categories","/api/tags", "/api/products/images/download/{productUuid}","/api/products/{productUuid}/*", "/api/search").permitAll()
93+
// 상품,카테고리,태그 조회 / 상품 파일 다운로드(테스트용) / 상품 상세 조회 / 상품 상세-작가 정보 조회 / 메인페이지에서 주제별 상품 조회 / 검색 / 상품별 찜 개수 조회 - 로그인 없이 접근 허용
94+
.requestMatchers(HttpMethod.GET, "/api/products","/api/products/*", "/api/categories","/api/tags", "/api/products/images/download/{productUuid}","/api/products/{productUuid}/*", "/api/search", "/api/wishlist/{productUuid}/count").permitAll()
95+
// 상품 찜 등록, 삭제 - 로그인한 유저만 접근 가능
96+
.requestMatchers("/api/wishlist/{productUuid}").authenticated()
9597
// 상품 등록, 수정, 삭제 / 상품 이미지 업로드 / 작가 사업자 정보 조회 - ARTIST, ADMIN, ROOT만 접근 가능
9698
.requestMatchers("/api/products", "/api/products/*", "/api/artist/business-info").hasAnyRole("ARTIST", "ADMIN", "ROOT")
9799
// 카테고리,태그 등록, 수정, 삭제 - ADMIN, ROOT만 접근 가능

0 commit comments

Comments
 (0)