Skip to content

Feat/#4 장바구니 조회, 제품 등록, 수량 수정, 삭제 api #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 74 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
cc6e999
feat: cart, category, product, review 엔터티 생성
724thomas Apr 18, 2025
ff32d39
refactor: 테스트를 위한 인증 해제
724thomas Apr 18, 2025
dee4601
feat: 요청, 응답, dto
724thomas Apr 18, 2025
e44b583
feat: minio 추가
724thomas Apr 18, 2025
dbdc92c
feat: Role이 존재하지 않음 메시지 추가
724thomas Apr 18, 2025
bd2c4d2
feat: redis, s3 config 추가
724thomas Apr 18, 2025
223d837
feat: s3(minio) 유틸 추가
724thomas Apr 18, 2025
ae77b0a
feat: 제품 이미지 업로드, 제품 생성, 제품 수정, 제품 제거, 제품 조회 기능 추가
724thomas Apr 18, 2025
616cef3
fix: remove cascade
724thomas Apr 11, 2025
646a43f
fix: admin 엔터티 수정
724thomas Apr 18, 2025
e88d56a
fix: JPQL 삭제
724thomas Apr 18, 2025
4209b8b
fix: Test Profile 설정
724thomas Apr 18, 2025
16a3b60
feat: querydsl, redis, s3(minio), h2 추가
724thomas Apr 18, 2025
219ab60
remove: initData 입력 삭제
724thomas Apr 18, 2025
8cd60ed
fix: Cart가 연결되지 않던 문제 해결
724thomas Apr 18, 2025
37bf7e9
fix: Cart가 연결되지 않아서 생긴 test 문제 해결
724thomas Apr 18, 2025
4c8a5e8
fix: 테스트 프로파일 추가
724thomas Apr 18, 2025
694472f
feat: JPQL을 querydsl로 변경
724thomas Apr 18, 2025
40307f2
feat: Querydsl config 추가
724thomas Apr 18, 2025
25be277
fix: user 테이블 users로 변경. h2에서 오류
724thomas Apr 18, 2025
00ccf14
feat: 테스트용 h2 데이터베이스 적용
724thomas Apr 18, 2025
a252a45
refactor: 테스트를 위한 인증 해제 및 역할 권한 연결 수정
724thomas Apr 18, 2025
7dc4602
rebase: gradle
724thomas Apr 18, 2025
b683141
Merge branch 'feat/#2_Admin_Login' into feat/#3_product_api
724thomas Apr 18, 2025
52aaa9d
feat: 단일 상품 조회
724thomas Apr 18, 2025
6c90318
build: 하위 모듈 gradlew, gradle-wrapper 제거
724thomas Apr 27, 2025
47a33fa
refactor: findByEmail과 동일한 기능을 하는 existsByEmail 삭제
724thomas Apr 27, 2025
adc5ce3
Build: 루트 gradle 수정
724thomas Apr 27, 2025
470430a
Build: jwt claimSubject 수정
724thomas Apr 27, 2025
4773fbd
fix: remove cascade
724thomas Apr 11, 2025
e882f8c
fix: JPQL 삭제
724thomas Apr 18, 2025
931ef59
feat: 테스트용 h2 데이터베이스 적용
724thomas Apr 18, 2025
c37fd59
fix: gradle 수정
724thomas Apr 30, 2025
a79fa91
refactor: image key 필드 삭제
724thomas Apr 30, 2025
1fe35c0
fix: 권한 관련 문제를 해결하기전까지 임시 permitall
724thomas Apr 30, 2025
0731ad3
fix: Pathvariable 변수 이름 명시
724thomas Apr 30, 2025
4ee9377
refactor: 제품 등록 요청시 이미지 정보를 리스트 -> 객체로 받을 수 있도록 수정
724thomas Apr 30, 2025
9ba5b63
refactor: 이미지 저장시, 이미지 종류에 따라 경로를 다르게 하기 위해 파라미터 추가
724thomas Apr 30, 2025
8188cda
refactor: 제품조회시 categoryId 기본값을 null로 변경
724thomas Apr 30, 2025
26c8ef0
refactor: minio 이미지 업로드시 이미지 정보를 db에 같이 저장하지 않고, 상품 등록시 이미지 정보를 저장하도록…
724thomas Apr 30, 2025
51f44cd
feat: adminId로 역할 조회
724thomas Apr 30, 2025
4e82b4e
Merge branch 'feat/#2_Admin_Login' into feat/#3_product_api
724thomas Apr 30, 2025
c7fb43e
refactor:
724thomas May 3, 2025
82b7c77
feat: 실제 AWS에서 사용될 코드 주석으로 남겨뒀습니다.
724thomas May 3, 2025
0f5e076
fix: gradle 수정
724thomas May 9, 2025
34d28ce
feat: Cart, User 엔터티 1:1 매핑 생성
724thomas May 9, 2025
d4740ac
fix: Cart, User 엔터티 매핑으로 인한 순환 저장 문제 해결
724thomas May 9, 2025
2cb340a
refactor: Product엔터티에 썸네일 필드 추가(빠른 조회를 위해)
724thomas May 9, 2025
a306d93
feat: 제품 정보 요약 Dto
724thomas May 9, 2025
27ddd42
feat: 장바구니 제품 추가, 수량 수정 request
724thomas May 9, 2025
32a0e8c
feat: 유저 장바구니에 포함된 제품요약, 장바구니 수량, 제품 총 가격, 제품의 상태를 저장하는 dto
724thomas May 9, 2025
0c1e9a0
feat: 장바구니에 포함된 제품의 상태
724thomas May 9, 2025
4ad7f86
feat: 장바구니 N:N 제품 연결 엔터티
724thomas May 9, 2025
3b2445c
feat: 장바구니 정보 response
724thomas May 9, 2025
86a355b
feat: 장바구니 조회, 제품 등록, 제품 수량 수정, 제품 삭제 api
724thomas May 9, 2025
9c61f64
refactor: findByEmail과 동일한 기능을 하는 existsByEmail 삭제
724thomas Apr 27, 2025
f92aa81
Merge branch 'feat/#3_product_api' into feat/#4_cart-CRUD
724thomas May 9, 2025
8862ee1
Merge branch 'main' into feat/#4_cart-CRUD
724thomas May 9, 2025
25da040
Merge branch 'main' into feat/#3_product_api
724thomas May 9, 2025
de8a3b1
fix: offset과 limit이 없어서 페이징이 되지 않던 문제 해결
724thomas May 15, 2025
49b6403
fix: offset과 limit이 없어서 페이징이 되지 않던 문제 해결
724thomas May 15, 2025
48b8a63
fix: gradle 수정
724thomas May 9, 2025
6de61cd
feat: Cart, User 엔터티 1:1 매핑 생성
724thomas May 9, 2025
746d6f2
fix: Cart, User 엔터티 매핑으로 인한 순환 저장 문제 해결
724thomas May 9, 2025
db35984
refactor: Product엔터티에 썸네일 필드 추가(빠른 조회를 위해)
724thomas May 9, 2025
c6bb1b0
feat: 제품 정보 요약 Dto
724thomas May 9, 2025
cbd15f0
feat: 장바구니 제품 추가, 수량 수정 request
724thomas May 9, 2025
8f2f1b1
feat: 유저 장바구니에 포함된 제품요약, 장바구니 수량, 제품 총 가격, 제품의 상태를 저장하는 dto
724thomas May 9, 2025
3f057f3
feat: 장바구니에 포함된 제품의 상태
724thomas May 9, 2025
b8df186
feat: 장바구니 N:N 제품 연결 엔터티
724thomas May 9, 2025
5b2c07e
feat: 장바구니 정보 response
724thomas May 9, 2025
db57b3b
feat: 장바구니 조회, 제품 등록, 제품 수량 수정, 제품 삭제 api
724thomas May 9, 2025
140ce59
Merge remote-tracking branch 'origin/feat/#4_cart-CRUD' into feat/#4_…
724thomas May 15, 2025
1c86edb
feat: 장바구니 조회 시 캐싱. 추가, 수정, 삭제시 캐시무효
724thomas May 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions admin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ plugins {
group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
springBoot {
mainClass = 'com.example.admin.AdminApplication'
}

configurations {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()

.requestMatchers("/admin/v1/auth/**").permitAll()
//v1
.anyRequest().authenticated()
//test purpose only
.requestMatchers("/**").permitAll()
// .requestMatchers("/admin/**").permitAll()

// .requestMatchers("/admin/v1/auth/**").permitAll()
// .anyRequest().authenticated()
)

// 인증 실패 시 처리
Expand Down
8 changes: 3 additions & 5 deletions api/build.gradle
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.3' apply false
id 'org.springframework.boot' version '3.4.3'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
bootJar {
mainClass = 'com.example.api.ApiApplication'
}

springBoot {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.httpBasic(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))

// /signyp, /loginm /refresh 경로는 인증 없이 접근 가능하도록 설정
// /signup, /login /refresh 경로는 인증 없이 접근 가능하도록 설정
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()

//v1
.requestMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
//test purpose only
.requestMatchers("/**").permitAll()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

임시 코드가 반영된 것 같습니다


// .requestMatchers("/api/v1/auth/**").permitAll()
// .anyRequest().authenticated()
)

// 인증 실패 시 처리
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ public AuthResponse signup(SignupRequestBody body) {
Cart cart = cartApiRepository.save(new Cart());

User user = User.of(body.getEmail(), body.getNickname(), salt, hashedPassword, body.getPhoneNumber(), cart);
user = userApiRepository.save(user);
cart.setUser(user);
userApiRepository.save(user);


return AuthResponse.from(user);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.api.module.cart.controller;

import com.example.api.module.cart.controller.request.CartProductAddRequest;
import com.example.api.module.cart.controller.request.CartProductQuantityUpdateRequest;
import com.example.api.module.cart.controller.response.CartSummaryResponse;
import com.example.api.module.cart.service.CartService;
import com.example.core.model.response.DataResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/cart")
public class CartController {

private final CartService cartService;

@GetMapping()
public DataResponse<CartSummaryResponse> getCart(
@AuthenticationPrincipal Long userId) {
return DataResponse.of(cartService.getCart(userId));
}


@PostMapping("/products")
public DataResponse<CartSummaryResponse> addCartProduct(
@AuthenticationPrincipal Long userId,
@RequestBody CartProductAddRequest req) {
return DataResponse.of(cartService.addCartProduct(userId, req));
}

@PutMapping("/products/{productId}")
public DataResponse<CartSummaryResponse> updateCartProduct(
@AuthenticationPrincipal Long userId,
@PathVariable Long productId,
@RequestBody CartProductQuantityUpdateRequest req) {
return DataResponse.of(cartService.updateCartProductQuantity(userId, productId, req));
}

@DeleteMapping("/products/{productId}")
public DataResponse<CartSummaryResponse> deleteCartProduct(
@AuthenticationPrincipal Long userId,
@PathVariable Long productId) {
return DataResponse.of(cartService.deleteCartProduct(userId, productId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.api.module.cart.controller.request;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class CartProductAddRequest {
private Long productId;
private Long addQuantity;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.api.module.cart.controller.request;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class CartProductQuantityUpdateRequest {
private Long quantity;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.api.module.cart.controller.response;

import com.example.core.dto.CartProductDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CartSummaryResponse {
List<CartProductDto> cartProductList;
Long cartTotalPrice; //총 가격

public static CartSummaryResponse of(List<CartProductDto> cartProductList) {
Long totalPrice = cartProductList.stream()
.mapToLong(CartProductDto::getProductTotalPrice)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이전에 말씀드린대로 BigDecimal 로 처리하시는게 좋습니다

.sum();

return builder()
.cartProductList(cartProductList)
.cartTotalPrice(totalPrice)
.build();
}
}
145 changes: 145 additions & 0 deletions api/src/main/java/com/example/api/module/cart/service/CartService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.example.api.module.cart.service;

import com.example.api.module.cart.controller.request.CartProductAddRequest;
import com.example.api.module.cart.controller.request.CartProductQuantityUpdateRequest;
import com.example.api.module.cart.controller.response.CartSummaryResponse;
import com.example.core.domain.cart.Cart;
import com.example.core.domain.cart_product.CartProduct;
import com.example.core.domain.cart_product.api.CartProductApiRepository;
import com.example.core.domain.product.Product;
import com.example.core.domain.product.api.ProductApiRepository;
import com.example.core.domain.user.User;
import com.example.core.domain.user.api.UserApiRepository;
import com.example.core.dto.CartProductDto;
import com.example.core.dto.ProductSummaryDto;
import com.example.core.enums.CartProductStatus;
import com.example.core.exception.BadRequestException;
import lombok.AllArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
@AllArgsConstructor
public class CartService {

private final UserApiRepository userApiRepository;
private final ProductApiRepository productApiRepository;
private final CartProductApiRepository cartProductApiRepository;

@Cacheable(value = "cart", key = "#userId")
@Transactional(readOnly = true)
public CartSummaryResponse getCart(Long userId) {
User user = userApiRepository.findById(userId)
.orElseThrow(() -> new BadRequestException("User not found"));
return getCartSummary(user);
}

@CacheEvict(value = "cart", key = "#userId")
@Transactional
public CartSummaryResponse addCartProduct(Long userId, CartProductAddRequest req) {
User user = userApiRepository.findById(userId)
.orElseThrow(() -> new BadRequestException("User not found"));
Cart cart = user.getCart();
Product product = productApiRepository.findById(req.getProductId())
.orElseThrow(() -> new BadRequestException("Product not found"));

Long quantity = req.getAddQuantity();
if (quantity <= 0) throw new BadRequestException("Quantity must be greater than 0");

Optional<CartProduct> cartProductOpt = cartProductApiRepository.
findByCart_IdAndProduct_Id(cart.getId(), req.getProductId());

CartProduct cartProduct;
if (cartProductOpt.isPresent()) {
//update existing cart product
cartProduct = cartProductOpt.get();
cartProduct.addQuantity(quantity);
} else {
//create new cart product
cartProduct = CartProduct.of(cart, product, quantity);
}
cartProductApiRepository.save(cartProduct);
return getCartSummary(user);
}

@CacheEvict(value = "cart", key = "#userId")
@Transactional
public CartSummaryResponse updateCartProductQuantity(Long userId, Long productId, CartProductQuantityUpdateRequest req) {
User user = userApiRepository.findById(userId)
.orElseThrow(() -> new BadRequestException("User not found"));
Cart cart = user.getCart();

CartProduct cartProduct = cartProductApiRepository.findByCart_IdAndProduct_Id(cart.getId(), productId)
.orElseThrow(() -> new BadRequestException("Cart product not found"));

Long quantity = req.getQuantity();

if (quantity <= 0) {
cartProductApiRepository.delete(cartProduct);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Softdelete 가 좋을 것 같습니다

} else {
cartProduct.setQuantity(quantity);
cartProductApiRepository.save(cartProduct);
}
return getCartSummary(user);
}

@CacheEvict(value = "cart", key = "#userId")
@Transactional
public CartSummaryResponse deleteCartProduct(Long userId, Long productId) {
User user = userApiRepository.findById(userId)
.orElseThrow(() -> new BadRequestException("User not found"));
Cart cart = user.getCart();

CartProduct cartProduct = cartProductApiRepository.findByCart_IdAndProduct_Id(cart.getId(), productId)
.orElseThrow(() -> new BadRequestException("Cart product not found"));

cartProductApiRepository.delete(cartProduct);
return getCartSummary(user);
}


private CartSummaryResponse getCartSummary(User user) {
Cart cart = user.getCart();

List<CartProduct> cartProductList = cartProductApiRepository.findByCartId(cart.getId());

List<CartProductDto> cartProductDtoList = new ArrayList<>();

for (CartProduct cartProduct : cartProductList) {
Product product = cartProduct.getProduct();
ProductSummaryDto productSummaryDto;
Long quantity = 0L;
CartProductStatus status;

if (product == null || product.isDeleted()) {
status = CartProductStatus.DELETED;
productSummaryDto = null;

} else if (product.getStockQuantity() < cartProduct.getQuantity()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분이 재고 부족을 체크하는 로직인가요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인

status = CartProductStatus.SHORTAGE;
productSummaryDto = ProductSummaryDto.of(product);
quantity = product.getStockQuantity();

} else {
status = CartProductStatus.AVAILABLE;
productSummaryDto = ProductSummaryDto.of(product);
quantity = cartProduct.getQuantity();
}
Long totalPrice = (productSummaryDto != null) ? productSummaryDto.getPrice() * cartProduct.getQuantity() : 0;

cartProductDtoList.add(new CartProductDto(
productSummaryDto,
quantity,
status,
totalPrice
));
}
return CartSummaryResponse.of(cartProductDtoList);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.api.module.product.controller;

import com.example.api.module.product.controller.request.ProductCreateRequest;
import com.example.api.module.product.controller.request.ProductUpdateRequest;
import com.example.api.module.product.controller.request.ProductSearchConditionRequest;
import com.example.api.module.product.controller.response.ImageUploadResponse;
import com.example.api.module.product.controller.response.ProductResponse;
import com.example.api.module.product.controller.response.ProductsSearchResponse;
import com.example.api.module.product.service.ProductService;
import com.example.core.model.response.DataResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/products")
public class ProductController {

private final ProductService productService;


//GET /api/v1/products?categoryId={categoryId}&minPrice={minprice}&maxPrice={maxprice}&sort={sortType},{sortBy}&page={page}&size={size}
@GetMapping()
public DataResponse<Page<ProductsSearchResponse>> getProducts(@ModelAttribute ProductSearchConditionRequest condition) {
return DataResponse.of(productService.getProducts(condition));
}

@GetMapping("/{productId}")
public DataResponse<ProductResponse> getProduct(@PathVariable("productId") Long productId) {
return DataResponse.of(productService.getProduct(productId));
}

// @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'ADMIN')")
@PostMapping("/image")
public DataResponse<ImageUploadResponse> uploadImage(@RequestParam("file") MultipartFile file) {
System.out.println("/image controller");
return DataResponse.of(productService.uploadImage(file));
}

// @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'ADMIN')")
@PostMapping()
public DataResponse<Long> createProduct(@RequestBody ProductCreateRequest request) {
return DataResponse.of(productService.createProduct(request));
}

// @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'ADMIN')")
@PutMapping("/{productId}")
public DataResponse<Long> updateProduct(@PathVariable Long productId, @RequestBody ProductUpdateRequest request) {
return DataResponse.of(productService.updateProduct(productId, request));
}

// @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'ADMIN')")
@DeleteMapping("/{productId}")
public DataResponse<Long> deleteProduct(@PathVariable Long productId) {
return DataResponse.of(productService.deleteProduct(productId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.api.module.product.controller.request;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

@Data
public class ImageUploadRequest {
private String title;
private String url;
private MultipartFile file;
}
Loading
Loading