From a6a4ec9049e4b39f4104ad7e34709ec5565f9fc6 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 24 Dec 2024 01:56:59 +0900 Subject: [PATCH 01/20] =?UTF-8?q?fix=20:=20Build=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20log=20=EB=A0=88=EB=B2=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정한 내용 : 1. 빌드 에러 수정 -> Clock Config (seoul ->Seoul)로 대문자로 수정 -> Jwt Config Propertiese 방식으로 수정 -> Payment Properties setter 추가 2. logging level 수정 -> debug 에서 Info로 로깅 레벨을 약하게 수정 --- .../auction/common/config/ClockConfig.java | 2 +- .../auction/common/config/JwtConfig.java | 24 ++++++++----------- .../common/properties/PaymentProperties.java | 2 ++ src/main/resources/application.yml | 6 ++--- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/common/config/ClockConfig.java b/src/main/java/com/tasksprints/auction/common/config/ClockConfig.java index 1757f93a..9764216b 100644 --- a/src/main/java/com/tasksprints/auction/common/config/ClockConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/ClockConfig.java @@ -9,6 +9,6 @@ public class ClockConfig { @Bean public Clock clock() { - return Clock.system(ZoneId.of("Asia/seoul")); + return Clock.system(ZoneId.of("Asia/Seoul")); } } diff --git a/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java index f272e578..e9982640 100644 --- a/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java @@ -2,23 +2,19 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; -@Component @Getter -@RequiredArgsConstructor +@Setter +@Configuration +@ConfigurationProperties(prefix = "jwt") public class JwtConfig { - - @Value("${jwt.expire-ms}") - private final Long accessExpireMs; - - @Value("${jwt.expire-ms}") - private final Long refreshExpireMs; - - @Value("${jwt.issuer}") - private final String issuer; - - @Value("${jwt.secret}") - private final String secretKey; + private Long accessExpireMs; + private Long refreshExpireMs; + private String issuer; + private String secretKey; } diff --git a/src/main/java/com/tasksprints/auction/common/properties/PaymentProperties.java b/src/main/java/com/tasksprints/auction/common/properties/PaymentProperties.java index ca6a1650..6a9a2ba1 100644 --- a/src/main/java/com/tasksprints/auction/common/properties/PaymentProperties.java +++ b/src/main/java/com/tasksprints/auction/common/properties/PaymentProperties.java @@ -2,6 +2,7 @@ import lombok.Getter; +import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -10,6 +11,7 @@ @Component @ConfigurationProperties(prefix = "payment.toss") @Getter +@Setter public class PaymentProperties { private String testClientApiKey; private String testSecretApiKey; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 67ef9efc..91768dd0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -49,9 +49,9 @@ springdoc: packages-to-scan: com.tasksprints.auction.api logging: level: - org.hibernate.SQL: DEBUG - org.hibernate.type.descriptor.sql.BasicBinder: TRACE - root: debug + org.hibernate.SQL: debug + org.hibernate.type.descriptor.sql.BasicBinder: INFO + root: INFO server: port: 8080 From 801059c4cf5efd9cd97d8f208d509d06a3f917bf Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 24 Dec 2024 02:06:14 +0900 Subject: [PATCH 02/20] =?UTF-8?q?delete=20:=20viewCount=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 삭제한 내용 : 1. viewCount 삭제 -> 불필요한 update 쿼리를 만들어서 추후에 더 좋은 방식으로 구현할 예정 예를 들면 redis --- .../auction/application/service/AuctionServiceImpl.java | 1 - .../auction/domain/dto/response/AuctionResponse.java | 4 ---- .../tasksprints/auction/auction/domain/entity/Auction.java | 7 ------- .../support/AuctionCriteriaRepositoryImpl.java | 1 - 4 files changed, 13 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java index 7f059082..91aa4376 100644 --- a/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java @@ -100,7 +100,6 @@ public List getAllAuctions() { public AuctionResponse getAuctionById(Long auctionId) { Auction foundAuction = auctionRepository.findAuctionById(auctionId) .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); - foundAuction.incrementViewCount(); auctionRepository.save(foundAuction); return AuctionResponse.of(foundAuction); } diff --git a/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionResponse.java b/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionResponse.java index 25254c70..ab08f864 100644 --- a/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionResponse.java +++ b/src/main/java/com/tasksprints/auction/auction/domain/dto/response/AuctionResponse.java @@ -22,7 +22,6 @@ public class AuctionResponse { private String category; private String status; private BigDecimal startingBid; - private Long viewCount; private Long sellerId; private String sellerNickName; @@ -37,7 +36,6 @@ public static class Details { private String category; private String status; private BigDecimal startingBid; - private Long viewCount; private Long sellerId; private String sellerNickName; private Long productId; @@ -53,7 +51,6 @@ public static AuctionResponse.Details of(Auction auction) { .category(auction.getAuctionCategory().name()) .status(auction.getAuctionStatus().name()) .startingBid(auction.getStartingBid()) - .viewCount(auction.getViewCount()) .sellerId(auction.getSeller() != null ? auction.getSeller().getId() : null) .sellerNickName(auction.getSeller() != null ? auction.getSeller().getNickName() : null) .productId(auction.getProduct() != null ? auction.getProduct().getId() : null) @@ -74,7 +71,6 @@ public static AuctionResponse of(Auction auction) { .category(auction.getAuctionCategory().name()) .status(auction.getAuctionStatus().name()) .startingBid(auction.getStartingBid()) - .viewCount(auction.getViewCount()) .sellerId(auction.getSeller().getId()) .sellerNickName(auction.getSeller().getNickName()) .build(); diff --git a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java index d9e9d694..d45ab075 100644 --- a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java +++ b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java @@ -54,9 +54,6 @@ public class Auction extends BaseEntity { @Builder.Default private List bids = new ArrayList<>(); - @Column(nullable = false) - private long viewCount = 0L; - public static Auction create(LocalDateTime startTime, LocalDateTime endTime, BigDecimal startingBid, AuctionCategory auctionCategory, AuctionStatus auctionStatus, User seller) { Auction newAuction = Auction.builder() .startTime(startTime) @@ -79,9 +76,5 @@ public void addUser(User seller) { this.seller = seller; } - public void incrementViewCount() { - this.viewCount += 1; - } - } diff --git a/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepositoryImpl.java b/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepositoryImpl.java index 83128f64..a72da439 100644 --- a/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepositoryImpl.java +++ b/src/main/java/com/tasksprints/auction/auction/infrastructure/support/AuctionCriteriaRepositoryImpl.java @@ -110,7 +110,6 @@ private OrderSpecifier getSortOrder(AuctionRequest.SearchCondition condition) case "bidsDesc" -> auction.bids.size().desc(); case "endTimeASC" -> auction.endTime.asc(); case "startTimeASC" -> auction.startTime.asc(); - case "viewCountDESC" -> auction.viewCount.desc(); default -> null; }; From 54dd042f0931374031c972b95171d924edcbe7c6 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 24 Dec 2024 11:41:25 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat=20:=20SearchCondition=20Resolver?= =?UTF-8?q?=EC=97=90=20=EC=9C=A0=ED=9A=A8=20=EA=B0=92=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추가한 내용 : 1. 유효한 값 인지 검증하는 validate 추가 -> 비즈니스 로직이 아닌 유효 값 검증은 resolver에서 처리해도 되겠다고 생각함 - 경매 시작,종료 시각 범위는 함께 주어지도록 검증 - 경매 시작 시각이 종료 시각보다 이전이도록 검증 - MinPrice의 값이 MaxPrice보다 작도록 검증 --- .../resolver/SearchConditionResolver.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java b/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java index 6ca52788..eb75639d 100644 --- a/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java +++ b/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java @@ -26,7 +26,6 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { - // QueryString에서 값을 추출 String auctionCategory = webRequest.getParameter("auctionCategory"); String productCategory = webRequest.getParameter("productCategory"); @@ -45,6 +44,12 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m BigDecimal parsedMaxPrice = maxPrice != null ? new BigDecimal(maxPrice) : null; AuctionStatus parsedAuctionStatus = auctionStatus != null ? AuctionStatus.fromDisplayName(auctionStatus) : null; + //값 검증(비즈니스 로직이 아닌 값 유효성 검증은 resolver가 담당) + validateBothTimesProvided(parsedStartTime, parsedEndTime); + validateIsStartBeforeEnd(parsedStartTime, parsedEndTime); + validateMinLessThanMax(parsedMinPrice, parsedMaxPrice); + + // SearchCondition 객체 생성 및 반환 return new AuctionRequest.SearchCondition( parsedAuctionCategory, @@ -57,4 +62,26 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m sortBy ); } + + private void validateMinLessThanMax(BigDecimal parsedMinPrice, BigDecimal parsedMaxPrice) { + if (parsedMinPrice != null && parsedMaxPrice != null) { + if (parsedMinPrice.compareTo(parsedMaxPrice) > 0) { + throw new IllegalArgumentException("minPrice cannot be greater than maxPrice."); + } + } + } + + private void validateIsStartBeforeEnd(LocalDateTime parsedStartTime, LocalDateTime parsedEndTime) { + if (parsedStartTime != null && parsedEndTime != null) { + if (parsedStartTime.isAfter(parsedEndTime)) { + throw new IllegalArgumentException("startTime cannot be after endTime."); + } + } + } + + private void validateBothTimesProvided(LocalDateTime parsedStartTime, LocalDateTime parsedEndTime) { + if ((parsedStartTime == null && parsedEndTime != null) || (parsedStartTime != null && parsedEndTime == null)) { + throw new IllegalArgumentException("Both startTime and endTime must be provided together."); + } + } } From 795083846ba26bc7952d469ce436def3a14f875d Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 24 Dec 2024 11:45:26 +0900 Subject: [PATCH 04/20] =?UTF-8?q?refactor=20:=20Wallet=20ID=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=EA=B2=BD(String=20->=20Long)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 변경한 내용 : 1. UserDetailResponse에서 Wallet ID 필드가 String이던 것을 Long으로 변경 -> 엔티티 필드에서 walletId는 Long 타입이라서. --- .../user/domain/dto/response/UserDetailResponse.java | 4 ++-- .../auction/user/presentation/UserControllerTest.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserDetailResponse.java b/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserDetailResponse.java index 3e0004c1..3c3585fa 100644 --- a/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserDetailResponse.java +++ b/src/main/java/com/tasksprints/auction/user/domain/dto/response/UserDetailResponse.java @@ -8,19 +8,19 @@ @Data public class UserDetailResponse { private Long id; + private Long walletId; private String name; private String email; private String password; private String nickName; - private String walletId; private UserDetailResponse(User user) { this.id = user.getId(); + this.walletId = user.getWallet().getId(); this.name = user.getName(); this.email = user.getEmail(); this.password = user.getPassword(); this.nickName = user.getNickName(); - this.walletId = String.valueOf(user.getWallet().getId()); } public static UserDetailResponse of(User user) { diff --git a/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java b/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java index 102c944b..549b81be 100644 --- a/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java +++ b/src/test/java/com/tasksprints/auction/user/presentation/UserControllerTest.java @@ -48,7 +48,7 @@ class SuccessfulTests { @Test @DisplayName("POST /api/v1/user - 성공") void registerUser() throws Exception { - UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123", "1L"); + UserDetailResponse userDetailResponse = new UserDetailResponse(1L, 1L,"John", "john@example.com", "password", "john123"); Mockito.when(userService.createUser(any(UserRequest.Register.class))).thenReturn(userDetailResponse); @@ -67,7 +67,7 @@ void registerUser() throws Exception { @Test @DisplayName("GET /api/v1/user/{id} - 성공") void getUserById() throws Exception { - UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123", "1L"); + UserDetailResponse userDetailResponse = new UserDetailResponse(1L, 1L,"John", "john@example.com", "password", "john123" ); Mockito.when(userService.getUserDetailsById(anyLong())).thenReturn(userDetailResponse); @@ -97,7 +97,7 @@ void getAllUsers() throws Exception { @Test @DisplayName("PUT /api/v1/user - 성공") void updateUser() throws Exception { - UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John Updated", "john@example.com", "newpassword", "john123updated", "1L"); + UserDetailResponse userDetailResponse = new UserDetailResponse(1L, 1L,"John Updated", "john@example.com", "newpassword", "john123updated" ); Mockito.when(userService.updateUser(anyLong(), any(UserRequest.Update.class))).thenReturn(userDetailResponse); From d3ffce0c66eddb992f02ffede280b023861745bf Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 24 Dec 2024 17:17:36 +0900 Subject: [PATCH 05/20] =?UTF-8?q?refactor=20:=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94=20(N+1=20=EB=AC=B8=EC=A0=9C)=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정한 내용 : - 명시적으로 모든 연관 관계를 지연 로딩으로 설정(디폴트는 toMany는 지연 로딩, toOne은 즉시 로딩) 1. getAuctionById 메서드 쿼리 최적화 - findAuctionById V2 메서드 추가 -> Auction 조회 시 User를 fetch join하여 의도치 않은 쿼리 방지 -> 쿼리 3개 => 쿼리 1개로 최적화 2. getProductByAucitonId 메서드 쿼리 최적화 - Product <-> ProductImage 간 양방향으로 매핑 관계 변경 -> 기존은 Product -> ProductImage로의 단방향 매핑으로 조인 테이블 방식이었지만, 양방향 매핑 시 외래 키 매핑 방식으로 조인 테이블을 경유하지 않아 성능이 향상 됨. - findByAuctionIdV2 메서드 추가 -> Product 조회 시 User와 컬렉션인 ProductImage까지 fetch join해서 가져 옴 -> 컬렉션은 지연 로딩 초기화 하는 방식이랑 트레이드 오프 고려 해야 함. 3. application.yml에 default batch size 설정 -> 엔티티 조회 시 지연 로딩을 in 쿼리로 묶어서 한 번에 로딩 하기 위함(쿼리 최적화) 추가한 메서드는 적용 안 했음 --- .../auction/auction/domain/entity/Auction.java | 4 ++-- .../auction/auction/infrastructure/AuctionRepository.java | 3 +++ .../auction/product/domain/entity/Product.java | 8 +++++--- .../auction/product/domain/entity/ProductImage.java | 6 ++++++ .../auction/product/infrastructure/ProductRepository.java | 6 +++++- src/main/resources/application.yml | 2 +- .../auction/infrastructure/AuctionRepositoryTest.java | 6 +++--- 7 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java index d45ab075..53ff8406 100644 --- a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java +++ b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java @@ -46,11 +46,11 @@ public class Auction extends BaseEntity { @ToString.Exclude private User seller; - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @Builder.Default private Product product = null; - @OneToMany + @OneToMany(fetch = FetchType.LAZY) @Builder.Default private List bids = new ArrayList<>(); diff --git a/src/main/java/com/tasksprints/auction/auction/infrastructure/AuctionRepository.java b/src/main/java/com/tasksprints/auction/auction/infrastructure/AuctionRepository.java index 76f82e81..489f466a 100644 --- a/src/main/java/com/tasksprints/auction/auction/infrastructure/AuctionRepository.java +++ b/src/main/java/com/tasksprints/auction/auction/infrastructure/AuctionRepository.java @@ -18,5 +18,8 @@ public interface AuctionRepository extends JpaRepository, Auction @Query("SELECT a FROM auction a WHERE a.id = :auctionId") Optional findAuctionById(@Param("auctionId") Long auctionId); + @Query("SELECT a FROM auction a JOIN FETCH a.seller u WHERE a.id = :auctionId") + Optional findAuctionByIdV2(@Param("auctionId") Long auctionId); + } diff --git a/src/main/java/com/tasksprints/auction/product/domain/entity/Product.java b/src/main/java/com/tasksprints/auction/product/domain/entity/Product.java index bb195282..3f2e9818 100644 --- a/src/main/java/com/tasksprints/auction/product/domain/entity/Product.java +++ b/src/main/java/com/tasksprints/auction/product/domain/entity/Product.java @@ -31,16 +31,16 @@ public class Product extends BaseEntity { @Enumerated(EnumType.STRING) private ProductCategory category; //제품의 Category - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner_id") private User owner; - @OneToOne + @OneToOne(fetch = FetchType.LAZY) //mappedby @JoinColumn(name = "auction_id") private Auction auction; - @OneToMany + @OneToMany(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL) @Builder.Default private List productImageList = new ArrayList<>(); @@ -62,7 +62,9 @@ public void addAuction(Auction auction) { } public void initProductImageList(List productImageList) { + if (productImageList == null) return; this.productImageList = productImageList; + productImageList.forEach(image -> image.addProduct(this)); } public void addOwnerAndAuction(User owner, Auction auction) { diff --git a/src/main/java/com/tasksprints/auction/product/domain/entity/ProductImage.java b/src/main/java/com/tasksprints/auction/product/domain/entity/ProductImage.java index 4ebac704..78adcdea 100644 --- a/src/main/java/com/tasksprints/auction/product/domain/entity/ProductImage.java +++ b/src/main/java/com/tasksprints/auction/product/domain/entity/ProductImage.java @@ -19,7 +19,13 @@ public class ProductImage { @Getter private String imageUrl; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + public void addProduct(Product product) { + this.product = product; + } // @ColumnDefault("false") // private Boolean isPrime; diff --git a/src/main/java/com/tasksprints/auction/product/infrastructure/ProductRepository.java b/src/main/java/com/tasksprints/auction/product/infrastructure/ProductRepository.java index 16597e97..c92ca2b8 100644 --- a/src/main/java/com/tasksprints/auction/product/infrastructure/ProductRepository.java +++ b/src/main/java/com/tasksprints/auction/product/infrastructure/ProductRepository.java @@ -2,13 +2,17 @@ import com.tasksprints.auction.product.domain.entity.Product; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; public interface ProductRepository extends JpaRepository { Optional findByAuctionId(Long auctionId); - + //컬렉션까지 Fetch Join 하거나, 컬렉션은 지연 로딩 초기화로 하는 방식 트레이드 오프 고려하기 + @Query("SELECT DISTINCT p FROM products p JOIN FETCH p.owner o LEFT JOIN FETCH p.productImageList pi WHERE p.auction.id = :auctionId") + Optional findByAuctionIdV2(@Param("auctionId")Long auctionId); // 쿼리를 메서드 이름으로 표현 List findByOwnerId(Long ownerId); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 91768dd0..7f036cf3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,7 +23,7 @@ spring: properties: hibernate: format_sql: true -# default_batch_fetch_size: 100 + default_batch_fetch_size: 100 database-platform: org.hibernate.dialect.H2Dialect thymeleaf: diff --git a/src/test/java/com/tasksprints/auction/auction/infrastructure/AuctionRepositoryTest.java b/src/test/java/com/tasksprints/auction/auction/infrastructure/AuctionRepositoryTest.java index ea29e9d5..86b75275 100644 --- a/src/test/java/com/tasksprints/auction/auction/infrastructure/AuctionRepositoryTest.java +++ b/src/test/java/com/tasksprints/auction/auction/infrastructure/AuctionRepositoryTest.java @@ -1,6 +1,5 @@ -package com.tasksprints.auction.domain.auction.repository; +package com.tasksprints.auction.auction.infrastructure; -import com.tasksprints.auction.auction.infrastructure.AuctionRepository; import com.tasksprints.auction.common.config.QueryDslConfig; import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; import com.tasksprints.auction.auction.domain.entity.Auction; @@ -24,6 +23,7 @@ import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -86,7 +86,7 @@ private Product createProduct(User user, Auction auction, String productCategory user, auction, productCategory, - null + new ArrayList<>() ); } From 8f5f7389ee4dd1f201e341c8fde907a7ac4e8f6b Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:33:06 +0900 Subject: [PATCH 06/20] =?UTF-8?q?refactor=20:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20=EA=B0=92=20=EC=A0=80=EC=9E=A5=20Session?= =?UTF-8?q?=20->=20Redis=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 변경한 내용 : 1. application.yml 문법 수정 및 Redis yml 추가, build.gradle 추가, include : 아래 - jwt, - payment -> 띄어쓰기 안해서 값이 제대로 안들어가던 오류 수정 2. Session으로 결제 임시 값 저장 -> Redis에 임시 값 저장(5분 후 삭제) 3. PaymentController 세션 관련 테스트 수정 -> Redis 이용하도록 테스트 수정 --- build.gradle | 3 + .../handler/GlobalExceptionHandler.java | 6 ++ .../application/service/PaymentService.java | 1 - .../service/PaymentServiceImpl.java | 5 -- .../exception/RedisKeyNotFoundException.java | 7 ++ .../infrastructure/redis/RedisService.java | 26 ++++++ .../presentation/PaymentController.java | 45 +++++----- src/main/resources/application.yml | 10 ++- .../application/PaymentServiceImplTest.java | 23 ----- .../redis/RedisServiceTest.java | 23 +++++ .../presentation/PaymentControllerTest.java | 85 ++++++++----------- 11 files changed, 132 insertions(+), 102 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/payment/exception/RedisKeyNotFoundException.java create mode 100644 src/main/java/com/tasksprints/auction/payment/infrastructure/redis/RedisService.java create mode 100644 src/test/java/com/tasksprints/auction/payment/infrastructure/redis/RedisServiceTest.java diff --git a/build.gradle b/build.gradle index 49842485..d72e6a28 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,9 @@ dependencies { //jjwt implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'javax.xml.bind:jaxb-api:2.3.1' + //Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.apache.commons:commons-pool2' } tasks.named('test') { useJUnitPlatform() diff --git a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java index e569c4aa..b4469305 100644 --- a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java @@ -11,6 +11,7 @@ import com.tasksprints.auction.auth.exception.AuthException; import com.tasksprints.auction.payment.exception.InvalidSessionException; import com.tasksprints.auction.payment.exception.PaymentDataMismatchException; +import com.tasksprints.auction.payment.exception.RedisKeyNotFoundException; import com.tasksprints.auction.product.exception.ProductNotFoundException; import com.tasksprints.auction.user.exception.UserNotFoundException; import org.springframework.http.HttpStatus; @@ -70,6 +71,11 @@ public ResponseEntity> handleInvalidSessionException(InvalidSe return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message)); } + @ExceptionHandler(RedisKeyNotFoundException.class) + public ResponseEntity> handleRedisKeyNotFoundException(RedisKeyNotFoundException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(ex.getMessage())); + } + @ExceptionHandler(PaymentDataMismatchException.class) public ResponseEntity> PaymentDataMismatchException(PaymentDataMismatchException ex) { String message = "Session Data Mismatch Error. "; diff --git a/src/main/java/com/tasksprints/auction/payment/application/service/PaymentService.java b/src/main/java/com/tasksprints/auction/payment/application/service/PaymentService.java index fe2c6e70..90810eb4 100644 --- a/src/main/java/com/tasksprints/auction/payment/application/service/PaymentService.java +++ b/src/main/java/com/tasksprints/auction/payment/application/service/PaymentService.java @@ -9,7 +9,6 @@ public interface PaymentService { - public void prepare(HttpSession session, PaymentRequest.Prepare prepareRequest); public Response sendPaymentRequest(PaymentRequest.Confirm confirmRequest) throws IOException, InterruptedException; public Response handleTossPaymentResponse(Long userId, PaymentRequest.Confirm confirmRequest, Response response) throws IOException, InterruptedException ; } diff --git a/src/main/java/com/tasksprints/auction/payment/application/service/PaymentServiceImpl.java b/src/main/java/com/tasksprints/auction/payment/application/service/PaymentServiceImpl.java index 1d92715f..cbfc4009 100644 --- a/src/main/java/com/tasksprints/auction/payment/application/service/PaymentServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/payment/application/service/PaymentServiceImpl.java @@ -25,11 +25,6 @@ public class PaymentServiceImpl implements PaymentService { private final WalletService walletService; private final PaymentApiSerializer paymentApiSerializer; - @Override - public void prepare(HttpSession session, PaymentRequest.Prepare prepareRequest) { - session.setAttribute("orderId", prepareRequest.getOrderId()); - session.setAttribute("amount", prepareRequest.getAmount()); - } @Override public Response sendPaymentRequest(PaymentRequest.Confirm confirmRequest) throws IOException, InterruptedException{ return paymentApiSerializer.sendPaymentRequest(confirmRequest); diff --git a/src/main/java/com/tasksprints/auction/payment/exception/RedisKeyNotFoundException.java b/src/main/java/com/tasksprints/auction/payment/exception/RedisKeyNotFoundException.java new file mode 100644 index 00000000..2141e2c2 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/payment/exception/RedisKeyNotFoundException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.payment.exception; + +public class RedisKeyNotFoundException extends RuntimeException { + public RedisKeyNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/payment/infrastructure/redis/RedisService.java b/src/main/java/com/tasksprints/auction/payment/infrastructure/redis/RedisService.java new file mode 100644 index 00000000..70e68467 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/payment/infrastructure/redis/RedisService.java @@ -0,0 +1,26 @@ +package com.tasksprints.auction.payment.infrastructure.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +@Service +@RequiredArgsConstructor +public class RedisService { + + private final StringRedisTemplate redisTemplate; + + public void saveValue(String key, String value) { + redisTemplate.opsForValue().set(key, value); + } + + public void saveDataWithTimeOut(String key, String value, long timeoutSeconds) { + redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutSeconds)); + } + + public String getValue(String key) { + return redisTemplate.opsForValue().get(key); + } +} diff --git a/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java b/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java index 35abe18a..b55fdc55 100644 --- a/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java +++ b/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java @@ -8,6 +8,8 @@ import com.tasksprints.auction.payment.exception.InvalidSessionException; import com.tasksprints.auction.payment.exception.PaymentDataMismatchException; import com.tasksprints.auction.payment.application.service.PaymentService; +import com.tasksprints.auction.payment.exception.RedisKeyNotFoundException; +import com.tasksprints.auction.payment.infrastructure.redis.RedisService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpSession; @@ -18,25 +20,31 @@ import java.io.IOException; import java.math.BigDecimal; +import java.util.NoSuchElementException; +import java.util.Optional; @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/payment") public class PaymentController { private final PaymentService paymentService; + private final RedisService redisService; @PostMapping("/prepare") @Operation(summary = "Temporarily stores the payment element", description = "Save orderID and amount in session") @ApiResponse(responseCode = "200", description = "Payment prepared successfully") - public ResponseEntity> preparePayment(HttpSession session, @RequestBody PaymentRequest.Prepare prepareRequest) { - paymentService.prepare(session, prepareRequest); + public ResponseEntity> preparePayment(@RequestBody PaymentRequest.Prepare prepareRequest) { + redisService.saveDataWithTimeOut( + prepareRequest.getOrderId(), // key + prepareRequest.getAmount().toString(), // value + 300 // 5분 TTL + ); return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.PAYMENT_PREPARED_SUCCESS)); } @PostMapping("/confirm") - public ResponseEntity confirmPayment(HttpSession session, @RequestBody PaymentRequest.Confirm confirmRequest, @RequestParam Long userId) throws IOException, InterruptedException { - validateSession(session); - validatePaymentConfirmRequest(confirmRequest, session); + public ResponseEntity confirmPayment(@RequestBody PaymentRequest.Confirm confirmRequest, @RequestParam Long userId) throws IOException, InterruptedException { + validatePaymentConfirmRequestV2(confirmRequest); Response response = paymentService.sendPaymentRequest(confirmRequest); //토스페이먼츠로 보낸 결제 승인 요청에 대한 response 리턴 @@ -49,28 +57,19 @@ public ResponseEntity confirmPayment(HttpSession session, @RequestBody Paymen return ResponseEntity.status(response.getStatusCode()).body(response.getBody()); } + private void validatePaymentConfirmRequestV2(PaymentRequest.Confirm confirmRequest) { + Optional amountStr = getAmountStr(confirmRequest.getOrderId()); - private void validatePaymentConfirmRequest(PaymentRequest.Confirm confirmRequest, HttpSession session) { - String savedOrderId = (String) session.getAttribute("orderId"); - BigDecimal savedAmount = (BigDecimal) session.getAttribute("amount"); + BigDecimal savedAmount = amountStr + .map(BigDecimal::new) + .orElseThrow(() -> new RedisKeyNotFoundException("Redis key 'orderId' not found.")); - if (!confirmRequest.getOrderId().equals(savedOrderId) || !confirmRequest.getAmount().equals(savedAmount)) { - throw new PaymentDataMismatchException("Payment data mismatch"); + if (!confirmRequest.getAmount().equals(savedAmount)) { + throw new PaymentDataMismatchException("Payment data mismatch : Amount does not match the previous value"); } } - private void validateSession(HttpSession session) { - if (session == null) { - throw new InvalidSessionException("Invalid session"); - } - - String savedOrderId = (String) session.getAttribute("orderId"); - BigDecimal savedAmount = (BigDecimal) session.getAttribute("amount"); - - if (savedOrderId == null || savedAmount == null) { - throw new InvalidSessionException("Invalid session"); - } + private Optional getAmountStr(String orderId) { + return Optional.ofNullable(redisService.getValue(orderId)); } - - } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7f036cf3..5cc2d14f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,8 +35,14 @@ spring: cache: false profiles: include: - -jwt - -payment + - jwt + - payment + data: + redis: + host: localhost + port: 6379 + password: + timeout: 5000 springdoc: api-docs: diff --git a/src/test/java/com/tasksprints/auction/payment/application/PaymentServiceImplTest.java b/src/test/java/com/tasksprints/auction/payment/application/PaymentServiceImplTest.java index 7151f42f..5ca540ae 100644 --- a/src/test/java/com/tasksprints/auction/payment/application/PaymentServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/payment/application/PaymentServiceImplTest.java @@ -43,15 +43,11 @@ public class PaymentServiceImplTest { @Mock private PaymentRepository paymentRepository; - private MockHttpSession session; - private User user; private Wallet wallet; @BeforeEach void setUp() { - session = new MockHttpSession(); - wallet = Wallet.builder() .id(1L) .balance(BigDecimal.ZERO) @@ -70,25 +66,6 @@ void setUp() { } - @Nested - @DisplayName("결제 전 세션 임시 저장 테스트") - class 임시_저장_테스트 { - @Test - void 결제_요청을_받았을_때_세션에_값이_저장되면_성공한다() { - //given - String orderId = "testOrderId"; - BigDecimal amount = BigDecimal.valueOf(1000.00); - PaymentRequest.Prepare prepareRequest = new PaymentRequest.Prepare(orderId, amount); - - //when - paymentService.prepare(session, prepareRequest); - //then - assertThat(session.getAttribute("orderId")).isEqualTo(orderId); - assertThat(session.getAttribute("amount")).isEqualTo(amount); - } - - } - @Nested @DisplayName("토스_페이_응답_처리") class handleTossPayResponse { diff --git a/src/test/java/com/tasksprints/auction/payment/infrastructure/redis/RedisServiceTest.java b/src/test/java/com/tasksprints/auction/payment/infrastructure/redis/RedisServiceTest.java new file mode 100644 index 00000000..18493b42 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/payment/infrastructure/redis/RedisServiceTest.java @@ -0,0 +1,23 @@ +package com.tasksprints.auction.payment.infrastructure.redis; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +@ExtendWith(MockitoExtension.class) +class RedisServiceTest { + @InjectMocks + private RedisService redisService; + @Mock + private StringRedisTemplate redisTemplate; + @Mock + private ValueOperations valueOperations; + + /** + * Redis 자체를 테스트하는 것은 무의미한 것 같다.. + */ + +} diff --git a/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java b/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java index fa47274b..43bad8d8 100644 --- a/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java +++ b/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java @@ -8,6 +8,8 @@ import com.tasksprints.auction.payment.exception.InvalidSessionException; import com.tasksprints.auction.payment.exception.PaymentDataMismatchException; import com.tasksprints.auction.payment.application.service.PaymentService; +import com.tasksprints.auction.payment.exception.RedisKeyNotFoundException; +import com.tasksprints.auction.payment.infrastructure.redis.RedisService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -21,10 +23,13 @@ import org.springframework.test.web.servlet.MockMvc; import java.math.BigDecimal; +import java.util.concurrent.TimeUnit; +import static com.tasksprints.auction.common.constant.ApiResponseMessages.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -32,20 +37,16 @@ @WebMvcTest(PaymentController.class) @MockBean(JpaMetamodelMappingContext.class) - public class PaymentControllerTest extends BaseControllerTest { @Autowired private MockMvc mockMvc; - @MockBean private PaymentService paymentService; - @MockBean - MockHttpSession session; + private RedisService redisService; @BeforeEach void setup() { - session = new MockHttpSession(); } @Test @@ -53,75 +54,71 @@ void setup() { public void 결제_전_임시_값_저장() throws Exception { String jsonRequest = """ { - "orderId": "test1", + "orderId": "orderId", "amount": 1000.00 } """; mockMvc.perform(post("/api/v1/payment/prepare") - .session(session) .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(ApiResponseMessages.PAYMENT_PREPARED_SUCCESS)); + .andExpect(jsonPath("$.message").value(PAYMENT_PREPARED_SUCCESS)); + verify(redisService).saveDataWithTimeOut( + eq("orderId"), + eq("1000.00"), + eq(300L) + ); } @Nested - class sessionTest { - - + class RedisTest { @Test - void 결제_전_세션_값이_null인_경우_예외가_발생한다() throws Exception { + void 결제_전_Redis_key_value값이_null인_경우_예외가_발생한다() throws Exception { // Given String jsonRequest = """ { - "orderId": "12345", + "orderId": "orderId", "amount": 10000 } """; + when(redisService.getValue(any(String.class))).thenReturn(null); // When & Then mockMvc.perform(post("/api/v1/payment/confirm") - .session(session) .param("userId", "1") .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isBadRequest()) - .andExpect(result -> { - Exception resolvedException = result.getResolvedException(); - assertNotNull(resolvedException); - assertInstanceOf(InvalidSessionException.class, resolvedException); - }); + .andExpect(result -> assertThat(result.getResolvedException()) + .isInstanceOf(RedisKeyNotFoundException.class) + .hasMessage("Redis key 'orderId' not found.")); + } @Test - void 결제_전_세션_OrderId와_Request의_OrderId가_다른_경우_예외가_발생한다() throws Exception { + @DisplayName("결제 전 Redis에 저장된 amount와 결제 요청 전 request의 amount가 다르면 예외가 발생한다") + void 결제_amount가_결제_과정중_변경되면_예외가_발생한다() throws Exception { // Given String jsonRequest = """ { - "orderId": "12345", + "orderId": "orderId", "amount": 10000 } """; - MockHttpSession session = new MockHttpSession(); - session.setAttribute("orderId", "changed-OrderId"); - session.setAttribute("amount", BigDecimal.valueOf(10000)); // + when(redisService.getValue(any(String.class))).thenReturn("99999"); //changed-amount // When & Then mockMvc.perform(post("/api/v1/payment/confirm") - .session(session) .param("userId", "1") .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isBadRequest()) - .andExpect(result -> { - Exception resolvedException = result.getResolvedException(); - assertNotNull(resolvedException); - assertInstanceOf(PaymentDataMismatchException.class, resolvedException); - assertEquals("Payment data mismatch", resolvedException.getMessage()); + .andExpect(result -> assertThat(result.getResolvedException()) + .isInstanceOf(PaymentDataMismatchException.class) + .hasMessage("Payment data mismatch : Amount does not match the previous value")); - }); } } @@ -131,50 +128,42 @@ class sessionTest { // Given String jsonRequest = """ { - "orderId": "12345", + "orderId": "orderId", "amount": 10000 } """; - MockHttpSession session = new MockHttpSession(); - session.setAttribute("orderId", "12345"); - session.setAttribute("amount", BigDecimal.valueOf(10000)); - - PaymentResponse successPaymentResponse = new PaymentResponse("CARD", "paymentKey", BigDecimal.valueOf(10000), "Test Order", "12345", "DONE"); + PaymentResponse successPaymentResponse = new PaymentResponse("CARD", "paymentKey", BigDecimal.valueOf(10000), "Test Order", "orderId", "DONE"); Response mockResponse = Response.success(200, successPaymentResponse); + when(redisService.getValue("orderId")).thenReturn("10000"); when(paymentService.sendPaymentRequest(any())).thenReturn(mockResponse); when(paymentService.handleTossPaymentResponse(anyLong(), any(), any())) .thenReturn(mockResponse); // When / Then mockMvc.perform(post("/api/v1/payment/confirm") - .session(session) .param("userId", "1") .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.message").value("결제가 성공적으로 처리되었습니다.")) - .andExpect(jsonPath("$.data.orderId").value("12345")) + .andExpect(jsonPath("$.data.orderId").value("orderId")) .andExpect(jsonPath("$.data.totalAmount").value(10000)); } @Test - @DisplayName("결제 승인 성공 시 HTTP 400 응답을 반환한다") + @DisplayName("결제 승인 실패 시 HTTP 400 응답을 반환한다") void 결제_승인_실패_시_응답() throws Exception { // Given String jsonRequest = """ { - "orderId": "12345", + "orderId": "orderId", "amount": 10000 } """; - MockHttpSession session = new MockHttpSession(); - session.setAttribute("orderId", "12345"); - session.setAttribute("amount", BigDecimal.valueOf(10000)); - PaymentErrorResponse failurePaymentResponse = PaymentErrorResponse.builder() .version("2022-11-16") .traceId("{traceId}") @@ -183,13 +172,13 @@ class sessionTest { .build(); Response mockResponse = Response.failure(400, failurePaymentResponse); + when(redisService.getValue("orderId")).thenReturn("10000"); when(paymentService.sendPaymentRequest(any())).thenReturn(mockResponse); when(paymentService.handleTossPaymentResponse(anyLong(), any(), any())) .thenReturn(mockResponse); // When / Then mockMvc.perform(post("/api/v1/payment/confirm") - .session(session) .param("userId", "1") .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) From 4d8b566a04d70ecb206ae1123d988e6d5d23ac38 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 31 Dec 2024 00:51:27 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat=20:=20Auction=20=EC=8B=9C=EC=9E=91,?= =?UTF-8?q?=20=EC=A2=85=EB=A3=8C=20=EC=8B=9C=EA=B0=81=EC=9D=B4=20=EB=90=98?= =?UTF-8?q?=EB=A9=B4=20=EA=B2=BD=EB=A7=A4=20=EC=83=81=ED=83=9C=EA=B0=80=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EB=90=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추가한 내용 : Job Runr 라이브러리를 통해 경매 스케쥴링을 구현 1) 경매 시작 시각이 되면 경매 Status가 변경됨 - PENDING -> ACTIVE 2) 경매 종료 시각이 되면 경매 Status가 변경 - ACTIVE -> CLOSE 미완 : 낙찰 시 로직 구현? --- build.gradle | 3 + .../service/AuctionScheduleService.java | 55 +++++++++++++++++++ .../service/AuctionServiceImpl.java | 7 ++- .../auction/domain/entity/Auction.java | 24 ++++++++ .../InvalidAuctionStateException.java | 7 +++ .../initializer/AuctionInitializer.java | 22 ++++++-- .../auction/common/jobrunr/AuctionJob.java | 30 ++++++++++ src/main/resources/application.yml | 14 +++++ .../application/AuctionServiceImplTest.java | 3 + 9 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/auction/application/service/AuctionScheduleService.java create mode 100644 src/main/java/com/tasksprints/auction/auction/exception/InvalidAuctionStateException.java create mode 100644 src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java diff --git a/build.gradle b/build.gradle index d72e6a28..644db963 100644 --- a/build.gradle +++ b/build.gradle @@ -77,6 +77,9 @@ dependencies { //Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.apache.commons:commons-pool2' + + //Job Runr + implementation 'org.jobrunr:jobrunr-spring-boot-3-starter:7.3.2' } tasks.named('test') { useJUnitPlatform() diff --git a/src/main/java/com/tasksprints/auction/auction/application/service/AuctionScheduleService.java b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionScheduleService.java new file mode 100644 index 00000000..f0ce398e --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionScheduleService.java @@ -0,0 +1,55 @@ +package com.tasksprints.auction.auction.application.service; + +import com.tasksprints.auction.common.jobrunr.AuctionJob; +import lombok.RequiredArgsConstructor; +import org.jobrunr.scheduling.JobScheduler; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@RequiredArgsConstructor +public class AuctionScheduleService { + private final JobScheduler jobScheduler; + private final AuctionJob auctionJob; + + // 작업 ID 관리 (수정/삭제 지원) + private final Map startJobMap = new ConcurrentHashMap<>(); + private final Map endJobMap = new ConcurrentHashMap<>(); + + // 시작 시각 작업 스케줄 + public void scheduleStart(Long auctionId, LocalDateTime startTime) { + if (startJobMap.containsKey(auctionId)) { + UUID jobId = startJobMap.get(auctionId); + jobScheduler.delete(jobId); // 기존 작업 삭제 + } + UUID jobId = jobScheduler.schedule( + Instant.from(startTime.atZone(ZoneId.systemDefault())), + () -> auctionJob.startAuction(auctionId) + ).asUUID(); + + startJobMap.put(auctionId, jobId); + System.out.println("경매 시작 작업 등록 완료 - ID: " + auctionId + ", 시간: " + startTime); + } + + // 종료 시각 작업 스케줄 + public void scheduleEnd(Long auctionId, LocalDateTime endTime) { + + if (endJobMap.containsKey(auctionId)) { + UUID jobId = endJobMap.get(auctionId); + jobScheduler.delete(jobId); // 기존 작업 삭제 + } + UUID jobId = jobScheduler.schedule( + Instant.from(endTime.atZone(ZoneId.systemDefault())), + () -> auctionJob.endAuction(auctionId) + ).asUUID(); + + endJobMap.put(auctionId, jobId); + System.out.println("경매 종료 작업 등록 완료 - ID: " + auctionId + ", 시간: " + endTime); + } +} diff --git a/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java index 91aa4376..380d6a5a 100644 --- a/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/auction/application/service/AuctionServiceImpl.java @@ -26,7 +26,7 @@ public class AuctionServiceImpl implements AuctionService { private final UserRepository userRepository; private final AuctionRepository auctionRepository; - + private final AuctionScheduleService schedulerService; @Override public AuctionResponse createAuction(Long userId, AuctionRequest.Create auctionRequest) { User seller = userRepository.findById(userId) @@ -53,6 +53,10 @@ public AuctionResponse createAuction(Long userId, AuctionRequest.Create auctionR * STEP 2 * - 각각의 기능을 완전 분리 */ + // 시작 및 종료 시간 스케줄 등록 + schedulerService.scheduleStart(savedAuction.getId(), savedAuction.getStartTime()); + schedulerService.scheduleEnd(savedAuction.getId(), savedAuction.getEndTime()); + return AuctionResponse.of(savedAuction); } @@ -100,7 +104,6 @@ public List getAllAuctions() { public AuctionResponse getAuctionById(Long auctionId) { Auction foundAuction = auctionRepository.findAuctionById(auctionId) .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); - auctionRepository.save(foundAuction); return AuctionResponse.of(foundAuction); } diff --git a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java index 53ff8406..4b46df10 100644 --- a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java +++ b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java @@ -1,5 +1,6 @@ package com.tasksprints.auction.auction.domain.entity; +import com.tasksprints.auction.auction.exception.InvalidAuctionStateException; import com.tasksprints.auction.bid.domain.entity.Bid; import com.tasksprints.auction.common.entity.BaseEntity; import com.tasksprints.auction.product.domain.entity.Product; @@ -77,4 +78,27 @@ public void addUser(User seller) { } + public void activate() { + canActivate(); + this.auctionStatus = AuctionStatus.ACTIVE; + } + + public void close() { + canClose(); + this.auctionStatus = AuctionStatus.CLOSED; + } + + private void canActivate() { + if (!auctionStatus.equals(AuctionStatus.PENDING)) { + throw new InvalidAuctionStateException("Auction state change error: Can only start an auction from PENDING state."); + } + } + + private void canClose() { + if (!auctionStatus.equals(AuctionStatus.ACTIVE)) { + throw new InvalidAuctionStateException("Auction state change error: Can only close an auction from ACTIVE state."); + } + } + + } diff --git a/src/main/java/com/tasksprints/auction/auction/exception/InvalidAuctionStateException.java b/src/main/java/com/tasksprints/auction/auction/exception/InvalidAuctionStateException.java new file mode 100644 index 00000000..7c57f9fc --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/exception/InvalidAuctionStateException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.auction.exception; + +public class InvalidAuctionStateException extends RuntimeException { + public InvalidAuctionStateException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java index 770eb9c1..9a596ea3 100644 --- a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java +++ b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java @@ -1,5 +1,6 @@ package com.tasksprints.auction.common.initializer; +import com.tasksprints.auction.auction.application.service.AuctionScheduleService; import com.tasksprints.auction.auction.domain.entity.Auction; import com.tasksprints.auction.auction.domain.entity.AuctionCategory; import com.tasksprints.auction.auction.domain.entity.AuctionStatus; @@ -27,12 +28,14 @@ public class AuctionInitializer implements ApplicationRunner { private final ProductRepository productRepository; private final ProductImageRepository productImageRepository; + private final AuctionScheduleService auctionScheduleService; // 스케줄 서비스 추가 - public AuctionInitializer(UserRepository userRepository, AuctionRepository auctionRepository, ProductRepository productRepository, ProductImageRepository productImageRepository) { + public AuctionInitializer(UserRepository userRepository, AuctionRepository auctionRepository, ProductRepository productRepository, ProductImageRepository productImageRepository, AuctionScheduleService auctionScheduleService) { this.userRepository = userRepository; this.auctionRepository = auctionRepository; this.productRepository = productRepository; this.productImageRepository = productImageRepository; + this.auctionScheduleService = auctionScheduleService; } private void createDummyUser() { @@ -40,8 +43,8 @@ private void createDummyUser() { userRepository.save(user1); } - private Auction createDummyAuction(User user) { - Auction auction = Auction.create(LocalDateTime.now(), LocalDateTime.now().plusHours(2), BigDecimal.TEN, AuctionCategory.PRIVATE_FREE, AuctionStatus.ACTIVE, user); + private Auction createDummyAuction(User user, LocalDateTime startTime, LocalDateTime endTime) { + Auction auction = Auction.create(startTime, endTime, BigDecimal.TEN, AuctionCategory.PRIVATE_FREE, AuctionStatus.PENDING, user); return auctionRepository.save(auction); } @@ -58,11 +61,18 @@ private void createDummyProduct(User user, Auction auction) { @Transactional public void run(ApplicationArguments args) throws Exception { User user = userRepository.save(User.createWithWallet("name", "email@email.com", "password", "NickName")); - + LocalDateTime now = LocalDateTime.now(); // 각 제품에 대해 새로운 경매를 생성 - for (int i = 0; i < 100; i++) { - Auction auction = createDummyAuction(user); + for (int i = 0; i < 50; i++) { + Thread.sleep(100); + LocalDateTime startTime = now.plusSeconds(i * 2); // 시작 시간: 2초 간격 + LocalDateTime endTime = startTime.plusSeconds(15); // 종료 시간: 시작 후 15초 + + Auction auction = createDummyAuction(user, startTime, endTime); createDummyProduct(user, auction); + + auctionScheduleService.scheduleStart(auction.getId(), startTime); + auctionScheduleService.scheduleEnd(auction.getId(), endTime); } } } diff --git a/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java b/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java new file mode 100644 index 00000000..76a507a3 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java @@ -0,0 +1,30 @@ +package com.tasksprints.auction.common.jobrunr; + +import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.exception.AuctionNotFoundException; +import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class AuctionJob { + private final AuctionRepository auctionRepository; + + // 경매 시작 상태 변경 + @Transactional + public void startAuction(Long auctionId) { + Auction auction = auctionRepository.findById(auctionId).orElseThrow(() -> new AuctionNotFoundException("Not Found Auction ID: " + auctionId)); + auction.activate(); + System.out.println("경매 시작 - ID: " + auctionId); + } + + // 경매 종료 상태 변경 + @Transactional + public void endAuction(Long auctionId) { + Auction auction = auctionRepository.findById(auctionId).orElseThrow(() -> new AuctionNotFoundException("Not Found Auction ID: " + auctionId)); + auction.close(); + System.out.println("경매 종료 - ID: " + auctionId); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5cc2d14f..d9e646fd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -62,3 +62,17 @@ server: port: 8080 +org : + jobrunr: + job-scheduler: + enabled: true # 작업 스케줄러 활성화 + background-job-server: + enabled: true # 백그라운드 서버 활성화 + worker-count: 2 # 스레드 풀 수 + dashboard: + enabled: true # 대시보드 활성화 + port: 8000 # 대시보드 포트 (http://localhost:8000) + database: + table-prefix: JOBRUNR_ # JobRunr 테이블 접두사 + + diff --git a/src/test/java/com/tasksprints/auction/auction/application/AuctionServiceImplTest.java b/src/test/java/com/tasksprints/auction/auction/application/AuctionServiceImplTest.java index 4c190814..0a1b80ea 100644 --- a/src/test/java/com/tasksprints/auction/auction/application/AuctionServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/auction/application/AuctionServiceImplTest.java @@ -1,5 +1,6 @@ package com.tasksprints.auction.domain.auction.service; +import com.tasksprints.auction.auction.application.service.AuctionScheduleService; import com.tasksprints.auction.auction.domain.dto.request.AuctionRequest; import com.tasksprints.auction.auction.domain.dto.response.AuctionResponse; import com.tasksprints.auction.auction.exception.AuctionAlreadyClosedException; @@ -48,6 +49,8 @@ class AuctionServiceImplTest { @InjectMocks private AuctionServiceImpl auctionService; + @Mock + private AuctionScheduleService scheduleService; private User seller; @BeforeEach From 19d910fd66c8c9100cf874e660ed034b7276c497 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 31 Dec 2024 02:05:35 +0900 Subject: [PATCH 08/20] =?UTF-8?q?refactor=20:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C=20JWT=20=ED=86=A0=ED=81=B0=EC=97=90=EC=84=9C=20userId?= =?UTF-8?q?=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정한 내용 : 1) 결제 시 쿼리 스트링으로 userId를 전달하던 부분에서 JWT 토큰에서 userId를 추출하는 방식으로 리팩터링 2) resolver에서 refresh token의 subject를 가져오던 것을 access token으로 가져오도록 수정 --- .../application/resolver/AuthenticationResolver.java | 2 +- .../payment/presentation/PaymentController.java | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/auth/application/resolver/AuthenticationResolver.java b/src/main/java/com/tasksprints/auction/auth/application/resolver/AuthenticationResolver.java index efe5a249..6a5dcddb 100644 --- a/src/main/java/com/tasksprints/auction/auth/application/resolver/AuthenticationResolver.java +++ b/src/main/java/com/tasksprints/auction/auth/application/resolver/AuthenticationResolver.java @@ -45,7 +45,7 @@ public Object resolveArgument(MethodParameter parameter, jwtProvider.validateToken(accessToken); jwtProvider.validateToken(refreshToken); - Long userId = Long.valueOf(jwtProvider.getSubject(refreshToken)); + Long userId = Long.valueOf(jwtProvider.getSubject(accessToken)); return Accessor.user(userId); } catch (RefreshTokenException e) { return Accessor.guest(); diff --git a/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java b/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java index b55fdc55..2956c662 100644 --- a/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java +++ b/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java @@ -1,6 +1,9 @@ package com.tasksprints.auction.payment.presentation; +import com.tasksprints.auction.auth.domain.model.Accessor; import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.common.jwt.Auth; +import com.tasksprints.auction.common.jwt.UserOnly; import com.tasksprints.auction.common.response.ApiResult; import com.tasksprints.auction.payment.api.Response; import com.tasksprints.auction.payment.domain.dto.request.PaymentRequest; @@ -43,9 +46,12 @@ public ResponseEntity> preparePayment(@RequestBody PaymentRequ } @PostMapping("/confirm") - public ResponseEntity confirmPayment(@RequestBody PaymentRequest.Confirm confirmRequest, @RequestParam Long userId) throws IOException, InterruptedException { - validatePaymentConfirmRequestV2(confirmRequest); + @UserOnly + public ResponseEntity confirmPayment(@RequestBody PaymentRequest.Confirm confirmRequest, @Auth Accessor accessor) throws IOException, InterruptedException { + Long userId = accessor.userId(); + System.out.println("유저:"+userId); + validatePaymentConfirmRequestV2(confirmRequest); Response response = paymentService.sendPaymentRequest(confirmRequest); //토스페이먼츠로 보낸 결제 승인 요청에 대한 response 리턴 Response objectResponse = paymentService.handleTossPaymentResponse(userId, confirmRequest, response); From 88a956afda772c5b4ec186d54b896e2e88ffe094 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:01:15 +0900 Subject: [PATCH 09/20] =?UTF-8?q?fix=20:=20PaymentController=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D/=EC=9D=B8=EA=B0=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정한 내용 : 1)결제 컨트롤러에서 인증/인가 관련 테스트 오류 수정 --- .../presentation/PaymentControllerTest.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java b/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java index 43bad8d8..786be586 100644 --- a/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java +++ b/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java @@ -1,13 +1,13 @@ package com.tasksprints.auction.payment.presentation; import com.tasksprints.auction.BaseControllerTest; -import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.auth.application.resolver.AuthenticationResolver; +import com.tasksprints.auction.auth.domain.model.Accessor; import com.tasksprints.auction.payment.api.Response; +import com.tasksprints.auction.payment.application.service.PaymentService; import com.tasksprints.auction.payment.domain.dto.response.PaymentErrorResponse; import com.tasksprints.auction.payment.domain.dto.response.PaymentResponse; -import com.tasksprints.auction.payment.exception.InvalidSessionException; import com.tasksprints.auction.payment.exception.PaymentDataMismatchException; -import com.tasksprints.auction.payment.application.service.PaymentService; import com.tasksprints.auction.payment.exception.RedisKeyNotFoundException; import com.tasksprints.auction.payment.infrastructure.redis.RedisService; import org.junit.jupiter.api.BeforeEach; @@ -19,15 +19,12 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; import java.math.BigDecimal; -import java.util.concurrent.TimeUnit; -import static com.tasksprints.auction.common.constant.ApiResponseMessages.*; +import static com.tasksprints.auction.common.constant.ApiResponseMessages.PAYMENT_PREPARED_SUCCESS; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,8 +42,15 @@ public class PaymentControllerTest extends BaseControllerTest { @MockBean private RedisService redisService; + @MockBean + private AuthenticationResolver authResolver; + private Accessor accessor; + @BeforeEach - void setup() { + void setup() throws Exception { + accessor = Accessor.user(1L); + when(authResolver.supportsParameter(any())).thenReturn(true); + when(authResolver.resolveArgument(any(),any(),any(),any())).thenReturn(accessor); } @Test @@ -87,7 +91,6 @@ class RedisTest { // When & Then mockMvc.perform(post("/api/v1/payment/confirm") - .param("userId", "1") .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isBadRequest()) @@ -111,7 +114,6 @@ class RedisTest { // When & Then mockMvc.perform(post("/api/v1/payment/confirm") - .param("userId", "1") .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isBadRequest()) @@ -143,7 +145,6 @@ class RedisTest { // When / Then mockMvc.perform(post("/api/v1/payment/confirm") - .param("userId", "1") .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isOk()) @@ -179,7 +180,6 @@ class RedisTest { // When / Then mockMvc.perform(post("/api/v1/payment/confirm") - .param("userId", "1") .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isBadRequest()) From d1c204f0ac84c715d6db8f41e06bd152d88bf091 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:31:15 +0900 Subject: [PATCH 10/20] =?UTF-8?q?refactor=20:=20=EC=B5=9C=EA=B3=A0=20?= =?UTF-8?q?=EC=9E=85=EC=B0=B0=20=EC=8B=9C,=20Auction=EC=9D=98=20highestBid?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=B1=EC=8B=A0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정한 내용 : 1) auction의 highestBidAmount 필드, highestBidderId 필드 추가 - 관련 테스트 수정 - 입찰 갱신 시 양방향 참조 맺어줌 2) auction과 bid간 mappedBy 명시하지 않아서 조인 테이블 방식이던 걸, mappedBy 명시해서 외래키 방식으로 교체 -> 성능상 조인 테이블은 매우 안좋음 --- .../auction/auction/domain/entity/Auction.java | 18 ++++++++++++++++-- .../application/service/BidServiceImpl.java | 11 +++++++++++ .../auction/bid/domain/entity/Bid.java | 2 ++ .../infrastructure/ProductRepositoryTest.java | 2 ++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java index 4b46df10..53dea6ef 100644 --- a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java +++ b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java @@ -42,6 +42,12 @@ public class Auction extends BaseEntity { @Column(nullable = false) private BigDecimal startingBid; + @Builder.Default + @Column(nullable = false) + private BigDecimal highestBidAmount = BigDecimal.ZERO; + + private Long highestBidderId; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @ToString.Exclude @@ -51,7 +57,7 @@ public class Auction extends BaseEntity { @Builder.Default private Product product = null; - @OneToMany(fetch = FetchType.LAZY) + @OneToMany(mappedBy = "auction", fetch = FetchType.LAZY) @Builder.Default private List bids = new ArrayList<>(); @@ -62,6 +68,7 @@ public static Auction create(LocalDateTime startTime, LocalDateTime endTime, Big .startingBid(startingBid) .auctionCategory(auctionCategory) .auctionStatus(auctionStatus) + .highestBidAmount(BigDecimal.ZERO) .build(); newAuction.addUser(seller); return newAuction; @@ -77,6 +84,14 @@ public void addUser(User seller) { this.seller = seller; } + public void addBid(Bid bid) { + this.bids.add(bid); + } + + public void updateHighestBid(Long userId, BigDecimal amount) { + this.highestBidderId = userId; + this.highestBidAmount = amount; + } public void activate() { canActivate(); @@ -100,5 +115,4 @@ private void canClose() { } } - } diff --git a/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java index 8a3906c3..9c964c05 100644 --- a/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java @@ -52,6 +52,17 @@ public BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount) { // 입찰 생성 및 저장 Bid createdBid = Bid.create(amount, foundUser, foundAuction); Bid savedBid = bidRepository.save(createdBid); + + /** + * 추가 로직 + * - auction의 highest Bid인지 검증하고, highest bid라면 update한다. + * 연관 관계 편의 메서드로 양방향 참조 맺어줌 -> DB와 객체간 일관성을 위함 + */ + if (amount.compareTo(foundAuction.getHighestBidAmount()) < 0 ) { + throw new InvalidBidAmountException("The bid amount is greater than the current highest bid in the auction."); + } + foundAuction.updateHighestBid(userId, amount); + return BidResponse.of(savedBid); } diff --git a/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java b/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java index e011318e..19873152 100644 --- a/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java +++ b/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java @@ -54,7 +54,9 @@ public void addUser(User user) { } public void addAuction(Auction auction) { + // 양방향 참조 맺어줌 this.auction = auction; + auction.addBid(this); } public void addUserAndAuction(User user, Auction auction) { diff --git a/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java b/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java index a7067fdf..e4241298 100644 --- a/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java +++ b/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java @@ -178,6 +178,7 @@ private Auction createAuction(User owner, BigDecimal startingBid, LocalDateTime .auctionStatus(AuctionStatus.ACTIVE) .auctionCategory(AuctionCategory.PRIVATE_FREE) .seller(owner) + .highestBidAmount(BigDecimal.ZERO) .build(); return auctionRepository.save(auction); } @@ -190,6 +191,7 @@ private Auction createAuction(User owner, AuctionStatus auctionStatus) { .auctionStatus(auctionStatus) .auctionCategory(AuctionCategory.PRIVATE_FREE) .seller(owner) + .highestBidAmount(BigDecimal.ZERO) .build(); return auctionRepository.save(auction); } From 109d19cc0603de2f937436264c02168b4a79f76e Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Tue, 31 Dec 2024 19:25:00 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat=20:=20=EC=9E=85=EC=B0=B0=20=EC=8B=9C?= =?UTF-8?q?=20=EC=A7=80=EA=B0=91=20=EC=9E=94=EC=95=A1=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추가한 내용 : 1) 입찰 시 지갑 잔액 검증 -> User 조회 시 Wallet까지 fetch join -> n+1 쿼리 방지 2) 경매 종료 검증 로직 변경 -> time을 기준으로 하다가 auctionStatus를 기준으로 하도록 변경 --- .../bid/application/service/BidServiceImpl.java | 16 +++++++++++++++- .../user/infrastructure/UserRepository.java | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java index 9c964c05..663a549a 100644 --- a/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java @@ -1,5 +1,7 @@ package com.tasksprints.auction.bid.application.service; +import com.tasksprints.auction.auction.domain.entity.AuctionStatus; +import com.tasksprints.auction.auction.exception.InvalidAuctionStateException; import com.tasksprints.auction.bid.application.service.BidService; import com.tasksprints.auction.bid.exception.BidNotFoundException; import com.tasksprints.auction.bid.exception.InvalidBidAmountException; @@ -13,6 +15,8 @@ import com.tasksprints.auction.user.exception.UserNotFoundException; import com.tasksprints.auction.user.domain.entity.User; import com.tasksprints.auction.user.infrastructure.UserRepository; +import com.tasksprints.auction.wallet.domain.entity.Wallet; +import com.tasksprints.auction.wallet.exception.InSufficientBalanceException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -34,8 +38,9 @@ public class BidServiceImpl implements BidService { @Override public BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount) { // 입찰 시 유효성 검사 - User foundUser = userRepository.findById(userId) + User foundUser = userRepository.findByIdWithWallet(userId) .orElseThrow(() -> new UserNotFoundException("User not found")); + Auction foundAuction = auctionRepository.findById(auctionId) .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); @@ -43,12 +48,21 @@ public BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount) { if (foundAuction.getEndTime().isBefore(LocalDateTime.now())) { throw new AuctionEndedException("This auction has already ended."); } + // 경매가 종료되었는지 확인 + if (!foundAuction.getAuctionStatus().equals(AuctionStatus.ACTIVE)) { + throw new InvalidAuctionStateException("Bids can only be placed if the auction is active."); + } // 최소 입찰 금액 충족 여부 확인 if (amount.compareTo(foundAuction.getStartingBid()) < 0) { throw new InvalidBidAmountException("Bid amount is less than the minimum required bid amount."); } + // 지갑 잔액 검증 + if (foundUser.getWallet().getBalance().compareTo(amount) < 0) { + throw new InSufficientBalanceException("Insufficient Wallet balance"); + } + // 입찰 생성 및 저장 Bid createdBid = Bid.create(amount, foundUser, foundAuction); Bid savedBid = bidRepository.save(createdBid); diff --git a/src/main/java/com/tasksprints/auction/user/infrastructure/UserRepository.java b/src/main/java/com/tasksprints/auction/user/infrastructure/UserRepository.java index 392813b3..78781537 100644 --- a/src/main/java/com/tasksprints/auction/user/infrastructure/UserRepository.java +++ b/src/main/java/com/tasksprints/auction/user/infrastructure/UserRepository.java @@ -2,9 +2,13 @@ import com.tasksprints.auction.user.domain.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + @Query("SELECT u FROM users u JOIN FETCH u.wallet w WHERE u.id = :id") + Optional findByIdWithWallet(@Param("id") Long id); } From 3732f0fe42049edda71b967ba39954f9a04e954e Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Wed, 1 Jan 2025 13:45:06 +0900 Subject: [PATCH 12/20] =?UTF-8?q?refactor=20:=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EC=97=90=EC=84=9C=20=EC=A7=80=EA=B0=91=20=EC=9E=94?= =?UTF-8?q?=EC=95=A1=20=EC=B2=B4=ED=81=AC=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 변경한 내용 : 1) walletService에서 잔액 체크하는 메서드 삭제 -> 도메인 모델로 옮김 --- .../wallet/application/service/WalletService.java | 2 -- .../application/service/WalletServiceImpl.java | 5 ----- .../auction/wallet/domain/entity/Wallet.java | 13 ++++++++++++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/wallet/application/service/WalletService.java b/src/main/java/com/tasksprints/auction/wallet/application/service/WalletService.java index d38364cc..f5ffad1d 100644 --- a/src/main/java/com/tasksprints/auction/wallet/application/service/WalletService.java +++ b/src/main/java/com/tasksprints/auction/wallet/application/service/WalletService.java @@ -7,8 +7,6 @@ public interface WalletService { void chargeMoney(Wallet wallet, BigDecimal amount); - boolean isSufficientMoney(); - Wallet getWalletByUserId(Long userId); } diff --git a/src/main/java/com/tasksprints/auction/wallet/application/service/WalletServiceImpl.java b/src/main/java/com/tasksprints/auction/wallet/application/service/WalletServiceImpl.java index 7a4de7b2..8aea7fb1 100644 --- a/src/main/java/com/tasksprints/auction/wallet/application/service/WalletServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/wallet/application/service/WalletServiceImpl.java @@ -18,11 +18,6 @@ public void chargeMoney(Wallet wallet, BigDecimal amount) { walletRepository.save(wallet); } - @Override - public boolean isSufficientMoney() { - return false; - } - @Override public Wallet getWalletByUserId(Long userId) { return walletRepository.getWalletByUserId(userId); diff --git a/src/main/java/com/tasksprints/auction/wallet/domain/entity/Wallet.java b/src/main/java/com/tasksprints/auction/wallet/domain/entity/Wallet.java index 08a7fc2e..02cb6adf 100644 --- a/src/main/java/com/tasksprints/auction/wallet/domain/entity/Wallet.java +++ b/src/main/java/com/tasksprints/auction/wallet/domain/entity/Wallet.java @@ -3,6 +3,7 @@ import com.tasksprints.auction.common.entity.BaseEntityWithUpdate; import com.tasksprints.auction.payment.domain.entity.Payment; import com.tasksprints.auction.user.domain.entity.User; +import com.tasksprints.auction.wallet.exception.InSufficientBalanceException; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -57,6 +58,16 @@ public void addUser(User user) { } public void chargeBalance(BigDecimal amount) { - this.balance = this.balance.add(amount); + balance = balance.add(amount); + } + + public void deductBalance(BigDecimal amount) { + isSufficientBalance(amount); + balance = balance.subtract(amount); + } + private void isSufficientBalance(BigDecimal amount) { + if (balance.compareTo(amount) < 0) { + throw new InSufficientBalanceException("Insufficient Wallet balance"); + } } } From 8cbcf031998c5a91cf8d8b1ddf36c004948fa91a Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Wed, 1 Jan 2025 14:29:45 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat=20:=20Auction=20=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=20=EC=8B=9C=20closed=5Fat=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추가한 내용 : 1) 조회 되지 않도록 closed_at 생성 --- .../tasksprints/auction/auction/domain/entity/Auction.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java index 53dea6ef..5157bab6 100644 --- a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java +++ b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java @@ -7,6 +7,7 @@ import com.tasksprints.auction.user.domain.entity.User; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.SQLRestriction; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -17,6 +18,7 @@ @NoArgsConstructor @AllArgsConstructor @Getter +@SQLRestriction("closed_at is null") @ToString @Entity(name = "auction") public class Auction extends BaseEntity { @@ -61,6 +63,9 @@ public class Auction extends BaseEntity { @Builder.Default private List bids = new ArrayList<>(); + @Column(nullable = true, name= "closed_at") + private LocalDateTime closedAt; + public static Auction create(LocalDateTime startTime, LocalDateTime endTime, BigDecimal startingBid, AuctionCategory auctionCategory, AuctionStatus auctionStatus, User seller) { Auction newAuction = Auction.builder() .startTime(startTime) @@ -101,6 +106,7 @@ public void activate() { public void close() { canClose(); this.auctionStatus = AuctionStatus.CLOSED; + this.closedAt = LocalDateTime.now(); } private void canActivate() { From e61fba694f240e20e73e66e722719ab1513c7358 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Wed, 1 Jan 2025 14:32:03 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat=20:=20=EB=82=99=EC=B0=B0=20=EC=8B=9C?= =?UTF-8?q?=20=EC=A7=80=EA=B0=91=20=EC=9E=94=EC=95=A1=20=EC=B0=A8=EA=B0=90?= =?UTF-8?q?=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추가한 내용 : 1) 잔액 차감을 이벤트로 처리 -> 경매 종료 시 경매 상태를 바꾸면 트랜잭션이 커밋될 때 업데이트 쿼리가 나가는데, 지갑 잔액 차감 관련 로직까지 수행하게 되면 그 중간에 동시성 문제가 생길 수 있다고 판단함 - 비동기로 처리 - 지갑 잔액을 차감 - 실패 시 로깅 --- .../domain/event/AuctionClosedEvent.java | 14 ++++++ .../event/AuctionClosedEventListener.java | 38 ++++++++++++++++ .../auction/common/config/AsyncConfig.java | 23 ++++++++++ .../initializer/AuctionInitializer.java | 45 ++++++++++++++++--- .../auction/common/jobrunr/AuctionJob.java | 7 +++ src/main/resources/application.yml | 4 +- 6 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/auction/domain/event/AuctionClosedEvent.java create mode 100644 src/main/java/com/tasksprints/auction/auction/domain/event/AuctionClosedEventListener.java create mode 100644 src/main/java/com/tasksprints/auction/common/config/AsyncConfig.java diff --git a/src/main/java/com/tasksprints/auction/auction/domain/event/AuctionClosedEvent.java b/src/main/java/com/tasksprints/auction/auction/domain/event/AuctionClosedEvent.java new file mode 100644 index 00000000..e048d983 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/domain/event/AuctionClosedEvent.java @@ -0,0 +1,14 @@ +package com.tasksprints.auction.auction.domain.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.math.BigDecimal; + +@Getter +@RequiredArgsConstructor +public class AuctionClosedEvent { + private final Long auctionId; + private final Long highestBidderId; + private final BigDecimal highestBidAmount; +} diff --git a/src/main/java/com/tasksprints/auction/auction/domain/event/AuctionClosedEventListener.java b/src/main/java/com/tasksprints/auction/auction/domain/event/AuctionClosedEventListener.java new file mode 100644 index 00000000..ce6f837c --- /dev/null +++ b/src/main/java/com/tasksprints/auction/auction/domain/event/AuctionClosedEventListener.java @@ -0,0 +1,38 @@ +package com.tasksprints.auction.auction.domain.event; + +import com.tasksprints.auction.wallet.domain.entity.Wallet; +import com.tasksprints.auction.wallet.infrastructure.WalletRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuctionClosedEventListener { + private final WalletRepository walletRepository; + + /** + * 비동기로 지갑 잔액 차감 처리 + * 동기로 처리하게 되면 이벤트 처리 할 때까지 경매의 close 상태를 업데이트 커밋하지 않음(트랜잭션 커밋) + * 경매를 닫고(트랜잭션 끝내고), 비동기로 잔액 차감을 처리해도 문제 없다.. + */ + @Async("asyncExecutor") + @EventListener + @Transactional + public void handleAuctionClosed(AuctionClosedEvent event) { + log.info("경매 종료 이벤트 처리: auctionId={}", event.getAuctionId()); + try { + Wallet winnerWallet = walletRepository.getWalletByUserId(event.getHighestBidderId()); + winnerWallet.deductBalance(event.getHighestBidAmount()); + walletRepository.save(winnerWallet); + log.info("지갑 잔액 차감 성공: auctionId={}", event.getAuctionId()); + } catch (Exception e) { + log.error("지갑 잔액 차감 실패: auctionId={}, error={}", event.getAuctionId(), e.getMessage()); + } + } + +} diff --git a/src/main/java/com/tasksprints/auction/common/config/AsyncConfig.java b/src/main/java/com/tasksprints/auction/common/config/AsyncConfig.java new file mode 100644 index 00000000..5269e0ff --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/config/AsyncConfig.java @@ -0,0 +1,23 @@ +package com.tasksprints.auction.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +@EnableAsync +public class AsyncConfig { + @Bean(name = "asyncExecutor") + public Executor asyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(30); + executor.setThreadNamePrefix("Async-"); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java index 9a596ea3..df039865 100644 --- a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java +++ b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java @@ -11,13 +11,16 @@ import com.tasksprints.auction.product.infrastructure.ProductRepository; import com.tasksprints.auction.user.domain.entity.User; import com.tasksprints.auction.user.infrastructure.UserRepository; +import com.tasksprints.auction.wallet.domain.entity.Wallet; import jakarta.transaction.Transactional; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.time.Duration; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @Component @@ -43,8 +46,18 @@ private void createDummyUser() { userRepository.save(user1); } - private Auction createDummyAuction(User user, LocalDateTime startTime, LocalDateTime endTime) { - Auction auction = Auction.create(startTime, endTime, BigDecimal.TEN, AuctionCategory.PRIVATE_FREE, AuctionStatus.PENDING, user); + private Auction createDummyAuction(User user, LocalDateTime startTime, LocalDateTime endTime, Long highestBidderId) { + Auction auction = Auction.builder() + .startTime(startTime) + .endTime(endTime) + .startingBid(BigDecimal.TEN) + .auctionCategory(AuctionCategory.PUBLIC_PAID) + .auctionStatus(AuctionStatus.PENDING) + .highestBidAmount(BigDecimal.valueOf(100)) + .highestBidderId(highestBidderId) + .build(); + auction.addUser(user); + return auctionRepository.save(auction); } @@ -60,19 +73,37 @@ private void createDummyProduct(User user, Auction auction) { @Override @Transactional public void run(ApplicationArguments args) throws Exception { - User user = userRepository.save(User.createWithWallet("name", "email@email.com", "password", "NickName")); - LocalDateTime now = LocalDateTime.now(); + // 각 제품에 대해 새로운 경매를 생성 for (int i = 0; i < 50; i++) { - Thread.sleep(100); - LocalDateTime startTime = now.plusSeconds(i * 2); // 시작 시간: 2초 간격 + LocalDateTime now = LocalDateTime.now(); + LocalDateTime startTime = now.plus(Duration.ofMillis(15000 + i * 500)); // 시작 시간: 15초 텀을 두고 0.5 초 간격 LocalDateTime endTime = startTime.plusSeconds(15); // 종료 시간: 시작 후 15초 - Auction auction = createDummyAuction(user, startTime, endTime); + User user = createUserWithWallet(i); + Auction auction = createDummyAuction(user, startTime, endTime, (long) i + 1); createDummyProduct(user, auction); auctionScheduleService.scheduleStart(auction.getId(), startTime); auctionScheduleService.scheduleEnd(auction.getId(), endTime); } } + + private User createUserWithWallet(int i) { + User user = User.builder() + .name("name" + i) + .email("email" + i + "@email.com") + .password("password") + .nickName("NickName" + i) + .build(); + + Wallet wallet = Wallet.builder() + .user(user) + .userName("name" + i) + .balance(BigDecimal.valueOf(100000.0)) + .build(); + + user.addWallet(wallet); + return userRepository.save(user); + } } diff --git a/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java b/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java index 76a507a3..1299d97f 100644 --- a/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java +++ b/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java @@ -1,16 +1,21 @@ package com.tasksprints.auction.common.jobrunr; import com.tasksprints.auction.auction.domain.entity.Auction; +import com.tasksprints.auction.auction.domain.event.AuctionClosedEvent; import com.tasksprints.auction.auction.exception.AuctionNotFoundException; import com.tasksprints.auction.auction.infrastructure.AuctionRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; + @Component @RequiredArgsConstructor public class AuctionJob { private final AuctionRepository auctionRepository; + private final ApplicationEventPublisher applicationEventPublisher; // 경매 시작 상태 변경 @Transactional @@ -25,6 +30,8 @@ public void startAuction(Long auctionId) { public void endAuction(Long auctionId) { Auction auction = auctionRepository.findById(auctionId).orElseThrow(() -> new AuctionNotFoundException("Not Found Auction ID: " + auctionId)); auction.close(); + //잔액 차감 이벤트 요청 + applicationEventPublisher.publishEvent(new AuctionClosedEvent(auctionId, auction.getHighestBidderId(), auction.getHighestBidAmount())); System.out.println("경매 종료 - ID: " + auctionId); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d9e646fd..5bc9b158 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -55,6 +55,8 @@ springdoc: packages-to-scan: com.tasksprints.auction.api logging: level: + org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor: DEBUG + org.springframework.context.event: DEBUG org.hibernate.SQL: debug org.hibernate.type.descriptor.sql.BasicBinder: INFO root: INFO @@ -68,7 +70,7 @@ org : enabled: true # 작업 스케줄러 활성화 background-job-server: enabled: true # 백그라운드 서버 활성화 - worker-count: 2 # 스레드 풀 수 +# worker-count: 4 # 스레드 풀 수 dashboard: enabled: true # 대시보드 활성화 port: 8000 # 대시보드 포트 (http://localhost:8000) From b10e418cfa66e2aea0e3f605cc9a6f64bf5954d8 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Wed, 1 Jan 2025 21:58:22 +0900 Subject: [PATCH 15/20] =?UTF-8?q?fix=20:=20=EC=9E=85=EC=B0=B0=20=EC=8B=9C?= =?UTF-8?q?=20Auction=20=EC=88=98=EC=A0=95=20=ED=9B=84=20save()=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=ED=95=B4=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정한 내용 : 입찰 시 auction 쪽 highest bid를 갱신할 떄, save()를 하지 않아 update되지 않던 에러 --- .../auction/bid/application/service/BidServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java index 663a549a..991602ad 100644 --- a/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java @@ -76,6 +76,7 @@ public BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount) { throw new InvalidBidAmountException("The bid amount is greater than the current highest bid in the auction."); } foundAuction.updateHighestBid(userId, amount); + auctionRepository.save(foundAuction); return BidResponse.of(savedBid); } From b6c92c52fd7ca196378d68160b91314e5dc126f0 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Thu, 2 Jan 2025 23:53:02 +0900 Subject: [PATCH 16/20] =?UTF-8?q?refactor=20:=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B0=80=EB=8F=85=EC=84=B1=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=ED=8F=AC=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정한 내용 : 가독성을 위해 간단한 수정 --- .../resolver/SearchConditionResolver.java | 13 ++++--------- .../auction/common/jobrunr/AuctionJob.java | 3 ++- .../payment/infrastructure/redis/RedisService.java | 2 +- .../payment/presentation/PaymentController.java | 6 +++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java b/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java index eb75639d..a4bb2629 100644 --- a/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java +++ b/src/main/java/com/tasksprints/auction/auction/application/resolver/SearchConditionResolver.java @@ -49,7 +49,6 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m validateIsStartBeforeEnd(parsedStartTime, parsedEndTime); validateMinLessThanMax(parsedMinPrice, parsedMaxPrice); - // SearchCondition 객체 생성 및 반환 return new AuctionRequest.SearchCondition( parsedAuctionCategory, @@ -64,18 +63,14 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m } private void validateMinLessThanMax(BigDecimal parsedMinPrice, BigDecimal parsedMaxPrice) { - if (parsedMinPrice != null && parsedMaxPrice != null) { - if (parsedMinPrice.compareTo(parsedMaxPrice) > 0) { - throw new IllegalArgumentException("minPrice cannot be greater than maxPrice."); - } + if (parsedMinPrice != null && parsedMaxPrice != null && parsedMinPrice.compareTo(parsedMaxPrice) > 0) { + throw new IllegalArgumentException("minPrice cannot be greater than maxPrice."); } } private void validateIsStartBeforeEnd(LocalDateTime parsedStartTime, LocalDateTime parsedEndTime) { - if (parsedStartTime != null && parsedEndTime != null) { - if (parsedStartTime.isAfter(parsedEndTime)) { - throw new IllegalArgumentException("startTime cannot be after endTime."); - } + if (parsedStartTime != null && parsedEndTime != null && parsedStartTime.isAfter(parsedEndTime)) { + throw new IllegalArgumentException("startTime cannot be after endTime."); } } diff --git a/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java b/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java index 1299d97f..099f5333 100644 --- a/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java +++ b/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java @@ -31,7 +31,8 @@ public void endAuction(Long auctionId) { Auction auction = auctionRepository.findById(auctionId).orElseThrow(() -> new AuctionNotFoundException("Not Found Auction ID: " + auctionId)); auction.close(); //잔액 차감 이벤트 요청 - applicationEventPublisher.publishEvent(new AuctionClosedEvent(auctionId, auction.getHighestBidderId(), auction.getHighestBidAmount())); + AuctionClosedEvent event = new AuctionClosedEvent(auctionId, auction.getHighestBidderId(), auction.getHighestBidAmount()); + applicationEventPublisher.publishEvent(event); System.out.println("경매 종료 - ID: " + auctionId); } } diff --git a/src/main/java/com/tasksprints/auction/payment/infrastructure/redis/RedisService.java b/src/main/java/com/tasksprints/auction/payment/infrastructure/redis/RedisService.java index 70e68467..bb58036b 100644 --- a/src/main/java/com/tasksprints/auction/payment/infrastructure/redis/RedisService.java +++ b/src/main/java/com/tasksprints/auction/payment/infrastructure/redis/RedisService.java @@ -16,7 +16,7 @@ public void saveValue(String key, String value) { redisTemplate.opsForValue().set(key, value); } - public void saveDataWithTimeOut(String key, String value, long timeoutSeconds) { + public void saveDataWithExpiration(String key, String value, long timeoutSeconds) { redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutSeconds)); } diff --git a/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java b/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java index 2956c662..688eaaa6 100644 --- a/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java +++ b/src/main/java/com/tasksprints/auction/payment/presentation/PaymentController.java @@ -37,10 +37,10 @@ public class PaymentController { @Operation(summary = "Temporarily stores the payment element", description = "Save orderID and amount in session") @ApiResponse(responseCode = "200", description = "Payment prepared successfully") public ResponseEntity> preparePayment(@RequestBody PaymentRequest.Prepare prepareRequest) { - redisService.saveDataWithTimeOut( + redisService.saveDataWithExpiration( prepareRequest.getOrderId(), // key prepareRequest.getAmount().toString(), // value - 300 // 5분 TTL + 60 * 5 // 5분 TTL ); return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.PAYMENT_PREPARED_SUCCESS)); } @@ -49,7 +49,7 @@ public ResponseEntity> preparePayment(@RequestBody PaymentRequ @UserOnly public ResponseEntity confirmPayment(@RequestBody PaymentRequest.Confirm confirmRequest, @Auth Accessor accessor) throws IOException, InterruptedException { Long userId = accessor.userId(); - System.out.println("유저:"+userId); + System.out.println("유저:" + userId); validatePaymentConfirmRequestV2(confirmRequest); Response response = paymentService.sendPaymentRequest(confirmRequest); From 4ba03c0c639135e6cd7a6aeb109c6def6d828fa5 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Thu, 2 Jan 2025 23:57:36 +0900 Subject: [PATCH 17/20] =?UTF-8?q?fix=20:=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20=EC=8B=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메서드 변경 후 이전 메서드로 테스트 해서 생긴 테스트 오류 해결 --- .../auction/payment/presentation/PaymentControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java b/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java index 786be586..5afa1564 100644 --- a/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java +++ b/src/test/java/com/tasksprints/auction/payment/presentation/PaymentControllerTest.java @@ -69,7 +69,7 @@ void setup() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value(PAYMENT_PREPARED_SUCCESS)); - verify(redisService).saveDataWithTimeOut( + verify(redisService).saveDataWithExpiration( eq("orderId"), eq("1000.00"), eq(300L) From 4a04ef6906650c734c700a6b35b0e7e43200b0c9 Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:52:11 +0900 Subject: [PATCH 18/20] =?UTF-8?q?delete=20:=20highestBid,=20highestBidderI?= =?UTF-8?q?d=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 삭제한 내용 : 1) auction쪽 highest bid 관련 삭제 bid 로직은 auction당 1개의 bid만 존재 -> 여러 개의 bid가 존재하는 구조가 아니라 1개의 웹소켓 연결된 bid만 추적한다. --- .../auction/auction/domain/entity/Auction.java | 16 ---------------- .../bid/application/service/BidServiceImpl.java | 13 ------------- .../auction/bid/domain/entity/Bid.java | 2 -- .../infrastructure/ProductRepositoryTest.java | 2 -- 4 files changed, 33 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java index 5157bab6..a9a9a4f1 100644 --- a/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java +++ b/src/main/java/com/tasksprints/auction/auction/domain/entity/Auction.java @@ -44,12 +44,6 @@ public class Auction extends BaseEntity { @Column(nullable = false) private BigDecimal startingBid; - @Builder.Default - @Column(nullable = false) - private BigDecimal highestBidAmount = BigDecimal.ZERO; - - private Long highestBidderId; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @ToString.Exclude @@ -73,7 +67,6 @@ public static Auction create(LocalDateTime startTime, LocalDateTime endTime, Big .startingBid(startingBid) .auctionCategory(auctionCategory) .auctionStatus(auctionStatus) - .highestBidAmount(BigDecimal.ZERO) .build(); newAuction.addUser(seller); return newAuction; @@ -89,15 +82,6 @@ public void addUser(User seller) { this.seller = seller; } - public void addBid(Bid bid) { - this.bids.add(bid); - } - - public void updateHighestBid(Long userId, BigDecimal amount) { - this.highestBidderId = userId; - this.highestBidAmount = amount; - } - public void activate() { canActivate(); this.auctionStatus = AuctionStatus.ACTIVE; diff --git a/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java index 991602ad..a5ae3fe2 100644 --- a/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/bid/application/service/BidServiceImpl.java @@ -40,7 +40,6 @@ public BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount) { // 입찰 시 유효성 검사 User foundUser = userRepository.findByIdWithWallet(userId) .orElseThrow(() -> new UserNotFoundException("User not found")); - Auction foundAuction = auctionRepository.findById(auctionId) .orElseThrow(() -> new AuctionNotFoundException("Auction not found")); @@ -66,18 +65,6 @@ public BidResponse submitBid(Long userId, Long auctionId, BigDecimal amount) { // 입찰 생성 및 저장 Bid createdBid = Bid.create(amount, foundUser, foundAuction); Bid savedBid = bidRepository.save(createdBid); - - /** - * 추가 로직 - * - auction의 highest Bid인지 검증하고, highest bid라면 update한다. - * 연관 관계 편의 메서드로 양방향 참조 맺어줌 -> DB와 객체간 일관성을 위함 - */ - if (amount.compareTo(foundAuction.getHighestBidAmount()) < 0 ) { - throw new InvalidBidAmountException("The bid amount is greater than the current highest bid in the auction."); - } - foundAuction.updateHighestBid(userId, amount); - auctionRepository.save(foundAuction); - return BidResponse.of(savedBid); } diff --git a/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java b/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java index 19873152..e011318e 100644 --- a/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java +++ b/src/main/java/com/tasksprints/auction/bid/domain/entity/Bid.java @@ -54,9 +54,7 @@ public void addUser(User user) { } public void addAuction(Auction auction) { - // 양방향 참조 맺어줌 this.auction = auction; - auction.addBid(this); } public void addUserAndAuction(User user, Auction auction) { diff --git a/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java b/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java index e4241298..a7067fdf 100644 --- a/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java +++ b/src/test/java/com/tasksprints/auction/product/infrastructure/ProductRepositoryTest.java @@ -178,7 +178,6 @@ private Auction createAuction(User owner, BigDecimal startingBid, LocalDateTime .auctionStatus(AuctionStatus.ACTIVE) .auctionCategory(AuctionCategory.PRIVATE_FREE) .seller(owner) - .highestBidAmount(BigDecimal.ZERO) .build(); return auctionRepository.save(auction); } @@ -191,7 +190,6 @@ private Auction createAuction(User owner, AuctionStatus auctionStatus) { .auctionStatus(auctionStatus) .auctionCategory(AuctionCategory.PRIVATE_FREE) .seller(owner) - .highestBidAmount(BigDecimal.ZERO) .build(); return auctionRepository.save(auction); } From db378fb598f661678fc61f5294aba66d283a674a Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:54:08 +0900 Subject: [PATCH 19/20] =?UTF-8?q?refactor=20:=20initializer,=20auctionJob?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) initializer에서 bidderId를 삭제한다. -> DummyBid를 추가한다. 2) Auction Job (낙찰 로직) -> 낙찰 로직 수행 시 auction.getBids로 bid를 조회하도록 수정 --- .../initializer/AuctionInitializer.java | 23 +++++++++++++++---- .../auction/common/jobrunr/AuctionJob.java | 4 +++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java index df039865..40fe09c3 100644 --- a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java +++ b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java @@ -5,6 +5,8 @@ import com.tasksprints.auction.auction.domain.entity.AuctionCategory; import com.tasksprints.auction.auction.domain.entity.AuctionStatus; import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.bid.domain.entity.Bid; +import com.tasksprints.auction.bid.infrastructure.BidRepository; import com.tasksprints.auction.product.domain.entity.Product; import com.tasksprints.auction.product.domain.entity.ProductImage; import com.tasksprints.auction.product.infrastructure.ProductImageRepository; @@ -22,6 +24,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.UUID; @Component public class AuctionInitializer implements ApplicationRunner { @@ -32,13 +35,15 @@ public class AuctionInitializer implements ApplicationRunner { private final ProductRepository productRepository; private final ProductImageRepository productImageRepository; private final AuctionScheduleService auctionScheduleService; // 스케줄 서비스 추가 + private final BidRepository bidRepository; - public AuctionInitializer(UserRepository userRepository, AuctionRepository auctionRepository, ProductRepository productRepository, ProductImageRepository productImageRepository, AuctionScheduleService auctionScheduleService) { + public AuctionInitializer(UserRepository userRepository, AuctionRepository auctionRepository, ProductRepository productRepository, ProductImageRepository productImageRepository, AuctionScheduleService auctionScheduleService, BidRepository bidRepository) { this.userRepository = userRepository; this.auctionRepository = auctionRepository; this.productRepository = productRepository; this.productImageRepository = productImageRepository; this.auctionScheduleService = auctionScheduleService; + this.bidRepository = bidRepository; } private void createDummyUser() { @@ -46,15 +51,13 @@ private void createDummyUser() { userRepository.save(user1); } - private Auction createDummyAuction(User user, LocalDateTime startTime, LocalDateTime endTime, Long highestBidderId) { + private Auction createDummyAuction(User user, LocalDateTime startTime, LocalDateTime endTime) { Auction auction = Auction.builder() .startTime(startTime) .endTime(endTime) .startingBid(BigDecimal.TEN) .auctionCategory(AuctionCategory.PUBLIC_PAID) .auctionStatus(AuctionStatus.PENDING) - .highestBidAmount(BigDecimal.valueOf(100)) - .highestBidderId(highestBidderId) .build(); auction.addUser(user); @@ -70,6 +73,15 @@ private void createDummyProduct(User user, Auction auction) { productRepository.save(product); } + private void createDummyBid(Auction auction) { + Bid bid = Bid.builder() + .uuid(UUID.randomUUID().toString()) + .amount(BigDecimal.valueOf(100)) + .auction(auction) + .build(); + bidRepository.save(bid); + } + @Override @Transactional public void run(ApplicationArguments args) throws Exception { @@ -81,8 +93,9 @@ public void run(ApplicationArguments args) throws Exception { LocalDateTime endTime = startTime.plusSeconds(15); // 종료 시간: 시작 후 15초 User user = createUserWithWallet(i); - Auction auction = createDummyAuction(user, startTime, endTime, (long) i + 1); + Auction auction = createDummyAuction(user, startTime, endTime); createDummyProduct(user, auction); + createDummyBid(auction); auctionScheduleService.scheduleStart(auction.getId(), startTime); auctionScheduleService.scheduleEnd(auction.getId(), endTime); diff --git a/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java b/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java index 099f5333..33da7b02 100644 --- a/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java +++ b/src/main/java/com/tasksprints/auction/common/jobrunr/AuctionJob.java @@ -4,6 +4,7 @@ import com.tasksprints.auction.auction.domain.event.AuctionClosedEvent; import com.tasksprints.auction.auction.exception.AuctionNotFoundException; import com.tasksprints.auction.auction.infrastructure.AuctionRepository; +import com.tasksprints.auction.bid.domain.entity.Bid; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; @@ -31,7 +32,8 @@ public void endAuction(Long auctionId) { Auction auction = auctionRepository.findById(auctionId).orElseThrow(() -> new AuctionNotFoundException("Not Found Auction ID: " + auctionId)); auction.close(); //잔액 차감 이벤트 요청 - AuctionClosedEvent event = new AuctionClosedEvent(auctionId, auction.getHighestBidderId(), auction.getHighestBidAmount()); + Bid bid = auction.getBids().getFirst(); + AuctionClosedEvent event = new AuctionClosedEvent(auctionId, bid.getId(), bid.getAmount()); applicationEventPublisher.publishEvent(event); System.out.println("경매 종료 - ID: " + auctionId); } From 842fc9fb87b506b4097ccc82b6e0314d109b6aab Mon Sep 17 00:00:00 2001 From: na0th <105775683+na0th@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:54:36 +0900 Subject: [PATCH 20/20] =?UTF-8?q?fix=20:=20user=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20wallet=20=EC=83=9D=EC=84=B1=20=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8D=98=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) user create 시 wallet도 생성되도록 수정 --- .../auction/user/application/service/UserServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java index 5e56ff26..5acde192 100644 --- a/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/user/application/service/UserServiceImpl.java @@ -20,7 +20,7 @@ public class UserServiceImpl implements UserService { @Override public UserDetailResponse createUser(UserRequest.Register request) { - User user = User.create(request.getName(), request.getEmail(), request.getPassword(), request.getNickname()); + User user = User.createWithWallet(request.getName(), request.getEmail(), request.getPassword(), request.getNickname()); User newUser = userRepository.save(user); return UserDetailResponse.of(newUser);